import { isImportDefaultSpecifier, isImportNamespaceSpecifier, isIdentifier, isCallExpression, isStringLiteral, isObjectExpression, isSpreadElement, isObjectMethod, stringLiteral, logicalExpression, isArrayExpression, isTemplateLiteral, templateElement as templateElement$1, templateLiteral, isNullLiteral, isBooleanLiteral, isNumericLiteral, isLogicalExpression, isNodesEquivalent, isLiteral, isConditionalExpression, isFunction, isBlockStatement, isVariableDeclaration, isObjectPattern, isObjectProperty, isAssignmentPattern, isRestElement, memberExpression, identifier, isVariableDeclarator, isMemberExpression, isExpressionStatement, isAssignmentExpression, isImportDeclaration, isBinaryExpression, unaryExpression, numericLiteral, arrayExpression, isUnaryExpression, conditionalExpression, binaryExpression, objectExpression, objectProperty, isOptionalMemberExpression, callExpression, cloneNode } from '@babel/types';
import generate from '@babel/generator';
import findCacheDir from 'find-cache-dir';
import fs from 'fs';
import path from 'path';
import _flatMap from 'lodash/flatMap';
import _flattenDeep from 'lodash/flattenDeep';
import _isEqualWith from 'lodash/isEqualWith';
import hash from 'object-hash';
import template from '@babel/template';
import _map from 'lodash/fp/map';
import _uniqBy from 'lodash/fp/uniqBy';
import _flow from 'lodash/fp/flow';
import _partition from 'lodash/partition';
import _values from 'lodash/fp/values';
import _groupBy from 'lodash/fp/groupBy';
import _partition$1 from 'lodash/fp/partition';

function _defineProperty(obj, key, value) {
  if (key in obj) {
    Object.defineProperty(obj, key, {
      value: value,
      enumerable: true,
      configurable: true,
      writable: true
    });
  } else {
    obj[key] = value;
  }

  return obj;
}

function ownKeys(object, enumerableOnly) {
  var keys = Object.keys(object);

  if (Object.getOwnPropertySymbols) {
    var symbols = Object.getOwnPropertySymbols(object);
    if (enumerableOnly) symbols = symbols.filter(function (sym) {
      return Object.getOwnPropertyDescriptor(object, sym).enumerable;
    });
    keys.push.apply(keys, symbols);
  }

  return keys;
}

function _objectSpread2(target) {
  for (var i = 1; i < arguments.length; i++) {
    var source = arguments[i] != null ? arguments[i] : {};

    if (i % 2) {
      ownKeys(Object(source), true).forEach(function (key) {
        _defineProperty(target, key, source[key]);
      });
    } else if (Object.getOwnPropertyDescriptors) {
      Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
    } else {
      ownKeys(Object(source)).forEach(function (key) {
        Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
      });
    }
  }

  return target;
}

const DefaultSettings = {
  libraries: ['clsx', 'classnames'],
  functionNames: [],
  removeUnnecessaryCalls: true,
  collectCalls: false
};
function getOptions(options = {}) {
  return _objectSpread2({}, DefaultSettings, {}, options);
}

const getFunctions = {
  CallExpression(path) {
    const {
      callee
    } = path.node;

    if (isIdentifier(callee) && this.options.functionNames.includes(callee.name)) {
      path.setData('is_optimizable', true);
      this.functions.push(path);
    }
  }

};
function isImportMap(node) {
  return node.referenceCount !== undefined;
}
function findExpressions(program, options) {
  const result = [];

  if (options.functionNames.length) {
    program.traverse(getFunctions, {
      options,
      functions: result
    });
  }

  if (!options.libraries.length) {
    return result;
  }

  const addToState = (path, name) => {
    const binding = path.scope.getBinding(name);

    if (!binding) {
      return;
    }

    const expressions = [];
    const refPaths = binding.referencePaths;

    for (const ref of refPaths) {
      const call = ref.parentPath;

      if (call.isCallExpression()) {
        call.setData('is_optimizable', true);
        expressions.push(call);
      }
    }

    if (expressions.length) {
      result.push({
        source: path,
        referenceCount: binding.references,
        expressions
      });
    }
  };

  const body = program.get('body');

  for (const statement of body) {
    // import x from 'y';
    // import * as x from 'y';
    if (statement.isImportDeclaration() && options.libraries.includes(statement.node.source.value) && statement.node.specifiers.length === 1 && (isImportDefaultSpecifier(statement.node.specifiers[0]) || isImportNamespaceSpecifier(statement.node.specifiers[0]))) {
      addToState(statement, statement.node.specifiers[0].local.name);
    } // const x = require('y');
    else if (statement.isVariableDeclaration()) {
        statement.get('declarations').forEach(decPath => {
          const dec = decPath.node;

          if (isIdentifier(dec.id) && isCallExpression(dec.init) && isIdentifier(dec.init.callee, {
            name: 'require'
          }) && dec.init.arguments.length === 1 && isStringLiteral(dec.init.arguments[0]) && options.libraries.includes(dec.init.arguments[0].value)) {
            addToState(decPath, dec.id.name);
          }
        });
      }
  }

  return result;
}

