import { BackSide, PlaneBufferGeometry } from 'three';
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 { System } from '../../../ecs/System.js';
import { obtainTerrain } from "../../../ecs/terrain/util/obtainTerrain.js";
import { make_bvh_visibility_builder } from "../../render/make_bvh_visibility_builder.js";
import { StandardFrameBuffers } from "../../StandardFrameBuffers.js";
import ThreeFactory from "../../three/ThreeFactory.js";
import { makeAlexWaterMaterial } from "../water2/shader/AlexWaterShader.js";
import Water from './Water.js';
import { WATER_SIZE } from "./WATER_SIZE.js";

class WaterSystem extends System {
    dependencies = [Water];

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


    cleaup = [];

    /**
     *
     * @type {Water[]}
     */
    updateQueue = [];

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

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


        this.graphicsEngine = graphics;

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

    }


    async shutdown(entityManager) {

        this.graphicsEngine.size.onChanged.remove(this.setViewportSize);

        this.graphicsEngine.layers.remove(this.renderLayer);

    }

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

        const graphicsEngine = this.graphicsEngine;

        this.renderLayer = graphicsEngine.layers.create(WaterSystem.RENDER_LAYER_NAME);

        const viewport_size = graphicsEngine.viewport.size;

        this.renderLayer.buildVisibleSet = make_bvh_visibility_builder(
            this.entityManager, this.bvh,
            (destination, offset, entity, ecd) => {
                const component = ecd.getComponent(entity, Water);

                assert.defined(component, 'component');

                const mesh = component.__threeObject;

                if (mesh === null) {
                    return 0
                }

                destination[offset] = mesh;
                return 1;
            }
        )

        const bvh = this.bvh;
        this.renderLayer.compute_depth_range = (result, frustum, nx, ny, nz, c) => {
            bvh_query_depth_range_in_frustum(result, bvh, frustum, nx, ny, nz, c);
        };


        const self = this;

        /**
         *
         * @param {WebGLRenderer} renderer
         * @param camera
         * @param scene
         */
        function preRenderHook(renderer, camera, scene) {

            const em = self.entityManager;

            const dataset = em.dataset;

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

            const pixelRatio = graphicsEngine.computeTotalPixelRatio();

            dataset.traverseComponents(Water, function (component, entity) {
                const shader = component.__shader;

                shader.uniforms.fCameraNear.value = camera.near;
                shader.uniforms.fCameraFar.value = camera.far;

                shader.uniforms.vScreenResolution.value.set(viewport_size.x * pixelRatio, viewport_size.y * pixelRatio);

            });
        }

        graphicsEngine.on.preRender.add(preRenderHook);
    }

    /**
     *
     * @param {Water} component
     * @param {number} entity
     */
    link(component, entity) {
        const frameBuffer = this.graphicsEngine.frameBuffers.getById(StandardFrameBuffers.ColorAndDepth);

        /**
         * @type {ShaderMaterial}
         */
        let water;

        if (component.__shader === null) {
            water = makeAlexWaterMaterial();

            water.uniforms.tDepthTexture.value = frameBuffer.renderTarget.depthTexture;

            component.__shader = water;

            component.writeShaderUniforms();

        } else {
            water = component.__shader;
        }

        // water.level.value = component.level.getValue();

        const width = WATER_SIZE;
        const height = WATER_SIZE;

        // water.textureRepeat.value.set(width / 6, height / 6);

        water.side = BackSide;

        if (component.__threeObject === null) {
            const geometry = new PlaneBufferGeometry(width, height, 1, 1);

            const mesh = ThreeFactory.createMesh(
                geometry,
                water
            );

            component.__threeObject = mesh;
        }

        component.updateTransform();

        component.level.onChanged.add(component.updateTransform, component);

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

        this.cleaup[entity] = function () {
            component.level.onChanged.remove(component.updateTransform, component);
        };

        this.updateQueue.push(component);
    }

    /**
     *
     * @param {Water} component
     * @param {number} entity
     */
    unlink(component, entity) {
        component.bvh.unlink();

        const cleaup = this.cleaup[entity];
        cleaup();
        delete this.cleaup[entity];


        //remove from update queue
        const i = this.updateQueue.indexOf(component);

        if (i !== -1) {
            this.updateQueue.splice(i, 1);
        }
    }

    processUpdateQueue() {

        //do updates
        const updateQueue = this.updateQueue;

        let l = updateQueue.length;

        if (l === 0) {
            return;
        }

        const dataset = this.entityManager.dataset;

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

        const terrain = obtainTerrain(dataset);

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

        for (let i = 0; i < l; i++) {
            const water = updateQueue[i];

            water.updateShaderForTerrain(terrain, WATER_SIZE);

            updateQueue.splice(i, 1);
            i--;
            l--;
        }
    }

    /**
     *
     * @param {Water} component
     * @param {number} entity
     * @private
     */
    __visit_component(component, entity) {
        const material = component.__shader;

        if (material === undefined) {
            console.error('Material undefined on WaterComponent', entity, component);
            //skip
            return;
        }

        material.uniforms.time.value += this.__time_delta;
    }

    update(timeDelta) {
        this.__time_delta = timeDelta;

        const em = this.entityManager;

        const dataset = em.dataset;

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

        this.processUpdateQueue();

        dataset.traverseComponents(Water, this.__visit_component, this);
    }
}


WaterSystem.RENDER_LAYER_NAME = 'water-system';

export default WaterSystem;
