// Enhanced Occlusion Layer Module for Foundry VTT

export function registerOcclusionConfig() {
	// Global Hook Registration
	function registerGlobalHooks() {
		const updateTriggers = [
			'canvasReady', 
			'canvasPan',
			'canvasTokensRefresh', 
			'updateToken', 
			'controlToken', 
			'refreshToken',
			'preUpdateScene',
			'updateScene'
		];

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

	// 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();
}





// 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';
	occlusionConfig.container.eventMode = 'none';           // Otimização #1
	occlusionConfig.container.interactiveChildren = false;  // Otimização #1
	occlusionConfig.container.cullable = true; 

	// Create a layer for occlusion tokens
	occlusionConfig.tokensLayer = new PIXI.Container();
	occlusionConfig.tokensLayer.name = "OcclusionTokens";
	occlusionConfig.tokensLayer.sortableChildren = true;
	occlusionConfig.tokensLayer.cullable = 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;
}



// Comprehensive update mechanism for occlusion layer
function updateOcclusionLayer() {
	// Ensure canvas is ready and layer is initialized
	if (!canvas.ready) return;

	// Reinitialize if not yet set up
	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 => {
		// Find tiles that intersect with this token
		const intersectingTiles = tiles.filter(tile => 
			checkTokenTileIntersection(token, tile)
		);

		// If there are intersecting tiles, create an occlusion sprite
		if (intersectingTiles.length > 0) {
			const occlusionSprite = createOcclusionSprite(token, intersectingTiles);
			if (occlusionSprite) {
				occlusionConfig.tokensLayer.addChild(occlusionSprite);
			}
		}
	});
}







// Token-Tile Intersection Check
function checkTokenTileIntersection(token, tile) {
	// Basic intersection check using bounding boxes
	const tokenBounds = token.mesh.getBounds();
	const tileBounds = tile.mesh.getBounds();

	return (
		tokenBounds.x < tileBounds.x + tileBounds.width &&
		tokenBounds.x + tokenBounds.width > tileBounds.x &&
		tokenBounds.y < tileBounds.y + tileBounds.height &&
		tokenBounds.y + tokenBounds.height > tileBounds.y
	);
}







// Create Occlusion Sprite with Advanced Masking
function createOcclusionSprite(token, intersectingTiles) {
	if (!token.mesh || !token.mesh.texture) return null;

	// Create a sprite from the token's texture
	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);

	

	// Create a mask for the occlusion
	const mask = createOcclusionMask(token, intersectingTiles);
	
	if (mask) {
		sprite.mask = mask;
	}

	// sprite.filters = [colorMatrix, outlineFilter, alphaFilter];
	sprite.filters = [colorMatrix, outlineFilter]; //filter configs at eof

	//sprite.alpha = 0.75;
	sprite.eventMode = 'passive';

	return sprite;
}














