// Enhanced Occlusion Layer Module for Foundry VTT

export function registerOcclusionConfig() {
	// Global Hook Registration
	function registerGlobalHooks() {
		const updateTriggers = [ // Update triggers for occlusion layer
			'canvasReady', 
			'canvasPan',
			'canvasTokensRefresh', 
			'updateToken', 
			'controlToken', 
			'refreshToken',
			'preUpdateScene',  // Added to ensure update on scene modifications
			'updateScene'      // Added to ensure update on scene modifications
		];

		updateTriggers.forEach(hookName => {
			Hooks.on(hookName, () => {
				updateOcclusionLayer();
				//debouncedUpdate();
			});
		});
	}

	// Initialize on canvas setup
	// Hooks.on('refreshToken', () => { debouncedUpdate(); });

	// Initial layer setup
	Hooks.once('ready', () => {
		if (canvas.ready)
			initializeOcclusionLayer();
	});

	// Initialize on canvas setup
	Hooks.on('canvasInit', () => {
		initializeOcclusionLayer();
	});

	// Reset on scene change
	Hooks.on('changeScene', () => {
		if (occlusionConfig.container) {
			canvas.stage.removeChild(occlusionConfig.container);
			occlusionConfig.container.destroy({ children: true });
			occlusionConfig.container = null;
			occlusionConfig.tokensLayer = null;
			occlusionConfig.initialized = false;
		}
	});
	
	// Start the module
	registerGlobalHooks();
}

// Otimização 1: Debounce do updateOcclusionLayer
//const debouncedUpdate = debounce(updateOcclusionLayer, 50);
//const throttledUpdate = throttle(updateOcclusionLayer, 50);







// Persistent occlusion layer configuration
const occlusionConfig = {
	container: null,
	tokensLayer: null,
	initialized: false
};

// Initialize or reset the occlusion layer
function initializeOcclusionLayer() {
	// Remove existing container if it exists
	if (occlusionConfig.container) {
		canvas.stage.removeChild(occlusionConfig.container);
		occlusionConfig.container.destroy({ children: true });
	}

	// Create the main occlusion container
	occlusionConfig.container = new PIXI.Container();
	occlusionConfig.container.name = "OcclusionContainer";
	occlusionConfig.container.eventMode = 'passive';

	// Create a layer for occlusion tokens
	occlusionConfig.tokensLayer = new PIXI.Container();
	occlusionConfig.tokensLayer.name = "OcclusionTokens";
	occlusionConfig.tokensLayer.sortableChildren = true;

	// Add the layer to the container
	occlusionConfig.container.addChild(occlusionConfig.tokensLayer);

	// Add to canvas stage
	canvas.stage.addChild(occlusionConfig.container);
	canvas.stage.sortChildren();

	occlusionConfig.initialized = true;
}



function debugIntersection(token, tile) {
	const tokenBounds = token.mesh.getBounds();
	const tileBounds = tile.mesh.getBounds();
	
	const intersection = {
		x: Math.max(tokenBounds.x, tileBounds.x),
		y: Math.max(tokenBounds.y, tileBounds.y),
		width: Math.min(tokenBounds.x + tokenBounds.width, tileBounds.x + tileBounds.width) - Math.max(tokenBounds.x, tileBounds.x),
		height: Math.min(tokenBounds.y + tokenBounds.height, tileBounds.y + tileBounds.height) - Math.max(tokenBounds.y, tileBounds.y)
	};
	
	return intersection.width > 0 && intersection.height > 0;
}

function updateOcclusionLayer() {
	if (!canvas.ready) {
		return;
	}

	if (!occlusionConfig.initialized) {
		initializeOcclusionLayer();
	}

	// Clear existing occlusion tokens
	occlusionConfig.tokensLayer.removeChildren();

	// Get all tokens and tiles
	const tokens = canvas.tokens.placeables;
	const tiles = canvas.tiles.placeables;
	
	tokens.forEach((token, tokenIndex) => {
		const intersectingTiles = tiles.filter(tile => debugIntersection(token, tile));
		//performance upgrade: verify radius

		if (intersectingTiles.length > 0) {
			const sprite = createOcclusionSprite(token, intersectingTiles);
			if (sprite) {
				occlusionConfig.tokensLayer.addChild(sprite);
			}
		}
	});
}









