////////////////////////////////////////////////////////////////////////////////
// LevelEd.cpp
// Simulation window for MechVM
// Copyright Bjoern Ganster
// Developed since 2020
////////////////////////////////////////////////////////////////////////////////

#include "LevelEd.h"

#include "Vehicles.h"
#include "Config.h"
#include "dialogs.h"
#include "intersections.h"
#include "MechVM.h"

#include "GL/glu.h"

#include <iostream>

#define FILE_NUMBER FILE_NO_LEVEL_ED_CPP 

////////////////////////////////////////////////////////////////////////////////

class LevelEdMesh : public Mesh {
public:
	Point3D translation;
	double rx, ry, rz;
	double sx, sy, sz;
	BGCPString wtbFN, objFN, textureFN, description;
	int health;

	// Constructor
	LevelEdMesh()
	: rx(0), 
	  ry(0), 
	  rz(0),
	  sx(0),
	  sy(0),
	  sz(0)
	{
	}

	// Destructor
	~LevelEdMesh() OVERRIDE
	{
	}

	void recalcMatrix()
	{
		SET_SCOPE;
		Matrix objMatrix = TranslationMatrix(translation.x, translation.y, translation.z);

		if (rx != 0) {
			objMatrix = objMatrix * RotationMatrix(M_PI * rx / 180.0, 1, 0, 0);
		}
		if (ry != 0) {
			objMatrix = objMatrix * RotationMatrix(M_PI * ry / 180.0, 0, 1, 0);
		}
		if (rz != 0) {
			objMatrix = objMatrix * RotationMatrix(M_PI * rz / 180.0, 0, 0, 1);
		}

		if (sx != 1 || sy != 1 || sz != 1) {
			objMatrix = objMatrix * ScaleMatrix(sx, sy, sz, 1);
		}

		setMatrix(objMatrix);
		RETURN;
	}
};

////////////////////////////////////////////////////////////////////////////////
// Constructor, destructor

LevelEd::LevelEd()
: GLWindow("LevelEd", LevelEd_ID),
  m_sceneObjects(new RenderableObject("LevelEd", RenderableObjectID)),
  returnVis(nullptr),
  selectedObject(0),
  FOVY(60.0),
  viewerSpeed(50.0),
  moveSpeed(1.0),
  rotationSpeed(10.0)
{
	SET_SCOPE;
	mViewerDir = Point3D(0, 0, 1);
	mViewerPos = Point3D(0.0, 0.0, 0.0);
	mRightDir = Point3D(1, 0, 0);
	mTopDir = Point3D(0, 1, 0);

	Config* config = getConfig();
	viewerSpeed = config->getDouble("LevelEd.viewerSpeed");
	moveSpeed = config->getDouble("LevelEd.moveSpeed");
	rotationSpeed = config->getDouble("LevelEd.rotationSpeed");
	RETURN;
}

LevelEd::~LevelEd()
{
}

////////////////////////////////////////////////////////////////////////////////
// Receive messages

void LevelEd::receiveMessage(VirtualClass* /*sender*/, int /*MessageType*/, VirtualClass* data)
{
}

////////////////////////////////////////////////////////////////////////////////
// Update title with model graph name and framerate

void LevelEd::updateTitle()
{
}

////////////////////////////////////////////////////////////////////////////////
// Render

