Create FUS Gradle plugin

fus-statistics-gradle-plugin can be used by any other Gradle plugins to
collect additional metrics for FUS.

KT-59627
diff --git a/libraries/tools/gradle/fus-statistics-gradle-plugin/ReadMe.md b/libraries/tools/gradle/fus-statistics-gradle-plugin/ReadMe.md
new file mode 100644
index 0000000..5537fd1
--- /dev/null
+++ b/libraries/tools/gradle/fus-statistics-gradle-plugin/ReadMe.md
@@ -0,0 +1,4 @@
+## Description
+
+Contains a plugin for FUS statistics. fus-statistics-gradle-plugin can be used by other Gradle plugins to
+collect additional metrics for FUS.  
diff --git a/libraries/tools/gradle/fus-statistics-gradle-plugin/build.gradle.kts b/libraries/tools/gradle/fus-statistics-gradle-plugin/build.gradle.kts
new file mode 100644
index 0000000..1f93955
--- /dev/null
+++ b/libraries/tools/gradle/fus-statistics-gradle-plugin/build.gradle.kts
@@ -0,0 +1,22 @@
+plugins {
+    id("gradle-plugin-common-configuration")
+}
+
+
+dependencies {
+    commonApi(project(":kotlin-gradle-plugin-api"))
+    commonApi(project(":kotlin-gradle-plugin"))
+    commonCompileOnly(gradleKotlinDsl())
+}
+
+
+gradlePlugin {
+    plugins {
+        create("fus-statistics-gradle-plugin") {
+            id = "org.jetbrains.kotlin.fus-statistics-gradle-plugin"
+            displayName = "FusStatisticsPlugin"
+            description = displayName
+            implementationClass = "org.jetbrains.kotlin.gradle.fus.FusStatisticsPlugin"
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/tools/gradle/fus-statistics-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/fus/FusStatisticsPlugin.kt b/libraries/tools/gradle/fus-statistics-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/fus/FusStatisticsPlugin.kt
new file mode 100644
index 0000000..de90880
--- /dev/null
+++ b/libraries/tools/gradle/fus-statistics-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/fus/FusStatisticsPlugin.kt
@@ -0,0 +1,19 @@
+package org.jetbrains.kotlin.gradle.fus
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.provider.ProviderFactory
+import javax.inject.Inject
+
+/*
+ * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+
+class FusStatisticsPlugin @Inject constructor(
+    private val providerFactory: ProviderFactory
+) : Plugin<Project> {
+    override fun apply(project: Project) {
+        GradleBuildFusStatisticsService.registerIfAbsent(project).get()
+    }
+}
\ No newline at end of file
diff --git a/libraries/tools/gradle/fus-statistics-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/fus/GradleBuildFusStatistics.kt b/libraries/tools/gradle/fus-statistics-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/fus/GradleBuildFusStatistics.kt
new file mode 100644
index 0000000..1612f64
--- /dev/null
+++ b/libraries/tools/gradle/fus-statistics-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/fus/GradleBuildFusStatistics.kt
@@ -0,0 +1,9 @@
+/*
+ * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors.
+ * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
+ */
+package org.jetbrains.kotlin.gradle.fus
+interface GradleBuildFusStatistics {
+    fun reportMetric(name: String, value: Any, subprojectName: String? = null)
+
+}
\ No newline at end of file
diff --git a/libraries/tools/gradle/fus-statistics-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/fus/GradleBuildFusStatisticsService.kt b/libraries/tools/gradle/fus-statistics-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/fus/GradleBuildFusStatisticsService.kt
new file mode 100644
index 0000000..6f0689d
--- /dev/null
+++ b/libraries/tools/gradle/fus-statistics-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/fus/GradleBuildFusStatisticsService.kt
@@ -0,0 +1,114 @@
+package org.jetbrains.kotlin.gradle.fus
+
+
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.api.provider.Property
+import org.gradle.api.provider.Provider
+import org.gradle.api.services.BuildService
+import org.gradle.api.services.BuildServiceParameters
+import org.gradle.api.tasks.Internal
+import org.gradle.kotlin.dsl.withType
+import java.io.File
+import java.util.UUID
+
+
+interface UsesGradleBuildFusStatisticsService : Task {
+    @get:Internal
+    val fusStatisticsBuildService: Property<GradleBuildFusStatistics?>
+}
+
+internal abstract class GradleBuildFusStatisticsService : GradleBuildFusStatistics,
+    BuildService<GradleBuildFusStatisticsService.Parameters>, AutoCloseable {
+
+    interface Parameters : BuildServiceParameters {
+        val path: Property<String>
+        val uuid: Property<String>
+    }
+
+    private val metrics = HashMap<Metric, Any>()
+
+    override fun close() {
+        val reportFile = File(parameters.path.get())
+            .resolve(STATISTICS_FOLDER_NAME)
+            .also { it.mkdirs() }
+            .resolve(parameters.uuid.get())
+        reportFile.createNewFile()
+
+        for ((metric, value) in metrics) {
+            reportFile.appendText("$metric=$value\n")
+        }
+
+        reportFile.appendText(BUILD_SESSION_SEPARATOR)
+    }
+
+    override fun reportMetric(name: String, value: Any, subprojectName: String?) {
+        metrics[Metric(name, subprojectName)] = value
+    }
+
+    companion object {
+        private const val FUS_STATISTICS_PATH = "kotlin.fus.statistics.path"
+        private const val STATISTICS_FOLDER_NAME = "kotlin-fus"
+
+        private const val BUILD_SESSION_SEPARATOR = "BUILD FINISHED"
+
+        private var statisticsIsEnabled: Boolean = true //KT-59629 Wait for user confirmation before start to collect metrics
+        private val serviceClass = GradleBuildFusStatisticsService::class.java
+        private val serviceName = "${serviceClass.name}_${serviceClass.classLoader.hashCode()}"
+
+        fun registerIfAbsent(project: Project): Provider<GradleBuildFusStatisticsService> {
+            project.gradle.sharedServices.registrations.findByName(serviceName)?.let {
+                @Suppress("UNCHECKED_CAST")
+                return it.service as Provider<GradleBuildFusStatisticsService>
+            }
+
+            return if (statisticsIsEnabled) {
+                project.gradle.sharedServices.registerIfAbsent(serviceName, serviceClass) {
+                    val customPath: String = if (project.rootProject.hasProperty(FUS_STATISTICS_PATH)) {
+                        project.rootProject.property(FUS_STATISTICS_PATH) as String
+                    } else {
+                        project.gradle.gradleUserHomeDir.path //fix
+                    }
+                    it.parameters.path.set(customPath)
+                    it.parameters.uuid.set(UUID.randomUUID().toString())
+                }
+            } else {
+                project.gradle.sharedServices.registerIfAbsent(serviceName, DummyGradleBuildFusStatisticsService::class.java) {}
+                    .map { it as GradleBuildFusStatisticsService }
+            }.also { configureTasks(project, it) }
+        }
+
+        private fun configureTasks(project: Project, serviceProvider: Provider<GradleBuildFusStatisticsService>) {
+            project.tasks.withType<UsesGradleBuildFusStatisticsService>().configureEach { task ->
+                task.fusStatisticsBuildService.value(serviceProvider).disallowChanges()
+                task.usesService(serviceProvider)
+            }
+        }
+    }
+}
+
+internal abstract class DummyGradleBuildFusStatisticsService : GradleBuildFusStatisticsService() {
+    override fun reportMetric(name: String, value: Any, subprojectName: String?) {
+        //do nothing
+    }
+
+    override fun close() {
+        //do nothing
+    }
+}
+
+data class Metric(val name: String, val projectHash: String?) : Comparable<Metric> {
+    override fun compareTo(other: Metric): Int {
+        val compareNames = name.compareTo(other.name)
+        return when {
+            compareNames != 0 -> compareNames
+            projectHash == other.projectHash -> 0
+            else -> (projectHash ?: "").compareTo(other.projectHash ?: "")
+        }
+    }
+
+    override fun toString(): String {
+        val suffix = if (projectHash == null) "" else ".${projectHash}"
+        return name + suffix
+    }
+}
\ No newline at end of file
diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BuildFusStatisticsIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BuildFusStatisticsIT.kt
index 9b2edd0..8c72375 100644
--- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BuildFusStatisticsIT.kt
+++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BuildFusStatisticsIT.kt
@@ -8,7 +8,11 @@
 import org.gradle.api.logging.LogLevel
 import org.gradle.util.GradleVersion
 import org.jetbrains.kotlin.gradle.testbase.*
+import org.jetbrains.kotlin.test.KtAssert.assertEquals
 import org.junit.jupiter.api.DisplayName
+import kotlin.io.path.name
+import kotlin.io.path.pathString
+import kotlin.io.path.readText
 
 @DisplayName("Build FUS statistics")
 class BuildFusStatisticsIT : KGPDaemonsBaseTest() {
@@ -37,4 +41,50 @@
         }
     }
 
+    @DisplayName("smoke test for fus-statistics-gradle-plugin")
+    @GradleTest
+    fun smokeTestForFusStatisticsPlugin(gradleVersion: GradleVersion) {
+        val metricName = "METRIC_NAME"
+        val metricValue = 1
+        project("simpleProject", gradleVersion) {
+            buildGradle.modify {
+                """
+                ${
+                    it.replace(
+                        "plugins {",
+                        """
+                               plugins {
+                                  id "org.jetbrains.kotlin.fus-statistics-gradle-plugin" version "${'$'}kotlin_version"
+                           """.trimIndent()
+                    )
+                }
+                
+                import org.jetbrains.kotlin.gradle.fus.GradleBuildFusStatistics
+                class TestFusTask extends DefaultTask implements org.jetbrains.kotlin.gradle.fus.UsesGradleBuildFusStatisticsService {
+                  private Property<GradleBuildFusStatistics> fusStatisticsBuildService = project.objects.property(GradleBuildFusStatistics.class)
+
+                  org.gradle.api.provider.Property getFusStatisticsBuildService(){
+                    return fusStatisticsBuildService
+                  }
+
+                }
+                tasks.register("test-fus", TestFusTask.class).get().doLast {
+                  fusStatisticsBuildService.get().reportMetric("$metricName", $metricValue, null)
+                }
+                """.trimIndent()
+            }
+
+            val reportRelativePath = "reports"
+            build("test-fus", "-Pkotlin.fus.statistics.path=${projectPath.resolve(reportRelativePath).pathString}") {
+                val fusReport = projectPath.getSingleFileInDir("$reportRelativePath/kotlin-fus")
+                assertFileContains(
+                    fusReport,
+                    "METRIC_NAME=1",
+                    "BUILD FINISHED"
+                )
+            }
+        }
+    }
+
+
 }
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
index 8f596fe..a24e9a5 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -216,6 +216,7 @@
         ":gradle:kotlin-compiler-args-properties",
         ":gradle:regression-benchmark-templates",
         ":gradle:regression-benchmarks",
+        ":gradle:fus-statistics-gradle-plugin",
         ":kotlin-tooling-metadata",
         ":kotlin-tooling-core",
         ":kotlin-allopen",
@@ -758,6 +759,7 @@
 project(':gradle:gradle-warnings-detector').projectDir = "$rootDir/libraries/tools/gradle/gradle-warnings-detector" as File
 project(':gradle:kotlin-compiler-args-properties').projectDir = "$rootDir/libraries/tools/gradle/kotlin-compiler-args-properties" as File
 project(":gradle:regression-benchmark-templates").projectDir = "$rootDir/libraries/tools/gradle/regression-benchmark-templates" as File
+project(":gradle:kotlin-compiler-args-properties").projectDir = "$rootDir/libraries/tools/gradle/fus-statistics-gradle-plugin" as File
 project(":gradle:regression-benchmarks").projectDir = "$rootDir/libraries/tools/gradle/regression-benchmarks" as File
 project(':kotlin-tooling-metadata').projectDir = "$rootDir/libraries/tools/kotlin-tooling-metadata" as File
 project(':kotlin-tooling-core').projectDir = "$rootDir/libraries/tools/kotlin-tooling-core" as File