import { NodePath } from "@babel/traverse";
import * as t from "@babel/types";

import { last } from "../array-helpers";

export {
  isClassPropertyIdentifier,
  isVariableDeclarationIdentifier,
  isFunctionCallIdentifier,
  isJSXPartialElement,
  isPartOfMemberExpression,
  isArrayExpressionElement,
  areAllObjectProperties,
  isUndefinedLiteral,
  isGuardClause,
  isGuardConsequentBlock,
  isNonEmptyReturn,
  hasFinalReturn,
  isTruthy,
  isFalsy,
  areSameAssignments,
  areEquivalent,
  isTemplateExpression,
  isInBranchedLogic,
  isInAlternate,
  areOpposite,
  areOppositeOperators,
  getOppositeOperator,
  canBeShorthand
};

function isClassPropertyIdentifier(path: NodePath): boolean {
  return (
    t.isClassProperty(path.parent) &&
    !path.parent.computed &&
    t.isIdentifier(path)
  );
}

function isVariableDeclarationIdentifier(path: NodePath): boolean {
  return t.isVariableDeclarator(path.parent) && t.isIdentifier(path);
}

function isFunctionCallIdentifier(path: NodePath): boolean {
  return t.isCallExpression(path.parent) && path.parent.callee === path.node;
}

function isJSXPartialElement(path: NodePath): boolean {
  return t.isJSXOpeningElement(path) || t.isJSXClosingElement(path);
}

function isPartOfMemberExpression(path: NodePath): boolean {
  return t.isMemberExpression(path.parent) && t.isIdentifier(path);
}

function isArrayExpressionElement(
  node: t.Node | null
): node is null | t.Expression | t.SpreadElement {
  return node === null || t.isExpression(node) || t.isSpreadElement(node);
}

function areAllObjectProperties(
  nodes: (t.Node | null)[]
): nodes is t.ObjectProperty[] {
  return nodes.every((node) => t.isObjectProperty(node));
}

function isUndefinedLiteral(
  node: object | null | undefined,
  opts?: object | null
): node is t.Identifier {
  return t.isIdentifier(node, opts) && node.name === "undefined";
}

function isGuardClause(path: NodePath<t.IfStatement>) {
  const { consequent, alternate } = path.node;
  if (Boolean(alternate)) return false;

  return t.isReturnStatement(consequent) || isGuardConsequentBlock(consequent);
}

function isTruthy(test: t.Expression): boolean {
  return areEquivalent(test, t.booleanLiteral(true));
}

function isFalsy(test: t.Expression): boolean {
  return areEquivalent(test, t.booleanLiteral(false));
}

function isGuardConsequentBlock(
  consequent: t.IfStatement["consequent"]
): consequent is t.BlockStatement {
  return t.isBlockStatement(consequent) && hasFinalReturn(consequent.body);
}

function isNonEmptyReturn(node: t.Node) {
  return t.isReturnStatement(node) && node.argument !== null;
}

function hasFinalReturn(statements: t.Statement[]): boolean {
  return t.isReturnStatement(last(statements));
}

function areSameAssignments(
  expressionA: t.AssignmentExpression,
  expressionB: t.AssignmentExpression
): boolean {
  return (
    areEquivalent(expressionA.left, expressionB.left) &&
    expressionA.operator === expressionB.operator
  );
}

