#include "Character.h"
#include "render/TextRenderer.h"
#include <cmath>
#include <iostream>
#include <random>
#include <array>
#include "GameConstants.h"
#include "Environment.h"


namespace ZL {

	const float ATTACK_COOLDOWN_TIME = 1.6f; 
    extern float x;
    extern float y;
    const float ATTACK_RANGE = 1.25f;
    const float VISIBLE_RANGE = 6.f;

void Character::loadAnimation(AnimationState state, const std::string& filename, const std::string& zipFile) {
    auto& data = animations[state];
    data.model.LoadFromFile(filename, zipFile);
    if (!data.model.animations.empty() && !data.model.animations[0].keyFrames.empty()) {
        data.totalFrames = data.model.animations[0].keyFrames.back().frame + 1;
    } else {
        data.totalFrames = 1;
    }
}
void Character::loadBinaryAnimation(AnimationState state, const std::string& filename, const std::string& zipFile) {
    auto& data = animations[state];
    data.model.LoadFromBinaryFile(filename, zipFile);
    if (!data.model.animations.empty() && !data.model.animations[0].keyFrames.empty()) {
        data.totalFrames = data.model.animations[0].keyFrames.back().frame + 1;
    }
    else {
        data.totalFrames = 1;
    }
}

void Character::setTarget(const Eigen::Vector3f& target,
                          std::function<void()> onArrived) {
    Eigen::Vector3f normalizedTarget(target.x(), 0.f, target.z());

    const bool sameRequestedTarget =
        (normalizedTarget - requestedWalkTarget).norm() <= TARGET_REPLAN_THRESHOLD;
    const bool alreadyMovingToTarget =
        !pathWaypoints.empty() || (walkTarget - normalizedTarget).norm() <= TARGET_REPLAN_THRESHOLD;
    const bool stoppedAfterFailedPath =
        pathWaypoints.empty() && (walkTarget - position).norm() <= TARGET_REPLAN_THRESHOLD;

    if (!onArrived && sameRequestedTarget && (alreadyMovingToTarget || stoppedAfterFailedPath)) {
        return;
    }

    requestedWalkTarget = normalizedTarget;
    onArrivedCallback = std::move(onArrived);

    if (pathPlanner) {
        pathWaypoints = pathPlanner(position, normalizedTarget);
        currentWaypointIndex = 0;

        if (!pathWaypoints.empty()) {
            for (Eigen::Vector3f& waypoint : pathWaypoints) {
                waypoint.y() = 0.f;
            }
            walkTarget = pathWaypoints.back();
            return;
        }

        walkTarget = Eigen::Vector3f(position.x(), 0.f, position.z());
        onArrivedCallback = nullptr;
        return;
    }

    pathWaypoints.clear();
    currentWaypointIndex = 0;
    walkTarget = normalizedTarget;
}

void Character::setPathPlanner(PathPlanner planner) {
    pathPlanner = std::move(planner);
}

bool Character::isMoving() const
{
	Eigen::Vector3f toTarget = walkTarget - position;
	toTarget.y() = 0.f;
	return !pathWaypoints.empty() || toTarget.norm() > WALK_THRESHOLD;
}

Eigen::Vector3f Character::getCurrentNavigationTarget() const
{
	if (!pathWaypoints.empty() && currentWaypointIndex < pathWaypoints.size()) {
		return pathWaypoints[currentWaypointIndex];
	}
	return walkTarget;
}

void Character::stopInPlace()
{
    walkTarget = Eigen::Vector3f(position.x(), 0.f, position.z());
    requestedWalkTarget = walkTarget;
    pathWaypoints.clear();
    currentWaypointIndex = 0;
    onArrivedCallback = nullptr;
}

void Character::forceReplan()
{
	if (!pathPlanner) {
		return;
	}


	const Eigen::Vector3f normalizedTarget(requestedWalkTarget.x(), 0.f, requestedWalkTarget.z());
	pathWaypoints = pathPlanner(position, normalizedTarget);
	currentWaypointIndex = 0;

	if (!pathWaypoints.empty()) {
		for (Eigen::Vector3f& waypoint : pathWaypoints) {
			waypoint.y() = 0.f;
		}
		walkTarget = pathWaypoints.back();
		return;
	}

	walkTarget = Eigen::Vector3f(position.x(), 0.f, position.z());
	onArrivedCallback = nullptr;
}

void Character::setTexture(std::shared_ptr<ZL::Texture> texture)
{
    for (auto& animEntry : animations) {
        for (const auto& name : animEntry.second.model.meshNamesOrdered) {
            meshTextures[name] = texture;
        }
    }
}

void Character::setTexture(const std::string& meshName, std::shared_ptr<Texture> tex) {
    meshTextures[meshName] = std::move(tex);
}

AnimationState Character::resolveActiveState() const {
    
    if (animations.count(currentState)) return currentState;
    return AnimationState::STAND;
}

float Character::getSpeedMultiplier(float transitionProgress) const {

    if (isPlayer) {
        return 1.0f + (1.5f - 1.0f) * transitionProgress;
    }
    else {
        return 1.0f - (1.0f - 0.1f) * transitionProgress;
    }
}

void Character::update(int64_t deltaMs) {
    if (!enabled) return;
    if (slowMoTimeRemaining > 0.0f) {
        slowMoTimeRemaining -= static_cast<float>(deltaMs) / 1000.0f;

        float transitionProgress;
        if (slowMoTimeRemaining > slowMoActiveTime - slowMoTransitionTime) {
            float timeInTransition = slowMoActiveTime - slowMoTimeRemaining;
            transitionProgress = (std::min)(1.0f, timeInTransition / slowMoTransitionTime);
        }
        else if (slowMoTimeRemaining < slowMoTransitionTime) {
            transitionProgress = (std::max)(0.0f, slowMoTimeRemaining / slowMoTransitionTime);
        }
        else {
            transitionProgress = 1.0f;
        }

        float speedMultiplier = getSpeedMultiplier(transitionProgress);
        deltaMs = static_cast<int64_t>(static_cast<float>(deltaMs) * speedMultiplier);
    }

    Eigen::Vector3f activeTarget;
    Eigen::Vector3f lookTarget;

    if (attackTarget == nullptr)
    {
        battle_state = 0;
        attack = 0;
    }

    if (attackTarget != nullptr)
    {
        float distToGhost = (attackTarget->position - position).norm();
        if (distToGhost >= VISIBLE_RANGE)
        {
            if (isPlayer)
            {
                setTarget(attackTarget->position);
            }
            battle_state = 0;
           
        }
        else if (distToGhost < VISIBLE_RANGE && distToGhost >= ATTACK_RANGE) {
            setTarget(attackTarget->position);
            battle_state = 0;
        }
        else
        {
            battle_state = 1;
        }
    }
    
    
    if (!pathWaypoints.empty() && currentWaypointIndex < pathWaypoints.size()) {
        activeTarget = pathWaypoints[currentWaypointIndex];
    }
    else
    {

        activeTarget = walkTarget;
    }

    if (battle_state == 0)
    {
        lookTarget = activeTarget;
    }
    else
    {
		lookTarget = attackTarget->position;
    }

    targetFacingAngle = facingAngle;

    if (currentState == AnimationState::WALK || currentState == AnimationState::STAND)
    {
        Eigen::Vector3f toTarget = activeTarget - position;
        toTarget.y() = 0.f;
        float dist = toTarget.norm();

        if (dist > WALK_THRESHOLD) {
            Eigen::Vector3f dir = toTarget / dist;
            float moveAmount = walkSpeed * static_cast<float>(deltaMs) / 1000.f;
            if (moveAmount >= dist) {
                position = activeTarget;
                position.y() = 0.f;
            }
            else {
                position += dir * moveAmount;
            }
            targetFacingAngle = atan2(dir.x(), -dir.z());
            currentState = AnimationState::WALK;
        }
        else {
            currentState = AnimationState::STAND;

            const bool hasNextWaypoint = !pathWaypoints.empty() && currentWaypointIndex + 1 < pathWaypoints.size();
            if (hasNextWaypoint) {
                ++currentWaypointIndex;
            }
            else {
                    pathWaypoints.clear();
                    currentWaypointIndex = 0;
            }

            if (!hasNextWaypoint && onArrivedCallback) {
                    auto cb = std::move(onArrivedCallback);
                    onArrivedCallback = nullptr;
                    cb();
            }

            // While standing, optionally orient toward another character (e.g. the
            // player when this NPC is being talked to). The smoothed rotation block
            // below converges facingAngle to targetFacingAngle.
            if (faceTarget) {
                Eigen::Vector3f toFace = faceTarget->position - position;
                toFace.y() = 0.f;
                float d = toFace.norm();
                if (d > 1e-3f) {
                    targetFacingAngle = atan2(toFace.x() / d, -toFace.z() / d);
                }
                else {
                    targetFacingAngle = facingAngle;
                }
            }
            else {
                targetFacingAngle = facingAngle;
            }
        }
    }

    if (hp <= 0)
    {
		if (currentState != AnimationState::ACTION_TO_DEATH && currentState != AnimationState::DEATH_IDLE)
        {
            currentState = AnimationState::ACTION_TO_DEATH;
            resetAnim = true;
        }
    }
    else
    {

        if (battle_state == 1)
        {
            targetFacingAngle = atan2(lookTarget.x() - position.x(), -(lookTarget.z() - position.z()));

            if (currentState == AnimationState::STAND || currentState == AnimationState::WALK) {
                currentState = AnimationState::STAND_TO_ACTION;
                resetAnim = true;
            }

            if (canAttack && attack == 0 && attack_cooldown < 0.f && currentState == AnimationState::ACTION_IDLE)
            {
                attack = 1;
                attack_cooldown = ATTACK_COOLDOWN_TIME;
            }
            if (canAttack && attack_cooldown >= 0.f)
            {
                attack_cooldown = attack_cooldown - deltaMs / 1000.f;
            }

            if (attack == 1 && currentState == AnimationState::ACTION_IDLE)
            {
                if (attackTarget != nullptr && attackTarget->hp > 0)
                {
                    currentState = AnimationState::ACTION_ATTACK;
                    resetAnim = true;
                }
            }
        }
        else
        {
            if (currentState == AnimationState::STAND_TO_ACTION
                || currentState == AnimationState::ACTION_IDLE
                || currentState == AnimationState::ACTION_ATTACK
                ) {
                currentState = AnimationState::ACTION_TO_STAND;
                resetAnim = true;
            }
        }
    }

    // Rotate toward target facing angle at constant angular speed
    float angleDiff = targetFacingAngle - facingAngle;
    while (angleDiff >  static_cast<float>(M_PI)) angleDiff -= 2.f * static_cast<float>(M_PI);
    while (angleDiff < -static_cast<float>(M_PI)) angleDiff += 2.f * static_cast<float>(M_PI);
    float rotStep = rotationSpeed * static_cast<float>(deltaMs) / 1000.f;
    if (std::fabs(angleDiff) <= rotStep) {
        facingAngle = targetFacingAngle;
        // One-shot face-target: once aligned, stop tracking. Otherwise the NPC
        // would keep snapping to the player every tick after the conversation
        // started, even as the player moved away.
        faceTarget = nullptr;
    } else {
        facingAngle += (angleDiff > 0.f ? rotStep : -rotStep);
    }


    if (!isPlayer && hp > 0.f && battle_state == 0 && maxHomeDrift > 0.f) {
        homeDriftCheckTimer -= static_cast<float>(deltaMs) / 1000.f;
        if (homeDriftCheckTimer <= 0.f) {
            homeDriftCheckTimer = HOME_DRIFT_CHECK_INTERVAL;
            Eigen::Vector3f toHome = homePosition - position;
            toHome.y() = 0.f;
            if (!isMoving() && toHome.norm() > maxHomeDrift) {
                setTarget(homePosition);
            }
        }
    }

    if (hitSparkEmitter.isConfigured()) {
        hitSparkEmitter.update(static_cast<float>(deltaMs));
    }

    auto it = animations.find(currentState);
    if (it == animations.end()) return;

    auto& anim = it->second;

    if (resetAnim)
    {
        resetAnim = false;
        anim.currentFrame = 0;
    }
    int prevFrame = anim.currentFrame;
    anim.currentFrame += static_cast<float>(deltaMs) / 24.f;

    if (static_cast<int>(anim.currentFrame) >= 20 && currentState == AnimationState::STAND_TO_ACTION)
    {
            showWeapon = true;
    }
    else if (static_cast<int>(anim.currentFrame) <= 32 && currentState == AnimationState::ACTION_TO_STAND)
    {
            showWeapon = true;
    }
    else if (currentState == AnimationState::ACTION_ATTACK_2 || currentState == AnimationState::ACTION_ATTACK || currentState == AnimationState::ACTION_IDLE)
    {
		    showWeapon = true;
    }
    else
    {
           showWeapon = false;
    }

    auto attackDirectionToTarget = [this]() {
        Eigen::Vector3f dir = attackTarget->position - position;
        dir.y() = 0.f;
        float n = dir.norm();
        if (n > 1e-3f) dir /= n;
        else dir = Eigen::Vector3f::Zero();
        return dir;
    };

    if (isPlayer)
    {
        if (prevFrame < 19 && static_cast<int>(anim.currentFrame) >= 19 && (currentState == AnimationState::ACTION_ATTACK || currentState == AnimationState::ACTION_ATTACK_2))
        {
            if (attackTarget != nullptr)
            {
                attackTarget->applyDamage(10.f, attackDirectionToTarget());
            }
        }
    }
    else
    {
        if (prevFrame < 50 && static_cast<int>(anim.currentFrame) >= 50 && (currentState == AnimationState::ACTION_ATTACK || currentState == AnimationState::ACTION_ATTACK_2))
        {
            if (attackTarget != nullptr)
            {
                attackTarget->applyDamage(10.f, attackDirectionToTarget());
            }
        }
    }
    
    int frms = anim.model.animations[0].keyFrames[anim.model.animations[0].keyFrames.size() - 1].frame;
    if (static_cast<int>(anim.currentFrame) >= frms) {
        anim.currentFrame = anim.model.startingFrame;

        if (currentState == AnimationState::STAND_TO_ACTION)
        {
            currentState = AnimationState::ACTION_IDLE;
        }
        if (currentState == AnimationState::ACTION_TO_STAND)
        {
            currentState = AnimationState::STAND;
        }

		if (currentState == AnimationState::ACTION_ATTACK)
        {
            currentState = AnimationState::ACTION_IDLE;
            attack = 0;
        }

        if (currentState == AnimationState::ACTION_TO_DEATH)
        {
            currentState = AnimationState::DEATH_IDLE;
            if (onDeathAnimComplete) {
                //auto cb = std::move(onDeathAnimComplete);
                //onDeathAnimComplete = nullptr;
                onDeathAnimComplete();
                //TODO: need to add some delay here
                currentState = AnimationState::STAND;
                resetAnim = true;
                showWeapon = false;
                battle_state = 0;
                attack = 0;
            }
        }
    }


    if (static_cast<int>(anim.currentFrame) != anim.lastFrame) {
        anim.gpuSkinningShaderData.ComputeSkinningMatrices(anim.model.startBones, anim.model.animations[0].keyFrames, static_cast<int>(anim.currentFrame));
        if (!useGpuSkinning) {
            anim.model.Interpolate(static_cast<int>(anim.currentFrame));
        }
        anim.lastFrame = static_cast<int>(anim.currentFrame);
    }
}

void Character::draw(Renderer& renderer) {
    // Sparks are world-space billboards — render under the caller's camera matrices.
    if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
        hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
    }

