/*-
 *  Copyright 2020 Beuth Hochschule für Technik Berlin, Hochschule für Technik Stuttgart
 * 
 *  This file is part of CityDoctor2.
 *
 *  CityDoctor2 is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  CityDoctor2 is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with CityDoctor2.  If not, see <https://www.gnu.org/licenses/>.
 */
package de.hft.stuttgart.citydoctor2.tesselation;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import com.jogamp.opengl.GL;
import com.jogamp.opengl.glu.GLU;
import com.jogamp.opengl.glu.GLUtessellator;
import com.jogamp.opengl.glu.GLUtessellatorCallbackAdapter;

import de.hft.stuttgart.citydoctor2.datastructure.LinearRing;
import de.hft.stuttgart.citydoctor2.datastructure.Polygon;
import de.hft.stuttgart.citydoctor2.datastructure.Vertex;
import de.hft.stuttgart.citydoctor2.math.Vector3d;

/**
 * Tesselator to create triangles out of polygons.
 * 
 * @author Matthias Betz
 *
 */
public class JoglTesselator {

	private static final String DATA_WAS_NOT_A_LIST = "data was not a list";
	private static final GLU glu = new GLU();
	private static final GLUtessellator tess = GLU.gluNewTess();
	private static final GLUtessellatorCallbackAdapter callback;
	private static final ArrayList<Primitive> primitives = new ArrayList<>();

	static {
		callback = new GLUtessellatorCallbackAdapter() {

			private Primitive lastPrimitive;

			@Override
			public void beginData(int type, Object data) {
				if (!(data instanceof ArrayList<?>)) {
					throw new IllegalStateException(DATA_WAS_NOT_A_LIST);
				}
				@SuppressWarnings("unchecked")
				ArrayList<Vector3d> vertices = (ArrayList<Vector3d>) data;
				lastPrimitive = new Primitive(type, vertices);
				primitives.add(lastPrimitive);
			}

			@Override
			public void vertexData(Object vertex, Object data) {
				if (!(data instanceof ArrayList)) {
					throw new IllegalStateException(DATA_WAS_NOT_A_LIST);
				}
				if (!(vertex instanceof Integer)) {
					throw new IllegalStateException("vertex has to be an Integer");
				}
				lastPrimitive.addIndex((Integer) vertex);
			}

			@Override
			public void combineData(double[] coords, Object[] vertexIndices, float[] weights, Object[] outgoingData,
					Object data) {
				if (!(data instanceof ArrayList<?>)) {
					throw new IllegalStateException(DATA_WAS_NOT_A_LIST);
				}
				@SuppressWarnings("unchecked")
				ArrayList<Vector3d> vertices = (ArrayList<Vector3d>) data;
				// to ensure the coordinates are valid
				double[] myCoords = Arrays.copyOf(coords, 3);
				Vector3d newVertex = new Vector3d(myCoords);
				outgoingData[0] = vertices.size();
				vertices.add(newVertex);
			}

			@Override
			public void errorData(int errnum, Object data) {
				throw new TesselationException("GLU Error " + glu.gluErrorString(errnum));
			}

		};
		GLU.gluTessCallback(tess, GLU.GLU_TESS_BEGIN_DATA, callback);
		GLU.gluTessCallback(tess, GLU.GLU_TESS_VERTEX_DATA, callback);
		GLU.gluTessCallback(tess, GLU.GLU_TESS_ERROR_DATA, callback);
		GLU.gluTessCallback(tess, GLU.GLU_TESS_COMBINE_DATA, callback);
		GLU.gluTessCallback(tess, GLU.GLU_TESS_END_DATA, callback);
	}

	public static TesselatedPolygon tesselatePolygon(Polygon p) {
		ArrayList<Vector3d> vertices = new ArrayList<>();
		Vector3d normal = p.calculateNormal();
		synchronized (tess) {
			GLU.gluTessProperty(tess, GLU.GLU_TESS_BOUNDARY_ONLY, GL.GL_FALSE);
			GLU.gluTessBeginPolygon(tess, vertices);
			GLU.gluTessNormal(tess, normal.getX(), normal.getY(), normal.getZ());

			// exterior ring
			LinearRing ext = p.getExteriorRing();
			createContour(vertices, ext);

			// interior rings
			for (LinearRing lr : p.getInnerRings()) {
				createContour(vertices, lr);
			}

			GLU.gluTessEndPolygon(tess);
			ArrayList<Integer> indices = new ArrayList<>();
			for (Primitive primitive : primitives) {
				primitive.fillListWithIndices(indices);
			}
			primitives.clear();
			return new TesselatedPolygon(vertices, indices, p);
		}
	}

	private static void createContour(List<Vector3d> vertices, LinearRing lr) {
		GLU.gluTessBeginContour(tess);
		for (Vertex v : lr.getVertices()) {
			vertices.add(v);
			GLU.gluTessVertex(tess, v.getCoordinates(), 0, vertices.size() - 1);
		}
		GLU.gluTessEndContour(tess);
	}

	public static TesselatedRing tesselateRing(LinearRing r) {
		ArrayList<Vector3d> vertices = new ArrayList<>();
		Vector3d normal = r.calculateNormal();
		synchronized (tess) {
			GLU.gluTessBeginPolygon(tess, vertices);
			GLU.gluTessNormal(tess, normal.getX(), normal.getY(), normal.getZ());
			createContour(vertices, r);
			GLU.gluTessEndPolygon(tess);
			ArrayList<Integer> indices = new ArrayList<>();
			for (Primitive primitive : primitives) {
				primitive.fillListWithIndices(indices);
			}
			primitives.clear();
			return new TesselatedRing(vertices, indices, r);
		}
	}

	private JoglTesselator() {
		// only static useage
	}

}
