import { Angle, Point, Line, Rectangle, Polyline } from '../../geometry'
import { createSvgElement } from './elem'

const svgDocument = createSvgElement('svg') as SVGSVGElement
const transformRegex = /(\w+)\(([^,)]+),?([^)]+)?\)/gi
const transformSeparatorRegex = /[ ,]+/
const transformationListRegex = /^(\w+)\((.*)\)/

export interface MatrixLike {
  a: number
  b: number
  c: number
  d: number
  e: number
  f: number
}

export interface Translation {
  tx: number
  ty: number
}

export interface Rotation {
  angle: number
  cx?: number
  cy?: number
}

export interface Scale {
  sx: number
  sy: number
}

/**
 * Returns a SVG point object initialized with the `x` and `y` coordinates.
 * @see https://developer.mozilla.org/en/docs/Web/API/SVGPoint
 */
export function createSVGPoint(x: number, y: number) {
  const p = svgDocument.createSVGPoint()
  p.x = x
  p.y = y
  return p
}

/**
 * Returns the SVG transformation matrix initialized with the given matrix.
 *
 * The given matrix is an object of the form:
 * {
 *   a: number
 *   b: number
 *   c: number
 *   d: number
 *   e: number
 *   f: number
 * }
 *
 * @see https://developer.mozilla.org/en/docs/Web/API/SVGMatrix
 */
export function createSVGMatrix(matrix?: DOMMatrix | MatrixLike | null) {
  const mat = svgDocument.createSVGMatrix()
  if (matrix != null) {
    const source = matrix as any
    const target = mat as any
    // eslint-disable-next-line
    for (const key in source) {
      target[key] = source[key]
    }
  }
  return mat
}

/**
 * Returns a SVG transform object.
 * @see https://developer.mozilla.org/en/docs/Web/API/SVGTransform
 */
export function createSVGTransform(matrix?: DOMMatrix | MatrixLike) {
  if (matrix != null) {
    if (!(matrix instanceof DOMMatrix)) {
      matrix = createSVGMatrix(matrix) // eslint-disable-line
    }

    return svgDocument.createSVGTransformFromMatrix(matrix as DOMMatrix)
  }

  return svgDocument.createSVGTransform()
}

/**
 * Returns the SVG transformation matrix built from the `transformString`.
 *
 * E.g. 'translate(10,10) scale(2,2)' will result in matrix:
 * `{ a: 2, b: 0, c: 0, d: 2, e: 10, f: 10}`
 */
export function transformStringToMatrix(transform?: string | null) {
  let mat = createSVGMatrix()
  const matches = transform != null && transform.match(transformRegex)
  if (!matches) {
    return mat
  }

  for (let i = 0, n = matches.length; i < n; i += 1) {
    const transformationString = matches[i]

    const transformationMatch = transformationString.match(
      transformationListRegex,
    )

    if (transformationMatch) {
      let sx
      let sy
      let tx
      let ty
      let angle
      let ctm = createSVGMatrix()
      const args = transformationMatch[2].split(transformSeparatorRegex)
      switch (transformationMatch[1].toLowerCase()) {
        case 'scale':
          sx = parseFloat(args[0])
          sy = args[1] === undefined ? sx : parseFloat(args[1])
          ctm = ctm.scaleNonUniform(sx, sy)
          break
        case 'translate':
          tx = parseFloat(args[0])
          ty = parseFloat(args[1])
          ctm = ctm.translate(tx, ty)
          break
        case 'rotate':
          angle = parseFloat(args[0])
          tx = parseFloat(args[1]) || 0
          ty = parseFloat(args[2]) || 0
          if (tx !== 0 || ty !== 0) {
            ctm = ctm.translate(tx, ty).rotate(angle).translate(-tx, -ty)
          } else {
            ctm = ctm.rotate(angle)
          }
          break
        case 'skewx':
          angle = parseFloat(args[0])
          ctm = ctm.skewX(angle)
          break
        case 'skewy':
          angle = parseFloat(args[0])
          ctm = ctm.skewY(angle)
          break
        case 'matrix':
          ctm.a = parseFloat(args[0])
          ctm.b = parseFloat(args[1])
          ctm.c = parseFloat(args[2])
          ctm.d = parseFloat(args[3])
          ctm.e = parseFloat(args[4])
          ctm.f = parseFloat(args[5])
          break
        default:
          continue
      }

      mat = mat.multiply(ctm)
    }
  }
  return mat
}

