/*
 * Copyright Red Hat Inc. and/or its affiliates and other contributors
 * as indicated by the authors tag. All rights reserved.
 *
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU General Public License version 2.
 * 
 * This particular file is subject to the "Classpath" exception as provided in the 
 * LICENSE file that accompanied this code.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT A
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License,
 * along with this distribution; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA  02110-1301, USA.
 */

package com.redhat.ceylon.compiler.loader.model;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;

import com.redhat.ceylon.cmr.api.ArtifactResult;
import com.redhat.ceylon.cmr.api.ImportType;
import com.redhat.ceylon.cmr.api.JDKUtils;
import com.redhat.ceylon.cmr.api.VersionComparator;
import com.redhat.ceylon.common.Versions;
import com.redhat.ceylon.compiler.loader.AbstractModelLoader;
import com.redhat.ceylon.compiler.typechecker.analyzer.ModuleManager;
import com.redhat.ceylon.compiler.typechecker.context.Context;
import com.redhat.ceylon.compiler.typechecker.context.PhasedUnits;
import com.redhat.ceylon.compiler.typechecker.model.Module;
import com.redhat.ceylon.compiler.typechecker.model.ModuleImport;

/**
 * ModuleManager which can load artifacts from jars and cars.
 *
 * @author Stéphane Épardaud <stef@epardaud.fr>
 */
public abstract class LazyModuleManager extends ModuleManager {

    public LazyModuleManager(Context ceylonContext) {
        super(ceylonContext);
    }

    protected void setupIfJDKModule(LazyModule module) {
        // Make sure that the java modules are set up properly.
        // Bad jdk versions will not be made available and the module validator
        // will fail to load their artifacts, and the error is properly handled by the lazy module manager in
        // attachErrorToDependencyDeclaration()
        String nameAsString = module.getNameAsString();
        String version = module.getVersion();
        if(version != null
                && AbstractModelLoader.isJDKModule(nameAsString)){
            // substitute Java 8 for Java 7 if we're running on Java 8
            if(JDKUtils.jdk == JDKUtils.JDK.JDK8 && version.equals(JDKUtils.JDK.JDK7.version)){
                version = JDKUtils.JDK.JDK8.version;
                module.setVersion(version);
            }
            if(version.equals(JDKUtils.jdk.version)){
                module.setAvailable(true);
                module.setJava(true);
            }
        }
    }

    @Override
    public void resolveModule(ArtifactResult artifact, Module module, ModuleImport moduleImport, 
            LinkedList<Module> dependencyTree, List<PhasedUnits> phasedUnitsOfDependencies, boolean forCompiledModule) {
        String moduleName = module.getNameAsString();
        boolean moduleLoadedFromSource = isModuleLoadedFromSource(moduleName);
        boolean isLanguageModule = module == module.getLanguageModule();
        
        // if this is for a module we're compiling, or for an indirectly imported module, we need to check because the
        // module in question will be in the classpath
        if(moduleLoadedFromSource || forCompiledModule){
            // check for an already loaded module with the same name but different version
            for(Module loadedModule : getContext().getModules().getListOfModules()){
                if(loadedModule.getNameAsString().equals(moduleName)
                        && !loadedModule.getVersion().equals(module.getVersion())
                        && getModelLoader().isModuleInClassPath(loadedModule)){
                    // abort
                    // we need this error thrown rather than the typechecker error because the typechecker currently
                    // allows more than we do, such as having direct imports of the same module with different versions
                    // as long as they are not reexported, but we don't support that since they all go in the same
                    // classpath (direct imports of compiled modules)
                    String[] versions = VersionComparator.orderVersions(module.getVersion(), loadedModule.getVersion());
                    String error = "source code imports two different versions of module '" + 
                            module.getNameAsString() + "': "+
                            "version \""+versions[0] + "\" and version \""+ versions[1] +
                            "\"";
                    addErrorToModule(dependencyTree.getFirst(), error);
                    return;
                }
            }
        }
        
        if(moduleLoadedFromSource){
            super.resolveModule(artifact, module, moduleImport, dependencyTree, phasedUnitsOfDependencies, forCompiledModule);
        }else if(forCompiledModule || isLanguageModule || shouldLoadTransitiveDependencies()){
            // we only add stuff to the classpath and load the modules if we need them to compile our modules
            getModelLoader().addModuleToClassPath(module, artifact); // To be able to load it from the corresponding archive
            if(!module.isDefault() && !getModelLoader().loadCompiledModule(module)){
                // we didn't find module.class so it must be a java module if it's not the default module
                ((LazyModule)module).setJava(true);
                
                List<ArtifactResult> deps = artifact.dependencies();
                for (ArtifactResult dep : deps) {
                    Module dependency = getOrCreateModule(ModuleManager.splitModuleName(dep.name()), dep.version());

                    ModuleImport depImport = findImport(module, dependency);
                    if (depImport == null) {
                        moduleImport = new ModuleImport(dependency, dep.importType() == ImportType.OPTIONAL, dep.importType() == ImportType.EXPORT);
                        module.addImport(moduleImport);
                    }
                }
            }
            LazyModule lazyModule = (LazyModule) module;
            if(!lazyModule.isJava() && !module.isDefault()){
                // it must be a Ceylon module
                // default modules don't have any module descriptors so we can't check them
                if(!Versions.isJvmBinaryVersionSupported(lazyModule.getMajor(), lazyModule.getMinor())){
                    attachErrorToDependencyDeclaration(moduleImport,
                            dependencyTree,
                            "This module was compiled for an incompatible version of the Ceylon compiler ("+lazyModule.getMajor()+"."+lazyModule.getMinor()+")."
                                    +"\nThis compiler supports "+Versions.JVM_BINARY_MAJOR_VERSION+"."+Versions.JVM_BINARY_MINOR_VERSION+"."
                                    +"\nPlease try to recompile your module using a compatible compiler."
                                    +"\nBinary compatibility will only be supported after Ceylon 1.2.");
                }
            }
            // module is now available
            module.setAvailable(true);
        }
    }

