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

#include "Mui.h"
#include "WinUtil.h"

namespace mui {

// a critical section for everything that needs protecting in mui
// we use only one for simplicity as long as contention is not a problem
static CRITICAL_SECTION gMuiCs;

void EnterMuiCriticalSection()
{
    EnterCriticalSection(&gMuiCs);
}

void LeaveMuiCriticalSection()
{
    LeaveCriticalSection(&gMuiCs);
}

class FontListItem {
public:
    FontListItem(const WCHAR *name, float sizePt, FontStyle style, Font *font, HFONT hFont) : next(NULL) {
        cf.name = str::Dup(name);
        cf.sizePt = sizePt;
        cf.style = style;
        cf.font = font;
        cf.hFont = hFont;
    }
    ~FontListItem() {
        free((void *)cf.name);
        ::delete cf.font;
        DeleteObject(cf.hFont);
        delete next;
    }

    CachedFont cf;
    FontListItem *next;
};

// Global, thread-safe font cache. Font objects live forever.
static FontListItem *gFontsCache = NULL;

// Graphics objects cannot be used across threads. We have a per-thread
// cache so that it's easy to grab Graphics object to be used for
// measuring text
struct GraphicsCacheEntry
{
    enum {
        bmpDx = 32,
        bmpDy = 4,
        stride = bmpDx * 4,
    };

    DWORD       threadId;
    int         refCount;

    Graphics *  gfx;
    Bitmap *    bmp;
    BYTE        data[bmpDx * bmpDy * 4];

    bool Create();
    void Free();
};

static Vec<GraphicsCacheEntry> *gGraphicsCache = NULL;

// set consistent mode for our graphics objects so that we get
// the same results when measuring text
void InitGraphicsMode(Graphics *g)
{
    g->SetCompositingQuality(CompositingQualityHighQuality);
    g->SetSmoothingMode(SmoothingModeAntiAlias);
    //g.SetSmoothingMode(SmoothingModeHighQuality);
    g->SetTextRenderingHint(TextRenderingHintClearTypeGridFit);
    g->SetPageUnit(UnitPixel);
}

bool GraphicsCacheEntry::Create()
{
    memset(data, 0, sizeof(data));
    refCount = 1;
    threadId = GetCurrentThreadId();
    // using a small bitmap under assumption that Graphics used only
    // for measuring text doesn't need the actual bitmap
    bmp = ::new Bitmap(bmpDx, bmpDy, stride, PixelFormat32bppARGB, data);
    if (!bmp)
        return false;
    gfx = ::new Graphics((Image*)bmp);
    if (!gfx)
        return false;
    InitGraphicsMode(gfx);
    return true;
}

void GraphicsCacheEntry::Free()
{
    CrashIf(0 != refCount);
    ::delete gfx;
    ::delete bmp;
}

void InitializeBase()
{
    InitializeCriticalSection(&gMuiCs);
    gGraphicsCache = new Vec<GraphicsCacheEntry>();
    // allocate the first entry in gGraphicsCache for UI thread, ref count
    // ensures it stays alive forever
    AllocGraphicsForMeasureText();
}

void DestroyBase()
{
    FreeGraphicsForMeasureText(gGraphicsCache->At(0).gfx);
    for (GraphicsCacheEntry *e = gGraphicsCache->IterStart(); e; e = gGraphicsCache->IterNext()) {
        e->Free();
    }
    delete gGraphicsCache;
    delete gFontsCache;
    DeleteCriticalSection(&gMuiCs);
}

bool CachedFont::SameAs(const WCHAR *otherName, float otherSizePt, FontStyle otherStyle) const
{
    if (sizePt != otherSizePt)
        return false;
    if (style != otherStyle)
        return false;
    return str::Eq(name, otherName);
}

HFONT CachedFont::GetHFont()
{
    LOGFONTW lf;
    EnterMuiCriticalSection();
    if (!hFont) {
        // TODO: Graphics is probably only used for metrics,
        // so this might not be 100% correct (e.g. 2 monitors with different DPIs?)
        // but previous code wasn't much better
        Graphics *gfx = AllocGraphicsForMeasureText();
        Status status = font->GetLogFontW(gfx, &lf);
        FreeGraphicsForMeasureText(gfx);
        CrashIf(status != Ok);
        hFont = CreateFontIndirectW(&lf);
        CrashIf(!hFont);
    }
    LeaveMuiCriticalSection();
    return hFont;
}

// convenience function: given cached style, get a Font object matching the font
// properties.
// Caller should not delete the font - it's cached for performance and deleted at exit
CachedFont *GetCachedFont(const WCHAR *name, float sizePt, FontStyle style)
{
    ScopedMuiCritSec muiCs;

    for (FontListItem *item = gFontsCache; item; item = item->next) {
        if (item->cf.SameAs(name, sizePt, style) && item->cf.font != NULL) {
            return &item->cf;
        }
    }

    Font *font = ::new Font(name, sizePt, style);
    if (!font) {
        font = ::new Font(L"Times New Roman", sizePt, style);
        if (!font) {
            // if no font is available, return the last successfully created one
            return gFontsCache ? &gFontsCache->cf : NULL;
        }
    }

    FontListItem *item = new FontListItem(name, sizePt, style, font, NULL);
    ListInsert(&gFontsCache, item);
    return &item->cf;
}

Graphics *AllocGraphicsForMeasureText()
{
    ScopedMuiCritSec muiCs;

    DWORD threadId = GetCurrentThreadId();
    for (GraphicsCacheEntry *e = gGraphicsCache->IterStart(); e; e = gGraphicsCache->IterNext()) {
        if (e->threadId == threadId) {
            e->refCount++;
            return e->gfx;
        }
    }
    GraphicsCacheEntry ce;
    ce.Create();
    gGraphicsCache->Append(ce);
    if (gGraphicsCache->Count() < 64)
        return ce.gfx;

    // try to limit the size of cache by evicting the oldest entries, but don't remove
    // first (for ui thread) or last (one we just added) entries
    for (size_t i = 1; i < gGraphicsCache->Count() - 1; i++) {
        GraphicsCacheEntry e = gGraphicsCache->At(i);
        if (0 == e.refCount) {
            e.Free();
            gGraphicsCache->RemoveAt(i);
            return ce.gfx;
        }
    }
    // We shouldn't get here - indicates ref counting problem
    CrashIf(true);
    return ce.gfx;
}

void FreeGraphicsForMeasureText(Graphics *gfx)
{
    ScopedMuiCritSec muiCs;

    DWORD threadId = GetCurrentThreadId();
    for (GraphicsCacheEntry *e = gGraphicsCache->IterStart(); e; e = gGraphicsCache->IterNext()) {
        if (e->gfx == gfx) {
            CrashIf(e->threadId != threadId);
            e->refCount--;
            CrashIf(e->refCount < 0);
            return;
        }
    }
    CrashIf(true);
}

int CeilI(float n)
{
    n = ceil(n);
    return (int)n;
}

}
