package wow.doge.mygame.game.entities

import scala.concurrent.duration._
import scala.util.Failure
import scala.util.Success

import akka.actor.typed.ActorRef
import akka.actor.typed.Behavior
import akka.actor.typed.SupervisorStrategy
import akka.actor.typed.scaladsl.ActorContext
import akka.actor.typed.scaladsl.Behaviors
import akka.util.Timeout
import cats.syntax.show._
import monix.execution.CancelableFuture
import monix.execution.CancelablePromise
import wow.doge.mygame.game.subsystems.movement.CanMove
import wow.doge.mygame.implicits._
import wow.doge.mygame.math.ImVector3f
import wow.doge.mygame.subsystems.events.EntityMovementEvent
import wow.doge.mygame.subsystems.events.EntityMovementEvent.MovedDown
import wow.doge.mygame.subsystems.events.EntityMovementEvent.MovedLeft
import wow.doge.mygame.subsystems.events.EntityMovementEvent.MovedRight
import wow.doge.mygame.subsystems.events.EntityMovementEvent.MovedUp
import wow.doge.mygame.subsystems.events.Event
import wow.doge.mygame.subsystems.events.EventBus
import wow.doge.mygame.subsystems.events.EventsModule.GameEventBus
import wow.doge.mygame.subsystems.events.TickEvent
import wow.doge.mygame.subsystems.events.TickEvent.RenderTick
import wow.doge.mygame.subsystems.movement.ImMovementActor
import wow.doge.mygame.utils.GenericTimerActor

object NpcActorSupervisor {
  type Ref = ActorRef[Command]

  sealed trait Command
  final case class Move(pos: ImVector3f) extends Command
  private final case class InternalMove(
      move: Move,
      signal: CancelableFuture[NpcMovementActor.DoneMoving.type]
  ) extends Command
  private case object DoneMoving extends Command
  private case class LogError(err: Throwable) extends Command
  private case class MovementFailed(err: Throwable) extends Command

  class Props(
      val npcMovementActorBehavior: Behavior[NpcMovementActor.Command],
      val npcName: String,
      val initialPos: ImVector3f
  ) {
    def behavior =
      Behaviors.withMdc[Command](Map("actorName" -> npcName))(
        Behaviors.setup[Command] { ctx =>
          val npcMovementActor = ctx.spawn(
            (npcMovementActorBehavior),
            s"npc-${npcName}-NpcMovementActor"
          )

          new NpcActorSupervisor(ctx, this, Children(npcMovementActor))
            .idle(State())
        }
      )
  }
  final case class State()
  final case class Children(
      npcMovementActor: ActorRef[NpcMovementActor.Command]
  )
}
class NpcActorSupervisor(
    ctx: ActorContext[NpcActorSupervisor.Command],
    props: NpcActorSupervisor.Props,
    children: NpcActorSupervisor.Children
) {
  import NpcActorSupervisor._
  implicit val timeout = Timeout(1.second)

  val movementTimer = ctx.spawn(
    GenericTimerActor
      .Props(
        children.npcMovementActor,
        NpcMovementActor.MovementTick,
        100.millis
      )
      .behavior,
    s"npc-${props.npcName}-NpcActorTimer"
  )

  def idle(state: State): Behavior[NpcActorSupervisor.Command] =
    Behaviors.setup { _ =>
      ctx.log.debugP(s"npcActor-${props.npcName}: Entered Idle State")
      Behaviors.receiveMessage[Command] {
        case m @ Move(pos) =>
          ctx.ask(
            children.npcMovementActor,
            NpcMovementActor.MoveTo(pos, _)
          ) {
            case Success(signal)    => InternalMove(m, signal)
            case Failure(exception) => LogError(exception)
          }
          Behaviors.same
        case InternalMove(move, signal) =>
          moving(state, move.pos, signal)

        case LogError(err) =>
          logError(err)
          Behaviors.same
        case _ => Behaviors.unhandled
      }
    }

  def moving(
      state: State,
      targetPos: ImVector3f,
      signal: CancelableFuture[NpcMovementActor.DoneMoving.type]
  ): Behavior[NpcActorSupervisor.Command] =
    Behaviors.setup { _ =>
      ctx.log.debugP(s"npcActor-${props.npcName}: Entered Moving State")
      movementTimer ! GenericTimerActor.Start
      ctx.pipeToSelf(signal) {
        case Success(value)     => DoneMoving
        case Failure(exception) => MovementFailed(exception)
      }
      Behaviors.receiveMessagePartial[Command] {
        case LogError(err) =>
          logError(err)
          Behaviors.same
        case MovementFailed(err) =>
          ctx.self ! LogError(err)
          movementTimer ! GenericTimerActor.Stop
          idle(state)
        case m @ Move(pos) =>
          movementTimer ! GenericTimerActor.Stop
          children.npcMovementActor ! NpcMovementActor.StopMoving
          // new movement request received, cancel previous request
          signal.cancel()
          ctx.ask(
            children.npcMovementActor,
            NpcMovementActor.MoveTo(pos, _)
          ) {
            case Success(signal)    => InternalMove(m, signal)
            case Failure(exception) => MovementFailed(exception)
          }
          Behaviors.same
        case InternalMove(move, signal) =>
          moving(state, targetPos, signal)
        case DoneMoving =>
          movementTimer ! GenericTimerActor.Stop
          idle(state)

      }
    }

  def logError(err: Throwable) =
    ctx.log.errorP(s"npcActor-${props.npcName}: " + err.getMessage)
}

