/* eslint-disable default-param-last */

import { Point, PointLike } from '../point'

const regexSupportedData = new RegExp(`^[\\s\\dLMCZz,.]*$`) // eslint-disable-line prefer-regex-literals

export function isValid(data: any) {
  if (typeof data !== 'string') {
    return false
  }

  return regexSupportedData.test(data)
}

/**
 * Returns the remainder of division of `n` by `m`. You should use this
 * instead of the built-in operation as the built-in operation does not
 * properly handle negative numbers.
 */
function mod(n: number, m: number) {
  return ((n % m) + m) % m
}

export interface DrawPointsOptions {
  round?: number
  initialMove?: boolean
  close?: boolean
  exclude?: number[]
}

function draw(
  points: PointLike[],
  round?: number,
  initialMove?: boolean,
  close?: boolean,
  exclude?: number[],
) {
  if (!points || points.length === 0) return ''
  const data: (string | number)[] = []
  const end = points[points.length - 1]
  const rounded = round != null && round > 0
  const arcSize = round || 0

  // Adds virtual waypoint in the center between start and end point
  if (close && rounded) {
    points = points.slice() // eslint-disable-line
    const p0 = points[0]
    const wp = new Point(end.x + (p0.x - end.x) / 2, end.y + (p0.y - end.y) / 2)
    points.splice(0, 0, wp)
  }

  let pt = points[0]
  let i = 1

  // Draws the line segments
  if (initialMove) {
    data.push('M', pt.x, pt.y)
  } else {
    data.push('L', pt.x, pt.y)
  }

  while (i < (close ? points.length : points.length - 1)) {
    let tmp = points[mod(i, points.length)]
    let dx = pt.x - tmp.x
    let dy = pt.y - tmp.y

    if (
      rounded &&
      (dx !== 0 || dy !== 0) &&
      (exclude == null || exclude.indexOf(i - 1) < 0)
    ) {
      // Draws a line from the last point to the current
      // point with a spacing of size off the current point
      // into direction of the last point
      let dist = Math.sqrt(dx * dx + dy * dy)
      const nx1 = (dx * Math.min(arcSize, dist / 2)) / dist
      const ny1 = (dy * Math.min(arcSize, dist / 2)) / dist

      const x1 = tmp.x + nx1
      const y1 = tmp.y + ny1
      data.push('L', x1, y1)

      // Draws a curve from the last point to the current
      // point with a spacing of size off the current point
      // into direction of the next point
      let next = points[mod(i + 1, points.length)]

      // Uses next non-overlapping point
      while (
        i < points.length - 2 &&
        Math.round(next.x - tmp.x) === 0 &&
        Math.round(next.y - tmp.y) === 0
      ) {
        next = points[mod(i + 2, points.length)]
        i += 1
      }

      dx = next.x - tmp.x
      dy = next.y - tmp.y

      dist = Math.max(1, Math.sqrt(dx * dx + dy * dy))
      const nx2 = (dx * Math.min(arcSize, dist / 2)) / dist
      const ny2 = (dy * Math.min(arcSize, dist / 2)) / dist

      const x2 = tmp.x + nx2
      const y2 = tmp.y + ny2

      data.push('Q', tmp.x, tmp.y, x2, y2)
      tmp = new Point(x2, y2)
    } else {
      data.push('L', tmp.x, tmp.y)
    }

    pt = tmp
    i += 1
  }

  if (close) {
    data.push('Z')
  } else {
    data.push('L', end.x, end.y)
  }

  return data.map((v) => (typeof v === 'string' ? v : +v.toFixed(3))).join(' ')
}

export function drawPoints(
  points: PointLike[],
  options: DrawPointsOptions = {},
) {
  const pts: PointLike[] = []
  if (points && points.length) {
    points.forEach((p) => {
      if (Array.isArray(p)) {
        pts.push({ x: p[0], y: p[1] })
      } else {
        pts.push({ x: p.x, y: p.y })
      }
    })
  }

  return draw(
    pts,
    options.round,
    options.initialMove == null || options.initialMove,
    options.close,
    options.exclude,
  )
}

/**
 * Converts the given arc to a series of curves.
 */
