import { rgbaToHsla } from "./ColorUtils"

import type { Bounds, PathCommands, Quad } from "./common"

export type PathBounds = Bounds & {
  leftmostXForY: { [key: string]: number }
  rightmostXForY: { [key: string]: number }
  topmostYForX: { [key: string]: number }
  bottommostYForX: { [key: string]: number }
}

export interface LineStyle {
  color?: string
  pattern?: LinePattern
}

export interface BoxStyle {
  fillColor?: string
  hatchColor?: string
}

export const enum LinePattern {
  Solid = "solid",
  Dotted = "dotted",
  Dashed = "dashed",
}

export function drawPathWithLineStyle(
  context: CanvasRenderingContext2D,
  path: Path2D,
  lineStyle?: LineStyle,
  lineWidth = 1,
) {
  if (lineStyle && lineStyle.color) {
    context.save()
    context.translate(0.5, 0.5)
    context.lineWidth = lineWidth
    if (lineStyle.pattern === LinePattern.Dashed) {
      context.setLineDash([3, 3])
    }
    if (lineStyle.pattern === LinePattern.Dotted) {
      context.setLineDash([2, 2])
    }
    context.strokeStyle = lineStyle.color
    context.stroke(path)
    context.restore()
  }
}

export function fillPathWithBoxStyle(
  context: CanvasRenderingContext2D,
  path: Path2D,
  bounds: PathBounds,
  angle: number,
  boxStyle?: BoxStyle,
) {
  if (!boxStyle) {
    return
  }

  context.save()
  if (boxStyle.fillColor) {
    context.fillStyle = boxStyle.fillColor
    context.fill(path)
  }
  if (boxStyle.hatchColor) {
    hatchFillPath(context, path, bounds, 10, boxStyle.hatchColor, angle, false)
  }
  context.restore()
}

export function buildPath(
  commands: Array<string | number>,
  bounds: PathBounds,
  emulationScaleFactor: number,
): Path2D {
  let commandsIndex = 0

  function extractPoints(count: number): number[] {
    const points = []

    for (let i = 0; i < count; ++i) {
      const x = Math.round((commands[commandsIndex++] as number) * emulationScaleFactor)
      bounds.maxX = Math.max(bounds.maxX, x)
      bounds.minX = Math.min(bounds.minX, x)

      const y = Math.round((commands[commandsIndex++] as number) * emulationScaleFactor)
      bounds.maxY = Math.max(bounds.maxY, y)
      bounds.minY = Math.min(bounds.minY, y)

      bounds.leftmostXForY[y] = Math.min(bounds.leftmostXForY[y] || Number.MAX_VALUE, x)
      bounds.rightmostXForY[y] = Math.max(bounds.rightmostXForY[y] || Number.MIN_VALUE, x)
      bounds.topmostYForX[x] = Math.min(bounds.topmostYForX[x] || Number.MAX_VALUE, y)
      bounds.bottommostYForX[x] = Math.max(
        bounds.bottommostYForX[x] || Number.MIN_VALUE,
        y,
      )

      bounds.allPoints.push({ x, y })

      points.push(x, y)
    }

    return points
  }

  const commandsLength = commands.length
  const path = new Path2D()
  while (commandsIndex < commandsLength) {
    switch (commands[commandsIndex++]) {
      case "M":
        path.moveTo(...(extractPoints(1) as [number, number]))
        break
      case "L":
        path.lineTo(...(extractPoints(1) as [number, number]))
        break
      case "C":
        path.bezierCurveTo(
          ...(extractPoints(3) as [number, number, number, number, number, number]),
        )
        break
      case "Q":
        path.quadraticCurveTo(...(extractPoints(2) as [number, number, number, number]))
        break
      case "Z":
        path.closePath()
        break
    }
  }

  return path
}

export function emptyBounds(): PathBounds {
  const bounds = {
    minX: Number.MAX_VALUE,
    minY: Number.MAX_VALUE,
    maxX: -Number.MAX_VALUE,
    maxY: -Number.MAX_VALUE,
    leftmostXForY: {},
    rightmostXForY: {},
    topmostYForX: {},
    bottommostYForX: {},
    allPoints: [],
  }
  return bounds
}

export function applyMatrixToPoint(
  point: { x: number; y: number },
  matrix: DOMMatrix,
): { x: number; y: number } {
  let domPoint = new DOMPoint(point.x, point.y)
  domPoint = domPoint.matrixTransform(matrix)
  return { x: domPoint.x, y: domPoint.y }
}

const HATCH_LINE_LENGTH = 5
const HATCH_LINE_GAP = 3
let hatchLinePattern: CanvasPattern
let hatchLineColor = ""

/**
 * Draw line hatching at a 45 degree angle for a given
 * path.
 *   __________
 *   |\  \  \ |
 *   | \  \  \|
 *   |  \  \  |
 *   |\  \  \ |
 *   **********
 */