let stream = null;
let count = 0;
const collectCalls = ({
  expression,
  options,
  filename
}) => {
  if (!options.collectCalls) {
    return;
  }

  if (stream === null) {
    const cacheDir = findCacheDir({
      name: 'optimize-clsx',
      create: true
    });

    if (!cacheDir) {
      throw new Error('Unable to locate cache directory');
    }

    const filePath = path.join(cacheDir, `log-${new Date(Date.now()).toISOString().replace(/:/g, '.')}.js`);
    stream = fs.createWriteStream(filePath, {
      flags: 'w'
    });
    console.log('Writing CallExpressions to ' + filePath);
  }

  let locationStr = '';

  if (expression.node.loc) {
    const location = expression.node.loc.start;
    locationStr = `:${location.line}:${location.column}`;
  }

  stream.write(`// ${filename}${locationStr}\nconst x${++count} = ${generate(expression.node).code};\n\n`);
};

const extractObjectProperties = ({
  expression
}) => {
  expression.node.arguments = _flatMap(expression.node.arguments, node => {
    if (!isObjectExpression(node)) {
      return node;
    }

    return node.properties.map(p => {
      if (isSpreadElement(p)) {
        return p.argument;
      } else {
        const getKey = () => {
          return p.computed ? p.key : isStringLiteral(p.key) ? p.key : stringLiteral(p.key.name);
        };

        if (isObjectMethod(p)) {
          return getKey();
        } else {
          // @ts-ignore
          return logicalExpression('&&', p.value, getKey());
        }
      }
    });
  });
};

function unwrapArrayExpression(nodes) {
  return nodes.map(item => isArrayExpression(item) ? unwrapArrayExpression(item.elements) : item);
}

const flattenArrays = ({
  expression
}) => {
  expression.node.arguments = _flattenDeep(unwrapArrayExpression(expression.node.arguments));
};

function isStringLike(node) {
  return isStringLiteral(node) || isTemplateLiteral(node);
}
function combineStringLike(a, b) {
  if (isStringLikeEmpty(a) && isStringLikeEmpty(b)) {
    return stringLiteral('');
  }

  if (isStringLiteral(a) && isStringLiteral(b)) {
    return stringLiteral(a.value + ' ' + b.value);
  }

  if (isTemplateLiteral(a) && isTemplateLiteral(b)) {
    const expressions = [...a.expressions, ...b.expressions];
    const quasis = [...a.quasis];
    quasis[quasis.length - 1] = templateElement(quasis[quasis.length - 1].value.raw + ' ' + b.quasis[0].value.raw);
    quasis.push(...b.quasis.slice(1));
    return templateOrStringLiteral(quasis, expressions);
  }

  if (isTemplateLiteral(a) && isStringLiteral(b)) {
    const expressions = [...a.expressions];
    const quasis = [...a.quasis];
    const i = quasis.length - 1;
    quasis[i] = templateElement(quasis[i].value.raw + ' ' + b.value, true);
    return templateOrStringLiteral(quasis, expressions);
  }

  if (isStringLiteral(a) && isTemplateLiteral(b)) {
    const expressions = [...b.expressions];
    const quasis = [...b.quasis];
    const i = 0;
    quasis[i] = templateElement(a.value + ' ' + quasis[i].value.raw, true);
    return templateOrStringLiteral(quasis, expressions);
  }

  throw new Error('Unable to handle that input');
}
function isStringLikeEmpty(node) {
  if (isStringLiteral(node)) {
    return node.value.length === 0;
  }

  if (isTemplateLiteral(node)) {
    return node.expressions.length === 0 && node.quasis.every(q => q.value.raw.length === 0);
  }

  return false;
}