export function arcToCurves(
  x0: number,
  y0: number,
  r1: number,
  r2: number,
  angle = 0,
  largeArcFlag = 0,
  sweepFlag = 0,
  x: number,
  y: number,
) {
  if (r1 === 0 || r2 === 0) {
    return []
  }

  x -= x0 // eslint-disable-line
  y -= y0 // eslint-disable-line
  r1 = Math.abs(r1) // eslint-disable-line
  r2 = Math.abs(r2) // eslint-disable-line

  const ctx = -x / 2
  const cty = -y / 2
  const cpsi = Math.cos((angle * Math.PI) / 180)
  const spsi = Math.sin((angle * Math.PI) / 180)
  const rxd = cpsi * ctx + spsi * cty
  const ryd = -1 * spsi * ctx + cpsi * cty
  const rxdd = rxd * rxd
  const rydd = ryd * ryd
  const r1x = r1 * r1
  const r2y = r2 * r2
  const lamda = rxdd / r1x + rydd / r2y

  let sds

  if (lamda > 1) {
    r1 = Math.sqrt(lamda) * r1 // eslint-disable-line
    r2 = Math.sqrt(lamda) * r2 // eslint-disable-line
    sds = 0
  } else {
    let seif = 1
    if (largeArcFlag === sweepFlag) {
      seif = -1
    }

    sds =
      seif *
      Math.sqrt(
        (r1x * r2y - r1x * rydd - r2y * rxdd) / (r1x * rydd + r2y * rxdd),
      )
  }

  const txd = (sds * r1 * ryd) / r2
  const tyd = (-1 * sds * r2 * rxd) / r1
  const tx = cpsi * txd - spsi * tyd + x / 2
  const ty = spsi * txd + cpsi * tyd + y / 2

  let rad = Math.atan2((ryd - tyd) / r2, (rxd - txd) / r1) - Math.atan2(0, 1)
  let s1 = rad >= 0 ? rad : 2 * Math.PI + rad
  rad =
    Math.atan2((-ryd - tyd) / r2, (-rxd - txd) / r1) -
    Math.atan2((ryd - tyd) / r2, (rxd - txd) / r1)
  let dr = rad >= 0 ? rad : 2 * Math.PI + rad

  if (sweepFlag === 0 && dr > 0) {
    dr -= 2 * Math.PI
  } else if (sweepFlag !== 0 && dr < 0) {
    dr += 2 * Math.PI
  }

  const sse = (dr * 2) / Math.PI
  const seg = Math.ceil(sse < 0 ? -1 * sse : sse)
  const segr = dr / seg
  const t =
    ((8 / 3) * Math.sin(segr / 4) * Math.sin(segr / 4)) / Math.sin(segr / 2)
  const cpsir1 = cpsi * r1
  const cpsir2 = cpsi * r2
  const spsir1 = spsi * r1
  const spsir2 = spsi * r2

  let mc = Math.cos(s1)
  let ms = Math.sin(s1)
  let x2 = -t * (cpsir1 * ms + spsir2 * mc)
  let y2 = -t * (spsir1 * ms - cpsir2 * mc)
  let x3 = 0
  let y3 = 0

  const result = []

  for (let n = 0; n < seg; n += 1) {
    s1 += segr
    mc = Math.cos(s1)
    ms = Math.sin(s1)

    x3 = cpsir1 * mc - spsir2 * ms + tx
    y3 = spsir1 * mc + cpsir2 * ms + ty
    const dx = -t * (cpsir1 * ms + spsir2 * mc)
    const dy = -t * (spsir1 * ms - cpsir2 * mc)

    // CurveTo updates x0, y0 so need to restore it
    const index = n * 6
    result[index] = Number(x2 + x0)
    result[index + 1] = Number(y2 + y0)
    result[index + 2] = Number(x3 - dx + x0)
    result[index + 3] = Number(y3 - dy + y0)
    result[index + 4] = Number(x3 + x0)
    result[index + 5] = Number(y3 + y0)

    x2 = x3 + dx
    y2 = y3 + dy
  }

  return result.map((num) => +num.toFixed(2))
}

export function drawArc(
  startX: number,
  startY: number,
  rx: number,
  ry: number,
  xAxisRotation = 0,
  largeArcFlag: 0 | 1 = 0,
  sweepFlag: 0 | 1 = 0,
  stopX: number,
  stopY: number,
) {
  const data: (string | number)[] = []
  const points = arcToCurves(
    startX,
    startY,
    rx,
    ry,
    xAxisRotation,
    largeArcFlag,
    sweepFlag,
    stopX,
    stopY,
  )

  if (points != null) {
    for (let i = 0, ii = points.length; i < ii; i += 6) {
      data.push(
        'C',
        points[i],
        points[i + 1],
        points[i + 2],
        points[i + 3],
        points[i + 4],
        points[i + 5],
      )
    }
  }

  return data.join(' ')
}