void LevelEd::render(int x, int y, int w, int h)
{
	SET_SCOPE;

	// Remember width and height ...
	setWidth(w);
	setHeight(h);

	glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
	glViewport(x, y, w, h);

	// Activate light source
	glEnable(GL_LIGHTING);
	glEnable(GL_COLOR_MATERIAL);
	//glEnable (GL_TEXTURE_2D);
	//glFrontFace (GL_CCW);
	//glDisable (GL_LIGHT_MODEL_TWO_SIDE);
	//glCullFace (GL_BACK);
	//glEnable (GL_TEXTURE_2D);

	GLfloat pos[4] = { 0.5f, 1.0f, 0.250f, 0.0f };
	GLfloat color[3] = { 0.7f, 0.7f, 0.7f };
	GLfloat ambient[3] = { 0.3f, 0.3f, 0.3f };
	GLfloat k0 = 1.0;
	GLfloat k1 = 0.0;
	GLfloat k2 = 0.0;

	glEnable(GL_LIGHT0);
	glLightfv(GL_LIGHT0, GL_POSITION, pos);
	glLightfv(GL_LIGHT0, GL_AMBIENT, ambient);
	glLightfv(GL_LIGHT0, GL_DIFFUSE, color);
	glLightfv(GL_LIGHT0, GL_SPECULAR, color);
	glLightfv(GL_LIGHT0, GL_CONSTANT_ATTENUATION, &k0);
	glLightfv(GL_LIGHT0, GL_LINEAR_ATTENUATION, &k1);
	glLightfv(GL_LIGHT0, GL_QUADRATIC_ATTENUATION, &k2);

	// Set viewing projection
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(FOVY, ((double)w) / ((double)h), 10, 10000);
	gluLookAt(mViewerPos.x, mViewerPos.y, mViewerPos.z,
		mViewerPos.x + mViewerDir.x, mViewerPos.y + mViewerDir.y, mViewerPos.z + mViewerDir.z,
		0, 1, 0);
	glMatrixMode(GL_MODELVIEW);

	if (selectedObject < m_sceneObjects->getObjectCount()) {
		RenderableObject* object = m_sceneObjects->getObject(selectedObject);
		LevelEdMesh* mesh = dynamic_cast<LevelEdMesh*>(object);
		if (mesh != nullptr) {
			mesh->color = Color::random();
		}
	}

	// Render
	glLoadIdentity();
	glEnable(GL_DEPTH_TEST);
	glEnable(GL_NORMALIZE);
	glDisable(GL_TEXTURE_2D);

	glColor3d(1, 1, 1);
	if (m_sceneObjects != nullptr) {
		m_sceneObjects->render();
	}
	glDisable(GL_LIGHT0);
	glDisable(GL_LIGHTING);

	// Render selection point
	glColor3f(1.0, 1.0, 1.0);
	glPointSize(5.0);
	glBegin(GL_POINTS);
		selPoint.glVertex();
	glEnd();

	RETURN;
}

////////////////////////////////////////////////////////////////////////////////
// Keyboard event handler

