import curves from './config/curves'

const defaultTransitionBC = 'linear'

/**
 * @description Get the N-frame animation state by the start and end state
 *              of the animation and the easing curve
 * @param {String|Array} tBC               Easing curve name or data
 * @param {Number|Array|Object} startState Animation start state
 * @param {Number|Array|Object} endState   Animation end state
 * @param {Number} frameNum                Number of Animation frames
 * @param {Boolean} deep                   Whether to use recursive mode
 * @return {Array|Boolean} State of each frame of the animation (Invalid input will return false)
 */
export function transition (tBC, startState = null, endState = null, frameNum = 30, deep = false) {
  if (!checkParams(...arguments)) return false

  try {
    // Get the transition bezier curve
    const bezierCurve = getBezierCurve(tBC)

    // Get the progress of each frame state
    const frameStateProgress = getFrameStateProgress(bezierCurve, frameNum)

    // If the recursion mode is not enabled or the state type is Number, the shallow state calculation is performed directly.
    if (!deep || typeof endState === 'number') return getTransitionState(startState, endState, frameStateProgress)

    return recursionTransitionState(startState, endState, frameStateProgress)
  } catch {
    console.warn('Transition parameter may be abnormal!')

    return [endState]
  }
}

/**
 * @description Check if the parameters are legal
 * @param {String} tBC      Name of transition bezier curve
 * @param {Any} startState  Transition start state
 * @param {Any} endState    Transition end state
 * @param {Number} frameNum Number of transition frames
 * @return {Boolean} Is the parameter legal
 */
function checkParams (tBC, startState = false, endState = false, frameNum = 30) {
  if (!tBC || startState === false || endState === false || !frameNum) {
    console.error('transition: Missing Parameters!')

    return false
  }

  if (typeof startState !== typeof endState) {
    console.error('transition: Inconsistent Status Types!')

    return false
  }

  const stateType = typeof endState

  if (stateType === 'string' || stateType === 'boolean' || !tBC.length) {
    console.error('transition: Unsupported Data Type of State!')

    return false
  }

  if (!curves.has(tBC) && !(tBC instanceof Array)) {
    console.warn('transition: Transition curve not found, default curve will be used!')
  }

  return true
}

/**
 * @description Get the transition bezier curve
 * @param {String} tBC Name of transition bezier curve
 * @return {Array} Bezier curve data
 */
function getBezierCurve (tBC) {
  let bezierCurve = ''

  if (curves.has(tBC)) {
    bezierCurve = curves.get(tBC)
  } else if (tBC instanceof Array) {
    bezierCurve = tBC
  } else {
    bezierCurve = curves.get(defaultTransitionBC)
  }

  return bezierCurve
}

/**
 * @description Get the progress of each frame state
 * @param {Array} bezierCurve Transition bezier curve
 * @param {Number} frameNum   Number of transition frames
 * @return {Array} Progress of each frame state
 */
function getFrameStateProgress (bezierCurve, frameNum) {
  const tMinus = 1 / (frameNum - 1)

  const tState = new Array(frameNum).fill(0).map((t, i) => i * tMinus)

  const frameState = tState.map(t => getFrameStateFromT(bezierCurve, t))

  return frameState
}

/**
 * @description Get the progress of the corresponding frame according to t
 * @param {Array} bezierCurve Transition bezier curve
 * @param {Number} t          Current frame t
 * @return {Number} Progress of current frame
 */
function getFrameStateFromT (bezierCurve, t) {
  const tBezierCurvePoint = getBezierCurvePointFromT(bezierCurve, t)

  const bezierCurvePointT = getBezierCurvePointTFromReT(tBezierCurvePoint, t)

  return getBezierCurveTState(tBezierCurvePoint, bezierCurvePointT)
}

/**
 * @description Get the corresponding sub-curve according to t
 * @param {Array} bezierCurve Transition bezier curve
 * @param {Number} t          Current frame t
 * @return {Array} Sub-curve of t
 */
function getBezierCurvePointFromT (bezierCurve, t) {
  const lastIndex = bezierCurve.length - 1

  let [begin, end] = ['', '']

  bezierCurve.findIndex((item, i) => {
    if (i === lastIndex) return

    begin = item
    end = bezierCurve[i + 1]

    const currentMainPointX = begin[0][0]
    const nextMainPointX = end[0][0]

    return t >= currentMainPointX && t < nextMainPointX
  })

  const p0 = begin[0]
  const p1 = begin[2] || begin[0]
  const p2 = end[1] || end[0]
  const p3 = end[0]

  return [p0, p1, p2, p3]
}

