package wow.doge.mygame.subsystems.scriptsystem

import javax.script.ScriptEngine
import javax.script.ScriptEngineManager
import javax.script.ScriptException

import scala.concurrent.duration._

import ammonite.main.Defaults
import ammonite.runtime.Storage.Folder
import ammonite.util.Res
import ammonite.util.Res.Failure
import cats.effect.Resource
import cats.effect.concurrent.Deferred
import cats.syntax.either._
import cats.syntax.flatMap._
import com.softwaremill.macwire._
import com.softwaremill.tagging._
import groovy.util.GroovyScriptEngine
import io.odin.Logger
import monix.bio.IO
import monix.bio.Task
import monix.bio.UIO
import monix.catnap.ConcurrentQueue
import monix.reactive.Observable
import wow.doge.mygame.implicits._
import monix.{eval => me}
import groovy.util.ResourceException
import monix.catnap.MVar

trait Requestable[A] {

  protected def queue: ConcurrentQueue[Task, A]

  def request[T](
      compileRequest: Deferred[Task, T] => A
  )(implicit timeout: FiniteDuration) =
    for {
      d <- Deferred[Task, T]
      req = compileRequest(d)
      _ <- queue.offer(req)
      res <- d.get.timeout(timeout).map(_.get)
    } yield res
}

class ScriptCompiler private (
    _queue: ConcurrentQueue[Task, ScriptCompiler.Command]
) extends Requestable[ScriptCompiler.Command] {

  override protected def queue = _queue

  // def tell(item: Command) = queue.offer(item)

}
object ScriptCompiler {

  sealed trait State
  case object Idle extends State
  case object Active extends State

  /**
    * script representation
    */
  sealed trait ScriptTag
  type ScriptObject = Any @@ ScriptTag

  sealed trait KotlinEngineTag
  type KotlinScriptEngine = ScriptEngine @@ KotlinEngineTag

  sealed trait Error
  final case class AmmoniteFailure(error: Res.Failure) extends Error
  final case class AmmoniteException(error: Res.Exception) extends Error
  final case class ScriptExceptionError(error: ScriptException) extends Error
  final case class ResourceExceptionError(error: ResourceException)
      extends Error
  final case class GroovyScriptExceptionError(
      error: groovy.util.ScriptException
  ) extends Error
  final case class SomeError(reason: String) extends Error

  sealed trait Command
  final case class Get(
      path: os.Path,
      result: Deferred[Task, ScriptResult],
      force: Boolean
  ) extends Command
  final case class GetData(result: Deferred[Task, Data])
  final case class ObservableData(result: Deferred[Task, Observable[Data]])
      extends Command
  // extends Command
  // final case class CompileAll(paths: Seq[os.Path]) extends Command

  type ScriptsMap = Map[os.Path, Any]
  type ScriptResult = Either[Error, Any]

  sealed trait ScriptType
  case object ScalaType extends ScriptType
  case object KotlinType extends ScriptType
  case object GroovyType extends ScriptType

  val defaultScalaRunner =
    ammonite
      .Main(
        storageBackend = new Folder(
          // os.pwd / "target"
          Defaults.ammoniteHome,
          isRepl = false
        )
      )

  val defaultKotlinRunner: KotlinScriptEngine = {
    val manager = new ScriptEngineManager()
    val engine = manager.getEngineByExtension("main.kts")
    engine.taggedWith[KotlinEngineTag]
  }

  val defaultGroovyRunner: GroovyScriptEngine =
    new GroovyScriptEngine(os.pwd.toString)

  case class Data(scriptsMap: ScriptsMap)

  class SourceMaker(
      queue: ConcurrentQueue[Task, Command],
      worker: ScriptCompilerWorker
  ) {
    import com.softwaremill.quicklens._
    def get =
      for {
        dataVar <- MVar[Task].of(Data(Map.empty))
        obs <- Task.deferAction(implicit s =>
          Task(
            Observable
              .repeatEvalF(queue.poll)
              .scanEval0(me.Task.pure((Active: State) -> Data(Map.empty))) {
                case state -> data -> command =>
                  val nextState: IO[Error, (State, Data)] = state match {
                    case Idle => IO.pure(Idle -> data)
                    case Active =>
                      command match {
                        case Get(path, result, force) =>
                          def getAndUpdate =
                            worker
                              .request(
                                ScriptCompilerWorker.CompileAny(path, _)
                              )(
                                20.seconds
                              )
                              .flatTap(result.complete)
                              .hideErrors
                              .rethrow
                              .flatMap(res =>
                                UIO(pprint.log(res)) >>
                                  UIO.pure(
                                    data
                                      .modify(_.scriptsMap)
                                      .using(_ + (path -> res))
                                  )
                              )
                          for {
                            nextData <-
                              if (force) getAndUpdate
                              else
                                data.scriptsMap.get(path) match {
                                  case Some(e) =>
                                    result
                                      .complete(e.asRight[Error])
                                      .hideErrors >> UIO.pure(data)
                                  case None => getAndUpdate
                                }
                          } yield Active -> nextData
                        case ObservableData(result) =>
                          result
                            .complete(Observable.repeatEvalF(dataVar.take))
                            .hideErrors >> IO.pure(Active -> data)
                      }

                  }
                  nextState
                    .flatTap { case (_, data) => dataVar.put(data).hideErrors }
                    .tapError(err => UIO(pprint.log(err.toString)))
                    .attempt
                    // .mapFilter(_.toOption)
                    .map(_.getOrElse(state -> data))
                    .toTask
              }
          )
        )
      } yield obs

  }

