'use strict'

const util = require('util')

// DOMMatrix per https://drafts.fxtf.org/geometry/#DOMMatrix

class DOMPoint {
  constructor (x, y, z, w) {
    if (typeof x === 'object' && x !== null) {
      w = x.w
      z = x.z
      y = x.y
      x = x.x
    }
    this.x = typeof x === 'number' ? x : 0
    this.y = typeof y === 'number' ? y : 0
    this.z = typeof z === 'number' ? z : 0
    this.w = typeof w === 'number' ? w : 1
  }

  matrixTransform(init) {
    // TODO: this next line is wrong. matrixTransform is supposed to only take
    // an object with the DOMMatrix properties called DOMMatrixInit
    const m = init instanceof DOMMatrix ? init : new DOMMatrix(init)
    return m.transformPoint(this)
  }

  toJSON() {
    return {
      x: this.x,
      y: this.y,
      z: this.z,
      w: this.w
    }
  }

  static fromPoint(other) {
    return new this(other.x, other.y, other.z, other.w)
  }
}

// Constants to index into _values (col-major)
const M11 = 0; const M12 = 1; const M13 = 2; const M14 = 3
const M21 = 4; const M22 = 5; const M23 = 6; const M24 = 7
const M31 = 8; const M32 = 9; const M33 = 10; const M34 = 11
const M41 = 12; const M42 = 13; const M43 = 14; const M44 = 15

const DEGREE_PER_RAD = 180 / Math.PI
const RAD_PER_DEGREE = Math.PI / 180

function parseMatrix (init) {
  let parsed = init.replace('matrix(', '')
  parsed = parsed.split(',', 7) // 6 + 1 to handle too many params
  if (parsed.length !== 6) throw new Error(`Failed to parse ${init}`)
  parsed = parsed.map(parseFloat)
  return [
    parsed[0], parsed[1], 0, 0,
    parsed[2], parsed[3], 0, 0,
    0, 0, 1, 0,
    parsed[4], parsed[5], 0, 1
  ]
}

function parseMatrix3d (init) {
  let parsed = init.replace('matrix3d(', '')
  parsed = parsed.split(',', 17) // 16 + 1 to handle too many params
  if (parsed.length !== 16) throw new Error(`Failed to parse ${init}`)
  return parsed.map(parseFloat)
}

function parseTransform (tform) {
  const type = tform.split('(', 1)[0]
  switch (type) {
    case 'matrix':
      return parseMatrix(tform)
    case 'matrix3d':
      return parseMatrix3d(tform)
    // TODO This is supposed to support any CSS transform value.
    default:
      throw new Error(`${type} parsing not implemented`)
  }
}

class DOMMatrix {
  constructor (init) {
    this._is2D = true
    this._values = new Float64Array([
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, 1
    ])

    let i

    if (typeof init === 'string') { // parse CSS transformList
      if (init === '') return // default identity matrix
      const tforms = init.split(/\)\s+/, 20).map(parseTransform)
      if (tforms.length === 0) return
      init = tforms[0]
      for (i = 1; i < tforms.length; i++) init = multiply(tforms[i], init)
    }

