import _setImmediate from 'babel-runtime/core-js/set-immediate';
import _Object$getPrototypeOf from 'babel-runtime/core-js/object/get-prototype-of';
import _classCallCheck from 'babel-runtime/helpers/classCallCheck';
import _createClass from 'babel-runtime/helpers/createClass';
import _possibleConstructorReturn from 'babel-runtime/helpers/possibleConstructorReturn';
import _inherits from 'babel-runtime/helpers/inherits';
/*
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.

*/

import onsElements from '../ons/elements';
import util from '../ons/util';
import styler from '../ons/styler';
import platform from '../ons/platform';
import BaseElement from './base/base-element';
import GestureDetector from '../ons/gesture-detector';
import animit from '../ons/animit';

var STATE_INITIAL = 'initial';
var STATE_PREACTION = 'preaction';
var STATE_ACTION = 'action';

var throwType = function throwType(el, type) {
  return util.throw('"' + el + '" must be ' + type);
};

/**
 * @element ons-pull-hook
 * @category control
 * @description
 *   [en]
 *     Component that adds **Pull to refresh** functionality to an `<ons-page>` element.
 *
 *     It can be used to perform a task when the user pulls down at the top of the page. A common usage is to refresh the data displayed in a page.
 *   [/en]
 *   [ja][/ja]
 * @codepen WbJogM
 * @tutorial vanilla/Reference/pull-hook
 * @example
 * <ons-page>
 *   <ons-pull-hook>
 *     Release to refresh
 *   </ons-pull-hook>
 * </ons-page>
 *
 * <script>
 *   document.querySelector('ons-pull-hook').onAction = function(done) {
 *     setTimeout(done, 1000);
 *   };
 * </script>
 */

