#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
using namespace fggl::input;
using InputManager = std::shared_ptr<fggl::input::Input>;

void process_arcball(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, InputManager 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);
	auto& mouse = input->mouse;

	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 = ( -mouse.axisDelta(fggl::input::MouseAxis::X) ) * deltaAngleX;
	float yAngle = ( -mouse.axisDelta(fggl::input::MouseAxis::Y) ) * 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::ecs::ECS& ecs, InputManager input, fggl::ecs::entity_t cam) {
	float rotationValue = 0.0f;
	glm::vec3 translation(0.0f);

	auto& keyboard = input->keyboard;
	auto code_q = glfwGetKeyScancode(GLFW_KEY_Q);
	auto code_e = glfwGetKeyScancode(GLFW_KEY_E);
	auto code_w = glfwGetKeyScancode(GLFW_KEY_W);
	auto code_s = glfwGetKeyScancode(GLFW_KEY_S);
	auto code_d = glfwGetKeyScancode(GLFW_KEY_D);
	auto code_a = glfwGetKeyScancode(GLFW_KEY_A);

	// calulate rotation (user input)
	if ( keyboard.down( code_q ) ) {
		rotationValue = ROT_SPEED; 
	} else if ( keyboard.down(code_e) ) {
		rotationValue = -ROT_SPEED;
	}

	// calulate movement (user input)
	if ( keyboard.down(code_w) ) {
		translation -= fggl::math::RIGHT;
	}

	if ( keyboard.down(code_s) ) {
		translation += fggl::math::RIGHT;
	}

	if ( keyboard.down(code_d) ) {
		translation += fggl::math::FORWARD;
	}

	if ( keyboard.down(code_a) ) {
		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;
}

void process_camera(fggl::gfx::Window& window, fggl::ecs::ECS& ecs, InputManager input, fggl::ecs::entity_t cam) {
	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 = input->mouse.axisDelta( fggl::input::MouseAxis::SCROLL_Y );
	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 );

	if ( cam_mode == cam_arcball || input->mouse.button( fggl::input::MouseButton::MIDDLE ) ) { 
		process_arcball(window, ecs, input, cam);
	} else if ( cam_mode == cam_free ) {
		process_freecam(ecs, input, cam);
	}
}

int main(int argc, char* argv[]) {
	// setup ECS
	fggl::ecs::ECS ecs;
	auto inputs = std::make_shared<fggl::input::Input>();

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

	fggl::gfx::Window win;
	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);
	}

	bool joystickWindow = true;
	bool gamepadWindow = true;

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

	while( !win.closeRequested() ) {

		//
		// Setup setup
		//
		time.tick( glfwGetTimerValue() );
		inputs->frame( time.delta() );

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

		//
		// update step
		// 
		process_camera(win, ecs, inputs, camEnt);

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

			bool present = gamepads.present(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::input::GamepadButtonsMicrosoft ) {
							ImGui::Text( "%s: %i %i %i", btn.name,
									gamepads.button(i, btn.id),
									gamepads.buttonPressed(i, btn.id),
									gamepads.buttonReleased(i, btn.id)
									);
						}
						ImGui::TreePop();
					}

					if ( ImGui::TreeNode("axes##2") ) {
						for ( auto& axis : fggl::input::GamepadAxes ) {
							ImGui::Text("%s: %f %f", axis.name,
									gamepads.axis(i, axis.id),
									gamepads.axisDelta(i, axis.id)
								   );

						}
						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;
}