function templateOrStringLiteral(quasis, expressions) {
  if (expressions.length === 0) {
    return stringLiteral(quasis[0].value.raw);
  }

  return templateLiteral(quasis, expressions);
}

function templateElement(value, tail = false) {
  // Workaround to get the cooked value
  // https://github.com/babel/babel/issues/9242
  const valueAST = template.ast(`\`${value}\``).expression.quasis[0].value;
  return templateElement$1(valueAST, tail);
}

function flattenLogicalExpression(rootNode) {
  const result = [];
  checkNode(rootNode);
  return result;

  function checkNode(node) {
    if (isLogicalExpression(node)) {
      checkNode(node.left);
      result.push(node.right);
    } else {
      result.push(node);
    }
  }
}
function isNestedLogicalAndExpression(node) {
  if (!isLogicalExpression(node, {
    operator: '&&'
  })) {
    return false;
  }

  let temp = node.left;

  while (isLogicalExpression(temp)) {
    if (temp.operator !== '&&') {
      return false;
    }

    temp = temp.left;
  }

  return true;
}
function getMostFrequentNode(operators) {
  let maxNode = null;
  let maxCount = 0;
  let operators_n = operators.length;

  for (let y = 0, y_n = operators_n - 1; y < y_n; y++) {
    for (let x = 0, row = operators[y], x_n = row.length - 1; x < x_n; x++) {
      let col = row[x];
      let count = 0;

      for (let y2 = y + 1; y2 < operators_n; y2++) {
        for (let x2 = 0, row2 = operators[y2]; x2 < row2.length - 1; x2++) {
          if (compareNodes(col, row2[x2])) {
            count += 1;
          }
        }
      }

      if (count > maxCount) {
        maxNode = col;
        maxCount = count;
      }
    }
  }

  return maxNode;
}
function compareNodes(obj1, obj2) {
  if (obj1.type !== obj2.type) {
    return false;
  }

  switch (obj1.type) {
    case 'NullLiteral':
      {
        return true;
      }

    case 'RegExpLiteral':
      {
        return isNodesEquivalent(obj1, obj2);
      }

    case 'Identifier':
      return obj1.name === obj2.name;

    case 'MemberExpression':
      return compareNodes(obj1.object, obj2.object) && compareNodes(obj1.property, obj2.property);

    case 'BinaryExpression':
      return obj1.operator === obj2.operator && compareNodes(obj1.left, obj2.left) && compareNodes(obj1.right, obj2.right);

    default:
      {
        if (isLiteral(obj1) && !isTemplateLiteral(obj1)) {
          return obj1.value === obj2.value;
        }

        return _isEqualWith(obj1, obj2, (v1, v2, key) => key === 'start' || key === 'end' || key === 'loc' ? true : undefined);
      }
  }
}
function hashNode(node) {
  return hash(node, {
    excludeKeys: key => key === 'start' || key === 'end' || key === 'loc' || key === 'extra' ? true : false
  });
}
function isSafeConditionalExpression(node) {
  if (!isConditionalExpression(node)) {
    return false;
  }

  const {
    consequent,
    alternate
  } = node;

  if (isStringLike(consequent) && isStringLike(alternate)) {
    return true;
  }

  if (isStringLike(consequent) && isSafeConditionalExpression(alternate) || isStringLike(alternate) && isSafeConditionalExpression(consequent)) {
    return true;
  }

  return false;
}
function createLogicalAndExpression(items) {
  return items.reduce((prev, curr) => logicalExpression('&&', prev, curr));
}
function isNodeFalsy(node) {
  return isStringLikeEmpty(node) || isNullLiteral(node) || isIdentifier(node, {
    name: 'undefined'
  }) || isBooleanLiteral(node, {
    value: false
  }) || isNumericLiteral(node, {
    value: 0
  });
}

