import { DoubleSide, Mesh, MeshDepthMaterial, RGBADepthPacking, Scene } from "three";
import { ThreeBypassRenderer } from "../../../../render/utils/ThreeBypassRenderer.js";
import { CameraView } from "../../../../render/view/CameraView.js";
import { three_setSceneAutoUpdate } from "../../../../three/three_setSceneAutoUpdate.js";
import { threeUpdateTransform } from "../../../../util/threeUpdateTransform.js";
import { LightType } from "../../LightType.js";
import { ShadowMap } from "../../shadow/ShadowMap.js";
import { LightBinding } from "../LightBinding.js";
import { applyRotation } from "./applyRotation.js";
import { threeEnsureLightObject } from "./threeEnsureLightObject.js";

const shadow_scene = new Scene();
three_setSceneAutoUpdate(shadow_scene, false);
shadow_scene.matrixAutoUpdate = false;
shadow_scene.matrixWorldNeedsUpdate = false;

const __depth_material = new MeshDepthMaterial({
    depthPacking: RGBADepthPacking,
    side: DoubleSide
});

/**
 *
 * @param {Mesh} object
 * @returns {Material}
 * @private
 */
function __getDepthMaterial(object) {
    let result;

    const customDepthMaterial = object.customDepthMaterial;

    if (customDepthMaterial !== undefined) {
        result = customDepthMaterial;
    } else {

        const source_material = object.material;
        if (
            typeof source_material.alphaTest === "number"
            && source_material.alphaTest > 0
            && source_material.alphaTest < 1
            && source_material.map !== null
            && source_material.map !== undefined
        ) {
            // alpha-tested
            result = new MeshDepthMaterial({
                depthPacking: RGBADepthPacking,
                side: DoubleSide,
                map: source_material.map,
                alphaTest: source_material.alphaTest
            });

        } else {
            // generic
            result = __depth_material;
        }
    }

    return result;
}

/**
 *
 * @param {THREE.Object3D} object3D
 * @return {boolean}
 */
function scene_object_filter(object3D) {

    if (object3D.castShadow === false) {
        return false;
    }

    if (object3D.visible === false) {
        return false;
    }

    if (object3D.isMesh) {

        /**
         * @type {THREE.Material|null|undefined}
         */
        const material = (object3D.customDepthMaterial ? object3D.customDepthMaterial : object3D.material);

        if (material && material.visible === false) {
            //material's main
            return false;
        }

    }

    return true;
}

/**
 *
 * @param {LightShadow} shadow
 * @param {number} resolution
 */
export function three_set_shadow_resolution(shadow, resolution) {
    const mapSize = shadow.mapSize;

    if (mapSize.width !== resolution || mapSize.height !== resolution) {

        mapSize.width = mapSize.height = resolution;

        //destroy old map
        if (shadow.map !== null) {
            shadow.map.dispose(); // important
            shadow.map = null;
        }

    }
}

export class ThreeLightBinding extends LightBinding {


    /**
     *
     * @type {LightSystem|null}
     * @private
     */
    __system = null;

    /**
     *
     * @type {ShadowMap|null}
     * @private
     */
    __shadow_map = null;

    /**
     *
     * @type {WeakMap<THREE.Object3D, THREE.Object3D>}
     * @private
     */
    __object_map = new WeakMap();


    link(ctx) {
        this.__system = ctx.system;

        /**
         * @type {ThreeLightCache}
         */
        const cache = this.__system.__three_light_cache;

        const c_light = this.__c_light;
        threeEnsureLightObject(c_light, cache);

        super.link(ctx);
    }

    unlink(ctx) {
        /**
         * @type {ThreeLightCache}
         */
        const cache = this.__system.__three_light_cache;

        cache.release(this.__three_getLight());

        // clear bound light
        this.__c_light.__threeObject = null;

        this.__shadowmap_unlink();

        super.unlink(ctx);
    }

    /**
     *
     * @return {THREE.Light}
     * @private
     */
    __three_getLight() {
        return this.__c_light.__threeObject;
    }

    __apply_intensity() {
        this.__three_getLight().intensity = this.__c_light.intensity.getValue();
    }

