import { comparisonGroupKey, toleranceFromJitter } from "./partition.js";
import type {
  JitterEnvelope,
  MeasuredLayoutCellEvidence,
  MeasuredLayoutMatrixEvidence,
  MeasuredLayoutNode,
  MeasuredLayoutRect,
  MeasuredLayoutViolationFact,
} from "./types.js";

type PairedInsetCandidate = {
  cell: MeasuredLayoutCellEvidence;
  node: MeasuredLayoutNode;
  nodeIndex: number;
  identity: string;
  comparisonGroup: string;
  structuralSignature: string;
  insetVector: readonly [number, number, number, number];
  tolerance: readonly [number, number, number, number];
};

function compareText(left: string, right: string): number {
  return left.localeCompare(right, "en");
}

function positiveArea(rect: MeasuredLayoutRect): boolean {
  return rect.width > 0 && rect.height > 0;
}

export function hasChildUnionBox(node: MeasuredLayoutNode): boolean {
  return node.childUnionBox !== null;
}

export function excludesZeroAreaInsetCandidate(node: MeasuredLayoutNode): boolean {
  return !node.visibility.visible || node.visibility.zeroArea || !positiveArea(node.contentBox);
}

export function excludesChildUnionOutsideContentBox(node: MeasuredLayoutNode): boolean {
  const union = node.childUnionBox;
  if (union === null) return false;
  const content = node.contentBox;
  return union.x < content.x
    || union.y < content.y
    || union.x + union.width > content.x + content.width
    || union.y + union.height > content.y + content.height;
}

export function insetVectorFromContentBox(node: MeasuredLayoutNode): readonly [number, number, number, number] {
  const union = node.childUnionBox;
  if (union === null) throw new Error("paired inset candidate is missing its child union box");
  const content = node.contentBox;
  const inlineStart = node.direction === "rtl"
    ? content.x + content.width - (union.x + union.width)
    : union.x - content.x;
  const inlineEnd = node.direction === "rtl"
    ? union.x - content.x
    : content.x + content.width - (union.x + union.width);
  return [inlineStart, inlineEnd, union.y - content.y, content.y + content.height - (union.y + union.height)];
}

function jitterField(nodeIndex: number, field: string): string {
  return `nodes[${String(nodeIndex)}].${field}`;
}

function summedJitter(
  envelope: JitterEnvelope,
  nodeIndex: number,
  fields: readonly string[],
): number {
  return fields.reduce(
    (total, field) => total + toleranceFromJitter(envelope, jitterField(nodeIndex, field)),
    0,
  );
}

function insetTolerance(
  envelope: JitterEnvelope,
  nodeIndex: number,
  direction: MeasuredLayoutNode["direction"],
): readonly [number, number, number, number] {
  const inlineStart = direction === "rtl"
    ? ["contentBox.x", "contentBox.width", "childUnionBox.x", "childUnionBox.width"]
    : ["contentBox.x", "childUnionBox.x"];
  const inlineEnd = direction === "rtl"
    ? ["contentBox.x", "childUnionBox.x"]
    : ["contentBox.x", "contentBox.width", "childUnionBox.x", "childUnionBox.width"];
  return [
    summedJitter(envelope, nodeIndex, inlineStart),
    summedJitter(envelope, nodeIndex, inlineEnd),
    summedJitter(envelope, nodeIndex, ["contentBox.y", "childUnionBox.y"]),
    summedJitter(envelope, nodeIndex, ["contentBox.y", "contentBox.height", "childUnionBox.y", "childUnionBox.height"]),
  ];
}

function pairedInsetCandidate(
  cell: MeasuredLayoutCellEvidence,
  node: MeasuredLayoutNode,
  nodeIndex: number,
): PairedInsetCandidate | undefined {
  if (!hasChildUnionBox(node)
    || excludesZeroAreaInsetCandidate(node)
    || excludesChildUnionOutsideContentBox(node)) {
    return undefined;
  }
  return {
    cell,
    node,
    nodeIndex,
    identity: `${cell.cell.id}:${node.identity}`,
    comparisonGroup: comparisonGroupKey(cell),
    structuralSignature: node.structuralSignature,
    insetVector: insetVectorFromContentBox(node),
    tolerance: insetTolerance(cell.measuredLayoutProbe.jitterEnvelope, nodeIndex, node.direction),
  };
}

function hasComparablePeers(candidates: readonly PairedInsetCandidate[]): boolean {
  return candidates.length > 1;
}

function candidatesByPartition(
  matrix: MeasuredLayoutMatrixEvidence,
): Map<string, PairedInsetCandidate[]> {
  const partitions = new Map<string, PairedInsetCandidate[]>();
  for (const cell of matrix.cells) {
    for (const [nodeIndex, node] of cell.measuredLayoutProbe.measuredLayoutGraph.nodes.entries()) {
      const candidate = pairedInsetCandidate(cell, node, nodeIndex);
      if (candidate === undefined) continue;
      const key = JSON.stringify({
        comparisonGroup: candidate.comparisonGroup,
        structuralSignature: candidate.structuralSignature,
      });
      const partition = partitions.get(key) ?? [];
      partition.push(candidate);
      partitions.set(key, partition);
    }
  }
  for (const partition of partitions.values()) {
    partition.sort((left, right) => compareText(left.identity, right.identity));
  }
  return partitions;
}

