import { assert } from "../../../../core/assert.js";
import { BVH } from "../../../../core/bvh2/bvh3/BVH.js";
import { bvh_query_depth_range_in_frustum } from "../../../../core/bvh2/bvh3/query/bvh_query_depth_range_in_frustum.js";
import {
    bvh_query_user_data_overlaps_frustum
} from "../../../../core/bvh2/bvh3/query/bvh_query_user_data_overlaps_frustum.js";

import { noop } from "../../../../core/function/noop.js";
import { read_three_planes_to_array } from "../../../../core/geom/3d/frustum/read_three_planes_to_array.js";
import { M4_IDENTITY } from "../../../../core/geom/3d/mat4/M4_IDENTITY.js";
import { ImageRGBADataLoader } from "../../../asset/loaders/image/ImageRGBADataLoader.js";
import { TextureAssetLoader } from "../../../asset/loaders/texture/TextureAssetLoader.js";
import { CameraSystem } from '../../../graphics/ecs/camera/CameraSystem.js';
import { System } from '../../System.js';
import { Transform } from "../../transform/Transform.js";
import Terrain from './Terrain.js';
import { TerrainFlags } from "./TerrainFlags.js";

/**
 *
 * @param {TerrainTile} tile
 * @param {TerrainTileManager} tileManager
 */
function ensureTileBuilt(tile, tileManager) {
    if (!tile.isBuilt && !tile.isBuildInProgress) {

        const position = tile.gridPosition;

        tileManager.build(position.x, position.y, noop, noop);
    }
}

class TerrainSystem extends System {
    dependencies = [Terrain];

    gridScaleX = 1;
    gridScaleY = 1;

    /**
     *
     * @type {number}
     * @private
     */
    __time_delta = 0;

    /**
     *
     * @type {RenderLayer|null}
     */
    renderLayer = null;

    /**
     *
     * @type {BVH}
     */
    bvh = new BVH();

    /**
     *
     * @param {GraphicsEngine} graphics
     * @param {AssetManager} assetManager
     * @constructor
     */
    constructor(graphics, assetManager) {
        super();

        if (graphics === undefined) {
            throw new Error('No GraphicsEngine supplied');
        }

        if (assetManager === undefined) {
            throw new Error('No AssetManager supplied');
        }


        this.graphics = graphics;

        this.assetManager = assetManager;
    }

    mapPointGrid2World(x, y, v3) {
        const wX = x * this.gridScaleX;
        const wY = v3.y;
        const wZ = y * this.gridScaleY;

        v3.set(wX, wY, wZ);
    }

    async startup(entityManager) {
        this.entityManager = entityManager;

        this.renderLayer = this.graphics.layers.create('terrain-system');


        this.renderLayer.buildVisibleSet = (destination, destination_offset, view) => {
            const em = this.entityManager;

            if (em === null) {
                return 0;
            }

            const ecd = em.dataset;

            if (ecd === null) {
                return 0;
            }

            const tiles = [];

            let match_count = 0;

            ecd.traverseComponents(Terrain, (component, entity) => {
                const tile_manager = component.tiles;

                const hit_count = bvh_query_user_data_overlaps_frustum(
                    tiles, 0,
                    tile_manager.bvh, view.frustum
                );

                for (let i = 0; i < hit_count; i++) {

                    const tile_index = tiles[i];

                    const tile = tile_manager.tiles[tile_index];

                    if (!tile.isBuilt) {
                        continue;
                    }

                    destination[destination_offset + match_count] = tile.mesh;

                    match_count++;
                }

            });

            return match_count;
        };

        this.renderLayer.compute_depth_range = (result, frustum, nx, ny, nz, c) => {
            const em = this.entityManager;

            if (em === null) {
                return;
            }

            const ecd = em.dataset;

            if (ecd === null) {
                return;
            }

            ecd.traverseComponents(Terrain, (component) => {
                bvh_query_depth_range_in_frustum(result, component.tiles.bvh, frustum, nx, ny, nz, c);
            });
        };

        const am = this.assetManager;

        await am.tryRegisterLoader('image', new ImageRGBADataLoader());
        await am.tryRegisterLoader('texture', new TextureAssetLoader());

    }

    async shutdown(entityManager) {
        this.graphics.layers.remove(this.renderLayer);
    }

    /**
     *
     * @param {Terrain} component
     * @param entity
     */
    link(component, entity) {
        // check if component needs to be built
        if (!component.getFlag(TerrainFlags.Built)) {
            component.build(this.assetManager);
        }

        const gridSize = component.size;
        const gridScale = component.gridScale;

        const g_w = gridSize.x;
        const g_h = gridSize.y;

        this.gridScaleX = g_w * gridScale / (g_w - 1);
        this.gridScaleY = g_h * gridScale / (g_h - 1);

        component.link(this.graphics);

        component.bvh.link(this.bvh, entity);
    }

    /**
     *
     * @param {Terrain} component
     * @param entity
     */
    unlink(component, entity) {
        component.unlink();
    }

    update(timeDelta) {
        const em = this.entityManager;
        const dataset = em.dataset;

        this.__time_delta = timeDelta;


        if (dataset === null) {
            return;
        }


        dataset.traverseComponents(Terrain, this.__visitTerrainComponent, this);
    }

    /**
     *
     * @param {Terrain} terrain
     * @param {number} entity
     * @private
     */
    __visitTerrainComponent(terrain, entity) {
        const em = this.entityManager;

        const ecd = em.dataset;

        /**
         *
         * @type {Transform|undefined}
         */
        const transform = ecd.getComponent(entity, Transform);

        if (transform !== undefined) {
            // terrain has a transform
            terrain.transform = transform.matrix;
        } else {
            terrain.transform = M4_IDENTITY;
        }

        terrain.update(this.__time_delta);

        //do frustum culling
        if (terrain.frustumCulled) {
            TerrainSystem.traverseVisibleTiles(ecd, terrain, ensureTileBuilt);
        }
    }


    /**
     *
     * @param {EntityComponentDataset} ecd
     * @param {Terrain} terrain
     * @param {function(tile:TerrainTile, tileManager:TerrainTileManager)} callback
     */
    static traverseVisibleTiles(ecd, terrain, callback) {
        assert.notEqual(terrain, null, 'terrain is null');
        assert.notEqual(terrain, undefined, 'terrain is undefined');

        const tileManager = terrain.__tiles;

        const tiles = [];
        const set = new Set();
        let tile_count = 0;

        const frustum = new Float32Array(24);

        if (tileManager !== undefined) {
            CameraSystem.getActiveFrustums(ecd, function (frustums) {
                let found_tiles = 0;

                for (let i = 0; i < frustums.length; i++) {

                    read_three_planes_to_array(frustums[i].planes, frustum);

                    found_tiles += bvh_query_user_data_overlaps_frustum(tiles, tile_count, tileManager.bvh, frustum);
                }

                for (let i = 0; i < found_tiles; i++) {
                    const tile_index = tiles[i];

                    if (!set.has(tile_index)) {
                        const tile = tileManager.tiles[tile_index];

                        callback(tile, tileManager);
                    }

                    set.add(tile_index);
                }

            });
        }
    }
}


export default TerrainSystem;
