'use strict'
// 合抱之木，生於毫末；九层之台，起於累土；千里之行，始於足下。

import ease from './ease'
import { EventProvider } from '../Event'
const rAF = requestAnimationFrame
const gCS = getComputedStyle

/* Animate */
class Animate extends EventProvider {
  public el!: HTMLElement
  private _props = {}
  private _transforms = {}
  private _transforming = false
  private _proper: Array<object> = []
  private _caller: Array<Function> = []
  private _transitionProps: Array<string> = []
  constructor (el: HTMLElement) {
    super()
    if (!el) return
    this.el = el
    this.el.style.setProperty('transition-duration', '0ms')
  }
  public transform (transform: string) {
    this.transition('transform')
    const propName = (transform.match(/\w+\b/) || [])[0]
    if (propName) this._transforms[propName] = transform

    return this
  }
  public skew (x: number, y: number) {
    return this.transform('skew(' + x + 'deg, ' + (y || 0) + 'deg)')
  }
  public skewX (n: number) {
    return this.transform('skewX(' + n + 'deg)')
  }
  public skewY (n: number) {
    return this.transform('skewY(' + n + 'deg)')
  }
  public to (x: number = 0, y: number = 0, z: number = 0) {
    // 3d set
    this.transform('translate3d(' + (x ? x + 'px' : 0) + ',' + (y ? y + 'px' : 0) + ',' + (z ? z + 'px' : 0) + ')')
    return this
  }
  public translate = this.to
  public translate3d = this.to
  public x (n: number) {
    return this.transform('translateX(' + n + 'px)')
  }
  public translateX = this.x
  public y (n: number) {
    return this.transform('translateY(' + n + 'px)')
  }
  public translateY = this.y
  public z (z: number) {
    return this.transform('translateZ(' + z + 'px)')
  }
  public translateZ = this.z
  public scale (x: number, y: number) {
    return this.transform('scale(' +
      x + ', ' +
      (y || x) +
      ')')
  }
  public opacity (o: number) {
    this.transition('opacity')
    return this.style('opacity', o)
  }
  public scaleX (n: number) {
    return this.transform('scaleX(' + n + ')')
  }
  public matrix (m11: number, m12: number, m21: number, m22: number, m31: number, m32: number) {
    return this.transform('matrix(' + [m11, m12, m21, m22, m31, m32].join(',') + ')')
  }
  public scaleY (n: number) {
    return this.transform('scaleY(' + n + ')')
  }
  public rotate (n: number) {
    return this.transform('rotate(' + n + 'deg)')
  }
  public rotateX (n: number) {
    return this.transform('rotateX(' + n + 'deg)')
  }
  public rotateY (n: number) {
    return this.transform('rotateY(' + n + 'deg)')
  }
  public rotateZ (n: number) {
    return this.transform('rotateZ(' + n + 'deg)')
  }
  public rotate3d (x: number, y: number, z: number, d: number) {
    return this.transform('rotate3d(' + x + ', ' + y + ',' + z + ',' + d + 'deg)')
  }
  public perspective (z: number) {
    const box = this.el.parentElement
    if (box) {
      box.style.setProperty('transform-style', 'preserve-3d')
      box.style.setProperty('perspective', z + 'px')
    }
    return this
  }
  public backface (visibility: boolean = true) {
    return this.style('backface-visibility', visibility ? 'visible' : 'hidden')
  }
  public ease (s: string) {
    s = ease[s as string] || s || 'ease'
    return this.style('transition-timing-function', s)
  }
  public animate (name: string, props: object) {
    for (const i in props) {
      if (props.hasOwnProperty(i)) {
        this.style('animation-' + i, props[i])
      }
    }
    return this.style('animation-name', name)
  }
  public duration (n: string | number) {
    n = 'string' === typeof n ?
      parseFloat(n) * 1000 :
      n

    return this.style('transition-duration', n + 'ms')
  }
  public getDuration () {
    return !!parseFloat(gCS(this.el).transitionDuration)
  }
  public delay (n: string | number) {
    n = 'string' === typeof n ?
      parseFloat(n) * 1000 :
      n

    return this.style('transition-delay', n + 'ms')
  }
  public origin (x: number, y: number) {
    let n = 'center'
    if (typeof x === 'object') {
      y = x[1] || 0
      x = x[0] || 0
    }
    if (!isNaN(x) && !isNaN(y)) {
      n = x + 'px' + ' ' + y + 'px'
    } else if (y) {
      n = x + ' ' + y
    }
    return this.style('transform-origin', n)
  }
  public width (val?: number) {
    this.transition('width')
    return this.style('width', val === undefined ? '' : val + 'px')
  }
  public height (val?: number) {
    this.transition('height')
    return this.style('height', val === undefined ? '' : val + 'px')
  }
  public add (prop: string, val: number) {
    return this.on('start', () => {
      const curr = parseInt(this.current(prop), 10)
      this.style(prop, curr + val + 'px')
    })
  }
  public subc (prop: string, val: number) {
    return this.on('start', () => {
      const curr = parseInt(this.current(prop), 10)
      this.style(prop, curr - val + 'px')
    })
  }
  public current (prop: string) {
    return gCS(this.el).getPropertyValue(prop)
  }
  public transition (prop: string) {
    if (this._transitionProps.indexOf(prop) === -1) this._transitionProps.push(prop)
    return this
  }
  public filter (val: string) {
    this.style('filter', val)
    this.transition('filter')
    return this
  }
  public style (prop: string, val?: string | number) {
    this._props[prop] = val === undefined ? '' : val
    return this
  }
  private _transitionend (fn: Function) {
    if (!this.getDuration()) return setTimeout(fn, 0)
    const onec = (e: TransitionEvent) => {
      if (e.target !== this.el) return false
      fn(e)
      this.el.removeEventListener('transitionend', onec, false)
    }
    this.el.addEventListener('transitionend', onec, false)
  }
  private _applyTransform () {
    const transform: Array<string> = []
    if (!this._transforms['translate3d'] && !this._transforms['translateZ']) {
      this._transforms['translateZ'] = 'translateZ(0)'
    }
    for (const i in this._transforms) {
      transform.push(this._transforms[i])
    }
    if (transform.length) {
      this.style('transform', transform.join(' '))
    }
    return this
  }
  public applyProperties () {
    const prop = this._proper.shift()
    this._transforming = true
    for (const name in prop) {
      this.el.style.setProperty(name, prop[name])
    }

    this._transitionend(() => {
      this.clear()
      this.next()
    })

    return this
  }
  public next () {
    if (this._caller.length) {
      const fn = this._caller.shift()
      fn && fn()
    }

    if (this._caller.length === 0) {
      this.init()
    } else {
      rAF(() => { this.applyProperties() })
    }
  }
  public clear () {
    this._transforms = {}
  }
  public init () {
    if (this.getDuration()) {
      this.el.style.setProperty('transition-duration', '0ms')
    }

    this.clear()
    this._transforming = false

    return this
  }
  public then (fn: Function) {
    this._applyTransform()
    this.style('transition-property', this._transitionProps.join(', '))
    this._proper.push(this._props)
    this._props = {}
    this._caller.push(() => fn && fn.call(this))

    return this
  }
  public end (fn: Function) {
    this.then(fn)
    if (this._transforming) return this
    rAF(() => { this.applyProperties() })

    return this
  }
}

export {
  Animate
}
