#include <filesystem>
#include <glm/ext/matrix_transform.hpp>
#include <glm/geometric.hpp>
#include <glm/trigonometric.hpp>
#include <iostream>

#include <fggl/gfx/window.hpp>
#include <fggl/gfx/camera.hpp>

#include <fggl/gfx/compat.hpp>
#include <fggl/gfx/ogl/compat.hpp>

#include <fggl/data/procedural.hpp>
#include <fggl/ecs/ecs.hpp>
#include <fggl/debug/debug.h>
#include <fggl/data/storage.hpp>
#include <fggl/util/chrono.hpp>

#include <imgui.h>

constexpr bool showNormals = false;

template <typename T> int sgn(T val) {
    return (T(0) < val) - (val < T(0));
}

// prototype of resource discovery
void discover(std::filesystem::path base) {

	std::vector< std::filesystem::path > contentPacks;
	for ( auto& item : std::filesystem::directory_iterator(base) ) {

		// content pack detection
		if ( std::filesystem::is_directory( item ) ) {
			auto manifest = item.path() / "manifest.yml";
			if ( std::filesystem::exists( manifest ) ) {
				contentPacks.push_back( item.path() );
			}
		}
		
	}

	// what did we find?
	std::cerr << "found pack(s): " << std::endl;
	for ( auto& pack : contentPacks ) {
		std::cerr << pack << std::endl;
	}

}

enum camera_type { cam_free, cam_arcball };
camera_type cam_mode = cam_free;

//TODO proper input system
void process_camera(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, fggl::gfx::Input& input, fggl::ecs::entity_t cam) {

	if ( glfwGetKey(window.handle(), GLFW_KEY_F2) == GLFW_PRESS ) {
		cam_mode = cam_free; 
	}
	if ( glfwGetKey(window.handle(), GLFW_KEY_F3) == GLFW_PRESS ) {
		cam_mode = cam_arcball;
	}

	auto camTransform = ecs.getComponent<fggl::math::Transform>(cam);
	auto camComp = ecs.getComponent<fggl::gfx::Camera>(cam);

	const glm::vec3 dir = ( camTransform->origin() - camComp->target );
	const glm::vec3 forward = glm::normalize( dir );

	// scroll wheel
	glm::vec3 motion(0.0f);
	float delta = (float)input.scrollDeltaY();
	if ( (glm::length( dir ) < 25.0f && delta < 0.0f) || (glm::length( dir ) > 2.5f && delta > 0.0f) )
		motion -= (forward * delta);

	camTransform->origin( camTransform->origin() + motion );
}

void process_arcball(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, fggl::gfx::Input& input, fggl::ecs::entity_t cam) {
	// see https://asliceofrendering.com/camera/2019/11/30/ArcballCamera/
	auto camTransform = ecs.getComponent<fggl::math::Transform>(cam);
	auto camComp = ecs.getComponent<fggl::gfx::Camera>(cam);

	glm::vec4 position(camTransform->origin(), 1.0f);
	glm::vec4 pivot(camComp->target, 1.0f);
	glm::mat4 view = glm::lookAt( camTransform->origin(), camComp->target, camTransform->up() );
	glm::vec3 viewDir = -glm::transpose(view)[2];
	glm::vec3 rightDir = glm::transpose(view)[0];

	float deltaAngleX = ( 2 * M_PI / window.width() ); 
	float deltaAngleY = ( M_PI / window.height() );
	float xAngle = ( input.cursorDeltaX() ) * deltaAngleX;
	float yAngle = ( input.cursorDeltaY() ) * deltaAngleY;

	auto cosAngle = glm::dot( viewDir, fggl::math::UP );
	if ( cosAngle * sgn(deltaAngleY) > 0.99f ) {
		deltaAngleY = 0;
	}

	// rotate the camera around the pivot on the first axis
	glm::mat4x4 rotationMatrixX(1.0f);
	rotationMatrixX = glm::rotate( rotationMatrixX, xAngle, fggl::math::UP );
	position = ( rotationMatrixX * ( position - pivot ) ) + pivot;

	// rotate the camera aroud the pivot on the second axis
	glm::mat4x4 rotationMatrixY(1.0f);
	rotationMatrixY = glm::rotate(rotationMatrixY, yAngle, rightDir );
	glm::vec3 finalPos = ( rotationMatrixY * ( position - pivot ) ) + pivot;

	camTransform->origin( finalPos );
}

constexpr float ROT_SPEED = 0.05f;
constexpr float PAN_SPEED = 0.05f;
constexpr glm::mat4 MAT_IDENTITY(1.0f);

