import { getAddHeaderStartIndex, isDefined } from "../../helpers/misc";
import { rangeAdapterRegistry } from "../../helpers/range";
import { getRangeAdapterFunctions } from "../../helpers/range_adapter_functions";
import {
  moveHeaderIndexesOnHeaderAddition,
  moveHeaderIndexesOnHeaderDeletion,
} from "../../helpers/sheet";
import { isInside } from "../../helpers/zones";
import { otRegistry } from "../../registries/ot_registry";
import { specificRangeTransformRegistry } from "../../registries/srt_registry";
import {
  AddColumnsRowsCommand,
  AddMergeCommand,
  CoreCommand,
  HeadersDependentCommand,
  isHeadersDependant,
  isPositionDependent,
  isRangeDependant,
  isSheetDependent,
  isTargetDependent,
  isZoneDependent,
  PositionDependentCommand,
  RangesDependentCommand,
  RemoveColumnsRowsCommand,
  TargetDependentCommand,
  ZoneDependentCommand,
} from "../../types/commands";
import { HeaderIndex, Zone } from "../../types/misc";
import { transformRangeData, TransformResult, transformSheetId, transformZone } from "./ot_helpers";
import "./ot_specific";
import "./srt_specific";

const transformations: {
  match: (t: CoreCommand) => boolean;
  fn: (t: CoreCommand, e: CoreCommand) => CoreCommand | TransformResult;
}[] = [
  { match: isSheetDependent, fn: transformSheetId },
  { match: isTargetDependent, fn: transformTarget },
  { match: isZoneDependent, fn: transformZoneDependentCommand },
  { match: isPositionDependent, fn: transformPosition },
  { match: isHeadersDependant, fn: transformHeaders },
  { match: isRangeDependant, fn: transformRangesDependentCommand },
];

/**
 * Get the result of applying the operation transformations on the given command
 * to transform based on the executed command.
 * Let's see a small example:
 * Given
 *  - command A: set the content of C1 to "Hello"
 *  - command B: add a column after A
 *
 * If command B has been executed locally and not transmitted (yet) to
 * other clients, and command A arrives from an other client to be executed locally.
 * Command A is no longer valid and no longer reflects the user intention.
 * It needs to be transformed knowing that command B is already executed.
 * transform(A, B) => set the content of D1 to "Hello"
 */
export function transform(
  toTransform: CoreCommand,
  executed: CoreCommand
): CoreCommand | undefined {
  const specificTransform = otRegistry.getTransformation(toTransform.type, executed.type);
  const transformed = specificTransform
    ? specificTransform(toTransform, executed)
    : genericTransform(toTransform, executed);
  if (transformed) {
    return adaptTransform(transformed, executed);
  }
  return transformed;
}

function adaptTransform(toTransform: CoreCommand, executed: CoreCommand): CoreCommand {
  const adaptFn = specificRangeTransformRegistry.get(toTransform.type);
  if (!adaptFn) {
    return toTransform;
  }
  const adapterFunctions = getRangeAdapterFunctions(executed);
  if (adapterFunctions) {
    return adaptFn(toTransform, adapterFunctions);
  }
  return toTransform;
}

/**
 * Get the result of applying the operation transformations on all the given
 * commands to transform for each executed commands.
 */
export function transformAll(
  toTransform: readonly CoreCommand[],
  executed: readonly CoreCommand[]
): CoreCommand[] {
  let transformedCommands = [...toTransform];
  const possibleTransformations = new Set(otRegistry.getKeys());
  for (const executedCommand of executed) {
    // If the executed command is not in the registry, we skip it
    // because we know there won't be any transformation impacting the
    // commands to transform.
    if (
      possibleTransformations.has(executedCommand.type) ||
      rangeAdapterRegistry.contains(executedCommand.type)
    ) {
      transformedCommands = transformedCommands.reduce<CoreCommand[]>((acc, cmd) => {
        const transformed = transform(cmd, executedCommand);
        if (transformed) {
          acc.push(transformed);
        }
        return acc;
      }, []);
    }
  }
  return transformedCommands;
}

/**
 * Apply all generic transformation based on the characteristic of the given commands.
 */
function genericTransform(cmd: CoreCommand, executed: CoreCommand): CoreCommand | undefined {
  for (const { match, fn } of transformations) {
    if (match(cmd)) {
      const result = fn(cmd, executed);
      if (result === "SKIP_TRANSFORMATION") {
        continue;
      }
      if (result === "IGNORE_COMMAND") {
        return undefined;
      }
      cmd = result;
    }
  }
  return cmd;
}

function transformTarget(
  cmd: Extract<CoreCommand, TargetDependentCommand>,
  executed: CoreCommand
): Extract<CoreCommand, TargetDependentCommand> | TransformResult {
  const transformSheetResult = transformSheetId(cmd, executed);
  if (transformSheetResult !== "SKIP_TRANSFORMATION") {
    return transformSheetResult === "IGNORE_COMMAND" ? "IGNORE_COMMAND" : cmd;
  }
  const target: Zone[] = [];
  for (const zone of cmd.target) {
    const newZone = transformZone(zone, executed);
    if (newZone) {
      target.push(newZone);
    }
  }
  if (!target.length) {
    return "IGNORE_COMMAND";
  }
  return { ...cmd, target };
}

