/*

This Gradle init script adds a `jsonDeps` task that outputs the dependencies
of each subproject as JSON.

If you're debugging this script, you can directly run this on a Gradle project
by running `gradle -I /path/to/script $TASK` e.g.
    `gradle -I /tmp/jsondeps.gradle :jsonDeps`. This lets you see the output directly.

Useful documentation:
- Gradle init scripts: https://docs.gradle.org/current/userguide/init_scripts.html
- Gradle subprojects: https://docs.gradle.org/current/userguide/multi_project_builds.html
- Gradle configurations: https://docs.gradle.org/current/userguide/declaring_dependencies.html
- Gradle build script primer: https://docs.gradle.org/current/userguide/groovy_build_script_primer.html
- Gradle init script API reference:
  - https://docs.gradle.org/current/dsl/org.gradle.api.Project.html#org.gradle.api.Project:allprojects(groovy.lang.Closure)
  - https://docs.gradle.org/current/javadoc/index.html

----

There are two form of generated JSON: (1) from resolution API.


## For (1) Resolution API:
-------------------------

    The resulting JSON output is a list of all configuration with adjacency map of dependencies.

    For a project resulting json is:
    ```
        {
            resolvedProjectName: ..
            resolvedProjectConfigurations: [
                {
                    resolvedConfigurationName: ...,
                    resolvedConfigurationDirectComponents: [
                        { "type": "project", "name": ":project-name" },
                        ....
                    ],
                    resolvedConfigurationDependencies: [
                        {
                            "resolvedComponentNode": { "type": "project", "name": ":project-name" }
                            "resolvedComponentOutgoing": [
                                { "type": "package", "name": "group:module", "version": "1.0" },
                                ...
                            ]
                        }
                    ]
                }
            ]
        }
    ```
*/

