/**
 * 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 May 16, 2006
 */
package com.python.pydev.analysis.scopeanalysis;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.python.pydev.core.FullRepIterable;
import org.python.pydev.core.IModule;
import org.python.pydev.core.IPythonNature;
import org.python.pydev.core.IToken;
import org.python.pydev.core.docutils.PySelection;
import org.python.pydev.editor.codecompletion.revisited.modules.SourceModule;
import org.python.pydev.editor.codecompletion.revisited.modules.SourceToken;
import org.python.pydev.editor.codecompletion.revisited.visitors.AbstractVisitor;
import org.python.pydev.parser.jython.SimpleNode;
import org.python.pydev.parser.jython.ast.Import;
import org.python.pydev.parser.jython.ast.ImportFrom;
import org.python.pydev.parser.jython.ast.NameTok;
import org.python.pydev.parser.jython.ast.aliasType;
import org.python.pydev.parser.visitors.scope.ASTEntry;
import org.python.pydev.shared_core.structure.Tuple;
import org.python.pydev.shared_core.structure.Tuple3;
import org.python.pydev.shared_core.structure.Tuple4;

import com.python.pydev.analysis.visitors.Found;
import com.python.pydev.analysis.visitors.ImportChecker.ImportInfo;

/**
 * This class is used to discover the occurrences of some token having its scope as something important.
 */
public class ScopeAnalyzerVisitor extends ScopeAnalyzerVisitorWithoutImports {

    /**
     * Constructor when we have a PySelection object
     * @throws BadLocationException
     */
    public ScopeAnalyzerVisitor(IPythonNature nature, String moduleName, IModule current, IProgressMonitor monitor,
            PySelection ps) throws BadLocationException {
        super(nature, moduleName, current, monitor, ps);
    }

    public ScopeAnalyzerVisitor(IPythonNature nature, String moduleName, IModule current, IDocument document,
            IProgressMonitor monitor, String pNameToFind, int absoluteCursorOffset, String[] tokenAndQual)
            throws BadLocationException {
        super(nature, moduleName, current, document, monitor, pNameToFind, absoluteCursorOffset, tokenAndQual);
    }

    /**
     * This is the key in the importsFoundFromModuleName for tokens that were not resolved.
     */
    private static final String UNRESOLVED_MOD_NAME = "__UNRESOLVED__MOD__NAME__!";

    /**
     * It contains import artificially generated, such as module names in ImportFrom
     * E.g.: from os.path import xxx will generate an import for 'os' and an import for 'path'
     * artificially, just to make matches
     */
    protected Map<String, List<Tuple3<Found, Integer, ASTEntry>>> importsFoundFromModuleName = new HashMap<String, List<Tuple3<Found, Integer, ASTEntry>>>();
    /**
     * Same as the importsFoundFromModuleName, but works on the imports that actually become tokens
     * in the namespace.
     */
    protected Map<String, List<Tuple3<Found, Integer, ASTEntry>>> importsFound = new HashMap<String, List<Tuple3<Found, Integer, ASTEntry>>>();

    /**
     * Used to add some Found that is related to an import to a 'global import register'.
     * This is needed because, unlike other regular tokens, we want to find imports that are
     * in diferent contexts as being in the same context.
     * 
     * @param found this is the Found that we want to add to the imports
     * @param map this is the map that contains the imports Found occurrences (it has to be passed,
     * as there is a map for the imports that are actually in the namespace and another for those
     * that are 'artificially' generated).
     */
    private void addFoundToImportsMap(Found found, Map<String, List<Tuple3<Found, Integer, ASTEntry>>> map) {
        ImportInfo info = found.importInfo;
        String modName = UNRESOLVED_MOD_NAME;
        if (info != null && info.mod != null) {
            modName = info.mod.getName();
        }
        List<Tuple3<Found, Integer, ASTEntry>> prev = map.get(modName);
        if (prev == null) {
            prev = new ArrayList<Tuple3<Found, Integer, ASTEntry>>();
            map.put(modName, prev);
        }

        prev.add(new Tuple3<Found, Integer, ASTEntry>(found, -1, peekParent())); //col delta is undefined
    }

    @Override
    public Object visitImportFrom(ImportFrom node) throws Exception {
        Object ret = super.visitImportFrom(node);
        //the import from will generate the tokens that go into the module namespace, but still, it needs to
        //create tokens that will not be used in code-analysis, but will be used in matching tokens
        //regarding its module.
        NameTok tokModName = (NameTok) node.module;
        for (String m : new FullRepIterable(tokModName.id)) {
            if (m.indexOf(".") == -1) {
                aliasType[] names = new aliasType[1];
                NameTok importNameTok = new NameTok(m, NameTok.ImportModule);

                importNameTok.beginLine = tokModName.beginLine;
                importNameTok.beginColumn = tokModName.beginColumn;

                names[0] = new aliasType(importNameTok, null);
                names[0].beginLine = tokModName.beginLine;
                names[0].beginColumn = tokModName.beginColumn;

                Import importTok = new Import(names);
                importTok.beginLine = tokModName.beginLine;
                importTok.beginColumn = tokModName.beginColumn;

                List<IToken> createdTokens = AbstractVisitor.makeImportToken(importTok, null, this.current.getName(),
                        true, this.current.getNature());
                for (IToken token : createdTokens) {
                    ImportInfo info = this.scope.importChecker.visitImportToken(token, false, this.completionCache);
                    Found found = new Found(token, token, scope.getCurrScopeId(), scope.getCurrScopeItems());
                    found.importInfo = info;

                    checkFound(found, peekParent()); //check if this is actually a match
                    addFoundToImportsMap(found, importsFoundFromModuleName);
                }
            }
        }
        return ret;
    }

