// 17 january 2017
#include "uipriv_windows.hpp"
#include "draw.hpp"

// TODO
// - consider the warnings about antialiasing in the PadWrite sample
// - if that's not a problem, do we have overlapping rects in the hittest sample? I can't tell...
// - what happens if any nLines == 0?

struct uiDrawTextLayout {
	IDWriteTextFormat *format;
	IDWriteTextLayout *layout;
	UINT32 nLines;
	struct lineInfo *lineInfo;
	// for converting DirectWrite indices from/to byte offsets
	size_t *u8tou16;
	size_t nUTF8;
	size_t *u16tou8;
	size_t nUTF16;
};

// TODO copy notes about DirectWrite DIPs being equal to Direct2D DIPs here

// typographic points are 1/72 inch; this parameter is 1/96 inch
// fortunately Microsoft does this too, in https://msdn.microsoft.com/en-us/library/windows/desktop/dd371554%28v=vs.85%29.aspx
#define pointSizeToDWriteSize(size) (size * (96.0 / 72.0))

// TODO should be const but then I can't operator[] on it; the real solution is to find a way to do designated array initializers in C++11 but I do not know enough C++ voodoo to make it work (it is possible but no one else has actually done it before)
static std::map<uiDrawTextItalic, DWRITE_FONT_STYLE> dwriteItalics = {
	{ uiDrawTextItalicNormal, DWRITE_FONT_STYLE_NORMAL },
	{ uiDrawTextItalicOblique, DWRITE_FONT_STYLE_OBLIQUE },
	{ uiDrawTextItalicItalic, DWRITE_FONT_STYLE_ITALIC },
};

// TODO should be const but then I can't operator[] on it; the real solution is to find a way to do designated array initializers in C++11 but I do not know enough C++ voodoo to make it work (it is possible but no one else has actually done it before)
static std::map<uiDrawTextStretch, DWRITE_FONT_STRETCH> dwriteStretches = {
	{ uiDrawTextStretchUltraCondensed, DWRITE_FONT_STRETCH_ULTRA_CONDENSED },
	{ uiDrawTextStretchExtraCondensed, DWRITE_FONT_STRETCH_EXTRA_CONDENSED },
	{ uiDrawTextStretchCondensed, DWRITE_FONT_STRETCH_CONDENSED },
	{ uiDrawTextStretchSemiCondensed, DWRITE_FONT_STRETCH_SEMI_CONDENSED },
	{ uiDrawTextStretchNormal, DWRITE_FONT_STRETCH_NORMAL },
	{ uiDrawTextStretchSemiExpanded, DWRITE_FONT_STRETCH_SEMI_EXPANDED },
	{ uiDrawTextStretchExpanded, DWRITE_FONT_STRETCH_EXPANDED },
	{ uiDrawTextStretchExtraExpanded, DWRITE_FONT_STRETCH_EXTRA_EXPANDED },
	{ uiDrawTextStretchUltraExpanded, DWRITE_FONT_STRETCH_ULTRA_EXPANDED },
};

struct lineInfo {
	size_t startPos;			// in UTF-16 points
	size_t endPos;
	size_t newlineCount;
	double x;
	double y;
	double width;
	double height;
	double baseline;
};

