#include "dialogue/DialogueOverlay.h"
#include "dialogue/DialogueTypes.h"

#include "GameConstants.h"
#include "Environment.h"
#include <algorithm>
#include <array>
#include <cmath>

namespace ZL::Dialogue {

void DialogueOverlay::TexturedQuad::rebuild(const UiRect& newRect) {
	rect = newRect;
	mesh.data = CreateRect2D(
		{ rect.x + rect.w * 0.5f, rect.y + rect.h * 0.5f },
		{ rect.w * 0.5f, rect.h * 0.5f },
		0.0f
	);
	mesh.RefreshVBO();
	initialized = true;
}

void DialogueOverlay::TexturedQuad::rebuildWithUV(
    const UiRect& newRect,
    const Eigen::Vector2f& uvBottomLeft,
    const Eigen::Vector2f& uvTopLeft,
    const Eigen::Vector2f& uvTopRight,
    const Eigen::Vector2f& uvBottomRight
) {
    rect = newRect;

    const float x0 = rect.x;
    const float y0 = rect.y;
    const float x1 = rect.x + rect.w;
    const float y1 = rect.y + rect.h;

    VertexDataStruct data;
    data.PositionData = {
        { x0, y0, 0.0f }, // bottom-left
        { x0, y1, 0.0f }, // top-left
        { x1, y1, 0.0f }, // top-right

        { x1, y1, 0.0f }, // top-right
        { x1, y0, 0.0f }, // bottom-right
        { x0, y0, 0.0f }  // bottom-left
    };

    data.TexCoordData = {
        uvBottomLeft,
        uvTopLeft,
        uvTopRight,

        uvTopRight,
        uvBottomRight,
        uvBottomLeft
    };

    mesh.AssignFrom(data);
    initialized = true;
}

bool DialogueOverlay::init(Renderer& renderer, const std::string& zipFile) {
    rendererRef = &renderer;
    zipFilename = zipFile;

    textboxTexture = renderer.textureManager.LoadFromPng("resources/dialogue/textbox_bg.png", zipFile);
    portraitFrameTexture = renderer.textureManager.LoadFromPng("resources/dialogue/portrait_frame.png", zipFile);
    choiceMainTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_main.png", zipFile);
    choiceOptionalTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_optional.png", zipFile);
    choiceSelectedTexture = renderer.textureManager.LoadFromPng("resources/dialogue/choice_selected.png", zipFile);
    cutsceneSubtitleTexture = renderer.textureManager.LoadFromPng("resources/dialogue/cutscene_subtitle_bg.png", zipFile);

    nameRenderer = std::make_unique<TextRenderer>();
    bodyRenderer = std::make_unique<TextRenderer>();
    choiceRenderer = std::make_unique<TextRenderer>();
    cutsceneRenderer = std::make_unique<TextRenderer>();

    const bool ok =
        nameRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 28, zipFile) &&
        bodyRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 24, zipFile) &&
        choiceRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 22, zipFile) &&
        cutsceneRenderer->init(renderer, "resources/fonts/DroidSans.ttf", 24, zipFile);

    return ok;
}

void DialogueOverlay::update(const PresentationModel& model, int deltaMs) {
    if (model.mode == PresentationMode::Hidden) {
        hoveredChoiceIndex = -1;
        cutsceneSkipHintVisible = false;
        cutsceneSkipArmed = false;
        cutsceneSkipHolding = false;
        cutsceneSkipTriggered = false;
        cutsceneSkipHintRemainingMs = 0;
        cutsceneSkipHoldElapsedMs = 0;
        lastChoiceRects.clear();
        lastDialogueAdvanceRect = {};
        lastCutsceneAdvanceRect = {};
        return;
    }
    
    if (model.mode != PresentationMode::Choice) {
        hoveredChoiceIndex = -1;
    }

    if (model.mode != PresentationMode::Cutscene || !model.cutsceneSkippable) {
        cutsceneSkipHintVisible = false;
        cutsceneSkipArmed = false;
        cutsceneSkipHolding = false;
        cutsceneSkipTriggered = false;
        cutsceneSkipHintRemainingMs = 0;
        cutsceneSkipHoldElapsedMs = 0;
        return;
    }

    const int safeDeltaMs = max(deltaMs, 0);

    if (cutsceneSkipHintVisible) {
        cutsceneSkipHintRemainingMs -= safeDeltaMs;
        if (cutsceneSkipHintRemainingMs <= 0) {
            cutsceneSkipHintVisible = false;
            cutsceneSkipArmed = false;
            cutsceneSkipHolding = false;
            cutsceneSkipHintRemainingMs = 0;
            cutsceneSkipHoldElapsedMs = 0;
        }
    }

    if (cutsceneSkipHolding && cutsceneSkipArmed) {
        cutsceneSkipHoldElapsedMs += safeDeltaMs;
        if (cutsceneSkipHoldElapsedMs >= CutsceneSkipHoldDurationMs) {
            cutsceneSkipTriggered = true;
            cutsceneSkipHolding = false;
            cutsceneSkipHoldElapsedMs = CutsceneSkipHoldDurationMs;
        }
    }
}

