/**
 * Copyright 2004-present Facebook. All Rights Reserved.
 *
 * @providesModule buildStyleInterpolator
 */

/**
 * Cannot "use strict" because we must use eval in this file.
 */
/* eslint-disable global-strict */

var keyOf = require('fbjs/lib/keyOf');

var X_DIM = keyOf({x: null});
var Y_DIM = keyOf({y: null});
var Z_DIM = keyOf({z: null});
var W_DIM = keyOf({w: null});

var TRANSFORM_ROTATE_NAME = keyOf({transformRotateRadians: null});

var ShouldAllocateReusableOperationVars = {
  transformRotateRadians: true,
  transformScale: true,
  transformTranslate: true,
};

var InitialOperationField = {
  transformRotateRadians: [0, 0, 0, 1],
  transformTranslate: [0, 0, 0],
  transformScale: [1, 1, 1],
};


/**
 * Creates a highly specialized animation function that may be evaluated every
 * frame. For example:
 *
 *    var ToTheLeft = {
 *      opacity: {
 *        from: 1,
 *        to: 0.7,
 *        min: 0,
 *        max: 1,
 *        type: 'linear',
 *        extrapolate: false,
 *        round: 100,
 *      },
 *      left: {
 *        from: 0,
 *        to: -SCREEN_WIDTH * 0.3,
 *        min: 0,
 *        max: 1,
 *        type: 'linear',
 *        extrapolate: true,
 *        round: PixelRatio.get(),
 *      },
 *    };
 *
 *    var toTheLeft = buildStyleInterpolator(ToTheLeft);
 *
 *  Would returns a specialized function of the form:
 *
 *    function(result, value) {
 *      var didChange = false;
 *      var nextScalarVal;
 *      var ratio;
 *      ratio = (value - 0) / 1;
 *      ratio = ratio > 1 ? 1 : (ratio < 0 ? 0 : ratio);
 *      nextScalarVal = Math.round(100 * (1 * (1 - ratio) + 0.7 * ratio)) / 100;
 *      if (!didChange) {
 *        var prevVal = result.opacity;
 *        result.opacity = nextScalarVal;
 *        didChange = didChange  || (nextScalarVal !== prevVal);
 *      } else {
 *        result.opacity = nextScalarVal;
 *      }
 *      ratio = (value - 0) / 1;
 *      nextScalarVal = Math.round(2 * (0 * (1 - ratio) + -30 * ratio)) / 2;
 *      if (!didChange) {
 *        var prevVal = result.left;
 *        result.left = nextScalarVal;
 *        didChange = didChange  || (nextScalarVal !== prevVal);
 *      } else {
 *        result.left = nextScalarVal;
 *      }
 *      return didChange;
 *    }
 */

var ARGUMENT_NAMES_RE = /([^\s,]+)/g;
/**
 * This is obviously a huge hack. Proper tooling would allow actual inlining.
 * This only works in a few limited cases (where there is no function return
 * value, and the function operates mutatively on parameters).
 *
 * Example:
 *
 *
 *   var inlineMe(a, b) {
 *      a = b + b;
 *  };
 *
 *  inline(inlineMe, ['hi', 'bye']);  // "hi = bye + bye;"
 *
 * @param {function} func Any simple function who's arguments can be replaced via a regex.
 * @param {array<string>} replaceWithArgs Corresponding names of variables
 * within an environment, to replace `func` args with.
 * @return {string} Resulting function body string.
 */
var inline = function(func, replaceWithArgs) {
  var fnStr = func.toString();
  var parameterNames = fnStr.slice(fnStr.indexOf('(') + 1, fnStr.indexOf(')'))
    .match(ARGUMENT_NAMES_RE) ||
    [];
  var replaceRegexStr = parameterNames.map(function(paramName) {
    return '\\b' + paramName + '\\b';
  }).join('|');
  var replaceRegex = new RegExp(replaceRegexStr, 'g');
  var fnBody = fnStr.substring(fnStr.indexOf('{') + 1, fnStr.lastIndexOf('}'));
  var newFnBody = fnBody.replace(replaceRegex, function(parameterName) {
    var indexInParameterNames = parameterNames.indexOf(parameterName);
    var replacementName = replaceWithArgs[indexInParameterNames];
    return replacementName;
  });
  return newFnBody.split('\n');
};

