/* Copyright 2022 the SumatraPDF project authors (see AUTHORS file).
   License: Simplified BSD (see COPYING.BSD) */

#include "utils/BaseUtil.h"
#include "utils/WinUtil.h"
#include "utils/GdiPlusUtil.h"
#include "Mui.h"

/*
TODO:
 - text drawing is still too slow. each html page takes ~20ms to draw, which is
   terrible and much slower than what I think the test render was doing (~1ms)
   Is it beacuase it draws to gfx->GetHDC() instead of e.g. natural or bitmap
   HDC? In which case maybe I should render text to bitmap hdc and then
   blit that once to Graphics?
 - add transparent rendering to GDI, see:
   http://stackoverflow.com/questions/1340166/transparency-to-text-in-gdi
   http://theartofdev.wordpress.com/2013/10/24/transparent-text-rendering-with-gdi/
 - figure out a way to get rid of Lock()/Unlock(). One way is to turn ITextRender into a full-blown
   IGraphics abstraction (add drawing calls to it) and then GDI+-based implementation could
   track locking state internally, so that the caller doesn't have to.
   Another options would be to figure out a way to draw to a bitmap and blit that bitmap
   to Graphics object.
*/

/* Note: I would prefer this code be in utils but it depends on mui, so it must
be in mui to avoid circular dependency */

using Gdiplus::Bitmap;
using Gdiplus::Color;
using Gdiplus::Graphics;
using Gdiplus::Ok;
using Gdiplus::Region;
using Gdiplus::SolidBrush;
using Gdiplus::Status;
using Gdiplus::StringFormat;
using Gdiplus::StringFormatFlagsDirectionRightToLeft;