    i = 0
    if (init && init.length === 6) {
      setNumber2D(this, M11, init[i++])
      setNumber2D(this, M12, init[i++])
      setNumber2D(this, M21, init[i++])
      setNumber2D(this, M22, init[i++])
      setNumber2D(this, M41, init[i++])
      setNumber2D(this, M42, init[i++])
    } else if (init && init.length === 16) {
      setNumber2D(this, M11, init[i++])
      setNumber2D(this, M12, init[i++])
      setNumber3D(this, M13, init[i++])
      setNumber3D(this, M14, init[i++])
      setNumber2D(this, M21, init[i++])
      setNumber2D(this, M22, init[i++])
      setNumber3D(this, M23, init[i++])
      setNumber3D(this, M24, init[i++])
      setNumber3D(this, M31, init[i++])
      setNumber3D(this, M32, init[i++])
      setNumber3D(this, M33, init[i++])
      setNumber3D(this, M34, init[i++])
      setNumber2D(this, M41, init[i++])
      setNumber2D(this, M42, init[i++])
      setNumber3D(this, M43, init[i++])
      setNumber3D(this, M44, init[i])
    } else if (init !== undefined) {
      throw new TypeError('Expected string or array.')
    }
  }

  toString () {
    return this.is2D
      ? `matrix(${this.a}, ${this.b}, ${this.c}, ${this.d}, ${this.e}, ${this.f})`
      : `matrix3d(${this._values.join(', ')})`
  }

  multiply (other) {
    return newInstance(this._values).multiplySelf(other)
  }

  multiplySelf (other) {
    this._values = multiply(other._values, this._values)
    if (!other.is2D) this._is2D = false
    return this
  }

  preMultiplySelf (other) {
    this._values = multiply(this._values, other._values)
    if (!other.is2D) this._is2D = false
    return this
  }

  translate (tx, ty, tz) {
    return newInstance(this._values).translateSelf(tx, ty, tz)
  }

  translateSelf (tx, ty, tz) {
    if (typeof tx !== 'number') tx = 0
    if (typeof ty !== 'number') ty = 0
    if (typeof tz !== 'number') tz = 0
    this._values = multiply([
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      tx, ty, tz, 1
    ], this._values)
    if (tz !== 0) this._is2D = false
    return this
  }

  scale (scaleX, scaleY, scaleZ, originX, originY, originZ) {
    return newInstance(this._values).scaleSelf(scaleX, scaleY, scaleZ, originX, originY, originZ)
  }

  scale3d (scale, originX, originY, originZ) {
    return newInstance(this._values).scale3dSelf(scale, originX, originY, originZ)
  }

  scale3dSelf (scale, originX, originY, originZ) {
    return this.scaleSelf(scale, scale, scale, originX, originY, originZ)
  }

  /**
   * @deprecated
   */
  scaleNonUniform(scaleX, scaleY) {
    return this.scale(scaleX, scaleY)
  }

  scaleSelf (scaleX, scaleY, scaleZ, originX, originY, originZ) {
    // Not redundant with translate's checks because we need to negate the values later.
    if (typeof originX !== 'number') originX = 0
    if (typeof originY !== 'number') originY = 0
    if (typeof originZ !== 'number') originZ = 0
    this.translateSelf(originX, originY, originZ)
    if (typeof scaleX !== 'number') scaleX = 1
    if (typeof scaleY !== 'number') scaleY = scaleX
    if (typeof scaleZ !== 'number') scaleZ = 1
    this._values = multiply([
      scaleX, 0, 0, 0,
      0, scaleY, 0, 0,
      0, 0, scaleZ, 0,
      0, 0, 0, 1
    ], this._values)
    this.translateSelf(-originX, -originY, -originZ)
    if (scaleZ !== 1 || originZ !== 0) this._is2D = false
    return this
  }

  rotateFromVector (x, y) {
    return newInstance(this._values).rotateFromVectorSelf(x, y)
  }

  rotateFromVectorSelf (x, y) {
    if (typeof x !== 'number') x = 0
    if (typeof y !== 'number') y = 0
    const theta = (x === 0 && y === 0) ? 0 : Math.atan2(y, x) * DEGREE_PER_RAD
    return this.rotateSelf(theta)
  }

  rotate (rotX, rotY, rotZ) {
    return newInstance(this._values).rotateSelf(rotX, rotY, rotZ)
  }

  rotateSelf (rotX, rotY, rotZ) {
    if (rotY === undefined && rotZ === undefined) {
      rotZ = rotX
      rotX = rotY = 0
    }
    if (typeof rotY !== 'number') rotY = 0
    if (typeof rotZ !== 'number') rotZ = 0
    if (rotX !== 0 || rotY !== 0) this._is2D = false
    rotX *= RAD_PER_DEGREE
    rotY *= RAD_PER_DEGREE
    rotZ *= RAD_PER_DEGREE
    let c, s
    c = Math.cos(rotZ)
    s = Math.sin(rotZ)
    this._values = multiply([
      c, s, 0, 0,
      -s, c, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, 1
    ], this._values)
    c = Math.cos(rotY)
    s = Math.sin(rotY)
    this._values = multiply([
      c, 0, -s, 0,
      0, 1, 0, 0,
      s, 0, c, 0,
      0, 0, 0, 1
    ], this._values)
    c = Math.cos(rotX)
    s = Math.sin(rotX)
    this._values = multiply([
      1, 0, 0, 0,
      0, c, s, 0,
      0, -s, c, 0,
      0, 0, 0, 1
    ], this._values)
    return this
  }

  rotateAxisAngle (x, y, z, angle) {
    return newInstance(this._values).rotateAxisAngleSelf(x, y, z, angle)
  }

  rotateAxisAngleSelf (x, y, z, angle) {
    if (typeof x !== 'number') x = 0
    if (typeof y !== 'number') y = 0
    if (typeof z !== 'number') z = 0
    // Normalize axis
    const length = Math.sqrt(x * x + y * y + z * z)
    if (length === 0) return this
    if (length !== 1) {
      x /= length
      y /= length
      z /= length
    }
    angle *= RAD_PER_DEGREE
    const c = Math.cos(angle)
    const s = Math.sin(angle)
    const t = 1 - c
    const tx = t * x
    const ty = t * y
    // NB: This is the generic transform. If the axis is a major axis, there are
    // faster transforms.
    this._values = multiply([
      tx * x + c, tx * y + s * z, tx * z - s * y, 0,
      tx * y - s * z, ty * y + c, ty * z + s * x, 0,
      tx * z + s * y, ty * z - s * x, t * z * z + c, 0,
      0, 0, 0, 1
    ], this._values)
    if (x !== 0 || y !== 0) this._is2D = false
    return this
  }

  skewX (sx) {
    return newInstance(this._values).skewXSelf(sx)
  }

  skewXSelf (sx) {
    if (typeof sx !== 'number') return this
    const t = Math.tan(sx * RAD_PER_DEGREE)
    this._values = multiply([
      1, 0, 0, 0,
      t, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, 1
    ], this._values)
    return this
  }

  skewY (sy) {
    return newInstance(this._values).skewYSelf(sy)
  }

  skewYSelf (sy) {
    if (typeof sy !== 'number') return this
    const t = Math.tan(sy * RAD_PER_DEGREE)
    this._values = multiply([
      1, t, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, 1
    ], this._values)
    return this
  }

  flipX () {
    return newInstance(multiply([
      -1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, 1
    ], this._values))
  }

  flipY () {
    return newInstance(multiply([
      1, 0, 0, 0,
      0, -1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, 1
    ], this._values))
  }

  inverse () {
    return newInstance(this._values).invertSelf()
  }

  invertSelf () {
    const m = this._values
    const inv = m.map(v => 0)

    inv[0] = m[5] * m[10] * m[15] -
            m[5] * m[11] * m[14] -
            m[9] * m[6] * m[15] +
            m[9] * m[7] * m[14] +
            m[13] * m[6] * m[11] -
            m[13] * m[7] * m[10]

    inv[4] = -m[4] * m[10] * m[15] +
            m[4] * m[11] * m[14] +
            m[8] * m[6] * m[15] -
            m[8] * m[7] * m[14] -
            m[12] * m[6] * m[11] +
            m[12] * m[7] * m[10]

    inv[8] = m[4] * m[9] * m[15] -
            m[4] * m[11] * m[13] -
            m[8] * m[5] * m[15] +
            m[8] * m[7] * m[13] +
            m[12] * m[5] * m[11] -
            m[12] * m[7] * m[9]

    inv[12] = -m[4] * m[9] * m[14] +
            m[4] * m[10] * m[13] +
            m[8] * m[5] * m[14] -
            m[8] * m[6] * m[13] -
            m[12] * m[5] * m[10] +
            m[12] * m[6] * m[9]

    // If the determinant is zero, this matrix cannot be inverted, and all
    // values should be set to NaN, with the is2D flag set to false.

    const det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12]

    if (det === 0) {
      this._values = m.map(v => NaN)
      this._is2D = false
      return this
    }

    inv[1] = -m[1] * m[10] * m[15] +
            m[1] * m[11] * m[14] +
            m[9] * m[2] * m[15] -
            m[9] * m[3] * m[14] -
            m[13] * m[2] * m[11] +
            m[13] * m[3] * m[10]

    inv[5] = m[0] * m[10] * m[15] -
            m[0] * m[11] * m[14] -
            m[8] * m[2] * m[15] +
            m[8] * m[3] * m[14] +
            m[12] * m[2] * m[11] -
            m[12] * m[3] * m[10]

    inv[9] = -m[0] * m[9] * m[15] +
            m[0] * m[11] * m[13] +
            m[8] * m[1] * m[15] -
            m[8] * m[3] * m[13] -
            m[12] * m[1] * m[11] +
            m[12] * m[3] * m[9]

    inv[13] = m[0] * m[9] * m[14] -
            m[0] * m[10] * m[13] -
            m[8] * m[1] * m[14] +
            m[8] * m[2] * m[13] +
            m[12] * m[1] * m[10] -
            m[12] * m[2] * m[9]

    inv[2] = m[1] * m[6] * m[15] -
            m[1] * m[7] * m[14] -
            m[5] * m[2] * m[15] +
            m[5] * m[3] * m[14] +
            m[13] * m[2] * m[7] -
            m[13] * m[3] * m[6]

    inv[6] = -m[0] * m[6] * m[15] +
            m[0] * m[7] * m[14] +
            m[4] * m[2] * m[15] -
            m[4] * m[3] * m[14] -
            m[12] * m[2] * m[7] +
            m[12] * m[3] * m[6]

    inv[10] = m[0] * m[5] * m[15] -
            m[0] * m[7] * m[13] -
            m[4] * m[1] * m[15] +
            m[4] * m[3] * m[13] +
            m[12] * m[1] * m[7] -
            m[12] * m[3] * m[5]

    inv[14] = -m[0] * m[5] * m[14] +
            m[0] * m[6] * m[13] +
            m[4] * m[1] * m[14] -
            m[4] * m[2] * m[13] -
            m[12] * m[1] * m[6] +
            m[12] * m[2] * m[5]

    inv[3] = -m[1] * m[6] * m[11] +
            m[1] * m[7] * m[10] +
            m[5] * m[2] * m[11] -
            m[5] * m[3] * m[10] -
            m[9] * m[2] * m[7] +
            m[9] * m[3] * m[6]

    inv[7] = m[0] * m[6] * m[11] -
            m[0] * m[7] * m[10] -
            m[4] * m[2] * m[11] +
            m[4] * m[3] * m[10] +
            m[8] * m[2] * m[7] -
            m[8] * m[3] * m[6]

    inv[11] = -m[0] * m[5] * m[11] +
            m[0] * m[7] * m[9] +
            m[4] * m[1] * m[11] -
            m[4] * m[3] * m[9] -
            m[8] * m[1] * m[7] +
            m[8] * m[3] * m[5]

    inv[15] = m[0] * m[5] * m[10] -
            m[0] * m[6] * m[9] -
            m[4] * m[1] * m[10] +
            m[4] * m[2] * m[9] +
            m[8] * m[1] * m[6] -
            m[8] * m[2] * m[5]

    inv.forEach((v, i) => { inv[i] = v / det })
    this._values = inv
    return this
  }

  setMatrixValue (transformList) {
    const temp = new DOMMatrix(transformList)
    this._values = temp._values
    this._is2D = temp._is2D
    return this
  }

  transformPoint (point) {
    point = new DOMPoint(point)
    const x = point.x
    const y = point.y
    const z = point.z
    const w = point.w
    const values = this._values
    const nx = values[M11] * x + values[M21] * y + values[M31] * z + values[M41] * w
    const ny = values[M12] * x + values[M22] * y + values[M32] * z + values[M42] * w
    const nz = values[M13] * x + values[M23] * y + values[M33] * z + values[M43] * w
    const nw = values[M14] * x + values[M24] * y + values[M34] * z + values[M44] * w
    return new DOMPoint(nx, ny, nz, nw)
  }

  toFloat32Array () {
    return Float32Array.from(this._values)
  }

  toFloat64Array () {
    return this._values.slice(0)
  }

  static fromMatrix (init) {
    if (!(init instanceof DOMMatrix)) throw new TypeError('Expected DOMMatrix')
    return new DOMMatrix(init._values)
  }

  static fromFloat32Array (init) {
    if (!(init instanceof Float32Array)) throw new TypeError('Expected Float32Array')
    return new DOMMatrix(init)
  }

  static fromFloat64Array (init) {
    if (!(init instanceof Float64Array)) throw new TypeError('Expected Float64Array')
    return new DOMMatrix(init)
  }

  [util.inspect.custom || 'inspect'] (depth, options) {
    if (depth < 0) return '[DOMMatrix]'

    return `DOMMatrix [
      a: ${this.a}
      b: ${this.b}
      c: ${this.c}
      d: ${this.d}
      e: ${this.e}
      f: ${this.f}
      m11: ${this.m11}
      m12: ${this.m12}
      m13: ${this.m13}
      m14: ${this.m14}
      m21: ${this.m21}
      m22: ${this.m22}
      m23: ${this.m23}
      m23: ${this.m23}
      m31: ${this.m31}
      m32: ${this.m32}
      m33: ${this.m33}
      m34: ${this.m34}
      m41: ${this.m41}
      m42: ${this.m42}
      m43: ${this.m43}
      m44: ${this.m44}
      is2D: ${this.is2D}
      isIdentity: ${this.isIdentity} ]`
  }
}

