import { assert } from "../../core/assert.js";
import { binarySearchLowIndex } from "../../core/collection/array/binarySearchLowIndex.js";
import { HashMap } from "../../core/collection/map/HashMap.js";
import { QuadTreeNode } from "../../core/geom/2d/quad-tree/QuadTreeNode.js";
import { randomFloatBetween } from "../../core/math/random/randomFloatBetween.js";
import { seededRandom } from "../../core/math/random/seededRandom.js";
import { number_compare_ascending } from "../../core/primitives/numbers/number_compare_ascending.js";
import Future from "../../core/process/Future.js";
import Task from "../../core/process/task/Task.js";
import TaskGroup from "../../core/process/task/TaskGroup.js";
import { TaskSignal } from "../../core/process/task/TaskSignal.js";
import { actionTask } from "../../core/process/task/util/actionTask.js";
import { countTask } from "../../core/process/task/util/countTask.js";
import { emptyTask } from "../../core/process/task/util/emptyTask.js";
import { futureTask } from "../../core/process/task/util/futureTask.js";
import { TerrainLayer } from "../../engine/ecs/terrain/ecs/layers/TerrainLayer.js";
import { TerrainFlags } from "../../engine/ecs/terrain/ecs/TerrainFlags.js";
import { obtainTerrain } from "../../engine/ecs/terrain/util/obtainTerrain.js";
import { Sampler2D } from "../../engine/graphics/texture/sampler/Sampler2D.js";
import { TerrainLayerRuleAggregator } from "./TerrainLayerRuleAggregator.js";

/**
 *
 * @param {HashMap<TerrainLayerDescription,number>} layer_mapping
 * @param {TerrainTheme[]} themes
 * @returns {number}
 */
function prepare_terrain_for_themes(layer_mapping, themes) {
    let max = 0;

    // collect unique layers
    for (let i = 0; i < themes.length; i++) {
        const theme = themes[i];

        if (theme === undefined || theme === null) {
            // skip
            continue;
        }

        const rules = theme.rules;
        for (let j = 0; j < rules.length; j++) {
            const rule = rules[j];

            const layer = rule.layer;
            if (!layer_mapping.has(layer)) {
                layer_mapping.set(layer, max);
                max++;
            }
        }
    }

    return max;
}

/**
 *
 * @param {Terrain} terrain
 * @returns {Task}
 */
function makeTaskEnsureAllTiles(terrain) {
    return futureTask(new Future((resolve, reject) => {

        const is_linked = terrain.getFlag(TerrainFlags.Linked);

        if (!is_linked) {
            terrain.startBuildService();
        }

        if (!terrain.getFlag(TerrainFlags.Built)) {
            terrain.buildNonVisual();
        }

        terrain.promiseAllTiles()
            .then(resolve, reject)
            .finally(() => {
                if (!is_linked && !terrain.getFlag(TerrainFlags.Linked)) {
                    // cleanup after ourselves
                    terrain.stopBuildService();
                }
            });

    }), 'Wait for terrain tiles to be built');
}

export class ThemeEngine {

    /**
     *
     * @type {QuadTreeNode<AreaTheme>}
     */
    areas = new QuadTreeNode();

    /**
     * @readonly
     * @type {function():number}
     */
    random = seededRandom(1);

    /**
     *
     * @type {AssetManager}
     * @private
     */
    __assetManager = null;

    set assetManager(v) {
        this.__assetManager = v;
    }

    /**
     *
     * @param {number} v
     */
    set seed(v) {
        this.random.setCurrentSeed(v);
    }

    /**
     *
     * @returns {number}
     */
    get seed() {
        return this.random.getCurrentSeed();
    }

    /**
     *
     * @param {AreaTheme} theme
     */
    add(theme) {
        assert.defined(theme, 'theme');

        theme.mask.updateBounds();
        theme.mask.updateDistanceField();

        const bounds = theme.mask.bounds;

        this.areas.add(theme, bounds.x0, bounds.y0, bounds.x1, bounds.y1);

    }

    /**
     *
     * @param {AreaTheme[]} result
     * @param {number} x
     * @param {number} y
     * @returns {number}
     */
    getThemesByPosition(result, x, y) {

        const areaNodes = [];

        const intersections = this.areas.requestDatumIntersectionsPoint(areaNodes, x, y);

        let matches = 0;

        for (let i = 0; i < intersections; i++) {
            const areaNode = areaNodes[i];

            /**
             *
             * @type {AreaTheme}
             */
            const areaTheme = areaNode.data;

            const filled = areaTheme.mask.mask.readChannel(x, y, 0);

            if (filled === 0) {
                continue;
            }

            result[matches++] = areaTheme;
        }


        return matches;
    }

    /**
     *
     * @param {number} seed
     * @param {EntityComponentDataset} ecd
     * @param {GridData} grid
     * @returns {Task}
     */
    initializeThemes(seed, ecd, grid) {

        return actionTask(() => {
            /**
             *
             * @type {AreaTheme[]}
             */
            const areas = [];

            this.areas.getRawData(areas);

            /**
             *
             * @type {Theme[]}
             */
            const themes = [];

            const n = areas.length;
            for (let i = 0; i < n; i++) {
                const areaTheme = areas[i];

                const theme = areaTheme.theme;

                if (themes.indexOf(theme) === -1) {
                    themes.push(theme);

                    const seed = this.random();

                    theme.initialize(seed, ecd, grid);
                }
            }

        });
    }