    /**
     * To be overriden by reflection module manager, because reflection requires even types of private members
     * of imported modules to be in the classpath, and those could be of unimported modules (since they're private
     * that's allowed).
     */
    protected boolean shouldLoadTransitiveDependencies(){
        return false;
    }
    
    @Override
    protected abstract Module createModule(List<String> moduleName, String version);
    
    protected abstract AbstractModelLoader getModelLoader();

    /**
     * Return true if this module should be loaded from source we are compiling
     * and not from its compiled artifact at all. Returns false by default, so
     * modules will be laoded from their compiled artifact.
     */
    protected boolean isModuleLoadedFromSource(String moduleName){
        return false;
    }
    
    @Override
    public Iterable<String> getSearchedArtifactExtensions() {
        return Arrays.asList("car", "jar");
    }

    @Override
    public void addImplicitImports() {
        Module languageModule = getContext().getModules().getLanguageModule();
        for(Module m : getContext().getModules().getListOfModules()){
            // Java modules don't depend on ceylon.language
            if((m instanceof LazyModule == false || !((LazyModule)m).isJava()) && !m.equals(languageModule)) {
                // add ceylon.language if required
                ModuleImport moduleImport = findImport(m, languageModule);
                if (moduleImport == null) {
                    moduleImport = new ModuleImport(languageModule, false, true);
                    m.addImport(moduleImport);
                }
            }
        }
    }
    
    @Override
    public void attachErrorToDependencyDeclaration(ModuleImport moduleImport, List<Module> dependencyTree, String error) {
        // special case for the java modules, which we only get when using the wrong version
        String name = moduleImport.getModule().getNameAsString();
        if(AbstractModelLoader.isJDKModule(name)){
            error = "unsupported JDK module version: the only supported version is '" + 
                    JDKUtils.jdk.version + "' which you get with Java "+JDKUtils.jdk.version;
        }
        super.attachErrorToDependencyDeclaration(moduleImport, dependencyTree, error);
    }

    @Override
    protected boolean compareVersions(Module current, String version, String currentVersion) {
        String name = current.getNameAsString();
        if(JDKUtils.isJDKModule(name) || JDKUtils.isOracleJDKModule(name)){
            // if we're running JDK8, pretend that it provides JDK7 modules
            if(JDKUtils.jdk == JDKUtils.JDK.JDK8
                    && JDKUtils.JDK.JDK7.version.equals(version)
                    && JDKUtils.JDK.JDK8.version.equals(currentVersion))
                return true;
        }
        return currentVersion == null || version == null || currentVersion.equals(version);
    }
}
