import type { DataRawDump } from "../factorio-process-data.models.ts";

/**
 * Compare two prototype objects by their `order` field.
 * If `order` is missing, it defaults to an empty string.
 *
 * @param a - First prototype object.
 * @param b - Second prototype object.
 * @returns Negative if `a.order < b.order`, positive if greater, zero otherwise.
 */
function orderFactorioPrototype(a: { order?: string }, b: { order?: string }) {
  return (a.order ?? "")?.localeCompare(b.order ?? "");
}

export type SubGroup<T> = {
  name: string;
  order?: string;
  children: Array<T>;
};
export type Group<T> = {
  name: string;
  order?: string;
  icon?: string;
  subGroup: Array<SubGroup<T>>;
};
/** Type of an object that can be grouped by its `subgroup` property. */
export interface GroupablaPrototype {
  subgroup: string;
  order?: string;
}
/**
 * Create a map of subgroup names to their corresponding `SubGroup` objects.
 *
 * @param prototypes - The prototypes to group.
 * @param subGroupsDefinitions - Definition objects for the available subgroups.
 * @returns Map where the key is the subgroup name and the value is a `SubGroup`.
 */
function createSubGroupMap<T extends GroupablaPrototype>(
  prototypes: Array<T>,
  subGroupsDefinitions: Array<{ name: string; order?: string }>
): Map<string, SubGroup<T>> {
  const map = new Map<string, SubGroup<T>>();

  for (const proto of prototypes) {
    if (!map.has(proto.subgroup)) {
      const subGroup = subGroupsDefinitions.find((sg) => sg.name === proto.subgroup);
      if (subGroup) {
        map.set(proto.subgroup, {
          name: subGroup.name,
          order: subGroup.order ?? "",
          children: [],
        });
      }
    }

    const subgroup = map.get(proto.subgroup);
    subgroup?.children.push(proto);
  }

  return map;
}

/**
 * Create a map of group names to their corresponding `Group` objects.
 *
 * @param subGroupMap - Map of subgroup names to `SubGroup` objects.
 * @param groupsDef - Definition objects for the available groups.
 * @param subGroupsDef - Definition objects for the available subgroups.
 * @returns Map where the key is the group name and the value is a `Group`.
 */
function createGroupMap<T extends GroupablaPrototype>(
  subGroupMap: Map<string, SubGroup<T>>,
  groupsDef: Array<{ name: string; order?: string }>,
  subGroupsDef: Array<{ name: string; group: string; order?: string }>
): Map<string, Group<T>> {
  const map = new Map<string, Group<T>>();

  for (const subGroupDef of subGroupsDef) {
    const subGroup = subGroupMap.get(subGroupDef.name);
    if (!subGroup || subGroup.children.length === 0) continue;

    if (!map.has(subGroupDef.group)) {
      const groupDef = groupsDef.find((g) => g.name === subGroupDef.group);
      if (groupDef) {
        map.set(subGroupDef.group, {
          name: groupDef.name,
          icon: `item-group/${groupDef.name}.png`,
          order: groupDef.order,
          subGroup: [],
        });
      }
    }

    map.get(subGroupDef.group)?.subGroup.push(subGroup);
  }

  return map;
}

/**
 * Sort groups, subgroups, and their children by the `order` field.
 *
 * @param groups - Array of `Group` objects to be sorted.
 */
function sortGroups<T extends GroupablaPrototype>(groups: Group<T>[]) {
  groups.sort(orderFactorioPrototype);

  for (const group of groups) {
    group.subGroup.sort(orderFactorioPrototype);

    for (const subGroup of group.subGroup) {
      subGroup.children.sort(orderFactorioPrototype);
    }
  }
}
/**
 * Remove the `order` property from all groups, subgroups, and prototypes.
 *
 * @param groups - Array of `Group` objects from which to delete `order`.
 */
function stripOrder<T extends GroupablaPrototype>(groups: Group<T>[]) {
  for (const group of groups) {
    delete group.order;
    group.subGroup.sort(orderFactorioPrototype);

    for (const subGroup of group.subGroup) {
      delete subGroup.order;

      for (const child of subGroup.children) {
        delete child.order;
      }
    }
  }
}
/**
 * Group the factorio prototypes according to there subgroups
 * @param arr the array of item to be grouped
 * @param data the rawData object extracted from Factorio
 * @param keepOrder flag to indicate if the order values must be kept in the final output
 * @returns
 */
export function groupPrototypes<T extends GroupablaPrototype>(
  arr: Array<T>,
  data: DataRawDump,
  keepOrder: boolean = true
): Group<T>[] {
  const itemGroups = Object.values(data["item-group"]).filter((o) => !o.hidden);
  const itemSubGroups = Object.values(data["item-subgroup"]).filter((o) => !o.hidden);

  const subGroupMap = createSubGroupMap(arr, itemSubGroups);
  const groupMap = createGroupMap(subGroupMap, itemGroups, itemSubGroups);

  const output = Array.from(groupMap.values());
  // Sort the output
  sortGroups(output);

  if (!keepOrder) {
    stripOrder(output);
  }
  return output;
}
