import timezoneData from "./timezoneMap.json";

export interface Timezone {
  cities: string[];
  countryName: string;
  countryCode: string;
  continentName: string;
  continentCode: string;
  offset: number;
  names: string[];
  primaryName: string;
  descriptiveName: string;
  abbreviation: string;
  isPrimary: boolean;
}
export interface TimezoneMap {
  [key: string]: Timezone;
}

export interface TimezoneSearchResult {
  timezone: Timezone;
  searchType?: TimezoneSearchType;
  matches?: string[];
}
export interface TimezoneSearchOptions {
  by: TimezoneSearchType[];
}
export enum TimezoneSearchType {
  ABBREVIATION,
  NAMES,
  COUNTRY,
  COUNTRY_CODE,
  CONTINENT,
  CONTINENT_CODE,
  CITIES,
  DESCRIPTIVE_NAME,
  OFFSET,
}

export const timezoneMap = timezoneData as TimezoneMap;
export const timezones = Object.values(timezoneData) as Timezone[];

/**
 * Normalize a string
 *
 * @param {string} value The value to normalize
 * @returns {string} The normalized value
 */
const normalizeValue = (value: string): string => value.trim().toLowerCase();

type NaiveSearch = (searchable: string) => boolean;

/**
 * Create a naive string search function
 *
 * @param {string} searchTerm The term to search against
 * @returns {NaiveSearch} The function that matches searchable strings against the search term
 */
const createNaiveSearch =
  (searchTerm: string): NaiveSearch =>
  (searchable = "") => {
    const normalizedTerm = normalizeValue(searchTerm);
    const normalizedSearchable = normalizeValue(searchable);

    return normalizedSearchable.includes(normalizedTerm);
  };

type TimezonePropMap = (timezone: Timezone) => string | string[];

/**
 * Reduces a timezone search type to an array of timezone search results, using a search function
 *
 * @param {TimezoneSearchType} type The type of search to perform
 * @returns {string | string[]} The mapped timezone property
 */
export const reduceSearchTypeToResults = (
  type: TimezoneSearchType
): TimezonePropMap => {
  switch (type) {
    case TimezoneSearchType.ABBREVIATION:
      return ({ abbreviation }) => abbreviation;

    case TimezoneSearchType.DESCRIPTIVE_NAME:
      return ({ descriptiveName }) => descriptiveName;

    case TimezoneSearchType.OFFSET:
      return ({ offset }) => String(offset);

    case TimezoneSearchType.NAMES:
      return ({ names }) => names;

    case TimezoneSearchType.COUNTRY:
      return ({ countryName }) => countryName;

    case TimezoneSearchType.COUNTRY_CODE:
      return ({ countryCode }) => countryCode;

    case TimezoneSearchType.CONTINENT:
      return ({ continentName }) => continentName;

    case TimezoneSearchType.CONTINENT_CODE:
      return ({ continentCode }) => continentCode;

    case TimezoneSearchType.CITIES:
      return ({ cities }) => cities;

    default:
      throw new Error(`Timezone search type ${type} is unhandled`);
  }
};

/**
 * Maps timezones to timezone search results
 *
 * @param {Timezone[]} timezones The timezones to search
 * @param {TimezoneSearchType} type The type of search to perform
 * @param {NaiveSearch} search The function that matches against a search term
 * @returns {TimezoneSearchResult[]} An array of timezone search results
 */
const mapTimezonesToSeachResults = (
  timezones: Timezone[],
  type: TimezoneSearchType,
  search: (searchable: string) => boolean
): TimezoneSearchResult[] => {
  const map = reduceSearchTypeToResults(type);

  return timezones.reduce(
    (results: TimezoneSearchResult[], timezone: Timezone) => {
      const searchable = map(timezone);
      const searchables = Array.isArray(searchable) ? searchable : [searchable];

      const matches = searchables.filter(search);
      if (matches.length === 0) {
        return results;
      }

      return results.concat({
        timezone,
        searchType: type,
        matches,
      });
    },
    []
  );
};

/**
 * Search an array of timezones for a search term, configured by the specified options
 *
 * @param {string} searchTerm The term to search the timezones against
 * @param {Timezone[]} targetTimezones The timezones to search
 * @param {TimezoneSearchOptions} options The options for the timezone search
 * @returns {TimezoneSearchResult[]} The timezone search results
 */
export const searchTimezones = (
  searchTerm: string,
  targetTimezones: Timezone[] = timezones,
  options: TimezoneSearchOptions = {
    by: [
      TimezoneSearchType.ABBREVIATION,
      TimezoneSearchType.DESCRIPTIVE_NAME,
      TimezoneSearchType.NAMES,
      TimezoneSearchType.CITIES,
      TimezoneSearchType.COUNTRY,
      TimezoneSearchType.CONTINENT,
    ],
  }
): TimezoneSearchResult[] => {
  // Perform naive search on timezones
  const naiveSearch = createNaiveSearch(searchTerm);

  const timezoneResults = options.by
    .map((searchType) =>
      mapTimezonesToSeachResults(targetTimezones, searchType, naiveSearch)
    )
    .flat();

  return timezoneResults;
};
