#include "character/CharacterAnimation.h"
#include "character/CharacterObject.h"
#include <frostbite2D/graphics/renderer.h>
#include <frostbite2D/resource/pvf_archive.h>
#include <SDL2/SDL.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdio>
#include <sstream>
#include <utility>

namespace frostbite2D {
namespace {

const std::array<const char*, 12> kAvatarParts = {
    "weapon_avatar", "aurora_avatar", "hair_avatar", "hat_avatar",
    "face_avatar",   "breast_avatar", "coat_avatar", "skin_avatar",
    "waist_avatar",  "pants_avatar",  "shoes_avatar", "weapon"};

std::string truncatePath(const std::string& path) {
  size_t slashPos = path.find_last_of('/');
  if (slashPos == std::string::npos) {
    return {};
  }
  return path.substr(0, slashPos);
}

std::string actionTail(const std::string& path) {
  size_t slashPos = path.find_last_of('/');
  if (slashPos == std::string::npos) {
    return path;
  }
  return path.substr(slashPos);
}

struct BoundsAccumulator {
  bool valid = false;
  float minX = 0.0f;
  float minY = 0.0f;
  float maxX = 0.0f;
  float maxY = 0.0f;

  void includeRect(const Rect& rect) {
    if (rect.empty()) {
      return;
    }

    if (!valid) {
      minX = rect.left();
      minY = rect.top();
      maxX = rect.right();
      maxY = rect.bottom();
      valid = true;
      return;
    }

    minX = std::min(minX, rect.left());
    minY = std::min(minY, rect.top());
    maxX = std::max(maxX, rect.right());
    maxY = std::max(maxY, rect.bottom());
  }
};

Rect transformRectBounds(const Rect& rect, const Transform2D& transform) {
  Vec2 p0 = transform.transformPoint(Vec2(rect.left(), rect.top()));
  Vec2 p1 = transform.transformPoint(Vec2(rect.right(), rect.top()));
  Vec2 p2 = transform.transformPoint(Vec2(rect.left(), rect.bottom()));
  Vec2 p3 = transform.transformPoint(Vec2(rect.right(), rect.bottom()));

  float minX = std::min(std::min(p0.x, p1.x), std::min(p2.x, p3.x));
  float minY = std::min(std::min(p0.y, p1.y), std::min(p2.y, p3.y));
  float maxX = std::max(std::max(p0.x, p1.x), std::max(p2.x, p3.x));
  float maxY = std::max(std::max(p0.y, p1.y), std::max(p2.y, p3.y));
  return Rect(minX, minY, maxX - minX, maxY - minY);
}

void combineHash(uint64& seed, uint64 value) {
  constexpr uint64 kOffset = 0x9e3779b97f4a7c15ULL;
  seed ^= value + kOffset + (seed << 6) + (seed >> 2);
}

CharacterAnimation::CompositeFrameInfo combineCompositeFrameInfo(
    const CharacterAnimation::CompositeFrameInfo& lhs,
    const CharacterAnimation::CompositeFrameInfo& rhs) {
  if (!lhs.valid) {
    return rhs;
  }
  if (!rhs.valid) {
    return lhs;
  }

  float left = std::floor(std::min(lhs.localBounds.left(), rhs.localBounds.left()));
  float top = std::floor(std::min(lhs.localBounds.top(), rhs.localBounds.top()));
  float right = std::ceil(std::max(lhs.localBounds.right(), rhs.localBounds.right()));
  float bottom =
      std::ceil(std::max(lhs.localBounds.bottom(), rhs.localBounds.bottom()));

  CharacterAnimation::CompositeFrameInfo result;
  result.valid = true;
  result.localBounds =
      Rect(left, top, std::max(right - left, 1.0f), std::max(bottom - top, 1.0f));
  result.width =
      std::max(static_cast<int>(std::lround(result.localBounds.width())), 1);
  result.height =
      std::max(static_cast<int>(std::lround(result.localBounds.height())), 1);
  result.originInTexture =
      Vec2(-result.localBounds.left(), -result.localBounds.top());
  result.groundAnchorInTexture = result.originInTexture;
  return result;
}

} // namespace

bool CharacterAnimation::Init(CharacterObject* parent,
                              const character::CharacterConfig& config,
                              const CharacterEquipmentManager& equipmentManager) {
  parent_ = parent;
  actionAnimations_.clear();
  missingAnimationPaths_.clear();
  skippedMissingAnimationCount_ = 0;
  currentActionTag_.clear();
  direction_ = 1;
  actionFrameFlagCallback_ = nullptr;
  actionEndCallback_ = nullptr;
  compositeCanvas_ = MakePtr<CanvasActor>();
  compositeFrameInfoByAction_.clear();
  compositeDirty_ = true;
  compositeVersion_ = 0;
  lastCompositeSignature_ = 0;

  if (!compositeCanvas_->Init(1, 1)) {
    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                "CharacterAnimation: failed to initialize composite canvas");
    compositeCanvas_.Reset();
  } else {
    compositeCanvas_->SetCustomDrawCallback([this]() {
      RenderCurrentActionToCompositeCanvas();
    });
  }

