import { SelectOption } from "../types/select";
import { arrayify } from "./array";

/**
 * Finds the first option, if any, that matches the given value.
 * 
 * @param {OptionType[] | OptionType | undefined} values The values to find
 * @param {SelectOptionType[]} options The options to search in
 * @returns {SelectOptionType | undefined} The first option that matches the given value
 */
export const findValuesInOptions = <
  OptionType extends unknown = string,
  SelectOptionType extends SelectOption<OptionType> = SelectOption<OptionType>,
>(
  values: OptionType[] | OptionType | undefined,
  options: SelectOptionType[],
) => {
  const resolvedValues = arrayify(values);

  // Create a value-option map
  const maybeOptions = resolvedValues.map(value => {
    const option = options.find(option => option.value === value);
    return option;
  });

  return maybeOptions.filter<SelectOptionType>(
    (maybeOption): maybeOption is SelectOptionType => Boolean(maybeOption),
  );
};

/**
 * Convert an array of values to an array of options.
 * 
 * @param {OptionType[] | OptionType | undefined} values The values to convert
 * @returns {SelectOption<OptionType>[]} The array of options
 */
export const valuesToOptions = <
  OptionType extends string = string,
>(
  values: OptionType[] | OptionType | undefined,
) => {
  const resolvedValues = arrayify(values);

  // Map values to options
  return resolvedValues.map<SelectOption<OptionType>>(value => ({
    label: value,
    value,
  }));
};

/**
 * Convert an object to an array of options, with its entry values
 * as option values, and its entry keys as option labels.
 * 
 * @param {Record<PropertyKey, T>} obj The object to convert
 * @returns {SelectOption<T>[]} The array of options
 */
export const objectValuesToOptions = <T>(
  obj: Record<PropertyKey, T>,
): SelectOption<T>[] => {
  const entries = Object.entries(obj);
  return entries.map<SelectOption<T>>(
    ([label, value]) => ({ label, value })
  );
};

/**
 * Convert an object to an array of options, with its entry keys
 * as option values, and its entry values as option labels.
 * 
 * @param {Record<PropertyKey, T>} obj The object to convert
 * @returns {SelectOption<T>[]} The array of options
 **/
export const objectKeysToOptions = (
  obj: Record<string, string>,
): SelectOption<string>[] => {
  const entries = Object.entries(obj);
  return entries.map<SelectOption>(
    ([value, label]) => ({ label, value })
  );
};

/**
 * Group options by their group name in a map
 * 
 * @param {SelectOptionType[]} options 
 * @returns {Record<string, SelectOptionType[]>}
 */
export const groupOptions = <
  OptionType extends unknown,
  SelectOptionType extends SelectOption<OptionType>,
>(options: SelectOptionType[]): Record<string, SelectOptionType[]> => (
  options.reduce<Record<string, SelectOptionType[]>>(
    (groupedOptions, option) => {
      const { groupName = "" } = option;

      // Create the group if it doesn't exist
      if (!groupedOptions[groupName]) {
        groupedOptions[groupName] = [];
      }

      // Add the option to the group map entry
      groupedOptions[groupName].push(option);

      return groupedOptions;
    },
    {},
  )
);

/**
 * Convert an array of options to a string label.
 * 
 * @param {SelectOption<T>[]} options The options to labelize
 * @returns {string} The label, abbreviated if necessary
 */
export const optionsToAbbreviatedLabel = <T extends unknown = string>(options: SelectOption<T>[]): string => {
  // Resolve label if selected options is array
  if (options.length === 0) {
    return "";
  }

  const firstOption = options[0];
  if (options.length === 1) {
    return firstOption.label;
  }

  const secondOption = options[1];
  if (options.length === 2) {
    return `${firstOption.label} and ${secondOption.label}`;
  }

  const remainingLength = options.length - 2;
  return `${firstOption.label}, ${secondOption.label} and ${remainingLength} more`;
};