namespace mui {

// Match HtmlFormatter::IsCjkChar — used to split mixed runs for rendering.
static bool IsCjkDrawChar(WCHAR c) {
    return (c >= 0x2E80 && c <= 0xA4CF) || (c >= 0xAC00 && c <= 0xD7AF) || (c >= 0xF900 && c <= 0xFAFF);
}

TextRenderGdi* TextRenderGdi::Create(Graphics* gfx) {
    TextRenderGdi* res = new TextRenderGdi();
    res->gfx = gfx;
    res->method = TextRenderMethod::Gdi;
    // default to red to make mistakes stand out
    res->SetTextColor(Color(0xff, 0xff, 0x0, 0x0));
    res->CreateHdcForTextMeasure(); // could do lazily, but that's more things to track, so not
                                    // worth it
    return res;
}

void TextRenderGdi::CreateHdcForTextMeasure() {
    HDC hdc = hdcGfxLocked;
    bool unlock = false;
    if (!hdc) {
        hdc = gfx->GetHDC();
        unlock = true;
    }
    hdcForTextMeasure = CreateCompatibleDC(hdc);
    if (unlock) {
        gfx->ReleaseHDC(hdc);
    }
}

TextRenderGdi::~TextRenderGdi() {
    RestoreHdcForTextMeasurePrevFont();
    RestoreMemHdcPrevFont();
    RestoreMemHdcPrevBitmap();
    FreeMemBmp();
    DeleteDC(memHdc);
    DeleteDC(hdcForTextMeasure);
    ReportIf(hdcGfxLocked); // hasn't been Unlock()ed
}

void TextRenderGdi::RestoreMemHdcPrevBitmap() {
    if (memHdcPrevBitmap != nullptr) {
        SelectObject(memHdc, memHdcPrevBitmap);
        memHdcPrevBitmap = nullptr;
    }
}

void TextRenderGdi::RestoreMemHdcPrevFont() {
    if (memHdcPrevFont != nullptr) {
        SelectObject(memHdc, memHdcPrevFont);
        memHdcPrevFont = nullptr;
    }
}

void TextRenderGdi::RestoreHdcForTextMeasurePrevFont() {
    if (hdcForTextMeasurePrevFont != nullptr) {
        SelectObject(hdcForTextMeasure, hdcForTextMeasurePrevFont);
        hdcForTextMeasurePrevFont = nullptr;
    }
}

void TextRenderGdi::SetFont(mui::CachedFont* font) {
    // I'm not sure how expensive SelectFont() is so avoid it just in case
    if (currFont == font) {
        return;
    }
    currFont = font;
    HFONT hfont = font->GetHFont();
    if (hdcGfxLocked) {
        SelectFont(hdcGfxLocked, hfont);
    }
    if (hdcForTextMeasure) {
        RestoreHdcForTextMeasurePrevFont();
        hdcForTextMeasurePrevFont = SelectFont(hdcForTextMeasure, hfont);
    }
}

float TextRenderGdi::GetCurrFontLineSpacing() {
#if 1
    return currFont->font->GetHeight(gfx);
#else
    ReportIf(!currFont);
    TEXTMETRIC tm;
    GetTextMetrics(hdcForTextMeasure, &tm);
    return (float)tm.tmHeight;
#endif
}

RectF TextRenderGdi::Measure(const WCHAR* s, size_t sLen) {
    SIZE txtSize;
    GetTextExtentPoint32W(hdcForTextMeasure, s, (int)sLen, &txtSize);
    RectF res(0.0f, 0.0f, (float)txtSize.cx, (float)txtSize.cy);
    return res;
}

// Cumulative charRelX must use the same per-char integer advances as ExtTextOut(lpDx).
// Rounding float metrics per char avoids drift that grows toward the end of long lines.
static void CharRelXRoundForGdi(float* rel, int wlen) {
    if (wlen <= 0) {
        return;
    }
    Vec<float> raw;
    raw.AppendBlanks(wlen + 1);
    for (int k = 0; k <= wlen; k++) {
        raw[k] = rel[k];
    }
    int total = (int)(raw[wlen] + 0.5f);
    int sum = 0;
    rel[0] = 0.f;
    for (int k = 0; k < wlen - 1; k++) {
        int adv = (int)(raw[k + 1] - raw[k] + 0.5f);
        if (adv < 1) {
            adv = 1;
        }
        sum += adv;
        rel[k + 1] = (float)sum;
    }
    rel[wlen] = (float)total;
    if (rel[wlen] <= rel[wlen - 1]) {
        rel[wlen] = rel[wlen - 1] + 1.f;
    }
}

bool TextRenderGdi::MeasureCharRelX(const WCHAR* s, int wlen, float* rel) {
    if (!hdcForTextMeasure || !s || wlen <= 0 || !rel) {
        return false;
    }
    rel[0] = 0.f;
    Vec<int> advances;
    advances.AppendBlanks(wlen);
    SIZE sz{};
    if (!GetTextExtentExPointW(hdcForTextMeasure, s, wlen, 0, nullptr, advances.els, &sz)) {
        return false;
    }
    for (int k = 0; k < wlen; k++) {
        rel[k + 1] = rel[k] + (float)advances.at(k);
    }
    if (rel[wlen] <= 0.f) {
        return false;
    }
    CharRelXRoundForGdi(rel, wlen);
    for (int k = 1; k <= wlen; k++) {
        if (rel[k] <= rel[k - 1]) {
            return false;
        }
    }
    return true;
}

void TextRenderGdi::DrawWithCharRelX(const WCHAR* s, int sLen, RectF bb, const float* rel, bool isRtl) {
    ReportIf(!hdcGfxLocked);
    if (!s || sLen <= 0 || !rel) {
        Draw(s, (size_t)sLen, bb, isRtl);
        return;
    }
    int y = (int)bb.y;
    uint opts = ETO_OPAQUE;
    if (isRtl) {
        opts = opts | ETO_RTLREADING;
        // Fallback: per-char with explicit advances (RTL mixed runs are rare).
        for (int k = 0; k < sLen; k++) {
            int adv = (int)(rel[k + 1] - rel[k]);
            if (adv < 1) {
                adv = 1;
            }
            int x = (int)(bb.x + rel[k]);
            ExtTextOutW(hdcGfxLocked, x, y, opts, nullptr, s + k, 1, &adv);
        }
        return;
    }
    int k = 0;
    while (k < sLen) {
        int adv = (int)(rel[k + 1] - rel[k]);
        if (adv < 1) {
            adv = 1;
        }
        int x = (int)(bb.x + rel[k]);
        if (IsCjkDrawChar(s[k])) {
            ExtTextOutW(hdcGfxLocked, x, y, opts, nullptr, s + k, 1, &adv);
            k++;
        } else {
            int start = k;
            while (k < sLen && !IsCjkDrawChar(s[k])) {
                k++;
            }
            int n = k - start;
            int* dxArr = AllocArray<int>(GetTempAllocator(), n);
            for (int i = 0; i < n; i++) {
                int a = (int)(rel[start + i + 1] - rel[start + i]);
                dxArr[i] = a < 1 ? 1 : a;
            }
            x = (int)(bb.x + rel[start]);
            ExtTextOutW(hdcGfxLocked, x, y, opts, nullptr, s + start, n, dxArr);
        }
    }
}

RectF TextRenderGdi::Measure(const char* s, size_t sLen) {
    WCHAR* buf = ToWStrTemp(s, sLen);
    size_t bufLen = str::Len(buf);
    return Measure(buf, bufLen);
}

void TextRenderGdi::SetTextColor(Gdiplus::Color col) {
    if (textColor.GetValue() == col.GetValue()) {
        return;
    }
    textColor = col;
    if (hdcGfxLocked) {
        ::SetTextColor(hdcGfxLocked, col.ToCOLORREF());
    }
}

void TextRenderGdi::SetTextBgColor(Gdiplus::Color col) {
    if (textBgColor.GetValue() == col.GetValue()) {
        return;
    }
    textBgColor = col;
    if (hdcGfxLocked) {
        ::SetBkColor(hdcGfxLocked, textBgColor.ToCOLORREF());
    }
}

void TextRenderGdi::Lock() {
    ReportIf(hdcGfxLocked);
    Region r;
    Status st = gfx->GetClip(&r); // must call before GetHDC(), which locks gfx
    ReportIf(st != Ok);
    HRGN hrgn = r.GetHRGN(gfx);

    hdcGfxLocked = gfx->GetHDC();
    SelectClipRgn(hdcGfxLocked, hrgn);
    DeleteObject(hrgn);

    SelectFont(hdcGfxLocked, currFont);
    ::SetTextColor(hdcGfxLocked, textColor.ToCOLORREF());
    ::SetBkColor(hdcGfxLocked, textBgColor.ToCOLORREF());
}

void TextRenderGdi::Unlock() {
    ReportIf(!hdcGfxLocked);
    gfx->ReleaseHDC(hdcGfxLocked);
    hdcGfxLocked = nullptr;
}

void TextRenderGdi::Draw(const WCHAR* s, size_t sLen, const RectF bb, bool isRtl) {
#if 0
    DrawTransparent(s, sLen, bb, isRtl);
#else
    ReportIf(!hdcGfxLocked); // hasn't been Lock()ed
    int x = (int)bb.x;
    int y = (int)bb.y;
    uint opts = ETO_OPAQUE;
    if (isRtl) {
        opts = opts | ETO_RTLREADING;
    }
    ExtTextOut(hdcGfxLocked, x, y, opts, nullptr, s, (uint)sLen, nullptr);
#endif
}

void TextRenderGdi::Draw(const char* s, size_t sLen, const RectF bb, bool isRtl) {
#if 0
    DrawTransparent(s, sLen, bb, isRtl);
#else
    WCHAR* buf = ToWStrTemp(s, sLen);
    size_t bufLen = str::Len(buf);
    return Draw(buf, bufLen, bb, isRtl);
#endif
}

void TextRenderGdi::FreeMemBmp() {
    DeleteObject(memBmp);
}

void TextRenderGdi::CreateClearBmpOfSize(int dx, int dy) {
    // set minimums for less allocations
    if (dx < 128) {
        dx = 128;
    }
    if (dy < 48) {
        dy = 48;
    }

    if (dx <= memBmpDx && dy <= memBmpDy) {
        ZeroMemory(memBmpData, (size_t)memBmpDx * (size_t)memBmpDy * 4);
        return;
    }
    if (!memHdc) {
        memHdc = CreateCompatibleDC(hdcGfxLocked);
    }

    FreeMemBmp();

    BITMAPINFO bmi{};
    bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader);
    bmi.bmiHeader.biWidth = dx;
    bmi.bmiHeader.biHeight = dy;
    bmi.bmiHeader.biPlanes = 1;
    bmi.bmiHeader.biBitCount = 32;
    bmi.bmiHeader.biCompression = BI_RGB;
    bmi.bmiHeader.biSizeImage = dx * dy * 4; // doesn't seem necessary?

