function getBoolFlag(obj, flag, _default = false) {
    const MODULE_ID = "grape_juice-isometrics";
    const val = obj.getFlag(
        MODULE_ID,
        flag,
    );
    if (val === undefined) {
        return _default;
    } else {
        return val;
    }
}

function setFields(dst,src,keys){
    let changed=false;
    for (const key of keys){
        let diff = false;
        if (dst[key].equals && src[key].equals){
            diff = (!dst[key].equals(src[key]));
        }
        else{
            diff = (dst[key] !== src[key]);
        }
        if (diff){
            changed=true
            dst[key] = src[key]
        }
    }
    return changed;
}




export class TokenOccluder {
    constructor() {
        this.renderer = canvas.app.renderer;
        this.renderTexture = PIXI.RenderTexture.create({
            width: canvas.dimensions.width, height: canvas.dimensions.height
        });
        this.container = new PIXI.Container();
        this.container.sortableChildren = true;
        this.tileToIndexer = new Map();
        this.renderTextureSprite = null;
        this.rendered=false;
        this.generateAlphaMaskIndex = this._generateAlphaMaskIndex;
    }

    async init() {
        this.generateAlphaMaskIndex();
    }

    _generateAlphaMaskIndex() {
        canvas.tiles.objects.children.filter(x => getBoolFlag(x.document, 'is_tile_occluder')).sort().forEach((t, i) => {
            if (!this.tileToIndexer.has(t.document._id)) {
                this.#addTileToIndex(t.document._id, i);
            }
        });

        let renderTexture = this.renderTexture;
        this.renderer.render(this.container, {
            renderTexture
        });
    }

    #addTileToIndex(tile_id, index) {
        let tile = canvas.primary.tiles.get(`Tile.${tile_id}`);
        if (tile === undefined) return;

