import { type PathArray } from '@antv/util';
import type { Point, Vector2 } from '../../types';
import { getLinesIntersection } from '../../utils/line';
import { getClosedSpline } from '../../utils/path';
import { isCollinear, sortByClockwise } from '../../utils/point';
import { add, angle, normalize, perpendicular, scale, subtract, toVector2, toVector3 } from '../../utils/vector';
import type { HullOptions } from '../hull';

/**
 * <zh/> 计算 Hull 路径
 *
 * <en/> Compute Hull Path
 * @param points - <zh/> 顶点列表 | <en/> Vertices of Hull
 * @param padding - <zh/> 内边距 | <en/> padding
 * @param corner - <zh/> 拐角类型，目前支持 'sharp'、'rounded' 和 'smooth' | <en/> Corner type, currently supports 'sharp', 'rounded' and 'smooth'
 * @returns <zh/> Hull 路径 | <en/> Hull Path
 */
export function computeHullPath(points: Point[], padding: number, corner: 'rounded' | 'smooth' | 'sharp'): PathArray {
  if (points.length === 1) return genSinglePointHullPath(points[0], padding, corner);
  if (points.length === 2) return genTwoPointsHullPath(points, padding, corner);
  if (points.length === 3) {
    const [p1, p2, p3] = sortByClockwise(points);
    if (isCollinear(p1, p2, p3)) return genTwoPointsHullPath([p1, p3], padding, corner);
  }

  switch (corner) {
    case 'smooth':
      return genMultiPointsSmoothHull(points, padding);
    case 'sharp':
      return genMultiPointsSharpHull(points, padding);
    case 'rounded':
    default:
      return genMultiPointsRoundedHull(points, padding);
  }
}

/**
 * <zh/> 生成单点 Hull 路径
 *
 * <en/> Generate Hull Path for a single point
 * @param point - <zh/> 单点 | <en/> Single point
 * @param padding - <zh/> 内边距 | <en/> Padding
 * @param corner - <zh/> 拐角类型 | <en/> Corner type
 * @returns <zh/> 单点 Hull 路径 | <en/> Single point Hull Path
 */
const genSinglePointHullPath = (point: Point, padding: number, corner: HullOptions['corner']): PathArray => {
  if (corner === 'sharp')
    return [
      ['M', point[0] - padding, point[1] - padding],
      ['L', point[0] + padding, point[1] - padding],
      ['L', point[0] + padding, point[1] + padding],
      ['L', point[0] - padding, point[1] + padding],
      ['Z'],
    ];
  const arcData: [number, number, number, number, number] = [padding, padding, 0, 0, 0];
  return [
    ['M', point[0], point[1] - padding],
    ['A', ...arcData, point[0], point[1] + padding],
    ['A', ...arcData, point[0], point[1] - padding],
  ];
};

/**
 * <zh/> 生成两点 Hull 路径
 *
 * <en/> Generate Hull Path for two points
 * @param points - <zh/> 两点 | <en/> Two points
 * @param padding - <zh/> 内边距 | <en/> Padding
 * @param corner - <zh/> 拐角类型 | <en/> Corner type
 * @returns <zh/> 两点 Hull 路径 | <en/> Two points Hull Path
 */
const genTwoPointsHullPath = (points: Point[], padding: number, corner: HullOptions['corner']): PathArray => {
  const arcData: [number, number, number, number, number] = [padding, padding, 0, 0, 0];

  const point1 =
    corner === 'sharp' ? add(points[0], scale(normalize(subtract(points[0], points[1])), padding)) : points[0];
  const point2 =
    corner === 'sharp' ? add(points[1], scale(normalize(subtract(points[1], points[0])), padding)) : points[1];

  const offsetVector = scale(normalize(perpendicular(subtract(point1, point2) as Vector2, false)), padding);
  const invOffsetVector = scale(offsetVector, -1);

  const prev = add(point1, offsetVector);
  const current = add(point2, offsetVector);
  const p2 = add(point2, invOffsetVector);
  const p3 = add(point1, invOffsetVector);

  if (corner === 'sharp') {
    return [['M', prev[0], prev[1]], ['L', current[0], current[1]], ['L', p2[0], p2[1]], ['L', p3[0], p3[1]], ['Z']];
  }

  return [
    ['M', prev[0], prev[1]],
    ['L', current[0], current[1]],
    ['A', ...arcData, p2[0], p2[1]],
    ['L', p3[0], p3[1]],
    ['A', ...arcData, prev[0], prev[1]],
  ];
};

/**
 * <zh/> 生成多点 Hull 路径且拐角为圆角
 *
 * <en/> Generate Hull Path for multiple points with rounded corners
 * @param points - <zh/> 形成 Hull 的点集 | <en/> Points that form the Hull
 * @param padding - <zh/> 内边距 | <en/> Padding
 * @returns <zh/> 圆角外壳路径 | <en/> Rounded hull path
 */
