package sbt
/*
TODO:
- index all available AutoPlugins to get the tasks that will be added
- error message when a task doesn't exist that it would be provided by plugin x, enabled by natures y,z, blocked by a, b
*/

import logic.{Atom, Clause, Clauses, Formula, Literal, Logic, Negated}
import Logic.{CyclicNegation, InitialContradictions, InitialOverlap, LogicException}
import Def.Setting
import Plugins._
import annotation.tailrec

/**
An AutoPlugin defines a group of settings and the conditions where the settings are automatically added to a build (called "activation").
The `requires` and `trigger` methods together define the conditions, and a method like `projectSettings` defines the settings to add.

Steps for plugin authors:
1. Determine if the AutoPlugin should automatically be activated when all requirements are met, or should be opt-in.
2. Determine the [[AutoPlugins]]s that, when present (or absent), act as the requirements for the AutoPlugin.
3. Determine the settings/configurations to that the AutoPlugin injects when activated.
4. Determine the keys and other names to be automatically imported to *.sbt scripts.

For example, the following will automatically add the settings in `projectSettings`
  to a project that has both the `Web` and `Javascript` plugins enabled.

    object Plugin extends sbt.AutoPlugin {
        override def requires = Web && Javascript
        override def trigger = allRequirements
        override def projectSettings = Seq(...)
        
        object autoImport {
      lazy val obfuscate = taskKey[Seq[File]]("Obfuscates the source.")
        }
    }

Steps for users:
1. Add dependencies on plugins in `project/plugins.sbt` as usual with `addSbtPlugin`
2. Add key plugins to Projects, which will automatically select the plugin + dependent plugin settings to add for those Projects.
3. Exclude plugins, if desired.

For example, given plugins Web and Javascript (perhaps provided by plugins added with addSbtPlugin),

  <Project>.enablePlugins( Web && Javascript )

will activate `MyPlugin` defined above and have its settings automatically added.  If the user instead defines

  <Project>.enablePlugins( Web && Javascript ).disablePlugins(MyPlugin)

then the `MyPlugin` settings (and anything that activates only when `MyPlugin` is activated) will not be added.

*/
abstract class AutoPlugin extends Plugins.Basic with PluginsFunctions {
  /** Determines whether this AutoPlugin will be activated for this project when the `requires` clause is satisfied.
   *
   * When this method returns `allRequirements`, and `requires` method returns `Web && Javascript`, this plugin
   * instance will be added automatically if the `Web` and `Javascript` plugins are enabled.
   * 
   * When this method returns `noTrigger`, and `requires` method returns `Web && Javascript`, this plugin
   * instance will be added only if the build user enables it, but it will automatically add both `Web` and `Javascript`. */
  def trigger: PluginTrigger = noTrigger

  /** This AutoPlugin requires the plugins the [[Plugins]] matcher returned by this method. See [[trigger]].
   */
  def requires: Plugins = empty

  val label: String = getClass.getName.stripSuffix("$")

  override def toString: String = label

  /** The [[Configuration]]s to add to each project that activates this AutoPlugin.*/
  def projectConfigurations: Seq[Configuration] = Nil

  /** The [[Setting]]s to add in the scope of each project that activates this AutoPlugin. */
  def projectSettings: Seq[Setting[_]] = Nil

  /** The [[Setting]]s to add to the build scope for each project that activates this AutoPlugin.
  * The settings returned here are guaranteed to be added to a given build scope only once
  * regardless of how many projects for that build activate this AutoPlugin. */
  def buildSettings: Seq[Setting[_]] = Nil

  /** The [[Setting]]s to add to the global scope exactly once if any project activates this AutoPlugin. */
  def globalSettings: Seq[Setting[_]] = Nil

  // TODO?: def commands: Seq[Command]

  private[sbt] def unary_! : Exclude = Exclude(this)


  /** If this plugin does not have any requirements, it means it is actually a root plugin. */
  private[sbt] final def isRoot: Boolean = 
    requires match {
      case Empty => true
      case _ => false
    }

  /** If this plugin does not have any requirements, it means it is actually a root plugin. */
  private[sbt] final def isAlwaysEnabled: Boolean =
    isRoot && (trigger == AllRequirements)
}

