package com.mygdx.game;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;

import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Buttons;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.VertexAttributes.Usage;
import com.badlogic.gdx.graphics.VertexAttributes;
import com.badlogic.gdx.graphics.VertexAttribute;
import com.badlogic.gdx.graphics.g3d.Environment;
import com.badlogic.gdx.graphics.g3d.Model;
import com.badlogic.gdx.graphics.g3d.ModelBatch;
import com.badlogic.gdx.graphics.g3d.ModelInstance;
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
import com.badlogic.gdx.graphics.g3d.environment.DirectionalLight;
import com.badlogic.gdx.graphics.g3d.Material;
import com.badlogic.gdx.graphics.g3d.utils.CameraInputController;
import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder;
import com.badlogic.gdx.graphics.g3d.utils.MeshPartBuilder;

import com.badlogic.gdx.graphics.glutils.VertexArray;
import com.badlogic.gdx.graphics.glutils.IndexArray;
import com.badlogic.gdx.graphics.Mesh;
import com.badlogic.gdx.graphics.g3d.model.data.ModelData;
import com.badlogic.gdx.math.collision.Ray;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.graphics.g3d.shaders.DefaultShader;
import com.badlogic.gdx.utils.viewport.FillViewport;

import com.badlogic.gdx.math.*;

import java.util.*;
import java.util.stream.*;
import java.io.*;
import java.lang.Math.*;

public class MyGdxGame implements ApplicationListener, InputProcessor {
	SpriteBatch batch;
	Texture img;

	PerspectiveCamera cam;
	ModelBatch modelBatch;
	Model model;
	List<ModelInstance> instances;
	Environment environment;
	FillViewport viewport;

	ArrayList<Vector3> primalVertices;
	ArrayList<Integer> primalTriangles;

	ArrayList<Vector3> dualVertices;
	ArrayList<Integer> dualTriangles;
	ArrayList<ArrayList<Integer>> adjList;

	ArrayList<Integer> primalDual;

	String vertexShader;
	String fragmentShader;
	ShaderProgram shaderProgram;
	DefaultShader shader;
	DirectionalLight light;

	Mesh mesh;

	int oldMouseX, oldMouseY;
	int touchDownX, touchDownY;

	int pickedTile = -1;

	void makeIcoSphere(int subdivs, ArrayList<Vector3> vertices, ArrayList<Integer> faces)
	{
		float X=.525731112119133606f;
		float Z=.850650808352039932f;
		float N=0.f;

		ArrayList<Vector3> vertexList = new ArrayList<Vector3>(Arrays.asList(
					new Vector3(-X,N,Z), new Vector3(X,N,Z), new Vector3(-X,N,-Z), new Vector3(X,N,-Z),
					new Vector3(N,Z,X), new Vector3(N,Z,-X), new Vector3(N,-Z,X), new Vector3(N,-Z,-X),
					new Vector3(Z,X,N), new Vector3(-Z,X,N), new Vector3(Z,-X,N), new Vector3(-Z,-X, N)
					));

		ArrayList<Integer> triangleList = new ArrayList<Integer>(Arrays.asList(
					0,4,1,0,9,4,9,5,4,4,5,8,4,8,1,
					8,10,1,8,3,10,5,3,8,5,2,3,2,7,3,
					7,10,3,7,6,10,7,11,6,11,0,6,0,1,6,
					6,1,10,9,0,11,9,11,2,9,2,5,7,2,11
					));

		ArrayList<Integer> triangleList2 = new ArrayList<Integer>();
		triangleList2.addAll(triangleList);

		for (int i=subdivs; i>0; i--)
		{
			triangleList2 = new ArrayList<Integer>();

			int n = triangleList.size()/3;

			for (int j=0; j<n; j++)
			{
				Integer v1 = triangleList.get(3*j+0);
				Integer v2 = triangleList.get(3*j+1);
				Integer v3 = triangleList.get(3*j+2);

				Vector3 a = vertexList.get(v1.intValue());
				Vector3 b = vertexList.get(v2.intValue());
				Vector3 c = vertexList.get(v3.intValue());

				Vector3 d = a.cpy().lerp(b, 0.5f).nor();
				Vector3 e = b.cpy().lerp(c, 0.5f).nor();
				Vector3 f = c.cpy().lerp(a, 0.5f).nor();

				Integer id = vertexList.indexOf(d);
				Integer ie = vertexList.indexOf(e);
				Integer fi = vertexList.indexOf(f);

				if (id < 0) {
					id = vertexList.size();
					vertexList.add(id.intValue(), d);
				}

				if (ie < 0) {
					ie = vertexList.size();
					vertexList.add(ie.intValue(), e);
				}

				if (fi < 0) {
					fi = vertexList.size();
					vertexList.add(fi.intValue(), f);
				}

				triangleList2.addAll(Arrays.asList(v1,fi,id));
				triangleList2.addAll(Arrays.asList(v2,id,ie));
				triangleList2.addAll(Arrays.asList(v3,ie,fi));
				triangleList2.addAll(Arrays.asList(id,fi,ie));
			}

			triangleList = triangleList2;
		}

		vertices.addAll(vertexList);
		faces.addAll(triangleList2);

		//System.out.println(vertices);
	}

