"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = substituteClassApplyAtRules;

var _lodash = _interopRequireDefault(require("lodash"));

var _postcssSelectorParser = _interopRequireDefault(require("postcss-selector-parser"));

var _postcss = _interopRequireDefault(require("postcss"));

var _didyoumean = _interopRequireDefault(require("didyoumean"));

var _substituteTailwindAtRules = _interopRequireDefault(require("./substituteTailwindAtRules"));

var _evaluateTailwindFunctions = _interopRequireDefault(require("./evaluateTailwindFunctions"));

var _substituteVariantsAtRules = _interopRequireDefault(require("./substituteVariantsAtRules"));

var _substituteResponsiveAtRules = _interopRequireDefault(require("./substituteResponsiveAtRules"));

var _convertLayerAtRulesToControlComments = _interopRequireDefault(require("./convertLayerAtRulesToControlComments"));

var _substituteScreenAtRules = _interopRequireDefault(require("./substituteScreenAtRules"));

var _prefixSelector = _interopRequireDefault(require("../util/prefixSelector"));

var _useMemo = require("../util/useMemo");

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function hasAtRule(css, atRule, condition) {
  let found = false;
  css.walkAtRules(atRule, condition === undefined ? () => {
    found = true;
    return false;
  } : node => {
    if (condition(node)) {
      found = true;
      return false;
    }
  });
  return found;
}

function cloneWithoutChildren(node) {
  if (node.type === 'atrule') {
    return _postcss.default.atRule({
      name: node.name,
      params: node.params
    });
  }

  if (node.type === 'rule') {
    return _postcss.default.rule({
      name: node.name,
      selectors: node.selectors
    });
  }

  const clone = node.clone();
  clone.removeAll();
  return clone;
}

const tailwindApplyPlaceholder = _postcssSelectorParser.default.attribute({
  attribute: '__TAILWIND-APPLY-PLACEHOLDER__'
});

function generateRulesFromApply({
  rule,
  utilityName: className,
  classPosition
}, replaceWiths) {
  const parser = (0, _postcssSelectorParser.default)(selectors => {
    let i = 0;
    selectors.walkClasses(c => {
      if (classPosition === i++ && c.value === className) {
        c.replaceWith(tailwindApplyPlaceholder);
      }
    });
  });

  const processedSelectors = _lodash.default.flatMap(rule.selectors, selector => {
    // You could argue we should make this replacement at the AST level, but if we believe
    // the placeholder string is safe from collisions then it is safe to do this is a simple
    // string replacement, and much, much faster.
    return replaceWiths.map(replaceWith => parser.processSync(selector).replace('[__TAILWIND-APPLY-PLACEHOLDER__]', replaceWith));
  });

  const cloned = rule.clone();
  let current = cloned;
  let parent = rule.parent;

  while (parent && parent.type !== 'root') {
    const parentClone = cloneWithoutChildren(parent);
    parentClone.append(current);
    current.parent = parentClone;
    current = parentClone;
    parent = parent.parent;
  }

  cloned.selectors = processedSelectors;
  return current;
}

const extractUtilityNamesParser = (0, _postcssSelectorParser.default)(selectors => {
  let classes = [];
  selectors.walkClasses(c => classes.push(c.value));
  return classes;
});
const extractUtilityNames = (0, _useMemo.useMemo)(selector => extractUtilityNamesParser.transformSync(selector), selector => selector);
const cloneRuleWithParent = (0, _useMemo.useMemo)(rule => rule.clone({
  parent: rule.parent
}), rule => rule);

function buildCssUtilityMap(css, startIndex) {
  let index = startIndex;
  const utilityMap = {};

  function handle(getRule, rule) {
    const utilityNames = extractUtilityNames(rule.selector);
    utilityNames.forEach((utilityName, i) => {
      if (utilityMap[utilityName] === undefined) {
        utilityMap[utilityName] = [];
      }

      utilityMap[utilityName].push({
        index,
        utilityName,
        classPosition: i,
        ...getRule(rule)
      });
      index++;
    });
  } // This is the end user's css. This might contain rules that we want to
  // apply. We want immediate copies of everything in case that we have user
  // defined classes that are recursively applied. Down below we are modifying
  // the rules directly. We could do a better solution where we keep track of a
  // dependency tree, but that is a bit more complex. Might revisit later,
  // we'll see how this turns out!


  css.walkRules(handle.bind(null, rule => ({
    rule: cloneRuleWithParent(rule)
  })));
  return utilityMap;
}

