/**
 * 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 Nov 12, 2004
 *
 * @author Fabio Zadrozny
 */
package org.python.pydev.editor.codecompletion.revisited.modules;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.jface.text.IDocument;
import org.python.pydev.core.FileUtilsFileBuffer;
import org.python.pydev.core.FullRepIterable;
import org.python.pydev.core.ICodeCompletionASTManager;
import org.python.pydev.core.ICompletionCache;
import org.python.pydev.core.ICompletionState;
import org.python.pydev.core.IGrammarVersionProvider;
import org.python.pydev.core.ILocalScope;
import org.python.pydev.core.IModule;
import org.python.pydev.core.IModulesManager;
import org.python.pydev.core.IPythonNature;
import org.python.pydev.core.IToken;
import org.python.pydev.core.MisconfigurationException;
import org.python.pydev.core.ModulesKey;
import org.python.pydev.core.ModulesKeyForZip;
import org.python.pydev.core.TupleN;
import org.python.pydev.core.structure.CompletionRecursionException;
import org.python.pydev.editor.codecompletion.revisited.CompletionStateFactory;
import org.python.pydev.editor.codecompletion.revisited.PythonPathHelper;
import org.python.pydev.editor.codecompletion.revisited.visitors.Definition;
import org.python.pydev.parser.PyParser;
import org.python.pydev.parser.jython.SimpleNode;
import org.python.pydev.shared_core.io.FileUtils;
import org.python.pydev.shared_core.parsing.BaseParser.ParseOutput;

/**
 * @author Fabio Zadrozny
 */
public abstract class AbstractModule implements IModule {

    private static final IToken[] EMPTY_TOKEN_ARRAY = new IToken[0];

    /**
     * May be changed for tests
     */
    public static String MODULE_NAME_WHEN_FILE_IS_UNDEFINED = "";

    /** 
     * @see org.python.pydev.core.IModule#getWildImportedModules()
     */
    @Override
    public abstract IToken[] getWildImportedModules();

    /** 
     * @see org.python.pydev.core.IModule#getFile()
     */
    @Override
    public abstract File getFile();

    /** 
     * @see org.python.pydev.core.IModule#getTokenImportedModules()
     */
    @Override
    public abstract IToken[] getTokenImportedModules();

    /** 
     * @see org.python.pydev.core.IModule#getGlobalTokens()
     */
    @Override
    public abstract IToken[] getGlobalTokens();

    /**
     * Don't deal with zip files unless specifically specified
     */
    @Override
    public String getZipFilePath() {
        return null;
    }

    /** 
     * @see org.python.pydev.core.IModule#getLocalTokens(int, int, ILocalScope)
     */
    @Override
    public IToken[] getLocalTokens(int line, int col, ILocalScope scope) {
        return EMPTY_TOKEN_ARRAY;
    }

    /**
     * Checks if it is in the global tokens that were created in this module
     * @param tok the token we are looking for
     * @return true if it was found and false otherwise
     */
    @Override
    public abstract boolean isInDirectGlobalTokens(String tok, ICompletionCache completionCache);

    /** 
     * @throws CompletionRecursionException 
     * @see org.python.pydev.core.IModule#isInGlobalTokens(java.lang.String, org.python.pydev.plugin.nature.PythonNature)
     */
    @Override
    public boolean isInGlobalTokens(String tok, IPythonNature nature, ICompletionCache completionCache)
            throws CompletionRecursionException {
        return isInGlobalTokens(tok, nature, true, completionCache);
    }

    /** 
     * @throws CompletionRecursionException 
     * @see org.python.pydev.core.IModule#isInGlobalTokens(java.lang.String, org.python.pydev.plugin.nature.PythonNature, boolean)
     */
    @Override
    public boolean isInGlobalTokens(String tok, IPythonNature nature, boolean searchSameLevelMods,
            ICompletionCache completionCache) throws CompletionRecursionException {
        return isInGlobalTokens(tok, nature, searchSameLevelMods, false, completionCache) != IModule.NOT_FOUND;
    }