	void makeGraph(ArrayList<Vector3> vertices, ArrayList<Integer> faces, ArrayList<ArrayList<Integer>> adjList)
	{
		for (int i=0; i<vertices.size(); i++)
		{
			ArrayList<Integer> l = new ArrayList<Integer>();

			for (int j=0; j<faces.size(); j++)
			{
				if (i == faces.get(j))
				{
					int k = (j/3);

					Integer n1 = faces.get(3*k+0);
					Integer n2 = faces.get(3*k+1);
					Integer n3 = faces.get(3*k+2);

					if (!l.contains(n1))
						l.add(n1);

					if (!l.contains(n2))
						l.add(n2);

					if (!l.contains(n3))
						l.add(n3);

					l.remove(faces.get(j));	// remove itself
				}
			}

			adjList.add(l);
		}

		// sort to guarantee neighborhood arranged in circle
		for (ArrayList<Integer> l : adjList)
		{
			for (int i=0; i<l.size(); i++)
			{
				Integer lastSorted = l.get(i);

				for (int j=i+1; j<l.size(); j++)
				{
					// go to neighbors neighbors-list
					Integer neighborConsidered = l.get(j);
					if (adjList.get(neighborConsidered).contains(lastSorted))
					{
						Collections.swap(l, i+1, j);
						break;
					}
				}
			}
		}

		// correct winding
		for (ArrayList<Integer> orderedList : adjList)
		{
			Vector3 a = vertices.get(orderedList.get(0)).cpy().nor();
			Vector3 b = vertices.get(orderedList.get(1)).cpy().nor();
			Vector3 c = vertices.get(orderedList.get(2)).cpy().nor();

			a.sub(b);
			c.sub(b);
			a.crs(c).nor();

			// clockwise winding?
			if (a.dot(b) < 0)
				Collections.reverse(orderedList); // if not, reverse
		}
	}

	void makeDual(ArrayList<Vector3> primalVertices, ArrayList<Integer> primalTriangles, ArrayList<ArrayList<Integer>> adjList, ArrayList<Vector3> dualVertices, ArrayList<Integer> dualTriangles)
	{
		for (int i=0; i<primalVertices.size(); i++)
		{
			ArrayList<Integer> neighbors = adjList.get(i);

			int n = neighbors.size();
			int[] idx = new int[n];

			Vector3 center = primalVertices.get(i).cpy();

			for (int j=0; j<n; j++)
			{
				Vector3 v2 = primalVertices.get(neighbors.get(j)).cpy();
				Vector3 v3 = primalVertices.get(neighbors.get((j+1)%n)).cpy();

				v3.add(v2).add(center);
				v3.scl(1f/3f);

				// does v3 already exist in vertex list?
				idx[j] = -1;
				for (int k=0; k<dualVertices.size(); k++)
					if (Math.abs(v3.dot(dualVertices.get(k))) < 0.00000001)
						idx[j] = k;

				// if not, then append
				if (idx[j] == -1)
				{
					idx[j] = dualVertices.size();
					dualVertices.add(v3);
				}
			}


			// add triangles in same winding order
			int i1 = idx[0];

			// map from primal vertices to dual triangle starting index
			primalDual.add(dualTriangles.size());

			for (int k=1; k<n-1; k++)
			{
				int i2 = idx[k];
				int i3 = idx[k+1];

				dualTriangles.add(i1);
				dualTriangles.add(i2);
				dualTriangles.add(i3);
			}
		}
	}