    __apply_color() {
        const c = this.__c_light.color;

        this.__three_getLight().color.setRGB(c.r, c.g, c.b);
    }

    __apply_distance(v) {
        this.__three_getLight().distance = this.scaled_distance;
    }

    __apply_angle() {
        this.__three_getLight().angle = this.__c_light.angle.getValue();
    }

    __apply_penumbra() {
        this.__three_getLight().penumbra = this.__c_light.penumbra.getValue();
    }

    __shadowmap_link() {

        this.__shadow_map = new ShadowMap();

        const cameraView = new CameraView();
        cameraView.name = 'Shadow Map';

        this.__shadow_map.views.push(cameraView)

        /**
         *
         * @type {LightSystem}
         */
        const system = this.__system;

        const shadows = system.__shadows;

        shadows.add(this.__shadow_map);

        //
        cameraView.on.preVisibilityBuild.add(this.__shadowmap_prepare_view, this);

        cameraView.on.postVisibilityBuild.add(this.__shadowmap_render, this);
    }

    __shadowmap_unlink() {
        if (this.__shadow_map === null) {
            return;
        }

        /**
         *
         * @type {LightSystem}
         */
        const system = this.__system;

        const shadows = system.__shadows;

        shadows.remove(this.__shadow_map);

        this.__shadow_map = null;
    }

    __shadowmap_prepare_view() {

        const cameraView = this.__shadow_map.views[0];

        /**
         * @type {THREE.Light}
         */
        const light = this.__three_getLight();

        /**
         *
         * @type {LightShadow}
         */
        const shadow = light.shadow;

        /**
         * @type {THREE.Camera}
         */
        const camera = shadow.camera;

        // update camera matrices
        camera.updateMatrix();
        camera.updateMatrixWorld();

        cameraView.set_from_camera(camera);

    }


    __shadowmap_render() {

        /**
         *
         * @type {CameraView}
         */
        const cameraView = this.__shadow_map.views[0];

        // signal that we're about to render this view
        cameraView.on.preRender.send1(cameraView);

        /**
         *
         * @type {LightSystem}
         */
        const system = this.__system;

        /**
         *
         * @type {GraphicsEngine}
         */
        const graphics = system.__graphics;

        const threeLight = this.__three_getLight();

        const shadow = threeLight.shadow;


        // prepare shadow scene
        const visible_objects = cameraView.visible_objects;
        const visible_object_count = visible_objects.size;

        ThreeBypassRenderer.INSTANCE.build_scene({
            scene: shadow_scene,
            input: visible_objects.elements,
            input_size: visible_object_count,
            material_extractor: __getDepthMaterial,
            object_filter: scene_object_filter
        })

        // render map out
        graphics.shadowmap_renderer.update(graphics.renderer, shadow, shadow_scene, threeLight);
    }

    __apply_castShadow() {
        const v = this.__c_light.castShadow.getValue();

        const light = this.__three_getLight();

        light.castShadow = v;

        if (v && this.__shadow_map === null) {

            this.__shadowmap_link();

        } else if (!v && this.__shadow_map !== null) {

            this.__shadowmap_unlink();

        }
    }

    __apply_position() {
        const transform = this.__c_transform;
        const p = transform.position;

        const threeLight = this.__three_getLight();
        threeLight.position.set(p.x, p.y, p.z);
        const cLight = this.__c_light;
        const lightType = cLight.type.getValue();

        if (lightType === LightType.SPOT || lightType === LightType.DIRECTION) {
            applyRotation(cLight, transform.rotation);
        } else {
            // just update transform
            threeUpdateTransform(threeLight);
        }

    }

    __apply_rotation() {
        applyRotation(this.__c_light, this.__c_transform.rotation);
    }

    __apply_scale() {
        this.__three_getLight().distance = this.scaled_distance;
    }

    applySettings(settings) {
        const light = this.__three_getLight();

        const shadow = light.shadow;

        if (shadow !== undefined) {

            // disable automatic shadowmap rendering in three.js
            shadow.autoUpdate = false;
            shadow.camera.matrixAutoUpdate = false;

            const resolution = parseInt(settings.shadowResolution);

            shadow.radius = 1;
            shadow.blurSamples = 8;

            three_set_shadow_resolution(shadow, resolution);

        }
    }

}