/**
 * Simply a convenient way to inline functions using the function's toString
 * method.
 */
var MatrixOps = {
  unroll: function(matVar, m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15) {
    m0 = matVar[0];
    m1 = matVar[1];
    m2 = matVar[2];
    m3 = matVar[3];
    m4 = matVar[4];
    m5 = matVar[5];
    m6 = matVar[6];
    m7 = matVar[7];
    m8 = matVar[8];
    m9 = matVar[9];
    m10 = matVar[10];
    m11 = matVar[11];
    m12 = matVar[12];
    m13 = matVar[13];
    m14 = matVar[14];
    m15 = matVar[15];
  },

  matrixDiffers: function(retVar, matVar, m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15) {
    retVar = retVar ||
      m0 !== matVar[0] ||
      m1 !== matVar[1] ||
      m2 !== matVar[2] ||
      m3 !== matVar[3] ||
      m4 !== matVar[4] ||
      m5 !== matVar[5] ||
      m6 !== matVar[6] ||
      m7 !== matVar[7] ||
      m8 !== matVar[8] ||
      m9 !== matVar[9] ||
      m10 !== matVar[10] ||
      m11 !== matVar[11] ||
      m12 !== matVar[12] ||
      m13 !== matVar[13] ||
      m14 !== matVar[14] ||
      m15 !== matVar[15];
  },

  transformScale: function(matVar, opVar) {
    // Scaling matVar by opVar
    var x = opVar[0];
    var y = opVar[1];
    var z = opVar[2];
    matVar[0] = matVar[0] * x;
    matVar[1] = matVar[1] * x;
    matVar[2] = matVar[2] * x;
    matVar[3] = matVar[3] * x;
    matVar[4] = matVar[4] * y;
    matVar[5] = matVar[5] * y;
    matVar[6] = matVar[6] * y;
    matVar[7] = matVar[7] * y;
    matVar[8] = matVar[8] * z;
    matVar[9] = matVar[9] * z;
    matVar[10] = matVar[10] * z;
    matVar[11] = matVar[11] * z;
    matVar[12] = matVar[12];
    matVar[13] = matVar[13];
    matVar[14] = matVar[14];
    matVar[15] = matVar[15];
  },

  /**
   * All of these matrix transforms are not general purpose utilities, and are
   * only suitable for being inlined for the use of building up interpolators.
   */
  transformTranslate: function(matVar, opVar) {
    // Translating matVar by opVar
    var x = opVar[0];
    var y = opVar[1];
    var z = opVar[2];
    matVar[12] = matVar[0] * x + matVar[4] * y + matVar[8] * z + matVar[12];
    matVar[13] = matVar[1] * x + matVar[5] * y + matVar[9] * z + matVar[13];
    matVar[14] = matVar[2] * x + matVar[6] * y + matVar[10] * z + matVar[14];
    matVar[15] = matVar[3] * x + matVar[7] * y + matVar[11] * z + matVar[15];
  },

  /**
   * @param {array} matVar Both the input, and the output matrix.
   * @param {quaternion specification} q Four element array describing rotation.
   */
  transformRotateRadians: function(matVar, q) {
    // Rotating matVar by q
    var xQuat = q[0], yQuat = q[1], zQuat = q[2], wQuat = q[3];
    var x2Quat = xQuat + xQuat;
    var y2Quat = yQuat + yQuat;
    var z2Quat = zQuat + zQuat;
    var xxQuat = xQuat * x2Quat;
    var xyQuat = xQuat * y2Quat;
    var xzQuat = xQuat * z2Quat;
    var yyQuat = yQuat * y2Quat;
    var yzQuat = yQuat * z2Quat;
    var zzQuat = zQuat * z2Quat;
    var wxQuat = wQuat * x2Quat;
    var wyQuat = wQuat * y2Quat;
    var wzQuat = wQuat * z2Quat;
    // Step 1: Inlines the construction of a quaternion matrix (`quatMat`)
    var quatMat0 = 1 - (yyQuat + zzQuat);
    var quatMat1 = xyQuat + wzQuat;
    var quatMat2 = xzQuat - wyQuat;
    var quatMat4 = xyQuat - wzQuat;
    var quatMat5 = 1 - (xxQuat + zzQuat);
    var quatMat6 = yzQuat + wxQuat;
    var quatMat8 = xzQuat + wyQuat;
    var quatMat9 = yzQuat - wxQuat;
    var quatMat10 = 1 - (xxQuat + yyQuat);
    // quatMat3/7/11/12/13/14 = 0, quatMat15 = 1

    // Step 2: Inlines multiplication, takes advantage of constant quatMat cells
    var a00 = matVar[0];
    var a01 = matVar[1];
    var a02 = matVar[2];
    var a03 = matVar[3];
    var a10 = matVar[4];
    var a11 = matVar[5];
    var a12 = matVar[6];
    var a13 = matVar[7];
    var a20 = matVar[8];
    var a21 = matVar[9];
    var a22 = matVar[10];
    var a23 = matVar[11];

    var b0  = quatMat0, b1 = quatMat1, b2 = quatMat2;
    matVar[0] = b0 * a00 + b1 * a10 + b2 * a20;
    matVar[1] = b0 * a01 + b1 * a11 + b2 * a21;
    matVar[2] = b0 * a02 + b1 * a12 + b2 * a22;
    matVar[3] = b0 * a03 + b1 * a13 + b2 * a23;
    b0 = quatMat4; b1 = quatMat5; b2 = quatMat6;
    matVar[4] = b0 * a00 + b1 * a10 + b2 * a20;
    matVar[5] = b0 * a01 + b1 * a11 + b2 * a21;
    matVar[6] = b0 * a02 + b1 * a12 + b2 * a22;
    matVar[7] = b0 * a03 + b1 * a13 + b2 * a23;
    b0 = quatMat8; b1 = quatMat9; b2 = quatMat10;
    matVar[8] = b0 * a00 + b1 * a10 + b2 * a20;
    matVar[9] = b0 * a01 + b1 * a11 + b2 * a21;
    matVar[10] = b0 * a02 + b1 * a12 + b2 * a22;
    matVar[11] = b0 * a03 + b1 * a13 + b2 * a23;
  }
};