function partitionTolerance(
  candidates: readonly PairedInsetCandidate[],
): readonly [number, number, number, number] {
  if (candidates.length === 0) throw new Error("paired inset partition is empty");
  return [0, 1, 2, 3].map((coordinate) => Math.max(...candidates.map((candidate) => {
    const tolerance = candidate.tolerance[coordinate];
    if (tolerance === undefined) throw new Error("paired inset tolerance is missing a coordinate");
    return tolerance;
  }))) as unknown as readonly [number, number, number, number];
}

function differsFromModal(
  vector: readonly number[],
  modal: readonly number[],
  tolerance: readonly number[],
): boolean {
  return vector.some((value, index) => {
    const modalValue = modal[index];
    const coordinateTolerance = tolerance[index];
    return modalValue === undefined || coordinateTolerance === undefined
      || Math.abs(value - modalValue) > coordinateTolerance;
  });
}

type PairedInsetCluster = {
  modalValue: readonly [number, number, number, number];
  memberIndexes: number[];
};

function coordinateMedian(values: readonly number[]): number {
  const sorted = [...values].sort((left, right) => left - right);
  const median = sorted[Math.floor((sorted.length - 1) / 2)];
  if (median === undefined) throw new Error("paired inset cluster is empty");
  return median;
}

function coordinateWiseModalCluster(
  values: readonly (readonly [number, number, number, number])[],
  tolerance: readonly [number, number, number, number],
): PairedInsetCluster | undefined {
  const clusters = new Map<string, number[]>();
  for (const anchor of values) {
    const memberIndexes = values.flatMap((value, index) => value.every((coordinate, coordinateIndex) => {
      const allowed = tolerance[coordinateIndex];
      const anchorCoordinate = anchor[coordinateIndex];
      return allowed !== undefined && anchorCoordinate !== undefined
        && Math.abs(coordinate - anchorCoordinate) <= allowed;
    }) ? [index] : []);
    const isEquivalent = [0, 1, 2, 3].every((coordinate) => {
      const allowed = tolerance[coordinate];
      const coordinates = memberIndexes.map((index) => values[index]?.[coordinate]);
      return allowed !== undefined
        && coordinates.every((value): value is number => value !== undefined)
        && Math.max(...coordinates) - Math.min(...coordinates) <= allowed;
    });
    if (isEquivalent) clusters.set(memberIndexes.join(","), memberIndexes);
  }
  const maximumSize = Math.max(0, ...[...clusters.values()].map((members) => members.length));
  const largest = [...clusters.values()].filter((members) => members.length === maximumSize);
  if (largest.length !== 1 || maximumSize * 2 <= values.length) return undefined;
  const memberIndexes = largest[0];
  if (memberIndexes === undefined) throw new Error("paired inset cluster is missing members");
  const modalValue = [0, 1, 2, 3].map((coordinate) => coordinateMedian(memberIndexes.map((index) => {
    const value = values[index]?.[coordinate];
    if (value === undefined) throw new Error("paired inset cluster is missing a coordinate");
    return value;
  }))) as unknown as readonly [number, number, number, number];
  return { modalValue, memberIndexes };
}

export function detectPairedInsetAsymmetryOutliers(
  matrix: MeasuredLayoutMatrixEvidence,
): MeasuredLayoutViolationFact[] {
  const violations: MeasuredLayoutViolationFact[] = [];
  const partitions = candidatesByPartition(matrix);
  for (const key of [...partitions.keys()].sort(compareText)) {
    const candidates = partitions.get(key);
    if (candidates === undefined || !hasComparablePeers(candidates)) continue;
    const cluster = coordinateWiseModalCluster(
      candidates.map((candidate) => candidate.insetVector),
      partitionTolerance(candidates),
    );
    if (cluster === undefined) continue;
    const peerIdentities = cluster.memberIndexes.map((index) => candidates[index]?.identity)
      .filter((identity): identity is string => identity !== undefined)
      .sort(compareText);
    for (const [index, candidate] of candidates.entries()) {
      if (cluster.memberIndexes.includes(index)
        || !differsFromModal(candidate.insetVector, cluster.modalValue, candidate.tolerance)) {
        continue;
      }
      violations.push({
        ruleId: "UI-098",
        kind: "paired-inset-asymmetry-outlier",
        cellId: candidate.cell.cell.id,
        locator: candidate.identity,
        blamedIdentity: candidate.node.identity,
        summary: `paired inset vector differs from the observed modal asymmetry for structural signature ${candidate.structuralSignature}`,
        measurement: {
          laneEligibility: "advisory",
          rawMeasurement: candidate.insetVector,
          modalValue: cluster.modalValue,
          peerIdentities,
          comparisonGroup: candidate.comparisonGroup,
          structuralSignature: candidate.structuralSignature,
          contentBox: candidate.node.contentBox,
          childUnionBox: candidate.node.childUnionBox,
          jitterTolerance: candidate.tolerance,
        },
      });
    }
  }
  return violations.sort((left, right) => compareText(left.locator, right.locator));
}
