/*
Copyright 2013-2015 ASIAL CORPORATION

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.

*/

/**
 * Minimal animation library for managing css transition on mobile browsers.
 */
'use strict';

import _Object$keys from 'babel-runtime/core-js/object/keys';
import _setImmediate from 'babel-runtime/core-js/set-immediate';
var TIMEOUT_RATIO = 1.4;

var util = {};

// capitalize string
util.capitalize = function (str) {
  return str.charAt(0).toUpperCase() + str.slice(1);
};

/**
 * @param {Object} params
 * @param {String} params.property
 * @param {Float} params.duration
 * @param {String} params.timing
 */
util.buildTransitionValue = function (params) {
  params.property = params.property || 'all';
  params.duration = params.duration || 0.4;
  params.timing = params.timing || 'linear';

  var props = params.property.split(/ +/);

  return props.map(function (prop) {
    return prop + ' ' + params.duration + 's ' + params.timing;
  }).join(', ');
};

/**
 * Add an event handler on "transitionend" event.
 */
util.onceOnTransitionEnd = function (element, callback) {
  if (!element) {
    return function () {};
  }

  var fn = function fn(event) {
    if (element == event.target) {
      event.stopPropagation();
      removeListeners();

      callback();
    }
  };

  var removeListeners = function removeListeners() {
    util._transitionEndEvents.forEach(function (eventName) {
      element.removeEventListener(eventName, fn, false);
    });
  };

  util._transitionEndEvents.forEach(function (eventName) {
    element.addEventListener(eventName, fn, false);
  });

  return removeListeners;
};

util._transitionEndEvents = function () {

  if ('ontransitionend' in window) {
    return ['transitionend'];
  }

  if ('onwebkittransitionend' in window) {
    return ['webkitTransitionEnd'];
  }

  if (util.vendorPrefix === 'webkit' || util.vendorPrefix === 'o' || util.vendorPrefix === 'moz' || util.vendorPrefix === 'ms') {
    return [util.vendorPrefix + 'TransitionEnd', 'transitionend'];
  }

  return [];
}();

util._cssPropertyDict = function () {
  var styles = window.getComputedStyle(document.documentElement, '');
  var dict = {};
  var a = 'A'.charCodeAt(0);
  var z = 'z'.charCodeAt(0);

  var upper = function upper(s) {
    return s.substr(1).toUpperCase();
  };

  for (var i = 0; i < styles.length; i++) {

    var key = styles[i].replace(/^[-]+/, '').replace(/[-][a-z]/g, upper).replace(/^moz/, 'Moz');

    if (a <= key.charCodeAt(0) && z >= key.charCodeAt(0)) {
      if (key !== 'cssText' && key !== 'parentText') {
        dict[key] = true;
      }
    }
  }

  return dict;
}();

util.hasCssProperty = function (name) {
  return name in util._cssPropertyDict;
};

/**
 * Vendor prefix for css property.
 */
util.vendorPrefix = function () {
  var styles = window.getComputedStyle(document.documentElement, ''),
      pre = (Array.prototype.slice.call(styles).join('').match(/-(moz|webkit|ms)-/) || styles.OLink === '' && ['', 'o'])[1];
  return pre;
}();

util.forceLayoutAtOnce = function (elements, callback) {
  this.batchImmediate(function () {
    elements.forEach(function (element) {
      // force layout
      element.offsetHeight;
    });
    callback();
  });
};

util.batchImmediate = function () {
  var callbacks = [];

  return function (callback) {
    if (callbacks.length === 0) {
      _setImmediate(function () {
        var concreateCallbacks = callbacks.slice(0);
        callbacks = [];
        concreateCallbacks.forEach(function (callback) {
          callback();
        });
      });
    }

    callbacks.push(callback);
  };
}();

util.batchAnimationFrame = function () {
  var callbacks = [];

  var raf = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function (callback) {
    setTimeout(callback, 1000 / 60);
  };

  return function (callback) {
    if (callbacks.length === 0) {
      raf(function () {
        var concreateCallbacks = callbacks.slice(0);
        callbacks = [];
        concreateCallbacks.forEach(function (callback) {
          callback();
        });
      });
    }

    callbacks.push(callback);
  };
}();

util.transitionPropertyName = function () {
  if (util.hasCssProperty('transitionDuration')) {
    return 'transition';
  }

  if (util.hasCssProperty(util.vendorPrefix + 'TransitionDuration')) {
    return util.vendorPrefix + 'Transition';
  }

  throw new Error('Invalid state');
}();

/**
 * @param {HTMLElement} element
 */
