package wow.doge.mygame.state

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

import akka.actor.typed.ActorRef
import akka.actor.typed.Behavior
import akka.actor.typed.LogOptions
import akka.actor.typed.scaladsl.ActorContext
import akka.actor.typed.scaladsl.Behaviors
import ammonite.Main
import ammonite.main.Defaults
import ammonite.runtime.Storage.Folder
import ammonite.util.Res.Success
import cats.implicits._
import com.softwaremill.tagging._
import com.typesafe.scalalogging.Logger
import groovy.util.GroovyScriptEngine
import org.slf4j.event.Level

object ScriptActor {

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

  trait Kotlin
  type KotlinScriptEngine = ScriptEngine @@ Kotlin

  final case class Error(reason: String)
  sealed trait Command
  final case class CompileAny(
      path: os.Path,
      result: ActorRef[Either[Error, ScriptObject]]
  ) extends Command

  final case class CompileAll(
      paths: Seq[os.Path],
      result: ActorRef[Map[os.Path, Either[Error, Any]]]
  ) extends Command

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

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

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

  def apply(
      scalaRunner: Main = defaultScalaRunner,
      kotlinRunner: KotlinScriptEngine = defaultKotlinRunner,
      groovyRunner: GroovyScriptEngine = defaultGroovyRunner
  ): Behavior[ScriptActor.Command] =
    Behaviors.logMessages(
      LogOptions()
        .withLevel(Level.TRACE)
        .withLogger(
          Logger[ScriptActor].underlying
        ),
      Behaviors.setup(ctx =>
        new ScriptActor(
          scalaRunner,
          kotlinRunner,
          groovyRunner,
          ctx
        ).receiveMessage
      )
    )

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

  def determineScriptType(path: os.Path): Either[Error, ScriptType] =
    path.toString match {
      case s if s.endsWith(".sc")       => Right(ScalaType)
      case s if s.endsWith(".main.kts") => Right(KotlinType)
      case s if s.endsWith(".groovy")   => Right(GroovyType)
      case _                            => Left(Error("Unknown script type"))
    }

  def runScala(path: os.Path, scalaRunner: ammonite.Main): Either[Error, Any] =
    scalaRunner
      .runScript(
        path,
        Seq.empty
      )
      ._1 match {
      case ammonite.util.Res.Exception(t, msg) => Left(Error(msg))

      case Success(obj) => Right(obj)

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

  def runKotlin(
      path: os.Path,
      kotlinRunner: KotlinScriptEngine
  ): Either[Error, Any] =
    Either
      .catchNonFatal(kotlinRunner.eval(os.read(path)))
      .leftMap(t => Error(t.getMessage()))

  def runGroovy(
      path: os.Path,
      groovyRunner: GroovyScriptEngine
  ): Either[Error, Any] =
    Either
      .catchNonFatal(groovyRunner.run(path.relativeTo(os.pwd).toString(), ""))
      .leftMap(t => Error(t.getMessage()))

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

}

class ScriptActor(
    val scalaRunner: ammonite.Main,
    val kotlinRunner: ScriptActor.KotlinScriptEngine,
    val groovyRunner: GroovyScriptEngine,
    context: ActorContext[ScriptActor.Command]
) {
  import ScriptActor._

  def receiveMessage: Behavior[Command] =
    Behaviors.receiveMessage {
      case CompileAny(path, requester) =>
        context.log.debug(s"Received $path")
        val res = getScript(path)
        context.log.debug(s"result = $res")
        requester ! res
        Behaviors.same

      case CompileAll(paths, requester) =>
        context.log.debug(s"Received $paths")
        requester ! compileAll(paths)
        Behaviors.same
    }

  def getScript(path: os.Path): Either[Error, ScriptObject] =
    determineScriptType(path) match {
      case Right(ScalaType) =>
        runScala(path, scalaRunner)
          .flatMap(ensureReturnedObjectNotNull)
          .map(_.taggedWith[ScriptTag])
      case Right(KotlinType) =>
        runKotlin(path, kotlinRunner)
          .flatMap(ensureReturnedObjectNotNull)
          .map(_.taggedWith[ScriptTag])
      case Right(GroovyType) =>
        runGroovy(path, groovyRunner)
          .flatMap(ensureReturnedObjectNotNull)
          .map(_.taggedWith[ScriptTag])
      case l @ Left(err) => l.map(_.taggedWith[ScriptTag])
    }

  type LOL = Map[os.Path, Either[wow.doge.mygame.state.ScriptActor.Error, Any]]

  def compileAll(
      paths: Seq[os.Path]
  ): LOL = {
    @annotation.tailrec
    def loop(
        paths: Seq[os.Path],
        scriptsMap: Map[
          os.Path,
          Either[wow.doge.mygame.state.ScriptActor.Error, Any]
        ]
    ): LOL = {
      paths match {
        case head :: next => loop(next, scriptsMap + (head -> getScript(head)))
        case Nil          => scriptsMap
      }
    }
    loop(paths, Map.empty)
  }

}
