/**
 * Copyright (c) 2005-2013 by Appcelerator, Inc. All Rights Reserved.
 * Licensed under the terms of the Eclipse Public License (EPL).
 * Please see the license.txt included with this distribution for details.
 * Any modifications to this file must keep this entire header intact.
 */
/*
 * Created on Jan 19, 2005
 *
 * @author Fabio Zadrozny
 */
package org.python.pydev.editor.codecompletion.revisited.visitors;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Stack;

import org.python.pydev.core.ILocalScope;
import org.python.pydev.core.IModule;
import org.python.pydev.core.IPythonNature;
import org.python.pydev.core.docutils.PySelection;
import org.python.pydev.parser.jython.SimpleNode;
import org.python.pydev.parser.jython.ast.Assign;
import org.python.pydev.parser.jython.ast.Attribute;
import org.python.pydev.parser.jython.ast.Call;
import org.python.pydev.parser.jython.ast.ClassDef;
import org.python.pydev.parser.jython.ast.Comprehension;
import org.python.pydev.parser.jython.ast.FunctionDef;
import org.python.pydev.parser.jython.ast.Global;
import org.python.pydev.parser.jython.ast.ImportFrom;
import org.python.pydev.parser.jython.ast.ListComp;
import org.python.pydev.parser.jython.ast.Module;
import org.python.pydev.parser.jython.ast.Name;
import org.python.pydev.parser.jython.ast.NameTok;
import org.python.pydev.parser.jython.ast.NameTokType;
import org.python.pydev.parser.jython.ast.Subscript;
import org.python.pydev.parser.jython.ast.Tuple;
import org.python.pydev.parser.jython.ast.aliasType;
import org.python.pydev.parser.jython.ast.comprehensionType;
import org.python.pydev.parser.jython.ast.exprType;
import org.python.pydev.parser.visitors.NodeUtils;
import org.python.pydev.shared_core.structure.FastStack;

/**
 * @author Fabio Zadrozny
 */
public class FindDefinitionModelVisitor extends AbstractVisitor {

    /**
     * This is the token to find.
     */
    private String tokenToFind;

    /**
     * List of definitions.
     */
    public List<Definition> definitions = new ArrayList<Definition>();

    /**
     * Stack of classes / methods to get to a definition.
     */
    private FastStack<SimpleNode> defsStack = new FastStack<SimpleNode>(20);

    /**
     * This is a stack that will keep the globals for each stack
     */
    private FastStack<Set<String>> globalDeclarationsStack = new FastStack<Set<String>>(20);

    /**
     * This is the module we are visiting: just a weak reference so that we don't create a cycle (let's
     * leave things easy for the garbage collector).
     */
    private WeakReference<IModule> module;

    /**
     * It is only available if the cursor position is upon a NameTok in an import (it represents the complete
     * path for finding the module from the current module -- it can be a regular or relative import).
     */
    public String moduleImported;

    /**
     * Starts at 1
     */
    private int line;

    /**
     * Starts at 1
     */
    private int col;

    private boolean foundAsDefinition = false;

    private Definition definitionFound;

    /**
     * Call is stored for the context for a keyword parameter
     */
    private Stack<Call> call = new Stack<Call>();

    /**
     * Constructor
     * @param line: starts at 1
     * @param col: starts at 1
     */
    public FindDefinitionModelVisitor(String token, int line, int col, IModule module, IPythonNature nature) {
        super(nature);
        this.tokenToFind = token;
        this.module = new WeakReference<IModule>(module);
        this.line = line;
        this.col = col;
        this.moduleName = module.getName();
        //we may have a global declared in the global scope
        globalDeclarationsStack.push(new HashSet<String>());
    }

    @Override
    public Object visitImportFrom(ImportFrom node) throws Exception {
        String modRep = NodeUtils.getRepresentationString(node.module);
        if (NodeUtils.isWithin(line, col, node.module)) {
            //it is a token in the definition of a module
            int startingCol = node.module.beginColumn;
            int endingCol = startingCol;
            while (endingCol < this.col) {
                endingCol++;
            }
            int lastChar = endingCol - startingCol;
            moduleImported = modRep.substring(0, lastChar);
            int i = lastChar;
            while (i < modRep.length()) {
                if (Character.isJavaIdentifierPart(modRep.charAt(i))) {
                    i++;
                } else {
                    break;
                }
            }
            moduleImported += modRep.substring(lastChar, i);
        } else {
            //it was not the module, so, we have to check for each name alias imported
            for (aliasType alias : node.names) {
                //we do not check the 'as' because if it is some 'as', it will be gotten as a global in the module
                if (NodeUtils.isWithin(line, col, alias.name)) {
                    moduleImported = modRep + "." + NodeUtils.getRepresentationString(alias.name);
                }
            }
        }
        return super.visitImportFrom(node);
    }

