var esprima = require('esprima-fb');
var utils = require('../src/utils');

var Syntax = esprima.Syntax;

function _isFunctionNode(node) {
  return node.type === Syntax.FunctionDeclaration
         || node.type === Syntax.FunctionExpression
         || node.type === Syntax.ArrowFunctionExpression;
}

function visitClassProperty(traverse, node, path, state) {
  utils.catchup(node.range[0], state);
  utils.catchupWhiteOut(node.range[1], state);
  return false;
}
visitClassProperty.test = function(node, path, state) {
  return node.type === Syntax.ClassProperty;
};

function visitTypeAlias(traverse, node, path, state) {
  utils.catchupWhiteOut(node.range[1], state);
  return false;
}
visitTypeAlias.test = function(node, path, state) {
  return node.type === Syntax.TypeAlias;
};

function visitTypeCast(traverse, node, path, state) {
  path.unshift(node);
  traverse(node.expression, path, state);
  path.shift();

  utils.catchup(node.typeAnnotation.range[0], state);
  utils.catchupWhiteOut(node.typeAnnotation.range[1], state);
  return false;
}
visitTypeCast.test = function(node, path, state) {
  return node.type === Syntax.TypeCastExpression;
};

function visitInterfaceDeclaration(traverse, node, path, state) {
  utils.catchupWhiteOut(node.range[1], state);
  return false;
}
visitInterfaceDeclaration.test = function(node, path, state) {
  return node.type === Syntax.InterfaceDeclaration;
};

function visitDeclare(traverse, node, path, state) {
  utils.catchupWhiteOut(node.range[1], state);
  return false;
}
visitDeclare.test = function(node, path, state) {
  switch (node.type) {
    case Syntax.DeclareVariable:
    case Syntax.DeclareFunction:
    case Syntax.DeclareClass:
    case Syntax.DeclareModule:
      return true;
  }
  return false;
};

function visitFunctionParametricAnnotation(traverse, node, path, state) {
  utils.catchup(node.range[0], state);
  utils.catchupWhiteOut(node.range[1], state);
  return false;
}
visitFunctionParametricAnnotation.test = function(node, path, state) {
  return node.type === Syntax.TypeParameterDeclaration
         && path[0]
         && _isFunctionNode(path[0])
         && node === path[0].typeParameters;
};

function visitFunctionReturnAnnotation(traverse, node, path, state) {
  utils.catchup(node.range[0], state);
  utils.catchupWhiteOut(node.range[1], state);
  return false;
}
visitFunctionReturnAnnotation.test = function(node, path, state) {
  return path[0] && _isFunctionNode(path[0]) && node === path[0].returnType;
};

function visitOptionalFunctionParameterAnnotation(traverse, node, path, state) {
  utils.catchup(node.range[0] + node.name.length, state);
  utils.catchupWhiteOut(node.range[1], state);
  return false;
}
visitOptionalFunctionParameterAnnotation.test = function(node, path, state) {
  return node.type === Syntax.Identifier
         && node.optional
         && path[0]
         && _isFunctionNode(path[0]);
};

function visitTypeAnnotatedIdentifier(traverse, node, path, state) {
  utils.catchup(node.typeAnnotation.range[0], state);
  utils.catchupWhiteOut(node.typeAnnotation.range[1], state);
  return false;
}
visitTypeAnnotatedIdentifier.test = function(node, path, state) {
  return node.type === Syntax.Identifier && node.typeAnnotation;
};

function visitTypeAnnotatedObjectOrArrayPattern(traverse, node, path, state) {
  utils.catchup(node.typeAnnotation.range[0], state);
  utils.catchupWhiteOut(node.typeAnnotation.range[1], state);
  return false;
}
visitTypeAnnotatedObjectOrArrayPattern.test = function(node, path, state) {
  var rightType = node.type === Syntax.ObjectPattern
                || node.type === Syntax.ArrayPattern;
  return rightType && node.typeAnnotation;
};

/**
 * Methods cause trouble, since esprima parses them as a key/value pair, where
 * the location of the value starts at the method body. For example
 * { bar(x:number,...y:Array<number>):number {} }
 * is parsed as
 * { bar: function(x: number, ...y:Array<number>): number {} }
 * except that the location of the FunctionExpression value is 40-something,
 * which is the location of the function body. This means that by the time we
 * visit the params, rest param, and return type organically, we've already
 * catchup()'d passed them.
 */
function visitMethod(traverse, node, path, state) {
  path.unshift(node);
  traverse(node.key, path, state);

  path.unshift(node.value);
  traverse(node.value.params, path, state);
  node.value.rest && traverse(node.value.rest, path, state);
  node.value.returnType && traverse(node.value.returnType, path, state);
  traverse(node.value.body, path, state);

  path.shift();

  path.shift();
  return false;
}

visitMethod.test = function(node, path, state) {
  return (node.type === "Property" && (node.method || node.kind === "set" || node.kind === "get"))
      || (node.type === "MethodDefinition");
};

function visitImportType(traverse, node, path, state) {
  utils.catchupWhiteOut(node.range[1], state);
  return false;
}
visitImportType.test = function(node, path, state) {
  return node.type === 'ImportDeclaration'
         && node.isType;
};

exports.visitorList = [
  visitClassProperty,
  visitDeclare,
  visitImportType,
  visitInterfaceDeclaration,
  visitFunctionParametricAnnotation,
  visitFunctionReturnAnnotation,
  visitMethod,
  visitOptionalFunctionParameterAnnotation,
  visitTypeAlias,
  visitTypeCast,
  visitTypeAnnotatedIdentifier,
  visitTypeAnnotatedObjectOrArrayPattern
];
