#include <stdio.h> // sprintf()
#include <math.h>
#include "ft2_header.h"
#include "ft2_gui.h"
#include "ft2_bmp.h"
#include "ft2_video.h"
#include "ft2_structs.h"
#include "ft2_gfxdata.h"
#include "ft2_pattern_ed.h" // exitPatternEditorExtended()
#include "ft2_config.h"

#define OLD_NUM_STARS 1000
#define NUM_STARS 1500
#define LOGO_ALPHA_PERCENTAGE 75
#define STARSHINE_ALPHA_PERCENTAGE 25
#define SINUS_PHASES 1024
#define ABOUT_SCREEN_X 3
#define ABOUT_SCREEN_Y 3
#define ABOUT_SCREEN_W 626
#define ABOUT_SCREEN_H 167
#define ABOUT_LOGO_W 449
#define ABOUT_LOGO_H 75

typedef struct
{
	int16_t x, y, z;
} oldVector_t;

typedef struct
{
	uint16_t x, y, z;
} oldRotate_t;

typedef struct
{
	oldVector_t x, y, z;
} oldMatrix_t;

typedef struct
{
	float x, y, z;
} vector_t;

typedef struct
{
	vector_t x, y, z;
} matrix_t;

static const uint8_t starColConv[24] = { 2,2,2,2,2,2,2,2, 2,2,2,1,1,1,3,3, 3,3,3,3,3,3,3,3 };
static char *customText0 = "Original FT2 by Magnus \"Vogue\" H\224gdahl & Fredrik \"Mr.H\" Huss";
static char *customText1 = "Clone by Olav \"8bitbubsy\" S\233rensen";
static char *customText2 = "https://16-bits.org";
static char customText3[256];
static int16_t customText0X, customText0Y, customText1Y, customText2Y;
static int16_t customText3Y, customText1X, customText2X, customText3X;
static int16_t sin16[SINUS_PHASES], zSpeed;
static int32_t lastStarScreenPos[OLD_NUM_STARS];
static const uint16_t logoAlpha16 = (65535 * LOGO_ALPHA_PERCENTAGE) / 100;
static const uint16_t starShineAlpha16 = (65535 * STARSHINE_ALPHA_PERCENTAGE) / 100;
static uint32_t sinp1, sinp2;
static oldVector_t oldStarPoints[OLD_NUM_STARS];
static oldRotate_t oldStarRotation;
static oldMatrix_t oldStarMatrix;
static vector_t starPoints[NUM_STARS], starRotation;
static matrix_t starMatrix;

// exact Turbo Pascal Random() implementation
static int32_t Random(int32_t limit)
{
	static uint32_t randSeed; // seed is 0 in Turbo Pascal unless Randomize() is called

	randSeed *= 134775813;
	randSeed++;

	return ((int64_t)randSeed * limit) >> 32;
}

static uint32_t blendPixels(uint32_t pixelA, uint32_t pixelB, uint16_t alpha)
{
	const uint16_t invAlpha = alpha ^ 0xFFFF;

	const int32_t r = ((RGB32_R(pixelA) * invAlpha) + (RGB32_R(pixelB) * alpha)) >> 16;
	const int32_t g = ((RGB32_G(pixelA) * invAlpha) + (RGB32_G(pixelB) * alpha)) >> 16;
	const int32_t b = ((RGB32_B(pixelA) * invAlpha) + (RGB32_B(pixelB) * alpha)) >> 16;
	return RGB32(r, g, b);
}

static void blendPixelsXY(uint32_t x, uint32_t y, uint32_t pixelB_r, uint32_t pixelB_g, uint32_t pixelB_b, uint16_t alpha)
{
	uint32_t *p = &video.frameBuffer[(y * SCREEN_W) + x];
	const uint32_t pixelA = *p;

	const uint16_t invAlpha = alpha ^ 0xFFFF;
	const int32_t r = ((RGB32_R(pixelA) * invAlpha) + (pixelB_r * alpha)) >> 16;
	const int32_t g = ((RGB32_G(pixelA) * invAlpha) + (pixelB_g * alpha)) >> 16;
	const int32_t b = ((RGB32_B(pixelA) * invAlpha) + (pixelB_b * alpha)) >> 16;

	*p = RGB32(r, g, b);
}