    if (!enabled) return;
    if (!isPlayer && hp <= 0)
    {
        return;
    }
    if (useGpuSkinning) {
        drawGpuSkinning(renderer);
        return;
    }

    AnimationState drawState = resolveActiveState();
    auto it = animations.find(drawState);
    if (it == animations.end()) return;

    renderer.shaderManager.PushShader("fog");
    renderer.RenderUniform1i(textureUniformName, 0);
    const float playerEyePos[3] = { 0.0f, 0.0f, -Environment::zoom };
    renderer.RenderUniform3fv("uPlayerEyePos", playerEyePos);

    renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
        static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
        Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);

    renderer.PushMatrix();
    renderer.TranslateMatrix({ position.x(), position.y(), position.z() });
    renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
    renderer.ScaleMatrix(modelScale);
    renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix());

    auto& anim = it->second;
    for (const auto& name : anim.model.meshNamesOrdered) {
        auto mit = anim.model.meshes.find(name);
        if (mit == anim.model.meshes.end()) continue;
        auto tit = meshTextures.find(name);
        if (tit == meshTextures.end() || !tit->second) continue;
        glBindTexture(GL_TEXTURE_2D, tit->second->getTexID());
        modelMutable.AssignFrom(mit->second.mesh);
        modelMutable.RefreshVBO();
        renderer.DrawVertexRenderStruct(modelMutable);
    }

    renderer.PopMatrix();

    drawAttachedWeapon(renderer);

    renderer.PopProjectionMatrix();
    renderer.shaderManager.PopShader();
}

