// Local canvas rendering.
// Emit input events and receive draw calls seperately - these must be piped
// together externally if desired.

import { line, curveLinear } from "d3-shape"

import { canvas } from "./elements.js"

const SVG_URL = "http://www.w3.org/2000/svg"

// TODO: switch to curve interpolation that respects mouse points based on velocity
const lineFn = line()
  .x((d) => d[0])
  .y((d) => d[1])
  .curve(curveLinear)

const pathGroupElems = new Map()

let stroke_colour = "blue"
export let stroke_radius = 1
export const MIN_STROKE_RADIUS = 0.1
export const MAX_STROKE_RADIUS = 3.9

const MAX_POINT_DISTANCE = 5
const MAX_PRESSURE_DELTA = 0.05

// Interpolate a path so that:
// - The distance between two adjacent points is capped at MAX_POINT_DISTANCE.
// - The pressure delta between two adjacent points is capped at
//   MAX_PRESSURE_DELTA
// If paths are too choppy, try decreasing these constants.
const smoothPath = ([...path]) => {
  // Apply MAX_POINT_DISTANCE.
  for (let i = 1; i < path.length; i++) {
    const dx = path[i][0] - path[i - 1][0]
    const dy = path[i][1] - path[i - 1][1]
    const dw = path[i][2] - path[i - 1][2]
    const distance = Math.hypot(dx, dy)
    const segmentsToSplit = Math.ceil(distance / MAX_POINT_DISTANCE)
    const newPoints = []
    for (let j = 1; j < segmentsToSplit; j++) {
      newPoints.push([
        path[i - 1][0] + (dx / segmentsToSplit) * j,
        path[i - 1][1] + (dy / segmentsToSplit) * j,
        path[i - 1][2] + (dw / segmentsToSplit) * j,
        path[i - 1][3],
        path[i - 1][4],
      ])
    }
    path.splice(i, 0, ...newPoints)
    i += newPoints.length
  }

  // Apply MAX_PRESSURE_DELTA.
  for (let i = 1; i < path.length; i++) {
    const dx = path[i][0] - path[i - 1][0]
    const dy = path[i][1] - path[i - 1][1]
    const dw = path[i][2] - path[i - 1][2]
    const segmentsToSplit = Math.ceil(dw / MAX_PRESSURE_DELTA)
    const newPoints = []
    for (let j = 1; j < segmentsToSplit; j++) {
      newPoints.push([
        path[i - 1][0] + (dx / segmentsToSplit) * j,
        path[i - 1][1] + (dy / segmentsToSplit) * j,
        path[i - 1][2] + (dw / segmentsToSplit) * j,
        path[i - 1][3],
        path[i - 1][4],
      ])
    }
    path.splice(i, 0, ...newPoints)
    i += newPoints.length
  }
  return path
}

const getStrokeRadius = (pressure, radius) => {
  return (
    MIN_STROKE_RADIUS +
    (radius + pressure) * (MAX_STROKE_RADIUS - MIN_STROKE_RADIUS)
  )
}

export const input = new EventTarget()

const createPathElem = (d, width) => {
  const pathGroupElem = document.createElementNS(SVG_URL, "path")
  pathGroupElem.setAttribute("stroke-width", width)
  pathGroupElem.setAttribute("d", d)
  return pathGroupElem
}

export const renderPath = (id, points) => {
  points = points.filter(([x]) => x != null)
  let colour = ""
  let radius = 0

  // Split up points into completely non-erased segments.
  let segments = [[]]
  for (const point of points) {
    colour = point[3]
    radius = point[4]
    if (point[5] != false) {
      segments[segments.length - 1].push(point)
    } else {
      segments.push([])
    }
  }
  segments = segments.filter((a) => a.length > 0)

  let pathGroupElem = pathGroupElems.get(id)

  if (segments.length == 0) {
    if (pathGroupElem != null) {
      canvas.removeChild(pathGroupElem)
      pathGroupElems.delete(id)
    }
    return
  }

  if (pathGroupElem == null) {
    pathGroupElem = document.createElementNS(SVG_URL, "g")
    pathGroupElem.setAttribute("stroke", colour)
    pathGroupElem.setAttribute("fill", "none")
    pathGroupElem.setAttribute("stroke-linecap", "round")
    pathGroupElem.setAttribute("pointer-events", "none")
    canvas.appendChild(pathGroupElem)
    pathGroupElems.set(id, pathGroupElem)
  }

  pathGroupElem.innerHTML = ""

  for (const subpath of segments) {
    if (subpath.length == 1) {
      const circleElem = document.createElementNS(SVG_URL, "circle")
      circleElem.setAttribute("stroke", "none")
      circleElem.setAttribute("fill", colour)
      circleElem.setAttribute("cx", subpath[0][0])
      circleElem.setAttribute("cy", subpath[0][1])
      circleElem.setAttribute("r", getStrokeRadius(subpath[0][2], radius))
      console.log(getStrokeRadius(subpath[0][2], radius))
      pathGroupElem.appendChild(circleElem)
    } else {
      // Further split up segments based on thickness.
      const subpath_ = smoothPath(subpath)
      let w = subpath_[0][2]
      for (let i = 1; i < subpath_.length; i++) {
        if (subpath_[i][2] != w) {
          const d = lineFn([...subpath_.splice(0, i), subpath_[0]])
          pathGroupElem.appendChild(
            createPathElem(d, getStrokeRadius(w, radius) * 2),
          )
          w = subpath_[0][2]
          i = 1
        }
      }
      const d = lineFn(subpath_)
      pathGroupElem.appendChild(
        createPathElem(d, getStrokeRadius(w, radius) * 2),
      )
    }
  }
}

export const clear = () => {
  pathGroupElems.clear()
  canvas.innerHTML = ""
}

// Necessary since buttons property is non standard on iOS versions < 13.2
function checkValidPointerEvent(e) {
  return e.buttons & 1 || e.pointerType === "touch"
}

const dispatchPointerEvent = (name) => (e) => {
  if (checkValidPointerEvent(e)) {
    input.dispatchEvent(new CustomEvent(name, { detail: e }))
  }
}

// Note that the PointerEvent is passed as the detail in these 'stroke events'.
canvas.addEventListener("pointerdown", dispatchPointerEvent("strokestart"))
canvas.addEventListener("pointerenter", dispatchPointerEvent("strokestart"))
canvas.addEventListener("pointerup", dispatchPointerEvent("strokeend"))
canvas.addEventListener("pointerleave", dispatchPointerEvent("strokeend"))
canvas.addEventListener("pointermove", dispatchPointerEvent("strokemove"))
canvas.addEventListener("touchmove", (e) => e.preventDefault())

export function setStrokeColour(colour) {
  stroke_colour = colour
}

export function getStrokeColour() {
  return stroke_colour
}

export function setStrokeRadius(radius) {
  stroke_radius = radius
}