// Advanced Occlusion Mask Creation
function createOcclusionMask(token, intersectingTiles, chunkSize = 4) {
	const maskGraphics = new PIXI.Graphics();
	maskGraphics.beginFill(0xffffff);

	// Get the current scene scale
	const stage = token.mesh.parent;
	const sceneScale = stage?.scale?.x ?? 1;
	
	// Define minimum dimension here so it's available in the entire function scope
	const minDimension = 1 / sceneScale;
	
	// Adjust chunk size based on scene scale
	const adjustedChunkSize = Math.max(chunkSize / sceneScale, 1);

	// Pre-calculate token bounds once to avoid repeated calculations
	const tokenBounds = token.mesh.getBounds(false, undefined);
	
	// Create a single reusable canvas - smaller initial size
	const tempCanvas = document.createElement('canvas');
	tempCanvas.width = 512;  // Start with a reasonable size
	tempCanvas.height = 512;
	const tempCtx = tempCanvas.getContext('2d', { 
			willReadFrequently: true,
			alpha: true 
	});

	// Store rectangles in a simple array
	const rectangles = [];

	for (const tile of intersectingTiles) {
			// Early validation of tile
			if (!tile.texture?.baseTexture?.resource?.source) continue;

			const tileTexture = tile.texture.baseTexture.resource.source;
			const tileBounds = tile.mesh.getBounds(false, undefined);
			
			// Calculate intersection area
			const x = Math.max(tokenBounds.x, tileBounds.x);
			const y = Math.max(tokenBounds.y, tileBounds.y);
			const width = Math.min(tokenBounds.x + tokenBounds.width, tileBounds.x + tileBounds.width) - x;
			const height = Math.min(tokenBounds.y + tokenBounds.height, tileBounds.y + tileBounds.height) - y;
			
			// Early exit if no valid intersection
			if (width <= 0 || height <= 0 || width < minDimension || height < minDimension) continue;

			// Skip if texture dimensions are invalid
			if (tileTexture.width <= 0 || tileTexture.height <= 0) continue;

			// Calculate canvas dimensions needed
			const canvasWidth = Math.ceil(Math.max(1, width));
			const canvasHeight = Math.ceil(Math.max(1, height));

			// Resize canvas only if needed
			if (tempCanvas.width < canvasWidth) tempCanvas.width = canvasWidth;
			if (tempCanvas.height < canvasHeight) tempCanvas.height = canvasHeight;

			// Clear only the portion we'll use
			tempCtx.clearRect(0, 0, canvasWidth, canvasHeight);

			try {
					// Calculate source coordinates
					const sourceX = Math.max(0, (x - tileBounds.x) * (tileTexture.width / tileBounds.width));
					const sourceY = Math.max(0, (y - tileBounds.y) * (tileTexture.height / tileBounds.height));
					const sourceWidth = Math.min(width * (tileTexture.width / tileBounds.width), tileTexture.width - sourceX);
					const sourceHeight = Math.min(height * (tileTexture.height / tileBounds.height), tileTexture.height - sourceY);

					// Draw tile portion
					tempCtx.drawImage(tileTexture,
							sourceX, sourceY, sourceWidth, sourceHeight,
							0, 0, canvasWidth, canvasHeight
					);

					// Get image data efficiently
					const imageData = tempCtx.getImageData(0, 0, canvasWidth, canvasHeight);
					const data = new Uint32Array(imageData.data.buffer);

					// Process chunks
					const chunksX = Math.ceil(width / adjustedChunkSize);
					const chunksY = Math.ceil(height / adjustedChunkSize);

					for (let cy = 0; cy < chunksY; cy++) {
							const yPos = cy * adjustedChunkSize;
							const chunkHeight = Math.min(adjustedChunkSize, height - yPos);

							// Early continue if chunk is too small
							if (chunkHeight <= 0) continue;

							for (let cx = 0; cx < chunksX; cx++) {
									const xPos = cx * adjustedChunkSize;
									const chunkWidth = Math.min(adjustedChunkSize, width - xPos);

									// Early continue if chunk is too small
									if (chunkWidth <= 0) continue;

									// Calculate texture coordinates once
									const textureX = Math.floor(xPos * (canvasWidth / width));
									const textureY = Math.floor(yPos * (canvasHeight / height));
									const textureWidth = Math.ceil(chunkWidth * (canvasWidth / width));
									const textureHeight = Math.ceil(chunkHeight * (canvasHeight / height));

									// Check for opaque pixels efficiently
									let hasOpaque = false;
									chunkLoop:
									for (let py = textureY; py < Math.min(textureY + textureHeight, canvasHeight); py++) {
											const rowOffset = py * canvasWidth;
											for (let px = textureX; px < Math.min(textureX + textureWidth, canvasWidth); px++) {
													if ((data[rowOffset + px] >>> 24) > 0) {
															hasOpaque = true;
															break chunkLoop;
													}
											}
									}

									if (hasOpaque) {
											rectangles.push({
													x: x + xPos,
													y: y + yPos,
													width: chunkWidth,
													height: chunkHeight
											});
									}
							}
					}
			} catch (error) {
					console.warn('Error processing tile:', error);
					continue;
			}
	}

	// Handle transform efficiently
	const worldTransform = stage.transform.worldTransform;
	maskGraphics.transform.setFromMatrix(worldTransform);

	// Batch draw rectangles
	for (const rect of rectangles) {
			const localX = rect.x / sceneScale;
			const localY = rect.y / sceneScale;
			const localWidth = Math.max(rect.width / sceneScale, minDimension);
			const localHeight = Math.max(rect.height / sceneScale, minDimension);
			
			maskGraphics.drawRect(localX, localY, localWidth, localHeight);
	}

	// Reset transform
	maskGraphics.transform.setFromMatrix(new PIXI.Matrix());

	// Clean up
	tempCanvas.remove();
	return maskGraphics;
}














// 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 fragmentShaderExternal = `
		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);
		 }
	`;

	const fragmentShaderInternal = `
		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;
				
			// Procurar por pixels transparentes apenas onde já existe cor
			if (ownColor.a > 0.0) {
				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, 1.0 - curColor.a);
				}
					
				// Aplicar a cor do outline apenas nas bordas internas
				float innerEdgeAlpha = maxAlpha * ownColor.a;
				gl_FragColor = mix(ownColor, outlineColor, innerEdgeAlpha);
				gl_FragColor.a = ownColor.a;
			} else {
				gl_FragColor = ownColor;
			}
		}
	`;

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

			// 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;
outlineFilter.padding = 0; // Reduz a área de renderização do filtro
outlineFilter.quality = 0.5; // Reduz qualidade para melhor performance
		
// Create a greyscale/dimming filter
const colorMatrix = new PIXI.ColorMatrixFilter();
colorMatrix.enabled = true;
colorMatrix.alpha = 1;
colorMatrix.matrix = [
	0.3,  0.0,  0.0,  0.0,  0.0,
	0.0,  0.3,  0.0,  0.0,  0.0,
	0.0,  0.0,  0.3,  0.0,  0.0,
	0.0,  0.0,  0.0,  1.0,  0.0
];

const alphaFilter = new PIXI.AlphaFilter();
alphaFilter.alpha = 0.5;