// this function is deeply indebted to the PadWrite sample: https://github.com/Microsoft/Windows-classic-samples/blob/master/Samples/Win7Samples/multimedia/DirectWrite/PadWrite/TextEditor.cpp
static void computeLineInfo(uiDrawTextLayout *tl)
{
	DWRITE_LINE_METRICS *dlm;
	size_t nextStart;
	UINT32 i, j;
	DWRITE_HIT_TEST_METRICS *htm;
	UINT32 nFragments, unused;
	HRESULT hr;

	// TODO make sure this is legal; if not, switch to GetMetrics() and use its line count field instead
	hr = tl->layout->GetLineMetrics(NULL, 0, &(tl->nLines));
	// ugh, HRESULT_TO_WIN32() is an inline function and is not constexpr so we can't use switch here
	if (hr == S_OK) {
		// TODO what do we do here
	} else if (hr != E_NOT_SUFFICIENT_BUFFER)
		logHRESULT(L"error getting number of lines in IDWriteTextLayout", hr);
	tl->lineInfo = (struct lineInfo *) uiAlloc(tl->nLines * sizeof (struct lineInfo), "struct lineInfo[] (text layout)");

	dlm = new DWRITE_LINE_METRICS[tl->nLines];
	// we can't pass NULL here; it outright crashes if we do
	// TODO verify the numbers haven't changed
	hr = tl->layout->GetLineMetrics(dlm, tl->nLines, &unused);
	if (hr != S_OK)
		logHRESULT(L"error getting IDWriteTextLayout line metrics", hr);

	// assume the first line starts at position 0 and the string flow is incremental
	nextStart = 0;
	for (i = 0; i < tl->nLines; i++) {
		tl->lineInfo[i].startPos = nextStart;
		tl->lineInfo[i].endPos = nextStart + dlm[i].length;
		tl->lineInfo[i].newlineCount = dlm[i].newlineLength;
		nextStart = tl->lineInfo[i].endPos;

		// a line can have multiple fragments; for example, if there's a bidirectional override in the middle of a line
		hr = tl->layout->HitTestTextRange(tl->lineInfo[i].startPos, (tl->lineInfo[i].endPos - tl->lineInfo[i].newlineCount) - tl->lineInfo[i].startPos,
			0, 0,
			NULL, 0, &nFragments);
		if (hr != S_OK && hr != E_NOT_SUFFICIENT_BUFFER)
			logHRESULT(L"error getting IDWriteTextLayout line fragment count", hr);
		htm = new DWRITE_HIT_TEST_METRICS[nFragments];
		// TODO verify unused == nFragments?
		hr = tl->layout->HitTestTextRange(tl->lineInfo[i].startPos, (tl->lineInfo[i].endPos - tl->lineInfo[i].newlineCount) - tl->lineInfo[i].startPos,
			0, 0,
			htm, nFragments, &unused);
		// TODO can this return E_NOT_SUFFICIENT_BUFFER again?
		if (hr != S_OK)
			logHRESULT(L"error getting IDWriteTextLayout line fragment metrics", hr);
		// TODO verify htm.textPosition and htm.length against dtm[i]/tl->lineInfo[i]?
		tl->lineInfo[i].x = htm[0].left;
		tl->lineInfo[i].y = htm[0].top;
		// TODO does this not include trailing whitespace? I forget
		tl->lineInfo[i].width = htm[0].width;
		tl->lineInfo[i].height = htm[0].height;
		for (j = 1; j < nFragments; j++) {
			// this is correct even if the leftmost fragment on the line is RTL
			if (tl->lineInfo[i].x > htm[j].left)
				tl->lineInfo[i].x = htm[j].left;
			tl->lineInfo[i].width += htm[j].width;
			// TODO verify y and height haven't changed?
		}
		// TODO verify dlm[i].height == htm.height?
		delete[] htm;

		// TODO on Windows 8.1 and/or 10 we can use DWRITE_LINE_METRICS1 to get specific info about the ascent and descent; do we have an alternative?
		// TODO and even on those platforms can we somehow split tyographic leading from spacing?
		// TODO and on that note, can we have both line spacing proportionally above and uniformly below?
		tl->lineInfo[i].baseline = dlm[i].baseline;
	}

	delete[] dlm;
}