    /**
     *
     * @param {GridData} grid
     * @param {Terrain} terrain
     * @returns {TaskGroup}
     */
    applyTerrainThemes(grid, terrain) {

        assert.notNull(terrain);
        assert.notNull(this.__assetManager, 'assetManager');

        const width = grid.width;
        const height = grid.height;

        assert.equal(width, terrain.size.x);
        assert.equal(height, terrain.size.y);

        /**
         *
         * @type {AreaTheme[]}
         */
        const matchingThemes = [];

        /**
         *
         * @type {HashMap<TerrainLayerDescription, number>}
         */
        const layer_index_map = new HashMap();

        // collect layers
        /**
         *
         * @type {AreaTheme[]}
         */
        const all_areas = [];
        this.areas.getRawData(all_areas);
        const terrain_themes = all_areas.map(a => a.theme.terrain);
        const layer_count = prepare_terrain_for_themes(layer_index_map, terrain_themes);

        const splat = terrain.splat;


        const aggregator = new TerrainLayerRuleAggregator(layer_count);

        let matchingThemeCount = 0;

        let splatWeightData = splat.weightData;

        const splatWidth = splat.size.x;
        const splatHeight = splat.size.y;

        const splatLayerSize = splatWidth * splatHeight;

        //splat map size can vary from the terrain size, for that reason we write splat weights into an intermediate storage so we can re-sample it to splat map after
        const weights = Sampler2D.uint8(layer_count, width, height);

        const tInitialize = actionTask(() => {

            splat.resize(splat.size.x, splat.size.y, layer_count);

            terrain.layers.clear();

            /**
             *
             * @type {TerrainLayerDescription[]}
             */
            const layer_descriptors = [];
            for (const [description, index] of layer_index_map) {
                layer_descriptors.push(description);
            }

            layer_descriptors.sort((a, b) => {
                return layer_index_map.get(a) - layer_index_map.get(b);
            });

            layer_descriptors.forEach(l => {
                terrain.layers.addLayer(TerrainLayer.fromDescription(l));
            });

            terrain.clearFlag(TerrainFlags.Built);
            terrain.build(this.__assetManager);

            splatWeightData = splat.weightData;
        }, 'Initialize');


        const tApplyThemes = countTask(0, width * height, (index) => {
            const y = (index / width) | 0;
            const x = index % width;

            matchingThemeCount = this.getThemesByPosition(matchingThemes, x, y);

            aggregator.clear();

            for (let i = 0; i < matchingThemeCount; i++) {
                /**
                 *
                 * @type {AreaTheme}
                 */
                const areaTheme = matchingThemes[i];

                const areaMask = areaTheme.mask;

                /**
                 *
                 * @type {Theme}
                 */
                const theme = areaTheme.theme;

                const rules = theme.terrain.rules;

                const ruleCount = rules.length;

                for (let j = 0; j < ruleCount; j++) {
                    const terrainLayerRule = rules[j];

                    let power = terrainLayerRule.filter.execute(grid, x, y, 0);

                    if (power <= 0) {
                        continue;
                    }

                    /**
                     *
                     * @type {number}
                     */
                    const layerIndex = layer_index_map.get(terrainLayerRule.layer);


                    if (matchingThemeCount > 1) {
                        const distance = areaMask.distanceField.readChannel(x, y, 0);
                        const influence = distance / matchingThemeCount;

                        power *= influence;
                    }

                    aggregator.add(layerIndex, power);


                }
            }

            aggregator.normalize(255);

            weights.write(x, y, aggregator.powers);

        });

        tApplyThemes.addDependency(tInitialize);


        //re-sample weights
        const weightSample = [];

        const tResample = countTask(0, splatWidth * splatHeight, index => {
            const y = (index / splatWidth) | 0;
            const x = index % splatWidth;

            const v = y / splatHeight;
            const u = x / splatWidth;

            const source_y = v * weights.height;
            const source_x = u * weights.width;


            weights.sampleBilinear(source_x, source_y, weightSample, 0);

            for (let i = 0; i < layer_count; i++) {
                const targetAddress = (y * splatWidth + x) + i * splatLayerSize;
                splatWeightData[targetAddress] = weightSample[i];
            }

        });

        tResample.addDependency(tApplyThemes);


        return new TaskGroup([tInitialize, tApplyThemes, tResample], 'Applying a level generation theme');
    }