// Custom shader for handling both intersection masking and effects
const occlusionVertexShader = /*gls l*/`
	attribute vec2 aVertexPosition;
	attribute vec2 aTextureCoord;
	uniform mat3 projectionMatrix;
	varying vec2 vWorldPos;
	varying vec2 vTextureCoord;

	void main() {
		// Passa as coordenadas de textura para o fragment shader
		vTextureCoord = aTextureCoord;
		
		// Calcula a posição do vértice no espaço do canvas
		vec3 worldPos = projectionMatrix * vec3(aVertexPosition, 1.0);
		vWorldPos = worldPos.xy;
		
		// Define a posição final do vértice
		gl_Position = vec4(worldPos.xy, 0.0, 1.0);
	}
`;

const occlusionFragmentShader = /*gls l*/`
	varying vec2 vTextureCoord;
	varying vec2 vWorldPos;

	uniform sampler2D uSampler;           // Textura do token
	uniform sampler2D uTileMasks[8];      // Array de texturas dos tiles
	uniform vec4 uTileBounds[8];          // Array de bounds dos tiles [x, y, width, height]
	uniform int uTileCount;               // Número de tiles intersectando
	uniform float uAlpha;                 // Valor alpha global

	void main() {
		// Amostra a textura do token na posição atual
		vec4 tokenColor = texture2D(uSampler, vTextureCoord);
		
		// Se o pixel do token for transparente, descarta
		if (tokenColor.a < 0.01) {
			discard;
		}
		
		bool isIntersecting = false;
		
		// Verifica intersecção com cada tile
		for (int i = 0; i < 8; i++) {
			if (i >= uTileCount) break;
			
			// Obtém os bounds do tile atual
			vec4 tileBounds = uTileBounds[i];
			
			// Verifica se o pixel atual está dentro dos bounds do tile
			if (
				vWorldPos.x >= tileBounds.x && 
				vWorldPos.x <= tileBounds.x + tileBounds.z &&
				vWorldPos.y >= tileBounds.y && 
				vWorldPos.y <= tileBounds.y + tileBounds.w) {
				
				// Calcula as coordenadas UV relativas ao tile
				vec2 tileUV = vec2(
					(vWorldPos.x - tileBounds.x) / tileBounds.z,
					(vWorldPos.y - tileBounds.y) / tileBounds.w
				);
				
				// Amostra a textura do tile na posição calculada
				vec4 tileColor = texture2D(uTileMasks[i], tileUV);
				
				// Se o pixel do tile não for transparente, marca como intersectando
				if (tileColor.a > 0.01) {
						isIntersecting = true;
						break;
				}
			}
		}
		
		// Define a cor final baseado na intersecção
		if (isIntersecting) {
			// Pixels que intersectam ficam brancos, mantendo o alpha original do token
			gl_FragColor = vec4(1.0, 1.0, 1.0, tokenColor.a * uAlpha);
		} else {
			// Pixels que não intersectam ficam transparentes
			gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0);
		}
	}
`;

const vertexGrape = `
	attribute vec2 aVertexPosition;
	attribute vec2 aTextureCoord;

	uniform mat3 projectionMatrix;
	uniform mat3 otherMatrix;

	varying vec2 vMaskCoord;
	varying vec2 vTextureCoord;

	void main(void){
		gl_Position = vec4((projectionMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0);

		vTextureCoord = aTextureCoord;
		vMaskCoord = ( otherMatrix * vec3( aTextureCoord, 1.0)  ).xy;
	}
`

