package org.jboss.windup.rules.apps.mavenize;

import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.logging.Logger;
import org.apache.commons.collections4.OrderedMap;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.jboss.windup.graph.GraphContext;
import org.jboss.windup.graph.model.ArchiveModel;
import org.jboss.windup.graph.model.OrganizationModel;
import org.jboss.windup.graph.model.ProjectModel;
import org.jboss.windup.graph.model.TechnologyReferenceModel;
import org.jboss.windup.graph.model.WindupConfigurationModel;
import org.jboss.windup.graph.model.resource.FileModel;
import org.jboss.windup.rules.apps.java.archives.model.ArchiveCoordinateModel;
import org.jboss.windup.rules.apps.java.archives.model.IdentifiedArchiveModel;
import org.jboss.windup.rules.apps.java.model.project.MavenProjectModel;
import org.jboss.windup.util.Logging;

/**
 * Performs the actions needed to mavenize an application.
 *
 * @author <a href="http://ondra.zizka.cz/">Ondrej Zizka, ozizka at seznam.cz</a>
 */
public class MavenizationService
{
    private static final Logger LOG = Logging.get(MavenizationService.class);
    public static final String OUTPUT_SUBDIR_MAVENIZED = "mavenized";

    private final GraphContext grCtx;


    MavenizationService(GraphContext graphContext)
    {
        this.grCtx = graphContext;
    }

    /**
     * For the given application (Windup input), creates a stub of Mavenized project.
     * <p>
     * The resulting structure is: (+--- is "module", +~~~ is a dependency)
     * <pre>
     *  Parent POM
     *   +--- BOM
     *     +~~~ JBoss EAP BOM
     *   +--- JAR submodule
     *     +~~~ library JARs
     *   +--- WAR
     *     +~~~ library JARs
     *     +~~~ JAR submodule
     *   +--- EAR
     *     +~~~ library JAR
     *     +~~~ JAR submodule
     *     +~~~ WAR submodule
     * </pre>
     */
    void mavenizeApp(ProjectModel projectModel)
    {
        LOG.info("Mavenizing ProjectModel " + projectModel.toPrettyString());
        MavenizationContext mavCtx = new MavenizationContext();
        mavCtx.graphContext = grCtx;

        WindupConfigurationModel config = grCtx.getUnique(WindupConfigurationModel.class);
        mavCtx.mavenizedBaseDir = config.getOutputPath().asFile().toPath().resolve(OUTPUT_SUBDIR_MAVENIZED);
        mavCtx.unifiedGroupId = new ModuleAnalysisHelper(grCtx).deriveGroupId(projectModel);
        mavCtx.unifiedAppName = normalizeDirName(projectModel.getName());
        mavCtx.unifiedVersion = "1.0";

        // 1) create the overall structure - a parent, and a BOM.

        // Root pom.xml ( serves as a parent pom.xml in our resulting structure).
        mavCtx.rootPom = new Pom(new MavenCoord(mavCtx.getUnifiedGroupId(), mavCtx.getUnifiedAppName() + "-parent", mavCtx.getUnifiedVersion()));
        mavCtx.rootPom.role = Pom.ModuleRole.PARENT;
        mavCtx.rootPom.name = projectModel.getName() + " - Parent";
        mavCtx.rootPom.description = "Parent of " + projectModel.getName();
        mavCtx.rootPom.root = true;
        final String bomArtifactId = mavCtx.getUnifiedAppName() + "-bom";

        // BOM
        Pom bom = new Pom(new MavenCoord(mavCtx.getUnifiedGroupId(), bomArtifactId, mavCtx.getUnifiedVersion()));
        bom.bom = getTargetTechnologies().contains("eap7")
                ? MavenizeRuleProvider.JBOSS_BOM_JAVAEE7_WITH_ALL
                : MavenizeRuleProvider.JBOSS_BOM_JAVAEE6_WITH_ALL;
        bom.role = Pom.ModuleRole.BOM;
        bom.parent = new Pom(MavenizeRuleProvider.JBOSS_PARENT);
        bom.description = "Bill of Materials. See https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html";
        bom.name = projectModel.getName() + " - BOM";
        mavCtx.getRootPom().submodules.put(bomArtifactId, bom);
        mavCtx.bom = bom;

        // BOM - dependencyManagement dependencies
        for( ArchiveCoordinateModel dep : grCtx.getUnique(GlobalBomModel.class).getDependencies() ){
            LOG.info("Adding dep to BOM: " + dep.toPrettyString());
            bom.dependencies.add(new SimpleDependency(Dependency.Role.LIBRARY, MavenCoord.from(dep)));
        }

        // 2) Recursively add the modules.
        mavCtx.rootAppPom = mavenizeModule(mavCtx, projectModel, null);

        // TODO: MIGR-236 Sort the modules.
        ///mavCtx.rootPom.submodules = sortSubmodulesToReflectDependencies(mavCtx.rootAppPom);

        // 3) Write the pom.xml's.
        new MavenStructureRenderer(mavCtx).createMavenProjectDirectoryTree();
    }