const replaceVisitor = {
  BinaryExpression(path) {
    const node = path.node;

    if (isBinaryExpression(node) && (node.operator === '===' || node.operator === '!==') && (isStringLiteral(node.right) || isStringLiteral(node.left))) {
      const propType = this.types.find(obj => obj.matches.some(item => (obj.isRequired || item.hasDefaultValue) && (compareNodes(node.left, item) || compareNodes(node.right, item))));
      if (propType === undefined) return;
      const valueToUse = propType.optionA.length < propType.optionB.length ? propType.optionA : propType.optionB;
      const target = isStringLiteral(node.right) ? 'right' : 'left';

      if (node[target].value !== valueToUse && (node[target].value === propType.optionA || node[target].value === propType.optionB)) {
        node[target] = stringLiteral(valueToUse);
        node.operator = node.operator === '!==' ? '===' : '!==';
      }
    }
  }

};
const functionVisitor = {
  'FunctionExpression|FunctionDeclaration'(path) {
    const node = path.node;

    for (const propType of this.propTypes) {
      if (isFunction(node) && isIdentifier(node.id, {
        name: propType.name
      }) && node.params.length !== 0) {
        getObjectPattern(node.params[0], null);

        function getObjectPattern(item, matcher) {
          if (Array.isArray(item)) {
            for (const obj of item) {
              getObjectPattern(obj, matcher);
            }

            return;
          }

          if (isBlockStatement(item)) {
            return getObjectPattern(item.body, matcher);
          }

          if (isVariableDeclaration(item)) {
            return getObjectPattern(item.declarations, matcher);
          }

          if (isObjectPattern(item)) {
            return getObjectPattern(item.properties, null);
          }

          if (isObjectProperty(item) && isIdentifier(item.key)) {
            let matchValue = null;

            if (isIdentifier(item.value)) {
              matchValue = item.value;
            } else if (isAssignmentPattern(item.value)) {
              matchValue = item.value.left;
              matchValue.hasDefaultValue = true;
            } else {
              return;
            }

            propType.values = propType.values.map(prop => {
              if (prop.name !== item.key.name) return prop;

              if (prop.matches === undefined) {
                prop.matches = [];
              }

              return _objectSpread2({}, prop, {
                matches: [...prop.matches, matchValue]
              });
            });
            return;
          }

          if (isRestElement(item) && isIdentifier(item.argument)) {
            return getObjectPattern(item.argument, null);
          }

          if (isIdentifier(item)) {
            propType.values = propType.values.map(prop => {
              if (prop.matches === undefined) {
                prop.matches = [];
              }

              return _objectSpread2({}, prop, {
                matches: [...prop.matches, memberExpression(item, identifier(prop.name))]
              });
            });
            return getObjectPattern(node.body, item);
          }

          if (isVariableDeclarator(item)) {
            if (isMemberExpression(item.init) && compareNodes(item.init.object, matcher) && isIdentifier(item.init.property) && isIdentifier(item.id)) {
              propType.values = propType.values.map(prop => {
                if (prop.name !== item.init.property.name) return prop;

                if (prop.matches === undefined) {
                  prop.matches = [];
                }

                return _objectSpread2({}, prop, {
                  matches: [...prop.matches, item.id]
                });
              });
            } else if (isIdentifier(item.init) && compareNodes(item.init, matcher) && isObjectPattern(item.id)) {
              return getObjectPattern(item.id, null);
            }
          }
        }

        path.traverse(replaceVisitor, {
          types: propType.values
        });
        path.skip();
      }
    }
  }

};

function getPropTypes(body) {
  const propTypeName = getPropTypesName(body);
  if (propTypeName === undefined) return [];
  const result = [];

  for (const node of body) {
    if (isExpressionStatement(node)) {
      const expr = node.expression;

      if (isAssignmentExpression(expr, {
        operator: '='
      }) && isMemberExpression(expr.left) && isIdentifier(expr.left.property, {
        name: 'propTypes'
      }) && isIdentifier(expr.left.object)) {
        let propType = {
          name: expr.left.object.name,
          values: []
        };

        if (isObjectExpression(expr.right)) {
          for (const prop of expr.right.properties) {
            getArrayElements(prop.value, false);

            function getArrayElements(element, isRequired) {
              if (isMemberExpression(element) && isIdentifier(element.property, {
                name: 'isRequired'
              })) {
                getArrayElements(element.object, true);
              } else if (isCallExpression(element) && isMemberExpression(element.callee) && isIdentifier(element.callee.object, {
                name: propTypeName
              }) && isIdentifier(element.callee.property, {
                name: 'oneOf'
              }) && element.arguments.length === 1) {
                const value = element.arguments[0];

                if (isArrayExpression(value) && value.elements.length === 2 && isStringLiteral(value.elements[0]) && isStringLiteral(value.elements[1])) {
                  propType.values.push({
                    name: prop.key.name,
                    isRequired: isRequired,
                    optionA: value.elements[0].value,
                    optionB: value.elements[1].value
                  });
                }
              }
            }
          }
        }

        if (propType.values.length !== 0) {
          result.push(propType);
        }
      }
    }
  }

  return result;
}

