/* This is a port of <https://github.com/Finomnis/u8g2-fonts/blob/main/src/font_reader/> to C++ from Rust.
 * It is not complete but implements the most important parts.
 */

#ifndef MONOFORMAT_FONTREADER_HPP
#define MONOFORMAT_FONTREADER_HPP

#include "monoformat_parsehelpers.hpp"
#include "monoformat_gfx.hpp"
#include "monoformat_utf8.hpp"

#include <cassert>
#include <cstdint>
#include <cstddef>
#include <optional>
#include <span>
#include <string_view>
#include <limits>

namespace monoformat {

class GlyphReader;

enum class LookupError {
    GlyphNotFound = 0,
    InvalidUnicodeSequence = 1,
};

enum class RenderError {
    Unknown = -1,
};

struct UnicodeJumptableEntry {
    std::uint16_t jumpDistance{};
    std::uint16_t characterUpperLimit{};
};

class UnicodeJumptableReader {
public:
    explicit UnicodeJumptableReader(std::span<std::byte const> data)
        : m_data{data}
    {
    }

    std::optional<std::size_t> calculateJumpOffset(std::uint16_t encoding) {
        std::size_t jumpOffset = 0;

        std::optional<UnicodeJumptableEntry> entry;
        do {
            entry = nextEntry();
            if (!entry) {
                return std::nullopt;
            }

            jumpOffset += entry->jumpDistance;
        } while (entry->characterUpperLimit < encoding);

        return jumpOffset;
    }

    std::optional<UnicodeJumptableEntry> nextEntry() {
        auto jumpDistance_ = readU16BE(m_data);
        auto characterUpperLimit_ = readU16BE(m_data);
        if (!jumpDistance_ || !characterUpperLimit_) {
            return std::nullopt;
        }
        return UnicodeJumptableEntry{*jumpDistance_, *characterUpperLimit_};
    }

private:
    std::span<std::byte const> m_data;
};

template<std::size_t CharWidth>
class GlyphSearcher;

class FontReader {
public:
    explicit FontReader(std::span<std::byte const> data);

    bool supportsBackgroundColor() const noexcept { return m_supportsBackgroundColor; }
    std::uint8_t glyphCount() const noexcept { return m_glyphCount; }
    std::uint8_t m0() const noexcept { return m_m0; }
    std::uint8_t m1() const noexcept { return m_m1; }
    std::uint8_t bitCountW() const noexcept { return m_bitCountW; }
    std::uint8_t bitCountH() const noexcept { return m_bitCountH; }
    std::uint8_t bitCountX() const noexcept { return m_bitCountX; }
    std::uint8_t bitCountY() const noexcept { return m_bitCountY; }
    std::uint8_t bitCountD() const noexcept { return m_bitCountD; }
    std::uint8_t fontBoundingBoxWidth() const noexcept { return m_fontBoundingBoxWidth; }
    std::int8_t fontBoundingBoxHeight() const noexcept { return m_fontBoundingBoxHeight; }
    std::int8_t fontBoundingBoxXOffset() const noexcept { return m_fontBoundingBoxXOffset; }
    std::int8_t fontBoundingBoxYOffset() const noexcept { return m_fontBoundingBoxYOffset; }
    std::int8_t ascent() const noexcept { return m_ascent; }
    std::int8_t descent() const noexcept { return m_descent; }
    std::int8_t ascentOfParentheses() const noexcept { return m_ascentOfParentheses; }
    std::int8_t descentOfParentheses() const noexcept { return m_descentOfParentheses; }
    std::uint16_t arrayOffsetLowerA() const noexcept { return m_arrayOffsetLowerA; }
    std::uint16_t arrayOffsetUpperA() const noexcept { return m_arrayOffsetUpperA; }
    std::uint16_t arrayOffset0x0100() const noexcept { return m_arrayOffset0x0100; }
    bool doesIgnoreUnknownGlyphs() const noexcept { return m_ignoreUnknownGlyphs; }
    std::uint32_t lineHeight() const noexcept { return m_lineHeight; }