function transformZoneDependentCommand(
  cmd: Extract<CoreCommand, ZoneDependentCommand>,
  executed: CoreCommand
) {
  const transformSheetResult = transformSheetId(cmd, executed);
  if (transformSheetResult !== "SKIP_TRANSFORMATION") {
    return transformSheetResult === "IGNORE_COMMAND" ? "IGNORE_COMMAND" : cmd;
  }
  const newZone = transformZone(cmd.zone, executed);
  if (newZone) {
    return { ...cmd, zone: newZone };
  }
  return "IGNORE_COMMAND";
}
function transformRangesDependentCommand(
  toTransform: Extract<CoreCommand, RangesDependentCommand>,
  executed: CoreCommand
): Extract<CoreCommand, RangesDependentCommand> | TransformResult {
  if (!("sheetId" in executed)) {
    return toTransform;
  }
  const ranges = toTransform.ranges
    .map((range) => transformRangeData(range, executed))
    .filter(isDefined);
  if (!ranges.length) {
    return "IGNORE_COMMAND";
  }
  return { ...toTransform, ranges };
}

function transformHeaders(
  toTransform: Extract<CoreCommand, HeadersDependentCommand>,
  executed: CoreCommand
): CoreCommand | TransformResult {
  const transformSheetResult = transformSheetId(toTransform, executed);
  if (transformSheetResult !== "SKIP_TRANSFORMATION") {
    return transformSheetResult === "IGNORE_COMMAND" ? "IGNORE_COMMAND" : toTransform;
  }
  if (executed.type !== "ADD_COLUMNS_ROWS" && executed.type !== "REMOVE_COLUMNS_ROWS") {
    return "SKIP_TRANSFORMATION";
  }
  if (executed.dimension !== toTransform.dimension) {
    return toTransform;
  }

  let result: HeaderIndex[] = [];
  if (executed.type === "REMOVE_COLUMNS_ROWS") {
    result = moveHeaderIndexesOnHeaderDeletion(executed.elements, toTransform.elements);
  } else if (executed.type === "ADD_COLUMNS_ROWS") {
    const base = getAddHeaderStartIndex(executed.position, executed.base);
    result = moveHeaderIndexesOnHeaderAddition(base, executed.quantity, toTransform.elements);
  }

  if (result.length === 0) {
    return "IGNORE_COMMAND";
  }
  return { ...toTransform, elements: result };
}

/**
 * Transform a PositionDependentCommand. It could be impacted by a grid command
 * (Add/remove cols/rows) and a merge
 */
function transformPosition(
  toTransform: Extract<CoreCommand, PositionDependentCommand>,
  executed: CoreCommand
): Extract<CoreCommand, PositionDependentCommand> | TransformResult {
  const transformSheetResult = transformSheetId(toTransform, executed);
  if (transformSheetResult !== "SKIP_TRANSFORMATION") {
    return transformSheetResult === "IGNORE_COMMAND" ? "IGNORE_COMMAND" : toTransform;
  }
  if (executed.type === "ADD_COLUMNS_ROWS" || executed.type === "REMOVE_COLUMNS_ROWS") {
    return transformPositionWithGrid(toTransform, executed);
  }
  if (executed.type === "ADD_MERGE") {
    return transformPositionWithMerge(toTransform, executed);
  }
  return "SKIP_TRANSFORMATION";
}

/**
 * Transform a PositionDependentCommand after a grid shape modification. This
 * transformation consists of updating the position.
 */
function transformPositionWithGrid(
  toTransform: Extract<CoreCommand, PositionDependentCommand>,
  executed: AddColumnsRowsCommand | RemoveColumnsRowsCommand
): Extract<CoreCommand, PositionDependentCommand> | TransformResult {
  const field = executed.dimension === "COL" ? "col" : "row";
  let base = toTransform[field];
  if (executed.type === "REMOVE_COLUMNS_ROWS") {
    const elements = [...executed.elements].sort((a, b) => b - a);
    if (elements.includes(base)) {
      return "IGNORE_COMMAND";
    }
    for (const removedElement of elements) {
      if (base >= removedElement) {
        base--;
      }
    }
  }
  if (executed.type === "ADD_COLUMNS_ROWS") {
    if (base > executed.base || (base === executed.base && executed.position === "before")) {
      base = base + executed.quantity;
    }
  }
  return { ...toTransform, [field]: base };
}

/**
 * Transform a PositionDependentCommand after a merge. This transformation
 * consists of checking that the position is not inside the merged zones
 */
function transformPositionWithMerge(
  toTransform: Extract<CoreCommand, PositionDependentCommand>,
  executed: AddMergeCommand
): Extract<CoreCommand, PositionDependentCommand> | TransformResult {
  for (const zone of executed.target) {
    const sameTopLeft = toTransform.col === zone.left && toTransform.row === zone.top;
    if (!sameTopLeft && isInside(toTransform.col, toTransform.row, zone)) {
      return "IGNORE_COMMAND";
    }
  }
  return toTransform;
}
