text: 'use strict';
  
  const _ = require('lodash');
  const { getComponentAttributes, isComponent } = require('./utils/attributes');
  const { isPolymorphic } = require('./utils/associations');
  
  /**
   * Create utilities to populate a model on fetch
   */
  
  const populateFetch = (definition, options) => {
    // do not populate anything
    if (options.withRelated === false) return;
    if (options.isEager === true) return;
  
    if (_.isNil(options.withRelated)) {
      options.withRelated = []
        .concat(populateComponents(definition))
        .concat(populateAssociations(definition));
    } else if (_.isEmpty(options.withRelated)) {
      options.withRelated = populateComponents(definition);
    } else {
      options.withRelated = formatPopulateOptions(definition, options.withRelated).concat(
        populateComponents(definition)
      );
    }
  };
  
  const populateAssociations = (definition, { prefix = '' } = {}) => {
    return definition.associations
      .filter(ast => ast.autoPopulate !== false)
      .map(assoc => {
        if (isPolymorphic({ assoc })) {
          return formatPolymorphicPopulate({
            assoc,
            prefix,
          });
        }
  
        return formatAssociationPopulate({ assoc, prefix });
      })
      .reduce((acc, val) => acc.concat(val), []);
  };
  
  const populateBareAssociations = (definition, { prefix = '' } = {}) => {
    return (definition.associations || [])
      .filter(ast => ast.autoPopulate !== false)
      .map(assoc => {
        if (isPolymorphic({ assoc })) {
          return formatPolymorphicPopulate({
            assoc,
            prefix,
          });
        }
  
        const path = `${prefix}${assoc.alias}`;
        const assocModel = strapi.db.getModelByAssoc(assoc);
  
        const polyAssocs = assocModel.associations
          .filter(assoc => isPolymorphic({ assoc }))
          .map(assoc =>
            formatPolymorphicPopulate({
              assoc,
              prefix: `${path}.`,
            })
          );
  
        return [path, ...polyAssocs];
      })
      .reduce((acc, val) => acc.concat(val), []);
  };
  
  const formatAssociationPopulate = ({ assoc, prefix = '' }) => {
    const path = `${prefix}${assoc.alias}`;
    const assocModel = strapi.db.getModelByAssoc(assoc);
  
    const polyAssocs = assocModel.associations
      .filter(assoc => isPolymorphic({ assoc }))
      .map(assoc =>
        formatPolymorphicPopulate({
          assoc,
          prefix: `${path}.`,
        })
      );
  
    const components = populateComponents(assocModel, { prefix: `${path}.` });
  
    return [path, ...polyAssocs, ...components];
  };
  
  const populateComponents = (definition, { prefix = '' } = {}) => {
    return getComponentAttributes(definition)
      .map(key => {
        const attribute = definition.attributes[key];
        const autoPopulate = _.get(attribute, ['autoPopulate'], true);
  
        if (autoPopulate === true) {
          return populateComponent(key, attribute, { prefix });
        }
      }, [])
      .reduce((acc, val) => acc.concat(val), []);
  };
  
  const populateComponent = (key, attr, { prefix = '' } = {}) => {
    const path = `${prefix}${key}.component`;
    const componentPrefix = `${path}.`;
  
    if (attr.type === 'dynamiczone') {
      const componentKeys = attr.components;
  
      return componentKeys.reduce((acc, key) => {
        const component = strapi.components[key];
        const assocs = populateBareAssociations(component, {
          prefix: componentPrefix,
        });
  
        const components = populateComponents(component, {
          prefix: componentPrefix,
        });
  
        return acc.concat([path, ...assocs, ...components]);
      }, []);
    }
  
    const component = strapi.components[attr.component];
    const assocs = populateBareAssociations(component, {
      prefix: componentPrefix,
    });
  
    const components = populateComponents(component, {
      prefix: componentPrefix,
    });
  
    return [path, ...assocs, ...components];
  };
  
  const formatPopulateOptions = (definition, withRelated) => {
    if (!Array.isArray(withRelated)) withRelated = [withRelated];
  
    const obj = withRelated.reduce((acc, key) => {
      if (_.isString(key)) {
        acc[key] = () => {};
        return acc;
      }
  
      return _.extend(acc, key);
    }, {});
  
    const finalObj = Object.keys(obj).reduce((acc, key) => {
      // check the key path and update it if necessary
      const parts = key.split('.');
  
      let newKey;
      let prefix = '';
      let tmpModel = definition;
      for (let part of parts) {
        const attr = tmpModel.attributes[part];
  
        if (isComponent(tmpModel, part)) {
          if (attr.type === 'dynamiczone') {
            const path = `${prefix}${part}.component`;
            newKey = path;
            break;
          }
  
          tmpModel = strapi.components[attr.component];
          // add component path and there relations / images
          const path = `${prefix}${part}.component`;
  
          newKey = path;
          prefix = `${path}.`;
          continue;
        }
  
        const assoc = tmpModel.associations.find(association => association.alias === part);
  
        if (!assoc) return acc;
  
        tmpModel = strapi.db.getModelByAssoc(assoc);
  
        if (isPolymorphic({ assoc })) {
          const path = formatPolymorphicPopulate({
            assoc,
            prefix,
          });
  
          return _.extend(acc, path);
        }
  
        newKey = `${prefix}${part}`;
        prefix = `${newKey}.`;
      }
  
      acc[newKey] = obj[key];
      return acc;
    }, {});
  
    return [finalObj];
  };
  
  const formatPolymorphicPopulate = ({ assoc, prefix = '' }) => {
    // MorphTo side.
    if (assoc.related) {
      return { [`${prefix}${assoc.alias}.related`]: () => {} };
    }
  
    // oneToMorph or manyToMorph side.
    // Retrieve collection name because we are using it to build our hidden model.
    const model = strapi.db.getModelByAssoc(assoc);
  
    return {
      [`${prefix}${assoc.alias}.${model.collectionName}`]: function(query) {
        query.orderBy('created_at', 'desc');
      },
    };
  };
  
  module.exports = populateFetch;
  