    std::uint8_t getDefaultLineHeight() const;

    FontReader withIgnoreUnknownGlyphs(bool ignore) && {
        m_ignoreUnknownGlyphs = ignore;
        return *this;
    }

    FontReader withLineHeight(std::uint32_t lineHeight) && {
        m_lineHeight = lineHeight;
        return *this;
    }

    std::expected<std::optional<GlyphReader>, LookupError> tryRetrieveGlyphData(std::uint32_t ch) const;
    std::expected<GlyphReader, LookupError> retrieveGlyphData(std::uint32_t ch) const;

private:
    friend class GlyphReader;
    template<std::size_t CharWidth>
    friend class GlyphSearcher;

    std::span<std::byte const> m_data;
    std::uint8_t m_glyphCount{};
    bool m_supportsBackgroundColor{};
    std::uint8_t m_m0{};
    std::uint8_t m_m1{};
    std::uint8_t m_bitCountW{};
    std::uint8_t m_bitCountH{};
    std::uint8_t m_bitCountX{};
    std::uint8_t m_bitCountY{};
    std::uint8_t m_bitCountD{};
    std::int8_t m_fontBoundingBoxWidth{};
    std::int8_t m_fontBoundingBoxHeight{};
    std::int8_t m_fontBoundingBoxXOffset{};
    std::int8_t m_fontBoundingBoxYOffset{};
    std::int8_t m_ascent{};
    std::int8_t m_descent{};
    std::int8_t m_ascentOfParentheses{};
    std::int8_t m_descentOfParentheses{};
    std::uint16_t m_arrayOffsetUpperA{};
    std::uint16_t m_arrayOffsetLowerA{};
    std::uint16_t m_arrayOffset0x0100{};
    bool m_ignoreUnknownGlyphs{};
    std::uint32_t m_lineHeight{};
};

class GlyphReader {
public:
    explicit GlyphReader(std::span<std::byte const> data, FontReader const& font)
        : m_data{data}
        , m_bitCount0{font.m_m0}
        , m_bitCount1{font.m_m1}
    {
        m_glyphWidth = readUnsigned(font.m_bitCountW);
        m_glyphHeight = readUnsigned(font.m_bitCountH);
        m_offsetX = readSigned(font.m_bitCountX);
        m_offsetY = readSigned(font.m_bitCountY);
        m_advance = readSigned(font.m_bitCountD);
    }

    std::uint8_t readUnsigned(std::uint8_t bits) {
        std::uint8_t const bitStart = m_bitPos;
        std::uint8_t bitEnd = bitStart + bits;

        auto value = overflowingShr(m_currentByte, bitStart).first;
        if (bitEnd >= 8) {
            auto value2 = static_cast<std::uint8_t>(m_data[0]);
            m_data = m_data.subspan(1);
            bitEnd -= 8;
            m_currentByte = value2;

            value |= overflowingShl(value2, 8 - bitStart).first;
        }

        m_bitPos = bitEnd;

        return value & static_cast<std::uint8_t>((std::uint16_t{1} << bits) - 1);
    }

    std::int8_t readSigned(std::uint8_t bits) {
        return static_cast<std::int8_t>(readUnsigned(bits) - static_cast<std::uint8_t>(1 << (bits - 1)));
    }

    Point topLeft(Point pos) const {
        return {
            pos.x + m_offsetX,
            pos.y - (m_glyphHeight + m_offsetY),
        };
    }

    std::int32_t left(std::int32_t posX) const {
        return posX + m_offsetX;
    }

    Size size() const {
        return {m_glyphWidth, m_glyphHeight};
    }

    std::uint32_t width() const {
        return m_glyphWidth;
    }

    std::uint32_t advance() const {
        return m_advance;
    }

    std::uint8_t readRunLength0() {
        return readUnsigned(m_bitCount0);
    }