const buildLookupTreeUtilityMap = (0, _useMemo.useMemo)(lookupTree => {
  let index = 0;
  const utilityMap = {};

  function handle(getRule, rule) {
    const utilityNames = extractUtilityNames(rule.selector);
    utilityNames.forEach((utilityName, i) => {
      if (utilityMap[utilityName] === undefined) {
        utilityMap[utilityName] = [];
      }

      utilityMap[utilityName].push({
        index,
        utilityName,
        classPosition: i,
        ...getRule(rule)
      });
      index++;
    });
  } // Lookup tree is the big lookup tree, making the rule lazy allows us to save
  // some memory because we don't need everything.


  lookupTree.walkRules(handle.bind(null, rule => ({
    get rule() {
      return cloneRuleWithParent(rule);
    }

  })));
  return utilityMap;
}, tree => tree);

function mergeAdjacentRules(initialRule, rulesToInsert) {
  let previousRule = initialRule;
  rulesToInsert.forEach(toInsert => {
    if (toInsert.type === 'rule' && previousRule.type === 'rule' && toInsert.selector === previousRule.selector) {
      previousRule.append(toInsert.nodes);
    } else if (toInsert.type === 'atrule' && previousRule.type === 'atrule' && toInsert.params === previousRule.params) {
      const merged = mergeAdjacentRules(previousRule.nodes[previousRule.nodes.length - 1], toInsert.nodes);
      previousRule.append(merged);
    } else {
      previousRule = toInsert;
    }

    toInsert.walk(n => {
      if (n.nodes && n.nodes.length === 0) {
        n.remove();
      }
    });
  });
  return rulesToInsert.filter(r => r.nodes.length > 0);
}

function makeExtractUtilityRules(css, lookupTree, config) {
  const lookupTreeUtilityMap = buildLookupTreeUtilityMap(lookupTree);
  const lookupTreeUtilityMapKeys = Object.keys(lookupTreeUtilityMap);
  const utilityMap = buildCssUtilityMap(css, lookupTreeUtilityMapKeys.length);

  function getUtility(utilityName) {
    const utility = [];

    if (lookupTreeUtilityMap[utilityName]) {
      utility.push(...lookupTreeUtilityMap[utilityName]);
    }

    if (utilityMap[utilityName]) {
      utility.push(...utilityMap[utilityName]);
    }

    if (utility.length > 0) return utility;
  }

  return function extractUtilityRules(utilityNames, rule) {
    const combined = [];
    utilityNames.forEach(utilityName => {
      const utility = getUtility(utilityName);

      if (utility === undefined) {
        // Look for prefixed utility in case the user has goofed
        const prefixedUtilityName = (0, _prefixSelector.default)(config.prefix, `.${utilityName}`).slice(1);
        const prefixedUtility = getUtility(prefixedUtilityName);

        if (prefixedUtility !== undefined) {
          throw rule.error(`The \`${utilityName}\` class does not exist, but \`${prefixedUtilityName}\` does. Did you forget the prefix?`);
        }

        const suggestedClass = (0, _didyoumean.default)(utilityName, Object.keys(utilityMap).concat(lookupTreeUtilityMapKeys));
        const suggestionMessage = suggestedClass ? `, but \`${suggestedClass}\` does` : '';
        throw rule.error(`The \`${utilityName}\` class does not exist${suggestionMessage}. If you're sure that \`${utilityName}\` exists, make sure that any \`@import\` statements are being properly processed before Tailwind CSS sees your CSS, as \`@apply\` can only be used for classes in the same CSS tree.`, {
          word: utilityName
        });
      }

      combined.push(...utility);
    });
    return combined.sort((a, b) => a.index - b.index);
  };
}

function findParent(rule, predicate) {
  let parent = rule.parent;

  while (parent) {
    if (predicate(parent)) {
      return parent;
    }

    parent = parent.parent;
  }

  throw new Error('No parent could be found');
}