/**
 * Checks that `value` is a number and sets the value.
 */
function setNumber2D (receiver, index, value) {
  if (typeof value !== 'number') throw new TypeError('Expected number')
  return (receiver._values[index] = value)
}

/**
 * Checks that `value` is a number, sets `_is2D = false` if necessary and sets
 * the value.
 */
function setNumber3D (receiver, index, value) {
  if (typeof value !== 'number') throw new TypeError('Expected number')
  if (index === M33 || index === M44) {
    if (value !== 1) receiver._is2D = false
  } else if (value !== 0) receiver._is2D = false
  return (receiver._values[index] = value)
}

Object.defineProperties(DOMMatrix.prototype, {
  m11: { get () { return this._values[M11] }, set (v) { return setNumber2D(this, M11, v) } },
  m12: { get () { return this._values[M12] }, set (v) { return setNumber2D(this, M12, v) } },
  m13: { get () { return this._values[M13] }, set (v) { return setNumber3D(this, M13, v) } },
  m14: { get () { return this._values[M14] }, set (v) { return setNumber3D(this, M14, v) } },
  m21: { get () { return this._values[M21] }, set (v) { return setNumber2D(this, M21, v) } },
  m22: { get () { return this._values[M22] }, set (v) { return setNumber2D(this, M22, v) } },
  m23: { get () { return this._values[M23] }, set (v) { return setNumber3D(this, M23, v) } },
  m24: { get () { return this._values[M24] }, set (v) { return setNumber3D(this, M24, v) } },
  m31: { get () { return this._values[M31] }, set (v) { return setNumber3D(this, M31, v) } },
  m32: { get () { return this._values[M32] }, set (v) { return setNumber3D(this, M32, v) } },
  m33: { get () { return this._values[M33] }, set (v) { return setNumber3D(this, M33, v) } },
  m34: { get () { return this._values[M34] }, set (v) { return setNumber3D(this, M34, v) } },
  m41: { get () { return this._values[M41] }, set (v) { return setNumber2D(this, M41, v) } },
  m42: { get () { return this._values[M42] }, set (v) { return setNumber2D(this, M42, v) } },
  m43: { get () { return this._values[M43] }, set (v) { return setNumber3D(this, M43, v) } },
  m44: { get () { return this._values[M44] }, set (v) { return setNumber3D(this, M44, v) } },

  a: { get () { return this.m11 }, set (v) { return (this.m11 = v) } },
  b: { get () { return this.m12 }, set (v) { return (this.m12 = v) } },
  c: { get () { return this.m21 }, set (v) { return (this.m21 = v) } },
  d: { get () { return this.m22 }, set (v) { return (this.m22 = v) } },
  e: { get () { return this.m41 }, set (v) { return (this.m41 = v) } },
  f: { get () { return this.m42 }, set (v) { return (this.m42 = v) } },

  is2D: { get () { return this._is2D } }, // read-only

  isIdentity: {
    get () {
      const values = this._values
      return (values[M11] === 1 && values[M12] === 0 && values[M13] === 0 && values[M14] === 0 &&
             values[M21] === 0 && values[M22] === 1 && values[M23] === 0 && values[M24] === 0 &&
             values[M31] === 0 && values[M32] === 0 && values[M33] === 1 && values[M34] === 0 &&
             values[M41] === 0 && values[M42] === 0 && values[M43] === 0 && values[M44] === 1)
    }
  },

  toJSON: {
    value() {
      return {
        a: this.a,
        b: this.b,
        c: this.c,
        d: this.d,
        e: this.e,
        f: this.f,
        m11: this.m11,
        m12: this.m12,
        m13: this.m13,
        m14: this.m14,
        m21: this.m21,
        m22: this.m22,
        m23: this.m23,
        m23: this.m23,
        m31: this.m31,
        m32: this.m32,
        m33: this.m33,
        m34: this.m34,
        m41: this.m41,
        m42: this.m42,
        m43: this.m43,
        m44: this.m44,
        is2D: this.is2D,
        isIdentity: this.isIdentity,
      }
    }
  }
})

/**
 * Instantiates a DOMMatrix, bypassing the constructor.
 * @param {Float64Array} values Value to assign to `_values`. This is assigned
 *   without copying (okay because all usages are followed by a  multiply).
 */
function newInstance (values) {
  const instance = Object.create(DOMMatrix.prototype)
  instance.constructor = DOMMatrix
  instance._is2D = true
  instance._values = values
  return instance
}

function multiply (A, B) {
  const dest = new Float64Array(16)
  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
      let sum = 0
      for (let k = 0; k < 4; k++) {
        sum += A[i * 4 + k] * B[k * 4 + j]
      }
      dest[i * 4 + j] = sum
    }
  }
  return dest
}

module.exports = { DOMMatrix, DOMPoint }
