import type { DrawLine, DrawRect, DrawRegion, DrawableShape } from './layoutDrawTypes';

type RegionCell = {
  x: number;
  y: number;
  width: number;
  height: number;
};

export type ResolvedDrawRegion = {
  key: string;
  cells: RegionCell[];
  labelPosition: {
    x: number;
    y: number;
  };
  label: string;
  fillColor: string | null;
  assignmentId: string | null;
};

function normalizeLine(line: DrawLine) {
  return {
    x1: Math.min(line.x1, line.x2),
    y1: Math.min(line.y1, line.y2),
    x2: Math.max(line.x1, line.x2),
    y2: Math.max(line.y1, line.y2),
  };
}

function uniqueSorted(values: number[]) {
  return [...new Set(values.filter((value) => Number.isFinite(value)))].sort((left, right) => left - right);
}

function isVerticalBoundaryBlocked(verticalLines: DrawLine[], rects: DrawRect[], x: number, startY: number, endY: number) {
  if (
    verticalLines.some((line) => {
      const normalized = normalizeLine(line);
      return normalized.x1 === x && normalized.y1 <= startY && normalized.y2 >= endY;
    })
  ) {
    return true;
  }

  return rects.some(
    (rect) =>
      (rect.x === x || rect.x + rect.width === x) &&
      rect.y <= startY &&
      rect.y + rect.height >= endY,
  );
}

function isHorizontalBoundaryBlocked(horizontalLines: DrawLine[], rects: DrawRect[], y: number, startX: number, endX: number) {
  if (
    horizontalLines.some((line) => {
      const normalized = normalizeLine(line);
      return normalized.y1 === y && normalized.x1 <= startX && normalized.x2 >= endX;
    })
  ) {
    return true;
  }

  return rects.some(
    (rect) =>
      (rect.y === y || rect.y + rect.height === y) &&
      rect.x <= startX &&
      rect.x + rect.width >= endX,
  );
}

export function splitDrawShapes(shapes: DrawableShape[] | (DrawableShape | DrawRegion)[]) {
  const drawableShapes: DrawableShape[] = [];
  const regionAssignments: DrawRegion[] = [];

  shapes.forEach((shape) => {
    if (shape.type === 'region') {
      regionAssignments.push(shape);
      return;
    }

    drawableShapes.push(shape);
  });

  return { drawableShapes, regionAssignments };
}

