/*
   * (non-Javadoc)
   * @see
   * com.aptana.editor.js.parsing.ast.JSTreeWalker#visit(com.aptana.editor.js.parsing.ast.JSArithmeticOperatorNode)
   */
  @Override
  public void visit(JSBinaryArithmeticOperatorNode node) {
    String type = JSTypeConstants.NUMBER_TYPE;

    if (node.getNodeType() == IJSNodeTypes.ADD) {
      IParseNode lhs = node.getLeftHandSide();
      IParseNode rhs = node.getRightHandSide();

      // NOTE: Iterate down the tree until we find the first non-addition node or the first string
      while (lhs.getNodeType() == IJSNodeTypes.ADD) {
        rhs = lhs.getLastChild();
        lhs = lhs.getFirstChild();

        if (rhs instanceof JSStringNode) {
          break;
        }
      }

      if (lhs instanceof JSStringNode || rhs instanceof JSStringNode) {
        type = JSTypeConstants.STRING_TYPE;
      } else {
        List<String> lhsTypes = this.getTypes(lhs);
        List<String> rhsTypes = this.getTypes(rhs);

        if (lhsTypes.contains(JSTypeConstants.STRING_TYPE)
            || rhsTypes.contains(JSTypeConstants.STRING_TYPE)) {
          type = JSTypeConstants.STRING_TYPE;
        }
      }
    }

    this.addType(type);
  }
  /*
   * (non-Javadoc)
   * @see com.aptana.editor.js.parsing.ast.JSTreeWalker#visit(com.aptana.editor.js.parsing.ast.JSIdentifierNode)
   */
  @Override
  public void visit(JSIdentifierNode node) {
    String name = node.getText();
    Collection<PropertyElement> properties = null;

    if (this._scope != null && this._scope.hasSymbol(name)) {
      IParseNode parent = node.getParent();

      if (parent != null && parent.getNodeType() == IJSNodeTypes.PARAMETERS) {
        // special handling of parameters to potentially get the type
        // from documentation and to prevent an infinite loop since
        // parameters point to themselves in the symbol table
        this.addParameterTypes(node);
      } else {
        // Check the local scope for type first
        JSSymbolTypeInferrer symbolInferrer =
            new JSSymbolTypeInferrer(this._scope, this._index, this._location);
        PropertyElement property = symbolInferrer.getSymbolPropertyElement(name);
        if (property != null) {
          // We found a match in the local scope
          properties = CollectionsUtil.newList(property);
        } else {
          // No match in the local scope, query the globals in index
          properties = this._queryHelper.getGlobals(this._index, getProject(), getFileName(), name);
        }
      }
    } else {
      // Scope says it doesn't has a symbol with that name, so query the globals in index
      properties = this._queryHelper.getGlobals(this._index, getProject(), getFileName(), name);
    }

    // Hopefully we found at least one match...
    if (properties != null) {
      for (PropertyElement property : properties) {
        if (property instanceof FunctionElement) {
          FunctionElement function = (FunctionElement) property;
          for (String type : function.getSignatureTypes()) {
            this.addType(type);
          }
        } else {
          for (String type : property.getTypeNames()) {
            this.addType(type);
          }
        }
      }
    }
  }
  /**
   * addParameterTypes
   *
   * @param identifierNode
   */
  private void addParameterTypes(JSIdentifierNode identifierNode) {
    IParseNode parent = identifierNode.getParent();
    IParseNode grandparent = (parent != null) ? parent.getParent() : null;
    boolean foundType = false;

    if (grandparent != null && grandparent.getNodeType() == IJSNodeTypes.FUNCTION) {
      DocumentationBlock docs = ((JSNode) grandparent).getDocumentation();

      if (docs != null) {
        String name = identifierNode.getText();
        int index = identifierNode.getIndex();
        List<Tag> params = docs.getTags(TagType.PARAM);

        if (params != null && index < params.size()) {
          ParamTag param = (ParamTag) params.get(index);

          if (name.equals(param.getName())) {
            for (Type parameterType : param.getTypes()) {
              String type = parameterType.getName();

              // Fix up type names as might be necessary
              type = JSTypeMapper.getInstance().getMappedType(type);

              this.addType(type);
              foundType = true;
            }
          }
        }
      }
    }

    // Use "Object" as parameter type if we didn't find types by other
    // means
    if (!foundType) {
      this.addType(JSTypeConstants.DEFAULT_PARAMETER_TYPE);
    }
  }
  /*
   * (non-Javadoc)
   * @see com.aptana.editor.js.parsing.ast.JSTreeWalker#visit(com.aptana.editor.js.parsing.ast.JSInvokeNode)
   */
  @Override
  public void visit(JSInvokeNode node) {
    IParseNode child = node.getExpression();

    if (child instanceof JSNode) {
      // TODO hang the "require" string as a constant somewhere!
      if (child instanceof JSIdentifierNode
          && "require".equals(child.getNameNode().getName())) // $NON-NLS-1$
      {
        // it's a requires!
        JSArgumentsNode args = (JSArgumentsNode) node.getArguments();
        IParseNode[] children = args.getChildren();
        for (IParseNode arg : children) {
          if (arg instanceof JSStringNode) {
            JSStringNode string = (JSStringNode) arg;
            String text = string.getText();
            // strip quotes TODO Use util method to strip quotes!
            if (text.startsWith("'") || text.startsWith("\"")) // $NON-NLS-1$ //$NON-NLS-2$
            {
              text = text.substring(1, text.length() - 1);
            }

            // Handle resolving absolute versus relative module ids!
            URI resolved = _location.resolve(text);
            URI relative = _index.getRelativeDocumentPath(resolved);
            this.addType(relative.getPath() + ".exports"); // $NON-NLS-1$
          }
        }
      }

      List<String> types = this.getTypes(child);

      // NOTE: This is a special case for functions used as a RHS of assignments or as part of a
      // property chain.
      // If the invocation returns undefined, we change that to Object.
      // TODO: As a refinement, we want to check that the function being called is not defined in
      // the current
      // scope
      if (types.isEmpty()) {
        IParseNode parent = node.getParent();

        if (parent != null) {
          switch (parent.getNodeType()) {
            case IJSNodeTypes.ASSIGN:
              if (node.getIndex() == 1) {
                this.addType(JSTypeConstants.OBJECT_TYPE);
              }
              break;

            case IJSNodeTypes.GET_PROPERTY:
              this.addType(JSTypeConstants.OBJECT_TYPE);
              break;

            default:
              break;
          }
        }
      }

      for (String typeName : types) {
        if (JSTypeUtil.isFunctionPrefix(typeName)) {
          List<String> returnTypes = JSTypeUtil.getFunctionSignatureReturnTypeNames(typeName);
          for (String returnTypeName : returnTypes) {
            this.addType(returnTypeName);
          }
        }
      }
    }
  }