void process_freecam(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, fggl::gfx::Input& input, fggl::ecs::entity_t cam) {
	float rotationValue = 0.0f;
	glm::vec3 translation(0.0f);

	// calulate rotation (user input)
	if ( glfwGetKey(window.handle(), GLFW_KEY_Q) == GLFW_PRESS ) {
		rotationValue = ROT_SPEED; 
	} else if ( glfwGetKey(window.handle(), GLFW_KEY_E) == GLFW_PRESS ) {
		rotationValue = -ROT_SPEED;
	}

	// calulate movement (user input)
	if ( glfwGetKey(window.handle(), GLFW_KEY_W) == GLFW_PRESS ) {
		translation -= fggl::math::RIGHT;
	}

	if ( glfwGetKey(window.handle(), GLFW_KEY_S) == GLFW_PRESS ) {
		translation += fggl::math::RIGHT;
	}

	if ( glfwGetKey(window.handle(), GLFW_KEY_D) == GLFW_PRESS ) {
		translation += fggl::math::FORWARD;
	}

	if ( glfwGetKey(window.handle(), GLFW_KEY_A) == GLFW_PRESS ) {
		translation -= fggl::math::FORWARD;
	}

	// apply rotation/movement
	auto camTransform = ecs.getComponent<fggl::math::Transform>(cam);
	auto camComp = ecs.getComponent<fggl::gfx::Camera>(cam);

	glm::vec4 position( camTransform->origin(), 1.0f );
	glm::vec4 pivot( camComp->target, 1.0f );

	// apply movement
	if ( translation != glm::vec3(0.0f) ) {
		const auto rotation = (position - pivot);
		const float angle = atan2( rotation.x, rotation.z );
		const auto rotationMat = glm::rotate( MAT_IDENTITY, angle, fggl::math::UP );

		auto deltaMove = (rotationMat * glm::vec4( translation, 1.0f )) * PAN_SPEED;
		deltaMove.w = 0.0f;

		position += deltaMove;
		pivot += deltaMove;
	}

	// apply rotation
	if ( rotationValue != 0.0f ) {
		glm::mat4 rotation = glm::rotate( MAT_IDENTITY, rotationValue, fggl::math::UP );
		position = ( rotation * ( position - pivot ) ) + pivot;
	}

	camTransform->origin( position );
	camComp->target = pivot;
}