    /**
     * Mavenizes the particular project module, i.e. single pom.xml.
     * Then continues recursively to submodules.
     *
     * @return null if the project can't be processed for some reason, e.g. is from an unparsable jar.
     */
    private Pom mavenizeModule(MavenizationContext mavCtx, ProjectModel projectModel, Pom containingModule)
    {
        LOG.info("Mavenizing submodule ProjectModel " + projectModel.toPrettyString());
        LOG.info("    Root file: " + projectModel.getRootFileModel().toPrettyString());
        LOG.info("    Containing module: " + containingModule);

        if (projectModel.getRootFileModel().getParseError() != null)
            return null;

        // Known library -> add as a dependency and skip.

        // SHA1 identified archive?
        if (projectModel.getRootFileModel() instanceof IdentifiedArchiveModel)
        {
            final IdentifiedArchiveModel idArch = (IdentifiedArchiveModel)projectModel.getRootFileModel();
            if (idArch == null)
                LOG.warning("Project's IdentifiedArchiveModel getRootFileModel() returned null.");
            else if (idArch.getCoordinate() == null)
                LOG.warning("Project's IdentifiedArchiveModel getRootFileModel().getCoordinate() returned null.");
            else if(containingModule == null)
                LOG.warning("containingModule is null."); // IllegalStateEx?
            else
                containingModule.dependencies.add(new SimpleDependency(Dependency.Role.LIBRARY, MavenCoord.from(idArch.getCoordinate())));

            LOG.info("Known library, skipping recursive mavenization: " + idArch.getCoordinate());
            return null;
        }

        // ArchiveModel's organizations contain a known org, like Apache.
        if (projectModel.getRootFileModel() instanceof ArchiveModel)
        {
            Set<String> skipOrganizations = new HashSet(Arrays.asList("Apache Sun Iona IBM Codehaus Spring Sonatype JBoss Oracle".toLowerCase().split("")));

            final ArchiveModel arch = (ArchiveModel)projectModel.getRootFileModel();
            for (OrganizationModel org : arch.getOrganizationModels())
            {
                if (skipOrganizations.contains(org.getName().toLowerCase()))
                {
                    LOG.info("Library from 3rd party vendor ("+org.getName()+"), skipping recursive mavenization: " + arch.getFilePath());
                    return null;
                }
            }
        }

        // A MavenProject.
        // TODO: This covers both app modules and libraries with pom.xml inside. Need to diferentiate - skip only libraries.
        // TODO: This should disappear after WINDUP-981
        if (false && projectModel instanceof MavenProjectModel)
        {
            final MavenProjectModel mvnProject = (MavenProjectModel)projectModel;
            if (mvnProject.getMavenIdentifier() == null)
                LOG.warning("MavenProject's getMavenIdentifier() returned null.");
            else if(containingModule == null)
                LOG.warning("containingModule is null."); // IllegalStateEx?
            else if (true /* isLibraryAndNotProjectModule() */)
                containingModule.dependencies.add(new SimpleDependency(Dependency.Role.LIBRARY, MavenCoord.fromGAVPC(mvnProject.getMavenIdentifier())));
            return null;
        }

        MavenCoord modulePomCoords = new MavenCoord();
        modulePomCoords.setGroupId(mavCtx.getUnifiedGroupId());
        modulePomCoords.setVersion(mavCtx.getUnifiedVersion());
        final String artifactId = deriveAppropriateArtifactId(projectModel);
        modulePomCoords.setArtifactId(artifactId);
        modulePomCoords.setPackaging(guessPackaging(projectModel));

        Pom modulePom = new Pom(modulePomCoords);
        modulePom.role = Pom.ModuleRole.NORMAL;
        modulePom.parent = mavCtx.getRootPom();
        mavCtx.getRootPom().submodules.put(artifactId, modulePom);
        mavCtx.getKnownSubmodules().add(modulePom);


        // Set up the dependency of the containing module on the contained module. E.g. EAR depends on WAR.
        if(containingModule != null)
            containingModule.dependencies.add(modulePom);

        // Nested archives
        // For now, only count with the modules of this app. There are likely more in the other apps.
        Set<ArchiveModel> nestedModules = new HashSet<>();
        for (FileModel file : projectModel.getFileModelsNoDirectories())
        {
            if(!(file instanceof ArchiveModel)) //  TODO: Query for ArchiveModel directly.
                continue;

            // Known library -> simple dependency.
            if(file instanceof IdentifiedArchiveModel){
                IdentifiedArchiveModel artifact = (IdentifiedArchiveModel) file;
                modulePom.dependencies.add(new SimpleDependency(Dependency.Role.LIBRARY, MavenCoord.from(artifact.getCoordinate())));
            }
            // Unknown archives -> nested modules? -> local dependencies.
            else {
                nestedModules.add((ArchiveModel) file);
            }
        }

        // Nested modules already identified as ProjectModel
        for (ProjectModel subProject : projectModel.getChildProjects())
        {
            Pom subModulePom = mavenizeModule(mavCtx, subProject, modulePom);
            if (subModulePom == null)
                continue;
            modulePom.dependencies.add(subModulePom);
        }

        // Nested module candidates.
        for (ArchiveModel nestedModule : nestedModules)
        {
            // TODO: Is it a submodule or a library? Does it appear in multiple applications?
            //Pom subModulePom = mavenizeModule(mavCtx, nestedModule, containingModule);
            //modulePom.dependencies.add(subModulePom.identification);
        }

        // TODO: MIGR-237 Determine and add the project internal dependencies (on other submodules, cross-app)
        // Queues? JNDI? CDI? REST + WS endpoints?
        // Needs to be done after initial mavenization of all apps to have their G:A:V.

        // TODO: Determine and add compile-time (API) dependencies (like, Java EE API's)
        // One big Java EE API vs. individual?

        // TODO: Remove the deps versions overriding the BOM.
        new FeatureBasedApiDependenciesDeducer(mavCtx).addAppropriateDependencies(projectModel, modulePom);

        return modulePom;
    }


