import { type ConstDirectiveNode, type NameNode } from 'graphql';

export function keyMap<T>(list: readonly T[], keyFn: (item: T) => string): Record<string, T> {
  return list.reduce((map, item) => {
    map[keyFn(item)] = item;
    return map;
  }, Object.create(null));
}

export function isEqual<T>(a: T, b: T): boolean {
  if (Array.isArray(a) && Array.isArray(b)) {
    if (a.length !== b.length) return false;

    for (let index = 0; index < a.length; index++) {
      if (!isEqual(a[index], b[index])) {
        return false;
      }
    }

    return true;
  }

  if (a && b && typeof a === 'object' && typeof b === 'object') {
    const aRecord = a as Record<string, unknown>;
    const bRecord = b as Record<string, unknown>;

    const aKeys: string[] = Object.keys(aRecord);
    const bKeys: string[] = Object.keys(bRecord);

    if (aKeys.length !== bKeys.length) return false;

    for (const key of aKeys) {
      if (!isEqual(aRecord[key], bRecord[key])) {
        return false;
      }
    }

    return true;
  }

  return a === b || (!a && !b);
}

export function isNotEqual<T>(a: T, b: T): boolean {
  return !isEqual(a, b);
}

export function isVoid<T>(a: T): boolean {
  return typeof a === 'undefined' || a === null;
}

export function diffArrays<T>(a: T[] | readonly T[], b: T[] | readonly T[]): T[] {
  return a.filter(c => !b.some(d => isEqual(d, c)));
}

export function matchArrays<T>(a: T[] | readonly T[], b: T[] | readonly T[]): T[] {
  return a.filter(c => b.some(d => isEqual(d, c)));
}

function extractName(name: string | NameNode): string {
  if (typeof name === 'string') {
    return name;
  }

  return name.value;
}

export function compareLists<T extends { name: string | NameNode }>(
  oldList: readonly T[],
  newList: readonly T[],
  callbacks?: {
    onAdded?(t: T): void;
    onRemoved?(t: T): void;
    onMutual?(t: { newVersion: T; oldVersion: T }): void;
  },
) {
  const oldMap = keyMap(oldList, ({ name }) => extractName(name));
  const newMap = keyMap(newList, ({ name }) => extractName(name));

  const added: T[] = [];
  const removed: T[] = [];
  const mutual: Array<{ newVersion: T; oldVersion: T }> = [];

  for (const oldItem of oldList) {
    const newItem = newMap[extractName(oldItem.name)];
    if (newItem === undefined) {
      removed.push(oldItem);
    } else {
      mutual.push({
        newVersion: newItem,
        oldVersion: oldItem,
      });
    }
  }

  for (const newItem of newList) {
    if (oldMap[extractName(newItem.name)] === undefined) {
      added.push(newItem);
    }
  }

  if (callbacks) {
    if (callbacks.onAdded) {
      for (const item of added) {
        callbacks.onAdded(item);
      }
    }
    if (callbacks.onRemoved) {
      for (const item of removed) {
        callbacks.onRemoved(item);
      }
    }
    if (callbacks.onMutual) {
      for (const item of mutual) {
        callbacks.onMutual(item);
      }
    }
  }

  return {
    added,
    removed,
    mutual,
  };
}

export function keyMapList<T>(list: readonly T[], keyFn: (item: T) => string): Record<string, T[]> {
  return list.reduce(
    (map, item) => {
      const key = keyFn(item);
      if (map[key]) {
        map[key].push(item);
      } else {
        map[key] = [item];
      }
      return map;
    },
    Object.create(null) as Record<string, T[]>,
  );
}

export function compareDirectiveLists(
  oldList: readonly ConstDirectiveNode[],
  newList: readonly ConstDirectiveNode[],
  callbacks?: {
    onAdded?(t: ConstDirectiveNode): void;
    onRemoved?(t: ConstDirectiveNode): void;
    onMutual?(t: { newVersion: ConstDirectiveNode; oldVersion: ConstDirectiveNode }): void;
  },
) {
  const oldMap = keyMapList(oldList, ({ name }) => extractName(name));
  const newMap = keyMapList(newList, ({ name }) => extractName(name));

  const added: ConstDirectiveNode[] = [];
  const removed: ConstDirectiveNode[] = [];
  const mutual: Array<{ newVersion: ConstDirectiveNode; oldVersion: ConstDirectiveNode }> = [];

  for (const itemName of Object.keys(oldMap).sort()) {
    const newItems = newMap[itemName];
    const oldItems = oldMap[itemName];
    for (let i = 0; i < oldItems?.length; i++) {
      const oldItem = oldItems[i];
      const newItem = newItems?.[i];
      if (newItem === undefined) {
        removed.push(oldItem);
      } else {
        mutual.push({
          newVersion: newItem,
          oldVersion: oldItem,
        });
      }
    }
  }

  for (const itemName of Object.keys(newMap).sort()) {
    const newItems = newMap[itemName];
    const oldItems = oldMap[itemName];
    for (let i = (oldItems ?? []).length; i < (newItems ?? []).length; i++) {
      const oldItem = oldItems?.[i];
      const newItem = newItems[i];
      if (oldItem === undefined) {
        added.push(newItem);
      }
    }
  }

  if (callbacks) {
    if (callbacks.onAdded) {
      for (const item of added) {
        callbacks.onAdded(item);
      }
    }
    if (callbacks.onRemoved) {
      for (const item of removed) {
        callbacks.onRemoved(item);
      }
    }
    if (callbacks.onMutual) {
      for (const item of mutual) {
        callbacks.onMutual(item);
      }
    }
  }

  return {
    added,
    removed,
    mutual,
  };
}