export function hatchFillPath(
  context: CanvasRenderingContext2D,
  path: Path2D,
  bounds: Bounds,
  delta: number,
  color: string,
  rotationAngle: number,
  flipDirection: boolean | undefined,
) {
  // Make the bounds be at most the canvas size if it is bigger in any direction.
  // Making the bounds bigger than the canvas is useless as what's drawn there won't be visible.
  if (
    context.canvas.width < bounds.maxX - bounds.minX ||
    context.canvas.height < bounds.maxY - bounds.minY
  ) {
    bounds = {
      minX: 0,
      maxX: context.canvas.width,
      minY: 0,
      maxY: context.canvas.height,
      allPoints: [],
    }
  }

  // If we haven't done it yet, initialize an offscreen canvas used to create the dashed line repeated pattern.
  if (!hatchLinePattern || color !== hatchLineColor) {
    hatchLineColor = color

    const offscreenCanvas = document.createElement("canvas")
    offscreenCanvas.width = delta
    offscreenCanvas.height = HATCH_LINE_LENGTH + HATCH_LINE_GAP

    const offscreenCtx = offscreenCanvas.getContext("2d") as CanvasRenderingContext2D
    offscreenCtx.clearRect(0, 0, offscreenCanvas.width, offscreenCanvas.height)
    offscreenCtx.rect(0, 0, 1, HATCH_LINE_LENGTH)
    offscreenCtx.fillStyle = color
    offscreenCtx.fill()

    hatchLinePattern = context.createPattern(offscreenCanvas, "repeat") as CanvasPattern
  }

  context.save()

  const matrix = new DOMMatrix()
  hatchLinePattern.setTransform(
    matrix.scale(flipDirection ? -1 : 1, 1).rotate(0, 0, -45 + rotationAngle),
  )

  context.fillStyle = hatchLinePattern
  context.fill(path)

  context.restore()
}

/**
 * Given a quad, create the corresponding path object. This also accepts a list of quads to clip from the resulting
 * path.
 */
export function createPathForQuad(
  outerQuad: Quad,
  quadsToClip: Quad[],
  bounds: PathBounds,
  emulationScaleFactor: number,
) {
  let commands = [
    "M",
    outerQuad.p1.x,
    outerQuad.p1.y,
    "L",
    outerQuad.p2.x,
    outerQuad.p2.y,
    "L",
    outerQuad.p3.x,
    outerQuad.p3.y,
    "L",
    outerQuad.p4.x,
    outerQuad.p4.y,
  ]
  for (const quad of quadsToClip) {
    commands = [
      ...commands,
      "L",
      quad.p4.x,
      quad.p4.y,
      "L",
      quad.p3.x,
      quad.p3.y,
      "L",
      quad.p2.x,
      quad.p2.y,
      "L",
      quad.p1.x,
      quad.p1.y,
      "L",
      quad.p4.x,
      quad.p4.y,
      "L",
      outerQuad.p4.x,
      outerQuad.p4.y,
    ]
  }
  commands.push("Z")

  return buildPath(commands, bounds, emulationScaleFactor)
}

export function parseHexa(hexa: string): Array<number> {
  return (hexa.match(/#(\w\w)(\w\w)(\w\w)(\w\w)/) || [])
    .slice(1)
    .map(c => parseInt(c, 16) / 255)
}

export function formatRgba(rgba: number[], colorFormat: "rgb" | "hsl"): string {
  if (colorFormat === "rgb") {
    const [r, g, b, a] = rgba
    // rgb(r g b [ / a])
    return `rgb(${(r * 255).toFixed()} ${(g * 255).toFixed()} ${(b * 255).toFixed()}${
      a === 1 ? "" : " / " + Math.round(a * 100) / 100
    })`
  }

  if (colorFormat === "hsl") {
    const [h, s, l, a] = rgbaToHsla(rgba)
    // hsl(hdeg s l [ / a])
    return `hsl(${Math.round(h * 360)}deg ${Math.round(s * 100)} ${Math.round(l * 100)}${
      a === 1 ? "" : " / " + Math.round(a * 100) / 100
    })`
  }

  throw new Error("NOT_REACHED")
}

export function formatColor(hexa: string, colorFormat: string): string {
  if (colorFormat === "rgb" || colorFormat === "hsl") {
    return formatRgba(parseHexa(hexa), colorFormat)
  }

  if (hexa.endsWith("FF")) {
    // short hex if no alpha
    return hexa.substr(0, 7)
  }

  return hexa
}

export function drawPath(
  context: CanvasRenderingContext2D,
  commands: PathCommands,
  fillColor: string | undefined,
  outlineColor: string | undefined,
  outlinePattern: LinePattern | undefined,
  bounds: PathBounds,
  emulationScaleFactor: number,
) {
  context.save()
  const path = buildPath(commands, bounds, emulationScaleFactor)
  if (fillColor) {
    context.fillStyle = fillColor
    context.fill(path)
  }
  if (outlineColor) {
    if (outlinePattern === LinePattern.Dashed) {
      context.setLineDash([3, 3])
    }
    if (outlinePattern === LinePattern.Dotted) {
      context.setLineDash([2, 2])
    }
    context.lineWidth = 2
    context.strokeStyle = outlineColor
    context.stroke(path)
  }
  context.restore()
  return path
}
