import { ClampToEdgeWrapping, DataTexture, LinearFilter, RedFormat, UnsignedByteType } from "three";
import { assert } from "../../../core/assert.js";
import { BinaryDataType } from "../../../core/binary/type/BinaryDataType.js";
import { RowFirstTable } from "../../../core/collection/table/RowFirstTable.js";
import { RowFirstTableSpec } from "../../../core/collection/table/RowFirstTableSpec.js";
import Signal from "../../../core/events/signal/Signal.js";
import Vector1 from "../../../core/geom/Vector1.js";
import Vector2 from "../../../core/geom/Vector2.js";
import Vector4 from "../../../core/geom/Vector4.js";
import { clamp } from "../../../core/math/clamp.js";
import { max2 } from "../../../core/math/max2.js";
import { min2 } from "../../../core/math/min2.js";
import { computeUnsignedDistanceField } from "../../graphics/texture/sampler/distance/computeUnsignedDistanceField.js";
import { Sampler2D } from "../../graphics/texture/sampler/Sampler2D.js";
import { writeSample2DDataToDataTexture } from "../../graphics/texture/sampler/writeSampler2DDataToDataTexture.js";

const samplePosition = [];

const corners2d = new Float64Array(8);

/**
 * Measured in Uint8 channel value per second.
 * @example 255 would mean complete fade happens in 1s, 25.5 would mean fade happens over 10 seconds
 * @readonly
 * @type {number}
 */
const FADE_SPEED = 255;

/**
 * pre-fade to enable correct distance field calculation
 * @readonly
 * @type {number}
 */
const INITIAL_FADE_VALUE = 254;

/**
 *
 * @type {RowFirstTableSpec}
 */
const revealMaskTableSpec = new RowFirstTableSpec([
    BinaryDataType.Int32, // Index
    BinaryDataType.Float32, // Precise current value
    BinaryDataType.Float32 // Fade speed
]);

const fadeRow = [];

/**
 * @class
 */
export class FogOfWar {

    /**
     *
     * @type {boolean}
     */
    textureNeedsUpdate = true;
    /**
     *
     * @type {boolean}
     */
    distanceFieldNeedsUpdate = false;

    /**
     *
     * @type {DataTexture|null}
     */
    texture = null;

    /**
     * Resolution scale of FoW versus terrain resolution, higher number makes FoW more detailed
     * @type {Vector1}
     */
    scale = new Vector1(1);

    /**
     * Size of the fog area
     * @type {Vector2}
     */
    size = new Vector2(0, 0);

    color = new Vector4(0.1, 0.1, 0.1, 1);

    /**
     * Contains indices of pixes that are currently being updated
     * @type {RowFirstTable}
     */
    fadeMask = new RowFirstTable(revealMaskTableSpec);

    on = {
        textureChanged: new Signal()
    };

    /**
     *
     * @type {Sampler2D}
     */
    sampler = Sampler2D.uint8(1, 0, 0);

    /**
     *
     * @type {Sampler2D}
     */
    distanceSampler = Sampler2D.uint8(1, 0, 0);