    @Override
    public void onImportInfoSetOnFound(Found found) {
        super.onImportInfoSetOnFound(found);
        addFoundToImportsMap(found, importsFound);
    }

    /**
     * Called for each found occurrence in the complete token occurrences
     * 
     * Is used to add other returns to ret
     */
    @Override
    protected void onGetCompleteTokenOccurrences(Tuple3<Found, Integer, ASTEntry> found, Set<IToken> f,
            ArrayList<Tuple4<IToken, Integer, ASTEntry, Found>> ret) {

        //other matches for the imports that we had already found.
        Tuple<List<Tuple4<IToken, Integer, ASTEntry, Found>>, List<Tuple4<IToken, Integer, ASTEntry, Found>>> matchingImportEntries = getImportEntries(
                found, f);
        List<Tuple4<IToken, Integer, ASTEntry, Found>> fromModule = matchingImportEntries.o1;
        List<Tuple4<IToken, Integer, ASTEntry, Found>> fromImports = matchingImportEntries.o2;

        ret.addAll(fromModule);
        ret.addAll(fromImports);

        //let's iterate through the imports that are to be recognized as the same import we're checking and get the
        //individual occurrences for each one of those (but this only happens if the context of the current 
        //import is different from the context of that import)
        for (Tuple4<IToken, Integer, ASTEntry, Found> tuple3 : fromImports) {

            try {
                if (!(tuple3.o1 instanceof SourceToken)) {
                    continue;
                }

                SourceToken tok = (SourceToken) tuple3.o1;
                SimpleNode ast = tok.getAst();
                int line = 0;
                int col = 0;
                if (!(ast instanceof Import)) {
                    continue;
                }

                Import import1 = (Import) ast;
                line = import1.names[0].beginLine - 1;
                col = import1.names[0].beginColumn - 1;
                PySelection ps = new PySelection(this.document, line, col);
                ScopeAnalyzerVisitorWithoutImports analyzerVisitorWithoutImports = new ScopeAnalyzerVisitorWithoutImports(
                        this.nature, this.moduleName, this.current, this.monitor, ps);

                SourceModule s = (SourceModule) this.current;
                s.getAst().accept(analyzerVisitorWithoutImports);
                analyzerVisitorWithoutImports.checkFinished();

                //now, let's get the token occurrences for the analyzer that worked without gathering the imports
                ArrayList<Tuple4<IToken, Integer, ASTEntry, Found>> completeTokenOccurrences = analyzerVisitorWithoutImports
                        .getCompleteTokenOccurrences();

                for (Tuple4<IToken, Integer, ASTEntry, Found> oc : completeTokenOccurrences) {
                    if (!f.contains(oc.o1) && !oc.o1.isImport()) { //the import should be already added
                        if (oc.o2 < tuple3.o2) {
                            oc.o2 = tuple3.o2;
                        }
                        f.add(oc.o1);
                        ret.add(oc);
                    }
                }

            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * This method finds entries for found tokens that are the same import, but that may still not be there
     * because they are either in some other scope or are in the module part of an ImportFrom
     */
    private Tuple<List<Tuple4<IToken, Integer, ASTEntry, Found>>, List<Tuple4<IToken, Integer, ASTEntry, Found>>> getImportEntries(
            Tuple3<Found, Integer, ASTEntry> found, Set<IToken> f) {
        List<Tuple4<IToken, Integer, ASTEntry, Found>> fromModuleRet = new ArrayList<Tuple4<IToken, Integer, ASTEntry, Found>>();
        List<Tuple4<IToken, Integer, ASTEntry, Found>> fromImportsRet = new ArrayList<Tuple4<IToken, Integer, ASTEntry, Found>>();
        if (found.o1.isImport()) {
            //now, as it is an import, we have to check if there are more matching imports found
            String key = UNRESOLVED_MOD_NAME;
            if (found.o1.importInfo != null && found.o1.importInfo.mod != null) {
                key = found.o1.importInfo.mod.getName();
            }
            List<Tuple3<Found, Integer, ASTEntry>> fromModule = importsFoundFromModuleName.get(key);
            List<Tuple3<Found, Integer, ASTEntry>> fromImports = importsFound.get(key);

            checkImportEntries(fromModuleRet, f, fromModule, found.o2);
            checkImportEntries(fromImportsRet, f, fromImports, found.o2);

        }
        return new Tuple<List<Tuple4<IToken, Integer, ASTEntry, Found>>, List<Tuple4<IToken, Integer, ASTEntry, Found>>>(
                fromModuleRet, fromImportsRet);
    }

    /**
     * Checks the import entries for imports that are the same as the one that should be already found.
     */
    private void checkImportEntries(List<Tuple4<IToken, Integer, ASTEntry, Found>> ret, Set<IToken> f,
            List<Tuple3<Found, Integer, ASTEntry>> importEntries, int colDelta) {

        if (importEntries != null) {
            for (Tuple3<Found, Integer, ASTEntry> foundInFromModule : importEntries) {
                IToken generator = foundInFromModule.o1.getSingle().generator;

                Tuple4<IToken, Integer, ASTEntry, Found> tup3 = new Tuple4<IToken, Integer, ASTEntry, Found>(generator,
                        colDelta > foundInFromModule.o2 ? colDelta : foundInFromModule.o2, foundInFromModule.o3,
                        foundInFromModule.o1);

                if (!f.contains(generator)) {
                    f.add(generator);
                    ret.add(tup3);
                }
            }
        }
    }

}