void DialogueOverlay::draw(Renderer& renderer, const PresentationModel& model) {
    if (model.mode == PresentationMode::Hidden) {
        lastChoiceRects.clear();
        lastDialogueAdvanceRect = {};
        lastCutsceneAdvanceRect = {};
        cutsceneSkipHintVisible = false;
        cutsceneSkipArmed = false;
        cutsceneSkipHolding = false;
        cutsceneSkipHintRemainingMs = 0;
        cutsceneSkipHoldElapsedMs = 0;
        return;
    }

    if (model.mode == PresentationMode::Cutscene) {
        drawCutscene(renderer, model);
    }
    else {
        drawDialogue(renderer, model);
    }
}

void DialogueOverlay::drawDialogue(Renderer& renderer, const PresentationModel& model) {
    const float W = Environment::projectionWidth;
    // const float H = Environment::projectionHeight;

    const UiRect portraitRect{ 24.0f, 24.0f, 182.0f, 182.0f };
    const UiRect textboxRect{ 220.0f, 24.0f, max(200.0f, W - 244.0f), 182.0f };

    lastDialogueAdvanceRect = { portraitRect.x, portraitRect.y, textboxRect.x + textboxRect.w - portraitRect.x, textboxRect.h };
    lastCutsceneAdvanceRect = {};
    cutsceneSkipHintVisible = false;
    cutsceneSkipArmed = false;
    cutsceneSkipHolding = false;
    cutsceneSkipHintRemainingMs = 0;
    cutsceneSkipHoldElapsedMs = 0;

    if (!portraitQuad.initialized || portraitQuad.rect.w != portraitRect.w || portraitQuad.rect.h != portraitRect.h ||
        portraitQuad.rect.x != portraitRect.x || portraitQuad.rect.y != portraitRect.y) {
        portraitQuad.rebuild(portraitRect);
    }
    if (!textboxQuad.initialized || textboxQuad.rect.w != textboxRect.w || textboxQuad.rect.h != textboxRect.h ||
        textboxQuad.rect.x != textboxRect.x || textboxQuad.rect.y != textboxRect.y) {
        textboxQuad.rebuild(textboxRect);
    }

    glEnable(GL_BLEND);
    renderer.shaderManager.PushShader(defaultShaderName);
    renderer.RenderUniform1i(textureUniformName, 0);
    renderer.PushProjectionMatrix(0.0f, W, 0.0f, Environment::projectionHeight, -10.0f, 10.0f);
    renderer.PushMatrix();
    renderer.LoadIdentity();

    drawQuad(renderer, textboxQuad, textboxTexture);
    drawQuad(renderer, portraitQuad, model.portraitPath.empty() ? portraitFrameTexture : loadTextureCached(model.portraitPath));
    drawQuad(renderer, portraitQuad, portraitFrameTexture);

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

    const float nameX = textboxRect.x + 24.0f;
    const float nameY = textboxRect.y + textboxRect.h - 38.0f;
    const float bodyX = textboxRect.x + 24.0f;
    const float bodyY = textboxRect.y + textboxRect.h - 78.0f;

    if (!model.speaker.empty()) {
        nameRenderer->drawText(model.speaker, nameX, nameY, 1.0f, false, { 1.0f, 0.88f, 0.45f, 1.0f });
    }


    const float bodyTextScale = 1.0f;
    const float bodyMaxWidthPx = textboxRect.w - 48.0f;
    
    const std::string wrappedBody = wrapTextToWidth(model.visibleText, *bodyRenderer, bodyMaxWidthPx, bodyTextScale);
    bodyRenderer->drawText(wrappedBody, bodyX, bodyY, bodyTextScale, false, { 1.0f, 1.0f, 1.0f, 1.0f });

    lastChoiceRects.clear();
    if (model.mode == PresentationMode::Choice) {
        const float choiceStartY = textboxRect.y + 56.0f;
        const float choiceHeight = 30.0f;
        const float choiceSpacing = 8.0f;
        const float choiceWidth = textboxRect.w - 48.0f;

        if (choiceQuads.size() < model.choices.size()) {
            choiceQuads.resize(model.choices.size());
        }

        renderer.shaderManager.PushShader(defaultShaderName);
        renderer.RenderUniform1i(textureUniformName, 0);
        renderer.PushProjectionMatrix(0.0f, W, 0.0f, Environment::projectionHeight, -10.0f, 10.0f);
        renderer.PushMatrix();
        renderer.LoadIdentity();

        for (size_t i = 0; i < model.choices.size(); ++i) {
            const float y = choiceStartY + (choiceHeight + choiceSpacing) * static_cast<float>(model.choices.size() - 1 - i);
            UiRect rect{ textboxRect.x + 20.0f, y, choiceWidth, choiceHeight };
            lastChoiceRects.push_back(rect);
            choiceQuads[i].rebuild(rect);

            const bool isHighlighted = static_cast<int>(i) == hoveredChoiceIndex || static_cast<int>(i) == model.selectedChoice;
            std::shared_ptr<Texture> choiceTexture = (model.choices[i].kind == ChoiceKind::Optional) ? choiceOptionalTexture : choiceMainTexture;
            if (isHighlighted) {
                choiceTexture = choiceSelectedTexture;
            }
            drawQuad(renderer, choiceQuads[i], choiceTexture);
        }

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

        for (size_t i = 0; i < model.choices.size(); ++i) {
            const UiRect& rect = lastChoiceRects[i];
            const bool isHighlighted = static_cast<int>(i) == hoveredChoiceIndex || static_cast<int>(i) == model.selectedChoice;
            const std::array<float, 4> color = (model.choices[i].kind == ChoiceKind::Optional)
                ? std::array<float, 4>{0.82f, 0.82f, 0.82f, 1.0f}
                : std::array<float, 4>{ 1.0f, 0.93f, 0.65f, 1.0f };

            const float choiceTextScale = 1.0f;
            const float choiceMaxWidthPx = rect.w - 28.0f;

            const std::string wrappedChoiceText = wrapTextToWidth(
                model.choices[i].text,
                *choiceRenderer,
                choiceMaxWidthPx,
                choiceTextScale
            );

            choiceRenderer->drawText(
                wrappedChoiceText,
                rect.x + 14.0f,
                rect.y + 9.0f,
                choiceTextScale,
                false,
                isHighlighted ? std::array<float, 4>{1.0f, 1.0f, 1.0f, 1.0f} : color
            );
        }
    }

    glDisable(GL_BLEND);
}