    /**
     * @see org.python.pydev.parser.jython.ast.VisitorBase#unhandled_node(org.python.pydev.parser.jython.SimpleNode)
     */
    @Override
    protected Object unhandled_node(SimpleNode node) throws Exception {
        return null;
    }

    /**
     * @see org.python.pydev.parser.jython.ast.VisitorBase#traverse(org.python.pydev.parser.jython.SimpleNode)
     */
    @Override
    public void traverse(SimpleNode node) throws Exception {
        node.traverse(this);
    }

    /**
     * @see org.python.pydev.parser.jython.ast.VisitorBase#visitClassDef(org.python.pydev.parser.jython.ast.ClassDef)
     */
    @Override
    public Object visitClassDef(ClassDef node) throws Exception {
        globalDeclarationsStack.push(new HashSet<String>());
        defsStack.push(node);

        node.traverse(this);

        defsStack.pop();
        globalDeclarationsStack.pop();

        checkDeclaration(node, (NameTok) node.name);
        return null;
    }

    /**
     * @see org.python.pydev.parser.jython.ast.VisitorBase#visitFunctionDef(org.python.pydev.parser.jython.ast.FunctionDef)
     */
    @Override
    public Object visitFunctionDef(FunctionDef node) throws Exception {
        globalDeclarationsStack.push(new HashSet<String>());
        defsStack.push(node);

        if (node.args != null) {
            if (node.args.args != null) {
                for (exprType arg : node.args.args) {
                    if (arg instanceof Name) {
                        checkParam((Name) arg);
                    }
                }
            }
            if (node.args.kwonlyargs != null) {
                for (exprType arg : node.args.kwonlyargs) {
                    if (arg instanceof Name) {
                        checkParam((Name) arg);
                    }
                }
            }
        }
        node.traverse(this);

        defsStack.pop();
        globalDeclarationsStack.pop();

        checkDeclaration(node, (NameTok) node.name);
        return null;
    }

    /**
     * @param node the declaration node we're interested in (class or function)
     * @param name the token that represents the name of that declaration
     */
    private void checkParam(Name name) {
        String rep = NodeUtils.getRepresentationString(name);
        if (rep.equals(tokenToFind) && line == name.beginLine && col >= name.beginColumn
                && col <= name.beginColumn + rep.length()) {
            foundAsDefinition = true;
            // if it is found as a definition it is an 'exact' match, so, erase all the others.
            ILocalScope scope = new LocalScope(this.nature, this.defsStack);
            for (Iterator<Definition> it = definitions.iterator(); it.hasNext();) {
                Definition d = it.next();
                if (!d.scope.equals(scope)) {
                    it.remove();
                }
            }

            definitionFound = new Definition(line, name.beginColumn, rep, name, scope, module.get());
            definitions.add(definitionFound);
        }
    }

    @Override
    public Object visitCall(Call node) throws Exception {
        this.call.push(node);
        Object r = super.visitCall(node);
        this.call.pop();
        return r;
    }

    @Override
    public Object visitNameTok(NameTok node) throws Exception {
        if (node.ctx == NameTok.KeywordName) {
            if (this.line == node.beginLine && this.call.size() > 0) {
                String rep = NodeUtils.getRepresentationString(node);

                if (PySelection.isInside(col, node.beginColumn, rep.length())) {
                    foundAsDefinition = true;
                    // if it is found as a definition it is an 'exact' match, so, erase all the others.
                    ILocalScope scope = new LocalScope(nature, this.defsStack);
                    for (Iterator<Definition> it = definitions.iterator(); it.hasNext();) {
                        Definition d = it.next();
                        if (!d.scope.equals(scope)) {
                            it.remove();
                        }
                    }

                    definitions.clear();

                    definitionFound = new KeywordParameterDefinition(line, node.beginColumn, rep, node, scope,
                            module.get(), this.call.peek());
                    definitions.add(definitionFound);
                    throw STOP_VISITING_EXCEPTION;
                }
            }
        }
        return null;
    }

    private static final StopVisitingException STOP_VISITING_EXCEPTION = new StopVisitingException();