void LevelEd::keypressed(BGKey key, bool shiftHeld, bool controlHeld, bool down, bool repeat)
{
	SET_SCOPE;
	if (!down) {
		RETURN;
	}

	if (key == 'q') {
		auto msgboxResult = messageBox("Update XML?", "Hit cancel to continue editing. Warning: the level editor does not "
			"support all tags supported by the simulation, so you may want to keep a backup, otherwise you may lose data.", 
			MB_YESNOCANCEL);
		if (msgboxResult == IDYES) {
			saveXml();
		}

		if (msgboxResult != IDCANCEL) {
			getMechVm()->setScreen(returnVis);
			getMechVm()->deleteLater(this);
		}
		RETURN;
	}

	if (key == 'a') {
		Matrix m = RotationMatrix(-M_PI * 5.0 / 180.0, 0, 1.0, 0);
		mViewerDir = m.Transform(mViewerDir);
		mRightDir = m.Transform(mRightDir);
		mViewerDir.unitLength();
	} else if (key == 'd') {
		Matrix m = RotationMatrix(M_PI * 5.0 / 180.0, 0, 1.0, 0);
		mViewerDir = m.Transform(mViewerDir);
		mRightDir = m.Transform(mRightDir);
		mViewerDir.unitLength();
	} else if (key == 'w') {
		mViewerPos = mViewerPos + viewerSpeed * mViewerDir;
	} else if (key == 's') {
		mViewerPos = mViewerPos - viewerSpeed * mViewerDir;
	} else if (key == 'm') {
		mViewerPos.y += viewerSpeed;
	} else if (key == 'n') {
		mViewerPos.y -= viewerSpeed;
	}

	SET_OP;
	size_t cc = m_sceneObjects->getObjectCount();
	if (cc > 0) {
		RenderableObject* object = m_sceneObjects->getObject(selectedObject % cc);
		LevelEdMesh* mesh = dynamic_cast<LevelEdMesh*>(object);
		if (key == '+') {
			SET_OP;
			if (selectedObject + 1 >= cc) {
				selectedObject = 0;
			} else {
				++selectedObject;
			}
			RenderableObject* object = m_sceneObjects->getObject(selectedObject % cc);
			mesh = dynamic_cast<LevelEdMesh*>(object);
			if (mesh != nullptr) {
				mesh->wtbFN.print();
				mesh->objFN.print();
				COUT << " selected" << std::endl;
			}
		}
		if (key == '-') {
			SET_OP;
			if (selectedObject > 0) {
				--selectedObject;
			} else { 
				selectedObject = cc-1;
			}
			RenderableObject* object = m_sceneObjects->getObject(selectedObject % cc);
			mesh = dynamic_cast<LevelEdMesh*>(object);
			if (mesh != nullptr) {
				mesh->wtbFN.print();
				mesh->objFN.print();
				COUT << " selected" << std::endl;
			}
		}

		if (mesh != nullptr) {
			if (key == 'j') {
				Point3D left = mViewerDir.crossMultiply(Point3D(0, 1, 0));
				if (shiftHeld) {
					mesh->translation -= 10.0 * moveSpeed * left;
				} else {
					mesh->translation -= moveSpeed * left;
				}
			}
			if (key == 'l') {
				Point3D left = mViewerDir.crossMultiply(Point3D(0, 1, 0));
				if (shiftHeld) {
					mesh->translation += 10.0 * moveSpeed * left;
				} else {
					mesh->translation += moveSpeed * left;
				}
			}
			if (key == 'k') {
				if (shiftHeld) {
					mesh->translation -= 10.0 * moveSpeed * mViewerDir;
				} else {
					mesh->translation -= moveSpeed * mViewerDir;
				}
			}
			if (key == 'i') {
				if (shiftHeld) {
					mesh->translation += 10.0 * moveSpeed * mViewerDir;
				} else {
					mesh->translation += moveSpeed * mViewerDir;
				}
			}
			if (key == 'u') {
				mesh->ry += rotationSpeed;
			}
			if (key == 'o') {
				mesh->ry -= rotationSpeed;
			}
			if (key == SDLK_BACKSPACE) {
				m_sceneObjects->removeObject(selectedObject % cc, true);
				mesh = nullptr;
			}

			if (mesh != nullptr) {
				mesh->recalcMatrix();
				mesh->color = Color::random();
			}
		}
	}

	RETURN;
}

////////////////////////////////////////////////////////////////////////////////