  for (const auto& [actionName, actionPath] : config.animationPath) {
    for (const char* slotName : kAvatarParts) {
      CreateAnimationBySlot(actionName, slotName, actionPath, config, equipmentManager);
    }
  }

  if (actionAnimations_.empty()) {
    SDL_LogWarn(SDL_LOG_CATEGORY_APPLICATION,
                "CharacterAnimation: no usable action animations for job %d",
                config.jobId);
    return false;
  }

  if (skippedMissingAnimationCount_ > 0) {
    SDL_Log("CharacterAnimation: skipped %zu missing animation loads across %zu unique paths for job %d",
            skippedMissingAnimationCount_, missingAnimationPaths_.size(),
            config.jobId);
  }

  BuildCompositeActionFrames();
  return true;
}

void CharacterAnimation::Update(float deltaTime) {
  Actor::Update(deltaTime);

  uint64 renderSignature = CaptureCurrentRenderSignature();
  if (renderSignature != lastCompositeSignature_) {
    compositeDirty_ = true;
  }
}

void CharacterAnimation::Render() {
  if (compositeDirty_ && compositeCanvas_ && Renderer::get().isFrameActive()) {
    RefreshCompositeTextureIfNeeded();
  }

  Actor::Render();
}

std::string CharacterAnimation::FormatImgPath(std::string path, Animation::ReplaceData data) {
  size_t pos = path.find("%04d");
  if (pos != std::string::npos) {
    path.replace(pos, 4, "%02d%02d");
  }

  char buffer[512] = {};
  std::snprintf(buffer, sizeof(buffer), path.c_str(), data.param1, data.param2);
  return std::string(buffer);
}

void CharacterAnimation::CreateAnimationBySlot(
    const std::string& actionName,
    const std::string& slotName,
    const std::string& actionPath,
    const character::CharacterConfig& config,
    const CharacterEquipmentManager& equipmentManager) {
  if (slotName == std::string("skin_avatar")) {
    if (shouldSkipMissingAnimation(actionPath)) {
      return;
    }

    Animation::ReplaceData replaceData(0, 0);
    if (const auto* equip = equipmentManager.GetEquip(slotName)) {
      auto it = equip->jobAnimations.find(config.jobId);
      if (it != equip->jobAnimations.end() && !it->second.empty()) {
        replaceData.param1 = it->second.front().imgFormat[0];
        replaceData.param2 = it->second.front().imgFormat[1];
      }
    }

    auto animation = MakePtr<Animation>(actionPath, FormatImgPath, replaceData);
    if (!animation || !animation->IsUsable()) {
      return;
    }
    animation->SetVisible(false);
    AddChild(animation);
    actionAnimations_[actionName].push_back(animation);
    return;
  }

  const auto* equip = equipmentManager.GetEquip(slotName);
  if (!equip) {
    return;
  }

  auto jobAniIt = equip->jobAnimations.find(config.jobId);
  if (jobAniIt == equip->jobAnimations.end()) {
    return;
  }

  std::string equipDir = truncatePath(equip->path);
  std::string actionPathTail = actionTail(actionPath);
  for (const auto& variation : jobAniIt->second) {
    if (variation.animationGroup.empty()) {
      continue;
    }

    std::string aniPath = equipDir + "/" + variation.animationGroup + actionPathTail;
    if (shouldSkipMissingAnimation(aniPath)) {
      continue;
    }

    auto animation = MakePtr<Animation>(
        aniPath, FormatImgPath,
        Animation::ReplaceData(variation.imgFormat[0], variation.imgFormat[1]));
    if (!animation || !animation->IsUsable()) {
      continue;
    }

    animation->SetVisible(false);
    animation->SetZOrder(variation.layer);
    AddChild(animation);
    actionAnimations_[actionName].push_back(animation);
  }
}