/** An error that occurs when auto-plugins aren't configured properly.
* It translates the error from the underlying logic system to be targeted at end users. */
final class AutoPluginException private(val message: String, val origin: Option[LogicException]) extends RuntimeException(message) {
  /** Prepends `p` to the error message derived from `origin`. */
  def withPrefix(p: String) = new AutoPluginException(p + message, origin)
}
object AutoPluginException {
  def apply(msg: String): AutoPluginException = new AutoPluginException(msg, None)
  def apply(origin: LogicException): AutoPluginException = new AutoPluginException(Plugins.translateMessage(origin), Some(origin))
}

sealed trait PluginTrigger
case object AllRequirements extends PluginTrigger
case object NoTrigger extends PluginTrigger 

/** An expression that matches `AutoPlugin`s. */
sealed trait Plugins {
  def && (o: Basic): Plugins
}


sealed trait PluginsFunctions {
  /** [[Plugins]] instance that doesn't require any [[Plugins]]s. */
  def empty: Plugins = Plugins.Empty

  /** This plugin is activated when all required plugins are present. */
  def allRequirements: PluginTrigger = AllRequirements
  /** This plugin is activated only when it is manually activated. */
  def noTrigger: PluginTrigger = NoTrigger
}

object Plugins extends PluginsFunctions {
  /** Given the available auto plugins `defined`, returns a function that selects [[AutoPlugin]]s for the provided [[AutoPlugin]]s.
  * The [[AutoPlugin]]s are topologically sorted so that a required [[AutoPlugin]] comes before its requiring [[AutoPlugin]].*/
  def deducer(defined0: List[AutoPlugin]): (Plugins, Logger) => Seq[AutoPlugin] =
    if(defined0.isEmpty) (_, _) => Nil
    else {
      // TODO: defined should return all the plugins
      val allReqs = (defined0 flatMap { asRequirements }).toSet
      val diff = allReqs diff defined0.toSet
      val defined = if (!diff.isEmpty) diff.toList ::: defined0
              else defined0

      val byAtom = defined map { x => (Atom(x.label), x) }
      val byAtomMap = byAtom.toMap
      if(byAtom.size != byAtomMap.size) duplicateProvidesError(byAtom)
      // Ignore clauses for plugins that does not require anything else.
      // Avoids the requirement for pure Nature strings *and* possible
      // circular dependencies in the logic.
      val allRequirementsClause = defined.filterNot(_.isRoot).flatMap(d => asRequirementsClauses(d))
      val allEnabledByClause = defined.filterNot(_.isRoot).flatMap(d => asEnabledByClauses(d))

      // Note: Here is where the function begins.  We're given a list of plugins now.
      (requestedPlugins, log) => {
        def explicitlyDisabled(p: AutoPlugin): Boolean = hasExclude(requestedPlugins, p)
        val alwaysEnabled: List[AutoPlugin] = defined.filter(_.isAlwaysEnabled).filterNot(explicitlyDisabled)
        val knowlege0: Set[Atom] = ((flatten(requestedPlugins) ++ alwaysEnabled) collect {
          case x: AutoPlugin => Atom(x.label)
        }).toSet
        val clauses = Clauses((allRequirementsClause ::: allEnabledByClause) filterNot { _.head subsetOf knowlege0 })
        log.debug(s"deducing auto plugins based on known facts ${knowlege0.toString} and clauses ${clauses.toString}")
        Logic.reduce(clauses, (flattenConvert(requestedPlugins) ++ convertAll(alwaysEnabled)).toSet) match {
          case Left(problem) => throw AutoPluginException(problem)
          case Right(results) =>
            log.debug(s"  :: deduced result: ${results}")
            val selectedAtoms: List[Atom] = results.ordered
            val selectedPlugins = selectedAtoms map { a =>
              byAtomMap.getOrElse(a, throw AutoPluginException(s"${a} was not found in atom map."))
            }
            val forbidden: Set[AutoPlugin] = (selectedPlugins flatMap { Plugins.asExclusions }).toSet
            val c = selectedPlugins.toSet & forbidden
            if (!c.isEmpty) {
              exlusionConflictError(requestedPlugins, selectedPlugins, c.toSeq sortBy {_.label})
            }
            val retval = topologicalSort(selectedPlugins, log)
            log.debug(s"  :: sorted deduced result: ${retval.toString}")
            retval
        }
      }
    }
  private[sbt] def topologicalSort(ns: List[AutoPlugin], log: Logger): List[AutoPlugin] = {
    log.debug(s"sorting: ns: ${ns.toString}")
    @tailrec def doSort(found0: List[AutoPlugin], notFound0: List[AutoPlugin], limit0: Int): List[AutoPlugin] = {
      log.debug(s"  :: sorting:: found: ${found0.toString} not found ${notFound0.toString}")
      if (limit0 < 0) throw AutoPluginException(s"Failed to sort ${ns} topologically")
      else if (notFound0.isEmpty) found0
      else {
        val (found1, notFound1) = notFound0 partition { n => asRequirements(n).toSet subsetOf found0.toSet }
        doSort(found0 ::: found1, notFound1, limit0 - 1)
      }
    }
    val (roots, nonRoots) = ns partition (_.isRoot)
    doSort(roots, nonRoots, ns.size * ns.size + 1)
  }
  private[sbt] def translateMessage(e: LogicException) = e match {
    case ic: InitialContradictions => s"Contradiction in selected plugins.  These plugins were both included and excluded: ${literalsString(ic.literals.toSeq)}"
    case io: InitialOverlap => s"Cannot directly enable plugins.  Plugins are enabled when their required plugins are satisifed.  The directly selected plugins were: ${literalsString(io.literals.toSeq)}"
    case cn: CyclicNegation => s"Cycles in plugin requirements cannot involve excludes.  The problematic cycle is: ${literalsString(cn.cycle)}"
  }
  private[this] def literalsString(lits: Seq[Literal]): String =
    lits map { case Atom(l) => l; case Negated(Atom(l)) => l } mkString(", ")