allprojects {
    tasks.register("jsonDeps") {
        doLast {

            def printTrace = { message, scope ->
                println "TRACE (${scope}): ${message}"
            }

            // These will be included as debug logs in CLI!
            def printDebugToFossa = { message, scope ->
                println "FOSSA-DEBUG (${scope}): ${message}"
            }

            // Uses configuration resolution api to serialize adjacency map of dependencies to json.
            // This is recommended approach to infer resolved graph.
            //
            // See:
            //  - https://github.com/gradle/gradle/issues/5953#issuecomment-404514591
            //  -
            //
            // Reference:
            // - https://docs.gradle.org/current/javadoc/org/gradle/api/artifacts/ResolvableDependencies.html
            // - https://docs.gradle.org/current/javadoc/org/gradle/api/artifacts/Configuration.html#getIncoming--
            def resolvedConfigToJSON = { resolvedConfig ->
                def getComponentName = { component ->
                    if (component instanceof ProjectComponentIdentifier) { return "${component.projectPath}"}
                    if (component instanceof ModuleComponentIdentifier) { return "${component.group}:${component.module}"}
                    printTrace("Component is not project or module: ${component}", "resolvedConfigToJSON")
                    return null
                }

                def componentToJson = { component ->
                    if (component instanceof ProjectComponentIdentifier) { return "{\"type\":\"project\",\"name\":\"${getComponentName(component)}\"}"}
                    if (component instanceof ModuleComponentIdentifier) { return "{\"type\":\"package\",\"name\":\"${getComponentName(component)}\",\"version\":\"${component.version}\"}"}
                    printTrace("Component is not project or module: ${component}", "resolvedConfigToJSON")
                    return null
                }

                def resolutionResult = resolvedConfig.incoming.resolutionResult
                def adjacencyMap = [:]
                //noinspection GroovyUnusedAssignment
                def directComponents = []

                // Refs:
                // -----
                // resolutionResult: https://docs.gradle.org/current/javadoc/org/gradle/api/artifacts/ResolvableDependencies.html#getResolutionResult--
                // getAllComponents(): https://docs.gradle.org/current/javadoc/org/gradle/api/artifacts/result/ResolutionResult.html#getAllComponents--
                // resolvedComponent: https://docs.gradle.org/current/javadoc/org/gradle/api/artifacts/result/ComponentResult.html
                // getDependencies(): https://docs.gradle.org/current/javadoc/org/gradle/api/artifacts/result/ResolvedComponentResult.html#getDependencies--
                // resolvedDep: https://docs.gradle.org/current/javadoc/org/gradle/api/artifacts/result/DependencyResult.html
                // -
                resolvedConfig.incoming.resolutionResult.getAllComponents().each { resolvedComponent ->
                    if (resolvedComponent instanceof UnresolvedComponentResult) {
                        printDebugToFossa("Could not resolve component: ${resolvedComponent.getAttempted()}", "resolvedConfigToJSON")
                        return
                    }

                    resolvedComponent.getDependencies().each { resolvedDep ->
                        if (resolvedDep instanceof UnresolvedDependencyResult) {
                            printDebugToFossa("Could not resolve dependency: ${resolvedDep.getAttempted()}", "resolvedConfigToJSON")
                            return
                        }
                        def resolvedDependencyId = resolvedDep.getSelected().getId()
                        if (resolvedDep.isConstraint()) {
                            printDebugToFossa("Skipping constraint dependency: ${resolvedDependencyId}", "resolvedConfigToJSON")
                            return
                        }
                        if (resolvedDependencyId != resolvedComponent.getId()) {
                            adjacencyMap.get(resolvedComponent, []) << resolvedDependencyId
                        }
                    }
                }

                // Sort for reproducibility - ordering matches that of,
                // official gradle scan plugin dependencies results
                directComponents = adjacencyMap.get(resolutionResult.getRoot(), [])
                directComponents.sort { getComponentName(it) }

                def json = "{"
                json += "\"resolvedConfigurationName\": \"${resolvedConfig.getName()}\","
                json += "\"resolvedConfigurationDirectComponents\": [${directComponents.collect() { componentToJson(it) }.join(',')}],"
                json += "\"resolvedConfigurationDependencies\": ["

                adjacencyMap.eachWithIndex { node, listOfConnectedNodes, i ->
                    def connectedNodesJson = listOfConnectedNodes.sort { getComponentName(it) }.collect() { componentToJson(it) }.join(",")
                    json += "{"
                    json += "\"resolvedComponentNode\": ${componentToJson(node.getId())},"
                    json += "\"resolvedComponentOutgoing\": [${connectedNodesJson}]"
                    json += "}"
                    if (i < adjacencyMap.size() - 1 ) {
                        json += ","
                    }
                }
                json += "]"
                json += "}"
                return json
            }

            def projectToJsonWithResolutionApi = { project ->
                def jsonConfigs = []
                // project.configurations returns a `ConfigurationContainer`, which iteself implements the `DomainObjectCollection` interface.
                // This interface provides two additional ways of configuring elements in the collection in addition to Groovy's `each`:
                // `all`, a method that eagerly executes the provided `Action` or `Closure` against the current elements of the collection
                // **and** any subsequent additions
                // and `configureEach`, a method that lazily executes the provied `Closure` against the current elements of the collection
                // and anay additions on an as-needed basis.
                //
                // While theoretically the use of `each` should produce similar results to the use of `all` or `configureEach`, that cannot be
                // guaranteed as other plugins or even gradle build authors also have the ability to access and modify the configurations during
                // the Gradle configuration phase (eg, via dependency substitution). This is a particular risk for this script plugin, as it is
                // highly likely that the jsonDepstask will be configured early in the configuration phase (because it is added as an init
                // script). The solution here is to use `all` or `configureEach` for enhanced laziness; `all` seems most prudent given the
                // functionaliy that this task attempts to achieve.
                //
                // links:
                // ConfiguationContainer: https://docs.gradle.org/current/javadoc/org/gradle/api/artifacts/ConfigurationContainer.html
                // DomainObjectCollection: https://docs.gradle.org/current/javadoc/org/gradle/api/DomainObjectCollection.html
                project.configurations.all { config ->
                    //noinspection GroovyUnusedAssignment
                    def result = null

                    try {
                        // If we are in gradle v3.3 or greater (isCanBeResolved method should exists)
                        // And if configuration is not resolvable, disregard current config for dependency resolution.
                        if (config.respondsTo("isCanBeResolved") && !config.isCanBeResolved()) {
                            printDebugToFossa("Configuration is not resolvable: ${config}!", "projectToJsonWithResolutionApi")
                            return null
                        }
                        // If we are in grade v6.0 we need to check `isFullyDeprecated` to ensure we ignore deprecated
                        // configurations like `runtime`.
                        if (config.respondsTo("isFullyDeprecated") && config.isFullyDeprecated()) {
                            printDebugToFossa("Configuration is deprecated: ${config}", "projectToJsonWithResolutionApi")
                            return null
                        }
                        result = resolvedConfigToJSON (config)
                        jsonConfigs << result
                    } catch (Exception ignored) {
                        printDebugToFossa("${ignored}", "projectToJsonWithResolutionApi")
                    }
                }
                return "{ \"resolvedProjectName\": \"${project.path}\", \"resolvedProjectConfigurations\": [${jsonConfigs.join(",")}]}"
            }


            def resultWithResolutionApi = projectToJsonWithResolutionApi project

            // We use the "RESOLUTIONAPI_JSONDEPS_*" to print output. This is why it's
            // safe for us to print a bunch of other debugging messages
            // everywhere else - the parser in Spectrometer ignores those
            // messages.
            println "RESOLUTIONAPI_JSONDEPS_${project.path}_${resultWithResolutionApi}"
        }
    }
}
