//========================================================================
//
// MarkedContentOutputDev.cc
//
// This file is licensed under the GPLv2 or later
//
// Copyright 2013 Igalia S.L.
// Copyright 2018-2020, 2022, 2025 Albert Astals Cid <aacid@kde.org>
// Copyright 2021, 2023 Adrian Johnson <ajohnson@redneon.com>
// Copyright 2022 Oliver Sander <oliver.sander@tu-dresden.de>
// Copyright 2025 g10 Code GmbH, Author: Sune Stolborg Vuorela <sune@vuorela.dk>
// Copyright (C) 2025 Jonathan Hähne <jonathan.haehne@hotmail.com>
// Copyright 2026 Stefan Brüns <stefan.bruens@rwth-aachen.de>
//
//========================================================================

#include "MarkedContentOutputDev.h"
#include "GlobalParams.h"
#include "UnicodeMap.h"
#include "GfxState.h"
#include "GfxFont.h"
#include "Annot.h"
#include <cmath>
#include <vector>

MarkedContentOutputDev::MarkedContentOutputDev(int mcidA, const Object &stmObj) : currentFont(nullptr), currentText(nullptr), mcid(mcidA)
{
    stmRef = stmObj.copy();
    currentColor.r = currentColor.g = currentColor.b = 0;
}

MarkedContentOutputDev::~MarkedContentOutputDev() = default;

void MarkedContentOutputDev::endSpan()
{
    if (currentText && !currentText->empty()) {
        // The TextSpan takes ownership of currentText and
        // increases the reference count for currentFont.
        textSpans.push_back(TextSpan(std::move(currentText), currentFont, currentColor));
    }
    currentText.reset();
}

void MarkedContentOutputDev::startPage(int /*pageNum*/, GfxState *state, XRef * /*xref*/)
{
    if (state) {
        pageWidth = state->getPageWidth();
        pageHeight = state->getPageHeight();
    } else {
        pageWidth = pageHeight = 0.0;
    }
}

void MarkedContentOutputDev::endPage()
{
    pageWidth = pageHeight = 0.0;
}

void MarkedContentOutputDev::beginForm(Object * /* obj */, Ref id)
{
    formStack.push_back(id);
}

void MarkedContentOutputDev::endForm(Object * /* obj */, Ref /*id*/)
{
    formStack.pop_back();
}

bool MarkedContentOutputDev::contentStreamMatch()
{
    if (stmRef.isRef()) {
        if (formStack.empty()) {
            return false;
        }
        return formStack.back() == stmRef.getRef();
    }
    return formStack.empty();
}

void MarkedContentOutputDev::beginMarkedContent(const std::string & /*name*/, Dict *properties)
{
    int id = -1;
    if (properties) {
        properties->lookupInt("MCID", {}, &id);
    }

    if (id == -1) {
        return;
    }

    // The stack keep track of MCIDs of nested marked content.
    if (inMarkedContent() || (id == mcid && contentStreamMatch())) {
        mcidStack.push_back(id);
    }
}

void MarkedContentOutputDev::endMarkedContent(GfxState * /*state*/)
{
    if (inMarkedContent()) {
        mcidStack.pop_back();
        // The outer marked content sequence MCID was popped, ensure
        // that the last piece of text collected ends up in a TextSpan.
        if (!inMarkedContent()) {
            endSpan();
        }
    }
}

bool MarkedContentOutputDev::needFontChange(const std::shared_ptr<const GfxFont> &font) const
{
    if (currentFont == font) {
        return false;
    }

    if (!currentFont) {
        return font != nullptr && font->isOk();
    }

    if (font == nullptr) {
        return true;
    }

    // Two non-null valid fonts are the same if they point to the same Ref
    if (*currentFont->getID() == *font->getID()) {
        return false;
    }

    return true;
}

void MarkedContentOutputDev::drawChar(GfxState *state, double xx, double yy, double dx, double dy, double /*ox*/, double /*oy*/, CharCode c, int /*nBytes*/, const Unicode *u, int uLen)
{
    if (!inMarkedContent() || !uLen) {
        return;
    }

    // Color changes are tracked here so the color can be chosen depending on
    // the render mode (for mode 1 stroke color is used), so there is no need
    // to implement both updateFillColor() and updateStrokeColor().
    bool colorChange = false;
    GfxRGB color;
    if ((state->getRender() & 3) == 1) {
        state->getStrokeRGB(&color);
    } else {
        state->getFillRGB(&color);
    }

    colorChange = (color.r != currentColor.r || color.g != currentColor.g || color.b != currentColor.b);

    // Check also for font changes.
    bool fontChange = needFontChange(state->getFont());

    // Save a span with the current changes.
    if (colorChange || fontChange) {
        endSpan();
    }

    // Perform the color/font changes.
    if (colorChange) {
        currentColor = color;
    }

    if (fontChange) {
        currentFont = state->getFont();
    }

    double sp, dx2, dy2, w1, h1, x1, y1;

    // Subtract char and word spacing from the (dx,dy) values
    sp = state->getCharSpace();
    if (c == static_cast<CharCode>(0x20)) {
        sp += state->getWordSpace();
    }
    state->textTransformDelta(sp * state->getHorizScaling(), 0, &dx2, &dy2);
    dx -= dx2;
    dy -= dy2;
    state->transformDelta(dx, dy, &w1, &h1);
    state->transform(xx, yy, &x1, &y1);

    // Throw away characters that are not inside the page boundaries.
    if (x1 + w1 < 0 || x1 > pageWidth || y1 + h1 < 0 || y1 > pageHeight) {
        return;
    }

    if (std::isnan(x1) || std::isnan(y1) || std::isnan(w1) || std::isnan(h1)) {
        return;
    }

    for (int i = 0; i < uLen; i++) {
        // Soft hyphen markers are skipped, as they are invisible unless
        // rendering is done to an actual device and the hyphenation hint
        // used. MarkedContentOutputDev extracts the *visible* text content.
        if (u[i] != 0x00AD) {
            // Add the UTF-8 sequence to the current text span.
            if (!unicodeMap) {
                unicodeMap = globalParams->getTextEncoding();
            }

            char buf[8];
            int n = unicodeMap->mapUnicode(u[i], buf, sizeof(buf));
            if (n > 0) {
                if (currentText == nullptr) {
                    currentText = std::make_unique<GooString>();
                }
                currentText->append(buf, n);
            }
        }
    }
}

const TextSpanArray &MarkedContentOutputDev::getTextSpans() const
{
    return textSpans;
}