static void oldRotateStarfieldMatrix(void)
{
	// original code used a cos/sin table, but this only runs once per frame, no need...
	const int16_t sa = (int16_t)round(32767.0 * sin(oldStarRotation.x * (2.0 * PI / 65536.0)));
	const int16_t ca = (int16_t)round(32767.0 * cos(oldStarRotation.x * (2.0 * PI / 65536.0)));
	const int16_t sb = (int16_t)round(32767.0 * sin(oldStarRotation.y * (2.0 * PI / 65536.0)));
	const int16_t cb = (int16_t)round(32767.0 * cos(oldStarRotation.y * (2.0 * PI / 65536.0)));
	const int16_t sc = (int16_t)round(32767.0 * sin(oldStarRotation.z * (2.0 * PI / 65536.0)));
	const int16_t cc = (int16_t)round(32767.0 * cos(oldStarRotation.z * (2.0 * PI / 65536.0)));

	oldStarMatrix.x.x = ((ca * cc) >> 16) + (((sc * ((sa * sb) >> 16)) >> 16) << 1);
	oldStarMatrix.y.x = (sa * cb) >> 16;
	oldStarMatrix.z.x = (((cc * ((sa * sb) >> 16)) >> 16) << 1) - ((ca * sc) >> 16);

	oldStarMatrix.x.y = (((sc * ((ca * sb) >> 16)) >> 16) << 1) - ((sa * cc) >> 16);
	oldStarMatrix.y.y = (ca * cb) >> 16;
	oldStarMatrix.z.y = ((sa * sc) >> 16) + (((cc * ((ca * sb) >> 16)) >> 16) << 1);

	oldStarMatrix.x.z = (cb * sc) >> 16;
	oldStarMatrix.y.z = 0 - (sb >> 1);
	oldStarMatrix.z.z = (cb * cc) >> 16;
}

static void starfield(void)
{
	vector_t *star = starPoints;
	for (int16_t i = 0; i < NUM_STARS; i++, star++)
	{
		star->z += 0.0001f;
		if (star->z >= 0.5f)
			star->z -= 1.0f;

		const float z = (starMatrix.x.z * star->x) + (starMatrix.y.z * star->y) + (starMatrix.z.z * star->z) + 0.5f;
		if (z <= 0.0f)
			continue;

		float y = (((starMatrix.x.y * star->x) + (starMatrix.y.y * star->y) + (starMatrix.z.y * star->z)) / z) * 400.0f;
		const int32_t outY = (ABOUT_SCREEN_Y+(ABOUT_SCREEN_H/2)) + (int32_t)y;
		if (outY < ABOUT_SCREEN_Y || outY >= ABOUT_SCREEN_Y+ABOUT_SCREEN_H)
			continue;

		float x = (((starMatrix.x.x * star->x) + (starMatrix.y.x * star->y) + (starMatrix.z.x * star->z)) / z) * 400.0f;
		const int32_t outX = (ABOUT_SCREEN_X+(ABOUT_SCREEN_W/2)) + (int32_t)x;
		if (outX < ABOUT_SCREEN_X || outX >= ABOUT_SCREEN_X+ABOUT_SCREEN_W)
			continue;

		int32_t intensity255 = (int32_t)(z * 256.0f);
		if (intensity255 > 255)
			intensity255 = 255;
		intensity255 ^= 255;

		// add a tint of blue to the star pixel

		int32_t r = intensity255 - 79;
		if (r < 0)
			r = 0;

		int32_t g = intensity255 - 38;
		if (g < 0)
			g = 0;

		int32_t b = intensity255 + 64;
		if (b > 255)
			b = 255;

		// plot and blend sides of star (basic shine effect)

		if (outX-1 >= ABOUT_SCREEN_X)
			blendPixelsXY(outX-1, outY, r, g, b, starShineAlpha16);

		if (outX+1 < ABOUT_SCREEN_X+ABOUT_SCREEN_W)
			blendPixelsXY(outX+1, outY, r, g, b, starShineAlpha16);

		if (outY-1 >= ABOUT_SCREEN_Y)
			blendPixelsXY(outX, outY-1, r, g, b, starShineAlpha16);

		if (outY+1 < ABOUT_SCREEN_Y+ABOUT_SCREEN_H)
			blendPixelsXY(outX, outY+1, r, g, b, starShineAlpha16);

		// plot center pixel
		video.frameBuffer[(outY * SCREEN_W) + outX] = RGB32(r, g, b);
	}
}