  class ScriptCompileFns(
      val scalaRunner: ammonite.Main,
      val kotlinRunner: KotlinScriptEngine,
      val groovyRunner: GroovyScriptEngine
  ) {
    def runScala(path: os.Path): Either[Error, Any] =
      scalaRunner
        .runScript(path, Seq.empty)
        ._1 match {
        case e @ Res.Exception(t, msg) => Left(AmmoniteException(e))

        case f @ Failure(msg) => Left(AmmoniteFailure(f))

        case Res.Success(obj) => Right(obj)

        case _ => Left(SomeError("Failed to run script"))
      }

    def runKotlin(path: os.Path): Either[Error, Any] =
      Either
        .catchNonFatal(kotlinRunner.eval(os.read(path)))
        .leftMap {
          case ex: ScriptException => ScriptExceptionError(ex)
        }

    def runGroovy(path: os.Path): Either[Error, Any] =
      Either
        .catchNonFatal(groovyRunner.run(path.relativeTo(os.pwd).toString, ""))
        .leftMap {
          case ex: ResourceException           => ResourceExceptionError(ex)
          case ex: groovy.util.ScriptException => GroovyScriptExceptionError(ex)
        }

    def ensureReturnedObjectNotNull(scriptObject: Any): Either[Error, Any] =
      Either.fromOption(Option(scriptObject), SomeError("unknown object"))
  }

  class ScriptCompileSource(
      fns: ScriptCompileFns,
      logger: Logger[Task],
      queue: ConcurrentQueue[Task, ScriptCompilerWorker.CompileRequest]
  ) {
    import fns._

    val source =
      Task.deferAction(implicit s =>
        Task(
          Observable
            .repeatEvalF(queue.poll)
            .doOnNextF(el => logger.debug(s"Got $el"))
            .mapParallelUnorderedF(4) {
              case ScriptCompilerWorker.CompileAny(path, result) =>
                for {
                  mbRes <- Task(
                    runScala(path)
                      .flatMap(ensureReturnedObjectNotNull)
                    // .map(_.taggedWith[ScriptTag])
                  )
                  _ <- result.complete(mbRes)
                } yield mbRes
            }
        )
      )
  }
//  override private val
  final class ScriptCompilerWorker(
      logger: Logger[Task],
      _queue: ConcurrentQueue[Task, ScriptCompilerWorker.CompileRequest]
  ) extends Requestable[ScriptCompilerWorker.CompileRequest] {

    override def queue = _queue

  }

  object ScriptCompilerWorker {

    sealed trait CompileRequest
    final case class CompileAny(
        path: os.Path,
        result: Deferred[Task, ScriptResult]
    ) extends CompileRequest

    def apply(
        logger: Logger[Task],
        scalaRunner: ammonite.Main = defaultScalaRunner,
        kotlinRunner: KotlinScriptEngine = defaultKotlinRunner,
        groovyRunner: GroovyScriptEngine = defaultGroovyRunner
    ) = {
      val acquire = for {
        queue <- ConcurrentQueue[Task].bounded[CompileRequest](10)
        fns <- UIO.pure(wire[ScriptCompileFns])
        worker = wire[ScriptCompilerWorker]
        fib <- wire[ScriptCompileSource].source.flatMap(_.completedL.toIO.start)
        // resource = Concurrent[Task].background(
        //   wire[ScriptCompileSource].source.flatMap(_.completedL.toIO)
        // )
      } yield worker -> fib
      Resource
        .make(acquire) { case worker -> fib => fib.cancel }
        .map(_._1)
    }

  }

  def apply(logger: Logger[Task]) = {
    def acquire(worker: ScriptCompilerWorker) =
      for {
        queue <- ConcurrentQueue.bounded[Task, Command](10)
        fib <- wire[SourceMaker].get.flatMap(_.completedL.toIO.start)
      } yield new ScriptCompiler(queue) -> fib

    ScriptCompilerWorker(logger)
      .flatMap(worker =>
        Resource.make(acquire(worker)) { case (_, fib) => fib.cancel }
      )
      .map(_._1)
  }

}
