// Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
// SPDX-FileCopyrightText: 2021 Jorrit Rouwe
// SPDX-License-Identifier: MIT

#include <TestFramework.h>

#include <Renderer/Font.h>
#include <Renderer/Renderer.h>
#include <Image/Surface.h>
#include <Utils/ReadData.h>
#include <Jolt/Core/Profiler.h>
#include <Jolt/Core/ScopeExit.h>

JPH_SUPPRESS_WARNINGS_STD_BEGIN
JPH_CLANG_SUPPRESS_WARNING("-Wreserved-identifier")
JPH_CLANG_SUPPRESS_WARNING("-Wzero-as-null-pointer-constant")
JPH_CLANG_SUPPRESS_WARNING("-Wcast-qual")
JPH_CLANG_SUPPRESS_WARNING("-Wimplicit-fallthrough")
JPH_CLANG_SUPPRESS_WARNING("-Wcomma")
JPH_CLANG_SUPPRESS_WARNING("-Wdouble-promotion")
#define STB_TRUETYPE_IMPLEMENTATION
#include <External/stb_truetype.h>
JPH_SUPPRESS_WARNINGS_STD_END

Font::Font(Renderer *inRenderer) :
	mRenderer(inRenderer)
{
}

bool Font::Create(const char *inFontName, int inCharHeight)
{
	JPH_PROFILE("Create");

	// Initialize
	mFontName = inFontName;
	mCharHeight = inCharHeight;
	mHorizontalTexels = 64;
	mVerticalTexels = 64;

	constexpr int cSpacingH = 2; // Number of pixels to put horizontally between characters
	constexpr int cSpacingV = 2; // Number of pixels to put vertically between characters

	// Read font data
	Array<uint8> font_data = ReadData((String("Assets/Fonts/") + inFontName + ".ttf").c_str());

	// Construct a font info
	stbtt_fontinfo font;
	if (!stbtt_InitFont(&font, font_data.data(), stbtt_GetFontOffsetForIndex(font_data.data(), 0)))
		return false;

	// Get the base line for the font
	float scale = stbtt_ScaleForPixelHeight(&font, float(mCharHeight));
	int ascent;
	stbtt_GetFontVMetrics(&font, &ascent, nullptr, nullptr);
	int baseline = int(ascent * scale);

	// Create surface for characters
	Ref<SoftwareSurface> surface = new SoftwareSurface(mHorizontalTexels, mVerticalTexels, ESurfaceFormat::L8);
	surface->Clear();
	surface->Lock(ESurfaceLockMode::Write);

	// Draw all printable characters
try_again:;
	int x = 0, y = 0;
	static_assert(cBeginChar == ' ', "We skip space in the for loop below");
	for (int c = cBeginChar + 1; c < cEndChar; ++c)
	{
		// Get index in the arrays
		int idx = c - cBeginChar;

		int w, h, xoff, yoff;
		unsigned char *bitmap = stbtt_GetCodepointBitmap(&font, 0, scale, c, &w, &h, &xoff, &yoff);
		JPH_SCOPE_EXIT([bitmap]{ STBTT_free(bitmap, nullptr); });
		yoff = baseline + yoff;

		// Check if there is room on this line
		if (int(x + xoff + w + cSpacingH) > mHorizontalTexels)
		{
			// Next line
			x = 0;
			y += mCharHeight + cSpacingV;

			// Check if character fits
			if (y + mCharHeight + cSpacingV > mVerticalTexels)
			{
				// Character doesn't fit, enlarge surface
				if (mHorizontalTexels < 2 * mVerticalTexels)
					mHorizontalTexels <<= 1;
				else
					mVerticalTexels <<= 1;

				// Create new surface
				surface->UnLock();
				surface = new SoftwareSurface(mHorizontalTexels, mVerticalTexels, ESurfaceFormat::L8);
				surface->Clear();
				surface->Lock(ESurfaceLockMode::Write);

				// Try again with the larger texture
				goto try_again;
			}
		}

		// Get location of character in font surface
		JPH_ASSERT(x >= 0 && x <= 0xffff);
		JPH_ASSERT(y >= 0 && y <= 0xffff);
		JPH_ASSERT(w <= 0xff);
		mStartU[idx] = uint16(x);
		mStartV[idx] = uint16(y);
		mWidth[idx] = uint8(w + 1);

		// Copy the character data
		for (int y2 = 0; y2 < h; ++y2)
		{
			uint8 *src = bitmap + y2 * w;
			uint8 *dst = surface->GetScanLine(y + yoff + y2) + x + xoff;
			memcpy(dst, src, w);
		}

		// Go to the next character
		x += w + cSpacingH;
	}

	// Calculate spacing between characters
	for (int idx1 = 0; idx1 < cNumChars; ++idx1)
		for (int idx2 = 0; idx2 < cNumChars; ++idx2)
		{
			int c1 = cBeginChar + idx1;
			int c2 = cBeginChar + idx2;

			int advance;
			stbtt_GetCodepointHMetrics(&font, c1, &advance, nullptr);
			int spacing = Clamp(int(scale * (advance + stbtt_GetCodepointKernAdvance(&font, c1, c2))), 0, 0xff);
			mSpacing[idx1][idx2] = (uint8)spacing;
		}

	// Unlock surface
	surface->UnLock();

	// Create input layout
	const PipelineState::EInputDescription vertex_desc[] =
	{
		PipelineState::EInputDescription::Position,
		PipelineState::EInputDescription::TexCoord,
		PipelineState::EInputDescription::Color
	};

	// Load vertex shader
	Ref<VertexShader> vtx = mRenderer->CreateVertexShader("Assets/Shaders/FontVertexShader");

	// Load pixel shader
	Ref<PixelShader> pix = mRenderer->CreatePixelShader("Assets/Shaders/FontPixelShader");

	mPipelineState = mRenderer->CreatePipelineState(vtx, vertex_desc, std::size(vertex_desc), pix, PipelineState::EDrawPass::Normal, PipelineState::EFillMode::Solid, PipelineState::ETopology::Triangle, PipelineState::EDepthTest::Off, PipelineState::EBlendMode::AlphaBlend, PipelineState::ECullMode::Backface);

	// Create texture
	mTexture = mRenderer->CreateTexture(surface);

	// Trace success
	Trace("Created font \"%s\" with height %d in a %dx%d surface", mFontName.c_str(), mCharHeight, mHorizontalTexels, mVerticalTexels);

	return true;
}

