package sbt

  import Def.Setting
  import Plugins._
  import PluginsDebug._
  import java.net.URI

private[sbt] class PluginsDebug(val available: List[AutoPlugin], val nameToKey: Map[String, AttributeKey[_]], val provided: Relation[AutoPlugin, AttributeKey[_]]) {
  /** The set of [[AutoPlugin]]s that might define a key named `keyName`.
  * Because plugins can define keys in different scopes, this should only be used as a guideline. */
  def providers(keyName: String): Set[AutoPlugin] = nameToKey.get(keyName) match {
    case None => Set.empty
    case Some(key) => provided.reverse(key)
  }
  /** Describes alternative approaches for defining key [[keyName]] in [[context]].*/
  def toEnable(keyName: String, context: Context): List[PluginEnable] =
    providers(keyName).toList.map(plugin => pluginEnable(context, plugin))

  /** Provides text to suggest how [[notFoundKey]] can be defined in [[context]]. */
  def debug(notFoundKey: String, context: Context): String =
    {
      val (activated, deactivated) = Util.separate(toEnable(notFoundKey, context)) {
        case pa: PluginActivated => Left(pa)
        case pd: EnableDeactivated => Right(pd)
      }
      val activePrefix = if(activated.nonEmpty) s"Some already activated plugins define $notFoundKey: ${activated.mkString(", ")}\n" else ""
      activePrefix + debugDeactivated(notFoundKey, deactivated)
    }
  private[this] def debugDeactivated(notFoundKey: String, deactivated: Seq[EnableDeactivated]): String =
    {
      val (impossible, possible) = Util.separate(deactivated) {
        case pi: PluginImpossible => Left(pi)
        case pr: PluginRequirements => Right(pr)
      }
      if(possible.nonEmpty) {
        val explained = possible.map(explainPluginEnable)
        val possibleString =
          if(explained.size > 1) explained.zipWithIndex.map{case (s,i) => s"$i. $s"}.mkString("Multiple plugins are available that can provide $notFoundKey:\n", "\n", "")
          else s"$notFoundKey is provided by an available (but not activated) plugin:\n${explained.mkString}"
        def impossiblePlugins = impossible.map(_.plugin.label).mkString(", ")
        val imPostfix = if(impossible.isEmpty) "" else s"\n\nThere are other available plugins that provide $notFoundKey, but they are impossible to add: $impossiblePlugins"
        possibleString + imPostfix
      }
      else if(impossible.isEmpty)
        s"No available plugin provides key $notFoundKey."
      else {
        val explanations = impossible.map(explainPluginEnable)
        explanations.mkString(s"Plugins are available that could provide $notFoundKey, but they are impossible to add:\n\t", "\n\t", "")
      }
    }

  /** Text that suggests how to activate [[plugin]] in [[context]] if possible and if it is not already activated.*/
  def help(plugin: AutoPlugin, context: Context): String =
    if (context.enabled.contains(plugin)) activatedHelp(plugin)
    else deactivatedHelp(plugin, context)
  private def activatedHelp(plugin: AutoPlugin): String =
    {
      val prefix = s"${plugin.label} is activated."
      val keys = provided.forward(plugin)
      val keysString = if(keys.isEmpty) "" else s"\nIt may affect these keys: ${multi(keys.toList.map(_.label))}"
      val configs = plugin.projectConfigurations
      val confsString = if(configs.isEmpty) "" else s"\nIt defines these configurations: ${multi(configs.map(_.name))}"
      prefix + keysString + confsString
    }
  private def deactivatedHelp(plugin: AutoPlugin, context: Context): String =
    {
      val prefix = s"${plugin.label} is NOT activated."
      val keys = provided.forward(plugin)
      val keysString = if(keys.isEmpty) "" else s"\nActivating it may affect these keys: ${multi(keys.toList.map(_.label))}"
      val configs = plugin.projectConfigurations
      val confsString = if(configs.isEmpty) "" else s"\nActivating it will define these configurations: ${multi(configs.map(_.name))}"
      val toActivate = explainPluginEnable(pluginEnable(context, plugin))
      s"$prefix$keysString$confsString\n$toActivate"
    }

  private[this] def multi(strs: Seq[String]): String = strs.mkString(if(strs.size > 4) "\n\t" else ", ")
}

