// Copyright 2014 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
// limitations under the License.

(function(scope, testing) {

  // Evaluates a calc expression.
  // https://drafts.csswg.org/css-values-3/#calc-notation
  function calculate(expression) {
    // In calc expressions, white space is required on both sides of the
    // + and - operators. https://drafts.csswg.org/css-values-3/#calc-notation
    // Thus any + or - immediately adjacent to . or 0..9 is part of the number,
    // e.g. -1.23e+45
    // This regular expression matches ( ) * / + - and numbers.
    var tokenRegularExpression = /([\+\-\w\.]+|[\(\)\*\/])/g;
    var currentToken;
    function consume() {
      var matchResult = tokenRegularExpression.exec(expression);
      if (matchResult)
        currentToken = matchResult[0];
      else
        currentToken = undefined;
    }
    consume(); // Read the initial token.

    function calcNumber() {
      // https://drafts.csswg.org/css-values-3/#number-value
      var result = Number(currentToken);
      consume();
      return result;
    }

    function calcValue() {
      // <calc-value> = <number> | <dimension> | <percentage> | ( <calc-sum> )
      if (currentToken !== '(')
        return calcNumber();
      consume();
      var result = calcSum();
      if (currentToken !== ')')
        return NaN;
      consume();
      return result;
    }

    function calcProduct() {
      // <calc-product> = <calc-value> [ '*' <calc-value> | '/' <calc-number-value> ]*
      var left = calcValue();
      while (currentToken === '*' || currentToken === '/') {
        var operator = currentToken;
        consume();
        var right = calcValue();
        if (operator === '*')
          left *= right;
        else
          left /= right;
      }
      return left;
    }

    function calcSum() {
      // <calc-sum> = <calc-product> [ [ '+' | '-' ] <calc-product> ]*
      var left = calcProduct();
      while (currentToken === '+' || currentToken === '-') {
        var operator = currentToken;
        consume();
        var right = calcProduct();
        if (operator === '+')
          left += right;
        else
          left -= right;
      }
      return left;
    }

    // <calc()> = calc( <calc-sum> )
    return calcSum();
  }

  function parseDimension(unitRegExp, string) {
    string = string.trim().toLowerCase();

    if (string == '0' && 'px'.search(unitRegExp) >= 0)
      return {px: 0};

    // If we have parenthesis, we're a calc and need to start with 'calc'.
    if (!/^[^(]*$|^calc/.test(string))
      return;
    string = string.replace(/calc\(/g, '(');

    // We tag units by prefixing them with 'U' (note that we are already
    // lowercase) to prevent problems with types which are substrings of
    // each other (although prefixes may be problematic!)
    var matchedUnits = {};
    string = string.replace(unitRegExp, function(match) {
      matchedUnits[match] = null;
      return 'U' + match;
    });
    var taggedUnitRegExp = 'U(' + unitRegExp.source + ')';

    // Validating input is simply applying as many reductions as we can.
    var typeCheck = string.replace(/[-+]?(\d*\.)?\d+([Ee][-+]?\d+)?/g, 'N')
        .replace(new RegExp('N' + taggedUnitRegExp, 'g'), 'D')
        .replace(/\s[+-]\s/g, 'O')
        .replace(/\s/g, '');
    var reductions = [/N\*(D)/g, /(N|D)[*/]N/g, /(N|D)O\1/g, /\((N|D)\)/g];
    var i = 0;
    while (i < reductions.length) {
      if (reductions[i].test(typeCheck)) {
        typeCheck = typeCheck.replace(reductions[i], '$1');
        i = 0;
      } else {
        i++;
      }
    }
    if (typeCheck != 'D')
      return;

    for (var unit in matchedUnits) {
      var result = calculate(string.replace(new RegExp('U' + unit, 'g'), '').replace(new RegExp(taggedUnitRegExp, 'g'), '*0'));
      if (!isFinite(result))
        return;
      matchedUnits[unit] = result;
    }
    return matchedUnits;
  }

  function mergeDimensionsNonNegative(left, right) {
    return mergeDimensions(left, right, true);
  }

  function mergeDimensions(left, right, nonNegative) {
    var units = [], unit;
    for (unit in left)
      units.push(unit);
    for (unit in right) {
      if (units.indexOf(unit) < 0)
        units.push(unit);
    }

    left = units.map(function(unit) { return left[unit] || 0; });
    right = units.map(function(unit) { return right[unit] || 0; });
    return [left, right, function(values) {
      var result = values.map(function(value, i) {
        if (values.length == 1 && nonNegative) {
          value = Math.max(value, 0);
        }
        // Scientific notation (e.g. 1e2) is not yet widely supported by browser vendors.
        return scope.numberToString(value) + units[i];
      }).join(' + ');
      return values.length > 1 ? 'calc(' + result + ')' : result;
    }];
  }

  var lengthUnits = 'px|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc';
  var parseLength = parseDimension.bind(null, new RegExp(lengthUnits, 'g'));
  var parseLengthOrPercent = parseDimension.bind(null, new RegExp(lengthUnits + '|%', 'g'));
  var parseAngle = parseDimension.bind(null, /deg|rad|grad|turn/g);

  scope.parseLength = parseLength;
  scope.parseLengthOrPercent = parseLengthOrPercent;
  scope.consumeLengthOrPercent = scope.consumeParenthesised.bind(null, parseLengthOrPercent);
  scope.parseAngle = parseAngle;
  scope.mergeDimensions = mergeDimensions;

  var consumeLength = scope.consumeParenthesised.bind(null, parseLength);
  var consumeSizePair = scope.consumeRepeated.bind(undefined, consumeLength, /^/);
  var consumeSizePairList = scope.consumeRepeated.bind(undefined, consumeSizePair, /^,/);
  scope.consumeSizePairList = consumeSizePairList;

  var parseSizePairList = function(input) {
    var result = consumeSizePairList(input);
    if (result && result[1] == '') {
      return result[0];
    }
  };

  var mergeNonNegativeSizePair = scope.mergeNestedRepeated.bind(undefined, mergeDimensionsNonNegative, ' ');
  var mergeNonNegativeSizePairList = scope.mergeNestedRepeated.bind(undefined, mergeNonNegativeSizePair, ',');
  scope.mergeNonNegativeSizePair = mergeNonNegativeSizePair;

  scope.addPropertiesHandler(parseSizePairList, mergeNonNegativeSizePairList, [
    'background-size'
  ]);

  scope.addPropertiesHandler(parseLengthOrPercent, mergeDimensionsNonNegative, [
    'border-bottom-width',
    'border-image-width',
    'border-left-width',
    'border-right-width',
    'border-top-width',
    'flex-basis',
    'font-size',
    'height',
    'line-height',
    'max-height',
    'max-width',
    'outline-width',
    'width',
  ]);

  scope.addPropertiesHandler(parseLengthOrPercent, mergeDimensions, [
    'border-bottom-left-radius',
    'border-bottom-right-radius',
    'border-top-left-radius',
    'border-top-right-radius',
    'bottom',
    'left',
    'letter-spacing',
    'margin-bottom',
    'margin-left',
    'margin-right',
    'margin-top',
    'min-height',
    'min-width',
    'outline-offset',
    'padding-bottom',
    'padding-left',
    'padding-right',
    'padding-top',
    'perspective',
    'right',
    'shape-margin',
    'stroke-dashoffset',
    'text-indent',
    'top',
    'vertical-align',
    'word-spacing',
  ]);

})(webAnimations1, webAnimationsTesting);