Float2 Font::MeasureText(const string_view &inText) const
{
	JPH_PROFILE("MeasureText");

	Float2 extents(0, 1.0f);

	// Current raster position
	float x = 0;

	// Loop through string
	for (uint i = 0; i < inText.size(); ++i)
	{
		// Get character
		int ch = inText[i];

		// Create character if it is printable
		static_assert(cBeginChar == ' ', "We skip space in the for loop below");
		if (ch > cBeginChar && ch < cEndChar)
		{
			// Update extents
			int c1 = ch - cBeginChar;
			extents.x = max(extents.x, x + float(mWidth[c1]) / mCharHeight);
		}

		// Go to next (x, y) location
		if (ch == '\n')
		{
			// Next line
			x = 0;
			extents.y += 1.0f;
		}
		else if (i + 1 < inText.size())
		{
			// Do spacing between the two characters
			int c1 = ch - cBeginChar;
			int c2 = inText[i + 1] - cBeginChar;

			if (c1 >= 0 && c1 < cNumChars && c2 >= 0 && c2 < cNumChars)
				x += float(mSpacing[c1][c2]) / mCharHeight;
		}
	}

	return extents;
}

bool Font::CreateString(Mat44Arg inTransform, const string_view &inText, ColorArg inColor, RenderPrimitive &ioPrimitive) const
{
	JPH_PROFILE("CreateString");

	// Reset primitive
	ioPrimitive.Clear();

	// Count the number of printable chars
	int printable = 0;
	for (uint i = 0; i < inText.size(); ++i)
	{
		int ch = inText[i];
		static_assert(cBeginChar == ' ', "We skip space in the for loop below");
		if (ch > cBeginChar && ch < cEndChar) // Space is not printable
			printable++;
	}
	if (printable == 0)
		return false;

	// Get correction factor for texture size
	float texel_to_u = 1.0f / mHorizontalTexels;
	float texel_to_v = 1.0f / mVerticalTexels;

	int vtx_size = printable * 4;
	int idx_size = printable * 6;
	ioPrimitive.CreateVertexBuffer(vtx_size, sizeof(FontVertex));
	ioPrimitive.CreateIndexBuffer(idx_size);

	// Current vertex
	uint32 vtx = 0;

	// Lock buffers
	FontVertex *font_vtx = (FontVertex *)ioPrimitive.LockVertexBuffer();
	uint32 *idx_start = ioPrimitive.LockIndexBuffer();
	uint32 *idx = idx_start;

	// Current raster position
	float x = 0, y = -1.0f;

	// Loop through string
	for (uint i = 0; i < inText.size(); ++i)
	{
		// Get character
		int ch = inText[i];

		// Create character if it is printable
		static_assert(cBeginChar == ' ', "We skip space in the for loop below");
		if (ch > cBeginChar && ch < cEndChar)
		{
			// Get index for character
			int c1 = ch - cBeginChar;

			// Create indices
			*idx = vtx;
			++idx;
			*idx = vtx + 3;
			++idx;
			*idx = vtx + 1;
			++idx;
			*idx = vtx;
			++idx;
			*idx = vtx + 2;
			++idx;
			*idx = vtx + 3;
			++idx;
			vtx += 4;

			// Get properties of this character
			Float2 uv_start(texel_to_u * mStartU[c1], texel_to_v * mStartV[c1]);
			Float2 uv_end(texel_to_u * (mStartU[c1] + mWidth[c1]), texel_to_v * (mStartV[c1] + mCharHeight));
			Float2 xy_end(x + float(mWidth[c1]) / mCharHeight, y + 1.0f);

			// Create vertices
			(inTransform * Vec3(x, y, 0)).StoreFloat3(&font_vtx->mPosition);
			font_vtx->mColor = inColor;
			font_vtx->mTexCoord = Float2(uv_start.x, uv_end.y);
			++font_vtx;

			(inTransform * Vec3(x, xy_end.y, 0)).StoreFloat3(&font_vtx->mPosition);
			font_vtx->mColor = inColor;
			font_vtx->mTexCoord = uv_start;
			++font_vtx;

			(inTransform * Vec3(xy_end.x, y, 0)).StoreFloat3(&font_vtx->mPosition);
			font_vtx->mColor = inColor;
			font_vtx->mTexCoord = uv_end;
			++font_vtx;

			(inTransform * Vec3(xy_end.x, xy_end.y, 0)).StoreFloat3(&font_vtx->mPosition);
			font_vtx->mColor = inColor;
			font_vtx->mTexCoord = Float2(uv_end.x, uv_start.y);
			++font_vtx;
		}

		// Go to next (x, y) location
		if (ch == '\n')
		{
			// Next line
			x = 0.0f;
			y -= 1.0f;
		}
		else if (i + 1 < inText.size())
		{
			// Do spacing between the two characters
			int c1 = ch - cBeginChar;
			int c2 = inText[i + 1] - cBeginChar;

			if (c1 >= 0 && c1 < cNumChars && c2 >= 0 && c2 < cNumChars)
				x += float(mSpacing[c1][c2]) / mCharHeight;
		}
	}

	// Check that we completely filled the output buffer
	JPH_ASSERT(vtx == (uint32)vtx_size);
	JPH_ASSERT(idx == idx_start + idx_size);

	// Unlock buffers
	ioPrimitive.UnlockVertexBuffer();
	ioPrimitive.UnlockIndexBuffer();

	return true;
}

void Font::DrawText3D(Mat44Arg inTransform, const string_view &inText, ColorArg inColor) const
{
	JPH_PROFILE("DrawText3D");

	// Early out
	if (inText.empty())
		return;

	Ref<RenderPrimitive> primitive = mRenderer->CreateRenderPrimitive(PipelineState::ETopology::Triangle);
	if (CreateString(inTransform, inText, inColor, *primitive))
	{
		mTexture->Bind();

		mPipelineState->Activate();

		primitive->Draw();
	}
}