static void oldStarfield(void)
{
	oldVector_t *star = oldStarPoints;
	for (int32_t i = 0; i < OLD_NUM_STARS; i++, star++)
	{
		// erase last star pixel
		int32_t screenBufferPos = lastStarScreenPos[i];
		if (screenBufferPos >= 0)
		{
			video.frameBuffer[screenBufferPos] = video.palette[PAL_BCKGRND];
			lastStarScreenPos[i] = -1;
		}

		star->z += zSpeed; // this intentionally overflows int16_t

		int16_t z = (((oldStarMatrix.x.z * star->x) >> 16) + ((oldStarMatrix.y.z * star->y) >> 16) + ((oldStarMatrix.z.z * star->z) >> 16)) + 9000;
		if (z <= 100) continue;
		
		int32_t y = ((oldStarMatrix.x.y * star->x) >> 16) + ((oldStarMatrix.y.y * star->y) >> 16) + ((oldStarMatrix.z.y * star->z) >> 16);
		y = (int16_t)((y << 7) / z) + 84;
		if ((uint16_t)y > 173-6) continue;

		int32_t x = ((oldStarMatrix.x.x * star->x) >> 16) + ((oldStarMatrix.y.x * star->y) >> 16) + ((oldStarMatrix.z.x * star->z) >> 16);
		x = (int16_t)((((x >> 2) + x) << 7) / z) + (320-8);
		if ((uint16_t)x >= 640-16)
			continue;

		// render star pixel only if the pixel under it is the background color
		screenBufferPos = ((y + 4) * SCREEN_W) + (x + 4);
		if ((video.frameBuffer[screenBufferPos] >> 24) == PAL_BCKGRND)
		{
			const uint8_t col = ((uint8_t)~(z >> 8) >> 3) - (22 - 8);
			if (col < 24)
			{
				video.frameBuffer[screenBufferPos] = video.palette[starColConv[col]];
				lastStarScreenPos[i] = screenBufferPos;
			}
		}
	}
}

static void rotateStarfieldMatrix(void)
{
#define F_2PI (float)(2.0 * PI)

	const float rx2p = starRotation.x * F_2PI;
	const float xsin = sinf(rx2p);
	const float xcos = cosf(rx2p);

	const float ry2p = starRotation.y * F_2PI;
	const float ysin = sinf(ry2p);
	const float ycos = cosf(ry2p);

	const float rz2p = starRotation.z * F_2PI;
	const float zsin = sinf(rz2p);
	const float zcos = cosf(rz2p);

	// x
	starMatrix.x.x = (xcos * zcos) + (zsin * xsin * ysin);
	starMatrix.y.x = xsin * ycos;
	starMatrix.z.x = (zcos * xsin * ysin) - (xcos * zsin);

	// y
	starMatrix.x.y = (zsin * xcos * ysin) - (xsin * zcos);
	starMatrix.y.y = xcos * ycos;
	starMatrix.z.y = (xsin * zsin) + (zcos * xcos * ysin);

	// z
	starMatrix.x.z = ycos * zsin;
	starMatrix.y.z = 0.0f - ysin;
	starMatrix.z.z = ycos * zcos;
}

