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

#include <fggl/gfx/window.hpp>
#include <fggl/gfx/ogl.hpp>
#include <fggl/gfx/renderer.hpp>
#include <fggl/gfx/shader.hpp>
#include <fggl/gfx/camera.hpp>
#include <fggl/data/procedural.hpp>
#include <fggl/ecs/ecs.hpp>
#include <fggl/debug/debug.h>
#include <fggl/data/storage.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;
	}

}

//TODO proper input system
void process_camera(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, fggl::gfx::Input& input, fggl::ecs::entity_t cam) {
	auto camTransform = ecs.getComponent<fggl::math::Transform>(cam);
	auto camComp = ecs.getComponent<fggl::gfx::Camera>(cam);
	float moveSpeed = 1.0f;

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

	glm::vec3 motion(0.0f);
	if ( glfwGetKey(window.handle(), GLFW_KEY_W) == GLFW_PRESS ) {
		if ( glm::length( dir ) > 2.5f ) {
		  motion -= (forward * moveSpeed);
		}
	}
	if ( glfwGetKey(window.handle(), GLFW_KEY_S) == GLFW_PRESS ) {
		if ( glm::length( dir ) < 25.0f ) {
		  motion += (forward * moveSpeed);
		}
	}

	// scroll wheel
	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 );
}

int main(int argc, char* argv[]) {
	fggl::gfx::Context ctx;

	// build our main window
	fggl::gfx::Window win( fggl::gfx::Input::instance() );
	win.title("FGGL Demo");
	win.fullscreen( true );

	// opengl time
	fggl::gfx::Graphics ogl(win);
	fggl::gfx::MeshRenderer meshRenderer;

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

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

	fggl::gfx::ShaderCache cache(storage);

	fggl::gfx::ShaderConfig config;
	config.name = "unlit";
	config.vertex = "unlit_vert.glsl";
	config.fragment = "unlit_frag.glsl";
	auto shader = cache.load(config);

	fggl::gfx::ShaderConfig configPhong;
	configPhong.name = "phong";
	configPhong.vertex = configPhong.name + "_vert.glsl";
	configPhong.fragment = configPhong.name + "_frag.glsl";
//	configPhong.fragment = configPhong.name + "_normals_frag.glsl";
	auto shaderPhong = cache.load(configPhong);

	fggl::gfx::ShaderConfig configNormals;
	configNormals.name = "normals";
	configNormals.hasGeom = true;
	configNormals.vertex = configNormals.name + "_vert.glsl";
	configNormals.geometry = configNormals.name + "_geom.glsl";
	configNormals.fragment = configNormals.name + "_frag.glsl";
	auto shaderNormals = cache.load( configNormals );

	// create ECS
	fggl::ecs::ECS ecs;
	ecs.registerComponent<fggl::gfx::MeshToken>();
	ecs.registerComponent<fggl::gfx::Camera>();
	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);
	}

	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();
		auto token = meshRenderer.upload( mesh );
		token.pipeline = shaderPhong;
		ecs.addComponent<fggl::gfx::MeshToken>(entity, token);
	}

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

	bool joystickWindow = true;
	bool gamepadWindow = true;

	float time = 0.0f;
	float dt = 16.0f;
	while( !win.closeRequested() ) {
		input.frame();

		ctx.pollEvents();
		debug.frameStart();

		// update step
		time += dt;

		process_camera(win, ecs, input, camEnt);
		if ( input.mouseDown( fggl::gfx::MOUSE_2 ) ) {
			process_arcball(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();

/*		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
		ogl.clear();

		// render using real shader
		auto renderables = ecs.getEntityWith<fggl::gfx::MeshToken>();
		for ( auto renderable : renderables ) {
			auto token = ecs.getComponent<fggl::gfx::MeshToken>(renderable);
			token->pipeline = shaderPhong;
		}
		meshRenderer.render(win, ecs, camEnt, 16.0f);

		// render using normals shader
		if ( showNormals ) {
			for ( auto renderable : renderables ) {
				auto token = ecs.getComponent<fggl::gfx::MeshToken>(renderable);
				token->pipeline = shaderNormals;
			}
			meshRenderer.render(win, ecs, camEnt, 16.0f);
		}

		debug.draw();
		win.swap();
	}

	return 0;
}