const occlusionFragmentShaderGrape = /*gls l*/`
	// varying vec2 vTextureCoord;
	// uniform sampler2D uSampler;
	// uniform sampler2D mask; //renderTexture texture, provavelmebnte o sprite
	// varying vec2 vMaskCoord; //vem de otherMatrix = 

	// uniform vec4 fill_color; //a cor de sombra definida pelo usuário

	// uniform float x; //posição do sprite x, normalizada
	// uniform float y; //posição do sprite y, normalizada
	// uniform float z;

	// void main1(void) {
	// 	vec4 color = texture2D(uSampler, vTextureCoord);
	// 	vec4 mask = texture2D(mask, vMaskCoord);
	// 	vec4 fill_color = vec4(1.0, 0.5, 0.9, 0.8); //defini um valor qualquer
		
	// 	//mask = tex = maskTexture = texture = renderTexture = 
	// 	// 	= PIXI.RenderTexture.create({
	// 	// 	width: canvas.dimensions.width, height: canvas.dimensions.height
	// 	// });


	// 	if ((mask.b >= y &&  mask.g >= x)){

	// 		if (gl_FragColor.a <= 0.15 && color.a != 0.0){
	// 			gl_FragColor = fill_color;
	// 		}

	// 		} else {
	// 		gl_FragColor = color; // não muda a cor se estiver fora do retangulo da máscara
	// 	}
	// }



	varying vec2 vTextureCoord;
	varying vec2 vWorldPos;
	uniform sampler2D uSampler;       // Textura do token (não usada aqui)
	uniform sampler2D uTileMasks[8];  // Array de texturas dos tiles
	uniform vec4 uTileBounds[8];      // Array de bounds dos tiles [x, y, width, height]
	uniform int uTileCount;           // Número de tiles intersectando
	uniform vec4 dimensions;          // [x, y, width, height] da interseção
	uniform vec4 tileDimensions;      // [x, y, width, height] do tile


	void main(void) {
		// Posição local do fragmento na interseção
		vec2 localPos = gl_FragCoord.xy - dimensions.xy;
			
		// Coordenadas UV normalizadas para a textura do tile
		vec2 tileCoord = vec2(
				(localPos.x / dimensions.z) * (tileDimensions.z / dimensions.z),
				(localPos.y / dimensions.w) * (tileDimensions.w / dimensions.w)
		);

		// Ajuste para considerar a posição do tile no canvas
		tileCoord.x += (dimensions.x - tileDimensions.x) / tileDimensions.z;
		tileCoord.y += (dimensions.y - tileDimensions.y) / tileDimensions.w;

		// Amostra a textura do tile
		// vec4 tileColor = texture2D(uTileMask, tileCoord);

		// Amostra a textura do token
		vec4 tokenColor = texture2D(uSampler, vTextureCoord);

		// Se o pixel do tile for opaco, cria uma máscara branca opaca
		// if (tokenColor.a > 0.1 && vTextureCoord.x > 0.5) {
		// 	gl_FragColor.rgb = vec3(0.5, 1.0, 1.0); // Branco opaco
		// }

		if (tokenColor.a > 0.1 && uTileCount == 1) {
			gl_FragColor.rgb = vec3(0.5, 1.0, 1.0); // Branco opaco
		}

		if (tokenColor.a > 0.1 && uTileCount == 2) {
			gl_FragColor.rgb = vec3(0.5, 0.5, 1.0); // Branco opaco
		}

		if (tokenColor.a < 0.1) {
			gl_FragColor = vec4(0.0, 0.0, 0.0, 0.0); // Transparente
		}	
	}
`


// Create the enhanced occlusion filter
class EnhancedOcclusionFilter extends PIXI.Filter {
	constructor() {
		super(occlusionVertexShader, occlusionFragmentShaderGrape); //occlusionVertexShader, occlusionFragmentShader
		
		// Initialize uniforms
		this.uniforms.uTileMasks = [];
		this.uniforms.uTileBounds = new Float32Array(32);  // Support up to 8 tiles (8 * 4 values)
		this.uniforms.uTileCount = 0;
		this.uniforms.uColorMatrix = new Float32Array([0.2, 0.2, 0.2, 0.5]);
		this.uniforms.uOutlineColor = new Float32Array([0.0, 1.0, 0.35, 1.0]);
		this.uniforms.uOutlineThickness = 0.001;
		this.uniforms.uAlpha = 0.8;  // Slightly transparent by default

		// Keep track of current tiles for updates
		this._intersectingTiles = [];
	}
	
	updateTiles(intersectingTiles) {
		const count = Math.min(intersectingTiles.length, 8);
		this.uniforms.uTileCount = count;
		this._intersectingTiles = intersectingTiles.slice(0, count);
		
		for(let i = 0; i < count; i++) {
			const tile = intersectingTiles[i];
			const bounds = tile.mesh.getBounds();
			
			// Update tile texture
			this.uniforms.uTileMasks[i] = tile.texture;
			
			// Update tile bounds
			const idx = i * 4;
			this.uniforms.uTileBounds[idx] = bounds.x;
			this.uniforms.uTileBounds[idx + 1] = bounds.y;
			this.uniforms.uTileBounds[idx + 2] = bounds.width;
			this.uniforms.uTileBounds[idx + 3] = bounds.height;
		}
	}