    std::uint8_t readRunLength1() {
        return readUnsigned(m_bitCount1);
    }

private:
    std::span<std::byte const> m_data;
    // Start at 8 to mark the current byte as invalid
    std::uint8_t m_bitPos{8};
    std::uint8_t m_currentByte{0};
    std::uint8_t m_glyphWidth{};
    std::uint8_t m_glyphHeight{};
    std::int8_t m_offsetX{};
    std::int8_t m_offsetY{};
    std::int8_t m_advance{};
    std::uint8_t m_bitCount0{};
    std::uint8_t m_bitCount1{};
};

template<std::size_t CharWidth>
class GlyphSearcherBase {
public:
    void jumpBy(std::size_t offset) {
        m_data = m_data.subspan(offset);
    }

    std::uint8_t getOffset() const {
        return static_cast<std::uint8_t>(m_data[CharWidth]);
    }

    bool jumpToNext() {
        auto offset = getOffset();
        if (offset == 0) {
            return false;
        } else {
            jumpBy(offset);
            return true;
        }
    }

    GlyphReader intoGlyphReader() && {
        return GlyphReader{m_data.subspan(CharWidth + 1), m_font};
    }

protected:
    GlyphSearcherBase(std::span<std::byte const> data, FontReader const& font)
        : m_data{data}
        , m_font{font}
    {
    }

    std::span<std::byte const> m_data;
    FontReader const& m_font;
};

template<>
class GlyphSearcher<2> : public GlyphSearcherBase<2> {
public:
    std::uint16_t getCh() const {
        return peekU16BE(m_data).value();
    }

private:
    explicit GlyphSearcher(std::span<std::byte const> data, FontReader const& font)
        : GlyphSearcherBase<2>{data, font}
    {
    }

    template<std::size_t CharWidth>
    friend class GlyphSearcher;
};

template<>
class GlyphSearcher<1> : public GlyphSearcherBase<1> {
    constexpr static std::size_t const U8G2FontDataStructSize{23};

public:
    explicit GlyphSearcher(FontReader const& font)
        : GlyphSearcherBase<1>{font.m_data.subspan(U8G2FontDataStructSize), font}
    {
    }

    std::uint8_t getCh() const {
        return static_cast<std::uint8_t>(m_data[0]);
    }