function processApplyAtRules(css, lookupTree, config) {
  const extractUtilityRules = makeExtractUtilityRules(css, lookupTree, config);

  do {
    css.walkAtRules('apply', applyRule => {
      const parent = applyRule.parent; // Direct parent

      const nearestParentRule = findParent(applyRule, r => r.type === 'rule');
      const currentUtilityNames = extractUtilityNames(nearestParentRule.selector);

      const [importantEntries, applyUtilityNames, important = importantEntries.length > 0] = _lodash.default.partition(applyRule.params.split(/[\s\t\n]+/g), n => n === '!important');

      if (_lodash.default.intersection(applyUtilityNames, currentUtilityNames).length > 0) {
        const currentUtilityName = _lodash.default.intersection(applyUtilityNames, currentUtilityNames)[0];

        throw parent.error(`You cannot \`@apply\` the \`${currentUtilityName}\` utility here because it creates a circular dependency.`);
      } // Extract any post-apply declarations and re-insert them after apply rules


      const afterRule = parent.clone({
        raws: {}
      });
      afterRule.nodes = afterRule.nodes.slice(parent.index(applyRule) + 1);
      parent.nodes = parent.nodes.slice(0, parent.index(applyRule) + 1); // Sort applys to match CSS source order

      const applys = extractUtilityRules(applyUtilityNames, applyRule); // Get new rules with the utility portion of the selector replaced with the new selector

      const rulesToInsert = [];
      applys.forEach(nearestParentRule === parent ? util => rulesToInsert.push(generateRulesFromApply(util, parent.selectors)) : util => util.rule.nodes.forEach(n => afterRule.append(n.clone())));
      rulesToInsert.forEach(rule => {
        if (rule.type === 'atrule') {
          rule.walkRules(rule => {
            rule.__tailwind = { ...rule.__tailwind,
              important
            };
          });
        } else {
          rule.__tailwind = { ...rule.__tailwind,
            important
          };
        }
      });

      const {
        nodes
      } = _lodash.default.tap(_postcss.default.root({
        nodes: rulesToInsert
      }), root => {
        root.walkDecls(d => {
          d.important = important;
        });
      });

      const mergedRules = mergeAdjacentRules(nearestParentRule, [...nodes, afterRule]);
      applyRule.remove();
      parent.after(mergedRules); // If the base rule has nothing in it (all applys were pseudo or responsive variants),
      // remove the rule fuggit.

      if (parent.nodes.length === 0) {
        parent.remove();
      }
    }); // We already know that we have at least 1 @apply rule. Otherwise this
    // function would not have been called. Therefore we can execute this code
    // at least once. This also means that in the best case scenario we only
    // call this 2 times, instead of 3 times.
    // 1st time -> before we call this function
    // 2nd time -> when we check if we have to do this loop again (because do {} while (check))
    // .. instead of
    // 1st time -> before we call this function
    // 2nd time -> when we check the first time (because while (check) do {})
    // 3rd time -> when we re-check to see if we should do this loop again
  } while (hasAtRule(css, 'apply'));

  return css;
}

let defaultTailwindTree = new Map();

function substituteClassApplyAtRules(config, getProcessedPlugins, configChanged) {
  return function (css) {
    // We can stop already when we don't have any @apply rules. Vue users: you're welcome!
    if (!hasAtRule(css, 'apply')) {
      return css;
    }

    let requiredTailwindAtRules = ['base', 'components', 'utilities'];

    if (hasAtRule(css, 'tailwind', node => {
      let idx = requiredTailwindAtRules.indexOf(node.params);
      if (idx !== -1) requiredTailwindAtRules.splice(idx, 1);
      if (requiredTailwindAtRules.length <= 0) return true;
      return false;
    })) {
      // Tree already contains all the at rules (requiredTailwindAtRules)
      return processApplyAtRules(css, _postcss.default.root(), config);
    }

    let lookupKey = requiredTailwindAtRules.join(','); // We mutated the `requiredTailwindAtRules`, but when we hit this point in
    // time, it means that we don't have all the atrules. The missing atrules
    // are listed inside the requiredTailwindAtRules, which we can use to fill
    // in the missing pieces.
    //
    // Important for <style> blocks in Vue components.

    const generateLookupTree = configChanged || !defaultTailwindTree.has(lookupKey) ? () => {
      return (0, _postcss.default)([(0, _substituteTailwindAtRules.default)(config, getProcessedPlugins()), (0, _evaluateTailwindFunctions.default)({
        tailwindConfig: config
      }), (0, _substituteVariantsAtRules.default)(config, getProcessedPlugins()), (0, _substituteResponsiveAtRules.default)(config), (0, _convertLayerAtRulesToControlComments.default)(config), (0, _substituteScreenAtRules.default)({
        tailwindConfig: config
      })]).process(requiredTailwindAtRules.map(rule => `@tailwind ${rule};`).join('\n'), {
        from: __filename
      }).then(result => {
        defaultTailwindTree.set(lookupKey, result);
        return result;
      });
    } : () => Promise.resolve(defaultTailwindTree.get(lookupKey));
    return generateLookupTree().then(result => {
      return processApplyAtRules(css, result.root, config);
    });
  };
}