/*

### withMulti

Paginated items container

Options: 

  - collection: the collection to fetch the documents from
  - fragment: the fragment that defines which properties to fetch
  - fragmentName: the name of the fragment, passed to getFragment
  - limit: the number of documents to show initially
  - pollInterval: how often the data should be updated, in ms (set to 0 to disable polling)
  - terms: an object that defines which documents to fetch

Props Received: 

  - terms: an object that defines which documents to fetch

Terms object can have the following properties:

  - view: String
  - userId: String
  - cat: String
  - date: String
  - after: String
  - before: String
  - enableTotal: Boolean
  - enableCache: Boolean
  - listId: String
  - query: String # search query
  - postId: String
  - limit: String
         
*/

import React, { Component } from 'react';
import { withApollo, graphql } from 'react-apollo';
import gql from 'graphql-tag';
import update from 'immutability-helper';
import { getSetting, Utils, multiClientTemplate } from 'meteor/vulcan:lib';
import Mingo from 'mingo';
import compose from 'recompose/compose';
import withState from 'recompose/withState';
import find from 'lodash/find';

import { extractCollectionInfo, extractFragmentInfo } from './handleOptions';

export default function withMulti(options) {
  // console.log(options)

  const {
    limit = 10,
    pollInterval = getSetting('pollInterval', 20000),
    enableTotal = true,
    enableCache = false,
    extraQueries
  } = options;

  const { collectionName, collection } = extractCollectionInfo(options);
  const { fragmentName, fragment } = extractFragmentInfo(options, collectionName);

  const typeName = collection.options.typeName;
  const resolverName = collection.options.multiResolverName;

  // build graphql query from options
  const query = gql`
    ${multiClientTemplate({ typeName, fragmentName, extraQueries })}
    ${fragment}
  `;

  return compose(
    // wrap component with Apollo HoC to give it access to the store
    withApollo,

    // wrap component with HoC that manages the terms object via its state
    withState('paginationTerms', 'setPaginationTerms', props => {
      // get initial limit from props, or else options
      const paginationLimit = (props.terms && props.terms.limit) || limit;
      const paginationTerms = {
        limit: paginationLimit,
        itemsPerPage: paginationLimit
      };

      return paginationTerms;
    }),

    // wrap component with graphql HoC
    graphql(
      query,

      {
        alias: `with${Utils.pluralize(typeName)}`,

        // graphql query options
        options({ terms, paginationTerms, client: apolloClient, currentUser }) {
          // get terms from options, then props, then pagination
          const mergedTerms = { ...options.terms, ...terms, ...paginationTerms };

          const graphQLOptions = {
            variables: {
              input: {
                terms: mergedTerms,
                enableCache,
                enableTotal
              }
            },
            // note: pollInterval can be set to 0 to disable polling (20s by default)
            pollInterval,
            reducer: (previousResults, action) => {
              // see queryReducer function defined below
              return queryReducer(
                typeName,
                previousResults,
                action,
                collection,
                mergedTerms,
                resolverName,
                apolloClient
              );
            }
          };

          if (options.fetchPolicy) {
            graphQLOptions.fetchPolicy = options.fetchPolicy;
          }

          // set to true if running into https://github.com/apollographql/apollo-client/issues/1186
          if (options.notifyOnNetworkStatusChange) {
            graphQLOptions.notifyOnNetworkStatusChange = options.notifyOnNetworkStatusChange;
          }

          return graphQLOptions;
        },

        // define props returned by graphql HoC
        props(props) {
          // see https://github.com/apollographql/apollo-client/blob/master/packages/apollo-client/src/core/networkStatus.ts
          const refetch = props.data.refetch,
            // results = Utils.convertDates(collection, props.data[listResolverName]),
            results = props.data[resolverName] && props.data[resolverName].results,
            totalCount = props.data[resolverName] && props.data[resolverName].totalCount,
            networkStatus = props.data.networkStatus,
            loadingInitial = props.data.networkStatus === 1,
            loading = props.data.networkStatus === 1,
            loadingMore = props.data.networkStatus === 2,
            error = props.data.error,
            propertyName = options.propertyName || 'results';

          if (error) {
            // eslint-disable-next-line no-console
            console.log(error);
          }

          return {
            // see https://github.com/apollostack/apollo-client/blob/master/src/queries/store.ts#L28-L36
            // note: loading will propably change soon https://github.com/apollostack/apollo-client/issues/831
            loading,
            loadingInitial,
            loadingMore,
            [propertyName]: results,
            totalCount,
            refetch,
            networkStatus,
            error,
            count: results && results.length,

            // regular load more (reload everything)
            loadMore(providedTerms) {
              // if new terms are provided by presentational component use them, else default to incrementing current limit once
              const newTerms =
                typeof providedTerms === 'undefined'
                  ? {
                      /*...props.ownProps.terms,*/ ...props.ownProps.paginationTerms,
                    limit: results.length + props.ownProps.paginationTerms.itemsPerPage
                  }
                  : providedTerms;

              props.ownProps.setPaginationTerms(newTerms);
            },

            // incremental loading version (only load new content)
            // note: not compatible with polling
            loadMoreInc(providedTerms) {
              // get terms passed as argument or else just default to incrementing the offset
              const newTerms =
                typeof providedTerms === 'undefined'
                  ? { ...props.ownProps.terms, ...props.ownProps.paginationTerms, offset: results.length }
                  : providedTerms;

              return props.data.fetchMore({
                variables: { input: { terms: newTerms } }, // ??? not sure about 'terms: newTerms'
                updateQuery(previousResults, { fetchMoreResult }) {
                  // no more post to fetch
                  if (!fetchMoreResult.data) {
                    return previousResults;
                  }
                  const newResults = {};
                  newResults[resolverName] = [...previousResults[resolverName], ...fetchMoreResult.data[resolverName]];
                  // return the previous results "augmented" with more
                  return { ...previousResults, ...newResults };
                }
              });
            },

            fragmentName,
            fragment,
            ...props.ownProps, // pass on the props down to the wrapped component
            data: props.data
          };
        }
      }
    )
  );
}

