import { Element } from "./elements";

export interface Vector {
  readonly x: number;
  readonly y: number;
}
export const Vector = {
  equals(a: Vector, b: Vector) {
    return a.x === b.x && a.y === b.y;
  },
  length({ x, y }: Vector) {
    return Math.sqrt(x * x + y * y);
  },
  normalize({ x, y }: Vector) {
    if (x === 0 && y === 0) {
      return { x, y };
    }
    const inverseLength = 1 / Math.sqrt(x * x + y * y);
    return { x: x * inverseLength, y: y * inverseLength };
  },
  dot({ x: x1, y: y1 }: Vector, { x: x2, y: y2 }: Vector): number {
    return x1 * x2 + y1 * y2;
  },
  cross2D({ x: x1, y: y1 }: Vector, { x: x2, y: y2 }: Vector) {
    return x1 * y2 - y1 * x2;
  },
};

export interface Size {
  readonly width: number;
  readonly height: number;
}

export interface Rect {
  readonly x: number;
  readonly y: number;
  readonly width: number;
  readonly height: number;
}

export const Rect = {
  center({ x, y, width, height }: Rect) {
    return { x: x + width / 2, y: y + height / 2 };
  },
};

export function boundsOf(element: Element): Rect {
  const { x, y } = element.position;
  const { width, height } = element.size;
  return { x, y, width, height };
}

function intersectRayFromRectangleCenter(sourceRect: Rect, rayTarget: Vector) {
  const isTargetInsideRect =
    sourceRect.width === 0 ||
    sourceRect.height === 0 ||
    (rayTarget.x > sourceRect.x &&
      rayTarget.x < sourceRect.x + sourceRect.width &&
      rayTarget.y > sourceRect.y &&
      rayTarget.y < sourceRect.y + sourceRect.height);

  const halfWidth = sourceRect.width / 2;
  const halfHeight = sourceRect.height / 2;
  const center = {
    x: sourceRect.x + halfWidth,
    y: sourceRect.y + halfHeight,
  };
  if (isTargetInsideRect) {
    return center;
  }

  const direction = Vector.normalize({
    x: rayTarget.x - center.x,
    y: rayTarget.y - center.y,
  });

  const rightDirection = { x: Math.abs(direction.x), y: direction.y };
  const isHorizontal =
    Vector.cross2D({ x: halfWidth, y: -halfHeight }, rightDirection) > 0 &&
    Vector.cross2D({ x: halfWidth, y: halfHeight }, rightDirection) < 0;

  if (isHorizontal) {
    return {
      x: center.x + halfWidth * Math.sign(direction.x),
      y: center.y + (halfWidth * direction.y) / Math.abs(direction.x),
    };
  } else {
    return {
      x: center.x + (halfHeight * direction.x) / Math.abs(direction.y),
      y: center.y + halfHeight * Math.sign(direction.y),
    };
  }
}

export function isPolylineEqual(
  left: readonly Vector[],
  right: readonly Vector[]
) {
  if (left === right) {
    return true;
  }
  if (left.length !== right.length) {
    return false;
  }
  for (let i = 0; i < left.length; i++) {
    const a = left[i];
    const b = right[i];
    if (!(a.x === b.x && a.y === b.y)) {
      return false;
    }
  }
  return true;
}

export function computePolyline(
  source: Element,
  target: Element,
  vertices: readonly Vector[]
): Vector[] {
  const sourceRect = boundsOf(source);
  const targetRect = boundsOf(target);
  const startPoint = intersectRayFromRectangleCenter(
    sourceRect,
    vertices.length > 0 ? vertices[0] : Rect.center(targetRect)
  );
  const endPoint = intersectRayFromRectangleCenter(
    targetRect,
    vertices.length > 0
      ? vertices[vertices.length - 1]
      : Rect.center(sourceRect)
  );
  return [startPoint, ...vertices, endPoint];
}

export function computePolylineLength(polyline: readonly Vector[]): number {
  let previous: Vector;
  return polyline.reduce((acc, point) => {
    const segmentLength = previous
      ? Vector.length({ x: point.x - previous.x, y: point.y - previous.y })
      : 0;
    previous = point;
    return acc + segmentLength;
  }, 0);
}

export function getPointAlongPolyline(
  polyline: readonly Vector[],
  offset: number
): Vector {
  if (polyline.length === 0) {
    throw new Error("Cannot compute a point for empty polyline");
  }
  if (offset < 0) {
    return polyline[0];
  }
  let currentOffset = 0;
  for (let i = 1; i < polyline.length; i++) {
    const previous = polyline[i - 1];
    const point = polyline[i];
    const segment = { x: point.x - previous.x, y: point.y - previous.y };
    const segmentLength = Vector.length(segment);
    const newOffset = currentOffset + segmentLength;
    if (offset < newOffset) {
      const leftover = (offset - currentOffset) / segmentLength;
      return {
        x: previous.x + leftover * segment.x,
        y: previous.y + leftover * segment.y,
      };
    } else {
      currentOffset = newOffset;
    }
  }
  return polyline[polyline.length - 1];
}

export function findNearestSegmentIndex(
  polyline: readonly Vector[],
  location: Vector
): number {
  let minDistance = Infinity;
  let foundIndex = 0;

  for (let i = 0; i < polyline.length - 1; i++) {
    const pivot = polyline[i];
    const next = polyline[i + 1];

    const target = { x: location.x - pivot.x, y: location.y - pivot.y };
    const segment = { x: next.x - pivot.x, y: next.y - pivot.y };
    const segmentLength = Vector.length(segment);

    const projectionToSegment = Vector.dot(target, segment) / segmentLength;
    if (projectionToSegment < 0 || projectionToSegment > segmentLength) {
      continue;
    }

    const distanceToSegment =
      Math.abs(Vector.cross2D(target, segment)) / segmentLength;
    if (distanceToSegment < minDistance) {
      minDistance = distanceToSegment;
      foundIndex = i;
    }
  }
  return foundIndex;
}

export function findElementAtPoint(
  elements: readonly Element[],
  point: Vector
): Element | undefined {
  for (let i = elements.length - 1; i >= 0; i--) {
    const element = elements[i];
    const { x, y, width, height } = boundsOf(element);

    if (element.temporary) {
      continue;
    }

    if (
      point.x >= x &&
      point.x <= x + width &&
      point.y >= y &&
      point.y <= y + height
    ) {
      return element;
    }
  }
  return undefined;
}

export function computeGrouping(
  elements: readonly Element[]
): Map<string, Element[]> {
  const grouping = new Map<string, Element[]>();
  for (const element of elements) {
    const group = element.group;
    if (typeof group === "string") {
      let children = grouping.get(group);
      if (!children) {
        children = [];
        grouping.set(group, children);
      }
      children.push(element);
    }
  }
  return grouping;
}
