/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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
 *
 *    https://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.grails.cli.boot

import groovy.grape.Grape
import groovy.grape.GrapeEngine
import groovy.transform.CompileDynamic
import groovy.transform.CompileStatic
import groovy.xml.XmlSlurper
import groovy.xml.slurpersupport.GPathResult

import grails.util.Environment
import org.grails.cli.compiler.dependencies.Dependency
import org.grails.cli.compiler.dependencies.DependencyManagement

/**
 * Introduces dependency management based on a published BOM file
 *
 * @author Graeme Rocher
 * @since 3.0
 */
@CompileStatic
class GrailsDependencyVersions implements DependencyManagement {

    protected Map<String, Dependency> groupAndArtifactToDependency = [:]
    protected Map<String, String> artifactToGroupAndArtifact = [:]
    protected List<Dependency> dependencies = []
    protected Map<String, String> versionProperties = [:]
    private GrapeEngine grapeEngine

    GrailsDependencyVersions() {
        this(getDefaultEngine())
    }

    GrailsDependencyVersions(Map<String, String> bomCoords) {
        this(getDefaultEngine(), bomCoords)
    }

    GrailsDependencyVersions(GrapeEngine grape) {
        this(grape, [group: 'org.apache.grails', module: 'grails-bom', version: Environment.grailsVersion, type: 'pom'])
    }

    GrailsDependencyVersions(GrapeEngine grape, Map bomCoords) {
        this.grapeEngine = grape
        if (!bomCoords.containsKey('transitive')) {
            bomCoords.put('transitive', false)
        }
        def results = grape.resolve(null, bomCoords)

        for (URI u in results) {
            def pom = new XmlSlurper().parseText(u.toURL().text)
            addDependencyManagement(pom)
        }
    }

    static GrapeEngine getDefaultEngine() {
        def grape = Grape.getInstance()

        // Use apache repository with SNAPSHOTS when grailsVersion is not set or it ends in SNAPSHOT
        // otherwise use only mavenCentral
        if (!Environment.grailsVersion || Environment.grailsVersion.endsWith('SNAPSHOT')) {
            grape.addResolver([name: 'apacheRepository', root: 'https://repository.apache.org/content/groups/public'] as Map<String, Object>)
        }

        grape.addResolver([name: 'grailsCentral', root: 'https://repo.grails.org/grails/restricted'] as Map<String, Object>)

        grape
    }

    @CompileDynamic
    void addDependencyManagement(GPathResult pom) {
        versionProperties = pom.properties.'*'.collectEntries { [(it.name()): it.text()] }
        pom.dependencyManagement.dependencies.dependency.each { dep ->
            String groupId = dep.groupId.text()
            String artifactId = dep.artifactId.text()
            String version = versionLookup(dep.version.text())
            String scope = dep.scope.text()
            String type = dep.type.text()

            // Recursively resolve imported Grails BOMs (e.g. grails-base-bom) to pick up their managed dependencies.
            // Only follow Grails BOMs to avoid recursing into third-party BOMs like spring-boot-dependencies.
            if ((scope == 'import' && type == 'pom') && groupId == 'org.apache.grails') {
                resolveImportedBom(groupId, artifactId, version)
            } else if (scope != 'import') {
                addDependency(groupId, artifactId, version)
            }
        }
    }

    @CompileDynamic
    private void resolveImportedBom(String groupId, String artifactId, String version) {
        if (grapeEngine == null) {
            return
        }
        try {
            def results = grapeEngine.resolve(null, [group: groupId, module: artifactId, version: version, type: 'pom', transitive: false])
            for (URI u in results) {
                def importedPom = new XmlSlurper().parseText(u.toURL().text)
                addDependencyManagement(importedPom)
            }
        } catch (Exception e) {
            // If the imported BOM cannot be resolved, skip it
        }
    }

    /**
     * Handles properties version lookup in grails-bom
     *
     *   <properties>
     *    <ant.version>1.10.15</ant.version>
     *   </properties>
     *
     *   <dependencyManagement>
     *    <dependencies>
     *     <dependency>
     *      <groupId>org.apache.ant</groupId>
     *      <artifactId>ant</artifactId>
     *      <version>${ant.version}</version>
     *     </dependency>
     *    </dependencies>
     *   </dependencyManagement>
     *
     * @param version
     *            either the version or the version to lookup
     *
     * @return the version with lookup from properties when required
     */
    String versionLookup(String version) {
        version?.startsWith('${') && version?.endsWith('}') ?
                versionProperties[version[2..-2]] : version
    }

    protected void addDependency(String group, String artifactId, String version) {
        def groupAndArtifactId = "$group:$artifactId".toString()
        artifactToGroupAndArtifact[artifactId] = groupAndArtifactId

        def dep = new Dependency(group, artifactId, version)
        dependencies.add(dep)
        groupAndArtifactToDependency[groupAndArtifactId] = dep
    }

    Dependency find(String groupId, String artifactId) {
        return groupAndArtifactToDependency["$groupId:$artifactId".toString()]
    }

    @Override
    List<Dependency> getDependencies() {
        return dependencies
    }

    Map<String, String> getVersionProperties() {
        return versionProperties
    }

    @Override
    String getSpringBootVersion() {
        return find('spring-boot').getVersion()
    }

    @Override
    Dependency find(String artifactId) {
        def groupAndArtifact = artifactToGroupAndArtifact[artifactId]
        if (groupAndArtifact)
            return groupAndArtifactToDependency[groupAndArtifact]
    }

    Iterator<Dependency> iterator() {
        return groupAndArtifactToDependency.values().iterator()
    }
}