object NpcMovementActor {

  case object DoneMoving

  sealed trait Command
  case class AskPosition(replyTo: ActorRef[ImVector3f]) extends Command
  case object MovementTick extends Command
  case object StopMoving extends Command
  case class MoveTo(
      target: ImVector3f,
      doneSignal: ActorRef[CancelableFuture[DoneMoving.type]]
  ) extends Command

  class Props[T: CanMove](
      val enqueueR: Function1[() => Unit, Unit],
      val initialPos: ImVector3f,
      // val tickEventBus: GameEventBus[TickEvent],
      val npcName: String,
      val movable: T
  ) {
    def behavior =
      Behaviors.setup[Command] { ctx =>
        new NpcMovementActor(ctx, this).receive(State())
      }
  }

  final case class State()
}
class NpcMovementActor[T](
    ctx: ActorContext[NpcMovementActor.Command],
    props: NpcMovementActor.Props[T]
)(implicit cm: CanMove[T]) {
  import NpcMovementActor._

  def location = cm.location(props.movable)

  def receive(state: State): Behavior[NpcMovementActor.Command] =
    Behaviors.receiveMessagePartial {
      case AskPosition(replyTo) =>
        replyTo ! location
        Behaviors.same
      case MoveTo(
            target: ImVector3f,
            replyTo: ActorRef[CancelableFuture[DoneMoving.type]]
          ) =>
        props.enqueueR(() => cm.move(props.movable, target - location, 20f))
        val p = CancelablePromise[DoneMoving.type]()
        replyTo ! p.future
        ticking(p, target, state)
    }

  def ticking(
      reachDestination: CancelablePromise[DoneMoving.type],
      targetPos: ImVector3f,
      state: State
  ): Behavior[NpcMovementActor.Command] =
    Behaviors.receiveMessagePartial {
      case StopMoving =>
        ctx.log.debugP(
          show"npcActor-${props.npcName}: Position at Stop = " + location
        )
        props.enqueueR(() => cm.stop(props.movable))
        receive(state)

      case MovementTick =>
        val dst = ImVector3f.manhattanDst(targetPos, location)
        if (dst <= 10f) {
          ctx.self ! StopMoving
          reachDestination.success(DoneMoving)
        } else {
          // format:off
          ctx.log.traceP(
            show"npcActor-${props.npcName}: Difference = ${dst.formatted("%.2f")}"
          )
          ctx.log.traceP(
            show"npcActor-${props.npcName}: Current pos = $location"
          )
        }

        Behaviors.same
    }
}

object NpcMovementActorNotUsed {
  sealed trait Command
  class Props(
      imMovementActorBehavior: Behavior[ImMovementActor.Command],
      npcName: String,
      // movementActor: ActorRef[ImMovementActor.Command],
      mainEventBus: ActorRef[
        EventBus.Command[Event]
      ],
      tickEventBus: GameEventBus[TickEvent]
  ) {

    def behavior: Behavior[Command] =
      Behaviors.setup { ctx =>
        val movementActor = ctx.spawn(
          Behaviors
            .supervise(imMovementActorBehavior)
            .onFailure[Exception](
              SupervisorStrategy.restart.withLimit(2, 100.millis)
            ),
          s"npc-${npcName}-MovementActorChild"
        )
        val npcMovementElBehavior = Behaviors.receiveMessagePartial[Event] {
          case event: EntityMovementEvent =>
            event match {
              case MovedLeft(name, pressed) =>
                if (name == npcName)
                  movementActor ! ImMovementActor.MoveLeft(pressed)
                Behaviors.same
              case MovedUp(name, pressed) =>
                if (name == npcName)
                  movementActor ! ImMovementActor.MoveUp(pressed)
                Behaviors.same
              case MovedRight(name, pressed) =>
                if (name == npcName)
                  movementActor ! ImMovementActor.MoveRight(pressed)
                Behaviors.same
              case MovedDown(name, pressed) =>
                if (name == npcName)
                  movementActor ! ImMovementActor.MoveDown(pressed)
                Behaviors.same
            }
        }

        val npcMovementEl = ctx.spawn(
          Behaviors
            .supervise(npcMovementElBehavior)
            .onFailure[Exception](
              SupervisorStrategy.restart.withLimit(2, 100.millis)
            ),
          s"npc-${npcName}-MovementEventHandler"
        )
        val renderTickElBehavior =
          Behaviors.receiveMessage[RenderTick.type] {
            case RenderTick =>
              movementActor ! ImMovementActor.Tick
              Behaviors.same
          }

        val renderTickEl =
          ctx.spawn(
            renderTickElBehavior,
            s"npc-${npcName}-MovementTickListener"
          )

        mainEventBus ! EventBus.Subscribe(npcMovementEl)
        tickEventBus ! EventBus.Subscribe(renderTickEl)
        Behaviors.receiveMessage(msg => Behaviors.same)
      }

  }
}
