/*
 * 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.List;

import static org.lwjgl.opengl.GL11.GL_RGB;
import static org.lwjgl.opengl.GL11.GL_TEXTURE_2D;
import static org.lwjgl.opengl.GL11.glGetTexImage;
import static org.lwjgl.opengl.GL11C.GL_COLOR_BUFFER_BIT;
import static org.lwjgl.opengl.GL11C.GL_DEPTH_BUFFER_BIT;
import static org.lwjgl.opengl.GL11C.GL_FLOAT;
import static org.lwjgl.opengl.GL11C.glClear;
import static org.lwjgl.opengl.GL11C.glTexImage2D;
import static org.lwjgl.opengl.GL11C.glViewport;
import static org.lwjgl.opengl.GL12.GL_CLAMP_TO_EDGE;
import static org.lwjgl.opengl.GL13C.GL_TEXTURE_CUBE_MAP_POSITIVE_X;
import static org.lwjgl.opengl.GL14.GL_DEPTH_COMPONENT24;
import static org.lwjgl.opengl.GL30.GL_COLOR_ATTACHMENT0;
import static org.lwjgl.opengl.GL30.GL_DEPTH_ATTACHMENT;
import static org.lwjgl.opengl.GL30.GL_FRAMEBUFFER;
import static org.lwjgl.opengl.GL30.GL_FRAMEBUFFER_COMPLETE;
import static org.lwjgl.opengl.GL30.GL_RENDERBUFFER;
import static org.lwjgl.opengl.GL30.GL_RGB16F;
import static org.lwjgl.opengl.GL30.glBindFramebuffer;
import static org.lwjgl.opengl.GL30.glBindRenderbuffer;
import static org.lwjgl.opengl.GL30.glCheckFramebufferStatus;
import static org.lwjgl.opengl.GL30.glFramebufferRenderbuffer;
import static org.lwjgl.opengl.GL30.glFramebufferTexture2D;
import static org.lwjgl.opengl.GL30.glGenFramebuffers;
import static org.lwjgl.opengl.GL30.glGenRenderbuffers;
import static org.lwjgl.opengl.GL30.glRenderbufferStorage;
import static org.lwjgl.system.MemoryUtil.NULL;
import static rendering.ShaderPrograms.hdrToCubemapShader;

import lombok.Getter;
import org.joml.Matrix4f;
import org.joml.Vector3f;
import sceneobjects.Cube;

@Getter
public class HDRTexture extends CubemapTexture {

	private final Vector3f brightestArea;

	public HDRTexture(String path, int resolution, Vector3f colour, int textureUnit) {
		super(colour, textureUnit);

		int frameBuffer = glGenFramebuffers();
		glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
		int renderBuffer = glGenRenderbuffers();
		glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
		glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, resolution, resolution);
		glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, renderBuffer);
		if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
			System.err.println("HDR framebuffer not complete");
		}
//		glBindFramebuffer(GL_FRAMEBUFFER, 0);

		Texture2D hdr2DImage = new Texture2D(path, colour, 0, GL_RGB16F, GL_CLAMP_TO_EDGE, true);

		// Could use this area to determine sun brightness
		brightestArea = getBrightestArea(hdr2DImage);

		this.bind();
		for (int i = 0; i < 6; i++) {
			glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB16F, resolution, resolution,
					0, GL_RGB, GL_FLOAT, NULL);
		}
		setHints(GL_CLAMP_TO_EDGE);

		Vector3f origin = new Vector3f(0);
		List<Matrix4f> captureViews = List.of(
				new Matrix4f().lookAt(origin, new Vector3f(1, 0, 0), new Vector3f(0, -1, 0)),
				new Matrix4f().lookAt(origin, new Vector3f(-1, 0, 0), new Vector3f(0, -1, 0)),
				new Matrix4f().lookAt(origin, new Vector3f(0, 1, 0), new Vector3f(0, 0, 1)),
				new Matrix4f().lookAt(origin, new Vector3f(0, -1, 0), new Vector3f(0, 0, -1)),
				new Matrix4f().lookAt(origin, new Vector3f(0, 0, 1), new Vector3f(0, -1, 0)),
				new Matrix4f().lookAt(origin, new Vector3f(0, 0, -1), new Vector3f(0, -1, 0))
		);

		Cube cube = new Cube();
		cube.addTexture("hdr", hdr2DImage);
		cube.setShaderProgram(hdrToCubemapShader);
		hdrToCubemapShader.setUniform("projection",
				new Matrix4f().perspective((float) Math.toRadians(90f), 1, 0.1f, 1.1f)
		);
		hdr2DImage.bind();

//		glActiveTexture(GL_TEXTURE8);
		glViewport(0, 0, resolution, resolution);
		glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
		for (int i = 0; i < 6; i++) {
			hdrToCubemapShader.setUniform("view", captureViews.get(i));
			glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, this.handle, 0);
			glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
			cube.render();
		}
		glBindFramebuffer(GL_FRAMEBUFFER, 0);
	}

	private Vector3f getBrightestArea(Texture2D hdr2DImage) {
		int width = hdr2DImage.getWidth();
		int height = hdr2DImage.getHeight();
		float[] array = new float[width * height * 3];
		glGetTexImage(GL_TEXTURE_2D, 0, GL_RGB, GL_FLOAT, array);
		float brightest = 0;
		int brightestI = 0;
		for (int i = width; i < array.length / 3; i++) {
			if (i % width == 0 || (i + 1) % width == 0 || (i / width + 1) % height == 0) {
				continue;
			}
			float intensity = 0;
			for (int y = -1; y < 2; y++) {
				for (int x = -1; x < 2; x++) {
					for (int c = 0; c < 3; c++) {
						intensity += array[(i + x) * 3 + (y * width * 3) + c];
					}
				}
			}
			if (intensity > brightest) {
				brightest = intensity;
				brightestI = i;
			}
		}
		int u = brightestI % width;
		int v = brightestI / width;
		return equiRectangularToSpherical((float) u / width, (float) v / height);
	}

	private Vector3f equiRectangularToSpherical(float uNorm, float vNorm) {
		float theta = (uNorm - 0.5f) * 2 * (float) Math.PI; // range [0, 2pi]
		float phi = (vNorm - 0.5f) * (float) Math.PI; // range [0, pi]

		float x = (float) (Math.cos(theta) * Math.cos(phi));
		float z = (float) (Math.sin(phi) * Math.cos(phi));
		float y = (float) Math.sin(phi);

		float distance = 200;
		return new Vector3f(x, y, z).normalize().mul(distance);
	}

}