    memBmp = CreateDIBSection(memHdc, &bmi, DIB_RGB_COLORS, &memBmpData, nullptr, 0);
    if (!memBmp) {
        return;
    }

    ZeroMemory(memBmpData, (size_t)memBmpDx * (size_t)memBmpDy * 4);

    RestoreMemHdcPrevBitmap();
    memHdcPrevBitmap = SelectObject(memHdc, memBmp);
}

// based on http://theartofdev.wordpress.com/2013/10/24/transparent-text-rendering-with-gdi/,
// TODO: look into using http://theartofdev.wordpress.com/2014/01/12/gdi-text-rendering-to-image/
// TODO: doesn't actually look good (i.e. similar to DrawText when using transparent SetBkMode())
// which kind of makes sense, because I'm using transparent mode to draw to in-memory bitmap as well
// TODO: doesn't actually do alpha bf.SourceConstantAlpha > 4 looks the same, values 1-4 produce
// different, but not expected, results
// TODO: I would like to figure out a way to draw text without the need to Lock()/Unlock()
// maybe draw to in-memory bitmap, convert to Graphics bitmap and blit that bitmap to
// Graphics object
void TextRenderGdi::DrawTransparent(const WCHAR* s, size_t sLen, const RectF bb, bool isRtl) {
    ReportIf(!hdcGfxLocked); // hasn't been Lock()ed

    int x = (int)bb.x;
    int y = (int)bb.y;
    int dx = (int)bb.dx;
    int dy = (int)bb.dy;

    CreateClearBmpOfSize(dx, dy);
    // SetBkMode(hdcGfxLocked, 1);
    SetBkMode(memHdc, TRANSPARENT);

    // BitBlt(memHdc, 0, 0, dx, dy, hdcGfxLocked, x, y, SRCCOPY);
    RestoreMemHdcPrevFont();
    memHdcPrevFont = SelectObject(memHdc, currFont);
    ::SetTextColor(memHdc, textColor.ToCOLORREF());

#if 0
    TextOut(memHdc, 0, 0, s, sLen);
#else
    uint opts = 0; // ETO_OPAQUE;
    if (isRtl) {
        opts = ETO_RTLREADING;
    }
    ExtTextOut(memHdc, 0, 0, opts, nullptr, s, (uint)sLen, nullptr);
#endif

    BLENDFUNCTION bf{};
    bf.BlendOp = AC_SRC_OVER;
    bf.BlendFlags = 0;
    bf.AlphaFormat = 0;           // 0 - ignore source alpha, AC_SRC_ALPHA (1) - use source alpha
    bf.SourceConstantAlpha = 0x3; // textColor.GetA();
    AlphaBlend(hdcGfxLocked, x, y, dx, dy, memHdc, 0, 0, dx, dy, bf);
}