void Character::drawGpuSkinning(Renderer& renderer) {
    AnimationState drawState = resolveActiveState();
    auto it = animations.find(drawState);
    if (it == animations.end())
    {
        return;
    }

    auto& anim = it->second;
    anim.gpuSkinningShaderData.prepareGpuSkinningVBOs(anim.model);

    if (anim.gpuSkinningShaderData.skinningMatrices.empty())
    {
        if (anim.model.animations.empty() || anim.model.animations[0].keyFrames.empty()) return;
		
        anim.gpuSkinningShaderData.ComputeSkinningMatrices(anim.model.startBones, anim.model.animations[0].keyFrames, static_cast<int>(anim.currentFrame));
      
        if (anim.gpuSkinningShaderData.skinningMatrices.empty()) return;
    }
    static const std::string skinningShaderName = "fog_skinning";
    static const std::string boneMatricesUniform = "uBoneMatrices[0]";

    renderer.shaderManager.PushShader(skinningShaderName);
    renderer.RenderUniform1i(textureUniformName, 0);
    const float playerEyePosSkin[3] = { 0.0f, 0.0f, -Environment::zoom };
    renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosSkin);

    renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
        static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
        Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);

    renderer.PushMatrix();
    renderer.TranslateMatrix({ position.x(), position.y(), position.z() });
    renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
    renderer.ScaleMatrix(modelScale);
    renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix());

    // Upload bone skinning matrices
    renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
        static_cast<int>(anim.gpuSkinningShaderData.skinningMatrices.size()), false,
        anim.gpuSkinningShaderData.skinningMatrices[0].data());

    for (const auto& name : anim.model.meshNamesOrdered) {
        auto pit = anim.gpuSkinningShaderData.perMesh.find(name);
        if (pit == anim.gpuSkinningShaderData.perMesh.end()) continue;
        auto tit = meshTextures.find(name);
        if (tit == meshTextures.end() || !tit->second) continue;
        glBindTexture(GL_TEXTURE_2D, tit->second->getTexID());
        pit->second.RenderVBO(renderer);
    }

    renderer.PopMatrix();

    renderer.shaderManager.PushShader("fog");
    renderer.RenderUniform1i(textureUniformName, 0);
    renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosSkin);
    drawAttachedWeapon(renderer);
    renderer.shaderManager.PopShader();

    renderer.PopProjectionMatrix();
    renderer.shaderManager.PopShader();
}