	// Add method to update alpha
	setAlpha(alpha) {
		this.uniforms.uAlpha = Math.max(0, Math.min(1, alpha));
	}
		
	// Add method to update highlight color
	setHighlightColor(r, g, b, a = 1.0) {
		this.uniforms.uColorMatrix = new Float32Array([r, g, b, a]);
	}
}





// Modified createOcclusionSprite function
function createOcclusionSprite(token, intersectingTiles) {
	if (!token.mesh || !token.mesh.texture) return null;

	// Criar sprite com logs
	const sprite = new PIXI.Sprite(token.mesh.texture);
	sprite.position.set(token.mesh.position.x, token.mesh.position.y);
	sprite.anchor.set(token.mesh.anchor.x, token.mesh.anchor.y);
	sprite.angle = token.mesh.angle;
	sprite.scale.set(token.mesh.scale.x, token.mesh.scale.y);

	// Criar e configurar filtro
	const occlusionFilter = new EnhancedOcclusionFilter();
	
	// Obtenha a matriz de transformação do canvas (worldTransform)
	// Converta o worldTransform para uma matriz 3x3 para passar para o shader
	// const worldTransform = canvas.stage.worldTransform;
	// const projectionMatrix = new PIXI.Matrix(
	// 		worldTransform.a,
	// 		worldTransform.b, 
	// 		worldTransform.c,
	// 		worldTransform.d, 
	// 		worldTransform.tx,
	// 		worldTransform.ty
	// );
	//console.log('projectionMatrix :>> ', projectionMatrix);

	/** Matriz
	 *  | a, c, tx | a, d = view zoom
	 *  | b, d, ty | b, d = ???
	 *  | 0, 0, 1  | tx, ty = offset camera, com multiplicação do zoom
	 */

	// Set projection matrix uniform
	// occlusionFilter.uniforms.projectionMatrix = projectionMatrix.toArray();

	// Atualiza os tiles intersectados no filtro
	occlusionFilter.updateTiles(intersectingTiles);
	// occlusionFilter.updateTokenTransform(token);
	
	// Aplicar filtro
	sprite.filters = [occlusionFilter];
	//sprite.filters = [colorMatrix];
	sprite.eventMode = 'passive';

	// if (sprite) {
	// 	addDebugVisuals(token, intersectingTiles);
	// }

	return sprite;
}








function calculatePosition(token) {
	let {x, y} = token.center;

	// if (game.settings.get('grape_juice-isometrics', 'enable_token_occluder_elevation')){
	// 		x -= (token.document.elevation / canvas.scene.grid.distance) * canvas.grid.size;
	// 		y += (token.document.elevation / canvas.scene.grid.distance) * canvas.grid.size;
	// }

	// Passo 1: Ajustar para os limites do canvas
	if (x < 0) x = 0;
	if (x > canvas.dimensions.width) x = canvas.dimensions.width;
	if (y < 0) y = 0;
	if (y > canvas.dimensions.height) y = canvas.dimensions.height;

	// Passo 2: Normalizar para o intervalo [0, 1]
	x = x / canvas.dimensions.width;
	y = y / canvas.dimensions.height;

	return {x, y};
}






















// Adicionar um visual debug helper
function addDebugVisuals(token, intersectingTiles) {
	const debugGraphics = new PIXI.Graphics();
	
	// Desenhar bounds do token em vermelho
	const tokenBounds = token.mesh.getBounds();
	debugGraphics.lineStyle(2, 0xFF0000);
	debugGraphics.drawRect(tokenBounds.x, tokenBounds.y, tokenBounds.width, tokenBounds.height);
	
	// Desenhar bounds dos tiles em verde
	intersectingTiles.forEach(tile => {
		const tileBounds = tile.mesh.getBounds();
		debugGraphics.lineStyle(2, 0x00FF00);
		debugGraphics.drawRect(tileBounds.x, tileBounds.y, tileBounds.width, tileBounds.height);
	});
	
	// Adicionar ao stage
	canvas.stage.addChild(debugGraphics);
	
	// Remover após 5 segundos
	setTimeout(() => {
		canvas.stage.removeChild(debugGraphics);
		debugGraphics.destroy();
	}, 1000);
}

