/* eslint valid-jsdoc: 0 */
const React = require('react');
const shallowEqual = require('react/lib/shallowEqual');
const find = require('lodash/collection/find');
const TheStore = require('../TheStore');

/**
 * Determine if an object has an array of keys.
 *
 * @param {object} object
 *   Object to check.
 * @param {string[]} keys
 *   Array of keys to check.
 *
 * @return {boolean}
 *   If object has all of the keys.
 */
function hasKeys(object, keys) {
  let i = keys.length;
  while (i--) {
    if (!object.hasOwnProperty([keys[i]])) {
      return false;
    }
  }
  return true;
}

/**
 * Creates a Container component to wrap a given compoent.
 *
 * @param {ReactComponent} Component
 *   The Component to wrap with a data layer.
 * @param {object} options
 *   Key-value options
 * @param {string} options.name
 *   Name to call this (key to use in TheStore)
 * @param {ReactComponent} options.Loading
 *   Component to render while still loading.
 *   If undefined, uses Component
 * @param {ReactComponent} options.Failure
 *   Component to render while in a failure state.
 *   If undefined, uses Component
 * @param {function} options.load
 *   Function that, given params, promises to load data for this container.
 * @param {function} options.isStateAccurate
 *   Optional. Given (props, state), returns TRUE if state has accurate data.
 * @param {string[]} options.storeKeys
 *   An array of keys to validate if state from the Store is in valid.
 *
 * @return {ReactComponent}
 *   Wrapping Component w/ Data.
 */
module.exports = function createContainer(Component, options) {
  const {name, Loading, Failure, load, storeKeys} = options;
  const Container = React.createClass({
    displayName: name,
    // Pull Initial State from store (maybe it's already loaded)
    getInitialState() {
      return TheStore.getState();
    },
    componentWillReceiveDispatch() {
      this.setState(TheStore.getState());
    },
    // Pick up changes to the store
    componentDidMount() {
      this.unsubscribe = TheStore.subscribe(this.componentWillReceiveDispatch);
    },
    componentWillUnmount() {
      this.unsubscribe();
    },
    // Load & Pre-load
    componentWillMount() {
      // Client: Check if load if needed
      if (typeof window !== "undefined"
        && !this.isFailure()
        && this.isLoading()
      ) {
        Container.load(this.props).then(TheStore.set);
      }
    },
    // React Router is bringing this component in.
    componentWillReceiveProps(nextProps) {
      if (!shallowEqual(this.props, nextProps)) {
        Container.load(nextProps).then(TheStore.set);
      }
    },
    /**
     * Determine if a key, we are concerned with, is an error.
     *
     * @param {*} value
     *   Value in the store to check.
     * @param {string} key
     *   Key of the Store to check.
     * @return {boolean}
     *   True, if we are concerned with this key, and the value is an error.
     */
    isInvalidStoreValue(value, key) {
      return storeKeys.indexOf(key) >= 0 && (value instanceof Error || value.error);
    },
    /**
     * Determine if Container has failed for this component.
     *
     * @return {boolean}
     *   True if data has, in fact, failed.
     */
    isFailure() {
      if (storeKeys && hasKeys(this.state, storeKeys) && find(this.state, this.isInvalidStoreValue)) {
        return true;
      }
      return false;
    },
    /**
     * Determine if Container has data for this component.
     *
     * @return {boolean}
     *   True if data is, in fact, loaded.
     */
    isLoading() {
      if (storeKeys && !hasKeys(this.state, storeKeys)) {
        return true;
      }
      if (options.isStateAccurate) {
        return !options.isStateAccurate(this.props, this.state);
      }
      return false;
    },
    render() {
      const component = (
        (this.isFailure() && Failure)
        || (this.isLoading() && Loading)
        || Component
      );
      return component && React.createElement(component, {...this.props, ...this.state});
    }
  });
  Container.load = props => load(props);
  return Container;
};