	int[] getDualFaceVertices(int primalVertex, ArrayList<ArrayList<Integer>> adjList, ArrayList<Integer> dualTriangles)
	{
		int num = adjList.get(primalVertex).size();
		int[] dualFaceVertices = new int[num];

		int firstIdx = primalDual.get(primalVertex);

		int vertex = dualTriangles.get(firstIdx).intValue();
		dualFaceVertices[0] = vertex;

		for (int i=0; i<num-2; i++)
		{
			vertex = dualTriangles.get(firstIdx+3*i+1).intValue();
			dualFaceVertices[i+1] = vertex;

			vertex = dualTriangles.get(firstIdx+3*i+2).intValue();
			dualFaceVertices[i+2] = vertex;
		}

		return dualFaceVertices;
	}
	/*
	   Model makeDualModel(int subdivs, List<Integer> invertNormals)
	   {
	   ArrayList<Vector3> vertexList = new ArrayList<Vector3>();
	   ArrayList<Integer> triangleList = new ArrayList<Integer>();

	   makeIcoSphere(subdivs, vertexList, triangleList);

	   primalVertices = vertexList;

	   adjList = new ArrayList<ArrayList<Integer>>();
	   makeGraph(vertexList, triangleList, adjList);

	   ArrayList<Vector3> vertices = new ArrayList<Vector3>();
	   this.faces = new ArrayList<Integer>();
	   makeDual(vertexList, triangleList, adjList, vertices, this.faces);

	   float[] vertexNormalArray = new float[vertices.size()*6];
	   short[] triangleArray = new short[this.faces.size()];

	   for (int i=0; i<vertices.size(); i++)
	   {
	   vertexNormalArray[6*i+0] = vertices.get(i).x;
	   vertexNormalArray[6*i+1] = vertices.get(i).y;
	   vertexNormalArray[6*i+2] = vertices.get(i).z;
	   vertexNormalArray[6*i+3] = vertices.get(i).x;
	   vertexNormalArray[6*i+4] = vertices.get(i).y;
	   vertexNormalArray[6*i+5] = vertices.get(i).z;
	   }

	   if (invertNormals != null)
	   {
	   for (Integer n : invertNormals)
	   {
	   vertexNormalArray[6*n+3] *= -1;
	   vertexNormalArray[6*n+4] *= -1;
	   vertexNormalArray[6*n+5] *= -1;
	   }
	   }

	   for (int i=0; i<faces.size(); i++)
	   triangleArray[i] = (short)faces.get(i).intValue();

	   ModelBuilder modelBuilder = new ModelBuilder();
	   modelBuilder.begin();
	   MeshPartBuilder meshBuilder;
	   meshBuilder = modelBuilder.part("part1", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, new Material());
	   meshBuilder.addMesh(vertexNormalArray, triangleArray);
	   Model model = modelBuilder.end();

	   return model;
	   }
	   */
	Mesh makeDualMesh(int subdivs)
	{
		primalVertices = new ArrayList<Vector3>();
		primalTriangles = new ArrayList<Integer>();

		makeIcoSphere(subdivs, primalVertices, primalTriangles);

		adjList = new ArrayList<ArrayList<Integer>>();
		makeGraph(primalVertices, primalTriangles, adjList);

		dualVertices = new ArrayList<Vector3>();
		dualTriangles = new ArrayList<Integer>();
		makeDual(primalVertices, primalTriangles, adjList, dualVertices, dualTriangles);

		float[] vertexNormalArray = new float[dualVertices.size()*6];
		short[] triangleArray = new short[dualTriangles.size()];

		for (int i=0; i<dualVertices.size(); i++)
		{
			// vertex coordinates
			vertexNormalArray[6*i+0] = dualVertices.get(i).x;
			vertexNormalArray[6*i+1] = dualVertices.get(i).y;
			vertexNormalArray[6*i+2] = dualVertices.get(i).z;

			// additional vertex attributes
			vertexNormalArray[6*i+3] = dualVertices.get(i).x/dualVertices.get(i).y;
			vertexNormalArray[6*i+4] = dualVertices.get(i).x/dualVertices.get(i).z;
			vertexNormalArray[6*i+5] = 0;
		}

		for (int i=0; i<dualTriangles.size(); i++)
			triangleArray[i] = (short)dualTriangles.get(i).intValue();

		Mesh mesh = new Mesh(true, dualVertices.size(), dualTriangles.size(),
				new VertexAttribute( VertexAttributes.Usage.Position, 3, "a_position" ),
				new VertexAttribute( VertexAttributes.Usage.Position, 3, "a_color" ));

		mesh.setVertices(vertexNormalArray);
		mesh.setIndices(triangleArray);

		return mesh;
	}