float DialogueOverlay::lerpFloat(float a, float b, float t) {
    return a + (b - a) * t;
}

DialogueOverlay::ResolvedViewport DialogueOverlay::resolveViewportPose(
    const CutsceneCameraPose& pose,
    float texW,
    float texH,
    float screenW,
    float screenH
) {
    ResolvedViewport out{};

    const float safeTexW = max(texW, 1.0f);
    const float safeTexH = max(texH, 1.0f);
    const float safeScreenW = max(screenW, 1.0f);
    const float safeScreenH = max(screenH, 1.0f);

    const float screenAspect = safeScreenW / safeScreenH;
    const float imageAspect = safeTexW / safeTexH;

    float baseViewportW = 0.0f;
    float baseViewportH = 0.0f;

    if (imageAspect >= screenAspect) {
        baseViewportH = safeTexH;
        baseViewportW = safeTexH * screenAspect;
    }
    else {
        baseViewportW = safeTexW;
        baseViewportH = safeTexW / screenAspect;
    }

    const float zoom = max(pose.zoom, 0.01f);
    const float viewportW = baseViewportW / zoom;
    const float viewportH = baseViewportH / zoom;

    const float rotationRad = pose.rotationDeg * 3.14159265358979323846f / 180.0f;
    const float c = std::cos(rotationRad);
    const float s = std::sin(rotationRad);

    // Bounding box повернутого viewport внутри source image.
    const float halfRotatedW = std::abs((viewportW * 0.5f) * c) + std::abs((viewportH * 0.5f) * s);
    const float halfRotatedH = std::abs((viewportW * 0.5f) * s) + std::abs((viewportH * 0.5f) * c);

    float centerX = safeTexW * 0.5f;
    float centerY = safeTexH * 0.5f;

    switch (pose.anchor) {
    case CutsceneAnchor::TopLeft:
        centerX = halfRotatedW;
        centerY = safeTexH - halfRotatedH;
        break;
    case CutsceneAnchor::TopRight:
        centerX = safeTexW - halfRotatedW;
        centerY = safeTexH - halfRotatedH;
        break;
    case CutsceneAnchor::BottomRight:
        centerX = safeTexW - halfRotatedW;
        centerY = halfRotatedH;
        break;
    case CutsceneAnchor::BottomLeft:
        centerX = halfRotatedW;
        centerY = halfRotatedH;
        break;
    case CutsceneAnchor::Custom:
        // centerY: 0 = top, 1 = bottom
        centerX = std::clamp(pose.centerX, 0.0f, 1.0f) * safeTexW;
        centerY = (1.0f - std::clamp(pose.centerY, 0.0f, 1.0f)) * safeTexH;
        centerX = std::clamp(centerX, halfRotatedW, safeTexW - halfRotatedW);
        centerY = std::clamp(centerY, halfRotatedH, safeTexH - halfRotatedH);
        break;
    case CutsceneAnchor::Center:
    default:
        centerX = safeTexW * 0.5f;
        centerY = safeTexH * 0.5f;
        break;
    }

    out.centerXPx = centerX;
    out.centerYPx = centerY;
    out.widthPx = viewportW;
    out.heightPx = viewportH;
    out.rotationDeg = pose.rotationDeg;
    return out;
}