uiDrawTextLayout *uiDrawNewTextLayout(uiAttributedString *s, uiDrawFontDescriptor *defaultFont, double width)
{
	uiDrawTextLayout *tl;
	WCHAR *wDefaultFamily;
	DWRITE_WORD_WRAPPING wrap;
	FLOAT maxWidth;
	HRESULT hr;

	tl = uiNew(uiDrawTextLayout);

	wDefaultFamily = toUTF16(defaultFont->Family);
	hr = dwfactory->CreateTextFormat(
		wDefaultFamily, NULL,
		// for the most part, DirectWrite weights correlate to ours
		// the differences:
		// - Minimum — libui: 0, DirectWrite: 1
		// - Maximum — libui: 1000, DirectWrite: 999
		// TODO figure out what to do about this shorter range (the actual major values are the same (but with different names), so it's just a range issue)
		(DWRITE_FONT_WEIGHT) (defaultFont->Weight),
		dwriteItalics[defaultFont->Italic],
		dwriteStretches[defaultFont->Stretch],
		pointSizeToDWriteSize(defaultFont->Size),
		// see http://stackoverflow.com/questions/28397971/idwritefactorycreatetextformat-failing and https://msdn.microsoft.com/en-us/library/windows/desktop/dd368203.aspx
		// TODO use the current locale?
		L"",
		&(tl->format));
	if (hr != S_OK)
		logHRESULT(L"error creating IDWriteTextFormat", hr);

	hr = dwfactory->CreateTextLayout(
		(const WCHAR *) attrstrUTF16(s), attrstrUTF16Len(s),
		tl->format,
		// FLOAT is float, not double, so this should work... TODO
		FLT_MAX, FLT_MAX,
		&(tl->layout));
	if (hr != S_OK)
		logHRESULT(L"error creating IDWriteTextLayout", hr);

	// and set the width
	// this is the only wrapping mode (apart from "no wrap") available prior to Windows 8.1 (TODO verify this fact) (TODO this should be the default anyway)
	wrap = DWRITE_WORD_WRAPPING_WRAP;
	maxWidth = (FLOAT) width;
	if (width < 0) {
		// TODO is this wrapping juggling even necessary?
		wrap = DWRITE_WORD_WRAPPING_NO_WRAP;
		// setting the max width in this case technically isn't needed since the wrap mode will simply ignore the max width, but let's do it just to be safe
		maxWidth = FLT_MAX;		// see TODO above
	}
	hr = tl->layout->SetWordWrapping(wrap);
	if (hr != S_OK)
		logHRESULT(L"error setting IDWriteTextLayout word wrapping mode", hr);
	hr = tl->layout->SetMaxWidth(maxWidth);
	if (hr != S_OK)
		logHRESULT(L"error setting IDWriteTextLayout max layout width", hr);

	computeLineInfo(tl);

	// and finally copy the UTF-8/UTF-16 index conversion tables
	tl->u8tou16 = attrstrCopyUTF8ToUTF16(s, &(tl->nUTF8));
	tl->u16tou8 = attrstrCopyUTF16ToUTF8(s, &(tl->nUTF16));

	// TODO can/should this be moved elsewhere?
	uiFree(wDefaultFamily);
	return tl;
}

void uiDrawFreeTextLayout(uiDrawTextLayout *tl)
{
	uiFree(tl->u16tou8);
	uiFree(tl->u8tou16);
	uiFree(tl->lineInfo);
	tl->layout->Release();
	tl->format->Release();
	uiFree(tl);
}

static ID2D1SolidColorBrush *mkSolidBrush(ID2D1RenderTarget *rt, double r, double g, double b, double a)
{
	D2D1_BRUSH_PROPERTIES props;
	D2D1_COLOR_F color;
	ID2D1SolidColorBrush *brush;
	HRESULT hr;

	ZeroMemory(&props, sizeof (D2D1_BRUSH_PROPERTIES));
	props.opacity = 1.0;
	// identity matrix
	props.transform._11 = 1;
	props.transform._22 = 1;
	color.r = r;
	color.g = g;
	color.b = b;
	color.a = a;
	hr = rt->CreateSolidColorBrush(
		&color,
		&props,
		&brush);
	if (hr != S_OK)
		logHRESULT(L"error creating solid brush", hr);
	return brush;
}

// TODO this ignores clipping?
void uiDrawText(uiDrawContext *c, uiDrawTextLayout *tl, double x, double y)
{
	D2D1_POINT_2F pt;
	ID2D1Brush *black;

	// TODO document that fully opaque black is the default text color; figure out whether this is upheld in various scenarios on other platforms
	// TODO figure out if this needs to be cleaned out
	black = mkSolidBrush(c->rt, 0.0, 0.0, 0.0, 1.0);

	pt.x = x;
	pt.y = y;
	// TODO D2D1_DRAW_TEXT_OPTIONS_NO_SNAP?
	// TODO D2D1_DRAW_TEXT_OPTIONS_CLIP?
	// TODO when setting 8.1 as minimum (TODO verify), D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT?
	// TODO what is our pixel snapping setting related to the OPTIONS enum values?
	c->rt->DrawTextLayout(pt, tl->layout, black, D2D1_DRAW_TEXT_OPTIONS_NONE);

	black->Release();
}

