<template>
  <div class="infinite-loading-container">
    <div class="infinite-status-prompt" v-show="isShowSpinner" :style="slotStyles.spinner">
      <slot name="spinner">
        <spinner :spinner="spinner" />
      </slot>
    </div>
    <div class="infinite-status-prompt" :style="slotStyles.noResults" v-show="isShowNoResults">
      <slot name="no-results">
        <component v-if="slots.noResults.render" :is="slots.noResults"></component>
        <template v-else>{{ slots.noResults }}</template>
      </slot>
    </div>
    <div class="infinite-status-prompt" :style="slotStyles.noMore" v-show="isShowNoMore">
      <slot name="no-more">
        <component v-if="slots.noMore.render" :is="slots.noMore"></component>
        <template v-else>{{ slots.noMore }}</template>
      </slot>
    </div>
    <div class="infinite-status-prompt" :style="slotStyles.error" v-show="isShowError">
      <slot name="error" :trigger="attemptLoad">
        <component v-if="slots.error.render" :is="slots.error" :trigger="attemptLoad"> </component>
        <template v-else>
          {{ slots.error }}
          <br />
          <button
            class="btn-try-infinite"
            @click="attemptLoad"
            v-text="slots.errorBtnText"
          ></button>
        </template>
      </slot>
    </div>
  </div>
</template>
<script>
import Spinner from "./Spinner.vue";
import config, { evt3rdArg, WARNINGS, STATUS, SLOT_STYLES } from "../config";
import { warn, throttleer, loopTracker, scrollBarStorage, kebabCase, isVisible } from "../utils";