/**
 * @description Get local t based on t and sub-curve
 * @param {Array} bezierCurve Sub-curve
 * @param {Number} t          Current frame t
 * @return {Number} local t of sub-curve
 */
function getBezierCurvePointTFromReT (bezierCurve, t) {
  const reBeginX = bezierCurve[0][0]
  const reEndX = bezierCurve[3][0]

  const xMinus = reEndX - reBeginX

  const tMinus = t - reBeginX

  return tMinus / xMinus
}

/**
 * @description Get the curve progress of t
 * @param {Array} bezierCurve Sub-curve
 * @param {Number} t          Current frame t
 * @return {Number} Progress of current frame
 */
function getBezierCurveTState ([[, p0], [, p1], [, p2], [, p3]], t) {
  const { pow } = Math

  const tMinus = 1 - t

  const result1 = p0 * pow(tMinus, 3)

  const result2 = 3 * p1 * t * pow(tMinus, 2)

  const result3 = 3 * p2 * pow(t, 2) * tMinus

  const result4 = p3 * pow(t, 3)

  return 1 - (result1 + result2 + result3 + result4)
}

/**
 * @description Get transition state according to frame progress
 * @param {Any} startState   Transition start state
 * @param {Any} endState     Transition end state
 * @param {Array} frameState Frame state progress
 * @return {Array} Transition frame state
 */
function getTransitionState (begin, end, frameState) {
  let stateType = 'object'

  if (typeof begin === 'number') stateType = 'number'
  if (begin instanceof Array) stateType = 'array'

  if (stateType === 'number') return getNumberTransitionState(begin, end, frameState)
  if (stateType === 'array') return getArrayTransitionState(begin, end, frameState)
  if (stateType === 'object') return getObjectTransitionState(begin, end, frameState)

  return frameState.map(t => end)
}

/**
 * @description Get the transition data of the number type
 * @param {Number} startState Transition start state
 * @param {Number} endState   Transition end state
 * @param {Array} frameState  Frame state progress
 * @return {Array} Transition frame state
 */
function getNumberTransitionState (begin, end, frameState) {
  const minus = end - begin

  return frameState.map(s => begin + minus * s)
}

/**
 * @description Get the transition data of the array type
 * @param {Array} startState Transition start state
 * @param {Array} endState   Transition end state
 * @param {Array} frameState Frame state progress
 * @return {Array} Transition frame state
 */
function getArrayTransitionState (begin, end, frameState) {
  const minus = end.map((v, i) => {
    if (typeof v !== 'number') return false

    return v - begin[i]
  })

  return frameState.map(s =>
    minus.map((v, i) => {
      if (v === false) return end[i]

      return begin[i] + v * s
    }))
}

/**
 * @description Get the transition data of the object type
 * @param {Object} startState Transition start state
 * @param {Object} endState   Transition end state
 * @param {Array} frameState  Frame state progress
 * @return {Array} Transition frame state
 */
function getObjectTransitionState (begin, end, frameState) {
  const keys = Object.keys(end)

  const beginValue = keys.map(k => begin[k])
  const endValue = keys.map(k => end[k])

  const arrayState = getArrayTransitionState(beginValue, endValue, frameState)

  return arrayState.map(item => {
    const frameData = {}

    item.forEach((v, i) => (frameData[keys[i]] = v))

    return frameData
  })
}

/**
 * @description Get the transition state data by recursion
 * @param {Array|Object} startState Transition start state
 * @param {Array|Object} endState   Transition end state
 * @param {Array} frameState        Frame state progress
 * @return {Array} Transition frame state
 */
function recursionTransitionState (begin, end, frameState) {
  const state = getTransitionState(begin, end, frameState)

  for (let key in end) {
    const bTemp = begin[key]
    const eTemp = end[key]

    if (typeof eTemp !== 'object') continue

    const data = recursionTransitionState(bTemp, eTemp, frameState)

    state.forEach((fs, i) => (fs[key] = data[i]))
  }

  return state
}

/**
 * @description Inject new curve into curves as config
 * @param {Any} key     The key of curve
 * @param {Array} curve Bezier curve data
 * @return {Undefined} No return
 */
export function injectNewCurve (key, curve) {
  if (!key || !curve) {
    console.error('InjectNewCurve Missing Parameters!')

    return
  }

  curves.set(key, curve)
}

export default transition
