import $ from 'jquery';
import Component from 'ember-component';
import computed from 'ember-computed';
import observer from 'ember-metal/observer';
import { assert } from 'ember-metal/utils';
import run from 'ember-runloop';
import on from 'ember-evented/on';
import { trySet } from 'ember-metal/set';

const SCROLL_ELEMENT_CLASS = 'tse-content';

export default Component.extend({
  hideMore: false,

  _isLoading: computed.alias('isLoading'),
  ignoreContentLength: false,
  classNames: "infinite-scroll",
  buffer: 1,

  setupListeners: on('didInsertElement', function () {
    this.scrollingEl = this.$().closest(`.${SCROLL_ELEMENT_CLASS}`);
    assert(
      `A parent element of infinite-scroll component requires CSS class '${SCROLL_ELEMENT_CLASS}'`,
      this.scrollingEl.length > 0
    );

    this.scrollingParentEl = this.scrollingEl.parent();
    this.scrollingParentEl.on(`scroll.${this.elementId}`, () => {
      this.debounceTask('_ensureViewFilled', 100);
    });
    $(window).on(`resize.${this.elementId}`, () => {
      this.debounceTask('_ensureViewFilled', 100);
    });
    run.scheduleOnce('afterRender', this, this._ensureViewFilled);
  }),

  breakdownListeners: on('willDestroyElement', function () {
    this.scrollingParentEl.off(`scroll.${this.elementId}`);
    $(window).off(`resize.${this.elementId}`);
  }),

  /** Private Helper Functions */

  /**
   * the key condition for this algorithm is that we must ensure that the user can always manually scroll down,
   * and thus trigger a new content load. important considerations:
   *
   * - whenever the layout window is resized vertically or horizontally, the scroll view's content
   *    may be rearranged such that white space is opened up within the scrolling region. thus, on
   *    each such resize, we check to see if we need to load another page of content.
   *
   * - if there is white space from the bottom of the scrolling content element to the bottom of its
   *   parent, that means there is no usable scroll bar, so we must continue filling the space until
   *   the key condition above is satisfied.
   *
   * potential race conditions that have been addressed:
   * - underlying collection changes between page loads, such that the "next page"
   *   does not add any new items.
   */
  _scheduleEnsureViewFilled: observer('_isLoading', function () {
    run.scheduleOnce('afterRender', this, this._ensureViewFilled);
  }),

  _ensureViewFilled() {
    if (this.isDestroyed) {
      return;
    }

    let contentLength = this.get('contentLength');
    if (this.get('_isLoading') || (!this.get('ignoreContentLength') && (this.lastContentLength === contentLength))) {
      return;
    }

    if (this.get('ignoreContentLength') && this.lastContentLength === 0 && contentLength === 0) {
      return;
    }

    if (this.scrollingEl && this.scrollingParentEl) {
      let parent = this.scrollingParentEl[0];
      let scrollEl = this.scrollingEl[0];
      let scrollDistanceToBottom = scrollEl.scrollHeight - parent.scrollTop - parent.offsetHeight;

      // we check against 1px height because at various zoom levels, Chrome non-deterministically
      // introduces 1px gaps between DOM elements where they should not exist.
      if (scrollDistanceToBottom <= this.get('buffer') && (scrollDistanceToBottom < (scrollEl.offsetHeight))) {
        this.lastContentLength = contentLength;
        this._loadMore();
      }
    }
  },

  /**
   * Handle 2 use cases:
   *
   * 1. Passing a string based action and an isLoading property to the
   *    component. Use this strategy when passed `action`.
   *
   * 2. Passing a closure action that returns a promise. Use this strategy when
   *    passed `loadMore`.
   *
   * In the case of a string-based action we'll have the consumer pass in an
   * `isLoading` attribute. However, for closure actions we can use the return
   * value from the action to determine the isLoading state.
   *
   */
  _loadMore() {
    if (this.get('_isLoading') || this.get('hideMore')) {
      return;
    }

    let action = this.get('action');

    if (action) {
      this._loadMoreWithPassedIsLoadingAttribute();
    } else if (this.get('loadMore')) {
      this._loadMoreForClosureActionReturningPromise();
    } else {
      // There is no action passed so no need to do anything
    }
  },

  _loadMoreWithPassedIsLoadingAttribute() {
    this.sendAction();
  },

  _loadMoreForClosureActionReturningPromise() {
    this.set('_isLoading', true);
    this.get('loadMore')().finally(() => trySet(this, '_isLoading', false));
  }
});