bool Character::prepareGpuSkinning()
{
    AnimationState drawState = resolveActiveState();
    auto it = animations.find(drawState);
    if (it == animations.end())
    {
        return false;
    }

    auto& anim = it->second;
    anim.gpuSkinningShaderData.prepareGpuSkinningVBOs(anim.model);
    if (anim.gpuSkinningShaderData.skinningMatrices.empty()) {
        if (anim.model.animations.empty() || anim.model.animations[0].keyFrames.empty())
        {
            return false;
        }

        anim.gpuSkinningShaderData.ComputeSkinningMatrices(anim.model.startBones, anim.model.animations[0].keyFrames, static_cast<int>(anim.currentFrame));

        if (anim.gpuSkinningShaderData.skinningMatrices.empty())
        {
            return false;
        }
    }

    return true;
}

void Character::drawAttachedWeapon(Renderer& renderer)
{
    if (!showWeapon) return;
    if (weaponMesh.data.PositionData.size() == 0 || !weaponTexture) return;

    auto it = animations.find(resolveActiveState());
    if (it == animations.end()) return;

    auto& anim = it->second;
    if (anim.gpuSkinningShaderData.skinningMatrices.empty()) return;

    int boneIdx = anim.model.findBoneIndex(weaponAttachBoneName);
    if (boneIdx < 0) return;

    const Eigen::Matrix4f& bindBone = anim.model.animations[0].keyFrames[0].bones[boneIdx].boneMatrixWorld;
    Eigen::Matrix4f currentBone = anim.gpuSkinningShaderData.skinningMatrices[boneIdx] * bindBone;

    renderer.PushMatrix();
    renderer.TranslateMatrix({ position.x(), position.y(), position.z() });
    renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
    renderer.ScaleMatrix(modelScale);
    renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix());
    renderer.TranslateMatrix(Eigen::Vector3f(currentBone.block<3, 1>(0, 3)));
    renderer.RotateMatrix(Eigen::Matrix3f(currentBone.block<3, 3>(0, 0)));
    renderer.TranslateMatrix(weaponInitialPosition);
    renderer.RotateMatrix(weaponInitialRotation);

    glBindTexture(GL_TEXTURE_2D, weaponTexture->getTexID());
    renderer.DrawVertexRenderStruct(weaponMesh);
    renderer.PopMatrix();
}