    @Override
    public int isInGlobalTokens(String tok, IPythonNature nature, boolean searchSameLevelMods,
            boolean ifHasGetAttributeConsiderInTokens, ICompletionCache completionCache)
            throws CompletionRecursionException {

        //it's worth checking it if it is not dotted... (much faster as it's in a map already)
        if (tok.indexOf(".") == -1) {
            if (isInDirectGlobalTokens(tok, completionCache)) {
                return IModule.FOUND_TOKEN;
            }
        }

        String[] headAndTail = FullRepIterable.headAndTail(tok);
        String head = headAndTail[1];
        String generateTokensFor = headAndTail[0];

        Map<String, IToken> cachedTokens = getCachedCompletions(tok, nature, searchSameLevelMods, completionCache,
                generateTokensFor);

        if (cachedTokens.containsKey(head)) {
            return IModule.FOUND_TOKEN;
        }

        if (ifHasGetAttributeConsiderInTokens) {

            IToken token = cachedTokens.get("__getattribute__");
            if (token == null || isTokenFromBuiltins(token)) {
                token = cachedTokens.get("__getattr__");
            }
            if (token != null && !isTokenFromBuiltins(token)) {
                return IModule.FOUND_BECAUSE_OF_GETATTR;
            }

            //Try to determine if the user specified it has a @DynamicAttrs.
            String[] parentsHeadAndTail = FullRepIterable.headAndTail(generateTokensFor);
            cachedTokens = getCachedCompletions(tok, nature, searchSameLevelMods, completionCache,
                    parentsHeadAndTail[0]);
            IToken parentToken = cachedTokens.get(parentsHeadAndTail[1]);
            if (parentToken != null) {
                String docString = parentToken.getDocStr();
                if (docString != null) {
                    if (docString.indexOf("@DynamicAttrs") != -1) {
                        //class that has things dynamically defined.
                        return IModule.FOUND_BECAUSE_OF_GETATTR;
                    }
                }
            }
        }

        //if not found until now, it is not defined
        return IModule.NOT_FOUND;
    }

    @SuppressWarnings("unchecked")
    protected Map<String, IToken> getCachedCompletions(String tok, IPythonNature nature, boolean searchSameLevelMods,
            ICompletionCache completionCache, String generateTokensFor) throws CompletionRecursionException {
        //now, check if it's cached in a way we can use it (we cache it not as raw tokens, but as representation --> token)
        //to help in later searches.
        String name = this.getName();
        Object key = new TupleN("isInGlobalTokens", name != null ? name : "", generateTokensFor, tok,
                searchSameLevelMods);
        Map<String, IToken> cachedTokens = (Map<String, IToken>) completionCache.getObj(key);

        if (cachedTokens == null) {
            cachedTokens = internalGenerateCachedTokens(nature, completionCache, generateTokensFor,
                    searchSameLevelMods);
            completionCache.add(key, cachedTokens);
        }
        return cachedTokens;
    }

    private boolean isTokenFromBuiltins(IToken token) {
        String parentPackage = token.getParentPackage();
        return parentPackage.equals("__builtin__") || parentPackage.startsWith("__builtin__.")
                || parentPackage.equals("builtins") || parentPackage.startsWith("builtins.");
    }

    /**
     * Generates the cached tokens in the needed structure for a 'fast' search given a token representation
     * (creates a map with the name of the token --> token).
     */
    private Map<String, IToken> internalGenerateCachedTokens(IPythonNature nature, ICompletionCache completionCache,
            String activationToken, boolean searchSameLevelMods) throws CompletionRecursionException {

        Map<String, IToken> cachedTokens = new HashMap<String, IToken>();

        //if still not found, we have to get all the tokens, including regular and wild imports
        ICompletionState state = CompletionStateFactory.getEmptyCompletionState(nature, completionCache);
        ICodeCompletionASTManager astManager = nature.getAstManager();
        state.setActivationToken(activationToken);

        //we don't want to gather builtins in this case.
        state.setBuiltinsGotten(true);
        IToken[] globalTokens = astManager.getCompletionsForModule(this, state, searchSameLevelMods);
        for (IToken token : globalTokens) {
            String rep = token.getRepresentation();
            IToken t = cachedTokens.get(rep);
            if (t != null) {
                //only override tokens if it's a getattr that's not defined in the builtin module
                if (rep.equals("__getattribute__") || rep.equals("__getattr__")) {
                    if (!isTokenFromBuiltins(token)) {
                        cachedTokens.put(rep, token);
                    }
                }
            } else {
                cachedTokens.put(rep, token);
            }
        }
        return cachedTokens;
    }

    /**
     * The token we're looking for must be the state activation token
     */
    @Override
    public abstract Definition[] findDefinition(ICompletionState state, int line, int col, IPythonNature nature)
            throws Exception;

    /** 
     * @see org.python.pydev.core.IModule#getGlobalTokens(org.python.pydev.editor.codecompletion.revisited.CompletionState, org.python.pydev.core.ICodeCompletionASTManager)
     */
    @Override
    public abstract IToken[] getGlobalTokens(ICompletionState state, ICodeCompletionASTManager manager);

    /** 
     * @see org.python.pydev.core.IModule#getDocString()
     */
    @Override
    public abstract String getDocString();

    /**
     * Name of the module
     */
    protected String name;

    /** 
     * @see org.python.pydev.core.IModule#getName()
     */
    @Override
    public String getName() {
        return name;
    }