var PullHookElement = function (_BaseElement) {
  _inherits(PullHookElement, _BaseElement);

  /**
   * @event changestate
   * @description
   *   [en]Fired when the state is changed. The state can be either "initial", "preaction" or "action".[/en]
   *   [ja]コンポーネントの状態が変わった場合に発火します。状態は、"initial", "preaction", "action"のいずれかです。[/ja]
   * @param {Object} event
   *   [en]Event object.[/en]
   *   [ja]イベントオブジェクト。[/ja]
   * @param {Object} event.pullHook
   *   [en]Component object.[/en]
   *   [ja]コンポーネントのオブジェクト。[/ja]
   * @param {String} event.state
   *   [en]Current state.[/en]
   *   [ja]現在の状態名を参照できます。[/ja]
   */

  /**
   * @attribute disabled
   * @description
   *   [en]If this attribute is set the "pull-to-refresh" functionality is disabled.[/en]
   *   [ja]この属性がある時、disabled状態になりアクションが実行されなくなります[/ja]
   */

  /**
   * @attribute height
   * @type {String}
   * @description
   *   [en]Specify the height of the component. When pulled down further than this value it will switch to the "preaction" state. The default value is "64px".[/en]
   *   [ja]コンポーネントの高さを指定します。この高さ以上にpull downすると"preaction"状態に移行します。デフォルトの値は"64px"です。[/ja]
   */

  /**
   * @attribute threshold-height
   * @type {String}
   * @description
   *   [en]Specify the threshold height. The component automatically switches to the "action" state when pulled further than this value. The default value is "96px". A negative value will disable this property. If this value is lower than the height, it will skip "preaction" state.[/en]
   *   [ja]閾値となる高さを指定します。この値で指定した高さよりもpull downすると、このコンポーネントは自動的に"action"状態に移行します。[/ja]
   */

  /**
   * @attribute fixed-content
   * @description
   *   [en]If this attribute is set the content of the page will not move when pulling.[/en]
   *   [ja]この属性がある時、プルフックが引き出されている時にもコンテンツは動きません。[/ja]
   */

  function PullHookElement() {
    _classCallCheck(this, PullHookElement);

    var _this = _possibleConstructorReturn(this, (PullHookElement.__proto__ || _Object$getPrototypeOf(PullHookElement)).call(this));

    _this._shouldFixScroll = util.globals.isUIWebView;

    _this._onDrag = _this._onDrag.bind(_this);
    _this._onDragStart = _this._onDragStart.bind(_this);
    _this._onDragEnd = _this._onDragEnd.bind(_this);
    _this._onScroll = _this._onScroll.bind(_this);

    _this._setState(STATE_INITIAL, true);
    _this._hide(); // Fix for transparent toolbar transitions
    return _this;
  }

  _createClass(PullHookElement, [{
    key: '_setStyle',
    value: function _setStyle() {
      var height = this.height + 'px';
      styler(this, { height: height, lineHeight: height });
      this.style.display === '' && this._show();
    }
  }, {
    key: '_onScroll',
    value: function _onScroll(event) {
      var element = this._pageElement;

      if (element.scrollTop < 0) {
        element.scrollTop = 0;
      }
    }
  }, {
    key: '_canConsumeGesture',
    value: function _canConsumeGesture(gesture) {
      return gesture.direction === 'up' || gesture.direction === 'down';
    }
  }, {
    key: '_onDragStart',
    value: function _onDragStart(event) {
      var _this2 = this;

      if (!event.gesture || this.disabled) {
        return;
      }

      var tapY = event.gesture.center.clientY + this._pageElement.scrollTop;
      var maxY = window.innerHeight;
      // Only use drags that start near the pullHook to reduce flickerings
      var draggableAreaRatio = this._shouldFixScroll ? .8 : 1;

      this._ignoreDrag = event.consumed || tapY > maxY * draggableAreaRatio;

      if (!this._ignoreDrag) {
        var consume = event.consume;
        event.consume = function () {
          consume && consume();
          _this2._ignoreDrag = true;
          // This elements resizes .page__content so it is safer
          // to hide it when other components are dragged.
          _this2._hide();
        };

        if (this._canConsumeGesture(event.gesture)) {
          consume && consume();
          event.consumed = true;
          this._show(); // Not enough due to 'dragLockAxis'
        }
      }

      this._startScroll = this._pageElement.scrollTop;
    }
  }, {
    key: '_onDrag',
    value: function _onDrag(event) {
      var _this3 = this;

      if (!event.gesture || this.disabled || this._ignoreDrag || !this._canConsumeGesture(event.gesture)) {
        return;
      }

      // Necessary due to 'dragLockAxis' (25px)
      if (this.style.display === 'none') {
        this._show();
      }

      event.stopPropagation();

      var tapY = event.gesture.center.clientY + this._pageElement.scrollTop;
      var maxY = window.innerHeight;

      // Hack to make it work on Android 4.4 WebView and iOS UIWebView. Scrolls manually
      // near the top of the page so there will be no inertial scroll when scrolling down.
      // Allowing default scrolling will kill all 'touchmove' events.
      if (this._shouldFixScroll) {
        this._pageElement.scrollTop = this._startScroll - event.gesture.deltaY;
        // Allow inertia when scrolling down below 50% of the view to reduce flickerings
        if (event.gesture.interimDirection !== 'up' || tapY <= maxY * .5) {
          event.gesture.preventDefault();
        }
      }

      var scroll = Math.max(event.gesture.deltaY - this._startScroll, 0);
      if (scroll !== this._currentTranslation) {

        var th = this.thresholdHeight;
        if (th > 0 && scroll >= th) {
          event.gesture.stopDetect();
          _setImmediate(function () {
            return _this3._finish();
          });
        } else if (scroll >= this.height) {
          this._setState(STATE_PREACTION);
        } else {
          this._setState(STATE_INITIAL);
        }

        this._translateTo(scroll);
      }
    }
  }, {
    key: '_onDragEnd',
    value: function _onDragEnd(event) {
      if (!event.gesture || this.disabled || this._ignoreDrag) {
        return;
      }

      event.stopPropagation();

      if (this._currentTranslation > 0) {
        var scroll = this._currentTranslation;

        if (scroll > this.height) {
          this._finish();
        } else {
          this._translateTo(0, { animate: true });
        }
      }
    }

    /**
     * @property onAction
     * @type {Function}
     * @description
     *   [en]This will be called in the `action` state if it exists. The function will be given a `done` callback as it's first argument.[/en]
     *   [ja][/ja]
     */

  }, {
    key: '_finish',
    value: function _finish() {
      var _this4 = this;

      this._setState(STATE_ACTION);
      this._translateTo(this.height, { animate: true });
      var action = this.onAction || function (done) {
        return done();
      };
      action(function () {
        _this4._translateTo(0, { animate: true });
        _this4._setState(STATE_INITIAL);
      });
    }

    /**
     * @property height
     * @type {Number}
     * @description
     *   [en]The height of the pull hook in pixels. The default value is `64px`.[/en]
     *   [ja][/ja]
     */

  }, {
    key: '_setState',
    value: function _setState(state, noEvent) {
      var lastState = this.state;

      this.setAttribute('state', state);

      if (!noEvent && lastState !== this.state) {
        util.triggerElementEvent(this, 'changestate', {
          pullHook: this,
          state: state,
          lastState: lastState
        });
      }
    }

    /**
     * @property state
     * @readonly
     * @type {String}
     * @description
     *   [en]Current state of the element.[/en]
     *   [ja][/ja]
     */

  }, {
    key: '_show',
    value: function _show() {
      var _this5 = this;

      // Run asyncrhonously to avoid conflicts with Animit's style clean
      _setImmediate(function () {
        _this5.style.display = '';
        if (_this5._pageElement) {
          _this5._pageElement.style.marginTop = '-' + _this5.height + 'px';
        }
      });
    }
  }, {
    key: '_hide',
    value: function _hide() {
      this.style.display = 'none';
      if (this._pageElement) {
        this._pageElement.style.marginTop = '';
      }
    }

    /**
     * @param {Number} scroll
     * @param {Object} options
     * @param {Function} [options.callback]
     */

  }, {
    key: '_translateTo',
    value: function _translateTo(scroll) {
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};

      if (this._currentTranslation == 0 && scroll == 0) {
        return;
      }

      this._currentTranslation = scroll;
      var opt = options.animate ? { duration: .3, timing: 'cubic-bezier(.1, .7, .1, 1)' } : {};
      this._onPull && this._onPull((scroll / this.height).toFixed(2), opt);
      var scrollElement = this.hasAttribute('fixed-content') ? this : this._pageElement;

      animit(scrollElement).queue({ transform: 'translate3d(0px, ' + scroll + 'px, 0px)' }, opt).play(function () {
        scroll === 0 && styler.clear(scrollElement, 'transition transform');
        options.callback instanceof Function && options.callback();
      });
    }
  }, {
    key: '_disableDragLock',
    value: function _disableDragLock() {
      // e2e tests need it
      this._dragLockDisabled = true;
      this._setupListeners(true);
    }
  }, {
    key: '_setupListeners',
    value: function _setupListeners(add) {
      var _this6 = this;

      var scrollToggle = function scrollToggle(action) {
        return _this6._pageElement[action + 'EventListener']('scroll', _this6._onScroll, false);
      };
      var gdToggle = function gdToggle(action) {
        var passive = { passive: true };
        _this6._gestureDetector[action]('drag', _this6._onDrag, passive);
        _this6._gestureDetector[action]('dragstart', _this6._onDragStart, passive);
        _this6._gestureDetector[action]('dragend', _this6._onDragEnd, passive);
      };

      if (this._gestureDetector) {
        gdToggle('off');
        this._gestureDetector.dispose();
        this._gestureDetector = null;
      }
      scrollToggle('remove');

      if (add) {
        this._gestureDetector = new GestureDetector(this._pageElement, {
          dragMinDistance: 1,
          dragDistanceCorrection: false,
          dragLockToAxis: !this._dragLockDisabled,
          passive: !this._shouldFixScroll
        });

        gdToggle('on');
        scrollToggle('add');
      }
    }
  }, {
    key: 'connectedCallback',
    value: function connectedCallback() {
      this._currentTranslation = 0;
      this._pageElement = this.parentNode;

      this._setupListeners(true);
      this._setStyle();
    }
  }, {
    key: 'disconnectedCallback',
    value: function disconnectedCallback() {
      this._hide();
      this._setupListeners(false);
    }
  }, {
    key: 'attributeChangedCallback',
    value: function attributeChangedCallback(name, last, current) {
      if (name === 'height' && this._pageElement) {
        this._setStyle();
      }
    }
  }, {
    key: 'onAction',
    get: function get() {
      return this._onAction;
    },
    set: function set(value) {
      if (value && !(value instanceof Function)) {
        throwType('onAction', 'function or null');
      }
      this._onAction = value;
    }

    /**
     * @property onPull
     * @type {Function}
     * @description
     *   [en]Hook called whenever the user pulls the element. It gets the pulled distance ratio (scroll / height) and an animationOptions object as arguments.[/en]
     *   [ja][/ja]
     */

  }, {
    key: 'onPull',
    get: function get() {
      return this._onPull;
    },
    set: function set(value) {
      if (value && !(value instanceof Function)) {
        throwType('onPull', 'function or null');
      }
      this._onPull = value;
    }
  }, {
    key: 'height',
    set: function set(value) {
      if (!util.isInteger(value)) {
        throwType('height', 'integer');
      }

      this.setAttribute('height', value + 'px');
    },
    get: function get() {
      return parseInt(this.getAttribute('height') || '64', 10);
    }

    /**
     * @property thresholdHeight
     * @type {Number}
     * @description
     *   [en]The thresholdHeight of the pull hook in pixels. The default value is `96px`.[/en]
     *   [ja][/ja]
     */

  }, {
    key: 'thresholdHeight',
    set: function set(value) {
      if (!util.isInteger(value)) {
        throwType('thresholdHeight', 'integer');
      }

      this.setAttribute('threshold-height', value + 'px');
    },
    get: function get() {
      return parseInt(this.getAttribute('threshold-height') || '96', 10);
    }
  }, {
    key: 'state',
    get: function get() {
      return this.getAttribute('state');
    }

    /**
     * @property pullDistance
     * @readonly
     * @type {Number}
     * @description
     *   [en]The current number of pixels the pull hook has moved.[/en]
     *   [ja]現在のプルフックが引き出された距離をピクセル数。[/ja]
     */

  }, {
    key: 'pullDistance',
    get: function get() {
      return this._currentTranslation;
    }

    /**
     * @property disabled
     * @type {Boolean}
     * @description
     *   [en]Whether the element is disabled or not.[/en]
     *   [ja]無効化されている場合に`true`。[/ja]
     */

  }, {
    key: 'disabled',
    set: function set(value) {
      return util.toggleAttribute(this, 'disabled', value);
    },
    get: function get() {
      return this.hasAttribute('disabled');
    }
  }], [{
    key: 'observedAttributes',
    get: function get() {
      return ['height'];
    }
  }, {
    key: 'events',
    get: function get() {
      return ['changestate'];
    }
  }]);

  return PullHookElement;
}(BaseElement);

export default PullHookElement;


onsElements.PullHook = PullHookElement;
customElements.define('ons-pull-hook', PullHookElement);