// TODO for a single line the height includes the leading; should it? TextEdit on OS X always includes the leading and/or paragraph spacing, otherwise Klee won't work...
// TODO width does not include trailing whitespace
void uiDrawTextLayoutExtents(uiDrawTextLayout *tl, double *width, double *height)
{
	DWRITE_TEXT_METRICS metrics;
	HRESULT hr;

	hr = tl->layout->GetMetrics(&metrics);
	if (hr != S_OK)
		logHRESULT(L"error getting IDWriteTextLayout layout metrics", hr);
	*width = metrics.width;
	// TODO make sure the behavior of this on empty strings is the same on all platforms (ideally should be 0-width, line height-height; TODO note this in the docs too)
	*height = metrics.height;
}

int uiDrawTextLayoutNumLines(uiDrawTextLayout *tl)
{
	return tl->nLines;
}

// DirectWrite doesn't provide a direct way to do this, so we have to do this manually
// TODO does that comment still apply here or to the code at the top of this file?
void uiDrawTextLayoutLineByteRange(uiDrawTextLayout *tl, int line, size_t *start, size_t *end)
{
	*start = tl->lineInfo[line].startPos;
	*start = tl->u16tou8[*start];
	*end = tl->lineInfo[line].endPos - tl->lineInfo[line].newlineCount;
	*end = tl->u16tou8[*end];
}

void uiDrawTextLayoutLineGetMetrics(uiDrawTextLayout *tl, int line, uiDrawTextLayoutLineMetrics *m)
{
	m->X = tl->lineInfo[line].x;
	m->Y = tl->lineInfo[line].y;
	m->Width = tl->lineInfo[line].width;
	m->Height = tl->lineInfo[line].height;

	// TODO rename tl->lineInfo[line].baseline to .baselineOffset or something of the sort to make its meaning more clear
	m->BaselineY = tl->lineInfo[line].y + tl->lineInfo[line].baseline;
	m->Ascent = tl->lineInfo[line].baseline;
	m->Descent = tl->lineInfo[line].height - tl->lineInfo[line].baseline;
	m->Leading = 0;		// TODO

	m->ParagraphSpacingBefore = 0;		// TODO
	m->LineHeightSpace = 0;				// TODO
	m->LineSpacing = 0;				// TODO
	m->ParagraphSpacing = 0;			// TODO
}

#if 0 /* TODO */

// expected behavior on end of line:
// - same as OS X
// - TODO RETEST THIS ON OTHER PLATFORMS: EXCEPT: if you type something, then backspace, the cursor will move back to where you clicked!
// TODO this function does NOT yet work like that; it works like the Unix equivalent right now
void uiDrawTextLayoutHitTest(uiDrawTextLayout *tl, double x, double y, uiDrawTextLayoutHitTestResult *result)
{
	DWRITE_HIT_TEST_METRICS m;
	size_t line;
	double width, height;
	BOOL trailing, inside;		// crashes if I skip these :/
	HRESULT hr;

	hr = tl->layout->HitTestPoint(x, y,
		&trailing, &inside,
		&m);
	if (hr != S_OK)
		logHRESULT(L"error hit-testing IDWriteTextLayout", hr);

	// figure out what line this is
	if (y < 0)
		line = 0;
	else
		for (line = 0; line < tl->nLines; line++)
			if (y >= tl->lineInfo[line].y && y < (tl->lineInfo[line].y + tl->lineInfo[line].height))
				break;
	if (line == tl->nLines)		// last position
		line--;

	result->Pos = tl->u16tou8[m.textPosition];
	result->Line = line;

	uiDrawTextLayoutExtents(tl, &width, &height);
	result->XPosition = uiDrawTextLayoutHitTestPositionInside;
	if (x < 0)
		result->XPosition = uiDrawTextLayoutHitTestPositionBefore;
	else if (x >= width)
		result->XPosition = uiDrawTextLayoutHitTestPositionAfter;
	result->YPosition = uiDrawTextLayoutHitTestPositionInside;
	if (y < 0)
		result->YPosition = uiDrawTextLayoutHitTestPositionBefore;
	else if (y >= height)
		result->YPosition = uiDrawTextLayoutHitTestPositionAfter;

	result->CaretX = m.left;		// TODO is this correct?
	result->CaretY = tl->lineInfo[line].y;
	result->CaretHeight = tl->lineInfo[line].height;
}