export default {
  name: "InfiniteLoading",
  componentName: "InfiniteLoading",
  data() {
    return {
      scrollParent: null,
      scrollHandler: null,
      isFirstLoad: true, // save the current loading whether it is the first loading
      status: STATUS.READY,
      slots: config.slots,
    };
  },
  components: {
    Spinner,
  },
  computed: {
    isShowSpinner() {
      return this.status === STATUS.LOADING;
    },
    isShowError() {
      return this.status === STATUS.ERROR;
    },
    isShowNoResults() {
      return this.status === STATUS.COMPLETE && this.isFirstLoad;
    },
    isShowNoMore() {
      return this.status === STATUS.COMPLETE && !this.isFirstLoad;
    },
    slotStyles() {
      const styles = {};

      Object.keys(config.slots).forEach((key) => {
        const name = kebabCase(key);

        if (
          // no slot and the configured default slot is not a Vue component
          (!this.$slots[name] && !config.slots[key].render) ||
          // has slot and slot is pure text node
          (this.$slots[name] && !this.$slots[name][0].tag)
        ) {
          // only apply default styles for pure text slot
          styles[key] = SLOT_STYLES;
        }
      });

      return styles;
    },
  },
  props: {
    distance: {
      type: Number,
      default: config.props.distance,
    },
    spinner: String,
    direction: {
      type: String,
      default: "bottom",
    },
    forceUseInfiniteWrapper: {
      type: [Boolean, String],
      default: config.props.forceUseInfiniteWrapper,
    },
    identifier: {
      default: +new Date(),
    },
    onInfinite: Function,
  },
  watch: {
    identifier() {
      this.stateChanger.reset();
    },
  },
  mounted() {
    this.$watch(
      "forceUseInfiniteWrapper",
      () => {
        this.scrollParent = this.getScrollParent();
      },
      { immediate: true }
    );

    this.scrollHandler = (ev) => {
      if (this.status === STATUS.READY) {
        if (ev && ev.constructor === Event && isVisible(this.$el)) {
          throttleer.throttle(this.attemptLoad);
        } else {
          this.attemptLoad();
        }
      }
    };

    setTimeout(() => {
      this.scrollHandler();
      this.scrollParent.addEventListener("scroll", this.scrollHandler, evt3rdArg);
    }, 1);

    this.$on("$InfiniteLoading:loaded", (ev) => {
      this.isFirstLoad = false;

      if (this.direction === "top") {
        // wait for DOM updated
        this.$nextTick(() => {
          scrollBarStorage.restore(this.scrollParent);
        });
      }

      if (this.status === STATUS.LOADING) {
        this.$nextTick(this.attemptLoad.bind(null, true));
      }

      if (!ev || ev.target !== this) {
        warn(WARNINGS.STATE_CHANGER);
      }
    });

    this.$on("$InfiniteLoading:complete", (ev) => {
      this.status = STATUS.COMPLETE;

      // force re-complation computed properties to fix the problem of get slot text delay
      this.$nextTick(() => {
        this.$forceUpdate();
      });

      this.scrollParent.removeEventListener("scroll", this.scrollHandler, evt3rdArg);

      if (!ev || ev.target !== this) {
        warn(WARNINGS.STATE_CHANGER);
      }
    });

    this.$on("$InfiniteLoading:reset", (ev) => {
      this.status = STATUS.READY;
      this.isFirstLoad = true;
      scrollBarStorage.remove(this.scrollParent);
      this.scrollParent.addEventListener("scroll", this.scrollHandler, evt3rdArg);

      // wait for list to be empty and the empty action may trigger a scroll event
      setTimeout(() => {
        throttleer.reset();
        this.scrollHandler();
      }, 1);

      if (!ev || ev.target !== this) {
        warn(WARNINGS.IDENTIFIER);
      }
    });

    /**
     * change state for this component, pass to the callback
     */
    this.stateChanger = {
      loaded: () => {
        this.$emit("$InfiniteLoading:loaded", { target: this });
      },
      complete: () => {
        this.$emit("$InfiniteLoading:complete", { target: this });
      },
      reset: () => {
        this.$emit("$InfiniteLoading:reset", { target: this });
      },
      error: () => {
        this.status = STATUS.ERROR;
        throttleer.reset();
      },
    };

    if (this.onInfinite) {
      warn(WARNINGS.INFINITE_EVENT);
    }
  },
  /**
   * To adapt to keep-alive feature, but only work on Vue 2.2.0 and above, see: https://vuejs.org/v2/api/#keep-alive
   */
  deactivated() {
    /* istanbul ignore else */
    if (this.status === STATUS.LOADING) {
      this.status = STATUS.READY;
    }
    this.scrollParent.removeEventListener("scroll", this.scrollHandler, evt3rdArg);
  },
  activated() {
    this.scrollParent.addEventListener("scroll", this.scrollHandler, evt3rdArg);
  },
  methods: {
    /**
     * attempt trigger load
     * @param {Boolean} isContinuousCall  the flag of continuous call, it will be true
     *                                    if this method be called in the `loaded`
     *                                    event handler
     */
    attemptLoad(isContinuousCall) {
      if (
        this.status !== STATUS.COMPLETE &&
        isVisible(this.$el) &&
        this.getCurrentDistance() <= this.distance
      ) {
        this.status = STATUS.LOADING;

        if (this.direction === "top") {
          // wait for spinner display
          this.$nextTick(() => {
            scrollBarStorage.save(this.scrollParent);
          });
        }

        if (typeof this.onInfinite === "function") {
          this.onInfinite.call(null, this.stateChanger);
        } else {
          this.$emit("infinite", this.stateChanger);
        }

        if (isContinuousCall && !this.forceUseInfiniteWrapper && !loopTracker.isChecked) {
          // check this component whether be in an infinite loop if it is not checked
          // more details: https://github.com/PeachScript/vue-infinite-loading/issues/55#issuecomment-316934169
          loopTracker.track();
        }
      } else if (this.status === STATUS.LOADING) {
        this.status = STATUS.READY;
      }
    },
    /**
     * get current distance from the specified direction
     * @return {Number}     distance
     */
    getCurrentDistance() {
      let distance;

      if (this.direction === "top") {
        distance =
          typeof this.scrollParent.scrollTop === "number"
            ? this.scrollParent.scrollTop
            : this.scrollParent.pageYOffset;
      } else {
        const infiniteElmOffsetTopFromBottom = this.$el.getBoundingClientRect().top;
        const scrollElmOffsetTopFromBottom =
          this.scrollParent === window
            ? window.innerHeight
            : this.scrollParent.getBoundingClientRect().bottom;

        distance = infiniteElmOffsetTopFromBottom - scrollElmOffsetTopFromBottom;
      }

      return distance;
    },
    /**
     * get the first scroll parent of an element
     * @param  {DOM} elm    cache element for recursive search
     * @return {DOM}        the first scroll parent
     */
    getScrollParent(elm = this.$el) {
      let result;

      if (typeof this.forceUseInfiniteWrapper === "string") {
        result = elm.querySelector(this.forceUseInfiniteWrapper);
      }

      if (!result) {
        if (elm.tagName === "BODY") {
          result = window;
        } else if (
          !this.forceUseInfiniteWrapper &&
          ["scroll", "auto"].indexOf(getComputedStyle(elm).overflowY) > -1
        ) {
          result = elm;
        } else if (
          elm.hasAttribute("infinite-wrapper") ||
          elm.hasAttribute("data-infinite-wrapper")
        ) {
          result = elm;
        }
      }

      return result || this.getScrollParent(elm.parentNode);
    },
  },
  destroyed() {
    /* istanbul ignore else */
    if (!this.status !== STATUS.COMPLETE) {
      throttleer.reset();
      scrollBarStorage.remove(this.scrollParent);
      this.scrollParent.removeEventListener("scroll", this.scrollHandler, evt3rdArg);
    }
  },
};
</script>
<style scoped>
* {
  clear: both;
  text-align: center;
}
.infinite-loading-container *[class^="loading-"] {
  /* 28px: 28px */
  display: inline-block;
  margin: 5px 0;
  width: 28px;
  height: 28px;
  font-size: 28px;
  line-height: 28px;
  border-radius: 50%;
}
.btn-try-infinite {
  margin-top: 5px;
  padding: 5px 10px;
  color: #999;
  font-size: 14px;
  line-height: 1;
  background: transparent;
  border: 1px solid #ccc;
  border-radius: 3px;
  outline: none;
  cursor: pointer;
}
.btn-try-infinite:not(:active):hover {
  opacity: 0.8;
}
</style>