int main(int argc, char* argv[]) {
	// setup ECS
	fggl::ecs::ECS ecs;

	// build our main window
	auto glfwModule = fggl::gfx::ecsInitGlfw(ecs);

	fggl::gfx::Window win( fggl::gfx::Input::instance() );
	win.title("FGGL Demo");
	win.fullscreen( true );

	// storage API
	fggl::data::Storage storage;
	discover( storage.resolvePath(fggl::data::Data, "res") );

	// Opengl APIs
	auto glModule = fggl::gfx::ecsInitOpenGL(ecs, win, storage);
	fggl::gfx::loadPipeline(glModule, "unlit", false);
	fggl::gfx::loadPipeline(glModule, "phong", false);
	fggl::gfx::loadPipeline(glModule, "normals", false);

	// debug layer
	fggl::debug::DebugUI debug(win);
	debug.visible(true);

	// create ECS
	ecs.registerComponent<fggl::math::Transform>();

	// make camera
	auto camEnt = ecs.createEntity();
	{
		auto cameraTf = ecs.addComponent<fggl::math::Transform>(camEnt);
		cameraTf->origin( glm::vec3(0.0f, 3.0f, 3.0f) );

		ecs.addComponent<fggl::gfx::Camera>(camEnt);
	}

	auto floorEnt = ecs.createEntity();
	{
		ecs.addComponent<fggl::math::Transform>( floorEnt );
		fggl::data::Mesh mesh = fggl::data::make_quad_xz();

		ecs.addComponent<fggl::gfx::StaticMesh>(floorEnt, mesh, "phong");
		fggl::gfx::onStaticMeshAdded(ecs, floorEnt, glModule);
	}

	int nCubes = 3;
	int nSections = 2;

	constexpr float HALF_PI = M_PI / 2.0f;

	for ( int i=0; i<nCubes; i++ ) {
		auto entity = ecs.createEntity();

		// set the position
		auto result = ecs.addComponent<fggl::math::Transform>(entity);
		result->origin( glm::vec3( i * 5.0f, 0.0f, 0.0f) );

		fggl::data::Mesh mesh;
		for (int i=-(nSections/2); i<=nSections/2; i++) {
			const auto shapeOffset = glm::vec3( 0.0f, 0.0f, i * 1.0f );

			const auto cubeMat = glm::translate( fggl::math::mat4( 1.0f ) , shapeOffset );
			const auto leftSlope = fggl::math::modelMatrix( 
					glm::vec3(-1.0f, 0.0f, 0.0f) + shapeOffset,
					glm::vec3( 0.0f, -HALF_PI, 0.0f) );
			const auto rightSlope = fggl::math::modelMatrix( 
					glm::vec3( 1.0f, 0.0f, 0.0f) + shapeOffset,
					glm::vec3( 0.0f, HALF_PI, 0.0f) );

			fggl::data::make_cube( mesh, cubeMat );
			fggl::data::make_slope( mesh, leftSlope );
			fggl::data::make_slope( mesh, rightSlope );
		}
		mesh.removeDups();
		ecs.addComponent<fggl::gfx::StaticMesh>(entity, mesh, "phong");

		// pretend we have callbacks
		fggl::gfx::onStaticMeshAdded(ecs, entity, glModule);
	}

	fggl::gfx::Input& input = fggl::gfx::Input::instance();


	bool joystickWindow = true;
	bool gamepadWindow = true;

	fggl::util::Timer time;
	time.frequency( glfwGetTimerFrequency() );
	time.setup( glfwGetTimerValue() );

	while( !win.closeRequested() ) {

		//
		// Setup setup
		//
		time.tick( glfwGetTimerValue() );

		input.frame();

		glfwModule->context.pollEvents();
		debug.frameStart();

		//
		// update step
		// 
		process_camera(win, ecs, input, camEnt);
		if ( cam_mode == cam_arcball ) { 
			if ( input.mouseDown( fggl::gfx::MOUSE_2 ) ) {
				process_arcball(win, ecs, input, camEnt);
			}
		} else if ( cam_mode == cam_free ) {
			process_freecam(win, ecs, input, camEnt);
		}

		// imgui joystick debug
		ImGui::Begin("Joysticks", &joystickWindow);
		for ( int i=0; i<16; i++ ) {
			bool present = input.hasJoystick(i);
			std::string title = "Joystick " + std::to_string(i);

			if ( ImGui::TreeNode(title.c_str()) ) {
				ImGui::Text("present: %s", present ? "yes" : "no" );

				if ( present ) {
					const fggl::gfx::Joystick& joyData = input.joystick(i);
					ImGui::Text( "%s", joyData.name );
					ImGui::Text( "gamepad: %s", joyData.gamepad ? "yes" : "no" );
					ImGui::Text( "axes: %d, buttons: %d, hats: %d",
							joyData.axisCount,
							joyData.buttonCount,
							joyData.hatCount);

					if (ImGui::TreeNode("axis##2")) {
						// dump data
						for ( int axid = 0; axid < joyData.axisCount; axid++ ) {
							ImGui::Text("axis %d, value: %f", 
									axid, joyData.axes[axid] );
						}
						ImGui::TreePop();
					}

					if (ImGui::TreeNode("buttons##2")) {
						// dump data
						for ( int btnid = 0; btnid < joyData.buttonCount; btnid++ ) {
							ImGui::Text("button %d, value: %s", btnid,
									joyData.buttons[btnid] == GLFW_PRESS
									? "down" : "up" );
						}
						ImGui::TreePop();
					}

					if (ImGui::TreeNode("hats##2")) {
						// dump data
						for ( int btnid = 0; btnid < joyData.hatCount; btnid++ ) {
							ImGui::Text("button %d, value: %d",
									btnid, joyData.hats[btnid] );
						}
						ImGui::TreePop();
					}
				}

				ImGui::TreePop();
				ImGui::Separator();
			}

		}
		ImGui::End();

		// imgui gamepad debug
		ImGui::Begin("GamePad", &gamepadWindow);
		for ( int i=0; i<16; i++ ) {
			std::string title = "GamePad " + std::to_string(i);

			bool present = input.hasJoystick(i);
			if ( ImGui::TreeNode(title.c_str()) ) {
				ImGui::Text("present: %s", present ? "yes" : "no" );

				if ( present ) {

					if ( ImGui::TreeNode("buttons##2") ) {
						for ( auto& btn : fggl::gfx::PadButtons ) {
							auto label = fggl::gfx::PadButtonLabels[ (int) btn ];
							ImGui::Text( "%s: %i %i %i", label.c_str(), 
									input.padDown(i, btn),
									input.padPressed(i, btn),
									input.padReleased(i, btn)
									);
						}
						ImGui::TreePop();
					}

					if ( ImGui::TreeNode("axes##2") ) {
						for ( auto& axis : fggl::gfx::PadAxes ) {
							auto label = fggl::gfx::PadAxisLabels[ (int) axis ];
							ImGui::Text("%s: %f %f", label.c_str(),
									input.padAxis(i, axis),
									input.padAxisDelta(i, axis)
								   );

						}
						ImGui::TreePop();
					}

				}

				ImGui::TreePop();
				ImGui::Separator();
			}

		}
		ImGui::End();

		debug.showDemo();

/*		float amount = glm::radians( time / 2048.0f * 360.0f );
		auto spinners = ecs.getEntityWith<fggl::math::Transform>();
		for ( auto entity : spinners ) {
			auto transform = ecs.getComponent<fggl::math::Transform>(entity);
			transform->euler(glm::vec3(0.0f, amount, 0.0f));
		}*/

		//
		// render step
		// 
		win.activate();
		glModule->ogl.clear();

		// model rendering system
		fggl::gfx::renderMeshes(glModule, ecs, time.delta());
		debug.draw();

		// swap the windows - frame rendering over
		win.swap();
	}

	return 0;
}