void TextRenderGdi::DrawTransparent(const char* s, size_t sLen, const RectF bb, bool isRtl) {
    WCHAR* buf = ToWStrTemp(s, sLen);
    size_t bufLen = str::Len(buf);
    return DrawTransparent(buf, bufLen, bb, isRtl);
}

TextRenderGdiplus* TextRenderGdiplus::Create(Graphics* gfx, TextMeasureAlgorithm measureAlgo) {
    TextRenderGdiplus* res = new TextRenderGdiplus();
    res->gfx = gfx;
    res->currFont = nullptr;
    if (nullptr == measureAlgo) {
        res->measureAlgo = MeasureTextAccurate;
        res->method = TextRenderMethod::Gdiplus;
    } else {
        res->measureAlgo = measureAlgo;
        res->method = TextRenderMethod::GdiplusQuick;
    }
    // default to red to make mistakes stand out
    res->SetTextColor(Color(0xff, 0xff, 0x0, 0x0));
    return res;
}

void TextRenderGdiplus::SetFont(mui::CachedFont* font) {
    ReportIf(!font->font);
    currFont = font;
}

float TextRenderGdiplus::GetCurrFontLineSpacing() {
    return currFont->font->GetHeight(gfx);
}

RectF TextRenderGdiplus::Measure(const WCHAR* s, size_t sLen) {
    ReportIf(!currFont);
    return MeasureText(gfx, currFont->font, s, sLen, measureAlgo);
}