    std::pair<GlyphSearcher<2>, UnicodeJumptableReader> intoUnicodeMode(std::uint16_t offset) && {
        jumpBy(offset);
        return {GlyphSearcher<2>{m_data, m_font}, UnicodeJumptableReader{m_data}};
    }
};

inline FontReader::FontReader(std::span<std::byte const> data)
    : m_data{data}
    , m_glyphCount{static_cast<std::uint8_t>(data[0])}
    , m_supportsBackgroundColor{data[1] != std::byte{}}
    , m_m0{static_cast<std::uint8_t>(data[2])}
    , m_m1{static_cast<std::uint8_t>(data[3])}
    , m_bitCountW{static_cast<std::uint8_t>(data[4])}
    , m_bitCountH{static_cast<std::uint8_t>(data[5])}
    , m_bitCountX{static_cast<std::uint8_t>(data[6])}
    , m_bitCountY{static_cast<std::uint8_t>(data[7])}
    , m_bitCountD{static_cast<std::uint8_t>(data[8])}
    , m_fontBoundingBoxWidth{static_cast<std::int8_t>(data[9])}
    , m_fontBoundingBoxHeight{static_cast<std::int8_t>(data[10])}
    , m_fontBoundingBoxXOffset{static_cast<std::int8_t>(data[11])}
    , m_fontBoundingBoxYOffset{static_cast<std::int8_t>(data[12])}
    , m_ascent{static_cast<std::int8_t>(data[13])}
    , m_descent{static_cast<std::int8_t>(data[14])}
    , m_ascentOfParentheses{static_cast<std::int8_t>(data[15])}
    , m_descentOfParentheses{static_cast<std::int8_t>(data[16])}
    , m_arrayOffsetUpperA{*peekU16BE(data.subspan(17, 2))}
    , m_arrayOffsetLowerA{*peekU16BE(data.subspan(19, 2))}
    , m_arrayOffset0x0100{*peekU16BE(data.subspan(21, 2))}
    , m_ignoreUnknownGlyphs{false}
{
    m_lineHeight = getDefaultLineHeight();
}

inline std::uint8_t FontReader::getDefaultLineHeight() const {
    assert(m_fontBoundingBoxHeight >= 0);
    return static_cast<std::uint8_t>(m_fontBoundingBoxHeight) + 1;
}

inline std::expected<std::optional<GlyphReader>, LookupError> FontReader::tryRetrieveGlyphData(std::uint32_t ch) const {
    auto ret = retrieveGlyphData(ch);
    if (!ret) {
        if (m_ignoreUnknownGlyphs && ret.error() == LookupError::GlyphNotFound) {
            return std::nullopt;
        }
        return std::unexpected(ret.error());
    }
    return *ret;
}

inline std::expected<GlyphReader, LookupError> FontReader::retrieveGlyphData(std::uint32_t ch) const {
    if (ch > std::numeric_limits<std::uint16_t>::max()) {
        return std::unexpected(LookupError::GlyphNotFound);
    }

    auto glyph = GlyphSearcher<1>(*this);

    if (ch <= 255) {
        if (ch >= 'a') {
            glyph.jumpBy(m_arrayOffsetLowerA);
        } else if (ch >= 'A') {
            glyph.jumpBy(m_arrayOffsetUpperA);
        }

        while (glyph.getCh() != ch) {
            bool ok = glyph.jumpToNext();
            if (!ok) {
                return std::unexpected(LookupError::GlyphNotFound);
            }
        }

        return std::move(glyph).intoGlyphReader();
    } else {
        auto [gl, unicodeJumpTable] = std::move(glyph).intoUnicodeMode(m_arrayOffset0x0100);

        auto jumpOffset_ = unicodeJumpTable.calculateJumpOffset(ch);
        if (!jumpOffset_) {
            return std::unexpected(LookupError::GlyphNotFound);
        }

        gl.jumpBy(*jumpOffset_);

        while (true) {
            auto glCh = gl.getCh();
            if (glCh == 0) {
                return std::unexpected(LookupError::GlyphNotFound);
            }
            if (glCh == ch) {
                break;
            }
            if (!gl.jumpToNext()) {
                return std::unexpected(LookupError::GlyphNotFound);
            }
        }

        return std::move(gl).intoGlyphReader();
    }
}

class GlyphRenderer {
public:
    explicit GlyphRenderer(GlyphReader const& glyph)
        : m_glyph{glyph}
    {
    }

    Rectangle getBoundingBox(Point position) const {
        return Rectangle{m_glyph.topLeft(position), m_glyph.size()};
    }

    template<typename DrawTarget>
    std::expected<Rectangle, RenderError> renderAsBoxFill(Point position, DrawTarget& display, typename DrawTarget::Color fgColor, typename DrawTarget::Color bgColor) {
        auto boundingBox = getBoundingBox(position);
        int numZeros = m_glyph.readRunLength0();
        int numOnes = m_glyph.readRunLength1();
        int numZerosLeftOver = numZeros;
        int numOnesLeftOver = numOnes;
        auto colorIter = [&] () -> std::optional<typename DrawTarget::Color> {
            if (numZerosLeftOver == 0 && numOnesLeftOver == 0) {
                bool repeat = m_glyph.readUnsigned(1) != 0;
                if (!repeat) {
                    numZeros = m_glyph.readRunLength0();
                    numOnes = m_glyph.readRunLength1();
                }
                numZerosLeftOver = numZeros;
                numOnesLeftOver = numOnes;
            }

            if (numZerosLeftOver > 0) {
                --numZerosLeftOver;
                return bgColor;
            } else {
                --numOnesLeftOver;
                return fgColor;
            }
        };

        for (std::uint32_t dy = 0; dy < boundingBox.size.height; ++dy) {
            std::int32_t y = dy + boundingBox.topLeft.y;
            for (std::uint32_t dx = 0; dx < boundingBox.size.width; ++dx) {
                std::int32_t x = dx + boundingBox.topLeft.x;
                auto color = colorIter().value_or(bgColor);
                display.setPixel(x, y, color);
            }
        }

        return boundingBox;
    }