    /**
     * Normalizes the name so it can be used as Maven artifactId or groupId.
     */
    private static String normalizeDirName(String name)
    {
        if(name == null)
            return null;
        return name.toLowerCase().replaceAll("[^a-zA-Z0-9]", "-");
    }

    /**
     * Tries to guess the packaging of the archive - whether it's an EAR, WAR, JAR.
     * Maybe not needed as we can rely on the suffix?
     */
    private static String guessPackaging(ProjectModel projectModel)
    {
        String projectType = projectModel.getProjectType();
        if (projectType != null)
            return projectType;

        LOG.warning("WINDUP-983 getProjectType() returned null for: " + projectModel.getRootFileModel().getPrettyPath());

        String suffix = StringUtils.substringAfterLast(projectModel.getRootFileModel().getFileName(), ".");
        if ("jar war ear sar har ".contains(suffix+" ")){
            projectModel.setProjectType(suffix); // FIXME: Remove when WINDUP-983 is fixed.
            return suffix;
        }

        // Should we try something more? Used APIs? What if it's a source?

        return "unknown";
    }


    String deriveAppropriateArtifactId(ProjectModel projectModel)
    {
        String resultName = null;
        String name = projectModel.getName();
        name: {
            if (name == null)
                break name;
            if (name.length() > 40)
                break name;
            if (name.contains(" "))
                break name;
            resultName = name;
        }

        if (resultName == null)
        {
            resultName = projectModel.getRootFileModel().getFileName();
        }

        resultName = removeVersion(resultName);
        resultName = normalizeDirName(resultName);

        if (resultName == null)
            resultName = "unknownName-" + RandomStringUtils.randomAlphanumeric(4);

        // See WINDUP-1015
        if (resultName.length() > 40)
            resultName = resultName.substring(0,40) + "-" + RandomStringUtils.randomAlphanumeric(4);


        return resultName;
    }