bool CharacterAnimation::shouldSkipMissingAnimation(const std::string& aniPath) {
  PvfArchive& pvf = PvfArchive::get();
  std::string normalizedPath = pvf.normalizePath(aniPath);
  if (missingAnimationPaths_.find(normalizedPath) != missingAnimationPaths_.end()) {
    ++skippedMissingAnimationCount_;
    return true;
  }

  if (pvf.hasFile(normalizedPath)) {
    return false;
  }

  missingAnimationPaths_.insert(std::move(normalizedPath));
  ++skippedMissingAnimationCount_;
  return true;
}

std::string CharacterAnimation::DescribeAvailableActions() const {
  if (actionAnimations_.empty()) {
    return "<none>";
  }

  std::ostringstream stream;
  bool first = true;
  for (const auto& [actionName, animations] : actionAnimations_) {
    (void)animations;
    if (!first) {
      stream << ", ";
    }
    stream << actionName;
    first = false;
  }
  return stream.str();
}

Animation* CharacterAnimation::GetPrimaryAnimation(const std::string& actionName) const {
  auto it = actionAnimations_.find(actionName);
  if (it == actionAnimations_.end()) {
    return nullptr;
  }

  for (const auto& animation : it->second) {
    if (animation && animation->IsUsable()) {
      return animation.Get();
    }
  }

  for (const auto& animation : it->second) {
    if (animation) {
      return animation.Get();
    }
  }

  return nullptr;
}

Animation* CharacterAnimation::GetCurrentPrimaryAnimation() const {
  if (currentActionTag_.empty()) {
    return nullptr;
  }
  return GetPrimaryAnimation(currentActionTag_);
}

void CharacterAnimation::RefreshRuntimeCallbacks() {
  auto currentIt = actionAnimations_.find(currentActionTag_);
  if (currentIt == actionAnimations_.end()) {
    return;
  }

  Animation* primaryAnimation = GetPrimaryAnimation(currentActionTag_);
  for (auto& animation : currentIt->second) {
    if (!animation) {
      continue;
    }

    if (animation.Get() == primaryAnimation) {
      animation->changeFrameCallback_ = actionFrameFlagCallback_;
      animation->endCallback_ = actionEndCallback_;
    } else {
      animation->changeFrameCallback_ = nullptr;
      animation->endCallback_ = nullptr;
    }
  }
}

bool CharacterAnimation::SetAction(const std::string& actionName) {
  auto nextIt = actionAnimations_.find(actionName);
  if (nextIt == actionAnimations_.end()) {
    if (parent_) {
      parent_->ReportFatalCharacterError("CharacterAnimation::SetAction",
                                         "requested animation tag is not loaded",
                                         std::string(), actionName);
    } else {
      SDL_LogError(SDL_LOG_CATEGORY_APPLICATION,
                   "CharacterAnimation: action %s missing and parent is null",
                   actionName.c_str());
    }
    return false;
  }

  if (!currentActionTag_.empty()) {
    auto currentIt = actionAnimations_.find(currentActionTag_);
    if (currentIt != actionAnimations_.end()) {
      for (auto& animation : currentIt->second) {
        animation->Reset();
        animation->SetVisible(false);
      }
    }
  }

  for (auto& animation : nextIt->second) {
    animation->Reset();
    animation->SetVisible(true);
  }
  currentActionTag_ = actionName;
  RefreshRuntimeCallbacks();
  SetDirection(direction_);
  MarkCompositeDirty();
  return true;
}