    /**
     * @param node the declaration node we're interested in (class or function)
     * @param name the token that represents the name of that declaration
     */
    private void checkDeclaration(SimpleNode node, NameTok name) {
        String rep = NodeUtils.getRepresentationString(node);
        if (rep.equals(tokenToFind)
                && ((line == -1 && col == -1)
                        || (line == name.beginLine && col >= name.beginColumn && col <= name.beginColumn
                                + rep.length()))) {
            foundAsDefinition = true;
            // if it is found as a definition it is an 'exact' match, so, erase all the others.
            ILocalScope scope = new LocalScope(nature, this.defsStack);
            for (Iterator<Definition> it = definitions.iterator(); it.hasNext();) {
                Definition d = it.next();
                if (!d.scope.equals(scope)) {
                    it.remove();
                }
            }

            definitionFound = new Definition(name.beginLine, name.beginColumn, rep, node, scope, module.get());
            definitions.add(definitionFound);
        }
    }

    @Override
    public Object visitGlobal(Global node) throws Exception {
        for (NameTokType n : node.names) {
            globalDeclarationsStack.peek().add(NodeUtils.getFullRepresentationString(n));
        }
        return null;
    }

    @Override
    public Object visitModule(Module node) throws Exception {
        this.defsStack.push(node);
        return super.visitModule(node);
    }

    /**
     * @see org.python.pydev.parser.jython.ast.VisitorBase#visitAssign(org.python.pydev.parser.jython.ast.Assign)
     */
    @Override
    public Object visitAssign(Assign node) throws Exception {
        return this.visitAssign(node, -1);
    }

    public Object visitAssign(Assign node, int unpackPos) throws Exception {
        ILocalScope scope = new LocalScope(nature, this.defsStack);
        scope.setFoundAtASTNode(node);
        if (foundAsDefinition && !scope.equals(definitionFound.scope)) { //if it is found as a definition it is an 'exact' match, so, we do not keep checking it
            return null;
        }

        for (int i = 0; i < node.targets.length; i++) {
            exprType target = node.targets[i];
            if (target instanceof Subscript) {
                continue; //assigning to an element and not the variable itself. E.g.: mydict[1] = 10 (instead of mydict = 10)
            }

            if (target instanceof Tuple) {
                //if assign is xxx, yyy = 1, 2
                //let's separate those as different assigns and analyze one by one
                Tuple targetTuple = (Tuple) target;
                if (node.value instanceof Tuple) {
                    Tuple valueTuple = (Tuple) node.value;
                    checkTupleAssignTarget(targetTuple, valueTuple.elts, false);

                } else if (node.value instanceof org.python.pydev.parser.jython.ast.List) {
                    org.python.pydev.parser.jython.ast.List valueList = (org.python.pydev.parser.jython.ast.List) node.value;
                    checkTupleAssignTarget(targetTuple, valueList.elts, false);

                } else {
                    checkTupleAssignTarget(targetTuple, new exprType[] { node.value }, true);
                }

            } else {
                String rep = NodeUtils.getFullRepresentationString(target);

                if (tokenToFind.equals(rep)) { //note, order of equals is important (because one side may be null).
                    exprType nodeValue = node.value;
                    String value = NodeUtils.getFullRepresentationString(nodeValue);
                    if (value == null) {
                        value = "";
                    }

                    //get the line and column correspondent to the target
                    int line = NodeUtils.getLineDefinition(target);
                    int col = NodeUtils.getColDefinition(target);

                    AssignDefinition definition = new AssignDefinition(value, rep, i, node, line, col, scope,
                            module.get(), nodeValue, unpackPos);

                    //mark it as global (if it was found as global in some of the previous contexts).
                    for (Set<String> globals : globalDeclarationsStack) {
                        if (globals.contains(rep)) {
                            definition.foundAsGlobal = true;
                        }
                    }

                    definitions.add(definition);
                }
            }
        }

        return super.visitAssign(node);
    }

