// math.gl
// SPDX-License-Identifier: MIT and ISC
// Copyright (c) vis.gl contributors

/* eslint-disable max-statements, max-depth, complexity, no-unused-expressions */

import {equals} from '@math.gl/core';
import type {NumericArray} from '@math.gl/core';

export const WINDING = {
  CLOCKWISE: 1,
  COUNTER_CLOCKWISE: -1
} as const;

/** Polygon representation where each point is represented as a separate array of positions. */
type PointsArray = NumericArray[];

/** Segment visitor callback type for polygons defined with flat arrays, */
type SegmentVisitorFlat = (
  p1x: number,
  p1y: number,
  p2x: number,
  p2y: number,
  i1: number,
  i2: number
) => void;

/** Segment visitor callback type for polygons defined with array of points. */
export type SegmentVisitorPoints = (
  p1: NumericArray,
  p2: NumericArray,
  i1: number,
  i2: number
) => void;

export type Plane2D = 'xy' | 'yz' | 'xz';

/** Parameters of a polygon. */
type PolygonParams = {
  /**
   * Start index of the polygon in the array of positions.
   * @default `0`
   */
  start?: number;
  /**
   * End index of the polygon in the array of positions.
   * @default number of positions
   */
  end?: number;
  /**
   * Size of a point, 2 (XZ) or 3 (XYZ). Affects only polygons stored in flat arrays.
   * @default `2`
   */
  size?: number;
  /**
   * Indicates that the first point of the polygon is equal to the last point, and additional checks should be ommited.
   */
  isClosed?: boolean;
  /**
   * The 2D projection plane on which to calculate the area of a 3D polygon.
   * @default `'xy'`
   */
  plane?: Plane2D;
};

/**
 * Checks winding direction of the polygon and reverses the polygon in case of opposite winding direction.
 * Note: points are modified in-place.
 * @param points An array that represents points of the polygon.
 * @param direction Requested winding direction. 1 is for clockwise, -1 for counterclockwise winding direction.
 * @param options Parameters of the polygon.
 * @return Returns true if the winding direction was changed.
 */
export function modifyPolygonWindingDirection(
  points: NumericArray,
  direction: number,
  options: PolygonParams = {}
): boolean {
  const windingDirection = getPolygonWindingDirection(points, options);
  if (windingDirection !== direction) {
    reversePolygon(points, options);
    return true;
  }
  return false;
}

/**
 * Returns winding direction of the polygon.
 * @param points An array that represents points of the polygon.
 * @param options Parameters of the polygon.
 * @returns Winding direction of the polygon.
 */
export function getPolygonWindingDirection(
  points: NumericArray,
  options: PolygonParams = {}
): number {
  return Math.sign(getPolygonSignedArea(points, options));
}

export const DimIndex: Record<string, number> = {
  x: 0,
  y: 1,
  z: 2
} as const;

/**
 * Returns signed area of the polygon.
 * @param points An array that represents points of the polygon.
 * @param options Parameters of the polygon.
 * @returns Signed area of the polygon.
 * https://en.wikipedia.org/wiki/Shoelace_formula
 */
export function getPolygonSignedArea(points: NumericArray, options: PolygonParams = {}): number {
  const {start = 0, end = points.length, plane = 'xy'} = options;
  const dim = options.size || 2;
  let area = 0;
  const i0 = DimIndex[plane[0]];
  const i1 = DimIndex[plane[1]];

  for (let i = start, j = end - dim; i < end; i += dim) {
    area += (points[i + i0] - points[j + i0]) * (points[i + i1] + points[j + i1]);
    j = i;
  }
  return area / 2;
}

/**
 * Calls the visitor callback for each segment in the polygon.
 * @param points An array that represents points of the polygon
 * @param visitor A callback to call for each segment.
 * @param options Parameters of the polygon.
 */