void CharacterAnimation::SetDirection(int direction) {
  direction_ = direction >= 0 ? 1 : -1;

  auto currentIt = actionAnimations_.find(currentActionTag_);
  if (currentIt == actionAnimations_.end()) {
    return;
  }

  for (auto& animation : currentIt->second) {
    if (animation) {
      animation->SetDirection(direction_);
    }
  }

  MarkCompositeDirty();
}

bool CharacterAnimation::IsCurrentActionFinished() const {
  const Animation* primaryAnimation = GetCurrentPrimaryAnimation();
  if (!primaryAnimation) {
    return false;
  }
  return !primaryAnimation->IsUsable();
}

int CharacterAnimation::GetCurrentFrameIndex() const {
  const Animation* primaryAnimation = GetCurrentPrimaryAnimation();
  return primaryAnimation ? primaryAnimation->GetCurrentFrameIndex() : 0;
}

int CharacterAnimation::GetCurrentFrameCount() const {
  const Animation* primaryAnimation = GetCurrentPrimaryAnimation();
  return primaryAnimation ? primaryAnimation->GetTotalFrameCount() : 0;
}

float CharacterAnimation::GetCurrentNormalizedProgress() const {
  int totalFrames = GetCurrentFrameCount();
  if (totalFrames <= 1) {
    return IsCurrentActionFinished() ? 1.0f : 0.0f;
  }

  float progress = static_cast<float>(GetCurrentFrameIndex())
                 / static_cast<float>(totalFrames - 1);
  return std::clamp(progress, 0.0f, 1.0f);
}

animation::AniFrame CharacterAnimation::GetCurrentFrameInfo() const {
  const Animation* primaryAnimation = GetCurrentPrimaryAnimation();
  if (!primaryAnimation) {
    return animation::AniFrame();
  }
  return primaryAnimation->GetCurrentFrameInfo();
}

void CharacterAnimation::SetActionFrameFlagCallback(ActionFrameFlagCallback callback) {
  actionFrameFlagCallback_ = std::move(callback);
  RefreshRuntimeCallbacks();
}

void CharacterAnimation::SetActionEndCallback(ActionEndCallback callback) {
  actionEndCallback_ = std::move(callback);
  RefreshRuntimeCallbacks();
}

bool CharacterAnimation::EnsureCompositeTextureReady() {
  RefreshCompositeTextureIfNeeded();
  return HasCompositeTexture();
}

bool CharacterAnimation::HasCompositeTexture() const {
  return compositeCanvas_ && compositeCanvas_->IsCanvasReady() &&
         GetCurrentCompositeFrameInfo().valid;
}

Ptr<Texture> CharacterAnimation::GetCompositeTexture() const {
  return compositeCanvas_ ? compositeCanvas_->GetOutputTexture() : nullptr;
}

Vec2 CharacterAnimation::GetCompositeTextureSize() const {
  CompositeFrameInfo info = GetCurrentCompositeFrameInfo();
  return Vec2(static_cast<float>(info.width), static_cast<float>(info.height));
}

Vec2 CharacterAnimation::GetCompositeOriginInTexture() const {
  return GetCurrentCompositeFrameInfo().originInTexture;
}

Vec2 CharacterAnimation::GetCompositeGroundAnchorInTexture() const {
  return GetCurrentCompositeFrameInfo().groundAnchorInTexture;
}