private[sbt] object PluginsDebug {
  def helpAll(s: State): String =
    if(Project.isProjectLoaded(s))
      {
        val extracted = Project.extract(s)
        import extracted._
        def helpBuild(uri: URI, build: LoadedBuildUnit): String =
        {
          val pluginStrings = for(plugin <- availableAutoPlugins(build)) yield {
            val activatedIn = build.defined.values.toList.filter(_.autoPlugins.contains(plugin)).map(_.id)
            val actString = if(activatedIn.nonEmpty) activatedIn.mkString(": enabled in ", ", ", "") else "" // TODO: deal with large builds
            s"\n\t${plugin.label}$actString"
          }
          s"In $uri${pluginStrings.mkString}"
        }
        val buildStrings = for((uri, build) <- structure.units) yield helpBuild(uri, build)
        buildStrings.mkString("\n")
      }
    else "No project is currently loaded."

  def autoPluginMap(s: State): Map[String, AutoPlugin] =
    {
      val extracted = Project.extract(s)
      import extracted._
      structure.units.values.toList.flatMap(availableAutoPlugins).map(plugin => (plugin.label, plugin)).toMap
    }
  private[this] def availableAutoPlugins(build: LoadedBuildUnit): Seq[AutoPlugin] =
    build.unit.plugins.detected.autoPlugins map {_.value}

  def help(plugin: AutoPlugin, s: State): String =
    {
      val extracted = Project.extract(s)
      import extracted._
      def definesPlugin(p: ResolvedProject): Boolean = p.autoPlugins.contains(plugin)
      def projectForRef(ref: ProjectRef): ResolvedProject = get(Keys.thisProject in ref)
      val perBuild: Map[URI, Set[AutoPlugin]] = structure.units.mapValues(unit => availableAutoPlugins(unit).toSet)
      val pluginsThisBuild = perBuild.getOrElse(currentRef.build, Set.empty).toList
      lazy val context = Context(currentProject.plugins, currentProject.autoPlugins, Plugins.deducer(pluginsThisBuild), pluginsThisBuild, s.log)
      lazy val debug = PluginsDebug(context.available)
      if(!pluginsThisBuild.contains(plugin)) {
        val availableInBuilds: List[URI] = perBuild.toList.filter(_._2(plugin)).map(_._1)
        s"Plugin ${plugin.label} is only available in builds:\n\t${availableInBuilds.mkString("\n\t")}\nSwitch to a project in one of those builds using `project` and rerun this command for more information."
      } else if(definesPlugin(currentProject))
        debug.activatedHelp(plugin)
      else {
        val thisAggregated = BuildUtil.dependencies(structure.units).aggregateTransitive.getOrElse(currentRef, Nil)
        val definedInAggregated = thisAggregated.filter(ref => definesPlugin(projectForRef(ref)))
        if(definedInAggregated.nonEmpty) {
          val projectNames = definedInAggregated.map(_.project) // TODO: usually in this build, but could technically require the build to be qualified
          s"Plugin ${plugin.label} is not activated on this project, but this project aggregates projects where it is activated:\n\t${projectNames.mkString("\n\t")}"
        } else {
          val base = debug.deactivatedHelp(plugin, context)
          val aggNote = if(thisAggregated.nonEmpty) "Note: This project aggregates other projects and this" else "Note: This"
          val common =  " information is for this project only."
          val helpOther = "To see how to activate this plugin for another project, change to the project using `project <name>` and rerun this command."
          s"$base\n$aggNote$common\n$helpOther"
        }
      }
    }

  /** Precomputes information for debugging plugins. */
  def apply(available: List[AutoPlugin]): PluginsDebug =
    {
      val keyR = definedKeys(available)
      val nameToKey: Map[String, AttributeKey[_]] = keyR._2s.toList.map(key => (key.label, key)).toMap
      new PluginsDebug(available, nameToKey, keyR)
    }

  /** The context for debugging a plugin (de)activation.
  * @param initial The initially defined [[AutoPlugin]]s.
  * @param enabled The resulting model.
  * @param deducePlugin The function used to compute the model.
  * @param available All [[AutoPlugin]]s available for consideration. */
  final case class Context(initial: Plugins, enabled: Seq[AutoPlugin], deducePlugin: (Plugins, Logger) => Seq[AutoPlugin], available: List[AutoPlugin], log: Logger)

  /** Describes the steps to activate a plugin in some context. */
  sealed abstract class PluginEnable
  /** Describes a [[plugin]] that is already activated in the [[context]].*/
  final case class PluginActivated(plugin: AutoPlugin, context: Context) extends PluginEnable
  sealed abstract class EnableDeactivated extends PluginEnable
  /** Describes a [[plugin]] that cannot be activated in a [[context]] due to [[contradictions]] in requirements. */
  final case class PluginImpossible(plugin: AutoPlugin, context: Context, contradictions: Set[AutoPlugin]) extends EnableDeactivated

  /** Describes the requirements for activating [[plugin]] in [[context]].
  * @param context The base plugins, exclusions, and ultimately activated plugins
  * @param blockingExcludes Existing exclusions that prevent [[plugin]] from being activated and must be dropped
  * @param enablingPlugins [[AutoPlugin]]s that are not currently enabled, but need to be enabled for [[plugin]] to activate
  * @param extraEnabledPlugins Plugins that will be enabled as a result of [[plugin]] activating, but are not required for [[plugin]] to activate
  * @param willRemove Plugins that will be deactivated as a result of [[plugin]] activating
  * @param deactivate Describes plugins that must be deactivated for [[plugin]] to activate.  These require an explicit exclusion or dropping a transitive [[AutoPlugin]].*/
  final case class PluginRequirements(plugin: AutoPlugin, context: Context, blockingExcludes: Set[AutoPlugin], enablingPlugins: Set[AutoPlugin], extraEnabledPlugins: Set[AutoPlugin], willRemove: Set[AutoPlugin], deactivate: List[DeactivatePlugin]) extends EnableDeactivated

  /** Describes a [[plugin]] that must be removed in order to activate another plugin in some context.
  * The [[plugin]] can always be directly, explicitly excluded.
  * @param removeOneOf If non-empty, removing one of these [[AutoPlugin]]s will deactivate [[plugin]] without affecting the other plugin.  If empty, a direct exclusion is required.
  * @param newlySelected If false, this plugin was selected in the original context.  */
  final case class DeactivatePlugin(plugin: AutoPlugin, removeOneOf: Set[AutoPlugin], newlySelected: Boolean)

  /** Determines how to enable [[plugin]] in [[context]]. */
  def pluginEnable(context: Context, plugin: AutoPlugin): PluginEnable =
    if(context.enabled.contains(plugin))
      PluginActivated(plugin, context)
    else
      enableDeactivated(context, plugin)

  private[this] def enableDeactivated(context: Context, plugin: AutoPlugin): PluginEnable = {
    // deconstruct the context
    val initialModel = context.enabled.toSet
    val initial = flatten(context.initial)
    val initialPlugins = plugins(initial)
    val initialExcludes = excludes(initial)

    val minModel = minimalModel(plugin)

    /* example 1
    A :- B, not C
    C :- D, E
    initial: B, D, E
    propose: drop D or E

    initial: B, not A
    propose: drop 'not A'

    example 2
    A :- B, not C
    C :- B
    initial: <empty>
    propose: B, exclude C
    */

    // `plugin` will only be activated when all of these plugins are activated
    // Deactivating any one of these would deactivate `plugin`.
    val minRequiredPlugins = plugins(minModel)

    // The presence of any one of these plugins would deactivate `plugin`
    val minAbsentPlugins = excludes(minModel).toSet

    // Plugins that must be both activated and deactivated for `plugin` to activate.
    //  A non-empty list here cannot be satisfied and is an error.
    val contradictions = minAbsentPlugins & minRequiredPlugins

    if(contradictions.nonEmpty) PluginImpossible(plugin, context, contradictions)
    else {
      // Plguins that the user has to add to the currently selected plugins in order to enable `plugin`.
      val addToExistingPlugins = minRequiredPlugins -- initialPlugins

      // Plugins that are currently excluded that need to be allowed.
      val blockingExcludes = initialExcludes & minRequiredPlugins

      // The model that results when the minimal plugins are enabled and the minimal plugins are excluded.
      //  This can include more plugins than just `minRequiredPlugins` because the plguins required for `plugin`
      //  might activate other plugins as well.
      val modelForMin = context.deducePlugin(and(includeAll(minRequiredPlugins), excludeAll(minAbsentPlugins)), context.log)

      val incrementalInputs = and( includeAll(minRequiredPlugins ++ initialPlugins), excludeAll(minAbsentPlugins ++ initialExcludes -- minRequiredPlugins))
      val incrementalModel = context.deducePlugin(incrementalInputs, context.log).toSet

      // Plugins that are newly enabled as a result of selecting the plugins needed for `plugin`, but aren't strictly required for `plugin`.
      //   These could be excluded and `plugin` and the user's current plugins would still be activated.
      val extraPlugins = incrementalModel.toSet -- minRequiredPlugins -- initialModel

      // Plugins that will no longer be enabled as a result of enabling `plugin`.
      val willRemove = initialModel -- incrementalModel

      // Determine the plugins that must be independently deactivated.
      // If both A and B must be deactivated, but A transitively depends on B, deactivating B will deactivate A.
      // If A must be deactivated, but one if its (transitively) required plugins isn't present, it won't be activated.
      //   So, in either of these cases, A doesn't need to be considered further and won't be included in this set.
      val minDeactivate = minAbsentPlugins.filter(p => Plugins.satisfied(p.requires, incrementalModel))

      val deactivate = for(d <- minDeactivate.toList) yield {
        // removing any one of these plugins will deactivate `d`.  TODO: This is not an especially efficient implementation.
        val removeToDeactivate = plugins(minimalModel(d)) -- minRequiredPlugins
        val newlySelected = !initialModel(d)
        // a. suggest removing a plugin in removeOneToDeactivate to deactivate d
        // b. suggest excluding `d` to directly deactivate it in any case
        // c. note whether d was already activated (in context.enabled) or is newly selected
        DeactivatePlugin(d, removeToDeactivate, newlySelected)
      }

      PluginRequirements(plugin, context, blockingExcludes, addToExistingPlugins, extraPlugins, willRemove, deactivate)
    }
  }

  private[this] def includeAll[T <: Basic](basic: Set[T]): Plugins = And(basic.toList)
  private[this] def excludeAll(plugins: Set[AutoPlugin]): Plugins = And(plugins map (p => Exclude(p)) toList)

  private[this] def excludes(bs: Seq[Basic]): Set[AutoPlugin] = bs.collect { case Exclude(b) => b }.toSet
  private[this] def plugins(bs: Seq[Basic]): Set[AutoPlugin] = bs.collect { case n: AutoPlugin => n }.toSet

  // If there is a model that includes `plugin`, it includes at least what is returned by this method.
  // This is the list of plugins that must be included as well as list of plugins that must not be present.
  // It might not be valid, such as if there are contradictions or if there are cycles that are unsatisfiable.
  // The actual model might be larger, since other plugins might be enabled by the selected plugins.
  private[this] def minimalModel(plugin: AutoPlugin): Seq[Basic] = Dag.topologicalSortUnchecked(plugin: Basic) {
    case _: Exclude  => Nil
    case ap: AutoPlugin => Plugins.flatten(ap.requires) :+ plugin
  }

  /** String representation of [[PluginEnable]], intended for end users. */
  def explainPluginEnable(ps: PluginEnable): String =
    ps match {
      case PluginRequirements(plugin, context, blockingExcludes, enablingPlugins, extraEnabledPlugins, toBeRemoved, deactivate) =>
        def indent(str: String) = if(str.isEmpty) "" else s"\t$str"
        def note(str: String) = if(str.isEmpty) "" else s"Note: $str"
        val parts =
          indent(excludedError(false /* TODO */, blockingExcludes.toList)) ::
          indent(required(enablingPlugins.toList)) ::
          indent(needToDeactivate(deactivate)) ::
          note(willAdd(plugin, extraEnabledPlugins.toList)) ::
          note(willRemove(plugin, toBeRemoved.toList)) ::
          Nil
        parts.filterNot(_.isEmpty).mkString("\n")
      case PluginImpossible(plugin, context, contradictions) => pluginImpossible(plugin, contradictions)
      case PluginActivated(plugin, context) => s"Plugin ${plugin.label} already activated."
    }

  /** Provides a [[Relation]] between plugins and the keys they potentially define.
  * Because plugins can define keys in different scopes and keys can be overridden, this is not definitive.*/
  def definedKeys(available: List[AutoPlugin]): Relation[AutoPlugin, AttributeKey[_]] =
    {
      def extractDefinedKeys(ss: Seq[Setting[_]]): Seq[AttributeKey[_]] =
        ss.map(_.key.key)
      def allSettings(p: AutoPlugin): Seq[Setting[_]] = p.projectSettings ++ p.buildSettings ++ p.globalSettings
      val empty = Relation.empty[AutoPlugin, AttributeKey[_]]
      (empty /: available)( (r,p) => r + (p, extractDefinedKeys(allSettings(p))) )
    }

  private[this] def excludedError(transitive: Boolean, dependencies: List[AutoPlugin]): String =
    str(dependencies)(excludedPluginError(transitive), excludedPluginsError(transitive))

  private[this] def excludedPluginError(transitive: Boolean)(dependency: AutoPlugin) =
    s"Required ${transitiveString(transitive)}dependency ${dependency.label} was excluded."
  private[this] def excludedPluginsError(transitive: Boolean)(dependencies: List[AutoPlugin]) =
    s"Required ${transitiveString(transitive)}dependencies were excluded:\n\t${labels(dependencies).mkString("\n\t")}"
  private[this] def transitiveString(transitive: Boolean) =
    if(transitive) "(transitive) " else ""

  private[this] def required(plugins: List[AutoPlugin]): String =
    str(plugins)(requiredPlugin, requiredPlugins)

  private[this] def requiredPlugin(plugin: AutoPlugin) =
    s"Required plugin ${plugin.label} not present."
  private[this] def requiredPlugins(plugins: List[AutoPlugin]) =
    s"Required plugins not present:\n\t${plugins.map(_.label).mkString("\n\t")}"

  private[this] def str[A](list: List[A])(f: A => String, fs: List[A] => String): String =
    list match {
      case Nil           => ""
      case single :: Nil => f(single)
      case _             => fs(list)
    }

  private[this] def willAdd(base: AutoPlugin, plugins: List[AutoPlugin]): String =
    str(plugins)(willAddPlugin(base), willAddPlugins(base))

  private[this] def willAddPlugin(base: AutoPlugin)(plugin: AutoPlugin) =
    s"Enabling ${base.label} will also enable ${plugin.label}"
  private[this] def willAddPlugins(base: AutoPlugin)(plugins: List[AutoPlugin]) =
    s"Enabling ${base.label} will also enable:\n\t${labels(plugins).mkString("\n\t")}"

  private[this] def willRemove(base: AutoPlugin, plugins: List[AutoPlugin]): String =
    str(plugins)(willRemovePlugin(base), willRemovePlugins(base))

  private[this] def willRemovePlugin(base: AutoPlugin)(plugin: AutoPlugin) =
    s"Enabling ${base.label} will disable ${plugin.label}"
  private[this] def willRemovePlugins(base: AutoPlugin)(plugins: List[AutoPlugin]) =
    s"Enabling ${base.label} will disable:\n\t${labels(plugins).mkString("\n\t")}"

  private[this] def labels(plugins: List[AutoPlugin]): List[String] =
    plugins.map(_.label)

  private[this] def needToDeactivate(deactivate: List[DeactivatePlugin]): String =
    str(deactivate)(deactivate1, deactivateN)
  private[this] def deactivateN(plugins: List[DeactivatePlugin]): String =
    plugins.map(deactivateString).mkString("These plugins need to be deactivated:\n\t", "\n\t", "")
  private[this] def deactivate1(deactivate: DeactivatePlugin): String =
    s"Need to deactivate ${deactivateString(deactivate)}"
  private[this] def deactivateString(d: DeactivatePlugin): String =
    {
      val removePluginsString: String =
        d.removeOneOf.toList match {
          case Nil => ""
          case x :: Nil => s" or no longer include $x"
          case xs => s" or remove one of ${xs.mkString(", ")}"
        }
      s"${d.plugin.label}: directly exclude it${removePluginsString}"
    }

  private[this] def pluginImpossible(plugin: AutoPlugin, contradictions: Set[AutoPlugin]): String =
    str(contradictions.toList)(pluginImpossible1(plugin), pluginImpossibleN(plugin))

  private[this] def pluginImpossible1(plugin: AutoPlugin)(contradiction: AutoPlugin): String =
    s"There is no way to enable plugin ${plugin.label}.  It (or its dependencies) requires plugin ${contradiction.label} to both be present and absent.  Please report the problem to the plugin's author."
  private[this] def pluginImpossibleN(plugin: AutoPlugin)(contradictions: List[AutoPlugin]): String =
    s"There is no way to enable plugin ${plugin.label}.  It (or its dependencies) requires these plugins to be both present and absent:\n\t${labels(contradictions).mkString("\n\t")}\nPlease report the problem to the plugin's author."
}