RectF TextRenderGdiplus::Measure(const char* s, size_t sLen) {
    ReportIf(!currFont);
    WCHAR* buf = ToWStrTemp(s, sLen);
    size_t strLen = str::Len(buf);
    return MeasureText(gfx, currFont->font, buf, strLen, measureAlgo);
}

TextRenderGdiplus::~TextRenderGdiplus() {
    delete textColorBrush;
}

void TextRenderGdiplus::SetTextColor(Gdiplus::Color col) {
    if (textColor.GetValue() == col.GetValue()) {
        return;
    }
    textColor = col;
    delete textColorBrush;
    textColorBrush = new SolidBrush(col);
}

void TextRenderGdiplus::Draw(const WCHAR* s, size_t sLen, const RectF bb, bool isRtl) {
    Gdiplus::PointF pos = ToGdipPointF(bb.TL());
    if (!isRtl) {
        gfx->DrawString(s, (INT)sLen, currFont->font, pos, nullptr, textColorBrush);
    } else {
        StringFormat rtl;
        rtl.SetFormatFlags(StringFormatFlagsDirectionRightToLeft);
        pos.X += bb.dx;
        gfx->DrawString(s, (INT)sLen, currFont->font, pos, &rtl, textColorBrush);
    }
}

void TextRenderGdiplus::Draw(const char* s, size_t sLen, const RectF bb, bool isRtl) {
    WCHAR* buf = ToWStrTemp(s, sLen);
    size_t strLen = str::Len(buf);
    Draw(buf, strLen, bb, isRtl);
}

void TextRenderGdiplus::DrawWithCharRelX(const WCHAR* s, int sLen, RectF bb, const float* rel, int relLen, bool isRtl) {
    if (!s || sLen <= 0 || !rel || relLen != sLen || !currFont) {
        Draw(s, (size_t)sLen, bb, isRtl);
        return;
    }
    if (isRtl) {
        StringFormat sf(StringFormat::GenericTypographic());
        sf.SetFormatFlags(sf.GetFormatFlags() | StringFormatFlagsDirectionRightToLeft);
        for (int k = 0; k < sLen; k++) {
            float x = bb.x + bb.dx - rel[k + 1];
            Gdiplus::PointF pos(x, bb.y);
            gfx->DrawString(s + k, 1, currFont->font, pos, &sf, textColorBrush);
        }
        return;
    }
    StringFormat sf(StringFormat::GenericTypographic());
    int k = 0;
    while (k < sLen) {
        if (IsCjkDrawChar(s[k])) {
            float x = bb.x + rel[k];
            Gdiplus::PointF pos(x, bb.y);
            gfx->DrawString(s + k, 1, currFont->font, pos, &sf, textColorBrush);
            k++;
        } else {
            int start = k;
            while (k < sLen && !IsCjkDrawChar(s[k])) {
                k++;
            }
            float x = bb.x + rel[start];
            Gdiplus::PointF pos(x, bb.y);
            gfx->DrawString(s + start, k - start, currFont->font, pos, nullptr, textColorBrush);
        }
    }
}

