/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the "Elastic License
 * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
 * Public License v 1"; you may not use this file except in compliance with, at
 * your election, the "Elastic License 2.0", the "GNU Affero General Public
 * License v3.0 only", or the "Server Side Public License, v 1".
 */

import org.elasticsearch.gradle.util.Pair
import org.elasticsearch.gradle.util.GradleUtils
import org.elasticsearch.gradle.internal.test.TestUtil
import org.elasticsearch.gradle.internal.idea.EnablePreviewFeaturesTask
import org.elasticsearch.gradle.internal.idea.IdeaXmlUtil
import org.jetbrains.gradle.ext.JUnit

import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardCopyOption

allprojects {
  apply plugin: 'idea'

  tasks.named('idea').configure {
    doFirst { throw new GradleException("Use of the 'idea' task has been deprecated. For details on importing into IntelliJ see CONTRIBUTING.md.") }
  }
}

interface Injected {
  @Inject FileSystemOperations getFs()
}

// Applying this stuff, particularly the idea-ext plugin, has a cost so avoid it unless we're running in the IDE
if (providers.systemProperty('idea.active').getOrNull() == 'true') {
  project.apply(plugin: org.jetbrains.gradle.ext.IdeaExtPlugin)

  def elasticsearchProject = locateElasticsearchWorkspace(gradle)

  def rootFolder = project.rootDir
  tasks.register('configureIdeCheckstyle') {
    group = 'ide'
    description = 'Generated a suitable checkstyle config for IDEs'

    String resources = "${elasticsearchProject.left()}/build-tools-internal/src/main/resources"
    String buildConventionsJar = "${elasticsearchProject.left()}/build-conventions/build/libs/build-conventions.jar"
    String checkstyleConfig = "${resources}/checkstyle.xml"
    String checkstyleSuppressions = "${resources}/checkstyle_suppressions.xml"
    String checkstyleIdeFragment = "${resources}/checkstyle_ide_fragment.xml"
    String checkstyleIdeConfig = "${rootFolder}/checkstyle_ide.xml"

    String checkstylePluginConfigTemplate = "${resources}/checkstyle-idea.xml"
    String checkstylePluginConfig = "${rootFolder}/.idea/checkstyle-idea.xml"

    inputs.files(
      file(checkstyleConfig),
      file(checkstyleIdeFragment),
      file(checkstylePluginConfigTemplate)
    )
    outputs.files(
      file(checkstyleIdeConfig),
      file(checkstylePluginConfig)
    )
    def injected = project.objects.newInstance(Injected)

    def projectFolder = project.layout.projectDirectory.asFile
    doLast {
      // Configure the IntelliJ Checkstyle plugin by copying a standard file. We don't simply commit
      // the result to version control, because the plugin has a habit of modifying the file and
      // replacing the `$PROJECT_DIR$` placeholders, which developers must then revert.
      injected.fs.copy {
        from(checkstylePluginConfigTemplate)
        into("${rootFolder}/.idea")
        expand(jarLocation: buildConventionsJar, configLocation: checkstyleIdeConfig)
      }

      // Create an IDE-specific checkstyle config by first copying the standard config
      Files.copy(
        Paths.get(new File(checkstyleConfig).getPath()),
        Paths.get(new File(checkstyleIdeConfig).getPath()),
        StandardCopyOption.REPLACE_EXISTING
      )

      // There are some rules that we only want to enable in an IDE. These
      // are extracted to a separate file, and merged into the IDE-specific
      // Checkstyle config.
      Node xmlFragment = IdeaXmlUtil.parseXml(checkstyleIdeFragment)

      // Edit the copy so that IntelliJ can copy with it
      IdeaXmlUtil.modifyXml(checkstyleIdeConfig, { xml ->
        // Add all the nodes from the fragment file
        Node treeWalker = xml.module.find { it.'@name' == 'TreeWalker' }
        xmlFragment.module.each { treeWalker.append(it) }

        // Change the checkstyle config to inline the path to the
        // suppressions config. This removes a configuration step when using
        // the checkstyle config in an IDE.
        Node suppressions = xml.module.find { it.'@name' == 'SuppressionFilter' }
        suppressions.property.findAll { it.'@name' == 'file' }.each { it.'@value' = checkstyleSuppressions }
      },
        "<!DOCTYPE module PUBLIC\n" +
          "  \"-//Puppy Crawl//DTD Check Configuration 1.3//EN\"\n" +
          "  \"http://www.puppycrawl.com/dtds/configuration_1_3.dtd\">\n" +
          "<!-- Generated automatically from the following - do not edit this file directly. -->\n" +
          "<!--     ${checkstyleConfig} -->\n" +
          "<!--     ${checkstyleIdeFragment} -->\n"
      )
    }
  }

  tasks.register('configureIdeaGradleJvm') {
    group = 'ide'
    description = 'Configures the appropriate JVM for Gradle'

    doLast {
      IdeaXmlUtil.modifyXml('.idea/gradle.xml') { xml ->
        def gradleSettings = xml.component.find { it.'@name' == 'GradleSettings' }.option[0].GradleProjectSettings
        // Remove configured JVM option to force IntelliJ to use the project JDK for Gradle
        gradleSettings.option.findAll { it.'@name' == 'gradleJvm' }.each { it.parent().remove(it) }
      }
    }
  }

  // aggregate task so dependency artifacts below can can use one task name
  tasks.register("generateProviderImpls").configure {
    group = 'ide'
    description = 'Builds all embedded provider impls'

    dependsOn subprojects
      .collect { GradleUtils.findByName(it.tasks, 'generateProviderImpls') }
      .findAll { it != null }
  }

  // force IntelliJ to generate *.iml files for each imported module
  tasks.register("enableExternalConfiguration") {
    group = 'ide'
    description = 'Enable per-module *.iml files'

    doLast {
      IdeaXmlUtil.modifyXml('.idea/misc.xml') {xml ->
        def externalStorageConfig = xml.component.find { it.'@name' == 'ExternalStorageConfigurationManager' }
        if (externalStorageConfig) {
          xml.remove(externalStorageConfig)
        }
      }
    }
  }

  // modifies the idea module config to enable preview features on module that need them
  tasks.register("enablePreviewFeatures", EnablePreviewFeaturesTask) {
    group = 'ide'
    description = 'Enables preview features on modules that need them'
    dependsOn tasks.named("enableExternalConfiguration")
    doLast {
      enablePreview('.idea/modules/libs/native/elasticsearch.libs.native.main.iml', 'JDK_21_PREVIEW')
      enablePreview('.idea/modules/libs/native/elasticsearch.libs.native.test.iml', 'JDK_21_PREVIEW')
      // due to org.elasticsearch.plugins.PluginsLoader
      enablePreview('.idea/modules/server/elasticsearch.server.main.iml', 'JDK_21_PREVIEW')
      enablePreview('.idea/modules/server/elasticsearch.server.test.iml', 'JDK_21_PREVIEW')
      enablePreview('.idea/modules/libs/entitlement/elasticsearch.libs.entitlement.main.iml', 'JDK_21_PREVIEW')
      enablePreview('.idea/modules/libs/entitlement/elasticsearch.libs.entitlement.test.iml', 'JDK_21_PREVIEW')
      enablePreview('.idea/modules/libs/entitlement/bridge/elasticsearch.libs.entitlement.bridge.main.iml', 'JDK_21_PREVIEW')
      enablePreview('.idea/modules/libs/entitlement/bridge/elasticsearch.libs.entitlement.bridge.test.iml', 'JDK_21_PREVIEW')
      enablePreview('.idea/modules/libs/entitlement/qa/entitlement-test-plugin/elasticsearch.libs.entitlement.qa.entitlement-test-plugin.main.iml', 'JDK_21_PREVIEW')
      enablePreview('.idea/modules/libs/entitlement/qa/entitlement-test-plugin/elasticsearch.libs.entitlement.qa.entitlement-test-plugin.test.iml', 'JDK_21_PREVIEW')
    }
  }

  tasks.register('buildDependencyArtifacts') {
    group = 'ide'
    description = 'Builds artifacts needed as dependency for IDE modules'
    dependsOn([':x-pack:plugin:esql:compute:ann:jar',
      ':x-pack:plugin:esql:compute:gen:jar',
      ':server:generateModulesList',
      ':server:generatePluginsList',
      ':generateProviderImpls',
      ':libs:native:native-libraries:extractLibs',
      ':x-pack:libs:es-opensaml-security-api:shadowJar'].collect { elasticsearchProject.right()?.task(it) ?: it })
  }

  // this path is produced by the extractLibs task above
  String testLibraryPath = TestUtil.getTestLibraryPath("${elasticsearchProject.left()}/libs/native/libraries/build/platform")
  def enableIdeaCC = providers.gradleProperty("org.elasticsearch.idea-configuration-cache").getOrElse("true").toBoolean()
  def delegateToGradle = providers.gradleProperty("org.elasticsearch.idea-delegate-to-gradle").getOrElse("false").toBoolean()
  idea {
    project {
      vcs = 'Git'
      jdkName = buildParams.minimumCompilerVersion.majorVersion

      settings {
        delegateActions {
          delegateBuildRunToGradle = false
          testRunner = delegateToGradle ? 'gradle' : 'choose_per_test'
        }
        taskTriggers {
          afterSync tasks.named('configureIdeCheckstyle'),
            tasks.named('configureIdeaGradleJvm'),
            tasks.named('buildDependencyArtifacts'),
            tasks.named('enablePreviewFeatures')
        }
        encodings {
          encoding = 'UTF-8'
        }
        compiler {
          parallelCompilation = true
          processHeapSize = 2048
          addNotNullAssertions = false
          javac {
            generateDeprecationWarnings = false
            preferTargetJDKCompiler = false
          }
        }
        runConfigurations {
          defaults(org.jetbrains.gradle.ext.Gradle) {
            scriptParameters = enableIdeaCC ? [
              '--configuration-cache'
            ].join(' ') : ''
          }
          defaults(JUnit) {
            vmParameters = [
              '-ea',
              '-Djava.security.manager=allow',
              '-Djava.locale.providers=CLDR',
              '-Dtests.testfeatures.enabled=true',
              '-Des.nativelibs.path="' + testLibraryPath + '"',
              // TODO: only open these for mockito when it is modularized
              '--add-opens=java.base/java.security.cert=ALL-UNNAMED',
              '--add-opens=java.base/java.nio.channels=ALL-UNNAMED',
              '--add-opens=java.base/java.io=ALL-UNNAMED',
              '--add-opens=java.base/java.net=ALL-UNNAMED',
              '--add-opens=java.base/javax.net.ssl=ALL-UNNAMED',
              '--add-opens=java.base/java.nio.file=ALL-UNNAMED',
              '--add-opens=java.base/java.time=ALL-UNNAMED',
              '--add-opens=java.base/java.lang=ALL-UNNAMED',
              '--add-opens=java.management/java.lang.management=ALL-UNNAMED'
            ].join(' ')
          }
        }
        copyright {
          useDefault = 'Default'
          scopes = ['x-pack': 'Elastic', 'llrc': 'Apache2']
          profiles {
            Default {
              keyword = 'GNU Affero General Public License v3.0'
              notice = '''\
                Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
                or more contributor license agreements. Licensed under the "Elastic License
                2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
                Public License v 1"; you may not use this file except in compliance with, at
                your election, the "Elastic License 2.0", the "GNU Affero General Public
                License v3.0 only", or the "Server Side Public License, v 1".'''.stripIndent()
            }
            Elastic {
              keyword = '2.0; you may not use this file except in compliance with the Elastic License'
              notice = '''\
                Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
                or more contributor license agreements. Licensed under the Elastic License
                2.0; you may not use this file except in compliance with the Elastic License
                2.0.'''.stripIndent()
            }
            Apache2 {
              keyword = 'Licensed to Elasticsearch B.V. under one or more contributor'
              notice = '''\
                Licensed to Elasticsearch B.V. under one or more contributor
                license agreements. See the NOTICE file distributed with
                this work for additional information regarding copyright
                ownership. Elasticsearch B.V. 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

                    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.'''.stripIndent()
            }
          }
        }
      }
    }
  }
}

Pair<File, IncludedBuild> locateElasticsearchWorkspace(Gradle gradle) {
  if (gradle.parent == null) {
    // See if any of these included builds is the Elasticsearch gradle
    for (IncludedBuild includedBuild : gradle.includedBuilds) {
      File versionProperties = new File(includedBuild.projectDir, 'build-tools-internal/version.properties')
      if (versionProperties.exists()) {
        return Pair.of(includedBuild.projectDir, includedBuild)
      }
    }

    // Otherwise assume this gradle is the root elasticsearch workspace
    return Pair.of(gradle.getRootProject().getRootDir(), null)
  } else {
    // We're an included build, so keep looking
    return locateElasticsearchWorkspace(gradle.parent)
  }
}
