import { Point, BBox } from "../vendor/bezier-js";
import { getBoundingBoxForBeziers } from "./bounding-box";
import * as colors from "./colors";
import { pathToShapes } from "./path";
import { normalizePoint, subtractPoints } from "./point";
import type { SvgSymbolElement } from "./svg-symbol";

const SPEC_LAYER_ID_RE = /^specs.*/i;

export type PointWithNormal = {
  point: Point;
  normal: Point;
};

type AttachmentPointSpecs = {
  anchor: PointWithNormal[];
  tail: PointWithNormal[];
  leg: PointWithNormal[];
  arm: PointWithNormal[];
  horn: PointWithNormal[];
  crown: PointWithNormal[];
  wildcard: PointWithNormal[];
};

type FullSpecs = AttachmentPointSpecs & {
  nesting: BBox[];
};

export type Specs = Partial<FullSpecs>;

export type AttachmentPointType = keyof AttachmentPointSpecs;

export type AttachmentPoint = PointWithNormal & {
  type: AttachmentPointType;
  index: number;
};

export const ATTACHMENT_POINT_TYPES: AttachmentPointType[] = [
  "anchor",
  "tail",
  "leg",
  "arm",
  "horn",
  "crown",
  "wildcard",
];

const ATTACHMENT_POINT_SET = new Set(ATTACHMENT_POINT_TYPES);

export function isAttachmentPointType(
  value: any
): value is AttachmentPointType {
  return ATTACHMENT_POINT_SET.has(value);
}

export function* iterAttachmentPoints(specs: Specs): Iterable<AttachmentPoint> {
  for (let type of ATTACHMENT_POINT_TYPES) {
    const points = specs[type];
    if (points) {
      let index = 0;
      for (let point of points) {
        yield { ...point, type, index };
        index += 1;
      }
    }
  }
}

const NUM_ARROW_POINTS = 4;
const ARROW_TOP_POINT_IDX = 0;
const ARROW_BOTTOM_POINT_IDX = 2;

function getArrowPoints(path: string): PointWithNormal[] {
  const shapes = pathToShapes(path);
  const points: PointWithNormal[] = [];

  for (let shape of shapes) {
    if (shape.length !== NUM_ARROW_POINTS) {
      throw new Error(
        `Expected arrow to have ${NUM_ARROW_POINTS} points, not ${shape.length}!`
      );
    }
    const point = shape[ARROW_BOTTOM_POINT_IDX].get(0.0);
    const normal = normalizePoint(
      subtractPoints(shape[ARROW_TOP_POINT_IDX].get(0.0), point)
    );
    points.push({
      point,
      normal,
    });
  }

  return points;
}

function getBoundingBoxes(path: string): BBox[] {
  const shapes = pathToShapes(path);
  const bboxes: BBox[] = [];

  for (let shape of shapes) {
    bboxes.push(getBoundingBoxForBeziers(shape));
  }

  return bboxes;
}

/**
 * Sort points from top to bottom, left to right.
 */
function sortPoints(a: PointWithNormal, b: PointWithNormal): number {
  if (a.point.y < b.point.y) return -1;
  if (a.point.y > b.point.y) return 1;
  if (a.point.x < b.point.x) return -1;
  if (a.point.x > b.point.x) return 1;
  return 0;
}

function sortedPoints(points: PointWithNormal[]): PointWithNormal[] {
  const copy = [...points];
  copy.sort(sortPoints);
  return copy;
}

function concat<T>(first: T[] | undefined, second: T[]): T[] {
  return first ? [...first, ...second] : second;
}

const ATTACHMENT_COLOR_MAP = new Map(
  ATTACHMENT_POINT_TYPES.map((type) => [
    colors.ATTACHMENT_POINT_COLORS[type],
    type,
  ])
);

function updateSpecs(fill: string, path: string, specs: Specs): Specs {
  const attachmentType = ATTACHMENT_COLOR_MAP.get(fill);

  if (attachmentType) {
    return {
      ...specs,
      [attachmentType]: sortedPoints(
        concat(specs[attachmentType], getArrowPoints(path))
      ),
    };
  }

  if (fill === colors.NESTING_BOUNDING_BOX_COLOR) {
    return {
      ...specs,
      nesting: concat(specs.nesting, getBoundingBoxes(path)),
    };
  }

  console.log(
    `Not sure what to do with specs path with fill "${fill}", ignoring it.`
  );

  return specs;
}

function getSpecs(layers: SvgSymbolElement[]): Specs {
  let specs: Specs = {};

  for (let layer of layers) {
    if (layer.tagName !== "path") {
      throw new Error(
        `Found an unexpected <${layer.tagName}> in the specs layer!`
      );
    }
    const { fill, d } = layer.props;
    if (!(fill && d)) {
      throw new Error(
        `Specs layer does not contain 'fill' and/or 'd' attributes!`
      );
    }
    specs = updateSpecs(fill, d, specs);
  }

  return specs;
}

export function extractSpecs(
  layers: SvgSymbolElement[]
): [Specs | undefined, SvgSymbolElement[]] {
  const layersWithoutSpecs: SvgSymbolElement[] = [];
  let specs: Specs | undefined = undefined;

  const setSpecs = (s: Specs | undefined) => {
    if (s) {
      if (specs) {
        throw new Error("Duplicate specs layers found!");
      }
      specs = s;
    }
  };

  for (let layer of layers) {
    switch (layer.tagName) {
      case "g":
        const { id } = layer.props;
        if (id && SPEC_LAYER_ID_RE.test(id)) {
          setSpecs(getSpecs(layer.children));
        } else {
          let [s, children] = extractSpecs(layer.children);
          setSpecs(s);
          layersWithoutSpecs.push({
            ...layer,
            children,
          });
        }
        break;
      case "path":
        layersWithoutSpecs.push(layer);
        break;
    }
  }

  return [specs, layersWithoutSpecs];
}