// Optimized version of general operation applications that can be used when
// the target matrix is known to be the identity matrix.
var MatrixOpsInitial = {
  transformScale: function(matVar, opVar) {
    // Scaling matVar known to be identity by opVar
    matVar[0] = opVar[0];
    matVar[1] = 0;
    matVar[2] = 0;
    matVar[3] = 0;
    matVar[4] = 0;
    matVar[5] = opVar[1];
    matVar[6] = 0;
    matVar[7] = 0;
    matVar[8] = 0;
    matVar[9] = 0;
    matVar[10] = opVar[2];
    matVar[11] = 0;
    matVar[12] = 0;
    matVar[13] = 0;
    matVar[14] = 0;
    matVar[15] = 1;
  },

  transformTranslate: function(matVar, opVar) {
    // Translating matVar known to be identity by opVar';
    matVar[0] = 1;
    matVar[1] = 0;
    matVar[2] = 0;
    matVar[3] = 0;
    matVar[4] = 0;
    matVar[5] = 1;
    matVar[6] = 0;
    matVar[7] = 0;
    matVar[8] = 0;
    matVar[9] = 0;
    matVar[10] = 1;
    matVar[11] = 0;
    matVar[12] = opVar[0];
    matVar[13] = opVar[1];
    matVar[14] = opVar[2];
    matVar[15] = 1;
  },

  /**
   * @param {array} matVar Both the input, and the output matrix - assumed to be
   * identity.
   * @param {quaternion specification} q Four element array describing rotation.
   */
  transformRotateRadians: function(matVar, q) {

    // Rotating matVar which is known to be identity by q
    var xQuat = q[0], yQuat = q[1], zQuat = q[2], wQuat = q[3];
    var x2Quat = xQuat + xQuat;
    var y2Quat = yQuat + yQuat;
    var z2Quat = zQuat + zQuat;
    var xxQuat = xQuat * x2Quat;
    var xyQuat = xQuat * y2Quat;
    var xzQuat = xQuat * z2Quat;
    var yyQuat = yQuat * y2Quat;
    var yzQuat = yQuat * z2Quat;
    var zzQuat = zQuat * z2Quat;
    var wxQuat = wQuat * x2Quat;
    var wyQuat = wQuat * y2Quat;
    var wzQuat = wQuat * z2Quat;
    // Step 1: Inlines the construction of a quaternion matrix (`quatMat`)
    var quatMat0 = 1 - (yyQuat + zzQuat);
    var quatMat1 = xyQuat + wzQuat;
    var quatMat2 = xzQuat - wyQuat;
    var quatMat4 = xyQuat - wzQuat;
    var quatMat5 = 1 - (xxQuat + zzQuat);
    var quatMat6 = yzQuat + wxQuat;
    var quatMat8 = xzQuat + wyQuat;
    var quatMat9 = yzQuat - wxQuat;
    var quatMat10 = 1 - (xxQuat + yyQuat);
    // quatMat3/7/11/12/13/14 = 0, quatMat15 = 1

    // Step 2: Inlines the multiplication with identity matrix.
    var b0  = quatMat0, b1 = quatMat1, b2 = quatMat2;
    matVar[0] = b0;
    matVar[1] = b1;
    matVar[2] = b2;
    matVar[3] = 0;
    b0 = quatMat4; b1 = quatMat5; b2 = quatMat6;
    matVar[4] = b0;
    matVar[5] = b1;
    matVar[6] = b2;
    matVar[7] = 0;
    b0 = quatMat8; b1 = quatMat9; b2 = quatMat10;
    matVar[8] = b0;
    matVar[9] = b1;
    matVar[10] = b2;
    matVar[11] = 0;
    matVar[12] = 0;
    matVar[13] = 0;
    matVar[14] = 0;
    matVar[15] = 1;
  }
};


