package wow.doge.mygame

import akka.actor.typed.ActorSystem
import akka.actor.typed.Scheduler
import akka.actor.typed.SpawnProtocol
import akka.util.Timeout
import cats.effect.Resource
import cats.effect.concurrent.Deferred
import cats.syntax.eq._
import com.jme3.app.state.AppStateManager
import com.jme3.asset.AssetManager
import com.jme3.asset.plugins.ZipLocator
import com.jme3.bullet.control.BetterCharacterControl
import com.jme3.input.InputManager
import com.jme3.renderer.Camera
import com.jme3.renderer.RenderManager
import com.jme3.renderer.ViewPort
import com.jme3.scene.Node
import com.jme3.scene.control.AbstractControl
import com.softwaremill.macwire._
import com.softwaremill.tagging._
import io.odin.Logger
import monix.bio.Fiber
import monix.bio.IO
import monix.bio.Task
import monix.execution.exceptions.DummyException
import scalafx.scene.control.TextArea
import wow.doge.mygame.executors.Schedulers
import wow.doge.mygame.game.GameApp
import wow.doge.mygame.game.GameAppActor
import wow.doge.mygame.game.GameAppTags
import wow.doge.mygame.game.entities.EntityIds
import wow.doge.mygame.game.entities.NpcActorSupervisor
import wow.doge.mygame.game.entities.NpcMovementActor
import wow.doge.mygame.game.entities.PlayerController
import wow.doge.mygame.game.entities.PlayerControllerTags
import wow.doge.mygame.game.subsystems.input.GameInputHandler
import wow.doge.mygame.implicits._
import wow.doge.mygame.launcher.Launcher
import wow.doge.mygame.launcher.Launcher.LauncherResult
import wow.doge.mygame.math.ImVector3f
import wow.doge.mygame.subsystems.events.EventsModule
import wow.doge.mygame.subsystems.events.EventsModule.GameEventBus
import wow.doge.mygame.subsystems.events.PlayerEvent
import wow.doge.mygame.subsystems.events.TickEvent
import wow.doge.mygame.subsystems.scriptsystem.ScriptInitMode
import wow.doge.mygame.subsystems.scriptsystem.ScriptSystemResource
import wow.doge.mygame.utils.AkkaUtils
import wow.doge.mygame.utils.GenericConsoleStream
import wow.doge.mygame.utils.wrappers.jme.AppNode
import wow.doge.mygame.game.subsystems.level.DefaultGameLevel
import com.jme3.math.FastMath

class MainApp(
    logger: Logger[Task],
    jmeThread: monix.execution.Scheduler,
    schedulers: Schedulers,
    consoleStream: GenericConsoleStream[TextArea]
)(implicit
    spawnProtocol: ActorSystem[SpawnProtocol.Command],
    @annotation.unused timeout: Timeout,
    @annotation.unused scheduler: Scheduler
) {

  val scriptSystemInit =
    new ScriptSystemResource(os.pwd, ScriptInitMode.Eager).init

  val eventsModule = new EventsModule(spawnProtocol)

  def gameInit: Resource[Task, Fiber[Throwable, Unit]] =
    GameApp.resource(logger, jmeThread, schedulers).evalMap {
      case gameApp -> gameAppFib =>
        for {
          playerEventBus <- eventsModule.playerEventBusTask
          mainEventBus <- eventsModule.mainEventBusTask
          tickEventBus <- eventsModule.tickEventBusTask
          gameAppActor <- AkkaUtils.spawnActorL(
            GameAppActor.Props(tickEventBus).behavior,
            "gameAppActor"
          )
          _ <- gameAppActor !! GameAppActor.Start
          inputManager <- gameApp.inputManager
          assetManager <- gameApp.assetManager
          stateManager <- gameApp.stateManager
          camera <- gameApp.camera
          rootNode <- Task.pure(gameApp.rootNode)
          enqueueR <- Task(gameApp.enqueue _)
          viewPort <- gameApp.viewPort
          _ <- logger.info("before")
          // jfxUI <- gameApp.jfxUI
          consoleTextArea <- Task(new TextArea {
            text = "hello \n"
            editable = false
            wrapText = true
            // maxHeight = 150
            // maxWidth = 300
          })
          // _ <- Task(consoleStream := consoleTextArea)
          // _ <- Task(jfxUI += consoleTextArea)
          _ <- logger.info("after")
          // bulletAppState <- Task(new BulletAppState())
          // _ <- Task(stateManager.attach(bulletAppState))
          // bulletAppState <- Task.pure(gameApp.bulletAppstate)
          _ <- logger.info("Initializing console stream")
          _ <-
            wire[MainAppDelegate]
              .init()
              .executeOn(gameApp.scheduler)
        } yield gameAppFib
    }

  val program = for {
    scriptSystem <- scriptSystemInit
    launchSignal <- Deferred[Task, Launcher.LauncherResult]
    launcher <- new Launcher.Props(schedulers, launchSignal).create
    launchResult <- launcher.init.use(_ => launchSignal.get)
    _ <-
      /**
        * User chose to quit
        */
      if (launchResult === LauncherResult.Exit)
        logger.info("Exiting")
      /**
        * User chose launch. Wait for game window to close
        */
      else
        gameInit.use(_.join)
  } yield ()
}

