import { Utils } from '@tldraw/core'
import { intersectCircleCircle, intersectCircleLineSegment } from '@tldraw/intersect'
import Vec from '@tldraw/vec'
import getStroke from 'perfect-freehand'
import { EASINGS } from '~constants'
import { getShapeStyle } from '../shared/shape-styles'
import type { ArrowShape, TldrawHandle } from '~types'

export function getArrowArcPath(
  start: TldrawHandle,
  end: TldrawHandle,
  circle: number[],
  bend: number
) {
  return [
    'M',
    start.point[0],
    start.point[1],
    'A',
    circle[2],
    circle[2],
    0,
    0,
    bend < 0 ? 0 : 1,
    end.point[0],
    end.point[1],
  ].join(' ')
}

export function getBendPoint(handles: ArrowShape['handles'], bend: number) {
  const { start, end } = handles

  const dist = Vec.dist(start.point, end.point)

  const midPoint = Vec.med(start.point, end.point)

  const bendDist = (dist / 2) * bend

  const u = Vec.uni(Vec.vec(start.point, end.point))

  const point = Vec.toFixed(
    Math.abs(bendDist) < 10 ? midPoint : Vec.add(midPoint, Vec.mul(Vec.per(u), bendDist))
  )

  return point
}

export function renderFreehandArrowShaft(shape: ArrowShape) {
  const { style, id } = shape

  const { start, end } = shape.handles

  const getRandom = Utils.rng(id)

  const strokeWidth = getShapeStyle(style).strokeWidth

  const startPoint = shape.decorations?.start
    ? Vec.nudge(start.point, end.point, strokeWidth)
    : start.point

  const endPoint = shape.decorations?.end
    ? Vec.nudge(end.point, start.point, strokeWidth)
    : end.point

  const stroke = getStroke([startPoint, endPoint], {
    size: strokeWidth,
    thinning: 0.618 + getRandom() * 0.2,
    easing: EASINGS.easeOutQuad,
    simulatePressure: true,
    streamline: 0,
    last: true,
  })

  const path = Utils.getSvgPathFromStroke(stroke)

  return path
}

export function renderCurvedFreehandArrowShaft(
  shape: ArrowShape,
  circle: number[],
  length: number,
  easing: (t: number) => number
) {
  const { style, id } = shape

  const { start, end } = shape.handles

  const getRandom = Utils.rng(id)

  const strokeWidth = getShapeStyle(style).strokeWidth

  const center = [circle[0], circle[1]]

  const radius = circle[2]

  const startPoint = shape.decorations?.start
    ? Vec.rotWith(start.point, center, strokeWidth / length)
    : start.point

  const endPoint = shape.decorations?.end
    ? Vec.rotWith(end.point, center, -(strokeWidth / length))
    : end.point

  const startAngle = Vec.angle(center, startPoint)

  const endAngle = Vec.angle(center, endPoint)

  const points: number[][] = []

  const count = 8 + Math.floor((Math.abs(length) / 20) * 1 + getRandom() / 2)

  for (let i = 0; i < count; i++) {
    const t = easing(i / count)

    const angle = Utils.lerpAngles(startAngle, endAngle, t)

    points.push(Vec.toFixed(Vec.nudgeAtAngle(center, angle, radius)))
  }

  const stroke = getStroke([startPoint, ...points, endPoint], {
    size: 1 + strokeWidth,
    thinning: 0.618 + getRandom() * 0.2,
    easing: EASINGS.easeOutQuad,
    simulatePressure: false,
    streamline: 0,
    last: true,
  })

  const path = Utils.getSvgPathFromStroke(stroke)

  return path
}

export function getCtp(shape: ArrowShape) {
  const { start, end, bend } = shape.handles
  return Utils.circleFromThreePoints(start.point, end.point, bend.point)
}

export function getArrowArc(shape: ArrowShape) {
  const { start, end, bend } = shape.handles

  const [cx, cy, radius] = Utils.circleFromThreePoints(start.point, end.point, bend.point)

  const center = [cx, cy]

  const length = getArcLength(center, radius, start.point, end.point)

  return { center, radius, length }
}

