#include <iostream>
#include <raylib.h>
#include <filesystem>
#include <raymath.h>
#include <fstream>
#include <sstream>
#include <string>
#include "MPFW_Quake.h"
#include "MPFW_Console.h"
#include <print>
#include <rlgl.h>
#include <thread>
#include "MPFW_UI.h"
#include <algorithm>
#include <mutex>
#include "MPFW_Physics.h"


#define DISTANCE_FROM_FLOOR 15
#define DISTANCE_FROM_WALL 5

// turn quake miptex into rl texture
Texture2D RLMT_QUAKE(MPFW::Quake::Maps::rlMipTex rlmt) {
	Texture2D retval;


	retval.id = rlmt.glTextureID;
	retval.width = rlmt.width;
	retval.height = rlmt.height;
	retval.format = rlmt.type;
	
	return retval;
}


Camera camera = { 0 };

Vector3 rotation = { 1,0,0 };
Vector3 hRotation = { 1,0,0 };
Vector3 velocity = { 0,0,0 };
float accelFactor = 300.0f;

struct ConsoleSettings {
	int height;
};



void Look() {

	hRotation = Vector3RotateByAxisAngle(hRotation, { 0,1,0 }, -GetMouseDelta().x * GetFrameTime());
	rotation = Vector3RotateByAxisAngle(rotation, { 0,1,0 }, -GetMouseDelta().x * GetFrameTime());
	rotation = Vector3Normalize({ rotation.x, Clamp(rotation.y - GetMouseDelta().y * GetFrameTime(), -0.99, 0.99), rotation.z });
}


BoundingBox GenerateTriangleBoundingBox(Vector3 a, Vector3 b, Vector3 c) {
	BoundingBox retval;
	retval.min = {
		a.x < b.x && a.x < c.x ? a.x :
		b.x < a.x && b.x < c.x ? b.x : c.x,

		a.y < b.y && a.y < c.y ? a.y :
		b.y < a.y && b.y < c.y ? b.y : c.y,

		a.z < b.z && a.z < c.z ? a.z :
		b.z < a.z && b.z < c.z ? b.z : c.z
	};


	retval.max = {
		a.x > b.x && a.x > c.x ? a.x :
		b.x > a.x && b.x > c.x ? b.x : c.x,

		a.y > b.y && a.y > c.y ? a.y :
		b.y > a.y && b.y > c.y ? b.y : c.y,

		a.z > b.z && a.z > c.z ? a.z :
		b.z > a.z && b.z > c.z ? b.z : c.z
	};


	return retval;
}

BoundingBox GenerateBoundingBoxPolygon(std::vector<Vector3> poly) {
	std::vector<Vector3> polyCopy = poly;

	std::sort(polyCopy.begin(), polyCopy.end(), [](Vector3 a, Vector3 b) {
		return Vector3Distance({ 0,0,0 }, a) < Vector3Distance({ 0,0,0 }, b);
	});

	BoundingBox retval;
	retval.min = polyCopy[0];
	retval.max = polyCopy[polyCopy.size() - 1];
	return retval;
}

// because quake can't normalize vectors like a civilized piece of software

struct qNVec {
	Vector3 vectorReturn;
	float distance;
};

qNVec qNormalize(Vector3 v) {
	qNVec retval;
	retval.vectorReturn = Vector3Normalize(v);
	retval.distance = Vector3Length(v);
	return retval;
}

Vector3 wishDir = { 0,0,0 };
double wishSpeed;

float collisionFloor = 5;
void Accelerate() {

	double addSpeed, accelSpeed, currentSpeed;
	currentSpeed = Vector3DotProduct(velocity, wishDir);
	addSpeed = wishSpeed - currentSpeed;
	if(addSpeed > 0){
		accelSpeed = 10 * GetFrameTime() * wishSpeed;
		if (accelSpeed > addSpeed) accelSpeed = addSpeed;
		velocity = Vector3Add(velocity, wishDir * accelSpeed);
	}
}

void AirAccelerate(Vector3 wishVelocity) {
	double addSpeed, wishSpeedFunc, accelSpeed, currentSpeed;


	qNVec qnv = qNormalize(wishVelocity);
	wishSpeedFunc = qnv.distance;
	wishVelocity = qnv.vectorReturn;
	if (wishSpeedFunc > 30) wishSpeedFunc = 30;
	currentSpeed = Vector3DotProduct(velocity, wishVelocity);
	addSpeed = wishSpeedFunc - currentSpeed;
	if (addSpeed > 0) {
		accelSpeed = 10 * wishSpeed * GetFrameTime();
		if (accelSpeed > addSpeed) accelSpeed = addSpeed;
		velocity = Vector3Add(velocity, wishVelocity * accelSpeed);
	}

}