    /**
     * Constructor
     * 
     * @param name - name of the module
     */
    protected AbstractModule(String name) {
        this.name = name;
    }

    /**
     * This method creates a source module from a file.
     * 
     * @return
     * @throws IOException 
     * @throws MisconfigurationException 
     */
    public static AbstractModule createModule(String name, File f, IPythonNature nature, boolean checkForPath)
            throws IOException, MisconfigurationException {
        if (PythonPathHelper.isValidFileMod(f.getName())) {
            if (PythonPathHelper.isValidSourceFile(f.getName())) {
                return createModuleFromDoc(name, f, FileUtilsFileBuffer.getDocFromFile(f), nature, checkForPath);

            } else { //this should be a compiled extension... we have to get completions from the python shell.
                return new CompiledModule(name, nature.getAstManager().getModulesManager(), nature);
            }
        }

        //if we are here, return null...
        return null;
    }

    public static SourceModule createModuleFromDoc(String name, File f, IDocument doc, IPythonNature nature,
            boolean checkForPath) throws MisconfigurationException {
        return createModuleFromDoc(name, f, doc, nature, checkForPath, nature);
    }

    /** 
     * This function creates the module given that you have a document (that will be parsed)
     * @throws MisconfigurationException 
     */
    public static SourceModule createModuleFromDoc(String name, File f, IDocument doc, IPythonNature nature,
            boolean checkForPath, IGrammarVersionProvider grammarVersionProvider) throws MisconfigurationException {
        //for doc, we are only interested in python files.

        if (f != null) {
            if (!checkForPath || PythonPathHelper.isValidSourceFile(f.getName())) {
                ParseOutput obj = PyParser.reparseDocument(new PyParser.ParserInfo(doc, grammarVersionProvider, name,
                        f));
                return new SourceModule(name, f, (SimpleNode) obj.ast, obj.error, nature);
            }
        } else {
            ParseOutput obj = PyParser.reparseDocument(new PyParser.ParserInfo(doc, grammarVersionProvider, name, f));
            return new SourceModule(name, f, (SimpleNode) obj.ast, obj.error, nature);
        }
        return null;
    }

    /**
     * This function creates a module and resolves the module name (use this function if only the file is available).
     * @throws MisconfigurationException 
     */
    public static IModule createModuleFromDoc(File file, IDocument doc, IPythonNature pythonNature)
            throws MisconfigurationException {
        IModulesManager projModulesManager = pythonNature.getAstManager().getModulesManager();
        String moduleName = null;
        if (file != null) {
            moduleName = projModulesManager.resolveModule(FileUtils.getFileAbsolutePath(file));
        }
        if (moduleName == null) {
            moduleName = MODULE_NAME_WHEN_FILE_IS_UNDEFINED;
        }
        IModule module = createModuleFromDoc(moduleName, file, doc, pythonNature, false);
        return module;
    }

    /**
     * Creates a source file generated only from an ast.
     * @param n the ast root
     * @return the module
     */
    public static IModule createModule(SimpleNode n, IPythonNature nature) {
        return new SourceModule(null, null, n, null, nature);
    }

    /**
     * Creates a source file generated only from an ast.
     * 
     * @param n the ast root
     * @param file the module file
     * @param moduleName the name of the module
     * 
     * @return the module
     */
    public static IModule createModule(SimpleNode n, File file, String moduleName, IPythonNature nature) {
        return new SourceModule(moduleName, file, n, null, nature);
    }

    /**
     * @return an empty module representing the key passed.
     */
    public static AbstractModule createEmptyModule(ModulesKey key) {
        if (key instanceof ModulesKeyForZip) {
            ModulesKeyForZip e = ((ModulesKeyForZip) key);
            return new EmptyModuleForZip(key.name, key.file, e.zipModulePath, e.isFile);

        } else {
            return new EmptyModule(key.name, key.file);
        }
    }

    @Override
    public ILocalScope getLocalScope(int line, int col) {
        return null;
    }

    /** 
     * @see org.python.pydev.core.IModule#toString()
     */
    @Override
    public String toString() {
        String n2 = this.getClass().getName();
        String n = n2.substring(n2.lastIndexOf('.') + 1);
        return this.getName() + " (" + n + ")";
    }

    /**
     * @return true if the name we have ends with .__init__ (default for packages -- others are modules)
     */
    @Override
    public boolean isPackage() {
        return this.name != null && this.name.endsWith(".__init__");
    }

    @Override
    public String getPackageFolderName() {
        return FullRepIterable.getParentModule(this.name);
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (!(obj instanceof AbstractModule)) {
            return false;
        }
        AbstractModule other = (AbstractModule) obj;
        if (name == null) {
            if (other.name != null) {
                return false;
            }
        } else if (!name.equals(other.name)) {
            return false;
        }
        return true;
    }

}