    /**
     *
     * @param {number} timeDelta
     */
    updateFade(timeDelta) {
        const samplerData = this.sampler.data;

        const fadeMask = this.fadeMask;

        let numRows = fadeMask.length;

        let i = 0;

        for (; i < numRows; i++) {
            //read row
            fadeMask.readRow(i, fadeRow);

            //decode row
            const index = fadeRow[0];
            const oldValue = fadeRow[1];
            const speed = fadeRow[2];

            //compute new value
            const value = oldValue + (speed * timeDelta);

            let writeValue = value | 0;

            //write new value
            fadeMask.writeCellValue(i, 1, value);

            if (value <= 0) {
                if (speed < 0) {
                    //we were going down and reached the end, clear row
                    fadeMask.removeRows(i, 1);
                    i--;
                    numRows--;
                }
                writeValue = 0;
            } else if (value >= 255) {
                writeValue = 255;

                if (speed > 0) {
                    //we were going down and reached the end, clear row
                    fadeMask.removeRows(i, 1);
                    i--;
                    numRows--;
                }
            }

            if ((oldValue | 0) !== writeValue) {
                //value has changed
                samplerData[index] = writeValue;
                this.textureNeedsUpdate = true;
            }
        }
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     */
    revealPoint(x, y) {
        const fadeMask = this.fadeMask;

        const sampler = this.sampler;

        const samplerData = sampler.data;

        const samplerWidth = sampler.width;

        const index = (y + 1) * samplerWidth + (x + 1);

        if (samplerData[index] === 255) {
            samplerData[index] = INITIAL_FADE_VALUE;

            //add record to fade mask
            fadeRow[0] = index;
            fadeRow[1] = INITIAL_FADE_VALUE;
            fadeRow[2] = -FADE_SPEED;

            fadeMask.addRow(fadeRow);

            this.distanceFieldNeedsUpdate = true;
        }
    }

    clear() {
        this.fadeMask.clear();

        this.sampler.data.fill(255);

        this.distanceFieldNeedsUpdate = true;
        this.textureNeedsUpdate = true;
    }

    revealAll() {
        this.fadeMask.clear();

        this.sampler.data.fill(0);

        this.distanceFieldNeedsUpdate = true;
        this.textureNeedsUpdate = true;
    }

    concealAll() {
        this.clear();
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @param {number} radius
     */
    reveal(x, y, radius) {
        assert.isNumber(x, 'x');
        assert.isNumber(y, 'y');
        assert.isNumber(radius, 'radius');

        const sampler = this.sampler;
        const samplerWidth = sampler.width;

        const lastPixelY = sampler.height - 1;
        const lastPixelX = samplerWidth - 1;

        let changeFlag = false;
        const samplerData = sampler.data;

        const fadeMask = this.fadeMask;

        sampler.traverseCircle(x + 1, y + 1, radius, function (x, y, sampler) {

            if (x <= 0 || x >= lastPixelX || y <= 0 || y >= lastPixelY) {
                //edges are reserved to preserve cover outside of the working area
                return;
            }

            const index = y * samplerWidth + x;

            if (samplerData[index] === 255) {
                samplerData[index] = INITIAL_FADE_VALUE;

                //add record to fade mask
                fadeRow[0] = index;
                fadeRow[1] = INITIAL_FADE_VALUE;
                fadeRow[2] = -FADE_SPEED;

                fadeMask.addRow(fadeRow);

                changeFlag = true;
            }

        });

        if (changeFlag) {

            this.rebuildDistanceField();

        }
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @param {number} radius
     */
    conceal(x, y, radius) {
        assert.isNumber(x, 'x');
        assert.isNumber(y, 'y');
        assert.isNumber(radius, 'radius');

        const sampler = this.sampler;
        const samplerWidth = sampler.width;

        const lastPixelY = sampler.height - 1;
        const lastPixelX = samplerWidth - 1;

        let changeFlag = false;
        const samplerData = sampler.data;

        const fadeMask = this.fadeMask;

        sampler.traverseCircle(x + 1, y + 1, radius, function (x, y, sampler) {

            if (x <= 0 || x >= lastPixelX || y <= 0 || y >= lastPixelY) {
                //edges are reserved to preserve cover outside of the working area
                return;
            }

            const index = y * samplerWidth + x;

            const value = samplerData[index];

            if (value !== 255) {

                //add record to fade mask
                fadeRow[0] = index;
                fadeRow[1] = value;
                fadeRow[2] = FADE_SPEED;

                fadeMask.addRow(fadeRow);

                changeFlag = true;
            }

        });

        if (changeFlag) {

            this.rebuildDistanceField();

        }
    }

    dispose() {
        if (this.texture !== null) {
            this.texture.dispose();
        }
    }

    updateTexture() {
        const s = this.sampler;

        /**
         *
         * @type {Uint8Array}
         */
        const data = s.data;

        if (this.texture === null) {

            const texture = new DataTexture(data, s.width, s.height, RedFormat, UnsignedByteType);

            texture.flipY = false;
            texture.wrapS = ClampToEdgeWrapping;
            texture.wrapT = ClampToEdgeWrapping;

            texture.generateMipmaps = false;

            texture.magFilter = LinearFilter;
            texture.minFilter = LinearFilter;

            texture.needsUpdate = true;

            this.texture = texture;

        } else {

            writeSample2DDataToDataTexture(s, this.texture);

        }


        this.on.textureChanged.send0();

        //clear flag
        this.textureNeedsUpdate = false;
    }

    /**
     *
     * @param {number} timeDelta
     */
    update(timeDelta) {
        this.updateFade(timeDelta);

        if (this.textureNeedsUpdate) {
            this.updateTexture();
        }

        if (this.distanceFieldNeedsUpdate) {
            this.rebuildDistanceField();
        }
    }

    /**
     * Compute bounding box of revealed area in grid space, where coordinate system maps to discrete texels of the fog
     * @param {AABB2} result
     */
    computeRevealedGridBoundingRectangle(result) {
        const sampler = this.sampler;
        const data = sampler.data;

        const l = data.length;

        const width = sampler.width;

        let i = 0;

        let x0 = width - 1, y0 = sampler.height - 1, x1 = 0, y1 = 0;

        let x, y;

        for (; i < l; i++) {
            const value = data[i];

            if (value !== 255) {
                //revealed
                x = i % width;
                y = Math.floor(i / width);

                x0 = min2(x, x0);
                y0 = min2(y, y0);

                x1 = max2(x, x1);
                y1 = max2(y, y1);
            }
        }

        result.set(x0, y0, x1, y1);
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @param {number[]} result
     */
    worldToSamplePosition(x, y, result) {
        const scale = this.scale.getValue();

        const size = this.size;

        const sizeX = size.x;
        const sizeY = size.y;

        const scaleX = (sizeX - 1) / (scale * sizeX);
        const scaleY = (sizeY - 1) / (scale * sizeY);

        result[0] = x * scaleX + 1;
        result[1] = y * scaleY + 1;
    }

    /**
     *
     * @param {AABB3} aabb
     * @param {Vector3} cameraFocalPoint
     * @param {number} clearance distance to nearest non-occluding cell
     */
    computeAABBVisibility(aabb, cameraFocalPoint, clearance) {
        assert.isNumber(clearance, 'clearance');

        let sx0 = this.size.x - 1;
        let sy0 = this.size.y - 1;
        let sx1 = 0;
        let sy1 = 0;

        let i;

        //cast ray from object's position along inverted view plane normal
        corners2d[0] = aabb.x0;
        corners2d[1] = aabb.z0;

        corners2d[2] = aabb.x0;
        corners2d[3] = aabb.z1;

        corners2d[4] = aabb.x1;
        corners2d[5] = aabb.z0;

        corners2d[6] = aabb.x1;
        corners2d[7] = aabb.z1;

        const distanceSampler = this.distanceSampler;

        //check corners, this is fast compared to checking each cell of fog overlapping bounding box
        for (i = 0; i < 8; i += 2) {

            const x = corners2d[i];
            const z = corners2d[i + 1];

            //scale down position to obtain sample position
            this.worldToSamplePosition(x, z, samplePosition);
            const sampleX = samplePosition[0];
            const sampleY = samplePosition[1];

            const sX = clamp(sampleX, 0, this.size.x - 1);
            const sY = clamp(sampleY, 0, this.size.y - 1);

            const distance = distanceSampler.readChannel(sX | 0, sY | 0, 0);

            //Check if cell is visible
            if (distance <= clearance) {
                //if fog is not 100% opaque - return as visible
                return true;
            }

            sx0 = min2(sx0, sX);
            sy0 = min2(sy0, sY);

            sx1 = max2(sx1, sX);
            sy1 = max2(sy1, sY);
        }

        sx0 = Math.floor(sx0);
        sy0 = Math.floor(sy0);
        sx1 = Math.ceil(sx1);
        sy1 = Math.ceil(sy1);

        const shExtX = Math.ceil((sx1 - sx0) / 2);
        const shExtY = Math.ceil((sy1 - sy0) / 2);

        const sMidX = sx0 + shExtX;
        const sMidY = sy0 + shExtY;

        const centerOcclusionDistance = distanceSampler.readChannel(sMidX | 0, sMidY | 0, 0);

        const maxDistanceFromCenter = max2(shExtX, shExtY);

        const distanceToNearestClearance = centerOcclusionDistance - maxDistanceFromCenter;

        if (distanceToNearestClearance > clearance) {
            //distance from center of the rectangle to nearest non-occluded region is too great,  treat as fully occluded
            return false;
        } else {
            return true;
        }
    }

    /**
     * Returns fog clearance from a set of world coordinates
     * @param {number} x world X coordinate
     * @param {number} y world Y coordinate
     * @returns {number} clearance value
     */
    getWorldClearance(x, y) {
        this.worldToSamplePosition(x, y, samplePosition);

        const sampleX = samplePosition[0];
        const sampleY = samplePosition[1];

        return this.distanceSampler.readChannel(sampleX | 0, sampleY | 0, 0);
    }

    rebuildDistanceField() {
        // console.time('computeSignedDistanceField');
        computeUnsignedDistanceField(this.sampler, this.distanceSampler, 255);
        // console.timeEnd('computeSignedDistanceField');
        this.distanceFieldNeedsUpdate = false;
    }

    rebuildDistanceSampler() {
        const array = new Uint8Array(this.sampler.width * this.sampler.height);
        array.fill(0);

        this.distanceSampler.data = array;
        this.distanceSampler.width = this.sampler.width;
        this.distanceSampler.height = this.sampler.height;

        this.rebuildDistanceField();
    }

    rebuildSampler() {

        //resize the sampler
        const samplerHeight = this.size.x + 2;
        const samplerWidth = this.size.y + 2;
        const uint8Array = new Uint8Array(samplerWidth * samplerHeight);

        uint8Array.fill(255);

        const sampler = new Sampler2D(uint8Array, 1, samplerWidth, samplerHeight);

        sampler.copy(this.sampler, 0, 0, 0, 0, this.sampler.width, this.sampler.height);

        //rewrite sampler
        this.sampler.data = sampler.data;
        this.sampler.width = sampler.width;
        this.sampler.height = sampler.height;

        //rebuild distance sampler
        this.rebuildDistanceSampler();


        //Mark texture for update
        this.textureNeedsUpdate = true;

    }


    /**
     *
     * @param {number} w
     * @param {number} h
     * @param scale
     */
    resize(w, h, scale) {
        this.size.set(w, h);
        this.scale.set(scale);

        //TODO update fade mask as necessary

        this.rebuildSampler();
    }
}

FogOfWar.typeName = 'FogOfWar';