void TextRenderHdc::Lock() {
    int dx = bmi.bmiHeader.biWidth;
    int dy = bmi.bmiHeader.biHeight;
    ZeroMemory(bmpData, dx * dy * 4);
}

void TextRenderHdc::Unlock() {
    Bitmap* b = Bitmap::FromBITMAPINFO(&bmi, bmpData);
    gfx->DrawImage(b, 0, 0);
    delete b;
}

TextRenderHdc* TextRenderHdc::Create(Graphics* gfx, int dx, int dy) {
    TextRenderHdc* res = new TextRenderHdc();
    res->gfx = gfx;

    HDC hdc = gfx->GetHDC();
    res->hdc = CreateCompatibleDC(hdc);
    gfx->ReleaseHDC(hdc);

    res->bmi.bmiHeader.biSize = sizeof(res->bmi.bmiHeader);
    res->bmi.bmiHeader.biWidth = dx;
    res->bmi.bmiHeader.biHeight = dy;
    res->bmi.bmiHeader.biPlanes = 1;
    res->bmi.bmiHeader.biBitCount = 32;
    res->bmi.bmiHeader.biCompression = BI_RGB;
    res->bmi.bmiHeader.biSizeImage = dx * dy * 4; // doesn't seem necessary?

    res->bmp = CreateDIBSection(res->hdc, &res->bmi, DIB_RGB_COLORS, &res->bmpData, nullptr, 0);
    if (!res->bmp) {
        delete res;
        return nullptr;
    }

    if (res->bmpData) {
        size_t n = (size_t)dx * (size_t)dy * 4;
        ZeroMemory(res->bmpData, n);
    }
    SelectObject(res->hdc, res->bmp);

    // default to red to make mistakes stand out
    res->method = TextRenderMethod::Hdc;
    res->SetTextColor(Color(0xff, 0xff, 0x0, 0x0));
    return res;
}

void TextRenderHdc::SetFont(CachedFont* font) {
    ReportIf(!hdc);
    // I'm not sure how expensive SelectFont() is so avoid it just in case
    if (currFont == font) {
        return;
    }
    currFont = font;
    SelectFont(hdc, font->GetHFont());
}

void TextRenderHdc::SetTextColor(Gdiplus::Color col) {
    ReportIf(!hdc);
    if (textColor.GetValue() == col.GetValue()) {
        return;
    }
    textColor = col;
    ::SetTextColor(hdc, col.ToCOLORREF());
}

void TextRenderHdc::SetTextBgColor(Gdiplus::Color col) {
    ReportIf(!hdc);
    if (textBgColor.GetValue() == col.GetValue()) {
        return;
    }
    textBgColor = col;
    ::SetBkColor(hdc, textBgColor.ToCOLORREF());
}

float TextRenderHdc::GetCurrFontLineSpacing() {
    return currFont->font->GetHeight(gfx);
}

RectF TextRenderHdc::Measure(const char* s, size_t sLen) {
    ReportIf(!currFont);
    ReportIf(!hdc);
    WCHAR* buf = ToWStrTemp(s, sLen);
    size_t strLen = str::Len(buf);
    return Measure(buf, strLen);
}

RectF TextRenderHdc::Measure(const WCHAR* s, size_t sLen) {
    SIZE txtSize;
    ReportIf(!hdc);
    GetTextExtentPoint32W(hdc, s, (int)sLen, &txtSize);
    RectF res(0.0f, 0.0f, (float)txtSize.cx, (float)txtSize.cy);
    return res;
}