export function getCurvedArrowHeadPoints(
  A: number[],
  r1: number,
  C: number[],
  r2: number,
  sweep: boolean
) {
  const ints = intersectCircleCircle(A, r1 * 0.618, C, r2).points

  if (!ints) {
    console.warn('Could not find an intersection for the arrow head.')
    return { left: A, right: A }
  }

  const int = sweep ? ints[0] : ints[1]

  const left = int ? Vec.nudge(Vec.rotWith(int, A, Math.PI / 6), A, r1 * -0.382) : A

  const right = int ? Vec.nudge(Vec.rotWith(int, A, -Math.PI / 6), A, r1 * -0.382) : A

  return { left, right }
}

export function getStraightArrowHeadPoints(A: number[], B: number[], r: number) {
  const ints = intersectCircleLineSegment(A, r, A, B).points
  if (!ints) {
    console.warn('Could not find an intersection for the arrow head.')
    return { left: A, right: A }
  }

  const int = ints[0]

  const left = int ? Vec.rotWith(int, A, Math.PI / 6) : A

  const right = int ? Vec.rotWith(int, A, -Math.PI / 6) : A

  return { left, right }
}

export function getCurvedArrowHeadPath(
  A: number[],
  r1: number,
  C: number[],
  r2: number,
  sweep: boolean
) {
  const { left, right } = getCurvedArrowHeadPoints(A, r1, C, r2, sweep)

  return `M ${left} L ${A} ${right}`
}

export function getStraightArrowHeadPath(A: number[], B: number[], r: number) {
  const { left, right } = getStraightArrowHeadPoints(A, B, r)

  return `M ${left} L ${A} ${right}`
}

export function getArrowPath(shape: ArrowShape) {
  const {
    decorations,
    handles: { start, end, bend: _bend },
    style,
  } = shape

  const { strokeWidth } = getShapeStyle(style, false)

  const arrowDist = Vec.dist(start.point, end.point)

  const arrowHeadLength = Math.min(arrowDist / 3, strokeWidth * 8)

  const path: (string | number)[] = []

  const isStraightLine = Vec.dist(_bend.point, Vec.toFixed(Vec.med(start.point, end.point))) < 1

  if (isStraightLine) {
    // Path (line segment)
    path.push(`M ${start.point} L ${end.point}`)

    // Start arrow head
    if (decorations?.start) {
      path.push(getStraightArrowHeadPath(start.point, end.point, arrowHeadLength))
    }

    // End arrow head
    if (decorations?.end) {
      path.push(getStraightArrowHeadPath(end.point, start.point, arrowHeadLength))
    }
  } else {
    const { center, radius, length } = getArrowArc(shape)

    // Path (arc)
    path.push(`M ${start.point} A ${radius} ${radius} 0 0 ${length > 0 ? '1' : '0'} ${end.point}`)

    // Start Arrow head
    if (decorations?.start) {
      path.push(getCurvedArrowHeadPath(start.point, arrowHeadLength, center, radius, length < 0))
    }

    // End arrow head
    if (decorations?.end) {
      path.push(getCurvedArrowHeadPath(end.point, arrowHeadLength, center, radius, length >= 0))
    }
  }

  return path.join(' ')
}

export function getArcPoints(shape: ArrowShape) {
  const { start, bend, end } = shape.handles

  if (Vec.dist2(bend.point, Vec.med(start.point, end.point)) > 4) {
    const points: number[][] = []

    // We're an arc, calculate points along the arc
    const { center, radius } = getArrowArc(shape)

    const startAngle = Vec.angle(center, start.point)

    const endAngle = Vec.angle(center, end.point)

    for (let i = 1 / 20; i < 1; i += 1 / 20) {
      const angle = Utils.lerpAngles(startAngle, endAngle, i)
      points.push(Vec.nudgeAtAngle(center, angle, radius))
    }

    return points
  } else {
    return [start.point, end.point]
  }
}

export function isAngleBetween(a: number, b: number, c: number): boolean {
  if (c === a || c === b) return true
  const PI2 = Math.PI * 2
  const AB = (b - a + PI2) % PI2
  const AC = (c - a + PI2) % PI2
  return AB <= Math.PI !== AC > AB
}

export function getArcLength(C: number[], r: number, A: number[], B: number[]): number {
  const sweep = Utils.getSweep(C, A, B)
  return r * (2 * Math.PI) * (sweep / (2 * Math.PI))
}
