/*
 * Copyright 2012 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */



package org.gradle.buildinit.plugins.internal.maven

import groovy.util.slurpersupport.GPathResult
import org.apache.maven.project.MavenProject
import org.gradle.api.artifacts.ModuleIdentifier
import org.gradle.api.internal.artifacts.DefaultModuleIdentifier
import org.gradle.api.logging.Logger
import org.gradle.api.logging.Logging
import org.gradle.util.RelativePathUtil

/**
 * This script obtains the effective POM of the current project, reads its dependencies
 * and generates build.gradle scripts. It also generates settings.gradle for multimodule builds. <br/>
 *
 * It currently supports both single-module and multi-module POMs, inheritance, dependency management, properties - everything.
 */
class Maven2Gradle {

    def dependentWars = []
    def qualifiedNames
    def workingDir
    def effectivePom

    Logger logger = Logging.getLogger(getClass())
    private Set<MavenProject> mavenProjects

    Maven2Gradle(Set<MavenProject> mavenProjects, File workingDir) {
        assert !mavenProjects.empty: "No Maven projects provided."
        this.mavenProjects = mavenProjects
        this.workingDir = workingDir.canonicalFile;
    }

    def convert() {
        //For now we're building the effective POM XML from the model
        //and then we parse the XML using slurper.
        //This way we don't have to rewrite the Maven2Gradle just yet.
        //Maven2Gradle should be rewritten (with coverage) so that feeds of the maven object model, not XML.
        def effectivePom = new MavenProjectXmlWriter().toXml(mavenProjects)
        //use the Groovy XmlSlurper library to parse the text string
        this.effectivePom = new XmlSlurper().parseText(effectivePom)

        String build
        def multimodule = this.effectivePom.name() == "projects"

        if (multimodule) {
            def allProjects = this.effectivePom.project
            qualifiedNames = generateSettings(workingDir.getName(), allProjects[0].artifactId, allProjects);
            def dependencies = [:];
            allProjects.each { project ->
                dependencies[project.artifactId.text()] = getDependencies(project, allProjects)
            }

            def commonDeps = dependencies.get(allProjects[0].artifactId.text())
            build = """allprojects  {
  apply plugin: 'maven'

  ${getArtifactData(allProjects[0])}
}

subprojects {
  apply plugin: 'java'
  ${compilerSettings(allProjects[0], "  ")}
  ${packageSources(allProjects[0])}
  ${getRepositoriesForProjects(allProjects)}
  ${globalExclusions(allProjects[0])}
  ${commonDeps}
  ${testNg(commonDeps)}
}
"""
            modules(allProjects, false).each { module ->
                def id = module.artifactId.text()
                String moduleDependencies = dependencies.get(id)
                boolean warPack = module.packaging.text().equals("war")
                def hasDependencies = !(moduleDependencies == null || moduleDependencies.length() == 0)
                File submoduleBuildFile = new File(projectDir(module), 'build.gradle')

                def group = ''
                if (module.groupId != allProjects[0].groupId) {
                    group = "group = '${module.groupId}'"
                }
                String moduleBuild = "${group}\n"
                if (warPack) {
                    moduleBuild += """apply plugin: 'war'

"""
                    if (dependentWars.any { project ->
                        project.groupId.text() == module.groupId.text() &&
                                project.artifactId.text() == id
                    }) {
                        moduleBuild += """jar.enabled = true
"""
                    }
                }
                if (module.name) {
                    moduleBuild += "description = '${module.name}'\n"
                }


                if (hasDependencies) {
                    moduleBuild += moduleDependencies
                }

                moduleBuild += testNg(moduleDependencies)

                if (submoduleBuildFile.exists()) {
                    submoduleBuildFile.renameTo(new File(projectDir(module), "build.gradle.bak"))
                }
                def packageTests = packageTests(module);
                if (packageTests) {
                    moduleBuild += packageTests;
                }

                logger.debug("writing build.gradle file at ${submoduleBuildFile.absolutePath}");
                submoduleBuildFile.text = moduleBuild
            }
            //TODO deployment
        } else {//simple
            build = """apply plugin: 'java'
apply plugin: 'maven'

${getArtifactData(this.effectivePom)}

description = \"""${this.effectivePom.name}\"""

${compilerSettings(this.effectivePom, "")}
${globalExclusions(this.effectivePom)}

"""

            Set<String> repoSet = new LinkedHashSet<String>();
            getRepositoriesForModule(this.effectivePom, repoSet)
            String repos = """repositories {
        $localRepoUri
"""
            repoSet.each {
                repos = "${repos} ${it}\n"
            }
            build += "${repos}}\n"
            String dependencies = getDependencies(this.effectivePom, null)
            build += dependencies

            String packageTests = packageTests(this.effectivePom);
            if (packageTests) {
                build += '//packaging tests'
                build += packageTests;
            }
            generateSettings(workingDir.getName(), this.effectivePom.artifactId, null);
        }
        def buildFile = new File(workingDir, "build.gradle")
        if (buildFile.exists()) {
            buildFile.renameTo(new File(workingDir, "build.gradle.bak"))
        }
        logger.debug("writing build.gradle file at ${buildFile.absolutePath}");
        buildFile.text = build
    }

