import Vue from 'vue';

// Central application store for Habitica
// Heavily inspired to Vuex (https://github.com/vuejs/vuex) with a very
// similar internal implementation (thanks!), main difference is the absence of mutations.

export default class Store {
  constructor ({ state, getters, actions }) {
    // Store actions
    this._actions = actions;

    // Store getters (computed properties),
    // implemented as computed properties in the internal Vue VM
    this.getters = {};

    // Setup getters
    const _computed = {};

    Object.keys(getters).forEach(key => {
      const getter = getters[key];

      // Each getter is compiled to a computed property on the internal VM
      _computed[key] = () => getter(this);

      Object.defineProperty(this.getters, key, {
        get: () => this._vm[key],
      });
    });

    // Setup internal Vue instance to make state and getters reactive
    this._vm = new Vue({
      data: { state },
      computed: _computed,
    });
  }

  // Return the store's state
  get state () {
    return this._vm.$data.state;
  }

  // Actions should be called using store.dispatch(ACTION_NAME, ...ARGS)
  // They get passed the store instance and any additional argument passed to dispatch()
  async dispatch (type, ...args) {
    const action = this._actions[type];

    if (!action) throw new Error(`Action "${type}" not found.`);
    return action(this, ...args);
  }

  // Watch data on the store's state
  // Internally it uses vm.$watch and accept the same argument except
  // for the first one that must be a getter function to which the state is passed
  // For documentation see https://vuejs.org/api/#vm-watch
  watch (getter, cb, options) {
    if (typeof getter !== 'function') {
      throw new Error('The first argument of store.watch must be a function.');
    }

    return this._vm.$watch(() => getter(this.state), cb, options);
  }

  // Expose the store as this.$store in components
  // Is automatically called when Vue.plugin(Store) is used
  static install (_Vue) {
    _Vue.mixin({
      beforeCreate () {
        const options = this.$options;
        // store injection
        if (options.store) {
          this.$store = options.store;
        } else if (options.parent && options.parent.$store) {
          this.$store = options.parent.$store;
        }
      },
    });
  }
}

export {
  mapState,
  mapGetters,
  mapActions,
} from './helpers/public';