    template<typename DrawTarget>
    std::expected<Rectangle, RenderError> renderTransparent(Point position, DrawTarget& display, typename DrawTarget::Color fgColor) {
        auto boundingBox = getBoundingBox(position);
        auto colorIter = [&] () {
            auto numZeros = m_glyph.readRunLength0();
            auto numOnes = m_glyph.readRunLength1();
            auto numZerosLeftOver = numZeros;
            auto numOnesLeftOver = numOnes;
            return [&] () -> std::optional<typename DrawTarget::Color> {
                if (numZerosLeftOver == 0 || numOnesLeftOver == 0) {
                    bool repeat = m_glyph.readUnsigned(1) != 0;
                    if (!repeat) {
                        numZeros = m_glyph.readRunLength0();
                        numOnes = m_glyph.readRunLength1();
                    }
                    numZerosLeftOver = numZeros;
                    numOnesLeftOver = numOnes;
                }

                if (numZerosLeftOver > 0) {
                    --numZerosLeftOver;
                    return std::nullopt;
                } else {
                    --numOnesLeftOver;
                    return fgColor;
                }
            };
        }();

        for (std::uint32_t dy = 0; dy < boundingBox.size.height; ++dy) {
            std::int32_t y = dy + boundingBox.topLeft.y;
            for (std::uint32_t dx = 0; dx < boundingBox.size.width; ++dx) {
                std::int32_t x = dx + boundingBox.topLeft.x;
                if (auto color = colorIter()) {
                    display.setPixel(x, y, *color);
                }
            }
        }

        return boundingBox;
    }

private:
    GlyphReader m_glyph;
};

struct RenderedDimensions {
    Point advance;
    std::optional<Rectangle> boundingBox;
};

class FontRenderer {
public:
    explicit FontRenderer(std::span<std::byte const> fontData);

    FontRenderer withIgnoreUnknownChars(bool ignore) && {
        FontRenderer result{std::move(m_font).withIgnoreUnknownGlyphs(ignore)};
        return result;
    }

    FontRenderer withLineHeight(std::uint32_t lineHeight) && {
        FontRenderer result{std::move(m_font).withLineHeight(lineHeight)};
        return result;
    }

    std::uint32_t lineHeight() const noexcept {
        return m_font.lineHeight();
    }

    template<typename Display>
    std::expected<RenderedDimensions, LookupError> render(std::string_view content, Point position, Display& display, typename Display::Color fontColor, std::optional<typename Display::Color> bgColor) const {
        Point advance{0, 0};
        std::optional<Rectangle> boundingBox;

        position.y += 0; // Top alignment

        for (std::uint32_t cp : UTF8Iterator{content}) {
            if (cp == ~std::uint32_t{0}) {
                if (!m_font.doesIgnoreUnknownGlyphs()) {
                    return std::unexpected(LookupError::InvalidUnicodeSequence);
                }
                continue;
            }

            if (cp == '\n') {
                advance.x = 0;
                advance.y += m_font.lineHeight();
                continue;
            }

            auto dims = renderGlyph(cp, position + advance, m_font, display, fontColor, bgColor);
            if (!dims) {
                return std::unexpected(dims.error());
            }
            advance += dims->advance;
            boundingBox = combineBoundingBoxes(boundingBox, dims->boundingBox);
        }

        return RenderedDimensions{advance, boundingBox};
    }

    std::expected<RenderedDimensions, LookupError> getRenderedDimensions(std::string_view content, Point position) const {
        Point advance{0, 0};
        std::optional<Rectangle> boundingBox;

        position.y += 0; // Top alignment

        for (std::uint32_t cp : UTF8Iterator{content}) {
            if (cp == ~std::uint32_t{0}) {
                if (!m_font.doesIgnoreUnknownGlyphs()) {
                    return std::unexpected(LookupError::InvalidUnicodeSequence);
                }
                continue;
            }

            if (cp == '\n') {
                ensureAdvance(boundingBox, advance);
                advance.x = 0;
                advance.y += m_font.lineHeight();
                continue;
            }

            auto dims = computeGlyphDimensions(cp, position + advance, m_font);
            if (!dims) {
                return std::unexpected(dims.error());
            }
            advance += dims->advance;
            boundingBox = combineBoundingBoxes(boundingBox, dims->boundingBox);
        }

        boundingBox = ensureAdvance(boundingBox, advance);

        return RenderedDimensions{advance, boundingBox};
    }

private:
    explicit FontRenderer(FontReader&& font)
        : m_font{std::move(font)}
    {
    }