void LevelEd::addSceneObjectsRecursively(RenderableObject* container, const PathChar* levelDir, XMLTreeConstIterator& iter, const Matrix& m)
{
	SET_SCOPE;
	size_t childCount = iter.getChildCount();

	for (size_t i = 0; i < childCount; ++i) {
		iter.goToChild(i);
		if (iter.nodeNameEquals("transform")) {
			Matrix localMatrix = m;
			double tx = iter.getDoubleAttributeByName("tx");
			double ty = iter.getDoubleAttributeByName("ty");
			double tz = iter.getDoubleAttributeByName("tz");
			localMatrix = TranslationMatrix(tx, ty, tz) * localMatrix;
			double ry = iter.getDoubleAttributeByName("ry");
			if (fabs(ry) > 0.1) {
				localMatrix = RotationMatrix(M_PI * ry / 180.0, 0, 1, 0) * localMatrix;
			}
			double sx = iter.getDoubleAttributeByName("sx", 1.0);
			double sy = iter.getDoubleAttributeByName("sy", 1.0);
			double sz = iter.getDoubleAttributeByName("sz", 1.0);
			localMatrix = ScaleMatrix(sx, sy, sz) * localMatrix;
			RenderableObject* container = new LevelEdMesh();
			addSceneObjectsRecursively(container, levelDir, iter, localMatrix);
			m_sceneObjects->addChild(container);
			container->setMatrix(localMatrix);
		} else if (iter.nodeNameEquals("object")) {
			BGCPString wtbFN, objFN, fullObjFN;
			bool loaded = false;
			LevelEdMesh* mesh = new LevelEdMesh();
			if (iter.getAttributeValueByName("mw2prj", wtbFN)) {
				fullObjFN = BGCPString(levelDir, CP_SYSTEM);
				fullObjFN.addPathSep(wtbFN + BGCPString(".obj"));
				mesh->color = Color::random();
				loaded = mesh->loadFromObj(fullObjFN.getAsPath());
			} else {
				if (iter.getAttributeValueByName("obj", objFN)) {
					fullObjFN = getDataDir();
					fullObjFN.addPathSep("levels");
					fullObjFN.addPathSep(objFN);
					mesh->color = Color::random();
					loaded = mesh->loadFromObj(fullObjFN.getAsPath());
				}
			}

			if (loaded) {
				mesh->wtbFN = wtbFN;
				mesh->objFN = objFN;
				Point3D meshMin, meshMax;
				mesh->getMinCoords(meshMin);
				mesh->getMaxCoords(meshMax);
				//Point3D meshMin = mesh->getMinCoords();
				//Point3D meshMax = mesh->getMaxCoords();
				double dist = (meshMin - meshMax).length();
				mesh->translation.x = iter.getDoubleAttributeByName("tx");
				mesh->translation.y = iter.getDoubleAttributeByName("ty");
				mesh->translation.z = iter.getDoubleAttributeByName("tz");

				mesh->rx = iter.getDoubleAttributeByName("rx");
				mesh->ry = iter.getDoubleAttributeByName("ry");
				mesh->rz = iter.getDoubleAttributeByName("rz");

				mesh->sx = iter.getDoubleAttributeByName("sx", 1.0);
				mesh->sy = iter.getDoubleAttributeByName("sy", 1.0);
				mesh->sz = iter.getDoubleAttributeByName("sz", 1.0);

				mesh->health = (int)iter.getIntAttributeValueByName("health", 0);
				iter.getAttributeValueByName("description", mesh->description);

				mesh->recalcMatrix();

				BGCPString textureFN;
				if (iter.getAttributeValueByName("texture", textureFN)) {
					BGCPString fullTextureFN(getDataDir());
					fullTextureFN += OSPathSep;
					fullTextureFN += "levels";
					fullTextureFN += OSPathSep;
					fullTextureFN += textureFN;
					Texture* texture = new Texture;
					if (texture->loadFromFile(fullTextureFN.getAsPath())) {
						mesh->setTexture(texture);
						mesh->color = Color(1.0, 1.0, 1.0);
						mesh->textureFN = textureFN;
					} else {
						COUT << "Failed to load texture ";
						fullTextureFN.printLn();
						delete texture;
					}
				} else {
					mesh->color = Color::random();
				}


				if (dist < 500000) {
					container->addChild(mesh);
				} else {
					delete mesh;
					COUT << "Implausible mesh ";
					fullObjFN.printLn();
				}
			} else {
				delete mesh;
				COUT << "Failed to load mesh ";
				fullObjFN.printLn();
			}
		} else {
			if (iter.getChildCount() > 0) {
				addSceneObjectsRecursively(container, levelDir, iter, m);
			}
		}
		iter.goToParent();
	}

	RETURN;
}

void LevelEd::loadLevel(const PathChar* levelDir, const PathChar* xmlFN)
{
	SET_SCOPE;
	xmlFilename = getDataDir();
	xmlFilename.addPathSep();
	xmlFilename += BGCPString(xmlFN, CP_SYSTEM);

	XMLTree xml;
	Matrix m;
	if (xml.loadFromFile(xmlFilename.getAsPath())) {
		XMLTreeConstIterator iter(xml.getRoot());
		addSceneObjectsRecursively(m_sceneObjects, levelDir, iter, m);
	}

	RETURN;
}