void renderAboutScreenFrame(void)
{
	if (config.useNewAboutScreen)
	{
		clearRect(ABOUT_SCREEN_X, ABOUT_SCREEN_Y, ABOUT_SCREEN_W, ABOUT_SCREEN_H);

		// 3D starfield

		starfield();
		starRotation.x -= 0.0003f;
		starRotation.y -= 0.0002f;
		starRotation.z += 0.0001f;
		rotateStarfieldMatrix();

		// waving FT2 logo
	
		uint32_t *dstPtr = video.frameBuffer + (ABOUT_SCREEN_Y * SCREEN_W) + ABOUT_SCREEN_X;
		for (int32_t y = 0; y < ABOUT_SCREEN_H; y++, dstPtr += SCREEN_W)
		{
			for (int32_t x = 0; x < ABOUT_SCREEN_W; x++)
			{
				int32_t srcX = (x - ((ABOUT_SCREEN_W-ABOUT_LOGO_W)/2))    + (sin16[(sinp1+x)     & (SINUS_PHASES-1)] >> 10);
				int32_t srcY = (y - ((ABOUT_SCREEN_H-ABOUT_LOGO_H)/2)+20) + (sin16[(sinp2+y+x+x) & (SINUS_PHASES-1)] >> 11);

				if ((uint32_t)srcX < ABOUT_LOGO_W && (uint32_t)srcY < ABOUT_LOGO_H)
				{
					const uint32_t logoPixel = bmp.ft2AboutLogo[(srcY * ABOUT_LOGO_W) + srcX];
					if (logoPixel != 0x00FF00) // transparency
						dstPtr[x] = blendPixels(dstPtr[x], logoPixel, logoAlpha16);
				}
			}
		}

		sinp1 = (sinp1 + 2) & (SINUS_PHASES-1);
		sinp2 = (sinp2 + 3) & (SINUS_PHASES-1);

		// render static texts
		textOut(customText0X, customText0Y, PAL_FORGRND, customText0);
		textOut(customText1X, customText1Y, PAL_FORGRND, customText1);
		textOut(customText2X, customText2Y, PAL_FORGRND, customText2);
		textOut(customText3X, customText3Y, PAL_FORGRND, customText3);

		showPushButton(PB_EXIT_ABOUT); // yes, we also have to redraw the exit button per frame :)
	}
	else
	{
		// original FT2 about screen

		oldStarRotation.x += SCALE_VBLANK_DELTA(3*64); // 70Hz speed -> 60Hz speed
		oldStarRotation.y += SCALE_VBLANK_DELTA(2*64);
		oldStarRotation.z -= SCALE_VBLANK_DELTA(1*64);
		oldRotateStarfieldMatrix();

		oldStarfield();
	}
}

static inline int32_t Sqr(int32_t x)
{
	return x * x;
}