        let cloned_tile = this.#cloneAlphaTileSprite(tile);
        this.container.addChild(cloned_tile);
        this.tileToIndexer.set(tile_id, [null, cloned_tile]);
        return [null, cloned_tile];
    }

    #cloneAlphaTileSprite(tile) {
        let sprite = PIXI.Sprite.from(tile.texture);
        sprite.isSprite = true;
        sprite.name = tile.document.id;

        this.#copyTileToSprite(sprite, tile);
        return sprite;
    }

    #copyTileToSprite(sprite, tile) {
        const changed = setFields(sprite, tile, ["anchor", "width", "height", "position", "scale", "angle", "zIndex"]);

        if (changed) {
            let bounds = this.#getBounds(sprite, tile);
            if (!bounds) return;
            const x = tile.position.x + bounds.left - (bounds.width / 2);
            const y = tile.position.y + (bounds.height / 2) - (bounds.height - bounds.bottom);


            let normalized_x = (x - canvas.dimensions.width) / (0 - canvas.dimensions.width);
            normalized_x = ((0xff * normalized_x) & 255) / 255;

            let normalized_y = (y - 0) / (canvas.dimensions.height - 0);
            normalized_y = ((0xff * normalized_y) & 255) / 255;



            sprite.meta = {
                x: x,
                y: y,
                nx: normalized_x,
                ny: normalized_y
            }

            let filter = new PIXI.Filter(
                null,
                `
                varying vec2 vTextureCoord;
                uniform sampler2D uSampler;
                uniform float x;
                uniform float y;

                void main(void)
                {
                    mediump vec4 color = texture2D(uSampler, vTextureCoord);
                    color.a = floor(0.5+color.a);
                   gl_FragColor = vec4(color.a, x*color.a, y*color.a ,color.a);
                }
                `,
                {x: normalized_x, y: normalized_y}
            );

            sprite.filters = [filter]
        }
        return changed;
    }


    reindexTile(tile_id, visibleTiles) {
        // same goes for delete
        if (!this.tileToIndexer.has(tile_id)) return;

        let [indexer_index, cloned_tile] = this.tileToIndexer.get(tile_id);
        let tile = canvas.primary.tiles.get(`Tile.${tile_id}`);
        if (!tile) return;

        //todo: update the calculated xy point, figure out why when it loads the points are all wrong.
        if (this.#copyTileToSprite(cloned_tile, tile)) this.generateAlphaMaskIndex();
    }
    
    destructor() {
        this.indexer = new Map();
        this.tileToIndexer = new Map();
        this.boundsMap = new Map();
    }

    unload_filters() {
        canvas.tokens.objects.children.forEach(token=>{
            let index = token.mesh.filters.findIndex(x => typeof x.getName === "function" && x.getName() === "TextureMaskFilter");
            if (index>=0){
                token.mesh.filters[index]=null;
            }

        })
    }

    

    #checkRow(data, width, y) {
        for (let x = 0, index = 4 * y * width; x < width; ++x, index += 4) {
            if (data[index + 3] > 255 / 2) return false;
        }
        return true;
    }

    #checkColumn(data, width, x, top, bottom) {
        const stride = 4 * width;
        for (let y = top, index = (top * stride) + (4 * x); y <= bottom; ++y, index += stride) {
            if (data[index + 3] > 255 / 2) return false;
        }
        return true;
    }







    #getBounds(sprite, tile) {
        return this.#calculateBounds(sprite, tile);
    }

    #calculateBounds(sprite, tile) {
        sprite.calculateBounds();
        let {width, height} = sprite.getBounds()
        const size = Math.max(Math.ceil(width), Math.ceil(height));
        let {x: x_scale, y: y_scale} = canvas.primary.transform.scale;

        width = Math.ceil(size);
        height = Math.ceil(size);

        const renderTexture = PIXI.RenderTexture.create({width, height});
        let {x: pos_x, y: pos_y} = sprite.position;
        let {x: anchor_x, y: anchor_y} = sprite.anchor;
        sprite.anchor.set(0.5, 0.5);
        sprite.position.set(width / 2, height / 2);
        this.renderer.render(sprite, {renderTexture})
        const pixels = this.renderer.extract.pixels(renderTexture);
        renderTexture.destroy(false);
        sprite.position.set(pos_x, pos_y);
        sprite.anchor.set(anchor_x, anchor_y);

        let left = 0;
        let top = 0;
        let right = width - 1;
        let bottom = height - 1;
        while (top < height && this.#checkRow(pixels, width, top)) ++top;
        if (top === height) return undefined;
        while (this.#checkRow(pixels, width, bottom)) --bottom;
        while (this.#checkColumn(pixels, width, left, top, bottom)) ++left;
        while (this.#checkColumn(pixels, width, right, top, bottom)) --right;
        ++right;
        ++bottom;

        //console.log('left, top, right, bottom, width, height :>> ', left, top, right, bottom, width, height);

        return {left, top, right, bottom, width, height};
    }

    
    
    

    getSpriteMask() {
        return this.renderTexture;
    }


    refreshToken(_token, skip_visible = false) {
        let token = _token?.mesh ? _token : canvas.tokens.get(_token.id);

        if (((token.visible == false) && skip_visible == false) || token?.mesh === undefined) return;


    
        // calculate token center position normalized
        let x = token.center.x;
        let y = token.center.x;

        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;

        x = x / canvas.dimensions.width;
        y = y / canvas.dimensions.height;






        

        let fill_color = "#6a5858";
        let fill_color_alpha = true ? 1.0 : 0.0;

        if (token.mesh.filters === null) {
            token.mesh.filters = [];
        }
        let occ_idx = token.mesh.filters.findIndex(x => typeof x.getName === "function" && x.getName() === "TextureMaskFilter");
        let lastIndex = token.mesh.filters.length - 1; // Get the last index of the filters array

        switch (occ_idx) {
            case -1: // No existing TextureMaskFilter found
                const renderTexture = this.getSpriteMask();
                const textureMask = new TextureMaskFilter(renderTexture, x, y, fill_color, fill_color_alpha);
                // console.log('renderTexture :>> ', renderTexture);
                // console.log('textureMask :>> ', textureMask);
                token.mesh.filters.push(textureMask); // Add new filter to the end
                break;
            case lastIndex: // The existing TextureMaskFilter is already the last element
                // Directly update uniforms without moving the filter
                token.mesh.filters[occ_idx].updateUniforms(x, y);
                break;
            default: // Existing TextureMaskFilter found but not the last element
                // Move the existing filter to the end
                const [existingFilter] = token.mesh.filters.splice(occ_idx, 1);
                token.mesh.filters.push(existingFilter);
                // Update uniforms of the filter now at the end
                token.mesh.filters[lastIndex].updateUniforms(x, y);
                break;
        }
    }

}



















export const vertex = `
    attribute vec2 aVertexPosition;
    attribute vec2 aTextureCoord;

    uniform mat3 projectionMatrix;
    uniform mat3 otherMatrix;

    varying vec2 vMaskCoord;
    varying vec2 vTextureCoord;
    varying mat3 testMatrix;

    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;

        testMatrix = otherMatrix;
    }
`

const outline_occlusion_shader = /* gls l */`
    uniform sampler2D uSampler;
    uniform sampler2D mask;
    uniform vec4 fill_color;
    uniform float x;
    uniform float y;
    varying vec2 vTextureCoord;
    varying vec2 vMaskCoord;
    varying mat3 testMatrix;

    void main(void) {
        vec4 color = texture2D(uSampler, vTextureCoord);
        vec4 mask = texture2D(mask, vMaskCoord);
        if (mask.a >= x){
            float output_alpha = fill_color.a;
            vec3 output_color = fill_color.rgb * output_alpha;
            gl_FragColor = vec4(output_color.rgb*color.rgb, output_alpha)*(color.a);
            //gl_FragColor = vec4(mask.x, 0.0, 0.0, 1.0);
            
            //gl_FragColor = vec4(testMatrix[0][0], 1.0, 1.0, 1.0);
            //gl_FragColor = vec4(testMatrix[0][1], 1.0, 1.0, 1.0);
            //gl_FragColor = vec4(testMatrix[0][2], 1.0, 1.0, 1.0);
            //gl_FragColor = vec4(testMatrix[1][0], 1.0, 1.0, 1.0);
            //gl_FragColor = vec4(testMatrix[1][1], 1.0, 1.0, 1.0);
            //gl_FragColor = vec4(testMatrix[1][2], 1.0, 1.0, 1.0);
            //gl_FragColor = vec4(testMatrix[2][0], 1.0, 1.0, 1.0);
            //gl_FragColor = vec4(testMatrix[2][1], 1.0, 1.0, 1.0);
            //gl_FragColor = vec4(testMatrix[2][2], 1.0, 1.0, 1.0);
            
        } 
        else {
            gl_FragColor = color;
        }
    }
`;

export class TextureMaskFilter extends PIXI.Filter {

    maskMatrix;
    maskTexture;

    constructor(texture, x, y, fill_color, fill_alpha) {
        super(vertex, outline_occlusion_shader, undefined);

        this.maskTexture = texture;
        this.maskMatrix = new PIXI.Matrix();
        this.uniforms.fill_color = new Float32Array([0.0, 0.0, 0.9, 1.0]);

        this.updateUniforms(x, y);
    }

    getName() {
        return "TextureMaskFilter";
    }

    updateUniforms(x, y) {
        this.uniforms.x = x;
        this.uniforms.y = y;
    }

    calculateSpriteMatrix(outputMatrix, texture, filterManager) {
        const {sourceFrame, destinationFrame} = filterManager.activeState;
        const {orig} = texture;
        const mappedMatrix = outputMatrix.set(destinationFrame.width, 0, 0, destinationFrame.height, sourceFrame.x, sourceFrame.y);
        const worldTransform = canvas.stage.worldTransform.copyTo(PIXI.Matrix.TEMP_MATRIX);

        worldTransform.invert();
        mappedMatrix.prepend(worldTransform);
        mappedMatrix.scale(1.0 / orig.width, 1.0 / orig.height);

        return mappedMatrix;
    }

    apply(filterManager, input, output, clearMode) {
        const tex = this.maskTexture;

        if ((!tex) || (!tex.valid) || (!tex.baseTexture) ||(tex.baseTexture.destroyed)) {
            console.warn("bad text");
            return;
        }

        if (!tex.uvMatrix) {
            tex.uvMatrix = new PIXI.TextureMatrix(tex, 0.0);
        }
        tex.uvMatrix.update();

        this.uniforms.mask = tex;
        this.uniforms.otherMatrix = this
            .calculateSpriteMatrix(this.maskMatrix, tex, filterManager)
            .prepend(tex.uvMatrix.mapCoord);

        //console.log('otherMatrix :>> ', this.uniforms.otherMatrix);

        filterManager.applyFilter(this, input, output, clearMode);
    }
}