package wow.doge.mygame.game

import scala.concurrent.duration._

import akka.actor.typed.ActorRef
import akka.actor.typed.Behavior
import akka.actor.typed.Props
import akka.actor.typed.SpawnProtocol
import akka.actor.typed.scaladsl.AskPattern._
import akka.util.Timeout
import cats.effect.Resource
import cats.effect.concurrent.Deferred
import com.jme3.bullet.BulletAppState
import com.jme3.input.InputManager
import com.jme3.scene.Node
import com.jme3.scene.Spatial
import com.jme3.system.AppSettings
import com.softwaremill.tagging._
import com.typesafe.scalalogging.{Logger => SLogger}
import io.odin.Logger
import monix.bio.Fiber
import monix.bio.IO
import monix.bio.Task
import monix.bio.UIO
import monix.catnap.ConcurrentChannel
import monix.catnap.ConsumerF
import monix.eval.Coeval
import wow.doge.mygame.AppError
import wow.doge.mygame.AppError.TimeoutError
import wow.doge.mygame.Dispatchers
import wow.doge.mygame.executors.JMERunner
import wow.doge.mygame.executors.Schedulers
import wow.doge.mygame.game.subsystems.ui.JFxUI
import wow.doge.mygame.implicits._
import wow.doge.mygame.subsystems.events.EventsModule.GameEventBus
import wow.doge.mygame.subsystems.events.TickEvent
import wow.doge.mygame.types._
import wow.doge.mygame.utils.AkkaUtils
import wow.doge.mygame.utils.wrappers.jme._
object GameAppTags {
  sealed trait RootNode
  sealed trait GuiNode
}

class GameApp private[game] (
    logger: Logger[Task],
    app: SimpleAppExt,
    gameActor: ActorRef[GameAppActor.Command],
    gameSpawnProtocol: ActorRef[SpawnProtocol.Command],
    akkaScheduler: AkkaScheduler
) {
  def inputManager: UIO[InputManager] = UIO(app.getInputManager())
  val assetManager = new AssetManager(app.getAssetManager())
  val guiNode: GuiNode =
    AppNode2(app.getGuiNode()).taggedWith[GameAppTags.GuiNode]
  // def flyCam = Option(app.getFlyByCamera())
  def camera = UIO(app.getCamera())
  def viewPort = UIO(app.getViewPort())
  val rootNode: RootNode =
    AppNode2(app.getRootNode()).taggedWith[GameAppTags.RootNode]
  val physicsSpace =
    new PhysicsSpace(app.bulletAppState.physicsSpace)
  def enqueue(cb: () => Unit) = app.enqueueR(() => cb())
  def enqueueL[T](cb: () => T): Task[T] = app.enqueueL(cb)

  def whenTerminated: IO[AppError, Unit] =
    IO.deferFuture(app.whenTerminated).onErrorHandleWith(TimeoutError.from)

  def spawnGameActor[T](
      behavior: Behavior[T],
      actorName: Option[String] = None,
      props: Props = Dispatchers.jmeDispatcher
  )(implicit name: sourcecode.Name) =
    AkkaUtils.spawnActorL(behavior, actorName, props)(
      2.seconds,
      akkaScheduler.value,
      gameSpawnProtocol,
      name
    )

  def scheduler = JmeScheduler(app.scheduler)
  def jfxUI = JFxUI(app)

}