function getPropTypesName(body) {
  for (const node of body) {
    if (isImportDeclaration(node) && node.source.value === 'prop-types' && node.specifiers.length === 1) {
      const spec = node.specifiers[0];

      if (isImportDefaultSpecifier(spec)) {
        return spec.local.name;
      }
    }
  }
}

const propTypes = ({
  program: path,
  state
}) => {
  // This visitor should only run once
  if (state.has('proptypes')) {
    return;
  }

  state.add('proptypes');
  const propTypes = getPropTypes(path.node.body);
  if (propTypes.length === 0) return;
  path.traverse(functionVisitor, {
    propTypes
  });
};

const optimizations = {
  ConditionalExpression(node) {
    // foo ? bar : '' --> foo && bar
    if (isNodeFalsy(node.alternate)) {
      return logicalExpression('&&', node.test, node.consequent);
    } // foo ? '' : bar --> !foo && bar


    if (isNodeFalsy(node.consequent)) {
      return logicalExpression('&&', unaryExpression('!', node.test), node.alternate);
    }
  },

  BinaryExpression(node) {
    // This transform allows createConditionalExpression to optimize the expression later
    // foo % 2 === 1 --> foo % 2 !== 0
    // foo % 2 !== 1 --> foo % 2 === 0
    if ((node.operator === '!==' || node.operator === '===') && isNumericLiteral(node.right, {
      value: 1
    }) && isBinaryExpression(node.left, {
      operator: '%'
    }) && isNumericLiteral(node.left.right, {
      value: 2
    })) {
      return _objectSpread2({}, node, {
        right: numericLiteral(0),
        operator: node.operator === '===' ? '!==' : '==='
      });
    }
  },

  LogicalExpression(node) {
    // foo || '' -> foo
    if (node.operator === '||' && isNodeFalsy(node.right)) {
      return node.left;
    }

    if (isNestedLogicalAndExpression(node)) {
      return _flow(flattenLogicalExpression, // Remove duplicates in the same expression
      // foo && bar && bar --> foo && bar
      _uniqBy(hashNode), // Optimize individual expressions
      _map(optimizeExpression), createLogicalAndExpression)(node);
    }
  }

};

function optimizeExpression(node) {
  if (node.type in optimizations) {
    // @ts-ignore
    const result = optimizations[node.type](node);

    if (result) {
      // @ts-ignore
      return result.type !== node.type || result.operator !== node.operator ? optimizeExpression(result) : result;
    }
  }

  return node;
}

const optimizeExpressions = ({
  expression
}) => {
  expression.node.arguments = expression.node.arguments.map(optimizeExpression);
};

const stripLiterals = ({
  expression: path
}) => {
  const [match, noMatch] = _partition(path.node.arguments, isNestedLogicalAndExpression);

  const result = match.map(flattenLogicalExpression).map(expression => // Remove values that will always be true
  expression.filter((item, index) => !(isBooleanLiteral(item, {
    value: true
  }) || index !== expression.length - 1 && isNumericLiteral(item) && item.value !== 0 || index !== expression.length - 1 && isStringLike(item) && !isStringLikeEmpty(item)))) // Remove expressions that will always be false
  .filter(expression => !(expression.length === 0 || expression.some(isNodeFalsy))).map(createLogicalAndExpression);
  const rest = noMatch.filter(item => !isNodeFalsy(item));
  path.node.arguments = [...rest, ...result];
};

