'use strict';

// Deprecated
// Whether or not the node represents a generic define() call
// Note: this should not be used as it will have false positives.
// It is mostly used to guide sniffs for other methods and will be made private eventually.
module.exports.isDefine = function(node) {
  if (!node) return false;

  const c = node.callee;

  return c &&
         node.type === 'CallExpression' &&
         c.type === 'Identifier' &&
         c.name === 'define';
};

// Whether or not the node represents any of the AMD define() forms
module.exports.isDefineAMD = function(node) {
  if (!node) return false;

  const e = module.exports;

  return e.isNamedForm(node) || e.isDependencyForm(node) ||
         e.isFactoryForm(node) || e.isNoDependencyForm(node) ||
         e.isREMForm(node);
};

// Whether or not the node represents a require function call
module.exports.isRequire = function(node) {
  return this.isPlainRequire(node) || this.isMainScopedRequire(node);
};

// Whether or not the node represents a plain require function call [require(...)]
module.exports.isPlainRequire = function(node) {
  if (!node) return false;

  const c = node.callee;

  return c &&
         node.type === 'CallExpression' &&
         c.type === 'Identifier' &&
         c.name === 'require';
};

// Whether or not the node represents main-scoped require function call [require.main.require(...)]
module.exports.isMainScopedRequire = function(node) {
  if (!node) return false;

  const c = node.callee;

  return c &&
         node.type === 'CallExpression' &&
         c.type === 'MemberExpression' &&
         c.object.type === 'MemberExpression' &&
         c.object.object.type === 'Identifier' &&
         c.object.object.name === 'require' &&
         c.object.property.type === 'Identifier' &&
         c.object.property.name === 'main' &&
         c.property.type === 'Identifier' &&
         c.property.name === 'require';
};

// Whether or not the node represents a require at the top of the module
// Instead of trying to find the require then backtrack to the top,
// just take the root and check its immediate child
module.exports.isTopLevelRequire = function(node) {
  if (node.type !== 'Program' || !node.body ||
      node.body.length === 0 || !node.body[0].expression) {
    return false;
  }

  return this.isRequire(node.body[0].expression);
};

// Whether or not the node represents an AMD-style driver script's require
// Example: require(deps, function)
module.exports.isAMDDriverScriptRequire = function(node) {
  return this.isRequire(node) &&
         node.arguments &&
         node.arguments[0] && node.arguments[0].type &&
         node.arguments[0].type === 'ArrayExpression';
};

function isExportsIdentifier(obj) {
  return obj.type && obj.type === 'Identifier' && obj.name === 'exports';
}

function isModuleIdentifier(obj) {
  return obj.type && obj.type === 'Identifier' && obj.name === 'module';
}

function isFunctionLike(node) {
  if (!node) return false;

  return node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression';
}

// module.exports.foo
function isModuleExportsAttach(node) {
  if (!node.object || !node.object.object || !node.object.property) return false;

  return node.type === 'MemberExpression' &&
         isModuleIdentifier(node.object.object) &&
         isExportsIdentifier(node.object.property);
}

// module.exports
function isModuleExportsAssign(node) {
  if (!node.object || !node.property) return false;

  return node.type === 'MemberExpression' &&
         isModuleIdentifier(node.object) &&
         isExportsIdentifier(node.property);
}

// exports
function isExportsAssign(node) {
  return isExportsIdentifier(node);
}

// exports.foo
function isExportsAttach(node) {
  return node.type === 'MemberExpression' && isExportsIdentifier(node.object);
}

// Whether or not the node represents the use of
// assigning (and possibly attaching) something to module.exports or exports
module.exports.isExports = function(node) {
  if (node.type !== 'AssignmentExpression') return;

  // Only the left side matters
  const leftNode = node.left;

  return isModuleExportsAttach(leftNode) || isModuleExportsAssign(leftNode) ||
         isExportsAttach(leftNode) || isExportsAssign(leftNode);
};

// define('name', [deps], func)
module.exports.isNamedForm = function(node) {
  if (!this.isDefine(node)) return false;

  const args = node.arguments;

  return args && args.length === 3 &&
         (args[0].type === 'Literal' || args[0].type === 'StringLiteral') &&
         args[1].type === 'ArrayExpression' &&
         isFunctionLike(args[2]);
};

// define([deps], func)
module.exports.isDependencyForm = function(node) {
  if (!this.isDefine(node)) return false;

  const args = node.arguments;

  return args && args.length === 2 &&
         args[0].type === 'ArrayExpression' &&
         isFunctionLike(args[1]);
};

// define(func(require))
module.exports.isFactoryForm = function(node) {
  if (!this.isDefine(node)) return false;

  const args = node.arguments;
  const firstParamNode = args.length > 0 && args[0].params ? args[0].params[0] : null;

  // Node should have a function whose first param is 'require'
  return args && args.length === 1 &&
         isFunctionLike(args[0]) &&
         firstParamNode && firstParamNode.type === 'Identifier' &&
         firstParamNode.name === 'require';
};

// define({})
module.exports.isNoDependencyForm = function(node) {
  if (!this.isDefine(node)) return false;

  const args = node.arguments;

  return args && args.length === 1 && args[0].type === 'ObjectExpression';
};

// define(function(require, exports, module)
module.exports.isREMForm = function(node) {
  if (!this.isDefine(node)) return false;

  const args = node.arguments;
  const params = args.length > 0 ? args[0].params : null;

  if (!args || args.length === 0 || !isFunctionLike(args[0]) || params.length !== 3) {
    return false;
  }

  const [first, second, third] = params;

  return first.type === 'Identifier' && first.name === 'require' &&
         second.type === 'Identifier' && second.name === 'exports' &&
         third.type === 'Identifier' && third.name === 'module';
};

module.exports.isES6Import = function(node) {
  switch (node.type) {
    case 'Import':
    case 'ImportDeclaration':
    case 'ImportDefaultSpecifier':
    case 'ImportNamespaceSpecifier': {
      return true;
    }

    default: {
      return false;
    }
  }
};

module.exports.isES6Export = function(node) {
  switch (node.type) {
    case 'ExportDeclaration':
    case 'ExportNamedDeclaration':
    case 'ExportSpecifier':
    case 'ExportDefaultDeclaration':
    case 'ExportAllDeclaration': {
      return true;
    }

    default: {
      return false;
    }
  }
};

module.exports.isDynamicImport = function(node) {
  return node.callee && node.callee.type === 'Import' && node.arguments.length > 0;
};