/**
  * Class with all dependencies in one place for easy wiring
  */
class MainAppDelegate(
    gameApp: GameApp,
    loggerL: Logger[Task],
    playerEventBus: GameEventBus[PlayerEvent],
    tickEventBus: GameEventBus[TickEvent],
    inputManager: InputManager,
    assetManager: AssetManager,
    stateManager: AppStateManager,
    camera: Camera,
    viewPort: ViewPort,
    enqueueR: Function1[() => Unit, Unit],
    rootNode: AppNode[Task] @@ GameAppTags.RootNode
    // bulletAppState: BulletAppState
)(implicit
    spawnProtocol: ActorSystem[SpawnProtocol.Command],
    @annotation.unused timeout: Timeout,
    @annotation.unused scheduler: Scheduler
) {
  // val physicsSpace = bulletAppState.physicsSpace
  val physicsSpace = gameApp.physicsSpace
  def init(
      // appScheduler: monix.execution.Scheduler
      // consoleStream: GenericConsoleStream[TextArea]
  ) =
    for {
      _ <- loggerL.info("Initializing Systems")
      _ <- loggerL.debug(physicsSpace.toString())
      _ <- Task(
        assetManager.registerLocator(
          os.rel / "assets" / "town.zip",
          classOf[ZipLocator]
        )
      )
      _ <- loggerL.info("test")
      // _ <- Task(consoleStream.println("text"))
      _ <- DefaultGameLevel(assetManager, viewPort)
        .addToGame(rootNode, physicsSpace)
      _ <- createPlayerController()
        .absorbWith(e => DummyException(e.toString()))
      // .onErrorRestart(3)
      _ <- wire[GameInputHandler.Props].begin
      // .onErrorRestart(3)
      // johnActor <- createTestNpc(appScheduler, "John").executeOn(appScheduler)
      // _ <- johnActor !! NpcActorSupervisor.Move(ImVector3f(0, 0, 20))

      // _ <-
      //   (johnActor !! NpcActorSupervisor.Move(
      //     ImVector3f(-30, 0, 10)
      //   )).executeAsync
      //     .delayExecution(2.seconds)
      // _ <-
      //   IOUtils
      //     .toIO(
      //       rootNode
      //         .observableBreadthFirst()
      //         .doOnNext(spat => IOUtils.toTask(loggerL.debug(spat.getName())))
      //         .completedL
      //     )
      //     .executeOn(appScheduler)
      //     .startAndForget
    } yield ()

  def createPlayerController(
      // appScheduler: monix.execution.Scheduler
  ): IO[PlayerController.Error, Unit] = {
    val playerPos = ImVector3f.ZERO
    val modelPath = os.rel / "Models" / "Jaime" / "Jaime.j3o"
    val playerPhysicsControl =
      PlayerController.Defaults.defaultPlayerPhysicsControl
        .taggedWith[PlayerControllerTags.PlayerTag]
    // lazy val camNode =
    //   PlayerController.Defaults
    //     .defaultCamerNode(camera, playerPos)
    // .taggedWith[PlayerControllerTags.PlayerCameraNode]
    val playerModel = assetManager
      .loadModel(modelPath)
      .asInstanceOf[Node]
      .withRotate(0, FastMath.PI, 0)
    val mbPlayerNode = PlayerController.Defaults
      .defaultPlayerNode(
        playerPos,
        playerModel,
        playerPhysicsControl
      )
    val cameraPivotNode = new Node(EntityIds.CameraPivot.value)
      .taggedWith[PlayerControllerTags.PlayerCameraPivotNode]

    for {
      playerNode <- IO.fromEither(mbPlayerNode)
      _ <- IO(cameraPivotNode.addControl(new FollowControl(playerNode)))
        .onErrorHandleWith(e =>
          IO.raiseError(PlayerController.GenericError(e.getMessage()))
        )
      camNode <- IO(
        PlayerController.Defaults
          .defaultCamerNode(camera, cameraPivotNode, playerNode, playerPos)
      ).onErrorHandleWith(e =>
        IO.raiseError(PlayerController.GenericError(e.getMessage()))
      ).map(_.taggedWith[PlayerControllerTags.PlayerCameraNode])
      // _ <- Task {
      //   val chaseCam = new ChaseCamera(camera, playerNode, inputManager)
      //   chaseCam.setSmoothMotion(false)
      //   chaseCam.setLookAtOffset(new Vector3f(0, 1.5f, 10))
      //   chaseCam
      // }
      //   .onErrorHandleWith(e =>
      //     IO.raiseError(PlayerController.GenericError(e.getMessage()))
      //   )
      _ <- wire[PlayerController.Props].create
    } yield ()
  }
  def createTestNpc(
      // appScheduler: monix.execution.Scheduler,
      npcName: String
  ) = {
    val initialPos = ImVector3f(50, 5, 0)
    val npcPhysicsControl = new BetterCharacterControl(1.5f, 6f, 1f)
      // (1f, 2.1f, 10f)
      .withJumpForce(ImVector3f(0, 5f, 0))
    val mbNpcNode = PlayerController.Defaults.defaultNpcNode(
      assetManager,
      initialPos,
      npcPhysicsControl,
      npcName
    )
    val npcActorTask = AkkaUtils.spawnActorL(
      NpcActorSupervisor
        .Props(
          new NpcMovementActor.Props(
            enqueueR,
            initialPos,
            // tickEventBus,
            npcPhysicsControl
          ).behavior,
          npcName,
          initialPos
        )
        .behavior,
      s"${npcName}-npcActorSupervisor"
    )
    // .taggedWith[PlayerControllerTags.PlayerTag]

    for {
      npcNode <- IO.fromEither(mbNpcNode)
      npcActor <- npcActorTask
      // _ <- IO {
      //   physicsSpace += npcPhysicsControl
      //   physicsSpace += npcNode
      //   // rootNode += npcNode
      // }
      _ <- physicsSpace += npcPhysicsControl
      _ <- physicsSpace += npcNode
      _ <- rootNode += npcNode
    } yield npcActor
  }

}

class FollowControl(playerNode: Node) extends AbstractControl {
  override def controlUpdate(tpf: Float): Unit = {
    this.spatial.setLocalTranslation(playerNode.getLocalTranslation())
  }
  override def controlRender(
      rm: RenderManager,
      vp: ViewPort
  ): Unit = {}
}