const combineArguments = ({
  expression: path
}) => {
  // Not enough arguments to optimize
  if (path.node.arguments.length < 2) return;

  const [match, noMatch] = _partition(path.node.arguments, isNestedLogicalAndExpression); // Not enough arguments to optimize


  if (match.length < 2) return;
  const operators = match.map(flattenLogicalExpression);
  const node = getMostFrequentNode(operators); // No nodes appear more than once

  if (node === null) return;
  const rootNode = combineOperators(operators, node);
  const newAST = convertToAST(rootNode);
  path.node.arguments = [...noMatch, ...newAST];
  return;

  function convertToAST(node) {
    if (node.type !== 'rootNode') {
      return node;
    }

    const result = [];

    if (node.child.type === 'rootNode') {
      let right = convertToAST(node.child);

      if (right.length === 1) {
        right = right[0];
      } else {
        right = arrayExpression(right);
      }

      result.push(logicalExpression('&&', node.node, right));
    } else {
      result.push(logicalExpression('&&', node.node, node.child));
    }

    if (node.next !== undefined) {
      const r = convertToAST(node.next);
      Array.isArray(r) ? result.push(...r) : result.push(r);
    }

    return result;
  }

  function combineOperators(operators, node) {
    const newNode = {
      type: 'rootNode',
      node: node,
      child: [],
      next: []
    };
    operators.forEach(row => {
      const filtered = row.filter(item => !compareNodes(item, node));

      if (filtered.length === row.length) {
        newNode.next.push(row);
      } else {
        newNode.child.push(filtered);
      }
    });
    newNode.next = checkSub(newNode.next);
    const child = checkSub(newNode.child);

    if (Array.isArray(child)) {
      newNode.child = child.length === 1 ? child[0] : arrayExpression(child);
    } else {
      newNode.child = child;
    }

    return newNode;

    function checkSub(items) {
      if (items.length === 0) return undefined;

      if (items.length > 1) {
        const nextCheck = getMostFrequentNode(items);

        if (nextCheck !== null) {
          return combineOperators(items, nextCheck);
        }
      }

      return items.map(createLogicalAndExpression);
    }
  }
};

function combineStringsInArray(array) {
  if (array.length < 2) {
    return array;
  }

  const [match, noMatch] = _partition(array, isStringLike);

  if (match.length < 2) {
    return array;
  }

  return [match.reduce(combineStringLike), ...noMatch];
}

const arrayVisitor = {
  ArrayExpression(path) {
    const {
      node
    } = path;
    node.elements = combineStringsInArray(node.elements);

    if (node.elements.length === 1 && node.elements[0]) {
      path.replaceWith(node.elements[0]);
    }
  }

};
const combineStringLiterals = ({
  expression: path
}) => {
  path.node.arguments = combineStringsInArray(path.node.arguments);
  path.traverse(arrayVisitor);
};

const createConditionalExpression = ({
  expression: path
}) => {
  path.node.arguments = combine(path.node.arguments);

  function combine(args) {
    const [match, noMatch] = _partition(args, isNestedLogicalAndExpression);

    if (match.length === 0) return noMatch;
    let operators = match.map(flattenLogicalExpression) // If the last item in each row is an array, recursivly call ourself
    .map(row => {
      let col = row.pop();

      if (isArrayExpression(col)) {
        col.elements = combine(col.elements);

        if (col.elements.length === 1) {
          col = col.elements[0];
        }
      }

      row.push(col);
      return row;
    });
    const result = noMatch;

    for (let row_index = 0; row_index < operators.length; row_index++) {
      const row = operators[row_index];

      if (checkRow()) {
        row_index -= 1;
      }

      function checkRow() {
        for (let col_index = 0; col_index < row.length - 1; col_index++) {
          const col = row[col_index];
          const isUnary = isUnaryExpression(col, {
            operator: '!'
          });
          const isBinary = isBinaryExpression(col, {
            operator: '!=='
          });
          if (!isUnary && !isBinary) continue;

          for (let row2_index = 0; row2_index < operators.length; row2_index++) {
            const row2 = operators[row2_index];

            for (let col2_index = 0; col2_index < row2.length - 1; col2_index++) {
              const col2 = row2[col2_index];
              if (row_index === row2_index && col_index === col2_index) continue;

              if (isUnary && !compareNodes(col.argument, col2)) {
                continue;
              } else if (isBinary && !(isBinaryExpression(col2, {
                operator: '==='
              }) && (compareNodes(col.left, col2.left) && compareNodes(col.right, col2.right) || compareNodes(col.left, col2.right) && compareNodes(col.right, col2.left)))) {
                continue;
              }

              const left = createLogicalAndExpression(row2.filter((e, index) => index !== col2_index));
              const right = createLogicalAndExpression(row.filter((e, index) => index !== col_index)); // isUnary: col2 is 1 char shorter (col2: foo vs col: !foo)
              // isBinary: col2 has the === operator

              result.push(conditionalExpression(col2, left, right)); // Remove from collection

              operators = operators.filter((e, index) => index !== row_index && index !== row2_index);
              return true;
            }
          }
        }

        return false;
      }
    }

    return [...result, ...operators.map(createLogicalAndExpression)];
  }
};