void uiDrawTextLayoutByteRangeToRectangle(uiDrawTextLayout *tl, size_t start, size_t end, uiDrawTextLayoutByteRangeRectangle *r)
{
	DWRITE_HIT_TEST_METRICS mstart, mend;
	size_t line;
	FLOAT x, y;				// crashes if I skip these :/
	BOOL trailing, inside;		// crashes if I skip these :/
	HRESULT hr;

	start = tl->u8tou16[start];
	end = tl->u8tou16[end];

	// TODO explain why this is a leading hit
	hr = tl->layout->HitTestTextPosition(start, FALSE,
		&x, &y,
		&mstart);
	if (hr != S_OK)
		logHRESULT(L"error getting rect of start position", hr);

	// figure out what line this is
	for (line = 0; line < tl->nLines; line++)
		if (start >= tl->lineInfo[line].startPos && start < tl->lineInfo[line].endPos)
			break;
	if (line == tl->nLines)		// last position
		line--;
	if (end > tl->lineInfo[line].endPos)
		end = tl->lineInfo[line].endPos;

	hr = tl->layout->HitTestTextPosition(end, FALSE,
		&x, &y,
		&mend);
	if (hr != S_OK)
		logHRESULT(L"error getting rect of end position", hr);

	r->X = mstart.left;
	r->Y = tl->lineInfo[line].y;
	r->Width = mend.left - mstart.left;
	r->Height = tl->lineInfo[line].height;

	hr = tl->layout->HitTestPoint(r->X, r->Y,
		&trailing, &inside,
		&mstart);
	if (hr != S_OK)
		logHRESULT(L"TODO write this", hr);
	// TODO also get the end pos just so we can have an accurate r->RealEnd
	r->RealStart = mstart.textPosition;
	r->RealEnd = end;

	r->RealStart = tl->u16tou8[r->RealStart];
	r->RealEnd = tl->u16tou8[r->RealEnd];
}

#endif

// this algorithm comes from Microsoft's PadWrite sample, following TextEditor::SetSelectionFromPoint()
void uiDrawTextLayoutHitTest(uiDrawTextLayout *tl, double x, double y, size_t *pos, int *line)
{
	DWRITE_HIT_TEST_METRICS m;
	BOOL trailing, inside;
	size_t p;
	HRESULT hr;

	hr = tl->layout->HitTestPoint(x, y,
		&trailing, &inside,
		&m);
	if (hr != S_OK)
		logHRESULT(L"error hit-testing IDWriteTextLayout", hr);
	p = m.textPosition;
	// on a trailing hit, align to the nearest cluster
	if (trailing) {
		DWRITE_HIT_TEST_METRICS m2;
		FLOAT x, y;				// crashes if I skip these :/

		hr = tl->layout->HitTestTextPosition(m.textPosition, trailing,
			&x, &y, &m2);
		if (hr != S_OK)
			logHRESULT(L"error aligning trailing hit to nearest cluster", hr);
		p = m2.textPosition + m2.length;
	}
	// TODO should we just make the pointers required?
	if (pos != NULL)
		*pos = tl->u16tou8[p];

	// TODO do the line detection unconditionally?
	if (line != NULL) {
		UINT32 i;

		for (i = 0; i < tl->nLines; i++) {
			double ltop, lbottom;

			ltop = tl->lineInfo[i].y;
			lbottom = ltop + tl->lineInfo[i].height;
			// y will already >= ltop at this point since the past lbottom should == ltop
			if (y < lbottom)
				break;
		}
		if (i == tl->nLines)
			i--;
		*line = i;
	}
}

double uiDrawTextLayoutByteLocationInLine(uiDrawTextLayout *tl, size_t pos, int line)
{
	BOOL trailing;
	DWRITE_HIT_TEST_METRICS m;
	FLOAT x, y;
	HRESULT hr;

	pos = tl->u8tou16[pos];
	// this behavior seems correct
	// there's also PadWrite's TextEditor::GetCaretRect() but that requires state...
	// TODO where does this fail?
	trailing = FALSE;
	if (pos != 0 && pos != tl->nUTF16 && pos == tl->lineInfo[line].endPos) {
		pos--;
		trailing = TRUE;
	}
	hr = tl->layout->HitTestTextPosition(pos, trailing,
		&x, &y, &m);
	if (hr != S_OK)
		logHRESULT(L"error calling IDWriteTextLayout::HitTestTextPosition()", hr);
	return x;
}