/*
 * Copyright (c) 2021 Callum Newlands
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     The additional term of 7.b applies: Requiring preservation of specified
 *     reasonable legal notices or author attributions in that material or in the
 *     Appropriate Legal Notices displayed by works containing it
 *
 *     This program 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 General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package modeldata.meshdata;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static rendering.ShaderPrograms.shadowsShader;

import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apache.commons.lang3.ArrayUtils;
import org.joml.Matrix4f;
import org.joml.Vector2f;
import org.joml.Vector3f;
import params.Parameters;
import rendering.ShaderProgram;

@Getter
@Setter
public class Mesh {

	protected Map<String, Texture> textures = new HashMap<>();
	protected VertexArray vertexArray;
	private List<Vertex> vertices;
	private int[] indices;
	private List<VertexAttribute> vertexAttributes;
	private Matrix4f model = new Matrix4f().identity();
	private Parameters.ColourFilter colourFilter;
	private boolean isInstanced;
	private boolean isLeaf;
	@Accessors(fluent = true)
	private boolean hasNormalMap;
	@Accessors(fluent = true)
	private boolean hasTranslucencyMap;
	@Accessors(fluent = true)
	private boolean hasSpecularMap;
	@Accessors(fluent = true)
	private boolean hasHalfLifeBasisMap;
	private ShaderProgram shaderProgram;

	private boolean hasSegColour = false;
	private Vector3f segColour;
	public void setSegColour(Vector3f segColour) {
		hasSegColour = true;
		this.segColour = segColour;
	}

	public Mesh(List<Vertex> vertices, int[] indices, List<VertexAttribute> vertexAttributes) {
		this(vertices, indices, vertexAttributes, false, false, false, false, false, false);
	}

	public Mesh(List<Vertex> vertices,
				int[] indices,
				List<VertexAttribute> vertexAttributes,
				boolean isInstanced,
				boolean hasNormalMap,
				boolean hasTranslucencyMap,
				boolean hasSpecularMap,
				boolean hasHalfLifeBasisMap,
				boolean isLeaf) {
		this.vertices = vertices;
		this.indices = indices;
		this.vertexAttributes = vertexAttributes;
		this.isInstanced = isInstanced;
		this.hasNormalMap = hasNormalMap;
		this.hasTranslucencyMap = hasTranslucencyMap;
		this.hasSpecularMap = hasSpecularMap;
		this.hasHalfLifeBasisMap = hasHalfLifeBasisMap;
		this.isLeaf = isLeaf;
		this.colourFilter = null;
		this.vertexArray = createVAO();
	}

	public Mesh(Mesh mesh) {
		this(mesh, mesh.vertices);
	}

	public Mesh(Mesh mesh, boolean isInstanced) {
		this(mesh, mesh.vertices, isInstanced);
	}

	public Mesh(Mesh mesh, List<Vertex> transformedVertices) {
		this(mesh, transformedVertices, mesh.isInstanced);
	}

	public Mesh(Mesh mesh, List<Vertex> transformedVertices, boolean isInstanced) {
		this.vertices = transformedVertices;
		this.indices = mesh.indices.clone();
		this.vertexAttributes = new ArrayList<>(mesh.vertexAttributes);
		this.textures = new HashMap<>(mesh.textures);
		this.model = new Matrix4f(mesh.model);
		this.shaderProgram = mesh.shaderProgram;
		this.isInstanced = isInstanced;
		this.hasNormalMap = mesh.hasNormalMap;
		this.hasTranslucencyMap = mesh.hasTranslucencyMap;
		this.hasSpecularMap = mesh.hasSpecularMap;
		this.hasHalfLifeBasisMap = mesh.hasHalfLifeBasisMap;
		this.isLeaf = mesh.isLeaf;
		this.vertexArray = createVAO();
		this.colourFilter = mesh.colourFilter != null ? new Parameters.ColourFilter(mesh.colourFilter) : null;
		this.hasSegColour = mesh.hasSegColour;
		this.segColour = mesh.segColour;
	}

	public void addTexture(String uniform, Texture texture) {
		if (texture == null) {
			return;
		}
		switch (uniform) {
			case "normalTexture", "leafFrontNorm", "leafBackNorm" -> this.hasNormalMap = true;
			case "specularTexture" -> this.hasSpecularMap = true;
			case "leafFrontTranslucency", "leafBackTranslucency" -> this.hasTranslucencyMap = true;
			case "leafFrontHalfLife", "leafBackHalfLife" -> this.hasHalfLifeBasisMap = true;
		}
		textures.put(uniform, texture);
	}

	private VertexArray createVAO() {
		float[] data = ArrayUtils.toPrimitive(vertices.stream().flatMap(vd -> {
			List<Float> vertexData = new ArrayList<>();
			for (VertexAttribute attribute : vertexAttributes) {
				switch (attribute.getName()) {
					case "position" -> {
						Vector3f v = vd.getPosition();
						vertexData.addAll(List.of(v.x, v.y, v.z));
					}
					case "normal" -> {
						Vector3f n = vd.getNormal();
						vertexData.addAll(List.of(n.x, n.y, n.z));
					}
					case "tangent" -> {
						Vector3f t = vd.getTangent();
						vertexData.addAll(List.of(t.x, t.y, t.z));
					}
					case "texCoord" -> {
						Vector2f t = vd.getTexCoord();
						vertexData.addAll(List.of(t.x, t.y));
					}
					default -> throw new RuntimeException("Unknown attribute: " + attribute.getName());
				}
			}
			return vertexData.stream();
		}).toArray(Float[]::new));

		return new VertexArray(data, vertices.size(), indices, vertexAttributes);

	}

	private void bindForRender() {
		shaderProgram.use();
		if (!isInstanced) {
			shaderProgram.setUniform("model", model);
		}
		shaderProgram.setUniform("isInstanced", isInstanced);
		shaderProgram.setUniform("hasNormalMap", hasNormalMap);
		shaderProgram.setUniform("hasTranslucencyMap", hasTranslucencyMap);
		shaderProgram.setUniform("hasHalfLifeBasisMap", hasHalfLifeBasisMap);
		shaderProgram.setUniform("hasSpecularMap", hasSpecularMap);
		for (Map.Entry<String, Texture> texture : textures.entrySet()) {
			if (texture.getKey().equals("diffuseTexture")) {
				shaderProgram.setUniform("modelColour", texture.getValue().getColour());
			}
			shaderProgram.setUniform(texture.getKey(), texture.getValue().getUnitID());
			texture.getValue().bind();
		}
		if (colourFilter != null) {
			shaderProgram.setUniform("colourFilter", colourFilter.colour);
			shaderProgram.setUniform("mixFactor", colourFilter.mixFactor);
			shaderProgram.setUniform("expMix", colourFilter.expMix);
		} else {
			shaderProgram.setUniform("mixFactor", 0.0f);
		}

		shaderProgram.setUniform("hasSegCol", hasSegColour);
		if (hasSegColour) {
			shaderProgram.setUniform("segCol", segColour);
		}
	}

	private void unbindFromRender() {
		for (Texture texture : textures.values()) {
			texture.unbind();
		}
	}

	public void render() {
		render(false);
	}

	public void render(boolean renderForShadows) {
		ShaderProgram prevShader = null;
		if (renderForShadows) {
			prevShader = this.shaderProgram;
			this.shaderProgram = shadowsShader;
			shadowsShader.setUniform("isLeaf", isLeaf);
		}
		bindForRender();
		draw();
		unbindFromRender();
		if (renderForShadows) {
			this.shaderProgram = prevShader;
		}
	}

	public void render(int numberOfInstances) {
		render(numberOfInstances);
	}

	public void render(int numberOfInstances, boolean renderForShadows) {
		ShaderProgram prevShader = null;
		if (renderForShadows) {
			prevShader = this.shaderProgram;
			this.shaderProgram = shadowsShader;
			shadowsShader.setUniform("isLeaf", isLeaf);
		}
		bindForRender();
		draw(numberOfInstances);
		unbindFromRender();
		if (renderForShadows) {
			this.shaderProgram = prevShader;
		}
	}

	protected void draw() {
		vertexArray.draw();
	}

	protected void draw(int numberOfInstances) {
		if (isInstanced) {
			vertexArray.draw(numberOfInstances);
		} else {
			throw new RuntimeException("Attempting to draw single mesh as instanced mesh");
		}
	}

}