	Model makeIcoSphereModel(int subdivs)
	{
		ArrayList<Vector3> vertexList = new ArrayList<Vector3>();
		ArrayList<Integer> triangleList = new ArrayList<Integer>();

		makeIcoSphere(subdivs, vertexList, triangleList);

		float[] vertexNormalArray = new float[vertexList.size()*6];
		short[] triangleArray = new short[triangleList.size()];

		for (int i=0; i<vertexList.size(); i++)
		{
			vertexNormalArray[6*i+0] = vertexList.get(i).x;
			vertexNormalArray[6*i+1] = vertexList.get(i).y;
			vertexNormalArray[6*i+2] = vertexList.get(i).z;
			vertexNormalArray[6*i+3] = vertexList.get(i).x;
			vertexNormalArray[6*i+4] = vertexList.get(i).y;
			vertexNormalArray[6*i+5] = vertexList.get(i).z;
		}

		for (int i=0; i<triangleList.size(); i++)
			triangleArray[i] = (short)triangleList.get(i).intValue();

		ModelBuilder modelBuilder = new ModelBuilder();
		modelBuilder.begin();
		MeshPartBuilder meshBuilder;
		meshBuilder = modelBuilder.part("part1", GL20.GL_TRIANGLES, Usage.Position | Usage.Normal, new Material());
		meshBuilder.addMesh(vertexNormalArray, triangleArray);
		Model model = modelBuilder.end();

		return model;
	}

