package wow.doge.mygame.game

import cats.effect.concurrent.Deferred
import com.jme3.app.state.AppStateManager
import com.jme3.asset.AssetManager
import com.jme3.input.InputManager
import com.jme3.scene.Node
import com.jme3.scene.Spatial
import com.softwaremill.tagging._
import com.typesafe.scalalogging.{Logger => SLogger}
import io.odin.Logger
import monix.bio.IO
import monix.bio.Task
import monix.catnap.ConcurrentChannel
import monix.catnap.ConsumerF
import monix.catnap.Semaphore
import monix.eval.Coeval
import wow.doge.mygame.game.subsystems.ui.JFxUI

sealed trait Error
case object FlyCamNotExists extends Error

object GameAppTags {
  sealed trait RootNode
  sealed trait GuiNode
}

class GameApp(logger: Logger[Task], val app: SimpleAppExt) {
  import Ops._

  def stateManager: Task[AppStateManager] = Task(app.getStateManager())
  def inputManager: Task[InputManager] = Task(app.getInputManager())
  def assetManager: Task[AssetManager] = Task(app.getAssetManager())
  def guiNode = Task(app.getGuiNode().taggedWith[GameAppTags.GuiNode])
  def addToGuiNode = guiNode.flatMap(rn => Task(new AddToNode(rn)))
  def flyCam =
    IO(app.getFlyByCamera()).onErrorHandleWith(_ =>
      IO.raiseError(FlyCamNotExists)
    )
  def camera = Task(app.getCamera())
  def viewPort = Task(app.getViewPort())
  def rootNode = Task(app.getRootNode().taggedWith[GameAppTags.RootNode])
  // def rootNode2 = SynchedObject(app.getRootNode())
  def addToRootNode = rootNode.flatMap(rn => Task(new AddToNode(rn)))
  def enqueue(cb: () => Unit) =
    app.enqueue(new Runnable {
      override def run() = cb()
    })
  def enqueueL[T](cb: () => T): Task[T] = app.enqueueL(cb)

  def start = Task(app.start())
  def stop = Task(app.stop())
  def scheduler = app.scheduler
  def jfxUI = JFxUI(app)

}

object GameApp {

  class WrappedNode(node: Node, lock: Semaphore[Task]) {

    def +=(spat: Spatial) = lock.withPermit(Task(node.attachChild(spat)))
  }

  /**
    * Synchronization wrapper for a mutable object
    *
    * @param obj the mutable object
    * @param lock lock for synchronization
    */
  class SynchedObject[A](obj: A, lock: Semaphore[Task]) {
    def modify(f: A => Unit): Task[Unit] =
      lock.withPermit(Task(f(obj)))

    def flatModify(f: A => Task[Unit]): Task[Unit] =
      lock.withPermit(f(obj))

    def get: Task[A] = lock.withPermit(Task(obj))
  }

  object SynchedObject {
    def apply[A](obj: A) =
      Semaphore[Task](1).flatMap(lock => Task(new SynchedObject(obj, lock)))
  }

}

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
  final case object Ok extends Result

  sealed trait Complete
  final 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)
}