// Função de debug para verificar se os shaders compilaram corretamente
function validateShader(gl, shader) {
	if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
			const info = gl.getShaderInfoLog(shader);
			console.error('Shader compile error:', info);
			return false;
	}
	return true;
}



// Definição do isoOutlineFilter
if (typeof PIXI !== 'undefined' && PIXI.filters) {
	const vertexShader =`
		attribute vec2 aVertexPosition;
		attribute vec2 aTextureCoord;
		uniform mat3 projectionMatrix;
		varying vec2 vTextureCoord;

		void main(void) {
			gl_Position = vec4((projectionMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0);
			vTextureCoord = aTextureCoord;
		}
	`;
	const fragmentShader = `
		varying vec2 vTextureCoord;

		uniform sampler2D uSampler;
		uniform float alpha;
		uniform vec2 outlineThickness;
		uniform vec4 outlineColor;
		uniform vec4 filterArea;
		uniform vec4 filterClamp;

		void main(void) {
			vec4 ownColor = texture2D(uSampler, vTextureCoord);
			vec4 curColor;
			float maxAlpha = 0.0;
			vec2 displaced;

			for (float angle = 0.0; angle < 6.28318530718; angle += 0.78539816339) {
				displaced.x = vTextureCoord.x + outlineThickness.x * cos(angle);
				displaced.y = vTextureCoord.y + outlineThickness.y * sin(angle);
				curColor = texture2D(uSampler, displaced);
				maxAlpha = max(maxAlpha, curColor.a);
			}
			float resultAlpha = max(maxAlpha, ownColor.a);
			gl_FragColor = vec4((ownColor.rgb * ownColor.a + outlineColor.rgb * (1.0 - ownColor.a)) * resultAlpha, resultAlpha);
		 }
	`;

	// Defina a classe isoOutlineFilter
	class IsoOutlineFilter extends PIXI.Filter {
		constructor(thickness = 0.5, color = 0x000000, alpha = 0.5) {
			super(vertexShader, fragmentShader);

			// Inicialize os uniforms
			this.uniforms.outlineColor = new Float32Array(4);     // Para armazenar RGBA
			this.uniforms.outlineThickness = new Float32Array(2); // Para armazenar X e Y
			this.uniforms.filterArea = new Float32Array(2);       // Para área de filtro
			this.uniforms.alpha = alpha;

			// Configure as propriedades iniciais
			this.color = color;
			this.thickness = thickness;
		}

		get alpha() { return this.uniforms.alpha; }
		set alpha(value) { this.uniforms.alpha = value; }

		get color() { return PIXI.utils.rgb2hex(this.uniforms.outlineColor); }
		set color(value) { PIXI.utils.hex2rgb(value, this.uniforms.outlineColor); }

		get thickness() { return this.uniforms.outlineThickness[0]; }
		set thickness(value) {
			// Certifique-se de que filterArea tenha valores válidos
			const filterAreaX = this.uniforms.filterArea[0] || 1; // Evite divisão por 0
			const filterAreaY = this.uniforms.filterArea[1] || 1; // Evite divisão por 0
			
			this.uniforms.outlineThickness[0] = value / filterAreaX;
			this.uniforms.outlineThickness[1] = value / filterAreaY;
		}
	}

	// Add the isooutlinefilter to the PIXI.filters namepace
	PIXI.filters.isoOutlineFilter = IsoOutlineFilter;
} else {
	console.error('PIXI ou PIXI.filters não estão disponíveis.');
}

// Create new outline filter
const outlineFilter = new PIXI.filters.isoOutlineFilter();
outlineFilter.thickness = 0.005;
outlineFilter.color = 0x00ff59;

// Create a greyscale/dimming filter
const colorMatrix = new PIXI.ColorMatrixFilter();
colorMatrix.alpha = 1;
colorMatrix.matrix = [
	0.0,  0.0,  0.0,  0.0,  0.0,
	0.0,  0.0,  0.0,  0.0,  0.0,
	1.0,  1.0,  1.0,  0.0,  0.0,
	0.0,  0.0,  0.0,  1.0,  0.0
];