export function forEachSegmentInPolygon(
  points: NumericArray,
  visitor: SegmentVisitorFlat,
  options: PolygonParams = {}
): void {
  const {start = 0, end = points.length, size = 2, isClosed} = options;

  const numPoints = (end - start) / size;
  for (let i = 0; i < numPoints - 1; ++i) {
    visitor(
      points[start + i * size],
      points[start + i * size + 1],
      points[start + (i + 1) * size],
      points[start + (i + 1) * size + 1],
      i,
      i + 1
    );
  }

  const endPointIndex = start + (numPoints - 1) * size;
  const isClosedEx =
    isClosed ||
    (equals(points[start], points[endPointIndex]) &&
      equals(points[start + 1], points[endPointIndex + 1]));

  if (!isClosedEx) {
    visitor(
      points[endPointIndex],
      points[endPointIndex + 1],
      points[start],
      points[start + 1],
      numPoints - 1,
      0
    );
  }
}

function reversePolygon(
  points: NumericArray,
  options: {start?: number; end?: number; size?: number}
): void {
  const {start = 0, end = points.length, size = 2} = options;

  const numPoints = (end - start) / size;
  const numSwaps = Math.floor(numPoints / 2);
  for (let i = 0; i < numSwaps; ++i) {
    const b1 = start + i * size;
    const b2 = start + (numPoints - 1 - i) * size;
    for (let j = 0; j < size; ++j) {
      const tmp = points[b1 + j];
      points[b1 + j] = points[b2 + j];
      points[b2 + j] = tmp;
    }
  }
}

/**
 * Checks winding direction of the polygon and reverses the polygon in case of opposite winding direction.
 * Note: points are modified in-place.
 * @param points Array of points that represent the polygon.
 * @param direction Requested winding direction. 1 is for clockwise, -1 for counterclockwise winding direction.
 * @param options Parameters of the polygon.
 * @return Returns true if the winding direction was changed.
 */
export function modifyPolygonWindingDirectionPoints(
  points: PointsArray,
  direction: number,
  options: PolygonParams = {}
): boolean {
  const currentDirection = getPolygonWindingDirectionPoints(points, options);
  if (currentDirection !== direction) {
    points.reverse();
    return true;
  }
  return false;
}

/**
 * Returns winding direction of the polygon.
 * @param points Array of points that represent the polygon.
 * @param options Parameters of the polygon.
 * @returns Winding direction of the polygon.
 */
export function getPolygonWindingDirectionPoints(
  points: PointsArray,
  options: PolygonParams = {}
): number {
  return Math.sign(getPolygonSignedAreaPoints(points, options));
}

/**
 * Returns signed area of the polygon.
 * @param points Array of points that represent the polygon.
 * @param options Parameters of the polygon.
 * @returns Signed area of the polygon.
 */
export function getPolygonSignedAreaPoints(
  points: PointsArray,
  options: PolygonParams = {}
): number {
  // https://en.wikipedia.org/wiki/Shoelace_formula
  const {start = 0, end = points.length, plane = 'xy'} = options;
  let area = 0;
  const i0 = DimIndex[plane[0]];
  const i1 = DimIndex[plane[1]];

  for (let i = start, j = end - 1; i < end; ++i) {
    area += (points[i][i0] - points[j][i0]) * (points[i][i1] + points[j][i1]);
    j = i;
  }
  return area / 2;
}

/**
 * Calls visitor callback for each segment in the polygon.
 * @param points Array of points that represent the polygon.
 * @param visitor A callback to call for each segment.
 * @param options Parameters of the polygon.
 */
export function forEachSegmentInPolygonPoints(
  points: PointsArray,
  visitor: SegmentVisitorPoints,
  options: PolygonParams = {}
): void {
  const {start = 0, end = points.length, isClosed} = options;
  for (let i = start; i < end - 1; ++i) {
    visitor(points[i], points[i + 1], i, i + 1);
  }

  const isClosedEx = isClosed || equals(points[end - 1], points[0]);
  if (!isClosedEx) {
    visitor(points[end - 1], points[0], end - 1, 0);
  }
}