void LevelEd::saveXml()
{
	SET_SCOPE;
	XMLTree xml;
	XMLTreeIterator iter(&xml, xml.getRoot());
	iter.addChild("BWD");
	iter.goToChild(0);

	for (size_t i = 0; i < m_sceneObjects->getObjectCount(); ++i) {
		LevelEdMesh* mesh = (LevelEdMesh*)(m_sceneObjects->getObject(i));
		size_t childNum = iter.addChild("object");
		iter.goToChild(childNum);
		if (!mesh->wtbFN.empty()) {
			iter.addAttribute("mw2prj", mesh->wtbFN);
		}
		if (!mesh->objFN.empty()) {
			iter.addAttribute("obj", mesh->objFN);
		}
		if (!mesh->textureFN.empty()) {
			iter.addAttribute("texture", mesh->textureFN);
		}

		if (mesh->translation.x != 0.0) {
			iter.addDoubleAttribute("tx", mesh->translation.x);
		}

		if (mesh->translation.y != 0.0) {
			iter.addDoubleAttribute("ty", mesh->translation.y);
		}

		if (mesh->translation.z != 0.0) {
			iter.addDoubleAttribute("tz", mesh->translation.z);
		}

		if (mesh->sx != 1.0) {
			iter.addDoubleAttribute("sx", mesh->sx);
		}

		if (mesh->sy != 1.0) {
			iter.addDoubleAttribute("sy", mesh->sy);
		}

		if (mesh->sz != 1.0) {
			iter.addDoubleAttribute("sz", mesh->sz);
		}

		if (mesh->rx != 0.0) {
			iter.addDoubleAttribute("rx", mesh->rx);
		}

		if (mesh->ry != 0.0) {
			iter.addDoubleAttribute("ry", mesh->ry);
		}

		if (mesh->rz != 0.0) {
			iter.addDoubleAttribute("rz", mesh->rz);
		}

		if (mesh->health > 0) {
			iter.addAttribute("health", BGCPString::fromInt(mesh->health));
		}

		if (!mesh->description.empty()) {
			iter.addAttribute("description", mesh->description);
		}

		iter.goToParent();
	}

	xml.saveToFile(xmlFilename.getAsPath(), CP_ASCII);
	RETURN;
}

GLWindow* LevelEd::buttonStateChanged (int button, bool down, int x, int y) 
{
	SET_SCOPE;
	if (down) {
		int w = getWidth();
		int h = getHeight();
		double tanFOVY = tan(FOVY * M_PI / 360.0f);
		double xval = 2.0 * tanFOVY * (0.5*w-x) / h;
		double aspect = ((double)w) / ((double)h);
		double yval = 2.0 * tanFOVY * (0.5*h-y) / h;
		Point3D mouseRay = mViewerDir + xval * mRightDir + yval * mTopDir;
		selPoint = mViewerPos + 12.0 * mouseRay;
		bool found = false;
		double previousDist = 0.0;

		// Intersect the ray with all objects in the scene ... bounding boxes may speed this up ...
		Ray ray(mViewerPos, mViewerPos + mouseRay);
		for (size_t i = 0; i < m_sceneObjects->getObjectCount(); ++i) {
			RenderableObject* object = m_sceneObjects->getObject(i);
			if (object != nullptr) {
				Point3D min, max;
				object->getMinCoords(min);
				object->getMaxCoords(max);
				//Point3D min = object->getMinCoords();
				//Point3D max = object->getMaxCoords();
				Point3D middle = 0.5 * max + 0.5 * min;
				double radius = (middle-min).length();

				double dist = ray.distanceTo(middle);
				if (dist < radius) {
					// Todo: check if we can actually find a polygon that intersects the ray ...
					// Use TraverseScene from intersections.h ...
					if (!found || dist < previousDist) {
						selectedObject = i;
						found = true;
						previousDist = dist;
					}
				}
			}
		}
	}

	RETURN nullptr;
}
