// SPDX-License-Identifier: Apache-2.0
#include <vtbackend/SoAClusterWriter.hpp>

#include <libunicode/convert.h>
#include <libunicode/grapheme_segmenter.h>
#include <libunicode/utf8.h>
#include <libunicode/width.h>

#include <algorithm>
#include <cassert>
#include <cstdint>
#include <string_view>

namespace vtbackend
{

namespace
{

    /// Fast path: write pure ASCII text directly to SoA arrays.
    /// Bulk-fills SGR/width/hyperlink arrays instead of per-cell writeCellToSoA.
    /// Avoids the per-cell trivial flag check (sets it ONCE at the end).
    ///
    /// When writing to a freshly-reset trivial line with matching attributes,
    /// skips redundant widths/sgr/hyperlinks fills (they already hold correct values).
    size_t writeAsciiToSoA(LineSoA& line,
                           size_t startCol,
                           std::string_view text,
                           GraphicsAttributes const& attrs,
                           HyperlinkId hyperlink) noexcept
    {
        assert(!line.codepoints.empty());
        auto const maxCols = line.codepoints.size();
        if (startCol >= maxCols)
            return 0;
        auto const count = std::min(text.size(), maxCols - startCol);

        // Widen ASCII bytes to char32_t codepoints (always required)
        for (size_t i = 0; i < count; ++i)
            line.codepoints[startCol + i] = static_cast<char32_t>(text[i]);

        // clusterSize: always required (reset fills with 0, we need 1)
        std::fill_n(line.clusterSize.data() + startCol, count, uint8_t { 1 });

        // Skip redundant fills when writing to a freshly-reset trivial line
        // with matching attributes. The arrays already contain correct values
        // from the most recent resetLine/clearRange.
        auto const skipFills =
            line.trivial && (startCol == 0) && (attrs == line.fillAttrs) && (hyperlink == HyperlinkId {});

        if (!skipFills)
        {
            std::fill_n(line.widths.data() + startCol, count, uint8_t { 1 });
            std::fill_n(line.sgr.data() + startCol, count, attrs);
            std::fill_n(line.hyperlinks.data() + startCol, count, hyperlink);

            // Ordinary text carries no sizing. Every per-cell write path resets these two; leaving
            // them here let a previous `OSC 66` block's scale survive underneath the text that
            // replaced it, so the text was drawn at the block's size. A line that still carries a
            // block is never trivial (@see BasicCellProxy::setTextScale), so the skip above cannot
            // hide a stale scale.
            std::fill_n(line.scales.data() + startCol, count, uint8_t { 1 });
            std::fill_n(line.textScaleExtras.data() + startCol, count, uint16_t { 0 });
        }

        clearReplacedImageFragments(line.imageFragments, static_cast<uint16_t>(startCol), count);

        // Trivial flag: check if the written range breaks SGR uniformity.
        // Compare against an unwritten cell: when writing from col 0, check the cell just past
        // the written range; when writing from col > 0, check cell 0.
        if (line.trivial && count < maxCols)
        {
            auto const checkCol = (startCol > 0) ? size_t(0) : startCol + count;
            if (checkCol < maxCols && (attrs != line.sgr[checkCol] || hyperlink != line.hyperlinks[checkCol]))
            {
                line.trivial = false;
            }
        }

        return count;
    }