  private[this] def duplicateProvidesError(byAtom: Seq[(Atom, AutoPlugin)]) {
    val dupsByAtom = byAtom.groupBy(_._1).mapValues(_.map(_._2))
    val dupStrings = for( (atom, dups) <- dupsByAtom if dups.size > 1 ) yield
      s"${atom.label} by ${dups.mkString(", ")}"
    val (ns, nl) = if(dupStrings.size > 1) ("s", "\n\t") else ("", " ")
    val message = s"Plugin$ns provided by multiple AutoPlugins:$nl${dupStrings.mkString(nl)}"
    throw AutoPluginException(message)
  }
  private[this] def exlusionConflictError(requested: Plugins, selected: Seq[AutoPlugin], conflicting: Seq[AutoPlugin]) {
    def listConflicts(ns: Seq[AutoPlugin]) = (ns map { c =>
      val reasons = (if (flatten(requested) contains c) List("requested")
              else Nil) ++
        (if (c.requires != empty && c.trigger == allRequirements) List(s"enabled by ${c.requires.toString}")
          else Nil) ++
        {
          val reqs = selected filter { x => asRequirements(x) contains c }
          if (!reqs.isEmpty) List(s"""required by ${reqs.mkString(", ")}""")
          else Nil
        } ++
        {
          val exs = selected filter { x => asExclusions(x) contains c }
          if (!exs.isEmpty) List(s"""excluded by ${exs.mkString(", ")}""")
          else Nil
        }
      s"""  - conflict: ${c.label} is ${reasons.mkString("; ")}"""
    }).mkString("\n")
    throw AutoPluginException(s"""Contradiction in enabled plugins:
  - requested: ${requested.toString}
  - enabled: ${selected.mkString(", ")}
${listConflicts(conflicting)}""")
  }

  private[sbt] final object Empty extends Plugins {
    def &&(o: Basic): Plugins = o
    override def toString = "<none>"
  }