    def globalExclusions = { project ->
        def exclusions = ''
        def enforcerPlugin = plugin('maven-enforcer-plugin', project)
        def enforceGoal = pluginGoal('enforce', enforcerPlugin)
        if (enforceGoal) {
            exclusions += 'configurations.all {\n'
            enforceGoal.configuration.rules.bannedDependencies.excludes.childNodes().each {
                def tokens = it.text().tokenize(':')
                exclusions += "it.exclude group: '${tokens[0]}'"
                if (tokens.size() > 1 && tokens[1] != '*') {
                    exclusions += ", module: '${tokens[1]}'"
                }
                exclusions += '\n'
            }
        }
        exclusions = exclusions ? exclusions += '}' : exclusions
        exclusions
    }

    def testNg = { moduleDependencies ->
        if (moduleDependencies.contains('testng')) {
            """test.useTestNG()
"""
        } else {
            ''
        }
    }

    def modules = { allProjects, incReactors ->
        return allProjects.findAll { project ->
            def parentIsPartOfThisBuild = allProjects.find { proj ->
                proj.artifactId == project.parent.artifactId && proj.groupId == project.parent.groupId
            }
            project.parent.text().length() > 0 && parentIsPartOfThisBuild && (incReactors || project.packaging.text() != 'pom')
        }
    }

    def fqn = { project, allProjects ->
        def buffer = new StringBuilder()
        generateFqn(project, allProjects, buffer)
        return buffer.toString()
    }

    private generateFqn(GPathResult project, GPathResult allProjects, StringBuilder buffer) {
        def artifactId = project.artifactId.text()
        buffer.insert(0, ":${artifactId}")
        //we don't need the top-level parent in gradle, so we stop on it
        if (getModuleIdentifier(project.parent) != getModuleIdentifier(allProjects[0])) {
            def parentInBuild = allProjects.find { proj ->
                getModuleIdentifier(proj) == getModuleIdentifier(project.parent)
            }
            if (parentInBuild) {
                generateFqn(parentInBuild, allProjects, buffer)
            }
        }
    }

    private ModuleIdentifier getModuleIdentifier(GPathResult project) {
        def artifactId = project.artifactId.text()
        def groupId = project.groupId ? project.groupId.text() : project.parent.groupId.text()
        return new DefaultModuleIdentifier(groupId, artifactId)
    }

    def localRepoUri = {
        """mavenLocal()
    """
    }

    private String getArtifactData(project) {
        return """group = '$project.groupId'
version = '$project.version'""";
    }

    private String getRepositoriesForProjects(projects) {
        String repos = """repositories {
    ${localRepoUri()}
"""
        def repoSet = new LinkedHashSet<String>();
        projects.each {
            getRepositoriesForModule(it, repoSet)
        }
        repoSet.each {
            repos = "${repos}${it}\n"
        }
        repos = "${repos}  }\n"
        return repos
    }

    private void getRepositoriesForModule(module, repoSet) {
        module.repositories.repository.each {
            repoSet.add("    maven { url \"${it.url}\" }")
        }
        //No need to include plugin repos - who cares about maven plugins?
    }