var setNextValAndDetectChange = function(name, tmpVarName) {
  return (
    '  if (!didChange) {\n' +
    '    var prevVal = result.' + name + ';\n' +
    '    result.' + name + ' = ' + tmpVarName + ';\n' +
    '    didChange = didChange  || (' + tmpVarName + ' !== prevVal);\n' +
    '  } else {\n' +
    '    result.' + name + ' = ' + tmpVarName + ';\n' +
    '  }\n'
  );
};

var computeNextValLinear = function(anim, from, to, tmpVarName) {
  var hasRoundRatio = 'round' in anim;
  var roundRatio = anim.round;
  var fn = '  ratio = (value - ' + anim.min + ') / ' + (anim.max - anim.min) + ';\n';
  if (!anim.extrapolate) {
    fn += '  ratio = ratio > 1 ? 1 : (ratio < 0 ? 0 : ratio);\n';
  }

  var roundOpen = (hasRoundRatio ? 'Math.round(' + roundRatio + ' * ' : '' );
  var roundClose = (hasRoundRatio ? ') / ' + roundRatio : '' );
  fn +=
    '  ' + tmpVarName + ' = ' +
       roundOpen +
       '(' + from + ' * (1 - ratio) + ' + to + ' * ratio)' +
       roundClose + ';\n';
  return fn;
};

var computeNextValLinearScalar = function(anim) {
  return computeNextValLinear(anim, anim.from, anim.to, 'nextScalarVal');
};

var computeNextValConstant = function(anim) {
  var constantExpression = JSON.stringify(anim.value);
  return '  nextScalarVal = ' + constantExpression + ';\n';
};

var computeNextValStep = function(anim) {
  return (
    '  nextScalarVal = value >= ' +
         (anim.threshold + ' ? ' + anim.to + ' : ' + anim.from) + ';\n'
  );
};

var computeNextValIdentity = function(anim) {
  return '  nextScalarVal = value;\n';
};

var operationVar = function(name) {
  return name + 'ReuseOp';
};

var createReusableOperationVars = function(anims) {
  var ret = '';
  for (var name in anims) {
    if (ShouldAllocateReusableOperationVars[name]) {
      ret += 'var ' + operationVar(name) + ' = [];\n';
    }
  }
  return ret;
};

var newlines = function(statements) {
  return '\n' + statements.join('\n') + '\n';
};

/**
 * @param {Animation} anim Configuration entry.
 * @param {key} dimension Key to examine in `from`/`to`.
 * @param {number} index Field in operationVar to set.
 * @return {string} Code that sets the operation variable's field.
 */