  /** An included or excluded Nature/Plugin.  TODO: better name than Basic.  Also, can we dump
   *  this class.
   */
  sealed abstract class Basic extends Plugins {
    def &&(o: Basic): Plugins = And(this :: o :: Nil)
  }
  private[sbt] final case class Exclude(n: AutoPlugin) extends Basic  {
    override def toString = s"!$n"
  }
  private[sbt] final case class And(plugins: List[Basic]) extends Plugins {
    def &&(o: Basic): Plugins = And(o :: plugins)
    override def toString = plugins.mkString(" && ")
  }
  private[sbt] def and(a: Plugins, b: Plugins) = b match {
    case Empty => a
    case And(ns) => (a /: ns)(_ && _)
    case b: Basic => a && b
  }
  private[sbt] def remove(a: Plugins, del: Set[Basic]): Plugins = a match {
    case b: Basic => if(del(b)) Empty else b
    case Empty => Empty
    case And(ns) =>
      val removed = ns.filterNot(del)
      if(removed.isEmpty) Empty else And(removed)
  }

  /** Defines enabled-by clauses for `ap`. */
  private[sbt] def asEnabledByClauses(ap: AutoPlugin): List[Clause] =
    // `ap` is the head and the required plugins for `ap` is the body.
    if (ap.trigger == AllRequirements) Clause( convert(ap.requires), Set(Atom(ap.label)) ) :: Nil
    else Nil
  /** Defines requirements clauses for `ap`. */
  private[sbt] def asRequirementsClauses(ap: AutoPlugin): List[Clause] =
    // required plugin is the head and `ap` is the body.
    asRequirements(ap) map { x => Clause( convert(ap), Set(Atom(x.label)) ) }
  private[sbt] def asRequirements(ap: AutoPlugin): List[AutoPlugin] = flatten(ap.requires).toList collect {
    case x: AutoPlugin => x
  }
  private[sbt] def asExclusions(ap: AutoPlugin): List[AutoPlugin] = flatten(ap.requires).toList collect {
    case Exclude(x) => x
  }
  // TODO - This doesn't handle nested AND boolean logic...
  private[sbt] def hasExclude(n: Plugins, p: AutoPlugin): Boolean = n match {
    case `p` => false
    case Exclude(`p`) => true
    // TODO - This is stupidly advanced.  We do a nested check through possible and-ed
    // lists of plugins exclusions to see if the plugin ever winds up in an excluded=true case.
    // This would handle things like !!p or !(p && z)
    case Exclude(n) => hasInclude(n, p)
    case And(ns) => ns.forall(n => hasExclude(n, p))
    case b: Basic => false
    case Empty => false
  }
  private[sbt] def hasInclude(n: Plugins, p: AutoPlugin): Boolean = n match {
    case `p` => true
    case Exclude(n) => hasExclude(n, p)
    case And(ns) => ns.forall(n => hasInclude(n, p))
    case b: Basic => false
    case Empty => false
  }
  private[this] def flattenConvert(n: Plugins): Seq[Literal] = n match {
    case And(ns) => convertAll(ns)
    case b: Basic => convertBasic(b) :: Nil
    case Empty => Nil
  }
  private[sbt] def flatten(n: Plugins): Seq[Basic] = n match {
    case And(ns) => ns
    case b: Basic => b :: Nil
    case Empty => Nil
  }

  private[this] def convert(n: Plugins): Formula = n match {
    case And(ns) => convertAll(ns).reduce[Formula](_ && _)
    case b: Basic => convertBasic(b)
    case Empty => Formula.True
  }
  private[this] def convertBasic(b: Basic): Literal = b match {
    case Exclude(n) => !convertBasic(n)
    case a: AutoPlugin => Atom(a.label)
  }
  private[this] def convertAll(ns: Seq[Basic]): Seq[Literal] = ns map convertBasic

  /** True if the trigger clause `n` is satisifed by `model`. */
  def satisfied(n: Plugins, model: Set[AutoPlugin]): Boolean =
    flatten(n) forall {
      case Exclude(a) => !model(a)
      case ap: AutoPlugin => model(ap)
    }

  private[sbt] def hasAutoImportGetter(ap: AutoPlugin, loader: ClassLoader): Boolean = {
    import reflect.runtime.{universe => ru}
    import util.control.Exception.catching
    val m = ru.runtimeMirror(loader)
    val im = m.reflect(ap)
    val hasGetterOpt = catching(classOf[ScalaReflectionException]) opt {
      im.symbol.asType.toType.declaration(ru.newTermName("autoImport")) match {
        case ru.NoSymbol => false
        case sym => sym.asTerm.isGetter || sym.asTerm.isModule
      }
    }
    hasGetterOpt getOrElse false
  }
}