class GameAppResource(
    logger: Logger[Task],
    jmeThread: JmeScheduler,
    schedulers: Schedulers,
    tickEventBus: GameEventBus[TickEvent]
)(implicit
    timeout: Timeout,
    scheduler: AkkaScheduler,
    spawnProtocol: ActorRef[SpawnProtocol.Command]
) {
  implicit val as = scheduler.value
  def resource
      : Resource[UIO, Either[AppError, (GameApp, Fiber[Nothing, Unit])]] =
    Resource.make(
      (for {
        app <- UIO(new SimpleAppExt(schedulers, new BulletAppState))
        _ <- UIO(JMERunner.runner = app)
        _ <- UIO {
          val settings = new AppSettings(true)
          settings.setVSync(true)

          /**
            * disables the launcher
            * We'll be making our own launcher anyway
            */
          app.setShowSettings(false)
          app.setSettings(settings)
        }

        fib <- UIO(app.start).executeOn(jmeThread.value).start
        _ <- Task.deferFuture(app.started).onErrorHandleWith(TimeoutError.from)
        gameAppActor <- AkkaUtils.spawnActorL(
          new GameAppActor.Props(tickEventBus).behavior,
          Some("testGameActor"),
          props = Dispatchers.jmeDispatcher
        )
        _ <- gameAppActor !! GameAppActor.Start
        sp <-
          gameAppActor
            .askL(GameAppActor.GetSpawnProtocol)
            .onErrorHandleWith(TimeoutError.from)
        gameApp <- UIO(new GameApp(logger, app, gameAppActor, sp, scheduler))
        _ <- UIO {
          val fut = () => gameAppActor.ask(GameAppActor.Stop).flatten
          app.cancelToken = Some(fut)
        }
      } yield (gameApp, fib)).attempt
    ) {
      case Right(gameApp -> fib) => fib.cancel
      case Left(error)           => IO.terminate(new Exception(error.toString))
    }
}

object GameApp {}

object Ops {
  final class AddToNode[T <: Node](private val node: T) extends AnyVal {

    /**
      * Pure version
      */
    def apply(spatial: Spatial)(implicit logger: Logger[Task]) =
      logger.debug(
        s"Request to add spatial with name ${spatial.getName()} to node ${node.getName()}"
      ) >> Task(node.attachChild(spatial))

    /**
      * Impure version
      */
    def apply(spatial: Spatial)(implicit logger: SLogger) =
      Coeval {
        logger.debug(
          s"Request to add spatial with name ${spatial.getName()} to node ${node.getName()}"
        )
        node.attachChild(spatial)
      }
  }
}

object SpawnSystem {
  sealed trait Result
  case object Ok extends Result

  sealed trait Complete
  case object Complete extends Complete

  sealed trait SpawnRequest
  final case class SpawnSpatial(nodeTask: Task[Node]) extends SpawnRequest

  final case class SpawnRequestWrapper(
      spawnRequest: SpawnRequest,
      result: Deferred[Task, Result]
  )

  def apply(logger: Logger[Task]) =
    for {
      spawnChannel <- ConcurrentChannel[Task].of[Complete, SpawnRequestWrapper]
      spawnSystem <- Task(new SpawnSystem(logger, spawnChannel))
      consumer <-
        spawnChannel.consume
          .use(consumer => spawnSystem.receive(consumer))
          .startAndForget
    } yield (spawnSystem)
}

class SpawnSystem(
    logger: Logger[Task],
    spawnChannel: ConcurrentChannel[
      Task,
      SpawnSystem.Complete,
      SpawnSystem.SpawnRequestWrapper
    ]
) {
  import SpawnSystem._

  for {
    spawnSystem <- SpawnSystem(logger)
    res <- spawnSystem.request(SpawnSpatial(Task(new Node("Test"))))
  } yield ()

  // val spawnChannel = ConcurrentChannel[Task].of[Result, SpawnRequest]

  private def receive(
      consumer: ConsumerF[Task, Complete, SpawnRequestWrapper]
  ): Task[Unit] =
    consumer.pull.flatMap {
      case Right(message) =>
        for {
          _ <-
            logger
              .debug(s"Received spawn request $message")
          _ <- handleSpawn(message)
        } yield receive(consumer)
      case Left(r) =>
        logger.info("Closing Spawn System")
    }

  private def handleSpawn(spawnRequestWrapper: SpawnRequestWrapper) =
    spawnRequestWrapper match {
      case SpawnRequestWrapper(spawnRequest, result) =>
        spawnRequest match {
          case SpawnSpatial(spatialTask) =>
            spatialTask.flatMap(spatial =>
              logger.debug(
                s"Spawning spatial with name ${spatial.getName()}"
              ) >> result
                .complete(Ok)
            )

        }
    }

  def request(spawnRequest: SpawnRequest) =
    for {
      d <- Deferred[Task, Result]
      _ <- spawnChannel.push(SpawnRequestWrapper(spawnRequest, d))
      res <- d.get
    } yield (res)

  def stop = spawnChannel.halt(Complete)
}