export function matrixToTransformString(
  matrix?: DOMMatrix | Partial<MatrixLike>,
) {
  const m = matrix || ({} as DOMMatrix)
  const a = m.a != null ? m.a : 1
  const b = m.b != null ? m.b : 0
  const c = m.c != null ? m.c : 0
  const d = m.d != null ? m.d : 1
  const e = m.e != null ? m.e : 0
  const f = m.f != null ? m.f : 0
  return `matrix(${a},${b},${c},${d},${e},${f})`
}

export function parseTransformString(transform: string) {
  let translation
  let rotation
  let scale

  if (transform) {
    const separator = transformSeparatorRegex

    // Allow reading transform string with a single matrix
    if (transform.trim().indexOf('matrix') >= 0) {
      const matrix = transformStringToMatrix(transform)
      const decomposedMatrix = decomposeMatrix(matrix)

      translation = [decomposedMatrix.translateX, decomposedMatrix.translateY]
      rotation = [decomposedMatrix.rotation]
      scale = [decomposedMatrix.scaleX, decomposedMatrix.scaleY]

      const transformations = []
      if (translation[0] !== 0 || translation[1] !== 0) {
        transformations.push(`translate(${translation.join(',')})`)
      }

      if (scale[0] !== 1 || scale[1] !== 1) {
        transformations.push(`scale(${scale.join(',')})`)
      }

      if (rotation[0] !== 0) {
        transformations.push(`rotate(${rotation[0]})`)
      }

      transform = transformations.join(' ') // eslint-disable-line
    } else {
      const translateMatch = transform.match(/translate\((.*?)\)/)
      if (translateMatch) {
        translation = translateMatch[1].split(separator)
      }
      const rotateMatch = transform.match(/rotate\((.*?)\)/)
      if (rotateMatch) {
        rotation = rotateMatch[1].split(separator)
      }
      const scaleMatch = transform.match(/scale\((.*?)\)/)
      if (scaleMatch) {
        scale = scaleMatch[1].split(separator)
      }
    }
  }

  const sx = scale && scale[0] ? parseFloat(scale[0] as string) : 1

  return {
    raw: transform || '',
    translation: {
      tx:
        translation && translation[0]
          ? parseInt(translation[0] as string, 10)
          : 0,
      ty:
        translation && translation[1]
          ? parseInt(translation[1] as string, 10)
          : 0,
    } as Translation,

    rotation: {
      angle: rotation && rotation[0] ? parseInt(rotation[0] as string, 10) : 0,
      cx:
        rotation && rotation[1]
          ? parseInt(rotation[1] as string, 10)
          : undefined,
      cy:
        rotation && rotation[2]
          ? parseInt(rotation[2] as string, 10)
          : undefined,
    } as Rotation,

    scale: {
      sx,
      sy: scale && scale[1] ? parseFloat(scale[1] as string) : sx,
    } as Scale,
  }
}

function deltaTransformPoint(
  matrix: DOMMatrix | MatrixLike,
  point: Point | Point.PointLike,
) {
  const dx = point.x * matrix.a + point.y * matrix.c + 0
  const dy = point.x * matrix.b + point.y * matrix.d + 0
  return { x: dx, y: dy }
}

/**
 * Decomposes the SVG transformation matrix into separate transformations.
 *
 * Returns an object of the form:
 * {
 *   translateX: number
 *   translateY: number
 *   scaleX: number
 *   scaleY: number
 *   skewX: number
 *   skewY: number
 *   rotation: number
 * }
 *
 * @see https://developer.mozilla.org/en/docs/Web/API/SVGMatrix
 */