    private String getDependencies(project, allProjects) {
        // use GPath to navigate the object hierarchy and retrieve the collection of dependency nodes.
        def dependencies = project.dependencies.dependency
        def war = project.packaging == "war"

        def compileTimeScope = []
        def runTimeScope = []
        def testScope = []
        def providedScope = []
        def systemScope = []

        //cleanup duplicates from parent
        // using Groovy Looping and mapping a Groovy Closure to each element, we collect together all
        // the dependency nodes into corresponding collections depending on their scope value.
        dependencies.each() {
            if (!duplicateDependency(it, project, allProjects)) {
                def scope = (elementHasText(it.scope)) ? it.scope : "compile"
                switch (scope) {
                    case "compile":
                        compileTimeScope.add(it)
                        break
                    case "test":
                        testScope.add(it)
                        break
                    case "provided":
                        providedScope.add(it)
                        break
                    case "runtime":
                        runTimeScope.add(it)
                        break
                    case "system":
                        systemScope.add(it)
                        break
                }
            }
        }

        /**
         * print function then checks the exclusions node to see if it exists, if
         * so it branches off, otherwise we call our simple print function
         */
        def createGradleDep = { String scope, StringBuilder sb, mavenDependency ->
            def projectDep = allProjects.find { prj ->
                return prj.artifactId.text() == mavenDependency.artifactId.text() && prj.groupId.text() == mavenDependency.groupId.text()
            }

            if (projectDep) {
                createProjectDependency(projectDep, sb, scope, allProjects)
            } else {
                if (!war && scope == 'providedCompile') {
                    scope = 'compileOnly'
                }
                def exclusions = mavenDependency.exclusions.exclusion
                if (exclusions.size() > 0) {
                    createComplexDependency(mavenDependency, sb, scope)
                } else {
                    createBasicDependency(mavenDependency, sb, scope)
                }
            }
        }


        StringBuilder build = new StringBuilder()
        if (!compileTimeScope.isEmpty() || !runTimeScope.isEmpty() || !testScope.isEmpty() || !providedScope.isEmpty() || !systemScope.isEmpty()) {
            build.append("dependencies {").append("\n")
// for each collection, one at a time, we take each element and call our print function
            if (!compileTimeScope.isEmpty()) {
                compileTimeScope.each() { createGradleDep("compile", build, it) }
            }
            if (!runTimeScope.isEmpty()) {
                runTimeScope.each() { createGradleDep("runtime", build, it) }
            }
            if (!testScope.isEmpty()) {
                testScope.each() { createGradleDep("testCompile", build, it) }
            }
            if (!providedScope.isEmpty()) {
                providedScope.each() { createGradleDep("providedCompile", build, it) }
            }
            if (!systemScope.isEmpty()) {
                systemScope.each() { createGradleDep("system", build, it) }
            }
            build.append("}\n")
        }
        return build.toString();
    }

    def compilerSettings = { project, indent ->
        def configuration = plugin('maven-compiler-plugin', project).configuration
        def settings = new StringBuilder()
        settings.append "sourceCompatibility = ${configuration.source.text() ?: '1.5'}\n"
        settings.append "${indent}targetCompatibility = ${configuration.target.text() ?: '1.5'}\n"
        def encoding = project.properties.'project.build.sourceEncoding'.text()
        if (encoding) {
            settings.append "${indent}tasks.withType(JavaCompile) {\n"
            settings.append "${indent}\toptions.encoding = '${encoding}'\n"
            settings.append "${indent}}\n"
        }
        return settings
    }

    def plugin = { artifactId, project ->
        project.build.plugins.plugin.find { pluginTag ->
            pluginTag.artifactId.text() == artifactId
        }
    }

    def pluginGoal = { goalName, plugin ->
        plugin.executions.execution.find { exec ->
            exec.goals.goal.find { gl ->
                gl.text().startsWith(goalName)
            }
        }
    }

    def packSources = { sourceSets ->
        def sourceSetStr = ''
        if (!sourceSets.empty) {
            sourceSetStr = """task packageSources(type: Jar) {
classifier = 'sources'
"""
            sourceSets.each { sourceSet ->
                sourceSetStr += """from sourceSets.${sourceSet}.allSource
"""
            }
            sourceSetStr += """
}
artifacts.archives packageSources"""
        }
        sourceSetStr
    }


    def packageTests = { project ->
        def jarPlugin = plugin('maven-jar-plugin', project)
        pluginGoal('test-jar', jarPlugin) ? """
task packageTests(type: Jar) {
  from sourceSets.test.output
  classifier = 'tests'
}
artifacts.archives packageTests
""" : ''
    }