#ifdef _DEBUG

template <typename T>
void ___test___(T a, T b) {
	if (a != b) throw;
}

#endif


bool consoleOn = false;


// #define SOLIPSE_TEST
int main() {


#ifdef _DEBUG

	std::print("COMPILED IN DEBUG MODE\n----------------------\n\n");

	std::print("Unit testing\n\n\n");

	{ // UT01 : formatting a quake map directory entry
		std::print("UT01 : formatting a quake map directory entry\n");
		MPFW::Quake::Maps::DirectoryEntry dirent;

		dirent.offset = 1000;
		dirent.size = 2000;

		std::string retval = std::format("{}", dirent);
		std::print("{}", retval);
		___test___(retval, std::string("([MPFW::Quake::Maps::DirectoryEntry]: at pos 1000, 2000B in size)"));

		std::println();
		std::print("UT01 Success\n");

	}

	std::print("\nEnd of unit testing\n\n\n");
#endif




	// if not already created...
	std::filesystem::create_directories("data/cache/collisiondata");

	MPFW::Quake::Maps::MapFile map;
	
	map.pal = new MPFW::Quake::Maps::Palette("data/palette.lmp");


	MPFW::Physics::CollisionMap collisionMap; // map of the collisions

	map.collMap = &collisionMap;



	SetConfigFlags(FLAG_WINDOW_RESIZABLE);
	
	InitWindow(1280, 720, TextFormat("mpfw"));
	SetTargetFPS(60);
	SetExitKey(0);
	DisableCursor();

	MPFW::UI::UIRenderer uiRenderer;
	MPFW::Console::CommandHandlerResources chr;
	chr.mapQuake = &map;
	chr.ui = &uiRenderer;

	MPFW::Console::CommandHandler cmdH(&chr);

	Font robotoMonoRegular = LoadFontEx("data/fonts/RobotoMono/RobotoMono-Regular.ttf", 30, NULL, 6000);
	int robotoMonoHeight = MeasureTextEx(robotoMonoRegular, "X", 30, 0).y;


	cmdH.RunScript("data/cfg/startup.cfg");
	
	uiRenderer.cmh = &cmdH;

	camera.position = { 0,5,0 };
	camera.target = { 1,0,0 };
	camera.fovy = 120;
	camera.up = { 0,1,0 };
	camera.projection = CAMERA_PERSPECTIVE;

	bool indivFaceMode = false;
	int indivFace = 0;
	std::string cmdBuf = "";
	int framebuffer = 0; // buffer of frames, not a frame buffer
	
	
	while (!WindowShouldClose()) {
		

		if (framebuffer < 5) {

			if(!uiRenderer.showCursorOnLaunch) DisableCursor();
			framebuffer++;
		}

		if (IsKeyPressed(KEY_APOSTROPHE)) {
			consoleOn = !consoleOn;
		}
		

		if (IsKeyPressed(KEY_ESCAPE) && !chr.gameDisabled) {
			if (IsCursorHidden()) {
				EnableCursor();
				uiRenderer.rendererIsActive = true;
			}
			else { 
				DisableCursor(); 
				uiRenderer.rendererIsActive = false;
			}
		}

		BeginDrawing();
		ClearBackground(BLACK);

		if(IsCursorHidden() && !consoleOn){
			if(framebuffer >= 5){
				if (chr.cvars["math_3d_floor_collision_work"] == "true") {
					float potentialNewFloor = -FLT_MAX;
					for (int b = 0; b < collisionMap.collisionSets.size(); b++) {
						BoundingBox bb;
						bb.min = collisionMap.collisionSets[b].size.min;
						bb.max = collisionMap.collisionSets[b].size.max;
						if (CheckCollisionBoxSphere(bb, camera.position, 10)) {
							MPFW::Physics::CollisionSet cs = collisionMap.collisionSets[b];
							for (int i = 0; i < cs.collisionFaces.size(); i++) {
								for (int t = 2; t < cs.collisionFaces[i].polygon.size(); t++) {
									Vector3 a = cs.collisionFaces[i].polygon[0];
									Vector3 b = cs.collisionFaces[i].polygon[t];
									Vector3 c = cs.collisionFaces[i].polygon[t - 1];
									Ray r;
									r.position = camera.position;
									r.direction = { 0,-1,0 };
									RayCollision coll = GetRayCollisionTriangle(r, a, b, c);
									if (coll.hit) {
										if (coll.point.y > potentialNewFloor) potentialNewFloor = coll.point.y;
									}
								}
							}
						}

					}
					if (potentialNewFloor != -FLT_MAX) collisionFloor = potentialNewFloor + DISTANCE_FROM_FLOOR;
					else collisionFloor = camera.position.y - DISTANCE_FROM_FLOOR;
				}
			}
			Look();
			Vector3 wishVel = { 0,0,0 };
			wishSpeed = 320;


			if (IsKeyDown(KEY_W)) {
				wishVel += hRotation;
			}
			if (IsKeyDown(KEY_S)) {
				wishVel -= hRotation;
			}
			if (IsKeyDown(KEY_A)) {
				wishVel += Vector3RotateByAxisAngle(hRotation, { 0,1,0 }, 90 * DEG2RAD);
			}
			if (IsKeyDown(KEY_D)) {
				wishVel += Vector3RotateByAxisAngle(hRotation, { 0,1,0 }, -90 * DEG2RAD);
			}

			wishVel = Vector3Multiply(Vector3Normalize(wishVel), { 320, 320, 320 });

			wishDir = Vector3Normalize(wishVel);

			if (camera.position.y <= collisionFloor) {
				camera.position.y = collisionFloor;
				velocity.y = 0;
				if (IsKeyPressed(KEY_SPACE)) {
					velocity.y += wishSpeed * 0.5 * GetFrameTime();
				}
				Accelerate();

			}
			else {
				AirAccelerate(wishVel);
				velocity.y -= 10 * GetFrameTime();
			}

			velocity *= {GetFrameTime(), 1, GetFrameTime()};
			
			if (chr.cvars["math_3d_collision_work"] == "true") {
				for (int b = 0; b < collisionMap.collisionSets.size(); b++) {
					BoundingBox bb;
					bb.min = collisionMap.collisionSets[b].size.min;
					bb.max = collisionMap.collisionSets[b].size.max;
					if (CheckCollisionBoxSphere(bb, camera.position, 10)) {
						MPFW::Physics::CollisionSet cs = collisionMap.collisionSets[b];
						for (int i = 0; i < collisionMap.collisionSets[b].collisionFaces.size(); i++) {

							Ray x1, x2, z1, z2;
							x1.position = camera.position + velocity;
							x2.position = camera.position + velocity;
							z1.position = camera.position + velocity;
							z2.position = camera.position + velocity;
							x1.direction = { -1, 0, 0 };
							x2.direction = { 1, 0, 0 };
							z1.direction = { 0, 0, -1 };
							z2.direction = { 0, 0, 1 };

							
							for (int t = 2; t < cs.collisionFaces[i].polygon.size(); t++) {
								Vector3 a = cs.collisionFaces[i].polygon[0];
								Vector3 b = cs.collisionFaces[i].polygon[t];
								Vector3 c = cs.collisionFaces[i].polygon[t - 1];
								RayCollision cx1, cx2, cz1, cz2;


								cx1 = GetRayCollisionTriangle(x1, a, b, c);
								cx2 = GetRayCollisionTriangle(x2, a, b, c);
								cz1 = GetRayCollisionTriangle(z1, a, b, c);
								cz2 = GetRayCollisionTriangle(z2, a, b, c);


								if (cx1.hit) {
									if (cx1.distance < DISTANCE_FROM_WALL) velocity.x += DISTANCE_FROM_WALL - cx1.distance;
								}

								if (cx2.hit) {
									if (cx2.distance < DISTANCE_FROM_WALL) velocity.x -= DISTANCE_FROM_WALL - cx2.distance;
								}

								if (cz1.hit) {
									if (cz1.distance < DISTANCE_FROM_WALL) velocity.z += DISTANCE_FROM_WALL - cz1.distance;
								}

								if (cz2.hit) {
									if (cz2.distance < DISTANCE_FROM_WALL) velocity.z -= DISTANCE_FROM_WALL - cz2.distance;
								}

							}
							





						}
					}
				}
			}


			camera.position += velocity;
			camera.target = camera.position + rotation;

			
			
		}
		


		if (chr.cvars["vid_2d_background"] == "true") DrawRectangleGradientV(0, 0, GetScreenWidth(), GetScreenHeight(), WHITE, LIGHTGRAY);
		
		if (chr.cvars["vid_3d_renderer"] == "true") {
			
			  // Grab booleans once at the start of rendering instead of continuously grabbing them
			 // 66->536 FPS on Quake start.bsp
			// 500->8000 on test pre-lightmapping

			bool cullSky = chr.cvars["vid_3d_cull_sky"] == "true"; // if texture name starts by "sky"
			bool cullClips = chr.cvars["vid_3d_cull_clips"] == "true"; // if texture name == "clip"
			bool cullTriggers = chr.cvars["vid_3d_cull_triggers"] == "true"; // if texture name == "trigger"

			BeginMode3D(camera);
			if(chr.cvars["vid_3d_grid"] == "true") DrawGrid(10000, 10);
			for (int i = 0; i < map.data.renderFaces.size(); i++) {

				// verify culling parameters before rendering
				if (
					!(cullSky && map.data.renderFaces[i].skyFace) &&
					!(cullClips && map.data.renderFaces[i].clipFace) &&
					!(cullTriggers && map.data.renderFaces[i].triggerFace)
				){

					rlColor4ub(
						255,//  - (unsigned char)map.data.renderFaces[i].baselight, 
						255,//  - (unsigned char)map.data.renderFaces[i].baselight,
						255,//  - (unsigned char)map.data.renderFaces[i].baselight,
						255);

					rlSetTexture(map.data.renderFaces[i].glTextureId);
					
					
					for (int j = 1; j < map.data.renderFaces[i].vertices.size(); j++) {
						rlBegin(RL_QUADS); // for texturing reasons because rlgl
						Vector3 a = map.data.renderFaces[i].vertices[0], 
								b = map.data.renderFaces[i].vertices[j], 
								c = map.data.renderFaces[i].vertices[j - 1];

						Vector2 at = map.data.renderFaces[i].texCoords[0], 
								bt = map.data.renderFaces[i].texCoords[j], 
								ct = map.data.renderFaces[i].texCoords[j - 1];



						rlTexCoord2f(at.x, at.y);
						rlVertex3f(a.x, a.y, a.z);
						rlTexCoord2f(bt.x, bt.y);
						rlVertex3f(b.x, b.y, b.z);
						rlTexCoord2f(ct.x, ct.y);
						rlVertex3f(c.x, c.y, c.z);
						rlVertex3f(c.x, c.y, c.z); // why does RLGL fuck up triangles? is there something i'm missing?


						rlEnd();
					}

					if (map.data.renderFaces[i].hasLightMap) {
						rlColor4f(1, 1, 1, 0.5);
						rlSetTexture(map.data.renderFaces[i].glLightmapTextureId);
						for (int j = 1; j < map.data.renderFaces[i].vertices.size(); j++) {
							rlBegin(RL_QUADS); // for texturing reasons because rlgl
							Vector3 a = map.data.renderFaces[i].vertices[0],
								b = map.data.renderFaces[i].vertices[j],
								c = map.data.renderFaces[i].vertices[j - 1];

							Vector2 at = map.data.renderFaces[i].texCoords[0],
								bt = map.data.renderFaces[i].texCoords[j],
								ct = map.data.renderFaces[i].texCoords[j - 1];



							rlTexCoord2f(at.x, at.y);
							rlVertex3f(a.x, a.y, a.z);
							rlTexCoord2f(bt.x, bt.y);
							rlVertex3f(b.x, b.y, b.z);
							rlTexCoord2f(ct.x, ct.y);
							rlVertex3f(c.x, c.y, c.z);
							rlVertex3f(c.x, c.y, c.z); // why does RLGL screw up triangles? is there something i'm missing?


							rlEnd();
						}
					}
				}


			}
			EndMode3D();
		}



		DrawFPS(0, 0); // draw fps behind console for readability

		if (consoleOn) {
			DrawRectangle(0, 0, GetScreenWidth(), GetScreenHeight() / 2, {0,0,0,200});
			DrawTextEx(robotoMonoRegular, cmdH.logStr.c_str(), { 0, GetScreenHeight() / 2 - MeasureTextEx(robotoMonoRegular, cmdH.logStr.c_str(), 20, 0).y }, 20, 0, WHITE);
			
			DrawRectangle(0, GetScreenHeight() / 2, GetScreenWidth(), 20, { 0,0,0,200 });
			DrawLine(0, GetScreenHeight() / 2, GetScreenWidth(), GetScreenHeight() / 2, GOLD);
			DrawTextEx(robotoMonoRegular, cmdBuf.c_str(), { 0, (float)GetScreenHeight() / 2 }, 20, 0, GRAY);
			
			int key = GetCharPressed();
			while (key > 0) {
				if ((key >= 32) && (key <= 126)) {
					cmdBuf += (char)key;

				}
				key = GetCharPressed();
			}
			if (IsKeyPressed(KEY_BACKSPACE) && cmdBuf != "") cmdBuf.pop_back();

			if (IsKeyPressed(KEY_ENTER)) {
				cmdH.Run(cmdBuf);
				cmdBuf = "";
			}

		}
		uiRenderer.Render();

		EndDrawing();
	}

	CloseWindow();
}