// ==================== Shadow depth pass ====================

void Character::drawShadowDepth(Renderer& renderer) {
    if (!enabled) return;
    if (!isPlayer && hp <= 0)
    {
        return;
    }
    if (useGpuSkinning) {
        drawShadowDepthGpuSkinning(renderer);
    } else {
        drawShadowDepthCpu(renderer);
    }
}

void Character::drawShadowDepthCpu(Renderer& renderer) {
    AnimationState drawState = resolveActiveState();
    auto it = animations.find(drawState);
    if (it == animations.end()) return;

    // The caller has already pushed the shadow_depth shader and the
    // light's projection + view onto the renderer stacks.
    renderer.PushMatrix();
    renderer.TranslateMatrix({ position.x(), position.y(), position.z() });
    renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
    renderer.ScaleMatrix(modelScale);
    renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix());

    auto& anim = it->second;
    for (const auto& name : anim.model.meshNamesOrdered) {
        auto mit = anim.model.meshes.find(name);
        if (mit == anim.model.meshes.end()) continue;
        auto tit = meshTextures.find(name);
        if (tit == meshTextures.end() || !tit->second) continue;
        modelMutable.AssignFrom(mit->second.mesh);
        modelMutable.RefreshVBO();
        renderer.DrawVertexRenderStruct(modelMutable);
    }
    renderer.PopMatrix();

    drawAttachedWeapon(renderer);
}