export function resolveDrawRegions(
  shapes: DrawableShape[] | (DrawableShape | DrawRegion)[],
  canvasWidth: number,
  canvasHeight: number,
): ResolvedDrawRegion[] {
  if (canvasWidth <= 0 || canvasHeight <= 0) {
    return [];
  }

  const { drawableShapes, regionAssignments } = splitDrawShapes(shapes);
  const lines = drawableShapes.filter((shape): shape is DrawLine => shape.type === 'line');
  const rects = drawableShapes.filter((shape): shape is DrawRect => shape.type === 'rect');
  const verticalLines = lines.filter((line) => line.orientation === 'vertical');
  const horizontalLines = lines.filter((line) => line.orientation === 'horizontal');

  const xs = uniqueSorted([
    0,
    canvasWidth,
    ...verticalLines.map((line) => line.x1),
    ...rects.flatMap((rect) => [rect.x, rect.x + rect.width]),
  ]);
  const ys = uniqueSorted([
    0,
    canvasHeight,
    ...horizontalLines.map((line) => line.y1),
    ...rects.flatMap((rect) => [rect.y, rect.y + rect.height]),
  ]);

  if (xs.length < 2 || ys.length < 2) {
    return [];
  }

  const visited = new Set<string>();
  const assignmentMap = new Map(regionAssignments.map((shape) => [shape.regionKey, shape]));

  const regions: ResolvedDrawRegion[] = [];

  for (let xIndex = 0; xIndex < xs.length - 1; xIndex += 1) {
    for (let yIndex = 0; yIndex < ys.length - 1; yIndex += 1) {
      const startKey = `${xIndex}:${yIndex}`;
      if (visited.has(startKey)) {
        continue;
      }

      const stack: Array<[number, number]> = [[xIndex, yIndex]];
      const cells: RegionCell[] = [];
      const cellKeys: string[] = [];
      let totalArea = 0;
      let weightedCenterX = 0;
      let weightedCenterY = 0;

      while (stack.length > 0) {
        const [currentXIndex, currentYIndex] = stack.pop() as [number, number];
        const currentKey = `${currentXIndex}:${currentYIndex}`;
        if (visited.has(currentKey)) {
          continue;
        }

        visited.add(currentKey);
        cellKeys.push(currentKey);

        const cell = {
          x: xs[currentXIndex],
          y: ys[currentYIndex],
          width: xs[currentXIndex + 1] - xs[currentXIndex],
          height: ys[currentYIndex + 1] - ys[currentYIndex],
        };
        cells.push(cell);

        const area = cell.width * cell.height;
        totalArea += area;
        weightedCenterX += (cell.x + cell.width / 2) * area;
        weightedCenterY += (cell.y + cell.height / 2) * area;

        if (
          currentXIndex > 0 &&
          !isVerticalBoundaryBlocked(verticalLines, rects, xs[currentXIndex], cell.y, cell.y + cell.height)
        ) {
          stack.push([currentXIndex - 1, currentYIndex]);
        }

        if (
          currentXIndex < xs.length - 2 &&
          !isVerticalBoundaryBlocked(verticalLines, rects, xs[currentXIndex + 1], cell.y, cell.y + cell.height)
        ) {
          stack.push([currentXIndex + 1, currentYIndex]);
        }

        if (
          currentYIndex > 0 &&
          !isHorizontalBoundaryBlocked(horizontalLines, rects, ys[currentYIndex], cell.x, cell.x + cell.width)
        ) {
          stack.push([currentXIndex, currentYIndex - 1]);
        }

        if (
          currentYIndex < ys.length - 2 &&
          !isHorizontalBoundaryBlocked(horizontalLines, rects, ys[currentYIndex + 1], cell.x, cell.x + cell.width)
        ) {
          stack.push([currentXIndex, currentYIndex + 1]);
        }
      }

      if (cells.length === 0) {
        continue;
      }

      const centerX = totalArea > 0 ? weightedCenterX / totalArea : cells[0].x + cells[0].width / 2;
      const centerY = totalArea > 0 ? weightedCenterY / totalArea : cells[0].y + cells[0].height / 2;
      const anchorCell = cells.reduce((closest, cell) => {
        const closestDistance = Math.hypot(
          closest.x + closest.width / 2 - centerX,
          closest.y + closest.height / 2 - centerY,
        );
        const currentDistance = Math.hypot(cell.x + cell.width / 2 - centerX, cell.y + cell.height / 2 - centerY);
        return currentDistance < closestDistance ? cell : closest;
      }, cells[0]);
      const key = cellKeys.sort().join('|');
      const assignment = assignmentMap.get(key) ?? null;

      regions.push({
        key,
        cells,
        labelPosition: {
          x: anchorCell.x + anchorCell.width / 2,
          y: anchorCell.y + anchorCell.height / 2,
        },
        label: assignment?.label ?? '',
        fillColor: assignment?.fillColor ?? null,
        assignmentId: assignment?.id ?? null,
      });
    }
  }

  return regions;
}

export function findRegionAtPoint(regions: ResolvedDrawRegion[], x: number, y: number) {
  for (let index = regions.length - 1; index >= 0; index -= 1) {
    const region = regions[index];
    if (
      region.cells.some(
        (cell) => x >= cell.x && x <= cell.x + cell.width && y >= cell.y && y <= cell.y + cell.height,
      )
    ) {
      return region;
    }
  }

  return null;
}