void showAboutScreen(void) // called once when about screen is opened
{
	if (ui.extendedPatternEditor)
		exitPatternEditorExtended();

	hideTopScreen();

	drawFramework(0, 0, 632, 173, FRAMEWORK_TYPE1);
	drawFramework(2, 2, 628, 169, FRAMEWORK_TYPE2);

	showPushButton(PB_EXIT_ABOUT);

	if (!config.useNewAboutScreen)
	{
		oldVector_t *s = oldStarPoints;

		const int32_t type = Random(4);
		switch (type)
		{
			// classic "space stars"
			case 0:
			{
				zSpeed = 309;
				for (int32_t i = 0; i < OLD_NUM_STARS; i++, s++)
				{
					s->z = (int16_t)Random(0xFFFF) - 0x8000;
					s->y = (int16_t)Random(0xFFFF) - 0x8000;
					s->x = (int16_t)Random(0xFFFF) - 0x8000;
				}
			}
			break;

			// galaxy
			case 1:
			{
				zSpeed = 0;
				for (int32_t i = 0; i < OLD_NUM_STARS; i++, s++)
				{
					if (i < OLD_NUM_STARS/4)
					{
						s->z = (int16_t)Random(0xFFFF) - 0x8000;
						s->y = (int16_t)Random(0xFFFF) - 0x8000;
						s->x = (int16_t)Random(0xFFFF) - 0x8000;
					}
					else
					{
						int32_t r = Random(30000);
						int32_t n = Random(5);
						int32_t w = ((2 * Random(2)) - 1) * Sqr(Random(1000));
						double ww = (((PI * 2.0) / 5.0) * n) + (r * (1.0 / 12000.0)) + (w * (1.0 / 3000000.0));
						int32_t h = ((Sqr(r) / 30000) * (Random(10000) - 5000)) / 12000;

						s->x = (int16_t)(r * cos(ww));
						s->y = (int16_t)(r * sin(ww));
						s->z = (int16_t)h;
					}
				}
			}
			break;

			// spiral
			case 2:
			case 3:
			{
				zSpeed = 0;
				for (int32_t i = 0; i < OLD_NUM_STARS; i++, s++)
				{
					int32_t r = (int32_t)round(sqrt(Random(500) * 500));
					int32_t w = Random(3000);
					double ww = ((w * 8) + r) * (1.0 / 16.0);

					const int16_t z =  (int16_t)round(32767.0 * cos(w  * (2.0 * PI / 1024.0)));
					const int16_t y =  (int16_t)round(32767.0 * sin(w  * (2.0 * PI / 1024.0)));
					const int16_t x = ((int16_t)round(32767.0 * cos(ww * (2.0 * PI / 1024.0)))) / 4;

					s->z = (int16_t)((z * (w + r)) / 3500);
					s->y = (int16_t)((y * (w + r)) / 3500);
					s->x = (int16_t)((x * r) / 500);
				}
			}
			break;

			default:
				break;
		}

		oldStarRotation.x = 0;
		oldStarRotation.y = 748;
		oldStarRotation.z = 200;

		for (int32_t i = 0; i < OLD_NUM_STARS; i++)
			lastStarScreenPos[i] = -1;

		blit(91, 31, bmp.ft2OldAboutLogo, 449, 111);
	}

	ui.aboutScreenShown = true;
}

void initAboutScreen(void)
{
	vector_t *s = starPoints;
	for (int32_t i = 0; i < NUM_STARS; i++, s++)
	{
		s->x = (float)((Random(INT32_MAX) - (INT32_MAX/2)) * (1.0 / INT32_MAX));
		s->y = (float)((Random(INT32_MAX) - (INT32_MAX/2)) * (1.0 / INT32_MAX));
		s->z = (float)((Random(INT32_MAX) - (INT32_MAX/2)) * (1.0 / INT32_MAX));
	}

	sinp1 = 0;
	sinp2 = SINUS_PHASES / 4; // cosine offset

	// pre-calc sinus table
	for (int32_t i = 0; i < SINUS_PHASES; i++)
		sin16[i] = (int16_t)round(32767.0 * sin(i * PI * 2.0 / SINUS_PHASES));

	sprintf(customText3, "v%s (%s)", PROG_VER_STR, __DATE__);
	customText0X = (SCREEN_W    - textWidth(customText0)) / 2;
	customText1X = (SCREEN_W    - textWidth(customText1)) / 2;
	customText2X = (SCREEN_W-8) - textWidth(customText2);
	customText3X = (SCREEN_W-8) - textWidth(customText3);
	customText0Y = 157-28;
	customText1Y = 157-12;
	customText2Y = 157-12;
	customText3Y = 157;
}

void hideAboutScreen(void)
{
	hidePushButton(PB_EXIT_ABOUT);
	ui.aboutScreenShown = false;
}

void exitAboutScreen(void)
{
	hideAboutScreen();
	showTopScreen(true);
}
