export function norm(value, min, max) {
    return (value - min) / (max - min);
}

function getBoolFlag(obj, flag, _default = false) {
    const MODULE_ID = "grape_juice-isometrics";
    const val = obj.getFlag(
        MODULE_ID,
        flag,
    );
    if (val === undefined) {
        // obj.setFlag(MODULE_ID, flag, _default);
        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.indexer = new Map();
        this.tileToIndexer = new Map();
        this.renderTextureSprite = null;
        this.boundsMap = new Map();
        this.rendered=false;
        // this.generateAlphaMaskIndex = mdebounce(() => this._generateAlphaMaskIndex(), FRAME_TIME());
        this.generateAlphaMaskIndex = this._generateAlphaMaskIndex;

    }

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

    destructor() {
        // this.container.destroy(true)
        // this.renderTexture.destroy(true)
        this.indexer = new Map();
        this.tileToIndexer = new Map();
        this.boundsMap = new Map();
        // delete(this.container);
        // delete(this.renderTexture);
        // this.unload_filters()
    }

    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;
            }

        })
    }



    deleteTileFromIndex(tile_id) {
        let ret = this.tileToIndexer.get(tile_id);
        if (!ret) return;
        let [indexer_index, cloned_tile] = ret;
        cloned_tile.destroy()
        this.indexer.delete(indexer_index);
        this.tileToIndexer.delete(tile_id);

        this.generateAlphaMaskIndex();

    }

    reindexTile(tile_id, visibleTiles) {
        // this works wrong,
        // first this can fail if a tile didn't have the mask flag
        // second, we need to check if the "bounds" of the tile changed
        // meaning it's geometry changed, and also consider Z-index maybe
        // if it did, we need to re-paint the tile's anchor point into the sprite
        // same goes if the position changed, so i guess if any of the bounds/position
        // is different, the tile needs to be repainted with that info, and then updated
        // and have the renderTexture redrawn.
        // maybe for visible invisible tiles, we can simplify the loop, by just
        // hooking it's visibility status in the sight testing, into this
        // so no need to loop it.

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

    #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 = norm(x, canvas.dimensions.width, 0)
            normalized_x = ((0xff * normalized_x) & 255) / 255;

            let normalized_y = norm(y, 0, canvas.dimensions.height)
            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;
    }

    #boundsKey(sprite) {
        return `K.${sprite.anchor}.${sprite.width}.${sprite.height}.${sprite.scale}.${sprite.angle}.${sprite.texture.baseTexture.uid}`
    }

    //
    #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;
    }

    #rowSearch(data, width, height) {
        const getPixelAlpha = function (data, x, y, width) {
            let stride = width * 4;
            let _x = (4 * x) + 3;
            let _y = stride * y;
            return data[_x + _y];
        }


        for (let y = 0; y < height; y++) { //col
            for (let x = 0; x < width; x++) { //row
                if (getPixelAlpha(data, x, y, width) !== 0) {
                    break;
                }
            }
        }
    }


    #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;

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

    #getBounds(sprite, tile) {

        return this.#calculateBounds(sprite, tile);