export function decomposeMatrix(matrix: DOMMatrix | MatrixLike) {
  // @see https://gist.github.com/2052247

  const px = deltaTransformPoint(matrix, { x: 0, y: 1 })
  const py = deltaTransformPoint(matrix, { x: 1, y: 0 })

  const skewX = (180 / Math.PI) * Math.atan2(px.y, px.x) - 90
  const skewY = (180 / Math.PI) * Math.atan2(py.y, py.x)

  return {
    skewX,
    skewY,
    translateX: matrix.e,
    translateY: matrix.f,
    scaleX: Math.sqrt(matrix.a * matrix.a + matrix.b * matrix.b),
    scaleY: Math.sqrt(matrix.c * matrix.c + matrix.d * matrix.d),
    rotation: skewX,
  }
}

export function matrixToScale(matrix: DOMMatrix | MatrixLike): Scale {
  let a
  let b
  let c
  let d

  if (matrix) {
    a = matrix.a == null ? 1 : matrix.a
    d = matrix.d == null ? 1 : matrix.d
    b = matrix.b
    c = matrix.c
  } else {
    a = d = 1
  }
  return {
    sx: b ? Math.sqrt(a * a + b * b) : a,
    sy: c ? Math.sqrt(c * c + d * d) : d,
  }
}

export function matrixToRotation(matrix: DOMMatrix | MatrixLike): Rotation {
  let p = { x: 0, y: 1 }
  if (matrix) {
    p = deltaTransformPoint(matrix, p)
  }

  return {
    angle: Angle.normalize(Angle.toDeg(Math.atan2(p.y, p.x)) - 90),
  }
}

export function matrixToTranslation(
  matrix: DOMMatrix | MatrixLike,
): Translation {
  return {
    tx: (matrix && matrix.e) || 0,
    ty: (matrix && matrix.f) || 0,
  }
}

/**
 * Transforms point by an SVG transformation represented by `matrix`.
 */
export function transformPoint(point: Point.PointLike, matrix: DOMMatrix) {
  const ret = createSVGPoint(point.x, point.y).matrixTransform(matrix)
  return new Point(ret.x, ret.y)
}

/**
 * Transforms line by an SVG transformation represented by `matrix`.
 */
export function transformLine(line: Line, matrix: DOMMatrix) {
  return new Line(
    transformPoint(line.start, matrix),
    transformPoint(line.end, matrix),
  )
}

/**
 * Transforms polyline by an SVG transformation represented by `matrix`.
 */
export function transformPolyline(polyline: Polyline, matrix: DOMMatrix) {
  let points = polyline instanceof Polyline ? polyline.points : polyline
  if (!Array.isArray(points)) {
    points = []
  }

  return new Polyline(points.map((p) => transformPoint(p, matrix)))
}

export function transformRectangle(
  rect: Rectangle.RectangleLike,
  matrix: DOMMatrix,
) {
  const p = svgDocument.createSVGPoint()

  p.x = rect.x
  p.y = rect.y
  const corner1 = p.matrixTransform(matrix)

  p.x = rect.x + rect.width
  p.y = rect.y
  const corner2 = p.matrixTransform(matrix)

  p.x = rect.x + rect.width
  p.y = rect.y + rect.height
  const corner3 = p.matrixTransform(matrix)

  p.x = rect.x
  p.y = rect.y + rect.height
  const corner4 = p.matrixTransform(matrix)

  const minX = Math.min(corner1.x, corner2.x, corner3.x, corner4.x)
  const maxX = Math.max(corner1.x, corner2.x, corner3.x, corner4.x)
  const minY = Math.min(corner1.y, corner2.y, corner3.y, corner4.y)
  const maxY = Math.max(corner1.y, corner2.y, corner3.y, corner4.y)

  return new Rectangle(minX, minY, maxX - minX, maxY - minY)
}