	@Override
	public void create () {
		System.out.println("Hello World!\n");

		batch = new SpriteBatch();
		img = new Texture("badlogic.jpg");

		cam = new PerspectiveCamera(67, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
		cam.position.set(0f, 0f, 2f);
		cam.lookAt(0,0,0);
		cam.near = 0.5f;
		cam.far = 3.5f;
		cam.update();

		viewport = new FillViewport(Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), cam);

		ModelBuilder modelBuilder = new ModelBuilder();
		model = modelBuilder.createBox(0.1f, 0.1f, 0.1f, new Material(ColorAttribute.createDiffuse(Color.BLUE)), Usage.Position | Usage.Normal);
		//model = makeIcoSphereModel(2);
		//model = makeDualModel(1, null);
		instances = new ArrayList<ModelInstance>();
		ModelInstance instance1 = new ModelInstance(model);
		instance1.transform.translate(0,0,1);
		ModelInstance instance2 = new ModelInstance(model);
		instance2.transform.translate(0,1,0);
		ModelInstance instance3 = new ModelInstance(model);
		instance3.transform.translate(1,0,0);

		instances.add(instance1);
		instances.add(instance2);
		instances.add(instance3);

		modelBatch = new ModelBatch();

		primalDual = new ArrayList<>();
		mesh = makeDualMesh(2);

		environment = new Environment();
		environment.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.4f, 0.4f, 0.4f, 1f));
		light = new DirectionalLight().set(0.8f, 0.8f, 0.8f, -1f, -0.8f, -0.2f);
		light.setDirection(cam.direction);
		environment.add(light);

		vertexShader = Gdx.files.internal("vertex.glsl").readString();
		fragmentShader = Gdx.files.internal("fragment.glsl").readString();
		shaderProgram = new ShaderProgram(vertexShader,fragmentShader);

		//camController = new CameraInputController(cam);
		//Gdx.input.setInputProcessor(camController);
		Gdx.input.setInputProcessor(this);
	}

	class InvalidSphereSelectionException extends Exception {};

	Vector3 ray_intersect_unitsphere(Ray r) throws InvalidSphereSelectionException
	{
		Vector3 outer = r.origin;

		float t = -(r.origin.dot(r.direction))/r.direction.dot(r.direction);
		Vector3 inner = r.origin.cpy().add(r.direction.cpy().scl(t));

		if (inner.len2() > 1)
			throw new InvalidSphereSelectionException();

		float eps = 0.001f;

		System.out.println(inner);
		System.out.println(outer);

		Vector3 middle;
		float so, si, sm;

		do // bisection method
		{
			middle = outer.cpy().add(inner).scl(0.5f);

			so = outer.dot(outer);
			si = inner.dot(inner);
			sm = middle.dot(middle);

			if (sm > 1)
				outer = middle;
			else
				inner = middle;

		} while(Math.abs(so-si) > eps);

		return middle;
	}

	@Override
	public void render () {
		//camController.update();

		Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);

		/*
		   batch.begin();
		   batch.draw(img, 0, 0);
		   batch.end();
		   */

		//mesh.render(shaderProgram, GL20.GL_TRIANGLES);

		shaderProgram.begin();
		Gdx.gl.glEnable(GL20.GL_CULL_FACE);
		Gdx.gl.glFrontFace(GL20.GL_CW);
		Gdx.gl.glCullFace(GL20.GL_BACK);
		Gdx.gl.glEnable(GL20.GL_DEPTH_TEST);
		shaderProgram.setUniformMatrix("u_projView", cam.combined);
		//		float[] camPos = {cam.position.x/cam.position.len(), cam.position.y/cam.position.len(), cam.position.z/cam.position.len(), 0};
		//		shaderProgram.setUniform4fv("u_camPos", camPos, 0, 4);
		mesh.render(shaderProgram, GL20.GL_TRIANGLES);
		shaderProgram.end();

		if (instances != null){
			modelBatch.begin(cam);
			Gdx.gl.glEnable(GL20.GL_CULL_FACE);
			Gdx.gl.glFrontFace(GL20.GL_CCW);
			Gdx.gl.glCullFace(GL20.GL_BACK);
			Gdx.gl.glEnable(GL20.GL_DEPTH_TEST);
			modelBatch.render(instances, environment);
			modelBatch.end();
		}
	}

	int getPickedTile(int screenX, int screenY)
	{
		Vector3 p = new Vector3();
		Ray r = cam.getPickRay(screenX, screenY);

		try {
			p = ray_intersect_unitsphere(r);
			System.out.println("intersection: " + p);
		} catch(InvalidSphereSelectionException e) {
			return -1;
		}

		int min = 0;
		float minDist = p.cpy().sub(primalVertices.get(0)).len2();

		for (int i=1; i<primalVertices.size(); i++)
		{
			float dist = p.cpy().sub(primalVertices.get(i)).len2();

			if (dist < minDist)
			{
				min = i;
				minDist = dist;
			}
		}

		return min;
	}

	@Override
	public boolean touchDown (int screenX, int screenY, int pointer, int button) {
		oldMouseX = screenX; oldMouseY = screenY;
		touchDownX = screenX; touchDownY = screenY;

		System.out.println("touch down: " + screenX + " " + screenY);

		return false;
	}

	void setTileAttribute(int primalVertexId, int idx, float value)
	{
		int[] selectedVertices = getDualFaceVertices(primalVertexId, adjList, dualTriangles);

		for (int i: selectedVertices)
		{
			float[] src = {value};
			mesh.updateVertices(6*i+idx, src);
		}
	}

	@Override
	public boolean touchDragged (int screenX, int screenY, int pointer) {
		int deltaX = screenX - oldMouseX;
		int deltaY = screenY - oldMouseY;
		oldMouseX = screenX; oldMouseY = screenY;

		//System.out.println("touch drag: " + screenX + " " + screenY);

		cam.rotateAround(new Vector3(0,0,0), cam.up, deltaX);
		cam.rotateAround(new Vector3(0,0,0), cam.up.cpy().crs(cam.direction), deltaY);
		cam.update();
		light.setDirection(cam.direction);

		return false;
	}

	@Override
	public boolean touchUp (int screenX, int screenY, int pointer, int button) {

		if (Math.abs(touchDownX-screenX) <= 1 && Math.abs(touchDownY-screenY) <= 1)	// if there was little mouse movement
		{
			pickedTile = getPickedTile(screenX, screenY);

			if (pickedTile == -1) return false;

			System.out.println("pickedTile: " + " " + pickedTile + " " + primalVertices.get(pickedTile));
		}

		return false;
	}

	@Override
	public void dispose () {
		batch.dispose();
		img.dispose();
		mesh.dispose();
		model.dispose();
	}

	@Override
	public void resize (int width, int height) {
		System.out.println("old Viewport: " + " " + cam.viewportWidth  + " " + cam.viewportHeight);
		cam.viewportWidth = Gdx.graphics.getWidth();
		cam.viewportHeight = Gdx.graphics.getHeight();
		cam.update();
		System.out.println("new Viewport: " + " " + cam.viewportWidth  + " " + cam.viewportHeight);
	}

	@Override
	public void pause () {
	}

	@Override
	public void resume () {
	}

	@Override
	public boolean mouseMoved(int screenX, int screenY) {
		return false;
	}

	@Override
	public boolean scrolled(float a, float b) {
		return false;
	}

	@Override
	public boolean keyDown(int keycode) {
		if(Gdx.input.isKeyPressed(Keys.ESCAPE))
		{
			System.out.println("exit");
			Gdx.app.exit();
		}
		return true;
	}
	@Override
	public boolean keyUp(int keycode) {
		return false;
	}
	@Override
	public boolean keyTyped(char character) {
		return false;
	}

}

class Figures {
	public static Model getCube(int playerID){
		return new ModelBuilder().createBox(0.1f, 0.1f, 0.1f,
				new Material(ColorAttribute.createDiffuse(playerID == 1?new Color(0.4f, 0.4f, 1, 1):new Color(1, 1, 0.4f, 1))),
				Usage.Position | Usage.Normal);
	}

	public static Model getCone(int playerID){
		return new ModelBuilder().createCone(0.14f, 0.14f, 0.14f, 30,
				new Material(ColorAttribute.createDiffuse(playerID == 1?new Color(0.4f, 0.4f, 1, 1):new Color(1, 1, 0.4f, 1))),
				Usage.Position | Usage.Normal);
	}

	public static Model getSphere(int playerID){
		return new ModelBuilder().createSphere(0.14f, 0.14f, 0.14f, 30, 30,
				new Material(ColorAttribute.createDiffuse(playerID == 1?new Color(0.4f, 0.4f, 1, 1):new Color(1, 1, 0.4f, 1))),
				Usage.Position | Usage.Normal);
	}
}