function areEquivalent(nodeA: t.Node | null, nodeB: t.Node | null): boolean {
  if (nodeA === null) return false;
  if (nodeB === null) return false;

  if (t.isNullLiteral(nodeA) && t.isNullLiteral(nodeB)) return true;
  if (isUndefinedLiteral(nodeA) && isUndefinedLiteral(nodeB)) return true;
  if (t.isThisExpression(nodeA) && t.isThisExpression(nodeB)) return true;

  // Arrays
  if (t.isArrayExpression(nodeA) && t.isArrayExpression(nodeB)) {
    return areAllEqual(nodeA.elements, nodeB.elements);
  }

  // Objects
  if (t.isObjectExpression(nodeA) && t.isObjectExpression(nodeB)) {
    return areAllEqual(nodeA.properties, nodeB.properties);
  }
  if (t.isObjectProperty(nodeA) && t.isObjectProperty(nodeB)) {
    return (
      areEquivalent(nodeA.key, nodeB.key) &&
      areEquivalent(nodeA.value, nodeB.value)
    );
  }

  // Identifiers
  if (t.isIdentifier(nodeA) && t.isIdentifier(nodeB)) {
    return nodeA.name === nodeB.name;
  }

  // Functions
  if (
    t.isArrowFunctionExpression(nodeA) &&
    t.isArrowFunctionExpression(nodeB)
  ) {
    return areEquivalent(nodeA.body, nodeB.body);
  }

  // Call Expressions
  if (t.isCallExpression(nodeA) && t.isCallExpression(nodeB)) {
    return (
      areEquivalent(nodeA.callee, nodeB.callee) &&
      areAllEqual(nodeA.arguments, nodeB.arguments)
    );
  }

  // Binary & Logical Expressions
  if (
    (t.isLogicalExpression(nodeA) && t.isLogicalExpression(nodeB)) ||
    (t.isBinaryExpression(nodeA) && t.isBinaryExpression(nodeB))
  ) {
    return (
      nodeA.operator === nodeB.operator &&
      areEquivalent(nodeA.left, nodeB.left) &&
      areEquivalent(nodeA.right, nodeB.right)
    );
  }

  // Unary Expressions
  if (t.isUnaryExpression(nodeA) && t.isUnaryExpression(nodeB)) {
    return (
      nodeA.operator === nodeB.operator &&
      areEquivalent(nodeA.argument, nodeB.argument)
    );
  }

  // Member Expressions
  if (t.isMemberExpression(nodeA) && t.isMemberExpression(nodeB)) {
    return (
      areEquivalent(nodeA.property, nodeB.property) &&
      areEquivalent(nodeA.object, nodeB.object)
    );
  }

  // New Expressions
  if (t.isNewExpression(nodeA) && t.isNewExpression(nodeB)) {
    return (
      areEquivalent(nodeA.callee, nodeB.callee) &&
      areAllEqual(nodeA.arguments, nodeB.arguments)
    );
  }

  // JSX Elements
  if (t.isJSXElement(nodeA) && t.isJSXElement(nodeB)) {
    const areClosingElementsEqual =
      (nodeA.closingElement === null && nodeB.closingElement === null) ||
      areEquivalent(nodeA.closingElement, nodeB.closingElement);

    return (
      areEquivalent(nodeA.openingElement, nodeB.openingElement) &&
      areClosingElementsEqual &&
      areAllEqual(nodeA.children, nodeB.children)
    );
  }
  if (t.isJSXOpeningElement(nodeA) && t.isJSXOpeningElement(nodeB)) {
    return (
      areEquivalent(nodeA.name, nodeB.name) &&
      areAllEqual(nodeA.attributes, nodeB.attributes)
    );
  }
  if (t.isJSXClosingElement(nodeA) && t.isJSXClosingElement(nodeB)) {
    return areEquivalent(nodeA.name, nodeB.name);
  }
  if (t.isJSXAttribute(nodeA) && t.isJSXAttribute(nodeB)) {
    return (
      areEquivalent(nodeA.name, nodeB.name) &&
      areEquivalent(nodeA.value, nodeB.value)
    );
  }
  if (t.isJSXIdentifier(nodeA) && t.isJSXIdentifier(nodeB)) {
    return nodeA.name === nodeB.name;
  }

  // TS types
  if (t.isTSTypeAnnotation(nodeA) && t.isTSTypeAnnotation(nodeB)) {
    return nodeA.typeAnnotation.type === nodeB.typeAnnotation.type;
  }

  // Primitive values
  return "value" in nodeA && "value" in nodeB && nodeA.value === nodeB.value;
}

function areAllEqual(
  nodesA: (t.Node | null)[],
  nodesB: (t.Node | null)[]
): boolean {
  return (
    nodesA.length === nodesB.length &&
    nodesA.every((node, i) => areEquivalent(node, nodesB[i]))
  );
}

function isTemplateExpression(node: t.Node): node is TemplateExpression {
  return (
    t.isIdentifier(node) ||
    t.isCallExpression(node) ||
    t.isMemberExpression(node)
  );
}

type TemplateExpression = t.Identifier | t.CallExpression | t.MemberExpression;

function isInBranchedLogic(path: NodePath<t.ReturnStatement>) {
  return path.getAncestry().some((path) => t.isIfStatement(path));
}

function isInAlternate(path: NodePath<t.IfStatement>): boolean {
  const { parentPath } = path;

  return t.isBlockStatement(parentPath)
    ? t.isIfStatement(parentPath.parent) &&
        parentPath.parent.alternate === path.parent
    : t.isIfStatement(parentPath.node) &&
        parentPath.node.alternate === path.node;
}

function areOpposite(testA: t.Expression, testB: t.Expression): boolean {
  if (!t.isBinaryExpression(testA)) return false;
  if (!t.isBinaryExpression(testB)) return false;

  const EQUALS_OPERATORS = ["==", "==="];

  if (
    EQUALS_OPERATORS.includes(testA.operator) &&
    EQUALS_OPERATORS.includes(testB.operator)
  ) {
    return (
      areEquivalent(testA.left, testB.left) &&
      !areEquivalent(testA.right, testB.right)
    );
  }

  if (areOppositeOperators(testA.operator, testB.operator)) {
    return (
      areEquivalent(testA.left, testB.left) &&
      areEquivalent(testA.right, testB.right)
    );
  }

  return false;
}

const OPPOSITE_OPERATORS: t.BinaryExpression["operator"][][] = [
  ["===", "!=="],
  ["==", "!="],
  [">", "<="],
  [">", "<"],
  [">=", "<"]
];

function areOppositeOperators(
  operatorA: t.BinaryExpression["operator"],
  operatorB: t.BinaryExpression["operator"]
): boolean {
  return OPPOSITE_OPERATORS.some(
    ([left, right]) =>
      (operatorA === left && operatorB === right) ||
      (operatorA === right && operatorB === left)
  );
}

function getOppositeOperator(
  operator: t.BinaryExpression["operator"]
): t.BinaryExpression["operator"] {
  let result: t.BinaryExpression["operator"] | undefined;

  OPPOSITE_OPERATORS.forEach(([left, right]) => {
    if (operator === left) result = right;
    if (operator === right) result = left;
  });

  return result || operator;
}

function canBeShorthand(path: NodePath): path is NodePath<t.ObjectProperty> {
  return (
    t.isObjectProperty(path.node) &&
    !path.node.computed &&
    t.isIdentifier(path.node.key)
  );
}
