// 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) {

  var SVG_TRANSFORM_PROP = '_webAnimationsUpdateSvgTransformAttr';

  /**
   * IE/Edge do not support `transform` styles for SVG elements. Instead,
   * `transform` attribute can be animated with some restrictions.
   * See https://connect.microsoft.com/IE/feedback/details/811744/ie11-bug-with-implementation-of-css-transforms-in-svg,
   * https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/1173754/,
   * https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/101242/, etc.
   * The same problem is exhibited by pre-Chrome Android browsers (ICS).
   * Unfortunately, there's no easy way to feature-detect it.
   */
  function updateSvgTransformAttr(window, element) {
    if (!element.namespaceURI || element.namespaceURI.indexOf('/svg') == -1) {
      return false;
    }
    if (!(SVG_TRANSFORM_PROP in window)) {
      window[SVG_TRANSFORM_PROP] =
          /Trident|MSIE|IEMobile|Edge|Android 4/i.test(window.navigator.userAgent);
    }
    return window[SVG_TRANSFORM_PROP];
  }

  var styleAttributes = {
    cssText: 1,
    length: 1,
    parentRule: 1,
  };

  var styleMethods = {
    getPropertyCSSValue: 1,
    getPropertyPriority: 1,
    getPropertyValue: 1,
    item: 1,
    removeProperty: 1,
    setProperty: 1,
  };

  var styleMutatingMethods = {
    removeProperty: 1,
    setProperty: 1,
  };

  function configureProperty(object, property, descriptor) {
    descriptor.enumerable = true;
    descriptor.configurable = true;
    Object.defineProperty(object, property, descriptor);
  }

  function AnimatedCSSStyleDeclaration(element) {
    WEB_ANIMATIONS_TESTING && console.assert(!(element.style instanceof AnimatedCSSStyleDeclaration),
        'Element must not already have an animated style attached.');

    this._element = element;
    // Stores the inline style of the element on its behalf while the
    // polyfill uses the element's inline style to simulate web animations.
    // This is needed to fake regular inline style CSSOM access on the element.
    this._surrogateStyle = document.createElementNS('http://www.w3.org/1999/xhtml', 'div').style;
    this._style = element.style;
    this._length = 0;
    this._isAnimatedProperty = {};
    this._updateSvgTransformAttr = updateSvgTransformAttr(window, element);
    this._savedTransformAttr = null;

    // Copy the inline style contents over to the surrogate.
    for (var i = 0; i < this._style.length; i++) {
      var property = this._style[i];
      this._surrogateStyle[property] = this._style[property];
    }
    this._updateIndices();
  }

  AnimatedCSSStyleDeclaration.prototype = {
    get cssText() {
      return this._surrogateStyle.cssText;
    },
    set cssText(text) {
      var isAffectedProperty = {};
      for (var i = 0; i < this._surrogateStyle.length; i++) {
        isAffectedProperty[this._surrogateStyle[i]] = true;
      }
      this._surrogateStyle.cssText = text;
      this._updateIndices();
      for (var i = 0; i < this._surrogateStyle.length; i++) {
        isAffectedProperty[this._surrogateStyle[i]] = true;
      }
      for (var property in isAffectedProperty) {
        if (!this._isAnimatedProperty[property]) {
          this._style.setProperty(property, this._surrogateStyle.getPropertyValue(property));
        }
      }
    },
    get length() {
      return this._surrogateStyle.length;
    },
    get parentRule() {
      return this._style.parentRule;
    },
    // Mirror the indexed getters and setters of the surrogate style.
    _updateIndices: function() {
      while (this._length < this._surrogateStyle.length) {
        Object.defineProperty(this, this._length, {
          configurable: true,
          enumerable: false,
          get: (function(index) {
            return function() { return this._surrogateStyle[index]; };
          })(this._length)
        });
        this._length++;
      }
      while (this._length > this._surrogateStyle.length) {
        this._length--;
        Object.defineProperty(this, this._length, {
          configurable: true,
          enumerable: false,
          value: undefined
        });
      }
    },
    _set: function(property, value) {
      this._style[property] = value;
      this._isAnimatedProperty[property] = true;
      if (this._updateSvgTransformAttr &&
          scope.unprefixedPropertyName(property) == 'transform') {
        // On IE/Edge, also set SVG element's `transform` attribute to 2d
        // matrix of the transform. The `transform` style does not work, but
        // `transform` attribute can be used instead.
        // Notice, if the platform indeed supports SVG/CSS transforms the CSS
        // declaration is supposed to override the attribute.
        if (this._savedTransformAttr == null) {
          this._savedTransformAttr = this._element.getAttribute('transform');
        }
        this._element.setAttribute('transform', scope.transformToSvgMatrix(value));
      }
    },
    _clear: function(property) {
      this._style[property] = this._surrogateStyle[property];
      if (this._updateSvgTransformAttr &&
          scope.unprefixedPropertyName(property) == 'transform') {
        if (this._savedTransformAttr) {
          this._element.setAttribute('transform', this._savedTransformAttr);
        } else {
          this._element.removeAttribute('transform');
        }
        this._savedTransformAttr = null;
      }
      delete this._isAnimatedProperty[property];
    },
  };

  // Wrap the style methods.
  for (var method in styleMethods) {
    AnimatedCSSStyleDeclaration.prototype[method] = (function(method, modifiesStyle) {
      return function() {
        var result = this._surrogateStyle[method].apply(this._surrogateStyle, arguments);
        if (modifiesStyle) {
          if (!this._isAnimatedProperty[arguments[0]])
            this._style[method].apply(this._style, arguments);
          this._updateIndices();
        }
        return result;
      }
    })(method, method in styleMutatingMethods);
  }

  // Wrap the style.cssProperty getters and setters.
  for (var property in document.documentElement.style) {
    if (property in styleAttributes || property in styleMethods) {
      continue;
    }
    (function(property) {
      configureProperty(AnimatedCSSStyleDeclaration.prototype, property, {
        get: function() {
          return this._surrogateStyle[property];
        },
        set: function(value) {
          this._surrogateStyle[property] = value;
          this._updateIndices();
          if (!this._isAnimatedProperty[property])
            this._style[property] = value;
        }
      });
    })(property);
  }

  function ensureStyleIsPatched(element) {
    if (element._webAnimationsPatchedStyle)
      return;

    var animatedStyle = new AnimatedCSSStyleDeclaration(element);
    try {
      configureProperty(element, 'style', { get: function() { return animatedStyle; } });
    } catch (_) {
      // iOS and older versions of Safari (pre v7) do not support overriding an element's
      // style object. Animations will clobber any inline styles as a result.
      element.style._set = function(property, value) {
        element.style[property] = value;
      };
      element.style._clear = function(property) {
        element.style[property] = '';
      };
    }

    // We must keep a handle on the patched style to prevent it from getting GC'd.
    element._webAnimationsPatchedStyle = element.style;
  }

  scope.apply = function(element, property, value) {
    ensureStyleIsPatched(element);
    element.style._set(scope.propertyName(property), value);
  };

  scope.clear = function(element, property) {
    if (element._webAnimationsPatchedStyle) {
      element.style._clear(scope.propertyName(property));
    }
  };

  if (WEB_ANIMATIONS_TESTING) {
    testing.ensureStyleIsPatched = ensureStyleIsPatched;
    testing.updateSvgTransformAttr = updateSvgTransformAttr;
  }

})(webAnimations1, webAnimationsTesting);