const genMultiPointsRoundedHull = (points: Point[], padding: number): PathArray => {
  const segments = sortByClockwise(points).map((current, i) => {
    const prev2Index = (i - 2 + points.length) % points.length;
    const prevIndex = (i - 1 + points.length) % points.length;
    const nextIndex = (i + 1) % points.length;
    const prev2 = points[prev2Index];
    const prev = points[prevIndex];
    const next = points[nextIndex];

    const v0 = subtract(prev2, prev) as Vector2;
    const v1 = subtract(prev, current) as Vector2;
    const v2 = subtract(current, next) as Vector2;

    // 判断是否为凹角 ｜ Determine if it is a concave angle
    const isConcave = (v1: Vector2, v2: Vector2): boolean => {
      return angle(v1, v2, true) < Math.PI;
    };
    const concavePrev = isConcave(v0, v1);
    const concaveNext = isConcave(v1, v2);

    const offsetVector = (v: Vector2) => scale(normalize(perpendicular(v, false)), padding);
    const offset = offsetVector(v1);
    return [
      {
        p: toVector2(concavePrev ? add(prev, offsetVector(v0)) : add(prev, offset)),
        concave: concavePrev && prev,
      },
      {
        p: toVector2(concaveNext ? add(current, offsetVector(v2)) : add(current, offset)),
        concave: concaveNext && current,
      },
    ];
  });

  const arcData = [padding, padding, 0, 0, 0];
  const startIndex = segments.findIndex(
    (segment, i) =>
      !segments[(i - 1 + segments.length) % segments.length][0].concave &&
      !segments[(i - 1 + segments.length) % segments.length][1].concave &&
      !segment[0].concave &&
      !segment[0].concave &&
      !segment[1].concave,
  );
  const sortedSegments = segments.slice(startIndex).concat(segments.slice(0, startIndex));
  let concavePoints: Point[] = [];
  return sortedSegments.flatMap((segment, i) => {
    const pathFragment = [];
    const lastSegment = sortedSegments[segments.length - 1];
    if (i === 0) pathFragment.push(['M', ...lastSegment[1].p]);
    if (!segment[0].concave) {
      pathFragment.push(['A', ...arcData, ...segment[0].p]);
    } else {
      concavePoints.push(segment[0].p, segment[1].p);
    }
    if (!segment[1].concave) {
      pathFragment.push(['L', ...segment[1].p]);
    } else {
      concavePoints.unshift(segment[1].p);
    }
    if (concavePoints.length === 3) {
      pathFragment.pop();
      pathFragment.push(['C', ...concavePoints.flat()]);
      concavePoints = [];
    }
    return pathFragment;
  }) as PathArray;
};

/**
 * <zh/> 生成多点 Hull 路径且拐角为平滑
 *
 * <en/> Generate Hull Path for multiple points with smooth corners
 * @param points - <zh/> 形成 Hull 的点集 | <en/> Points that form the Hull
 * @param padding - <zh/> 内边距 | <en/> Padding
 * @returns <zh/> 平滑外壳路径 | <en/> Smooth hull path
 */
const genMultiPointsSmoothHull = (points: Point[], padding: number): PathArray => {
  const hullPoints = sortByClockwise(points).map((p, i) => {
    const pNext = points[(i + 1) % points.length];
    return { p, v: normalize(subtract(pNext, p)) };
  });

  // Compute the expanded hull points, and the nearest prior control point for each.
  hullPoints.forEach((hp, i) => {
    const priorIndex = i > 0 ? i - 1 : points.length - 1;
    const prevV = hullPoints[priorIndex].v as Vector2;
    const extensionVec = normalize(add(prevV, scale(hp.v, angle(prevV, hp.v, true) < Math.PI ? 1 : -1)));
    hp.p = add(hp.p, scale(extensionVec, padding));
  });

  return getClosedSpline(hullPoints.map((obj) => obj.p));
};

/**
 * <zh/> 生成多点 Hull 路径且拐角为尖锐
 *
 * <en/> Generate Hull Path for multiple points with sharp corners
 * @param points - <zh/> 形成 Hull 的点集 | <en/> Points that form the Hull
 * @param padding - <zh/> 内边距 | <en/> Padding
 * @returns <zh/> 锐角外壳路径 | <en/> Sharp hull path
 */
const genMultiPointsSharpHull = (points: Point[], padding: number): PathArray => {
  const segments = points.map((current, i) => {
    const prev = points[i === 0 ? points.length - 1 : i - 1];
    const offset = toVector3(scale(normalize(perpendicular(subtract(prev, current) as Vector2, false)), padding));
    return [add(prev, offset), add(current, offset)];
  });

  const arr = segments.flat();

  const vertices = arr
    .map((_, i) => {
      if (i % 2 === 0) return null;
      const l1 = [arr[(i - 1) % arr.length], arr[i % arr.length]] as [Point, Point];
      const l2 = [arr[(i + 1) % arr.length], arr[(i + 2) % arr.length]] as [Point, Point];
      return getLinesIntersection(l1, l2, true);
    })
    .filter(Boolean) as Point[];
  return vertices.map((point, i) => [i === 0 ? 'M' : 'L', point[0], point[1]]).concat([['Z']]) as PathArray;
};