// define query reducer separately
const queryReducer = (typeName, previousResults, action, collection, mergedTerms, resolverName, apolloClient) => {
  // if collection has no mutations defined, just return previous results
  if (!collection.options.mutations) {
    return previousResults;
  }

  let newResults = previousResults;

  // get mongo selector and options objects based on current terms
  const result = collection.getParameters(mergedTerms, apolloClient);
  const { selector, options } = result;

  const mingoQuery = new Mingo.Query(selector);

  // function to remove a document from a results object, used by edit and remove cases below
  const removeFromResults = (data, document) => {
    const listWithoutDocument = data[resolverName].results.filter(doc => doc._id !== document._id);
    const currentTotalCount = data[resolverName].totalCount;
    const newResults = update(data, {
      [resolverName]: { $set: { results: listWithoutDocument, totalCount: currentTotalCount - 1 } }
    });
    return newResults;
  };

  // add document to a results object
  const addToResults = (data, document) => {
    const listWithDocument = [...data[resolverName].results, document];
    const currentTotalCount = data[resolverName].totalCount;
    const newResults = update(data, {
      [resolverName]: { $set: { results: listWithDocument, totalCount: currentTotalCount + 1 } }
    });
    return newResults;
  };

  // reorder results according to a sort
  const reorderResults = (data, sort) => {
    const list = data[resolverName].results;
    // const convertedList = Utils.convertDates(collection, list); // convert date strings to date objects
    const convertedList = list;
    const cursor = mingoQuery.find(convertedList);
    const sortedList = cursor.sort(sort).all();
    data[resolverName].results = sortedList;
    return data;
  };

  // console.log('// withList reducer');
  // console.log('terms: ', mergedTerms);
  // console.log('selector: ', selector);
  // console.log('options: ', options);
  // console.log('previousResults: ', previousResults);
  // console.log('action: ', action);

  switch (action.operationName) {
    case `create${typeName}`:
      // if new document belongs to current list (based on view selector), add it
      const newDocument = action.result.data[`create${typeName}`].data;
      if (mingoQuery.test(newDocument)) {
        if (!find(previousResults[resolverName].results, { _id: newDocument._id })) {
          // make sure it hasn't been already added despite being a create mutation
          // as this reducer may be called several times
          newResults = addToResults(previousResults, newDocument);
        }
        newResults = reorderResults(newResults, options.sort);
      }
      // console.log('** new **')
      // console.log('newDocument: ', newDocument)
      // console.log('belongs to list: ', mingoQuery.test(newDocument))
      break;

    case `update${typeName}`:
      const editedDocument = action.result.data[`update${typeName}`].data;
      if (mingoQuery.test(editedDocument)) {
        // edited document belongs to the list
        if (!find(previousResults[resolverName].results, { _id: editedDocument._id })) {
          // if document wasn't already in list, add it
          newResults = addToResults(previousResults, editedDocument);
        }
        newResults = reorderResults(newResults, options.sort);
      } else {
        // if edited doesn't belong to current list anymore (based on view selector), remove it
        newResults = removeFromResults(previousResults, editedDocument);
      }
      // console.log('** edit **')
      // console.log('editedDocument: ', editedDocument)
      // console.log('belongs to list: ', mingoQuery.test(editedDocument))
      // console.log('exists in list: ', !!_.findWhere(previousResults[resolverName].results, {_id: editedDocument._id}))
      break;

    case `delete${typeName}`:
      const removedDocument = action.result.data[`delete${typeName}`].data;
      newResults = removeFromResults(previousResults, removedDocument);
      // console.log('** remove **')
      // console.log('removedDocument: ', removedDocument)
      break;

    default:
      // console.log('** no action **')
      return previousResults;
  }

  // console.log('newResults: ', newResults)
  // console.log('\n\n')

  // copy over arrays explicitely to ensure new sort is taken into account
  return {
    [resolverName]: {
      results: [...newResults[resolverName].results],
      totalCount: newResults[resolverName].totalCount,
      __typename: `Multi${typeName}Output`
    }
  };
};