/*
 * 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
 *
 *     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.
 */

import sbt.*
import sbt.KeyRanks.*
import sbt.Keys.*

object OsgiCheckPlugin extends AutoPlugin {

  class OsgiCheckException()
    extends RuntimeException("OSGI check failed")
    with FeedbackProvidedException

  object autoImport {
    val osgiOwnedPackages =
      taskKey[Set[String]]("Get all packages owned by this project").withRank(BTask)
    val osgiOwnedPackagesWithOwner = taskKey[(Set[String], String)](
      "Get a tuple of the osgiOwnedPackages and the project name that owns them"
    ).withRank(Invisible)
    val osgiCheck = taskKey[Unit]("Check all subpackages for osgi conflicts").withRank(ATask)
  }

  import autoImport.*

  private val packageMatcher = """^\s*package (\S+);?.*$""".r

  override def trigger = allRequirements

  val allSubprojectsFilter = ScopeFilter(inAggregates(ThisProject, includeRoot = false))

  override def projectSettings: Seq[Def.Setting[_]] = {
    Seq(
      osgiOwnedPackages := {
        val logger = streams.value.log
        (Compile / sources).value.flatMap { file =>
          var source = scala.io.Source.fromFile(file)
          val optPackage = source.getLines.collectFirst { case packageMatcher(packageName) =>
            packageName
          }
          if (optPackage.isEmpty) {
            logger.warn(s"${file.getPath}: could not find package statement")
          }
          optPackage
        }.toSet
      },
      osgiOwnedPackagesWithOwner := {
        (osgiOwnedPackages.value, name.value)
      },
      osgiCheck := {
        val logger = streams.value.log

        // create a List of tuples of all osgiOwnedPackages and their owner from all
        // subprojects. If a subproject disables this plugin, then it contributes tuple
        // of empty set and empty string and will be ignored
        val subprojectPackagesAndOwner = (osgiOwnedPackagesWithOwner ?? (Set.empty -> ""))
          .all(allSubprojectsFilter)
          .value

        // flatten all of our tuples so we have a single list of package name ->
        // project owner tuples. At this point, there might be multiple owners
        // for the same package name in this list
        val packageOwnerTuples = subprojectPackagesAndOwner.flatMap {
          case (ownedPackages, packageOwner) =>
            ownedPackages.map { _ -> packageOwner }
        }

        // create a map, grouping with a key of package name and value of all of the
        // owners that claim to own it. If only one project owns a package, the value
        // should be a list of one.
        val packageOwnersMap: Map[String, Seq[String]] = packageOwnerTuples
          .groupBy { case (packageName, _) => packageName }
          .mapValues { list => list.map { case (_, packageOwner) => packageOwner } }

        // find any packages with multiple project owners and error
        val multipleOwners = packageOwnersMap.filter { case (_, owners) => owners.length > 1 }
        if (multipleOwners.size > 0) {
          logger.err("Packages found owned by multiple projects, violating OSGI:")
          multipleOwners.foreach { case (packageName, owners) =>
            logger.err(s"- package $packageName: ${owners.mkString(", ")}")
          }
          throw new OsgiCheckException()
        }
      }
    )
  }

  override val globalSettings: Seq[Setting[_]] = {
    Seq(
      // the osgiCheck does aggregation itself by inspecting all subproject, it does
      // not need to be aggregated
      osgiCheck / aggregate := false
    )
  }

}