//this is the bug?!

        let key = this.#boundsKey(sprite);
        let bounds = this.boundsMap.get(key);
        if (bounds === undefined) {
            bounds = this.#calculateBounds(sprite, tile);
            this.boundsMap.set(key, bounds);
        }
        return bounds;
    }

    #cloneAlphaTileSprite(tile) {
        // tile.texture.baseTexture.alphaMode = PIXI.ALPHA_MODES.NPM;

        let sprite = PIXI.Sprite.from(tile.texture);
        // sprite.tint = 16777215;
        sprite.isSprite = true;
        // sprite.blendMode = 13;
        sprite.name = tile.document.id;
        // sprite.drawMode = 4;

        let z = (tile.x + (tile.width * 0.5)) - (tile.y + (tile.height * 0.5));
        let max = canvas.dimensions.width;
        let min = -canvas.dimensions.height;


        this.#copyTileToSprite(sprite, tile);


        return sprite;
    }

    /*
       // avoid alpha shenanigans for base64 export
        PIXI.Extract.arrayPostDivide = function(e, t) {
            for (var r = 0; r < e.length; r += 4) {
                var n = t[r + 3] = e[r + 3];
                t[r] = e[r];
                t[r + 1] = e[r + 1];
                t[r + 2] = e[r + 2];
            }
        }
    */

    _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
        });

        // this.renderer.extract.base64(renderTexture).then(x => {
        //     console.warn(x)
        // })


    }

    #testPixelRBG(x, y) {
        const webglPixels = new Uint8Array(4);
        const gl = this.renderer.gl;
        gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, webglPixels);
        return webglPixels;
    }

    #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];
    }

    updateTileVisibility(tile_id, visible) {
        let ret = this.tileToIndexer.get(tile_id) || this.#addTileToIndex(tile_id);
        if (ret) {
            let [indexer_index, cloned_tile] = ret;
            cloned_tile.alpha = visible ? 1 : 0;
        }
    }

    getSpriteMask() {
        // console.error(this.renderer.extract.base64(this.renderTexture));
        // debugger;

        return this.renderTexture;

    }


    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
        }

        // let x = token.x + ((canvas.grid.size / 2) * token.document.width);
        x = norm(x, canvas.dimensions.width, 0)
        x = ((0xff * x) & 255) / 255;

        // let y = token.y + ((canvas.grid.size / 2) * token.document.height);
        y = norm(y, 0, canvas.dimensions.height)
        y = ((0xff * y) & 255) / 255;
        return {x, y}
    }

    refreshToken(_token, skip_visible = false) {


        let token = _token?.mesh ? _token : canvas.tokens.get(_token.id);
        // let token = _token?.mesh || _token;

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

        let {x, y} = this.calculatePosition(token);
        let enable_outline = true;
        let outline_color = "#c3fe20";
        let fill_color = "#6a5858";
        let fill_color_alpha = true ? 1 : 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, token.document.disposition, x, y, enable_outline, outline_color, fill_color, fill_color_alpha);
                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(token.document.disposition, x, y, enable_outline, outline_color, fill_color, fill_color_alpha);
                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(token.document.disposition, x, y, enable_outline, outline_color, fill_color, fill_color_alpha);
                break;
        }
    }

}


export const vertex = `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;
}`

export const outline_occlusion_shader = `
varying vec2 vTextureCoord;
uniform sampler2D uSampler;
uniform sampler2D mask;
varying vec2 vMaskCoord;
uniform bool enable_outline;


varying vec4 vColor;


uniform float outerStrength;
uniform float innerStrength;

uniform vec4 glowColor;
uniform vec4 knockoutColor;
uniform vec4 fill_color;

uniform vec4 filterArea;
uniform vec4 filterClamp;
uniform bool knockout;

const float PI = 3.14159265358979323846264;

const float DIST = 8.0;
const float ANGLE_STEP_SIZE = min(1.2500000, PI * 2.0);
const float ANGLE_STEP_NUM = ceil(PI * 2.0 / ANGLE_STEP_SIZE);

const float MAX_TOTAL_ALPHA = ANGLE_STEP_NUM * DIST * (DIST + 1.0) / 2.0;
uniform float z;
uniform float x;
uniform float y;

void main(void) {
    vec4 color = texture2D(uSampler, vTextureCoord);
    vec4 mask = texture2D(mask, vMaskCoord);
    if ((mask.b >= y &&  mask.g >= x)){

        if (enable_outline){
            vec2 px = vec2(1.0 / filterArea.x, 1.0 / filterArea.y);

            float totalAlpha = 0.0;

            vec2 direction;
            vec2 displaced;
            vec4 curColor;

            for (float angle = 0.0; angle < PI * 2.0; angle += ANGLE_STEP_SIZE) {
                direction = vec2(cos(angle), sin(angle)) * px;

                for (float curDistance = 0.0; curDistance < DIST; curDistance++) {
                    displaced = clamp(vTextureCoord + direction *
                    (curDistance + 1.0), filterClamp.xy, filterClamp.zw);

                    curColor = texture2D(uSampler, displaced);

                    totalAlpha += (DIST - curDistance) * curColor.a;
                }
            }

            curColor = texture2D(uSampler, vTextureCoord);

            float alphaRatio = (totalAlpha / MAX_TOTAL_ALPHA);

            float innerGlowAlpha = (1.0 - alphaRatio) * innerStrength * curColor.a;
            float innerGlowStrength = min(1.0, innerGlowAlpha);

            vec4 innerColor = mix(curColor, glowColor, innerGlowStrength);

            float outerGlowAlpha = alphaRatio * outerStrength * (1. - curColor.a);
            float outerGlowStrength = min(1.0 - innerColor.a, outerGlowAlpha);

            vec4 outerGlowColor = outerGlowStrength * glowColor.rgba;


            float resultAlpha = outerGlowAlpha + innerGlowAlpha;
            gl_FragColor = vec4(glowColor.rgb * resultAlpha, resultAlpha);


            if (gl_FragColor.a <= 0.15 && color.a != 0.0){
                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*(1.0 - gl_FragColor.a));
            }
        }
        else{
            if (gl_FragColor.a <= 0.15 && color.a != 0.0){
                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*(1.0 - gl_FragColor.a));
            }
        }
    } else {
        gl_FragColor = color;
    }
}
`;