const transforms = [function noArgumentsToString() {
  return stringLiteral('');
}, function singleStringLike(arg) {
  if (isStringLike(arg) || isSafeConditionalExpression(arg)) {
    return arg;
  }
}, function multipleSafeConditionals(conditionalOne, conditionalTwo) {
  if (isSafeConditionalExpression(conditionalOne) && isSafeConditionalExpression(conditionalTwo)) {
    const newCond = conditionalExpression(conditionalOne.test, combineStringLike(conditionalOne.consequent, stringLiteral('')), combineStringLike(conditionalOne.alternate, stringLiteral('')));
    return binaryExpression('+', newCond, conditionalTwo);
  }
}, function stringAndSafeConditional(stringLike, conditional) {
  if (isStringLike(stringLike) && isSafeConditionalExpression(conditional)) {
    if (isStringLikeEmpty(conditional.consequent) || isStringLikeEmpty(conditional.alternate)) {
      return binaryExpression('+', stringLike, conditionalExpression(conditional.test, combineStringLike(stringLiteral(''), conditional.consequent), combineStringLike(stringLiteral(''), conditional.alternate)));
    }

    return binaryExpression('+', combineStringLike(stringLike, stringLiteral('')), conditional);
  }
}, function singleLogicalExpression(logicalExpr) {
  if (isLogicalExpression(logicalExpr, {
    operator: '&&'
  }) && (isStringLike(logicalExpr.right) || isSafeConditionalExpression(logicalExpr.right) || isBinaryExpression(logicalExpr.right, {
    operator: '+'
  }) && isStringLike(logicalExpr.right.left) && isSafeConditionalExpression(logicalExpr.right.right))) {
    return conditionalExpression(logicalExpr.left, logicalExpr.right, stringLiteral(''));
  }
}, function stringAndLogicalExpression(stringLike, logicalExpr) {
  if (isStringLike(stringLike) && isLogicalExpression(logicalExpr, {
    operator: '&&'
  }) && isStringLike(logicalExpr.right)) {
    return binaryExpression('+', stringLike, conditionalExpression(logicalExpr.left, combineStringLike(stringLiteral(''), logicalExpr.right), stringLiteral('')));
  }
}];

function runTransforms(path, elements) {
  const reversed = [...elements].reverse();

  for (const tr of transforms) {
    var _tr$apply;

    if (tr.length !== elements.length) {
      continue;
    }

    const result = (_tr$apply = tr.apply(undefined, elements)) !== null && _tr$apply !== void 0 ? _tr$apply : tr.apply(undefined, reversed);

    if (result !== undefined) {
      path.replaceWith(result);
      break;
    }
  }
}

const arrayVisitor$1 = {
  ArrayExpression(path) {
    runTransforms(path, path.node.elements);
  }

};
const removeUnnecessaryCalls = ({
  expression: path,
  options
}) => {
  if (!options.removeUnnecessaryCalls) {
    return;
  }

  path.traverse(arrayVisitor$1);
  runTransforms(path, path.node.arguments);
};

function matchLeftOrRight(node, checkOrChecks) {
  if (!Array.isArray(checkOrChecks)) {
    return checkOrChecks(node.left) || checkOrChecks(node.right);
  }

  for (const _check of checkOrChecks) {
    if (_check(node.left) || _check(node.right)) {
      return true;
    }
  }

  return false;
}