void TextRenderHdc::Draw(const char* s, size_t sLen, const RectF bb, bool isRtl) {
    WCHAR* buf = ToWStrTemp(s, sLen);
    size_t strLen = str::Len(buf);
    return Draw(buf, strLen, bb, isRtl);
}

void TextRenderHdc::Draw(const WCHAR* s, size_t sLen, const RectF bb, bool /* isRtl */) {
    ReportIf(!hdc);
    int x = (int)bb.x;
    int y = (int)bb.y;
    uint opts = ETO_OPAQUE;
#if 0
    if (isRtl) {
        opts = opts | ETO_RTLREADING;
    }
#endif
    ExtTextOutW(hdc, x, y, opts, nullptr, s, (uint)sLen, nullptr);
}

TextRenderHdc::~TextRenderHdc() {
    DeleteObject(bmp);
    // free(bmpData);
    DeleteDC(hdc);
}

ITextRender* CreateTextRender(TextRenderMethod method, Graphics* gfx, int dx, int dy) {
    ITextRender* res = nullptr;
    if (TextRenderMethod::Gdiplus == method) {
        res = TextRenderGdiplus::Create(gfx);
    }
    if (TextRenderMethod::GdiplusQuick == method) {
        res = TextRenderGdiplus::Create(gfx, MeasureTextQuick);
    }
    if (TextRenderMethod::Gdi == method) {
        res = TextRenderGdi::Create(gfx);
    }
    if (TextRenderMethod::Hdc == method) {
        res = TextRenderHdc::Create(gfx, dx, dy);
    }
    ReportIf(!res);
    if (res) {
        res->method = method;
    }
    return res;
}

// returns number of characters of string s that fits in a given width dx
// note: could be speed up a bit because in our use case we already know
// the width of the whole string so we could supply it to the function, but
// this shouldn't happen often, so that's fine. It's also possible that
// a smarter approach is possible, but this usually only does 3 MeasureText
// calls, so it's not that bad
size_t StringLenForWidth(ITextRender* textMeasure, const WCHAR* s, size_t len, float dx) {
    if (len == 0 || dx <= 0) {
        return 0;
    }
    RectF r = textMeasure->Measure(s, len);
    if (r.dx <= dx) {
        return len;
    }
    if (r.dx <= 0) {
        return 0;
    }
    // make the best guess of the length that fits
    size_t n = (size_t)((dx / r.dx) * (float)len);
    if (n == 0) {
        n = 1;
    } else if (n > len) {
        n = len;
    }
    r = textMeasure->Measure(s, n);
    // find the length len of s that fits within dx iff width of len+1 exceeds dx
    int dir = 1; // increasing length
    if (r.dx > dx) {
        dir = -1; // decreasing length
    }
    while (n > 1 && n <= len) {
        n += dir;
        if (n == 0 || n > len) {
            break;
        }
        r = textMeasure->Measure(s, n);
        if (1 == dir) {
            // if advancing length, we know that previous string did fit, so if
            // the new one doesn't fit, the previous length was the right one
            if (r.dx > dx) {
                return n - 1;
            }
        } else {
            // if decreasing length, we know that previous string didn't fit, so if
            // the one one fits, it's of the correct length
            if (r.dx < dx) {
                return n;
            }
        }
    }
    // even a single char is longer than available space
    return 0;
}

// TODO: not quite sure why spaceDx1 != spaceDx2, using spaceDx2 because
// is smaller and looks as better spacing to me
float GetSpaceDx(ITextRender* textMeasure) {
    RectF bbox;
#if 0
    bbox = textMeasure->Measure(L" ", 1, algo);
    float spaceDx1 = bbox.Width;
    return spaceDx1;
#else
    // this method seems to return (much) smaller size that measuring
    // the space itself
    bbox = textMeasure->Measure(L"wa", 2);
    float l1 = bbox.dx;
    bbox = textMeasure->Measure(L"w a", 3);
    float l2 = bbox.dx;
    float spaceDx2 = l2 - l1;
    return spaceDx2;
#endif
}

} // namespace mui