    static std::expected<RenderedDimensions, LookupError> computeGlyphDimensions(std::uint32_t cp, Point position, FontReader const& font) {
        auto glyph_ = font.tryRetrieveGlyphData(cp);
        if (!glyph_) {
            return std::unexpected(glyph_.error());
        }
        if (!*glyph_) {
            return RenderedDimensions{};
        }
        auto glyph = **glyph_;
        auto advance = glyph.advance();
        auto size = glyph.size();

        std::optional<Rectangle> boundingBox;
        if (size.width > 0 && size.height > 0) {
            auto renderer = GlyphRenderer{glyph};
            boundingBox = renderer.getBoundingBox(position);
        }

        return RenderedDimensions{Point{static_cast<std::int32_t>(advance), 0}, boundingBox};
    }

    static std::optional<Rectangle> combineBoundingBoxes(std::optional<Rectangle> a, std::optional<Rectangle> b) {
        if (!a && !b) {
            return std::nullopt;
        } else if (a && !b) {
            return a;
        } else if (b && !a) {
            return b;
        } else {
            auto topLeftA = a->topLeft;
            auto topLeftB = b->topLeft;
            auto bottomRightA = a->getBottomRight();
            auto bottomRightB = b->getBottomRight();

            Point topLeft{std::min(topLeftA.x, topLeftB.x), std::min(topLeftA.y, topLeftB.y)};
            Point bottomRight{std::max(bottomRightA.x, bottomRightB.x), std::max(bottomRightA.y, bottomRightB.y)};
            return Rectangle::fromCorners(topLeft, bottomRight);
        }
    }

    static std::optional<Rectangle> ensureAdvance(std::optional<Rectangle> a, Point advance) {
        if (!a) {
            return std::nullopt;
        }

        auto bottomRight = a->getBottomRight();
        if ((bottomRight.x + 1) < advance.x) {
            bottomRight.x = advance.x - 1;
        }
        if ((bottomRight.y + 1) < advance.y) {
            bottomRight.y = advance.y - 1;
        }
        return Rectangle::fromCorners(a->topLeft, bottomRight);
    }

    template<typename Display>
    static std::expected<RenderedDimensions, LookupError> renderGlyph(std::uint32_t cp, Point position, FontReader const& font, Display& display, typename Display::Color fontColor, std::optional<typename Display::Color> bgColor) {
        auto glyph_ = font.tryRetrieveGlyphData(cp);
        if (!glyph_) {
            return std::unexpected(glyph_.error());
        }
        if (!*glyph_) {
            return RenderedDimensions{};
        }
        auto glyph = **glyph_;
        auto advance = glyph.advance();
        auto size = glyph.size();

        std::optional<Rectangle> boundingBox;
        if (size.width > 0 && size.height > 0) {
            auto renderer = GlyphRenderer{glyph};
            auto bbox = bgColor
                ? renderer.renderAsBoxFill(position, display, fontColor, *bgColor)
                : renderer.renderTransparent(position, display, fontColor);
            boundingBox = renderer.getBoundingBox(position);
        }

        return RenderedDimensions{Point{static_cast<std::int32_t>(advance), 0}, boundingBox};
    }

    FontReader m_font;
};

inline FontRenderer::FontRenderer(std::span<std::byte const> fontData)
    : m_font{fontData}
{
}

extern std::span<std::byte const> findEmbeddedFont(std::size_t fontIndex);

} // namespace monoformat

#endif // MONOFORMAT_FONTREADER_HPP
