import assert from 'node:assert';

import type { Mask } from '../Mask.ts';
import type { Point } from '../utils/geometry/points.ts';

import {
  getBitSafe,
  getCoordsFromIndex,
} from './utils/getExternalContourUtils.ts';

interface Direction {
  /**
   * Horizontal direction change.
   */
  dc: number;
  /**
   * Vertical direction change.
   */
  dr: number;
}
/**
 * Possible directions.
 */
const directions: Direction[] = [
  { dc: -1, dr: 0 }, // left
  { dc: -1, dr: -1 }, // up-left
  { dc: 0, dr: -1 }, // up
  { dc: 1, dr: -1 }, // up-right
  { dc: 1, dr: 0 }, // right
  { dc: 1, dr: 1 }, // down-right
  { dc: 0, dr: 1 }, // down
  { dc: -1, dr: 1 }, // down-left
];
// Lookup table for directions' index.
const directionIndexLookup = new Int8Array(9).fill(-1);
for (let i = 0; i < directions.length; i++) {
  const { dc, dr } = directions[i];
  directionIndexLookup[(dr + 1) * 3 + (dc + 1)] = i;
}
/**
 * Return an array with the coordinates of the pixels that are on the border of the mask using Moore's tracing algorithm.
 * The reference is the top-left corner of the ROI.
 * @param mask - Mask to process.
 * @returns The array of border pixels.
 */
export function getContourMoore(mask: Mask): Point[] {
  let index = 0;

  const contour: Point[] = [];
  const visited = new Uint8Array(mask.width * mask.height);
  while (mask.getBitByIndex(index) !== 1 && index !== mask.size) {
    index++;
  }

  if (index === mask.size) return contour;
  const [row, col] = getCoordsFromIndex(index, mask.width);
  const startingPoint = { column: col, row };
  let currentPoint = startingPoint;

  visited[index] = 1;
  contour.push(currentPoint);

  const firstNext = findNextPoint(mask, currentPoint, { column: col - 1, row });
  if (!firstNext) {
    return contour;
  }

  let startingBacktrackPoint: Point = firstNext.prevPoint;

  let backtrackPoint = firstNext.prevPoint;
  currentPoint = firstNext.currPoint;

  while (true) {
    index = currentPoint.row * mask.width + currentPoint.column;
    if (!visited[index]) {
      visited[index] = 1;
      contour.push(currentPoint);
    }

    const next = findNextPoint(mask, currentPoint, backtrackPoint);
    assert.ok(next, 'Next border point is undefined.');
    backtrackPoint = next.prevPoint;
    currentPoint = next.currPoint;
    if (
      currentPoint.column === startingPoint.column &&
      currentPoint.row === startingPoint.row
    ) {
      if (
        backtrackPoint.column === startingBacktrackPoint.column &&
        backtrackPoint.row === startingBacktrackPoint.row
      ) {
        break;
      } else {
        startingBacktrackPoint = backtrackPoint;
      }
    }
  }

  return contour;
}
/**
 * Finds next point to trace.
 * @param mask - Mask in check.
 * @param current - Current border point.
 * @param previous - Last non-border point to start search from.
 * @returns Object with new border point and last non-border point.
 */
function findNextPoint(
  mask: Mask,
  current: Point,
  previous: Point,
): { prevPoint: Point; currPoint: Point } | undefined {
  let prevPoint = previous;

  // Vector from current back to previous
  const backDc = previous.column - current.column;
  const backDr = previous.row - current.row;

  const startIndex = directionIndexLookup[(backDr + 1) * 3 + (backDc + 1)];

  // Scan clockwise starting from that index.
  for (let i = 0; i < directions.length; i++) {
    const { dc, dr } = directions.at(
      (startIndex + i) % directions.length,
    ) as Direction;
    const newCol = current.column + dc;
    const newRow = current.row + dr;
    if (getBitSafe(mask, newCol, newRow) === 1) {
      // Stores found border point and last non-border point that
      // was found. Will be used as a starting point in the next
      // iteration.
      return {
        prevPoint,
        currPoint: { column: newCol, row: newRow },
      };
    } else {
      prevPoint = { column: newCol, row: newRow };
    }
  }
  // Is triggered only if the point has no neighbours. So it is undefined
  // only if mask is made of one pixel.
  return undefined;
}