export class TextureMaskFilter extends PIXI.Filter {

    maskMatrix;
    maskTexture;

    /** @ignore */
    constructor(texture, disposition, x, y, enable_outline, outline_color, fill_color, fill_alpha) {
        // super(vertex, fragment3aa, undefined);
        // super(vertex, fragment33, undefined);
        super(vertex, outline_occlusion_shader, undefined);

        // if (outline_color){
        //     this.uniforms.glowColor = new Float32Array(PIXI.utils.hex2rgb(PIXI.utils.string2hex(outline_color)));
        //     this.uniforms.fill_color =  new Float32Array([...PIXI.utils.hex2rgb(PIXI.utils.string2hex(fill_color)),fill_alpha])
        // }
        // switch (disposition) {
        //     case 1:
        //         this.uniforms.glowColor = new Float32Array([0.0, 0.7019607843137254, 0.0, 1]);
        //         break;
        //     case 0:
        //         this.uniforms.glowColor = new Float32Array([0.0, 0.03529411764705882, 1, 1]);
        //         break;
        //     case -1:
        //         this.uniforms.glowColor = new Float32Array([0.7019607843137254, 0.03529411764705882, 0.0, 1]);
        //         break;
        // }

        // this.uniforms.x = x;
        // this.uniforms.y = y;
        Object.assign(this, {
            outerStrength: 0.5, innerStrength: 0.5, padding: 0, knockout: !1
        });
        this.maskTexture = texture;
        this.maskMatrix = new PIXI.Matrix();

        const t = canvas.app.ticker.lastTime;
        this.uniforms.outerStrength =this.outerStrength * 2.0;// Math.oscillation(this.outerStrength * 1.0, this.outerStrength * 2.0, t, 6000);
        this.uniforms.innerStrength = this.outerStrength * 2.0;//Math.oscillation(this.innerStrength * 1.0, this.innerStrength * 2.0, t, 6000);

        this.updateUniforms(disposition, x, y, enable_outline, outline_color, fill_color, fill_alpha);

    }

    getName() {
        return "TextureMaskFilter";
    }

    updateUniforms(disposition, x, y, enable_outline, outline_color, fill_color, fill_alpha) {
        if (outline_color) {
            this.uniforms.enable_outline = enable_outline;
            this.uniforms.glowColor = new Float32Array(PIXI.utils.hex2rgb(PIXI.utils.string2hex(outline_color)));
            this.uniforms.fill_color = new Float32Array([...PIXI.utils.hex2rgb(PIXI.utils.string2hex(fill_color)), fill_alpha])
        }
        this.uniforms.x = x;
        this.uniforms.y = y;
    }

    calculateSpriteMatrix(outputMatrix, texture, filterManager) {
        // debugger;
        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);
        // mappedMatrix.translate(sprite.anchor.x, sprite.anchor.y);

        return mappedMatrix;
    }

    /**
     * Applies the filter
     * @param filterManager - The renderer to retrieve the filter from
     * @param input - The input render target.
     * @param output - The target to output to.
     * @param clearMode - Should the output be cleared before rendering to it.
     */
    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) {
            // margin = 0.0, let it bleed a bit, shader code becomes easier
            // assuming that atlas textures were made with 1-pixel padding
            tex.uvMatrix = new PIXI.TextureMatrix(tex, 0.0);
        }
        tex.uvMatrix.update();

        this.uniforms.npmAlpha = tex.baseTexture.alphaMode ? 0.0 : 1.0;
        this.uniforms.mask = tex;
        // get _normalized sprite texture coords_ and convert them to _normalized atlas texture coords_ with `prepend`
        this.uniforms.otherMatrix = this.calculateSpriteMatrix(this.maskMatrix, tex, filterManager)
            .prepend(tex.uvMatrix.mapCoord);
        // this.uniforms.alpha = maskSprite.worldAlpha;
        this.uniforms.maskClamp = tex.uvMatrix.uClampFrame;

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