package wow.doge.mygame

import scala.concurrent.duration._

import akka.actor.typed.ActorRef
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.asset.plugins.ZipLocator
import com.jme3.bullet.control.BetterCharacterControl
import com.jme3.input.InputManager
import com.jme3.material.Material
import com.jme3.material.MaterialDef
import com.jme3.math.ColorRGBA
import com.jme3.math.FastMath
import com.jme3.math.Quaternion
import com.jme3.math.Vector3f
import com.jme3.renderer.Camera
import com.jme3.renderer.ViewPort
import com.jme3.scene.Node
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.bio.UIO
import monix.eval.Coeval
import monix.execution.exceptions.DummyException
import monix.reactive.Observable
import scalafx.scene.control.TextArea
import wow.doge.mygame.AppError.TimeoutError
import wow.doge.mygame.executors.Schedulers
import wow.doge.mygame.game.GameApp
import wow.doge.mygame.game.GameAppResource
import wow.doge.mygame.game.controls.FollowControl
import wow.doge.mygame.game.entities.CharacterStats
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.PlayerActorSupervisor
import wow.doge.mygame.game.entities.PlayerController
import wow.doge.mygame.game.subsystems.input.GameInputHandler
import wow.doge.mygame.game.subsystems.input.PlayerCameraInput
import wow.doge.mygame.game.subsystems.level.DefaultGameLevel
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.Event
import wow.doge.mygame.subsystems.events.EventBus
import wow.doge.mygame.subsystems.events.EventBus.ObservableSubscription
import wow.doge.mygame.subsystems.events.EventsModule
import wow.doge.mygame.subsystems.events.EventsModule.GameEventBus
import wow.doge.mygame.subsystems.events.PlayerCameraEvent
import wow.doge.mygame.subsystems.events.PlayerEvent
import wow.doge.mygame.subsystems.events.PlayerMovementEvent
import wow.doge.mygame.subsystems.events.StatsEvent.DamageEvent
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.types._
import wow.doge.mygame.utils.AkkaUtils
import wow.doge.mygame.utils.GenericConsoleStream
import wow.doge.mygame.utils.IOUtils
import wow.doge.mygame.utils.wrappers.jme.AssetManager
import wow.doge.mygame.utils.wrappers.jme.PhysicsSpace
class MainApp(
    logger: Logger[Task],
    jmeThread: JmeScheduler,
    schedulers: Schedulers,
    consoleStream: GenericConsoleStream[TextArea]
)(implicit
    spawnProtocol: ActorRef[SpawnProtocol.Command],
    timeout: Timeout,
    scheduler: AkkaScheduler
) {
  implicit val as = scheduler.value

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

  val eventsModule = new EventsModule(scheduler, spawnProtocol)

  def eval(
      tickEventBus: GameEventBus[TickEvent],
      gameApp: GameApp,
      fib: Fiber[Nothing, Unit]
  ) =
    for {
      // g <- UIO.pure(gameApp)
      playerEventBus <- eventsModule.playerEventBus
      mainEventBus <- eventsModule.mainEventBus
      obs <-
        playerEventBus
          .askL[Observable[PlayerMovementEvent]](ObservableSubscription(_))
          .onErrorHandleWith(TimeoutError.from)
      _ <-
        IOUtils
          .toIO(
            obs
              .doOnNextF(pme => Coeval(pprint.log(s"Received event $pme")).void)
              .completedL
              .startAndForget
          )
          .hideErrors
      inputManager <- gameApp.inputManager
      assetManager <- UIO.pure(gameApp.assetManager)
      camera <- gameApp.camera
      rootNode <- UIO.pure(gameApp.rootNode)
      enqueueR <- UIO(gameApp.enqueue _)
      viewPort <- gameApp.viewPort
      physicsSpace <- UIO.pure(gameApp.physicsSpace)
      _ <- logger.infoU("before")
      // jfxUI <- gameApp.jfxUI
      consoleTextArea <- UIO(new TextArea {
        text = "hello \n"
        editable = false
        wrapText = true
        // maxHeight = 150
        // maxWidth = 300
      })
      // _ <- Task(consoleStream := consoleTextArea)
      // _ <- Task(jfxUI += consoleTextArea)
      _ <- logger.infoU("after")
      _ <- logger.infoU("Initializing console stream")
      _ <-
        wire[MainAppDelegate]
          .init()
          .executeOn(gameApp.scheduler.value)
    } yield fib

  def gameInit(
      tickEventBus: GameEventBus[TickEvent]
  ): Resource[UIO, Either[AppError, Fiber[Nothing, Unit]]] =
    wire[GameAppResource].resource.evalMap {
      case Right(gameApp -> gameAppFib) =>
        eval(tickEventBus, gameApp, gameAppFib).attempt
      case Left(error) => IO.terminate(new Exception(error.toString))
    }

  val program = for {
    // scriptSystem <- scriptSystemInit
    launchSignal <- Deferred[Task, Launcher.LauncherResult].hideErrors
    launcher <- new Launcher.Props(schedulers.fx, launchSignal).create
    launchResult <-
      launcher.init
        .use(_ => launchSignal.get)
        .hideErrors
    tickEventBus <-
      eventsModule.tickEventBus.hideErrorsWith(e => DummyException(e.toString))
    _ <-
      /**
        * User chose to quit
        */
      if (launchResult === LauncherResult.Exit)
        logger.infoU("Exiting")
      /**
        * User chose launch. Wait for game window to close
        */
      else
        gameInit(tickEventBus).use {
          case Right(fib)  => fib.join >> Task.unit
          case Left(error) => IO.terminate(new Exception(error.toString))
        }.hideErrors

  } yield ()
}

