package sbt

import Def.{ Initialize, resolvedScoped, ScopedKey, Setting, streamsManagerKey }
import Previous._
import Types._

import java.io.{ InputStream, OutputStream }
import sbinary.{ DefaultProtocol, Format }
import DefaultProtocol.{ StringFormat, withStamp }

/**
 * Reads the previous value of tasks on-demand.  The read values are cached so that they are only read once per task execution.
 * `referenced` provides the `Format` to use for each key.
 */
private[sbt] final class Previous(streams: Streams, referenced: IMap[ScopedTaskKey, Referenced]) {
  private[this] val map = referenced.mapValues(toValue)
  private[this] def toValue = new (Referenced ~> ReferencedValue) { def apply[T](x: Referenced[T]) = new ReferencedValue(x) }

  private[this] final class ReferencedValue[T](referenced: Referenced[T]) {
    import referenced.{ stamped, task }
    lazy val previousValue: Option[T] = {
      val in = streams(task).readBinary(task, StreamName)
      try read(in, stamped) finally in.close()
    }
  }

  /** Used by the .previous runtime implemention to get the previous value for task `key`. */
  private def get[T](key: ScopedKey[Task[T]]): Option[T] =
    map.get(key).flatMap(_.previousValue)
}
object Previous {
  private[sbt]type ScopedTaskKey[T] = ScopedKey[Task[T]]
  private type Streams = sbt.std.Streams[ScopedKey[_]]

  /** The stream where the task value is persisted. */
  private final val StreamName = "previous"

  /** Represents a reference task.previous*/
  private[sbt] final class Referenced[T](val task: ScopedKey[Task[T]], val format: Format[T]) {
    lazy val stamped = withStamp(task.key.manifest.toString)(format)
    def setTask(newTask: ScopedKey[Task[T]]) = new Referenced(newTask, format)
  }

  private[sbt] val references = SettingKey[References]("previous-references", "Collects all static references to previous values of tasks.", KeyRanks.Invisible)
  private[sbt] val cache = TaskKey[Previous]("previous-cache", "Caches previous values of tasks read from disk for the duration of a task execution.", KeyRanks.Invisible)
  private[this] val previousReferenced = AttributeKey[Referenced[_]]("previous-referenced")

  /** Records references to previous task value. This should be completely populated after settings finish loading. */
  private[sbt] final class References {
    private[this] var map = IMap.empty[ScopedTaskKey, Referenced]

    // TODO: this arbitrarily chooses a Format.
    // The need to choose is a fundamental problem with this approach, but this should at least make a stable choice.
    def recordReference[T](key: ScopedKey[Task[T]], format: Format[T]): Unit = synchronized {
      map = map.put(key, new Referenced(key, format))
    }
    def getReferences: IMap[ScopedTaskKey, Referenced] = synchronized { map }
  }

  /** Persists values of tasks t where there is some task referencing it via t.previous. */
  private[sbt] def complete(referenced: References, results: RMap[Task, Result], streams: Streams): Unit =
    {
      val map = referenced.getReferences
      def impl[T](key: ScopedKey[_], result: T): Unit =
        for (i <- map.get(key.asInstanceOf[ScopedTaskKey[T]])) {
          val out = streams.apply(i.task).binary(StreamName)
          try write(out, i.stamped, result) finally out.close()
        }

      for {
        results.TPair(Task(info, _), Value(result)) <- results.toTypedSeq
        key <- info.attributes get Def.taskDefinitionKey
      } impl(key, result)
    }

  private def read[T](stream: InputStream, format: Format[T]): Option[T] =
    try Some(format.reads(stream))
    catch { case e: Exception => None }

  private def write[T](stream: OutputStream, format: Format[T], value: T): Unit =
    try format.writes(stream, value)
    catch { case e: Exception => () }

  /** Public as a macro implementation detail.  Do not call directly. */
  def runtime[T](skey: TaskKey[T])(implicit format: Format[T]): Initialize[Task[Option[T]]] =
    {
      val inputs = (cache in Global) zip Def.validated(skey, selfRefOk = true) zip (references in Global)
      inputs {
        case ((prevTask, resolved), refs) =>
          refs.recordReference(resolved, format) // always evaluated on project load
          import std.TaskExtra._
          prevTask.map(_ get resolved) // evaluated if this task is evaluated
      }
    }

  private[sbt] def cacheSetting = (streamsManagerKey, references) map { (s, refs) => new Previous(s, refs.getReferences) }
}