    /// Slow path: decode UTF-8, segment grapheme clusters, write to SoA.
    /// Handles multi-codepoint clusters, wide characters, and combining marks.
    size_t writeNonAsciiToSoA(LineSoA& line,
                              size_t startCol,
                              std::string_view text,
                              GraphicsAttributes const& attrs,
                              HyperlinkId hyperlink) noexcept
    {
        assert(!line.codepoints.empty());
        auto const maxCols = line.codepoints.size();
        auto col = startCol;

        unicode::utf8_decoder_state utf8State {};
        unicode::grapheme_segmenter_state graphemeState {};
        char32_t prevCodepoint = 0;

        for (auto const byte: text)
        {
            auto const result = unicode::from_utf8(utf8State, static_cast<uint8_t>(byte));

            if (std::holds_alternative<unicode::Incomplete>(result))
                continue;

            if (std::holds_alternative<unicode::Invalid>(result))
            {
                // Invalid UTF-8: write replacement character
                if (col < maxCols)
                {
                    writeCellToSoA(line, col, U'\uFFFD', 1, attrs, hyperlink);
                    ++col;
                }
                utf8State = {};
                graphemeState = {};
                prevCodepoint = 0;
                continue;
            }

            auto const codepoint = std::get<unicode::Success>(result).value;
            auto const charWidth = static_cast<uint8_t>(std::max(1u, unicode::width(codepoint)));

            // Determine if this codepoint starts a new grapheme cluster
            bool const startsNewCluster = [&] {
                if (!prevCodepoint)
                {
                    unicode::grapheme_process_init(codepoint, graphemeState);
                    return true;
                }
                return unicode::grapheme_process_breakable(codepoint, graphemeState);
            }();

            if (startsNewCluster)
            {
                // Start a new cell
                if (col >= maxCols)
                    break;

                writeCellToSoA(line, col, codepoint, charWidth, attrs, hyperlink);

                // Fill continuation cells for wide characters
                if (charWidth > 1)
                    fillWideCharContinuation(line, col + 1, charWidth - 1, attrs, hyperlink);

                col += charWidth;
            }
            else
            {
                // Continuation codepoint — append to previous cell's grapheme cluster
                if (col > startCol)
                {
                    auto const prevCol = col - 1;
                    // Walk back to find the actual cell (skip continuation cells)
                    auto targetCol = prevCol;
                    while (targetCol > startCol
                           && line.sgr[targetCol].flags.contains(CellFlag::WideCharContinuation))
                        --targetCol;

                    // A variation selector can widen the cluster it joins, which moves the write
                    // position. There is no cursor, no margin and no insert mode here -- only the end
                    // of the line to stay inside of.
                    auto const delta = appendCodepointToCluster(line, targetCol, codepoint);
                    auto const newWidth = static_cast<size_t>(line.widths[targetCol]);

                    if (delta > 0)
                    {
                        if (targetCol + newWidth <= maxCols)
                        {
                            fillWideCharContinuation(line,
                                                     targetCol + newWidth - static_cast<size_t>(delta),
                                                     static_cast<size_t>(delta),
                                                     line.sgr[targetCol],
                                                     line.hyperlinks[targetCol]);
                            col += static_cast<size_t>(delta);
                        }
                        else
                            // No room to grow: keep the cluster at the width it already occupies.
                            line.widths[targetCol] = static_cast<uint8_t>(newWidth - delta);
                    }
                    else if (delta < 0)
                    {
                        // A cluster never narrows: VS15 selects a text presentation without changing
                        // the width, and this line is being rebuilt from text that was laid out under
                        // that same rule -- narrowing here would give a column back that the original
                        // write never released, so the inflated line would not match what was stored.
                        // @see Screen::applyClusterWidthChange, terminal-unicode-core "VS15".
                        line.widths[targetCol] = static_cast<uint8_t>(newWidth - delta);
                    }
                }
            }

            prevCodepoint = codepoint;
        }

        return col - startCol;
    }

} // namespace

size_t writeTextToSoA(LineSoA& line,
                      size_t startCol,
                      std::string_view text,
                      GraphicsAttributes const& attrs,
                      HyperlinkId hyperlink,
                      bool asciiHint)
{
    if (text.empty())
        return 0;

    // Fast path: caller confirmed ASCII
    if (asciiHint)
        return writeAsciiToSoA(line, startCol, text, attrs, hyperlink);

    // Slow path: mixed/non-ASCII — full UTF-8 decode + grapheme cluster segmentation
    return writeNonAsciiToSoA(line, startCol, text, attrs, hyperlink);
}

} // namespace vtbackend