void Character::drawShadowDepthGpuSkinning(Renderer& renderer) {

	CheckGlError(__FILE__, __LINE__);
    AnimationState drawState = resolveActiveState();
    auto it = animations.find(drawState);
    if (it == animations.end()) return;

    CheckGlError(__FILE__, __LINE__);

	if (!prepareGpuSkinning()) return;

    CheckGlError(__FILE__, __LINE__);

    static const std::string shadowSkinningShader = "shadow_depth_skinning";
    static const std::string boneMatricesUniform = "uBoneMatrices[0]";

    // Switch to the skinning depth shader (caller has the static depth shader active).
    renderer.shaderManager.PushShader(shadowSkinningShader);

    renderer.PushMatrix();
    renderer.TranslateMatrix({ position.x(), position.y(), position.z() });
    renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
    renderer.ScaleMatrix(modelScale);
    renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix());

    CheckGlError(__FILE__, __LINE__);
    renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
        static_cast<int>(it->second.gpuSkinningShaderData.skinningMatrices.size()), false,
        it->second.gpuSkinningShaderData.skinningMatrices[0].data());
   
    CheckGlError(__FILE__, __LINE__);
    for (const auto& name : it->second.model.meshNamesOrdered) {
        auto pit = it->second.gpuSkinningShaderData.perMesh.find(name);
        if (pit == it->second.gpuSkinningShaderData.perMesh.end()) continue;
        auto tit = meshTextures.find(name);
        if (tit == meshTextures.end() || !tit->second) continue;
        pit->second.RenderVBO(renderer);
    }

    CheckGlError(__FILE__, __LINE__);
    renderer.PopMatrix();
    renderer.shaderManager.PopShader();

    drawAttachedWeapon(renderer);

    CheckGlError(__FILE__, __LINE__);
}

// ==================== Main pass with shadows ====================

void Character::drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) {
    if (!enabled) return;
    // Sparks are world-space billboards — render under the caller's camera matrices.
    if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
        hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
    }

    renderer.RenderUniform1f("uAlpha", 1.0f);
    glDisable(GL_BLEND);

    if (!isPlayer && hp <= 0)
    {
        return;
    }
    if (useGpuSkinning) {
        drawGpuSkinningWithShadow(renderer, lightFromCamera, shadowMapTex, lightDirCamera);
    } else {
        drawCpuWithShadow(renderer, lightFromCamera, shadowMapTex, lightDirCamera);
    }
}

void Character::drawCpuWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) {
    AnimationState drawState = resolveActiveState();
    auto it = animations.find(drawState);
    if (it == animations.end()) return;

    static const std::string shadowShader = "fog_shadow";

    renderer.shaderManager.PushShader(shadowShader);
    renderer.RenderUniform1i(textureUniformName, 0);
    renderer.RenderUniform1i("uShadowMap", 1);
    renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data());
    renderer.RenderUniform3fv("uLightDir", lightDirCamera.data());
    const float playerEyePosShadow[3] = { 0.0f, 0.0f, -Environment::zoom };
    renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosShadow);

    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, shadowMapTex);
    glActiveTexture(GL_TEXTURE0);

    renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
        static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
        Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);

    renderer.PushMatrix();
    renderer.TranslateMatrix({ position.x(), position.y(), position.z() });
    renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
    renderer.ScaleMatrix(modelScale);
    renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix());

    auto& anim = it->second;
    for (const auto& name : anim.model.meshNamesOrdered) {
        auto mit = anim.model.meshes.find(name);
        if (mit == anim.model.meshes.end()) continue;
        auto tit = meshTextures.find(name);
        if (tit == meshTextures.end() || !tit->second) continue;
        glBindTexture(GL_TEXTURE_2D, tit->second->getTexID());
        modelMutable.AssignFrom(mit->second.mesh);
        modelMutable.RefreshVBO();
        renderer.DrawVertexRenderStruct(modelMutable);
    }

    renderer.PopMatrix();

    renderer.shaderManager.PushShader("fog");
    renderer.RenderUniform1i(textureUniformName, 0);
    renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosShadow);
    drawAttachedWeapon(renderer);
    renderer.shaderManager.PopShader();

    renderer.PopProjectionMatrix();
    renderer.shaderManager.PopShader();
}