    def packageSources = { project ->
        def sourcePlugin = plugin('maven-source-plugin', project)
        def sourceSets = []
        if (sourcePlugin) {
            if (pluginGoal('jar', sourcePlugin)) {
                sourceSets += 'main'
            } else if (pluginGoal('test-jar', sourcePlugin)) {
                sourceSets += 'test'
            }
        }
        packSources(sourceSets)
    }

    private boolean duplicateDependency(dependency, project, allProjects) {
        def parentTag = project.parent
        if (allProjects == null || parentTag.isEmpty()) {//simple project or no parent
            return false;
        } else {
            def parent = allProjects.find {
                it.groupId.equals(parentTag.groupId) && it.artifactId.equals(parentTag.artifactId)
            }
            def duplicate = parent.dependencies.dependency.any {
                it.groupId.equals(dependency.groupId) && it.artifactId.equals(dependency.artifactId)
            }
            if (duplicate) {
                return true;
            } else {
                duplicateDependency(dependency, parent, allProjects)
            }
        }
    }

    def artifactId = { File dir ->
        return new XmlSlurper().parse(new File(dir, 'pom.xml')).artifactId.text()
    }

    def projectDir = { project ->
        return new File(project.build.directory.text()).parentFile
    }

    private def generateSettings(def dirName, def mvnProjectName, def projects) {
        def qualifiedNames = [:]
        def projectName = "";
        if (dirName != mvnProjectName) {
            projectName = """rootProject.name = '${mvnProjectName}'
"""
        }
        def modulePoms = modules(projects, true)

        List<String> moduleNames = new ArrayList<String>();
        def artifactIdToDir = [:]
        if (projects) {
            modulePoms.each { project ->
                def fqn = fqn(project, projects)
                File projectDirectory = projectDir(project)
                // don't add project if it's the rootproject
                if (!workingDir.equals(projectDirectory)) {
                    artifactIdToDir[fqn] = RelativePathUtil.relativePath(workingDir, projectDirectory)
                    moduleNames.add(fqn)
                }
            }
        }
        File settingsFile = new File(workingDir, "settings.gradle")
        if (settingsFile.exists()) {
            settingsFile.renameTo(new File(workingDir, "settings.gradle.bak"))
        }
        StringBuffer settingsText = new StringBuffer(projectName)
        if (moduleNames.size() > 0) {
            moduleNames.each {
                settingsText.append("include '$it'\n")
            }
        }

        artifactIdToDir.each { entry ->
            settingsText.append("""
project('$entry.key').projectDir = """ + '"$rootDir/' + "${entry.value}" + '" as File')
        }
        settingsFile.text = settingsText.toString()
        return qualifiedNames
    }

/**
 * complex print statement does one extra task which is
 * iterate over each <exclusion> node and print out the artifact id.
 * It also provides review comments for the user.
 */
    private def createComplexDependency(it, build, scope) {
        build.append("    ${scope}(${contructSignature(it)}) {\n")
        it.exclusions.exclusion.each() {
            build.append("exclude(module: '${it.artifactId}')\n")
        }
        build.append("    }\n")
    }

/**
 * Print out the basic form og gradle dependency
 */
    private def createBasicDependency(mavenDependency, build, String scope) {
        def classifier = contructSignature(mavenDependency)
        build.append("    ${scope} ${classifier}\n")
    }
/**
 * Print out the basic form of gradle dependency
 */
    private def createProjectDependency(projectDep, build, String scope, allProjects) {
        if (projectDep.packaging.text() == 'war') {
            dependentWars += projectDep
        }
        build.append("  ${scope} project('${fqn(projectDep, allProjects)}')\n")
    }

/**
 * Construct and return the signature of a dependency, including its version and
 * classifier if it exists
 */
    private def contructSignature(mavenDependency) {
        def gradelDep = "group: '${mavenDependency.groupId.text()}', name: '${mavenDependency.artifactId.text()}', version:'${mavenDependency?.version?.text()}'"
        def classifier = elementHasText(mavenDependency.classifier) ? gradelDep + ", classifier:'" + mavenDependency.classifier.text().trim() + "'" : gradelDep
        return classifier
    }

/**
 * Check to see if the selected node has content
 */
    private boolean elementHasText(it) {
        return it.text().length() != 0
    }
}