CharacterAnimation::CompositeFrameInfo CharacterAnimation::ComputeCompositeFrameInfo(
    const std::vector<RefPtr<Animation>>& animations, int direction) const {
  CompositeFrameInfo info;
  BoundsAccumulator bounds;

  for (const auto& animation : animations) {
    if (!animation) {
      continue;
    }

    Rect animationBounds;
    if (!animation->GetStaticLocalBounds(direction, animationBounds)) {
      continue;
    }

    bounds.includeRect(
        transformRectBounds(animationBounds, animation->GetLocalTransform()));
  }

  if (!bounds.valid) {
    return info;
  }

  float left = std::floor(bounds.minX);
  float top = std::floor(bounds.minY);
  float right = std::ceil(bounds.maxX);
  float bottom = std::ceil(bounds.maxY);

  info.valid = true;
  info.localBounds = Rect(left, top, std::max(right - left, 1.0f),
                          std::max(bottom - top, 1.0f));
  info.width = std::max(static_cast<int>(std::lround(info.localBounds.width())), 1);
  info.height = std::max(static_cast<int>(std::lround(info.localBounds.height())), 1);
  info.originInTexture = Vec2(-info.localBounds.left(), -info.localBounds.top());
  info.groundAnchorInTexture = info.originInTexture;
  return info;
}

void CharacterAnimation::BuildCompositeActionFrames() {
  compositeFrameInfoByAction_.clear();
  for (const auto& [actionName, animations] : actionAnimations_) {
    CompositeFrameInfo rightInfo = ComputeCompositeFrameInfo(animations, 1);
    CompositeFrameInfo leftInfo = ComputeCompositeFrameInfo(animations, -1);
    compositeFrameInfoByAction_[actionName] =
        combineCompositeFrameInfo(rightInfo, leftInfo);
  }
}

CharacterAnimation::CompositeFrameInfo CharacterAnimation::GetCurrentCompositeFrameInfo() const {
  auto it = compositeFrameInfoByAction_.find(currentActionTag_);
  if (it == compositeFrameInfoByAction_.end()) {
    return CompositeFrameInfo();
  }
  return it->second;
}

uint64 CharacterAnimation::CaptureCurrentRenderSignature() const {
  uint64 signature = 1469598103934665603ULL;
  combineHash(signature, static_cast<uint64>(direction_ >= 0 ? 1 : 0));
  combineHash(signature, static_cast<uint64>(std::hash<std::string>{}(currentActionTag_)));

  auto currentIt = actionAnimations_.find(currentActionTag_);
  if (currentIt == actionAnimations_.end()) {
    return signature;
  }

  for (const auto& animation : currentIt->second) {
    if (!animation) {
      continue;
    }
    combineHash(signature, animation->GetRenderSignature());
  }

  return signature;
}

void CharacterAnimation::MarkCompositeDirty() {
  compositeDirty_ = true;
  if (compositeCanvas_) {
    compositeCanvas_->SetDirty();
  }
}

void CharacterAnimation::UpdateCompositeCamera() {
  if (!compositeCanvas_) {
    return;
  }

  CompositeFrameInfo info = GetCurrentCompositeFrameInfo();
  Camera& camera = compositeCanvas_->GetCanvasCamera();
  camera.setViewport(std::max(info.width, 1), std::max(info.height, 1));
  camera.setZoom(1.0f);
  Vec2 cameraOrigin = GetWorldTransform().transformPoint(
      Vec2(info.localBounds.left(), info.localBounds.top()));
  camera.setPosition(cameraOrigin);
}

void CharacterAnimation::RefreshCompositeTextureIfNeeded() {
  if (!compositeDirty_ || !compositeCanvas_) {
    return;
  }

  CompositeFrameInfo info = GetCurrentCompositeFrameInfo();
  if (!info.valid) {
    compositeDirty_ = false;
    lastCompositeSignature_ = CaptureCurrentRenderSignature();
    return;
  }

  if (!compositeCanvas_->SetCanvasSize(std::max(info.width, 1),
                                       std::max(info.height, 1))) {
    return;
  }

  UpdateCompositeCamera();
  if (!compositeCanvas_->Redraw()) {
    return;
  }

  compositeDirty_ = false;
  lastCompositeSignature_ = CaptureCurrentRenderSignature();
  ++compositeVersion_;
}

void CharacterAnimation::RenderCurrentActionToCompositeCanvas() {
  if (actionAnimations_.find(currentActionTag_) == actionAnimations_.end()) {
    return;
  }

  RenderChildren();
}

} // namespace frostbite2D