var Animit = function Animit(element, defaults) {
  if (!(this instanceof Animit)) {
    return new Animit(element, defaults);
  }

  if (element instanceof HTMLElement) {
    this.elements = [element];
  } else if (Object.prototype.toString.call(element) === '[object Array]') {
    this.elements = element;
  } else {
    throw new Error('First argument must be an array or an instance of HTMLElement.');
  }

  this.defaults = defaults;
  this.transitionQueue = [];
  this.lastStyleAttributeDict = [];
};

Animit.prototype = {

  /**
   * @property {Array}
   */
  transitionQueue: undefined,

  /**
   * @property {Array}
   */
  elements: undefined,

  /**
   * @property {Object}
   */
  defaults: undefined,

  /**
   * Start animation sequence with passed animations.
   *
   * @param {Function} callback
   */
  play: function play(callback) {
    if (typeof callback === 'function') {
      this.transitionQueue.push(function (done) {
        callback();
        done();
      });
    }

    this.startAnimation();

    return this;
  },

  /**
   * Most of the animations follow this default process.
   *
   * @param {from} css or options object containing css
   * @param {to} css or options object containing css
   * @param {delay} delay to wait
   */
  default: function _default(from, to, delay) {
    function step(params, duration, timing) {
      if (params.duration !== undefined) {
        duration = params.duration;
      }
      if (params.timing !== undefined) {
        timing = params.timing;
      }

      return {
        css: params.css || params,
        duration: duration,
        timing: timing
      };
    }

    return this.saveStyle().queue(step(from, 0, this.defaults.timing)).wait(delay === undefined ? this.defaults.delay : delay).queue(step(to, this.defaults.duration, this.defaults.timing)).restoreStyle();
  },

  /**
   * Queue transition animations or other function.
   *
   * e.g. animit(elt).queue({color: 'red'})
   * e.g. animit(elt).queue({color: 'red'}, {duration: 0.4})
   * e.g. animit(elt).queue({css: {color: 'red'}, duration: 0.2})
   *
   * @param {Object|Animit.Transition|Function} transition
   * @param {Object} [options]
   */
  queue: function queue(transition, options) {
    var queue = this.transitionQueue;

    if (transition && options) {
      options.css = transition;
      transition = new Animit.Transition(options);
    }

    if (!(transition instanceof Function || transition instanceof Animit.Transition)) {
      if (transition.css) {
        transition = new Animit.Transition(transition);
      } else {
        transition = new Animit.Transition({
          css: transition
        });
      }
    }

    if (transition instanceof Function) {
      queue.push(transition);
    } else if (transition instanceof Animit.Transition) {
      queue.push(transition.build());
    } else {
      throw new Error('Invalid arguments');
    }

    return this;
  },

  /**
   * Queue transition animations.
   *
   * @param {Float} seconds
   */
  wait: function wait(seconds) {
    if (seconds > 0) {
      this.transitionQueue.push(function (done) {
        setTimeout(done, 1000 * seconds);
      });
    }

    return this;
  },

  saveStyle: function saveStyle() {

    this.transitionQueue.push(function (done) {
      this.elements.forEach(function (element, index) {
        var css = this.lastStyleAttributeDict[index] = {};

        for (var i = 0; i < element.style.length; i++) {
          css[element.style[i]] = element.style[element.style[i]];
        }
      }.bind(this));
      done();
    }.bind(this));

    return this;
  },

  /**
   * Restore element's style.
   *
   * @param {Object} [options]
   * @param {Float} [options.duration]
   * @param {String} [options.timing]
   * @param {String} [options.transition]
   */
  restoreStyle: function restoreStyle(options) {
    options = options || {};
    var self = this;

    if (options.transition && !options.duration) {
      throw new Error('"options.duration" is required when "options.transition" is enabled.');
    }

    var transitionName = util.transitionPropertyName;

    if (options.transition || options.duration && options.duration > 0) {
      var transitionValue = options.transition || 'all ' + options.duration + 's ' + (options.timing || 'linear');

      this.transitionQueue.push(function (done) {
        var elements = this.elements;
        var timeoutId;

        var clearTransition = function clearTransition() {
          elements.forEach(function (element) {
            element.style[transitionName] = '';
          });
        };

        // add "transitionend" event handler
        var removeListeners = util.onceOnTransitionEnd(elements[0], function () {
          clearTimeout(timeoutId);
          clearTransition();
          done();
        });

        // for fail safe.
        timeoutId = setTimeout(function () {
          removeListeners();
          clearTransition();
          done();
        }, options.duration * 1000 * TIMEOUT_RATIO);

        // transition and style settings
        elements.forEach(function (element, index) {

          var css = self.lastStyleAttributeDict[index];

          if (!css) {
            throw new Error('restoreStyle(): The style is not saved. Invoke saveStyle() before.');
          }

          self.lastStyleAttributeDict[index] = undefined;

          var name;
          for (var i = 0, len = element.style.length; i < len; i++) {
            name = element.style[i];
            if (css[name] === undefined) {
              css[name] = '';
            }
          }

          element.style[transitionName] = transitionValue;

          _Object$keys(css).forEach(function (key) {
            if (key !== transitionName) {
              element.style[key] = css[key];
            }
          });

          element.style[transitionName] = transitionValue;
        });
      });
    } else {
      this.transitionQueue.push(function (done) {
        reset();
        done();
      });
    }

    return this;

    function reset() {
      // Clear transition animation settings.
      self.elements.forEach(function (element, index) {
        element.style[transitionName] = 'none';

        var css = self.lastStyleAttributeDict[index];

        if (!css) {
          throw new Error('restoreStyle(): The style is not saved. Invoke saveStyle() before.');
        }

        self.lastStyleAttributeDict[index] = undefined;

        for (var i = 0, name = ''; i < element.style.length; i++) {
          name = element.style[i];
          if (typeof css[element.style[i]] === 'undefined') {
            css[element.style[i]] = '';
          }
        }

        _Object$keys(css).forEach(function (key) {
          element.style[key] = css[key];
        });
      });
    }
  },

  /**
   * Start animation sequence.
   */
  startAnimation: function startAnimation() {
    this._dequeueTransition();

    return this;
  },

  _dequeueTransition: function _dequeueTransition() {
    var transition = this.transitionQueue.shift();
    if (this._currentTransition) {
      throw new Error('Current transition exists.');
    }
    this._currentTransition = transition;
    var self = this;
    var called = false;

    var done = function done() {
      if (!called) {
        called = true;
        self._currentTransition = undefined;
        self._dequeueTransition();
      } else {
        throw new Error('Invalid state: This callback is called twice.');
      }
    };

    if (transition) {
      transition.call(this, done);
    }
  }

};