    /**
     * Remove 1.0.0 from foo-1.0.0.jar
     */
    String removeVersion(String resultName)
    {
        if(resultName == null)
            return null;

        // Regex test at http://fiddle.re/dnwhca
        return resultName.replaceFirst("[-_]\\d+(\\.\\d+)+(-SNAPSHOT|[.-_](?i:CR|RC|GA|Alpha|Beta|b|milestone|m|Final|RELEASE)[.-_]?\\d+|-incubating)?(?=[-_.]\\w+)", "");
    }


    /**
     * Sorts the submodules of given Pom so that their cross-dependencies are satisfied if built in that order.
     * TODO - MIGR-236.
     */
    private OrderedMap<String, Pom> sortSubmodulesToReflectDependencies(Pom pom)
    {
        Set<MavenCoord> dependenciesMet = new HashSet();
        dependenciesMet.add(pom.coord);

        SortedSet<MavenCoord> dependenciesSatisfied = new TreeSet<>();

        for (Dependency dep : pom.dependencies)
        {
            // Traverse the tree, depth-first, take items at node exit.
        }

        throw new UnsupportedOperationException("Not implemented yet.");
    }


    /**
     * Context of the mavenization - things to carry around.
     */
    static class MavenizationContext
    {
        private Path mavenizedBaseDir;
        private Pom rootPom;
        private Set<Pom> knownSubmodules = new HashSet<>();
        private String unifiedVersion;
        private String unifiedGroupId;
        private String unifiedAppName;
        private Pom bom; // BOM shared by all other submodules.
        private GraphContext graphContext;
        private Pom rootAppPom;


        public Path getMavenizedBaseDir() {
            return mavenizedBaseDir;
        }

        public Pom getRootPom() {
            return rootPom;
        }

        public Set<Pom> getKnownSubmodules() {
            return knownSubmodules;
        }

        public String getUnifiedVersion() {
            return unifiedVersion;
        }

        public String getUnifiedGroupId() {
            return unifiedGroupId;
        }

        public String getUnifiedAppName() {
            return unifiedAppName;
        }

        public Pom getBom() {
            return bom;
        }

        public GraphContext getGraphContext() {
            return graphContext;
        }

        public Pom getRootAppPom()
        {
            return rootAppPom;
        }

        public void setRootAppPom(Pom rootAppPom)
        {
            this.rootAppPom = rootAppPom;
        }
    }

    /**
     * Used to determine which BOM to take.
     * Currently we only have these tags: eap, eap7.
     */
    private Set<String> getTargetTechnologies()
    {
        WindupConfigurationModel wc = grCtx.getUnique(WindupConfigurationModel.class);
        Iterable<TechnologyReferenceModel> targetTechnologies = wc.getTargetTechnologies();
        Set<String> techs = new HashSet<>();
        for (TechnologyReferenceModel tech : targetTechnologies)
        {
            techs.add(tech.getTechnologyID());
        }
        return techs;
    }

}