var computeNextMatrixOperationField = function(anim, name, dimension, index) {
  var fieldAccess = operationVar(name) + '[' + index + ']';
  if (anim.from[dimension] !== undefined && anim.to[dimension] !== undefined) {
    return '  ' + anim.from[dimension] !== anim.to[dimension] ?
        computeNextValLinear(anim, anim.from[dimension], anim.to[dimension], fieldAccess) :
        fieldAccess + ' = ' + anim.from[dimension] + ';';
  } else {
    return '  ' + fieldAccess + ' = ' + InitialOperationField[name][index] + ';';
  }
};

var unrolledVars = [];
for (var varIndex = 0; varIndex < 16; varIndex++) {
  unrolledVars.push('m' + varIndex);
}
var setNextMatrixAndDetectChange = function(orderedMatrixOperations) {
  var fn = [
    '  var transform = result.transform !== undefined ? ' +
    'result.transform : (result.transform = [{ matrix: [] }]);' +
    '  var transformMatrix = transform[0].matrix;'
  ];
  fn.push.apply(
    fn,
    inline(MatrixOps.unroll, ['transformMatrix'].concat(unrolledVars))
  );
  for (var i = 0; i < orderedMatrixOperations.length; i++) {
    var opName = orderedMatrixOperations[i];
    if (i === 0) {
      fn.push.apply(
        fn,
        inline(MatrixOpsInitial[opName], ['transformMatrix', operationVar(opName)])
      );
    } else {
      fn.push.apply(
        fn,
        inline(MatrixOps[opName], ['transformMatrix', operationVar(opName)])
      );
    }
  }
  fn.push.apply(
    fn,
    inline(MatrixOps.matrixDiffers, ['didChange', 'transformMatrix'].concat(unrolledVars))
  );
  return fn;
};

var InterpolateMatrix = {
  transformTranslate: true,
  transformRotateRadians: true,
  transformScale: true,
};

var createFunctionString = function(anims) {
  // We must track the order they appear in so transforms are applied in the
  // correct order.
  var orderedMatrixOperations = [];

  // Wrapping function allows the final function to contain state (for
  // caching).
  var fn = 'return (function() {\n';
  fn += createReusableOperationVars(anims);
  fn += 'return function(result, value) {\n';
  fn += '  var didChange = false;\n';
  fn += '  var nextScalarVal;\n';
  fn += '  var ratio;\n';

  for (var name in anims) {
    var anim = anims[name];
    if (anim.type === 'linear') {
      if (InterpolateMatrix[name]) {
        orderedMatrixOperations.push(name);
        var setOperations = [
          computeNextMatrixOperationField(anim, name, X_DIM, 0),
          computeNextMatrixOperationField(anim, name, Y_DIM, 1),
          computeNextMatrixOperationField(anim, name, Z_DIM, 2)
        ];
        if (name === TRANSFORM_ROTATE_NAME) {
          setOperations.push(computeNextMatrixOperationField(anim, name, W_DIM, 3));
        }
        fn += newlines(setOperations);
      } else {
        fn += computeNextValLinearScalar(anim, 'nextScalarVal');
        fn += setNextValAndDetectChange(name, 'nextScalarVal');
      }
    } else if (anim.type === 'constant') {
      fn += computeNextValConstant(anim);
      fn += setNextValAndDetectChange(name, 'nextScalarVal');
    } else if (anim.type === 'step') {
      fn += computeNextValStep(anim);
      fn += setNextValAndDetectChange(name, 'nextScalarVal');
    } else if (anim.type === 'identity') {
      fn += computeNextValIdentity(anim);
      fn += setNextValAndDetectChange(name, 'nextScalarVal');
    }
  }
  if (orderedMatrixOperations.length) {
    fn += newlines(setNextMatrixAndDetectChange(orderedMatrixOperations));
  }
  fn += '  return didChange;\n';
  fn += '};\n';
  fn += '})()';
  return fn;
};

/**
 * @param {object} anims Animation configuration by style property name.
 * @return {function} Function accepting style object, that mutates that style
 * object and returns a boolean describing if any update was actually applied.
 */
var buildStyleInterpolator = function(anims) {
  // Defer compiling this method until we really need it.
  var interpolator = null;
  function lazyStyleInterpolator(result, value) {
    if (interpolator === null) {
      interpolator = Function(createFunctionString(anims))();
    }
    return interpolator(result, value);
  }
  return lazyStyleInterpolator;
};

module.exports = buildStyleInterpolator;