/**
 * @param {Animit} arguments
 */
Animit.runAll = function () /* arguments... */{
  for (var i = 0; i < arguments.length; i++) {
    arguments[i].play();
  }
};

/**
 * @param {Object} options
 * @param {Float} [options.duration]
 * @param {String} [options.property]
 * @param {String} [options.timing]
 */
Animit.Transition = function (options) {
  this.options = options || {};
  this.options.duration = this.options.duration || 0;
  this.options.timing = this.options.timing || 'linear';
  this.options.css = this.options.css || {};
  this.options.property = this.options.property || 'all';
};

Animit.Transition.prototype = {

  /**
   * @param {HTMLElement} element
   * @return {Function}
   */
  build: function build() {

    if (_Object$keys(this.options.css).length === 0) {
      throw new Error('options.css is required.');
    }

    var css = createActualCssProps(this.options.css);

    if (this.options.duration > 0) {
      var transitionValue = util.buildTransitionValue(this.options);
      var self = this;

      return function (callback) {
        var elements = this.elements;
        var timeout = self.options.duration * 1000 * TIMEOUT_RATIO;
        var timeoutId;

        var removeListeners = util.onceOnTransitionEnd(elements[0], function () {
          clearTimeout(timeoutId);
          callback();
        });

        timeoutId = setTimeout(function () {
          removeListeners();
          callback();
        }, timeout);

        elements.forEach(function (element) {
          element.style[util.transitionPropertyName] = transitionValue;

          _Object$keys(css).forEach(function (name) {
            element.style[name] = css[name];
          });
        });
      };
    }

    if (this.options.duration <= 0) {
      return function (callback) {
        var elements = this.elements;

        elements.forEach(function (element) {
          element.style[util.transitionPropertyName] = '';

          _Object$keys(css).forEach(function (name) {
            element.style[name] = css[name];
          });
        });

        if (elements.length > 0) {
          util.forceLayoutAtOnce(elements, function () {
            util.batchAnimationFrame(callback);
          });
        } else {
          util.batchAnimationFrame(callback);
        }
      };
    }

    function createActualCssProps(css) {
      var result = {};

      _Object$keys(css).forEach(function (name) {
        var value = css[name];

        if (util.hasCssProperty(name)) {
          result[name] = value;
          return;
        }

        var prefixed = util.vendorPrefix + util.capitalize(name);
        if (util.hasCssProperty(prefixed)) {
          result[prefixed] = value;
        } else {
          result[prefixed] = value;
          result[name] = value;
        }
      });

      return result;
    }
  }
};

export default Animit;