import { FilterValue, FilterValues } from '../../Filters/Filters.model';

interface StringKeyedObject {
  [key: string]: unknown;
}

export type FilterFunction = (
  value: FilterValue,
) => Record<string, unknown> | boolean;

type FilterMap<K> = {
  [U in keyof K]: string | string[] | FilterFunction;
};

/**
 * Function to transform @axinom/mosaic-ui FilterValues to PostGraphile filters
 * @param filters FilterValues from @axinom/mosaic-ui
 * @param map Object containing paths of PostGraphile filters.
 * 
 * @example
 * filterToPostGraphileFilter<MovieFilter>(filters, {
      title: 'includes', // Resolves to {title: {includes: ${value}}}
      tags: ['some', 'name', 'includes'] // Resolves to {tags: {some: {name: {includes: ${value}}}}}
      createdDate: value => {
        const values = String(value).split('|');
        return {
          greaterThan: values[0],
          lessThan: values[1],
        };
      }, // Resolves to {createdDate: {greaterThan: ${values[0]}, lessThan: ${values[1]}}}
    };
 */
export const filterToPostGraphileFilter = <T extends StringKeyedObject>(
  filters: FilterValues<unknown>,
  map: FilterMap<T>,
): T | undefined => {
  const filterEntries: [string, FilterValue][] = Object.entries(filters);
  if (filterEntries.length === 0) {
    return undefined;
  } else {
    const filterWithQuery: StringKeyedObject = {};

    for (const [prop, value] of filterEntries) {
      const mapValues = map[prop];

      if (typeof mapValues === 'string') {
        setProperty(filterWithQuery, [prop, mapValues], value);
      } else if (typeof mapValues === 'function') {
        const customValue = mapValues(value);
        filterWithQuery[prop] = customValue;
      } else if (typeof mapValues === 'object') {
        mapValues.unshift(prop);
        setProperty(filterWithQuery, mapValues, value);
      }
    }

    return filterWithQuery as T;
  }
};

function setProperty(
  obj: StringKeyedObject,
  keys: string[],
  value: FilterValue,
): void {
  if (keys.length > 1) {
    const propertyName = String(keys.shift());
    obj[propertyName] = {};
    setProperty(obj[propertyName] as StringKeyedObject, keys, value);
  } else {
    const propertyName = String(keys.shift());
    obj[propertyName] = value;
  }
}