/**
  * Class with all dependencies in one place for easy wiring
  */
class MainAppDelegate(
    gameApp: GameApp,
    loggerL: Logger[Task],
    mainEventBus: GameEventBus[Event],
    playerEventBus: GameEventBus[PlayerEvent],
    tickEventBus: GameEventBus[TickEvent],
    inputManager: InputManager,
    assetManager: AssetManager,
    physicsSpace: PhysicsSpace,
    camera: Camera,
    viewPort: ViewPort,
    enqueueR: Function1[() => Unit, Unit],
    rootNode: RootNode,
    schedulers: Schedulers
)(implicit
    spawnProtocol: ActorRef[SpawnProtocol.Command],
    timeout: Timeout,
    scheduler: AkkaScheduler
) {
  implicit val as = scheduler.value

  def init(
      // appScheduler: monix.execution.Scheduler
      // consoleStream: GenericConsoleStream[TextArea]
  ): IO[AppError, Unit] =
    for {
      _ <- loggerL.infoU("Initializing Systems")
      _ <- assetManager.registerLocator(
        os.rel / "assets" / "town.zip",
        classOf[ZipLocator]
      )
      _ <- loggerL.infoU("test")
      // _ <- Task(consoleStream.println("text"))
      level <- DefaultGameLevel(assetManager, viewPort)
      _ <- level.addToGame(rootNode, physicsSpace)
      playerActor <- createPlayerController()
      // .onErrorRestart(3)
      _ <- wire[GameInputHandler.Props].begin
      // .onErrorRestart(3)
      johnActor <- createTestNpc("John")
      // _ <- johnActor !! NpcActorSupervisor.Move(ImVector3f(0, 0, 20))

      // _ <-
      //   johnActor
      //     .tellL(NpcActorSupervisor.Move(ImVector3f(-30, 0, 10)))
      //     .delayExecution(2.seconds)
      _ <-
        rootNode.depthFirstTraversal
          .doOnNextF(spat => loggerL.debug(spat.getName).toTask)
          .completedL
          .toIO
          .hideErrors
      damageObs <-
        mainEventBus
          .askL[Observable[DamageEvent]](ObservableSubscription(_))
          .onErrorHandleWith(TimeoutError.from)
      _ <-
        damageObs
          .doOnNextF(event =>
            (loggerL.debug(s"Received Damage Event $event") >>
              (if (event.victimName === "PlayerNode")
                 //  playerActor !! PlayerActorSupervisor.TakeDamage(event.amount)
                 playerActor.askL(
                   PlayerActorSupervisor.TakeDamage2(event.amount, _)
                 )
               else IO.unit)).toTask
          )
          .completedL
          .toIO
          .hideErrors
          .startAndForget
      _ <-
        Observable
          .interval(1.second)
          .doOnNextF(_ =>
            playerActor
              .askL(PlayerActorSupervisor.GetStatus)
              .flatMap(s => loggerL.debug(s"Player actor status: $s"))

              // .flatMap(s =>
              //   if (s == Status.Alive)
              //     playerActor
              //       .askL(PlayerActorSupervisor.CurrentStats )
              //       .flatMap(s => loggerL.debug(s"Got state $s"))
              //   else IO.unit
              // )
              .toTask
          )
          // .doOnNextF(_ =>
          //   playerActor
          //     .askL(PlayerActorSupervisor.GetStatus )
          //     .flatMap(s => loggerL.debug(s"Player actor status: $s"))
          //     .toTask
          // )
          .completedL
          .toIO
          .hideErrors
          .startAndForget
      _ <-
        physicsSpace.collisionObservable
          // .filter(event =>
          //   (for {
          //     nodeA <- event.nodeA
          //     nodeB <- event.nodeB
          //   } yield nodeA.getName === "PlayerNode" && nodeB.getName === "John" ||
          //     nodeB.getName === "PlayerNode" && nodeA.getName === "John")
          //     .getOrElse(false)
          // )
          // .doOnNextF(event =>
          //   loggerL
          //     .debug(s"$event ${event.appliedImpulse()}")
          //     .toTask
          // )
          .filter(_.nodeA.map(_.getName =!= "main-scene_node").getOrElse(false))
          .filter(_.nodeB.map(_.getName =!= "main-scene_node").getOrElse(false))
          .doOnNextF(event =>
            (for {
              victim <- Coeval(for {
                nodeA <- event.nodeA
                nodeB <- event.nodeB
              } yield if (nodeB.getName === "John") nodeA else nodeB)
              _ <- Coeval(
                victim.foreach { v =>
                  pprint.log(s"emitted event ${v.getName}")
                  mainEventBus ! EventBus.Publish(
                    DamageEvent(
                      "John",
                      v.getName,
                      CharacterStats.DamageHealth(10)
                    ),
                    "damageHandler"
                  )
                }
              )
            } yield ()).void
          )
          .completedL
          .toIO
          .hideErrors
          .startAndForget
      // _ <-
      //   IOUtils
      //     .toIO(
      //       rootNode
      //         .observableBreadthFirst()
      //         .doOnNext(spat => IOUtils.toTask(loggerL.debug(spat.getName())))
      //         .completedL
      //     )
      //     .executeOn(appScheduler)
      //     .startAndForget
    } yield ()

  def createPlayerController(
      // appScheduler: monix.execution.Scheduler
  ): IO[AppError, PlayerActorSupervisor.Ref] = {
    val playerPos = ImVector3f.Zero
    val modelPath = os.rel / "Models" / "Jaime" / "Jaime.j3o"
    // val modelPath = os.rel / "Models" / "Oto" / "Oto.mesh.xml"
    val playerPhysicsControl =
      PlayerController.Defaults.defaultPlayerPhysicsControl

    for {
      playerModel <-
        assetManager
          .loadModelAs[Node](modelPath)
          .map(_.withRotate(0, FastMath.PI, 0))
          .tapEval(m => UIO(m.center()))
          .mapError(AppError.AssetManagerError)
      playerNode <- UIO(
        PlayerController.Defaults
          .defaultPlayerNode(
            playerPos,
            playerModel,
            playerPhysicsControl
          )
      )
      cameraPivotNode <- UIO(
        new Node(EntityIds.CameraPivot.value)
          .withControl(new FollowControl(playerNode))
          .taggedWith[PlayerController.Tags.PlayerCameraPivotNode]
      )
      camNode <- UIO(
        PlayerController.Defaults
          .defaultCamerNode(camera, playerPos)
          .taggedWith[PlayerController.Tags.PlayerCameraNode]
      )
      playerCameraEvents <-
        playerEventBus
          .askL[Observable[PlayerCameraEvent]](ObservableSubscription(_))
          .onErrorHandleWith(TimeoutError.from)
      _ <-
        inputManager
          .enumAnalogObservable(PlayerCameraInput)
          .sample(1.millis)
          .scan(new Quaternion) {
            case (rotationBuf, action) =>
              action.binding match {
                // case PlayerCameraEvent.CameraLeft =>
                case PlayerCameraInput.CameraRotateLeft =>
                  // me.Task {
                  val rot = rotationBuf
                    .fromAngleAxis(1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_Y)
                  cameraPivotNode.rotate(rot)
                  rotationBuf
                // }
                // case PlayerCameraEvent.CameraRight =>
                case PlayerCameraInput.CameraRotateRight =>
                  // me.Task {
                  val rot = rotationBuf
                    .fromAngleAxis(-1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_Y)
                  cameraPivotNode.rotate(rot)
                  rotationBuf
                // }
                // case PlayerCameraEvent.CameraMovedUp =>
                case PlayerCameraInput.CameraRotateUp =>
                  // me.Task {
                  val rot = rotationBuf
                    .fromAngleAxis(-1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_X)
                  cameraPivotNode.rotate(rot)
                  rotationBuf
                // }
                // case PlayerCameraEvent.CameraMovedDown =>
                case PlayerCameraInput.CameraRotateDown =>
                  // me.Task {
                  val rot = rotationBuf
                    .fromAngleAxis(1 * FastMath.DEG_TO_RAD, Vector3f.UNIT_X)
                  cameraPivotNode.rotate(rot)
                  rotationBuf
                // }
              }
          }
          .completedL
          .toIO
          .hideErrors
          .startAndForget
      // _ <-
      //   Observable
      //     .interval(10.millis)
      //     .doOnNextF(_ =>
      //       Coeval {
      //         val location = playerNode.getWorldTranslation()
      //         cameraPivotNode.setLocalTranslation(location)
      //       }
      //     )
      //     .completedL
      //     .toIO
      //     .hideErrors
      //     .startAndForget
      sched <- UIO.pure(schedulers.async)
      playerActor <- wire[PlayerController.Props].create
      obs <-
        playerActor
          .askL(PlayerActorSupervisor.GetStatsObservable2)
          .onErrorHandleWith(TimeoutError.from)
      _ <-
        obs
          .doOnNext(s => loggerL.debug(s"Got state $s").toTask)
          .completedL
          .toIO
          .hideErrors
          .startAndForget
    } yield playerActor
  }

  def createTestNpc(
      // appScheduler: monix.execution.Scheduler,
      npcName: String
  ): IO[AppError, NpcActorSupervisor.Ref] = {
    val initialPos = ImVector3f(50, 5, 0)
    val npcPhysicsControl = new BetterCharacterControl(1.5f, 6f, 1f)
      // (1f, 2.1f, 10f)
      .withJumpForce(ImVector3f(0, 5f, 0))

    val npcActorTask = AkkaUtils.spawnActorL(
      new NpcActorSupervisor.Props(
        new NpcMovementActor.Props(
          enqueueR,
          initialPos,
          npcName,
          npcPhysicsControl
        ).behavior,
        npcName,
        initialPos
      ).behavior,
      actorName = Some(s"${npcName}-npcActorSupervisor")
    )

    (for {
      materialDef <-
        assetManager
          .loadAssetAs[MaterialDef](
            os.rel / "Common" / "MatDefs" / "Misc" / "Unshaded.j3md"
          )
          .mapError(AppError.AssetManagerError)
      material = new Material(materialDef)
      _ = material.setColor("Color", ColorRGBA.Blue)
      mesh = PlayerController.Defaults.defaultMesh.withMaterial(material)
      npcNode = PlayerController.Defaults.defaultNpcNode(
        mesh,
        initialPos,
        npcPhysicsControl,
        npcName
      )
      _ <- (for {
          _ <- physicsSpace += npcPhysicsControl
          _ <- physicsSpace += npcNode
          _ <- rootNode += npcNode
        } yield ()).mapError(AppError.AppNodeError)
      npcActor <- npcActorTask
    } yield npcActor)

  }

}