void Character::drawGpuSkinningWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) {
    AnimationState drawState = resolveActiveState();
    auto it = animations.find(drawState);
    if (it == animations.end()) return;

    CheckGlError(__FILE__, __LINE__);

    if (!prepareGpuSkinning()) return;

    CheckGlError(__FILE__, __LINE__);

    static const std::string skinningShadowShader = "fog_skinning_shadow";
    static const std::string boneMatricesUniform = "uBoneMatrices[0]";

    renderer.shaderManager.PushShader(skinningShadowShader);
    renderer.RenderUniform1i(textureUniformName, 0);
    renderer.RenderUniform1i("uShadowMap", 1);
    renderer.RenderUniformMatrix4fv("uLightFromCamera", false, lightFromCamera.data());
    renderer.RenderUniform3fv("uLightDir", lightDirCamera.data());
    const float playerEyePosSkinShadow[3] = { 0.0f, 0.0f, -Environment::zoom };
    renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosSkinShadow);

    CheckGlError(__FILE__, __LINE__);

    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, shadowMapTex);
    glActiveTexture(GL_TEXTURE0);

    CheckGlError(__FILE__, __LINE__);

    renderer.PushPerspectiveProjectionMatrix(1.0 / 1.5,
        static_cast<float>(Environment::width) / static_cast<float>(Environment::height),
        Environment::CONST_Z_NEAR, Environment::CONST_Z_FAR);

    renderer.PushMatrix();
    renderer.TranslateMatrix({ position.x(), position.y(), position.z() });
    renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(-facingAngle, Eigen::Vector3f::UnitY())).toRotationMatrix());
    renderer.ScaleMatrix(modelScale);
    renderer.RotateMatrix(modelCorrectionRotation.toRotationMatrix());

    CheckGlError(__FILE__, __LINE__);
    renderer.RenderUniformMatrix4fvArray(boneMatricesUniform,
        static_cast<int>(it->second.gpuSkinningShaderData.skinningMatrices.size()), false,
        it->second.gpuSkinningShaderData.skinningMatrices[0].data());

    CheckGlError(__FILE__, __LINE__);

    for (const auto& name : it->second.model.meshNamesOrdered) {
        auto pit = it->second.gpuSkinningShaderData.perMesh.find(name);
        if (pit == it->second.gpuSkinningShaderData.perMesh.end()) continue;
        auto tit = meshTextures.find(name);
        if (tit == meshTextures.end() || !tit->second) continue;
        glBindTexture(GL_TEXTURE_2D, tit->second->getTexID());
        pit->second.RenderVBO(renderer);
    }

    renderer.PopMatrix();

    renderer.shaderManager.PushShader("fog");
    renderer.RenderUniform1i(textureUniformName, 0);
    renderer.RenderUniform3fv("uPlayerEyePos", playerEyePosSkinShadow);
    drawAttachedWeapon(renderer);
    renderer.shaderManager.PopShader();

    CheckGlError(__FILE__, __LINE__);


    renderer.PopProjectionMatrix();
    renderer.shaderManager.PopShader();

    CheckGlError(__FILE__, __LINE__);
}

void Character::setupHitSparks(std::shared_ptr<Texture> sparkTexture)
{
    hitSparkEmitter.setTexture(sparkTexture);
    hitSparkEmitter.setShaderProgramName("spark");
    hitSparkEmitter.setMaxParticles(64);
    hitSparkEmitter.setParticleSize(0.08f);
    hitSparkEmitter.setBiasX(0.0f);
    hitSparkEmitter.setEmissionRate(1.0e9f); // effectively disables auto-emission
    hitSparkEmitter.setSpeedRange(2.0f, 4.0f);
    hitSparkEmitter.setZSpeedRange(0.5f, 2.0f);
    hitSparkEmitter.setScaleRange(0.8f, 1.2f);
    hitSparkEmitter.setLifeTimeRange(300.0f, 600.0f);
    hitSparkEmitter.setIsActive(false);
    hitSparkEmitter.markConfigured();
}

void Character::applyDamage(float damageAmount, const Eigen::Vector3f& attackDirection)
{
    hp = hp - damageAmount;
    if (hp < 0) hp = 0;

    if (!hitSparkEmitter.isConfigured()) return;

    Eigen::Vector3f emitPos = position + Eigen::Vector3f(0.f, 1.f, 0.f);
    hitSparkEmitter.resetEmissionPoints({ emitPos });
    hitSparkEmitter.setEmissionDirection(attackDirection);

    static std::mt19937 gen(std::random_device{}());
    std::uniform_int_distribution<int> countDist(10, 15);
    int count = countDist(gen);
    for (int i = 0; i < count; ++i) {
        hitSparkEmitter.emit();
    }
}