    /**
     *
     * @param {GridData} grid
     * @param {EntityComponentDataset} ecd
     * @param {number} seed
     * @returns {Task|TaskGroup}
     */
    applyNodes(grid, ecd, seed = 0) {
        /**
         *
         * @type {MarkerNode[]}
         */
        const nodes = [];

        let i = 0;

        /**
         *
         * @type {AreaTheme[]}
         */
        const themeAreas = [];

        /**
         *
         * @type {number[]}
         */
        const themeInfluence = [];

        const random = seededRandom(seed);

        const cycleFunction = () => {
            if (i >= nodes.length) {
                return TaskSignal.EndSuccess;
            }

            const node = nodes[i];

            i++;

            const nodePosition = node.position;

            const x = nodePosition.x;
            const y = nodePosition.y;

            const matchingThemeCount = this.getThemesByPosition(themeAreas, x, y);

            if (matchingThemeCount === 0) {
                return TaskSignal.Continue;
            }

            let influenceSum = 0;

            //compute influences of active themes
            for (let j = 0; j < matchingThemeCount; j++) {

                /**
                 *
                 * @type {AreaTheme}
                 */
                const theme = themeAreas[j];

                const areaMask = theme.mask;

                const distance = areaMask.distanceField.readChannel(x, y, 0);
                const influence = distance / matchingThemeCount;

                themeInfluence[j] = influenceSum;

                influenceSum += influence;
            }

            //select a theme
            const t = randomFloatBetween(random, 0, influenceSum);

            const themeIndex = binarySearchLowIndex(themeInfluence, t, number_compare_ascending);

            const themeArea = themeAreas[themeIndex];

            /**
             *
             * @type {Theme}
             */
            const theme = themeArea.theme;

            /**
             *
             * @type {MarkerNodeProcessingRuleSet}
             */
            const ruleSet = theme.nodes;

            ruleSet.processNode(grid, ecd, node);


            return TaskSignal.Continue;
        }

        return new Task({
            initializer() {
                grid.markers.getRawData(nodes);

                // sort nodes by priority
                nodes.sort((a, b) => b.priority - a.priority);
            },
            cycleFunction
        });
    }

    /**
     *
     * @param {GridData} grid
     * @param {EntityComponentDataset} ecd
     */
    applyCellRules(grid, ecd) {

        const width = grid.width;
        const height = grid.height;

        /**
         *
         * @type {AreaTheme[]}
         */
        const matchingThemes = [];

        return countTask(0, width * height, (index) => {
            const y = (index / width) | 0;
            const x = index % width;

            const matchingThemeCount = this.getThemesByPosition(matchingThemes, x, y);

            for (let i = 0; i < matchingThemeCount; i++) {
                const areaTheme = matchingThemes[i];

                const theme = areaTheme.theme;

                /**
                 *
                 * @type {CellProcessingRule[]}
                 */
                const cellRules = theme.cells.elements;

                const cellRuleCount = cellRules.length;

                for (let j = 0; j < cellRuleCount; j++) {
                    const rule = cellRules[j];

                    rule.action.execute(ecd, grid, x, y, 0, rule.filter);
                }
            }
        });
    }

    /**
     *
     * @param {Terrain} terrain
     * @returns {Task}
     */
    updateTerrain(terrain) {
        return emptyTask();
    }

    /**
     *
     * @returns {TaskGroup}
     * @param {EntityComponentDataset} ecd
     */
    optimize(ecd) {

        const terrain = obtainTerrain(ecd);

        const tLightMap = futureTask(new Future((resolve, reject) => {
            terrain.buildLightMap().then(resolve, reject);
        }), 'Building Lightmap');

        return new TaskGroup([tLightMap]);
    }

    /**
     *
     * @param {GridData} grid
     * @param {EntityComponentDataset} ecd
     * @returns {TaskGroup}
     */
    apply(grid, ecd) {
        const terrain = obtainTerrain(ecd);

        const tInitializeThemes = this.initializeThemes(this.random(), ecd, grid);

        const tNodes = this.applyNodes(grid, ecd);

        const tCells = this.applyCellRules(grid, ecd);

        tNodes.addDependencies([
            tInitializeThemes,
        ]);

        tCells.addDependency(tInitializeThemes);

        const tOptimize = this.optimize(ecd);

        tOptimize.addDependencies([
            tNodes,
            tCells,
            tInitializeThemes
        ]);

        const subtasks = [tInitializeThemes, tNodes, tCells, tOptimize];

        if (terrain !== null) {
            // build terrain tiles
            const tTerrain = this.applyTerrainThemes(grid, terrain);
            tTerrain.addDependency(tInitializeThemes);

            const tTerrainGeometry = makeTaskEnsureAllTiles(terrain);
            tTerrainGeometry.addDependency(tTerrain);

            const tUpdateTerrain = this.updateTerrain(terrain);

            tUpdateTerrain.addDependencies([
                tTerrain,
                tNodes,
                tCells,
                tTerrainGeometry
            ]);

            tNodes.addDependencies([
                tTerrain,
                tTerrainGeometry,
            ]);
            tTerrainGeometry.addDependency(tCells);

            tOptimize.addDependencies([
                tUpdateTerrain,
                tTerrain,
            ]);

            subtasks.push(tTerrainGeometry, tTerrain, tUpdateTerrain);
        } else {
            console.warn(`No terrain present, skipping terrain theming`);
        }

        return new TaskGroup(subtasks);
    }
}