DialogueOverlay::ResolvedViewport DialogueOverlay::blendViewport(
    const ResolvedViewport& from,
    const ResolvedViewport& to,
    float t
) {
    ResolvedViewport out;
    out.centerXPx = lerpFloat(from.centerXPx, to.centerXPx, t);
    out.centerYPx = lerpFloat(from.centerYPx, to.centerYPx, t);
    out.widthPx = lerpFloat(from.widthPx, to.widthPx, t);
    out.heightPx = lerpFloat(from.heightPx, to.heightPx, t);
    out.rotationDeg = lerpFloat(from.rotationDeg, to.rotationDeg, t);
    return out;
}

void DialogueOverlay::drawCutscene(Renderer& renderer, const PresentationModel& model) {
    const float W = Environment::projectionWidth;
    const float H = Environment::projectionHeight;
    const UiRect subtitleRect{ W * 0.12f, 22.0f, W * 0.76f, 110.0f };

    lastDialogueAdvanceRect = {};
    lastCutsceneAdvanceRect = subtitleRect;

    glEnable(GL_BLEND);

    renderer.shaderManager.PushShader("cutsceneFade");
    renderer.RenderUniform1i(textureUniformName, 0);
    renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
    renderer.PushMatrix();
    renderer.LoadIdentity();

    const UiRect screenRect{ 0.0f, 0.0f, W, H };

    std::vector<PresentedCutsceneImage> imageLayers = model.cutsceneImages;
    if (imageLayers.empty() && !model.backgroundPath.empty()) {
        imageLayers.push_back({ model.backgroundPath, 1.0f });
    }

    for (const PresentedCutsceneImage& layer : imageLayers) {
        const auto texture = loadTextureCached(layer.path);
        if (!texture) {
            continue;
        }

        // Logical content dimensions for camera and UV math.
        // backgroundWidth/backgroundHeight define the coordinate space the camera track was authored in;
        // the texture is assumed to fill this space entirely regardless of its actual pixel dimensions.
        const float imgW = (model.backgroundWidth > 0) ? static_cast<float>(model.backgroundWidth) : static_cast<float>(texture->getWidth());
        const float imgH = (model.backgroundHeight > 0) ? static_cast<float>(model.backgroundHeight) : static_cast<float>(texture->getHeight());

        ResolvedViewport layerViewport{};

        if (model.cutsceneCamera.active) {
            const ResolvedViewport fromViewport = resolveViewportPose(model.cutsceneCamera.from, imgW, imgH, W, H);
            const ResolvedViewport toViewport = resolveViewportPose(model.cutsceneCamera.to, imgW, imgH, W, H);
            layerViewport = blendViewport(
                fromViewport,
                toViewport,
                std::clamp(model.cutsceneCamera.t, 0.0f, 1.0f)
            );
        }
        else {
            layerViewport = resolveViewportPose(CutsceneCameraPose{}, imgW, imgH, W, H);
        }

        const float halfW = layerViewport.widthPx * 0.5f;
        const float halfH = layerViewport.heightPx * 0.5f;
        const float rotationRad = layerViewport.rotationDeg * 3.14159265358979323846f / 180.0f;

        const float c = std::cos(rotationRad);
        const float s = std::sin(rotationRad);

        auto rotatePoint = [&](float x, float y) -> Eigen::Vector2f {
			return {
				layerViewport.centerXPx + x * c - y * s,
				layerViewport.centerYPx + x * s + y * c
			};
		};

        // Source viewport corners in logical image space, then converted to UV via actual texture size.
        const Eigen::Vector2f srcBL = rotatePoint(-halfW, -halfH);
        const Eigen::Vector2f srcTL = rotatePoint(-halfW, +halfH);
        const Eigen::Vector2f srcTR = rotatePoint(+halfW, +halfH);
        const Eigen::Vector2f srcBR = rotatePoint(+halfW, -halfH);

        auto toUV = [&](const Eigen::Vector2f& p) -> Eigen::Vector2f {
			return {
				std::clamp(p.x() / max(imgW, 1.0f), 0.0f, 1.0f),
				std::clamp(p.y() / max(imgH, 1.0f), 0.0f, 1.0f)
			};
		};

        backgroundQuad.rebuildWithUV(
            screenRect,
            toUV(srcBL),
            toUV(srcTL),
            toUV(srcTR),
            toUV(srcBR)
        );

        renderer.RenderUniform1f("uAlpha", std::clamp(layer.alpha * model.cutsceneGlobalFadeAlpha, 0.0f, 1.0f));
        drawQuad(renderer, backgroundQuad, texture);
    }

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

    // Black overlay: fades IN over the game world (phase 1), then fades OUT to reveal cutscene (phase 2).
    // Image renders at cutsceneGlobalFadeAlpha (0 during phase 1 so game world shows through for the
    // fade-to-black effect, 1 during phase 2 so it blocks the world while black peels away).
    if (model.cutsceneBlackAlpha > 0.001f) {
        const float blackAlpha = model.cutsceneBlackAlpha;
        renderer.shaderManager.PushShader("cutsceneBlack");
        renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
        renderer.PushMatrix();
        renderer.LoadIdentity();

        backgroundQuad.rebuild(screenRect);
        renderer.RenderUniform1f("uAlpha", blackAlpha);
        renderer.DrawVertexRenderStruct(backgroundQuad.mesh);

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

    // UI quads over the image: subtitle panel and skip progress hint background.
    renderer.shaderManager.PushShader(defaultShaderName);
    renderer.RenderUniform1i(textureUniformName, 0);
    renderer.PushProjectionMatrix(0.0f, W, 0.0f, H, -10.0f, 10.0f);
    renderer.PushMatrix();
    renderer.LoadIdentity();

    if (model.showCutsceneSubtitle) {
        subtitleQuad.rebuild(subtitleRect);
        drawQuad(renderer, subtitleQuad, cutsceneSubtitleTexture);
    }

    if (model.cutsceneSkippable && cutsceneSkipHintVisible) {
        const UiRect hintBg{ W - 250.0f, H - 62.0f, 226.0f, 42.0f };
        skipHintBgQuad.rebuild(hintBg);
        drawQuad(renderer, skipHintBgQuad, choiceOptionalTexture);

        const UiRect progressBg{ W - 232.0f, H - 34.0f, 190.0f, 7.0f };
        skipProgressBgQuad.rebuild(progressBg);
        drawQuad(renderer, skipProgressBgQuad, choiceOptionalTexture);

        if (cutsceneSkipHolding) {
            const float progress = std::clamp(
                static_cast<float>(cutsceneSkipHoldElapsedMs) / static_cast<float>(CutsceneSkipHoldDurationMs),
                0.0f,
                1.0f
            );
            const UiRect progressFill{ progressBg.x, progressBg.y, progressBg.w * progress, progressBg.h };
            skipProgressFillQuad.rebuild(progressFill);
            drawQuad(renderer, skipProgressFillQuad, choiceMainTexture);
        }
    }

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

    if (model.showCutsceneSubtitle) {
        if (!model.speaker.empty()) {
            nameRenderer->drawText(
                model.speaker, 
                subtitleRect.x + 24.0f, 
                subtitleRect.y + subtitleRect.h - 32.0f, 
                1.0f, 
                false, 
                { 1.0f, 0.88f, 0.45f, 1.0f }
            );
        }
        const float subtitleTextScale = 1.0f;
        const float subtitleMaxWidthPx = subtitleRect.w - 48.0f;

        const std::string wrappedSubtitle = wrapTextToWidth(
            model.visibleText,
            *cutsceneRenderer,
            subtitleMaxWidthPx,
            subtitleTextScale
        );

        cutsceneRenderer->drawText(
            wrappedSubtitle,
            subtitleRect.x + 24.0f,
            subtitleRect.y + 30.0f,
            subtitleTextScale,
            false,
            { 1.0f, 1.0f, 1.0f, 1.0f }
        );
    }

    if (model.cutsceneSkippable && cutsceneSkipHintVisible) {
        choiceRenderer->drawText(
            cutsceneSkipHolding ? "Hold to skip..." : "Hold LMB to skip",
            W - 232.0f,
            H - 50.0f,
            0.85f,
            false,
            { 1.0f, 1.0f, 1.0f, 0.95f }
        );
    }

    glDisable(GL_BLEND);
}

bool DialogueOverlay::consumeSkipRequested() {
    const bool result = cutsceneSkipTriggered;
    cutsceneSkipTriggered = false;

    if (result) {
        cutsceneSkipHintVisible = false;
        cutsceneSkipArmed = false;
        cutsceneSkipHolding = false;
        cutsceneSkipHintRemainingMs = 0;
        cutsceneSkipHoldElapsedMs = 0;
    }

    return result;
}

void DialogueOverlay::handlePointerDown(float x, float y, const PresentationModel& model) {
    (void)x;
    (void)y;
    
    if (model.mode == PresentationMode::Choice) {
        handlePointerMoved(x, y, model);
        return;
    }

    if (model.mode == PresentationMode::Cutscene && model.cutsceneSkippable) {
        // First click/tap only arms skip and shows the hint for a short time.
        // It does not immediately start skipping, to avoid accidental skip.
        if (!cutsceneSkipArmed) {
            cutsceneSkipHintVisible = true;
            cutsceneSkipArmed = true;
            cutsceneSkipHolding = false;
            cutsceneSkipTriggered = false;
            cutsceneSkipHintRemainingMs = CutsceneSkipHintDurationMs;
            cutsceneSkipHoldElapsedMs = 0;
            return;
        }

        // Once armed, holding anywhere on the screen starts skip progress.
        cutsceneSkipHintVisible = true;
        cutsceneSkipHintRemainingMs = CutsceneSkipHintDurationMs;
        cutsceneSkipHolding = true;
        cutsceneSkipTriggered = false;
        cutsceneSkipHoldElapsedMs = 0;
        return;
    }
}

void DialogueOverlay::handlePointerMoved(float x, float y, const PresentationModel& model) {
    if (model.mode == PresentationMode::Choice) {
        hoveredChoiceIndex = -1;
        for (size_t i = 0; i < lastChoiceRects.size(); ++i) {
            if (rectContains(lastChoiceRects[i], x, y)) {
                hoveredChoiceIndex = static_cast<int>(i);
                break;
            }
        }
        return;
    }

    hoveredChoiceIndex = -1;
}

bool DialogueOverlay::handlePointerReleased(float x, float y, const PresentationModel& model, int& outChoiceIndex, bool& outAdvanceDialogue) {
    outChoiceIndex = -1;
    outAdvanceDialogue = false;

    if (model.mode == PresentationMode::Choice) {
        for (size_t i = 0; i < lastChoiceRects.size(); ++i) {
            if (rectContains(lastChoiceRects[i], x, y)) {
                outChoiceIndex = static_cast<int>(i);
                return true;
            }
        }
        return false;
    }
    
    if (model.mode == PresentationMode::Dialogue) {
        outAdvanceDialogue = rectContains(lastDialogueAdvanceRect, x, y);
        return outAdvanceDialogue;
    }

    if (model.mode == PresentationMode::Cutscene) {
		if (cutsceneSkipHolding && cutsceneSkipHoldElapsedMs < CutsceneSkipHoldDurationMs) {
            cutsceneSkipHolding = false;
            cutsceneSkipHoldElapsedMs = 0;
        }
        return true;
	}

    return false;
}

std::shared_ptr<Texture> DialogueOverlay::loadTextureCached(const std::string& path) {
    if (path.empty()) {
        return nullptr;
    }

    return rendererRef->textureManager.LoadFromPng(path, zipFilename);
}

void DialogueOverlay::drawQuad(Renderer& renderer, const TexturedQuad& quad, const std::shared_ptr<Texture>& texture) const {
    if (!texture) {
        return;
    }

    glBindTexture(GL_TEXTURE_2D, texture->getTexID());
    renderer.DrawVertexRenderStruct(quad.mesh);
}

std::string DialogueOverlay::wrapText(const std::string& input, size_t maxLineLength) {
    if (input.size() <= maxLineLength) {
        return input;
    }

    std::string output;
    size_t lineStart = 0;
    while (lineStart < input.size()) {
        size_t lineEnd = min(lineStart + maxLineLength, input.size());
        if (lineEnd < input.size()) {
            const size_t split = input.rfind(' ', lineEnd);
            if (split != std::string::npos && split > lineStart) {
                lineEnd = split;
            }
        }

        output.append(input.substr(lineStart, lineEnd - lineStart));
        if (lineEnd < input.size()) {
            output.push_back('\n');
            lineStart = lineEnd + (input[lineEnd] == ' ' ? 1 : 0);
        }
        else {
            lineStart = lineEnd;
        }
    }

    return output;
}

std::string DialogueOverlay::wrapTextToWidth(const std::string& input, const TextRenderer& textRenderer, float maxWidthPx, float scale)
{
    if (input.empty() || maxWidthPx <= 1.0f) {
        return input;
    }

    std::string output;
    std::string currentLine;
    std::string currentWord;

    auto flushLine = [&]() {
        if (!currentLine.empty()) {
            if (!output.empty()) {
                output.push_back('\n');
            }
            output += currentLine;
            currentLine.clear();
        }
    };

    auto pushWord = [&](const std::string& word) {
        if (word.empty()) {
            return;
        }

        if (currentLine.empty()) {
            currentLine = word;
            return;
        }
        
        const std::string candidate = currentLine + " " + word;
        if (textRenderer.measureTextWidth(candidate, scale) <= maxWidthPx) {
            currentLine = candidate;
        }
        else {
            flushLine();
            currentLine = word;
        }
    };

    for (size_t i = 0; i < input.size(); ++i) {
        const char ch = input[i];

        if (ch == '\n') {
            pushWord(currentWord);
            currentWord.clear();
            flushLine();
            continue;
        }

        if (ch == ' ' || ch == '\t' || ch == '\r') {
            pushWord(currentWord);
            currentWord.clear();
            continue;
        }

        currentWord.push_back(ch);
    }

    pushWord(currentWord);
    flushLine();

    return output;
}

bool DialogueOverlay::rectContains(const UiRect& rect, float x, float y) {
    return x >= rect.x && x <= rect.x + rect.w && y >= rect.y && y <= rect.y + rect.h;
}

} // namespace ZL::Dialogue