    @Override
    public Object visitListComp(ListComp node) throws Exception {
        exprType elt = node.elt;
        elt = fixMissingAttribute(elt);
        if (this.line == elt.beginLine) {
            ILocalScope scope = new LocalScope(nature, this.defsStack);
            scope.setFoundAtASTNode(node);
            if (foundAsDefinition && !scope.equals(definitionFound.scope)) { //if it is found as a definition it is an 'exact' match, so, we do not keep checking it
                return super.visitListComp(node);
            }

            if (this.tokenToFind.equals(NodeUtils.getRepresentationString(elt))) {
                // Something as [a for a in [F(), C()]]
                if (node.generators != null && node.generators.length == 1) {
                    comprehensionType comprehensionType = node.generators[0];
                    if (comprehensionType instanceof Comprehension) {
                        Comprehension comprehension = (Comprehension) comprehensionType;
                        if (comprehension.iter != null) {
                            if (this.tokenToFind.equals(NodeUtils.getRepresentationString(comprehension.target))) {
                                exprType[] elts = NodeUtils.getEltsFromCompoundObject(comprehension.iter);
                                String rep = "";
                                if (elts != null && elts.length > 0) {
                                    rep = NodeUtils.getRepresentationString(elts[0]);
                                }
                                ListCompDefinition definition = new ListCompDefinition(rep, this.tokenToFind, node,
                                        line, col, scope, module.get());

                                definitions.add(definition);
                            }
                        }
                    }
                }
            } else if (elt instanceof Tuple || elt instanceof List) {
                // something as [(a, b) for (a, b) in [(F(), G()), ...]]

                exprType[] eltsFromCompoundObject = NodeUtils.getEltsFromCompoundObject(elt);
                if (eltsFromCompoundObject != null) {
                    int length = eltsFromCompoundObject.length;
                    for (int i = 0; i < length; i++) {
                        exprType eltFromCompound = fixMissingAttribute(eltsFromCompoundObject[i]);
                        if (this.tokenToFind.equals(NodeUtils.getRepresentationString(eltFromCompound))) {

                            if (node.generators != null && node.generators.length == 1) {
                                comprehensionType comprehensionType = node.generators[0];

                                if (comprehensionType instanceof Comprehension) {
                                    Comprehension comprehension = (Comprehension) comprehensionType;

                                    if (comprehension.iter != null) {
                                        exprType target = comprehension.target;

                                        if (target != null) {
                                            exprType[] targetElts = NodeUtils.getEltsFromCompoundObject(target);

                                            if (targetElts != null) {

                                                for (int j = 0; j < targetElts.length; j++) {
                                                    exprType targetElt = targetElts[j];

                                                    if (this.tokenToFind
                                                            .equals(NodeUtils.getRepresentationString(targetElt))) {
                                                        exprType[] elts = NodeUtils
                                                                .getEltsFromCompoundObject(comprehension.iter);
                                                        String rep = "";
                                                        if (elts != null && elts.length > j) {
                                                            rep = NodeUtils.getRepresentationString(elts[j]);
                                                        }
                                                        ListCompDefinition definition = new ListCompDefinition(rep,
                                                                this.tokenToFind, node,
                                                                line, col, scope, module.get());

                                                        definitions.add(definition);
                                                    }

                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
        return super.visitListComp(node);
    }

    private exprType fixMissingAttribute(exprType elt) {
        if (elt instanceof Attribute) {
            // I.e.: in this case we have: Attribute[value=Name[id=i, ctx=Load, reserved=false], attr=NameTok[id=!<MissingName>!, ctx=Attrib], ctx=Load]
            Attribute attribute = (Attribute) elt;
            String rep = NodeUtils.getRepresentationString(attribute.attr);
            if (rep == null || rep.startsWith("!")) {
                elt = attribute.value;
            }
        }
        return elt;
    }

    /**
     * Analyze an assign that has the target as a tuple and the multiple elements in the other side.
     *
     * E.g.: www, yyy = 1, 2
     *
     * @param targetTuple the target in the assign
     * @param valueElts the values that are being assigned
     * @param unpackElements
     */
    private void checkTupleAssignTarget(Tuple targetTuple, exprType[] valueElts, boolean unpackElements)
            throws Exception {
        if (valueElts == null || valueElts.length == 0) {
            return; //nothing to do if we don't have any values
        }

        for (int i = 0; i < targetTuple.elts.length; i++) {
            int j = i;
            //that's if the number of values is less than the number of assigns (actually, that'd
            //probably be an error, but let's go on gracefully, as the user can be in an invalid moment
            //in his code)
            if (j >= valueElts.length) {
                j = valueElts.length - 1;
            }
            Assign assign = new Assign(new exprType[] { targetTuple.elts[i] }, valueElts[j], null);
            assign.beginLine = targetTuple.beginLine;
            assign.beginColumn = targetTuple.beginColumn;
            if (unpackElements) {
                visitAssign(assign, i);

            } else {
                visitAssign(assign);

            }
        }
    }
}