void Character::prepareHitSparksForDraw(const Eigen::Matrix4f& cameraViewMatrix)
{
    if (!hitSparkEmitter.isConfigured()) return;
    hitSparkEmitter.prepareForDraw(cameraViewMatrix);
}

void Character::drawName(TextRenderer& textRenderer,
                         const Eigen::Matrix4f& cameraViewMatrix,
                         const Eigen::Matrix4f& projectionMatrix)
{
    if (isPlayer) return;
    if (!enabled) return;
    if (hp <= 0.f) return;
    if (npcName.empty()) return;

    Eigen::Vector4f worldPos(position.x(), position.y() + 1.83f, position.z(), 1.f);
    Eigen::Vector4f clip = projectionMatrix * cameraViewMatrix * worldPos;
    if (clip.w() <= 0.f) return; // behind the camera

    float ndcX = clip.x() / clip.w();
    float ndcY = clip.y() / clip.w();
    if (ndcX < -1.f || ndcX > 1.f || ndcY < -1.f || ndcY > 1.f) return; // off-screen

    float screenX = (ndcX * 0.5f + 0.5f) * Environment::projectionWidth;
    float screenY = (ndcY * 0.5f + 0.5f) * Environment::projectionHeight;

    std::array<float, 4> color = canAttack
        ? std::array<float, 4>{ 1.f, 0.f, 0.f, 1.f }
        : std::array<float, 4>{ 0.f, 1.f, 0.f, 1.f };

    textRenderer.drawText(npcName, screenX, screenY, 1.0f, true, color);
}

void Character::drawHealthBar(Renderer& renderer,
                              const Eigen::Matrix4f& cameraViewMatrix,
                              const Eigen::Matrix4f& projectionMatrix)
{
    if (!isPlayer && !canAttack) return;
    if (!enabled) return;
    if (hp <= 0.f) return;
    if (initialHp <= 0.f) return;

    Eigen::Vector4f worldPos(position.x(), position.y() + 1.75f, position.z(), 1.f);
    Eigen::Vector4f clip = projectionMatrix * cameraViewMatrix * worldPos;
    if (clip.w() <= 0.f) return;
    float ndcX = clip.x() / clip.w();
    float ndcY = clip.y() / clip.w();
    if (ndcX < -1.2f || ndcX > 1.2f || ndcY < -1.2f || ndcY > 1.2f) return;

    const float sx = (ndcX * 0.5f + 0.5f) * Environment::projectionWidth;
    const float sy = (ndcY * 0.5f + 0.5f) * Environment::projectionHeight;

    const float barW = 60.f;
    const float barH = 6.f;

    const float left   = sx - barW * 0.5f;
    const float right  = sx + barW * 0.5f;
    const float bottom = sy - barH * 0.5f;
    const float top    = sy + barH * 0.5f;

    float frac = hp / initialHp;
    if (frac < 0.f) frac = 0.f;
    if (frac > 1.f) frac = 1.f;
    const float split = left + barW * frac;

    VertexDataStruct data;
    const Eigen::Vector3f green(0.f, 1.f, 0.f);
    const Eigen::Vector3f red  (1.f, 0.f, 0.f);

    auto pushQuad = [&](float x0, float x1, const Eigen::Vector3f& col) {
        data.PositionData.push_back({ x0, bottom, 0.f });
        data.PositionData.push_back({ x1, bottom, 0.f });
        data.PositionData.push_back({ x1, top,    0.f });
        data.PositionData.push_back({ x0, bottom, 0.f });
        data.PositionData.push_back({ x1, top,    0.f });
        data.PositionData.push_back({ x0, top,    0.f });
        for (int i = 0; i < 6; ++i) data.ColorData.push_back(col);
    };

    if (frac > 0.f)  pushQuad(left,  split, green);
    if (frac < 1.f)  pushQuad(split, right, red);

    healthBarMesh.AssignFrom(data);
    healthBarMesh.RefreshVBO();

    renderer.shaderManager.PushShader("defaultColor");
    renderer.PushProjectionMatrix(0.f, Environment::projectionWidth,
                                  0.f, Environment::projectionHeight,
                                  -1.f, 1.f);
    renderer.PushMatrix();
    renderer.LoadIdentity();

    renderer.DrawVertexRenderStruct(healthBarMesh);

    renderer.PopMatrix();
    renderer.PopProjectionMatrix();
    renderer.shaderManager.PopShader();
}

} // namespace ZL