function combineFromArray(arr) {
  // x === 'foo', 'foo' === x
  // x.y === 'foo', 'foo' === x.y
  // x?.y === 'foo', 'foo' === x?.y
  const [match, noMatch] = _partition$1(itm => {
    return checkItem(itm);

    function checkItem(item) {
      if (isNestedLogicalAndExpression(item)) {
        return checkItem(item.left);
      }

      return isBinaryExpression(item, {
        operator: '==='
      }) && matchLeftOrRight(item, isStringLiteral) && matchLeftOrRight(item, [isMemberExpression, isIdentifier, isOptionalMemberExpression]);
    }
  }, arr);

  if (match.length === 0) {
    return arr;
  }

  const newArgs = _flow(_map(flattenLogicalExpression), // Set the string to always be on the right side of ===
  // Simplifies the rest of the code
  _map(row => {
    const tempNode = row[0];

    if (isStringLiteral(tempNode.left)) {
      const strNode = tempNode.left;
      tempNode.left = tempNode.right;
      tempNode.right = strNode;
    }

    return row;
  }), // Group on whatever the strings are compared to
  _groupBy(row => hashNode(row[0].left)), // Removes the key created by groupBy
  _values, // Create the objects
  _map(group => {
    if (group.length === 1) {
      return createLogicalAndExpression(group[0]);
    }

    return memberExpression(objectExpression(group.map(row => objectProperty(row[0].right, createLogicalAndExpression(row.slice(1))))), group[0][0].left, true);
  }))(match);

  return [...noMatch, ...newArgs];
}

const arrayVisitor$2 = {
  ArrayExpression(path) {
    path.node.elements = combineFromArray(path.node.elements);

    if (path.node.elements.length === 1) {
      path.replaceWith(path.node.elements[0]);
    }
  }

};
const createObjectKeyLookups = ({
  expression: path
}) => {
  path.traverse(arrayVisitor$2);
  path.node.arguments = combineFromArray(path.node.arguments);
};

const referencedObjects = ({
  expression,
  pushToQueue
}) => {
  const args = expression.get('arguments');

  for (const nodePath of args) {
    if (!nodePath.isIdentifier()) {
      continue;
    }

    const binding = nodePath.scope.getBinding(nodePath.node.name);

    if (!binding) {
      continue;
    }

    if (binding.constant && binding.path.isVariableDeclarator() && isObjectExpression(binding.path.node.init) && binding.referencePaths.every(ref => ref.parentPath.isCallExpression() && ref.parentPath.getData('is_optimizable') === true)) {
      const init = binding.path.get('init');
      init.replaceWith(callExpression(cloneNode(expression.node.callee), [binding.path.node.init]));
      pushToQueue(init);
    }
  }
};

const visitors = [collectCalls, flattenArrays, extractObjectProperties, propTypes, optimizeExpressions, stripLiterals, combineArguments, combineStringLiterals, createConditionalExpression, removeUnnecessaryCalls, createObjectKeyLookups, referencedObjects];
var index = (() => ({
  visitor: {
    Program(path, state) {
      const options = getOptions(state.opts);
      const expressions = findExpressions(path, options);

      if (expressions.length === 0) {
        return;
      }

      const internalState = new Set();

      const runVisitors = (expression, pushToQueue) => {
        for (const visitor of visitors) {
          visitor({
            program: path,
            expression,
            state: internalState,
            options,
            filename: state.filename,
            pushToQueue
          });

          if (!expression.isCallExpression()) {
            return false;
          }
        }
      };

      for (let x = 0; x < expressions.length; x++) {
        const item = expressions[x];

        if (isImportMap(item)) {
          for (let y = 0; y < item.expressions.length; y++) {
            if (runVisitors(item.expressions[y], newExpression => {
              newExpression.setData('is_optimizable', true);
              item.expressions.push(newExpression);
              item.referenceCount += 1;
            }) === false) {
              item.expressions.splice(y, 1);
              item.referenceCount -= 1;
              y -= 1;
            }

            if (item.referenceCount === 0) {
              item.source.remove();
              expressions.splice(x, 1);
              x -= 1;
              break;
            }
          }
        } else if (runVisitors(item, newExpression => {
          newExpression.setData('is_optimizable', true);
          expressions.push(newExpression);
        }) === false) {
          expressions.splice(x, 1);
          x -= 1;
        }
      }
    }

  }
}));

export default index;
