#include "character/CharacterStateMachine.h"
#include "character/CharacterObject.h"
#include "character/states/CharacterStateRegistry.h"

namespace frostbite2D {
namespace {

bool IsCompatiblePendingActionContext(const CharacterActionContextData* context,
                                      const std::string& requestedActionId) {
  return !context || context->requestedActionId.empty()
      || context->requestedActionId == requestedActionId;
}

} // namespace

void CharacterStateMachine::Configure(const character::CharacterConfig& config) {
  ClearRegisteredStates();
  RegisterCommonCharacterStates(*this);
  RegisterJobCharacterStates(*this, config);
  Reset();
}

void CharacterStateMachine::Reset() {
  currentState_ = CharacterStateId::Idle;
  currentActionId_.clear();
  currentAction_ = nullptr;
  stateTime_ = 0.0f;
}

void CharacterStateMachine::Update(CharacterObject& owner,
                                   CharacterCommandBuffer& commandBuffer,
                                   const CharacterIntent& intent,
                                   float deltaTime) {
  stateTime_ += deltaTime;

  CharacterStateContext context{owner, commandBuffer, intent, deltaTime};
  if (auto* stateNode = FindStateNode(currentState_)) {
    stateNode->OnUpdate(*this, context);
  }
}

void CharacterStateMachine::ForceHurt(CharacterObject& owner,
                                      const CharacterActionDefinition* hurtAction) {
  if (!hurtAction) {
    owner.ReportFatalCharacterError("CharacterStateMachine::ForceHurt",
                                    "hurt action definition is null");
    return;
  }

  owner.ClearPendingActionContext();

  const CharacterActionDefinition* previousAction = currentAction_;
  currentAction_ = hurtAction;
  currentActionId_ = hurtAction->actionId;
  if (currentState_ == CharacterStateId::Hurt) {
    InvokeStateHook(owner, previousAction, "on_exit");
    stateTime_ = 0.0f;
    CharacterStateContext context{owner, owner.GetCommandBufferMutable(), owner.GetCurrentIntent(),
                                  owner.GetLastDeltaTime()};
    if (auto* hurtNode = FindStateNode(CharacterStateId::Hurt)) {
      hurtNode->OnEnter(*this, context, CharacterStateId::Hurt);
    }
    return;
  }
  ChangeState(owner, CharacterStateId::Hurt);
}

bool CharacterStateMachine::CanAcceptGroundActions() const {
  return currentState_ == CharacterStateId::Idle
      || currentState_ == CharacterStateId::Move
      || currentState_ == CharacterStateId::Landing;
}

bool CharacterStateMachine::CanBeInterrupted() const {
  return currentState_ != CharacterStateId::Dead;
}

bool CharacterStateMachine::IsMovementLocked() const {
  return currentState_ == CharacterStateId::Action
      || currentState_ == CharacterStateId::Hurt
      || currentState_ == CharacterStateId::Dead;
}

bool CharacterStateMachine::CanTurn() const {
  return currentState_ != CharacterStateId::Action
      && currentState_ != CharacterStateId::Hurt
      && currentState_ != CharacterStateId::Dead;
}

void CharacterStateMachine::RegisterState(std::unique_ptr<ICharacterStateNode> stateNode) {
  if (!stateNode) {
    return;
  }
  stateRegistry_[stateNode->GetId()] = std::move(stateNode);
}

void CharacterStateMachine::ClearRegisteredStates() {
  stateRegistry_.clear();
}

ICharacterStateNode* CharacterStateMachine::FindStateNode(CharacterStateId stateId) const {
  auto it = stateRegistry_.find(stateId);
  if (it == stateRegistry_.end()) {
    return nullptr;
  }
  return it->second.get();
}

void CharacterStateMachine::ChangeState(CharacterObject& owner,
                                        CharacterStateId nextState) {
  if (currentState_ == nextState) {
    return;
  }

  CharacterStateId previousState = currentState_;
  CharacterStateContext context{owner, owner.GetCommandBufferMutable(), owner.GetCurrentIntent(),
                                owner.GetLastDeltaTime()};

  if (auto* previousNode = FindStateNode(previousState)) {
    previousNode->OnExit(*this, context, nextState);
  }

  if (previousState == CharacterStateId::Action && nextState != CharacterStateId::Action) {
    owner.ClearCurrentActionContext();
  }
  if (nextState != CharacterStateId::Action) {
    owner.ClearPendingActionContext();
  }

  currentState_ = nextState;
  stateTime_ = 0.0f;
  if (nextState != CharacterStateId::Action && nextState != CharacterStateId::Hurt) {
    currentAction_ = nullptr;
    currentActionId_.clear();
  }

  if (auto* nextNode = FindStateNode(nextState)) {
    nextNode->OnEnter(*this, context, previousState);
  }
}

void CharacterStateMachine::EnterAction(CharacterObject& owner,
                                        const CharacterActionDefinition* action) {
  if (!action) {
    owner.ReportFatalCharacterError("CharacterStateMachine::EnterAction",
                                    "action definition is null");
    return;
  }

  const std::string previousActionId = currentActionId_;
  CharacterStateId previousState = currentState_;
  if (currentState_ == CharacterStateId::Action) {
    CharacterStateContext context{owner, owner.GetCommandBufferMutable(), owner.GetCurrentIntent(),
                                  owner.GetLastDeltaTime()};
    if (auto* actionNode = FindStateNode(CharacterStateId::Action)) {
      actionNode->OnExit(*this, context, CharacterStateId::Action);
    }

    currentAction_ = action;
    currentActionId_ = action->actionId;
    owner.CommitPendingActionContext(currentActionId_, previousActionId, previousState);
    stateTime_ = 0.0f;
    if (auto* actionNode = FindStateNode(CharacterStateId::Action)) {
      actionNode->OnEnter(*this, context, CharacterStateId::Action);
    }
    return;
  }

  currentAction_ = action;
  currentActionId_ = action->actionId;
  owner.CommitPendingActionContext(currentActionId_, previousActionId, previousState);
  ChangeState(owner, CharacterStateId::Action);
}

void CharacterStateMachine::InvokeStateHook(CharacterObject& owner,
                                            const CharacterActionDefinition* action,
                                            const char* phase) const {
  if (!action || !phase || phase[0] == '\0') {
    return;
  }
  owner.RunStateHook(phase, currentState_, action);
}

bool CharacterStateMachine::TryStartAction(CharacterObject& owner,
                                           CharacterCommandBuffer& commandBuffer,
                                           const std::string& requestedActionId,
                                           const std::string& actionId) {
  if (!commandBuffer.HasBufferedAction(requestedActionId)) {
    if (IsCompatiblePendingActionContext(owner.GetPendingActionContext(), requestedActionId)) {
      owner.ClearPendingActionContext();
    }
    return false;
  }

  commandBuffer.ConsumeAction(requestedActionId);
  return StartAction(owner, requestedActionId, actionId);
}

bool CharacterStateMachine::StartAction(CharacterObject& owner,
                                        const std::string& requestedActionId,
                                        const std::string& actionId) {
  const CharacterActionContextData* pendingActionContext = owner.GetPendingActionContext();
  if (!IsCompatiblePendingActionContext(pendingActionContext, requestedActionId)) {
    owner.ReportFatalCharacterError("CharacterStateMachine::StartAction",
                                    "prepared action context does not match requested action",
                                    requestedActionId);
    owner.ClearPendingActionContext();
    return false;
  }
  if (!pendingActionContext) {
    owner.PreparePendingActionContext(requestedActionId, currentActionId_, currentState_);
  }

  const CharacterActionDefinition* action = owner.RequireAction(
      actionId, "CharacterStateMachine::StartAction");
  if (!action) {
    owner.ClearPendingActionContext();
    return false;
  }

  EnterAction(owner, action);
  return true;
}

bool CharacterStateMachine::TryStartRegisteredAction(CharacterStateContext& context) {
  auto* stateNode = FindStateNode(CharacterStateId::Action);
  auto* actionNode = dynamic_cast<ICharacterActionStateNode*>(stateNode);
  if (!actionNode) {
    return false;
  }
  return actionNode->TryEnter(*this, context);
}

} // namespace frostbite2D
