import { mat4 } from "gl-matrix";
import { MeshStandardMaterial, OctahedronBufferGeometry, PerspectiveCamera, PlaneBufferGeometry } from "three";
import { PLYLoader } from "three/examples/jsm/loaders/PLYLoader.js";

import '../../../../../../../../css/game.scss';
import { float_to_uint8 } from "../../../../core/binary/float_to_uint8.js";
import { Color } from "../../../../core/color/Color.js";
import { linear_to_sRGB } from "../../../../core/color/sRGB/linear_to_sRGB.js";

import { noop } from "../../../../core/function/noop.js";
import { Ray3 } from "../../../../core/geom/3d/ray/Ray3.js";
import { ray3_array_compose } from "../../../../core/geom/3d/ray/ray3_array_compose.js";
import Quaternion from "../../../../core/geom/Quaternion.js";
import { v3_distance } from "../../../../core/geom/vec3/v3_distance.js";
import Vector3 from "../../../../core/geom/Vector3.js";
import { Localization } from "../../../../core/localization/Localization.js";
import { min2 } from "../../../../core/math/min2.js";
import { seededRandom } from "../../../../core/math/random/seededRandom.js";
import { number_pretty_print } from "../../../../core/primitives/numbers/number_pretty_print.js";
import ConcurrentExecutor from "../../../../core/process/executor/ConcurrentExecutor.js";
import Task from "../../../../core/process/task/Task.js";
import { TaskSignal } from "../../../../core/process/task/TaskSignal.js";
import { CanvasView } from "../../../../view/elements/CanvasView.js";
import EmptyView from "../../../../view/elements/EmptyView.js";
import { Transform } from "../../../ecs/transform/Transform.js";
import { MouseEvents } from "../../../input/devices/events/MouseEvents.js";
import { makeSimpleTaskProgressView } from "../../../makeSimpleTaskProgressView.js";
import { Camera } from "../../ecs/camera/Camera.js";
import { three_camera_set_transform_rotation } from "../../ecs/camera/CameraSystem.js";
import { makeGeometryIndexed } from "../../geometry/buffered/makeGeometryIndexed.js";
import { DirectionalLight } from "../../render/forward_plus/model/DirectionalLight.js";
import { sampler2d_scale } from "../../texture/sampler/resize/sampler2d_scale.js";
import { Sampler2D } from "../../texture/sampler/Sampler2D.js";
import sampler2D2Canvas from "../../texture/sampler/sampler2d_to_html_canvas.js";
import { PathTracedScene } from "./PathTracedScene.js";
import { PathTracer } from "./PathTracer.js";
import { make_sun, prepare_scene_gltf } from "./prepare_scene_gltf.js";

document.body.style.margin = 0;
document.body.style.overflow = "hidden";

const vContainer = new EmptyView();


const vCanvas = new CanvasView();
vContainer.addChild(vCanvas);


vContainer.size.set(window.innerWidth, window.innerHeight);
// vCanvas.size.set(512, 512);
// vCanvas.size.set(1080, 1080);
vCanvas.size.copy(vContainer.size);

vContainer.css({
    position: 'absolute',
    top: 0,
    left: 0,
    overflowY: "visible",
    overflowX: "visible"

});
vCanvas.css({
    overflow: "visible"
});

/**
 * How many rays to use per-pixel
 * @type {number}
 */
const PIXEL_SAMPLE_COUNT = 16;
const PIXEL_RENDER_RATIO = 1;

const scene = new PathTracedScene();
const pt = new PathTracer();

window.scene = scene;

function prepare_scene_sphere_01(pt, camera) {
    camera.position.set(0, 0, -2.1);
    camera.lookAt(0, 0, 0);


    const sphere_geo = new OctahedronBufferGeometry(1, 9);
    makeGeometryIndexed(sphere_geo);

    pt.createMesh(
        sphere_geo,
        // new MeshStandardMaterial({ color: '#FF0000' }),
        new MeshStandardMaterial({ color: '#FFFFFF' }),
        Transform.fromJSON({
            scale: 0.5
        }).matrix
    );

    const ground_material = new MeshStandardMaterial({
        color: '#FFFFFF'
    });
    pt.createMesh(new PlaneBufferGeometry(), ground_material, Transform.fromJSON({
        position: new Vector3(0, -0.5, 0),
        scale: 50,
        rotation: Quaternion.fromEulerAngles(-Math.PI / 2, 0, 0)
    }).matrix);


    const light = new DirectionalLight();
    light.color.setRGB(1, 0, 0);
    light.intensity.set(1);
    light.direction.set(-1, -1, 0);
    light.direction.normalize();

    pt.addLight(make_sun());
}

function promise_ply(url) {

    return new Promise((resolve, reject) => {
        new PLYLoader().load(url, resolve, noop, reject);
    });
}

/**
 *
 * @param {PathTracedScene} scene
 * @param {THREE.Camera} camera
 * @return {Promise<void>}
 */
async function prepare_scene_lucy(scene, camera) {


    const lucy_geom = await promise_ply("data/models/stanford/Lucy100k.ply")

    const lucy_material = new MeshStandardMaterial({
        color: '#95c9ff'
    });
    scene.createMesh(lucy_geom, lucy_material, Transform.fromJSON({
        position: new Vector3(0, 0, 0),
        scale: 1,
        // rotation: Quaternion.fromEulerAngles(-Math.PI / 2, 0, 0)
    }).matrix);

    const ground_material = new MeshStandardMaterial({
        color: '#ffceae'
    });
    scene.createMesh(new PlaneBufferGeometry(), ground_material, Transform.fromJSON({
        position: new Vector3(0, -800, 0),
        scale: 5000,
        rotation: Quaternion.fromEulerAngles(-Math.PI / 2, 0, 0)
    }).matrix);


    scene.addLight(make_sun());

    camera.position.set(0, 0, -2300);
    camera.lookAt(0, 0, 0);
}


/**
 *
 * @param {THREE.Camera} camera
 * @param {Transform} transform
 */
function set_camera_from_transform(camera, transform) {
    camera.position.copy(transform.position);
    three_camera_set_transform_rotation(camera, transform.rotation);

    camera.updateMatrixWorld();
}

/**
 *
 * @param {PathTracedScene} scene
 * @param {THREE.Camera} camera
 * @returns {Promise<void>}
 */
async function prepare_gi_box_scene(scene, camera) {
    const path = 'data/models/samples/gi_box_01/model.glb';
    await prepare_scene_gltf({
        scene: scene,
        camera: camera,
        url: path,
        sun: {
            direction: new Vector3(1.2, -1, 0.2)
        }
    });

    const transform = Transform.fromJSON({
        "position": { "x": -2.320852352090763, "y": 4.765851637629835, "z": 5.154489928319274 },
        "rotation": {
            "x": 0.00480805808535541,
            "y": 0.9797795216336188,
            "z": -0.19861114162102378,
            "w": 0.02371889518587569
        },
        "scale": {
            "x": 1, "y": 1, "z": 1
        }
    });

    set_camera_from_transform(camera, transform);

}

/**
 *
 * @param {PathTracedScene} scene
 * @param {THREE.Camera} camera
 * @returns {Promise<void>}
 */
async function prepare_attic_scene(scene, camera) {
    const path = 'data/models/samples/the_attic_environment/scene1.gltf';
    await prepare_scene_gltf({
        scene: scene,
        camera: camera,
        url: path,
        sun: {
            direction: new Vector3(1.2, -1, 0.2)
        }
    });

    const transform = Transform.fromJSON({
        "position": {
            "x": -11.027168212431539,
            "y": 1.0396201881596159,
            "z": -2.9677969519799223
        },
        "rotation": {
            "x": 0.024907147178871554,
            "y": -0.08487115365830479,
            "z": 0.0021222209892540838,
            "w": 0.9960783189459508
        },
        "scale": { "x": 1, "y": 1, "z": 1 }
    });

    set_camera_from_transform(camera, transform);

}

/**
 *
 * @param {PathTracedScene} scene
 * @param {THREE.Camera} camera
 * @returns {Promise<void>}
 */
async function prepare_sponza(scene, camera) {
    const path = 'data/models/sponza-pbr/gltf/sponza.glb';
    await prepare_scene_gltf({
        scene: scene,
        camera: camera,
        url: path,
        no_materials: false,
        sun: {
            direction: new Vector3(0.2, -1, 0.2),
            intensity: 3
        }
    });

    const transform = Transform.fromJSON({
        "position": {
            "x": -5.888122646477855,
            "y": 4.631130854487186,
            "z": -0.35698656745007035
        },
        "rotation": {
            "x": 0.10780203229473442,
            "y": 0.6847605545801206,
            "z": -0.10349143573419042,
            "w": 0.7132820111666399
        },
        "scale": { "x": 1, "y": 1, "z": 1 }
    });

    set_camera_from_transform(camera, transform);
}

/**
 * Ray-Tracing In One Weekend test scene
 * @param {PathTracer} pt
 * @param {THREE.Camera} camera
 */
async function prepare_scene_rtiow(pt, camera) {
    const random = seededRandom();

    const sphere_geo = new OctahedronBufferGeometry(1, 8);
    makeGeometryIndexed(sphere_geo);

    const ground_material = new MeshStandardMaterial({
        color: '#ffffff'
    });
    scene.createMesh(new PlaneBufferGeometry(), ground_material, Transform.fromJSON({
        position: new Vector3(0, 0, 0),
        scale: 50,
        rotation: Quaternion.fromEulerAngles(-Math.PI / 2, 0, 0)
    }).matrix);

    const spheres = [];

    function overlaps_existing(x, y, z, r) {
        for (let i = 0; i < spheres.length; i++) {
            const sphere = spheres[i];

            const r_1 = sphere[3];

            if (v3_distance(sphere[0], sphere[1], sphere[2], x, y, z) < r_1 + r) {
                return true;
            }
        }

        return false;
    }

    function add_sphere(center, radius, material) {

        const mat = new Float32Array(16);
        mat4.fromRotationTranslationScale(mat, [0, 0, 0, 1], center, [radius, radius, radius])

        scene.createMesh(sphere_geo, material, mat);

        spheres.push([
            center[0],
            center[1],
            center[2],
            radius
        ]);
    }

    const material1 = new MeshStandardMaterial({
        color: '#FFFFFF'
    });
    add_sphere([0, 1, 0], 1, material1);

    const material2 = new MeshStandardMaterial({
        color: '#FFFFFF'
    });
    add_sphere([-4, 1, 0], 1, material2);

    const material3 = new MeshStandardMaterial({
        color: '#FFFFFF'
    });
    add_sphere([4, 1, 0], 1, material3);


    for (let a = -11; a < 11; a++) {
        for (let b = -11; b < 11; b++) {

            const choose_mat = random();

            const radius = 0.2;

            const center = [0, radius, 0];

            do {
                center[0] = a + 0.9 * random();
                center[2] = b + 0.9 * random();
            } while (overlaps_existing(center[0], center[1], center[2], radius));


            const mat = new Float32Array(16);
            mat4.fromRotationTranslationScale(mat, [0, 0, 0, 1], center, [radius, radius, radius])

            const material = new MeshStandardMaterial({
                color: Color.fromHSV(random(), 0.9, 0.9).toHex()
            });

            if (choose_mat < 0.8) {

                // diffuse
                add_sphere(center, radius, material);
            } else if (choose_mat < 0.95) {
                // metal
                add_sphere(center, radius, material);
            } else {
                // glass
                add_sphere(center, radius, material);
            }


        }
    }


    scene.addLight(make_sun());

    camera.position.set(13, 2, 3);
    camera.lookAt(0, 0, 0);

}


/**
 *
 * @param {Sampler2D} target
 * @param {PathTracer} pt
 * @param {Camera} camera
 * @param {PathTracedScene} scene
 * @param {{current:number, total:number}} progress
 */
function* render(target, pt, camera, scene, progress = { current: 0, total: 0 }) {
    const random = seededRandom(0);

    // update camera
    camera.updateProjectionMatrix();
    camera.updateMatrixWorld(true);

    //update world inverse matrix
    camera.matrixWorldInverse.copy(camera.matrixWorld);
    camera.matrixWorldInverse.invert();

    const pixel_color = [];
    const pixel_accummulation = [];
    const ray = new Ray3();

    const width = target.width;
    const height = target.height;


    const t0 = performance.now();
    let sample_count = 0;

    const pixel_scale_x = 1 / (width - 1);
    const pixel_scale_y = 1 / (height - 1);

    const pixel_sample_count = PIXEL_SAMPLE_COUNT;

    progress.total = width * height;

    const max_y = height - 1;

    const RENDER_TILE_SIZE = 8;

    const tile_count_x = width / RENDER_TILE_SIZE;
    const tile_count_y = height / RENDER_TILE_SIZE;

    // render in square tiles to get better memory exploitation, as rays are more likely to hit same things in smaller region
    for (let tile_y = 0; tile_y < tile_count_y; tile_y++) {

        const y0 = tile_y * RENDER_TILE_SIZE;
        const y1 = min2(height, y0 + RENDER_TILE_SIZE);

        for (let tile_x = 0; tile_x < tile_count_x; tile_x++) {

            const x0 = tile_x * RENDER_TILE_SIZE;
            const x1 = min2(width, x0 + RENDER_TILE_SIZE);

            for (let y = y0; y < y1; y++) {

                const v = y * pixel_scale_y;
                const _y = v * 2 - 1;

                for (let x = x0; x < x1; x++) {

                    const u = x * pixel_scale_x;
                    const _x = u * 2 - 1;

                    pixel_accummulation[0] = 0;
                    pixel_accummulation[1] = 0;
                    pixel_accummulation[2] = 0;

                    const pixel_address = ((max_y - y) * width + x) * 4;

                    const output_data = target.data;
                    output_data[pixel_address + 3] = 255;

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

                        ray.projectFromPerspectiveCamera(
                            _x + random() * pixel_scale_x * 2,
                            _y + random() * pixel_scale_y * 2,
                            0.5,
                            camera.matrixWorld.elements,
                            camera.projectionMatrixInverse.elements
                        );

                        ray.tMax = Infinity;
                        pt.path_trace(pixel_color, ray, 1, 7, random, scene);

                        pixel_accummulation[0] += pixel_color[0];
                        pixel_accummulation[1] += pixel_color[1];
                        pixel_accummulation[2] += pixel_color[2];


                        sample_count++;
                    }

                    pixel_accummulation[0] /= pixel_sample_count
                    pixel_accummulation[1] /= pixel_sample_count
                    pixel_accummulation[2] /= pixel_sample_count

                    linear_to_sRGB(pixel_accummulation, 0, pixel_accummulation, 0);

                    output_data[pixel_address] = float_to_uint8(pixel_accummulation[0]);
                    output_data[pixel_address + 1] = float_to_uint8(pixel_accummulation[1]);
                    output_data[pixel_address + 2] = float_to_uint8(pixel_accummulation[2]);


                    progress.current++;

                    yield;
                }
            }
        }

    }


    // console.profile('trace');


    // console.profileEnd('trace');

    const total_time = performance.now() - t0;

    console.log(`Trace finished in ${number_pretty_print(total_time)}ms. Total of ${number_pretty_print(sample_count)} samples, ${number_pretty_print(sample_count * 1000 / total_time)} samples per second`);

}

const executor = new ConcurrentExecutor(1, 15);

const camera = new PerspectiveCamera();

/**
 *
 * @param {THREE.Camera} camera
 * @return {Promise<void>}
 */
async function start_renderer(camera) {
    camera.aspect = vCanvas.size.x / vCanvas.size.y;

    // const path = 'data/models/LowPolyTownshipSet/Small_house/Small_house.gltf';
    // const path = 'data/models/pica_pica/pica_pica.gltf';
    // const path = 'data/models/road_bike/road_bike.gltf'; //large CAD-type model

    // await prepare_scene_lucy(scene, camera);
    // await prepare_scene_rtiow(pt, camera);
    // await prepare_scene_sphere_01(pt, camera);
    // await prepare_gi_box_scene(scene, camera);
    await prepare_sponza(scene, camera);
    // await prepare_attic_scene(scene, camera);
    // await prepare_scene_gltf({scene, camera, path, url: path});


    // pt.optimize();


    const rt = Sampler2D.uint8clamped(4, vCanvas.size.x, vCanvas.size.y);

    let scaled_rt = rt;
    if (PIXEL_RENDER_RATIO !== 1) {
        scaled_rt = Sampler2D.uint8clamped(4, Math.ceil(rt.width * PIXEL_RENDER_RATIO), Math.ceil(rt.height * PIXEL_RENDER_RATIO));
    }

    function make_render_task() {
        const progress_v = { current: 0, total: 0 };

        const it = render(scaled_rt, pt, camera, scene, progress_v);

        return new Task({
            name: 'render',
            cycleFunction() {
                const next = it.next();

                if (!next.done) {
                    return TaskSignal.Continue;
                } else {
                    return TaskSignal.EndSuccess;
                }

            },
            computeProgress() {
                return progress_v.total > 0 ? progress_v.current / progress_v.total : 0;
            }
        })
    }


    const loc = new Localization();

    async function update() {


        const t = make_render_task();


        const vProgress = makeSimpleTaskProgressView({
            task: t,
            size: vContainer.size,
            localization: loc
        })

        vContainer.addChild(vProgress);

        const p_done = t.promise();

        executor.run(t);

        await p_done;

        vContainer.removeChild(vProgress);

        if (scaled_rt !== rt) {
            sampler2d_scale(scaled_rt, rt);
        }

        sampler2D2Canvas(rt, 1, 0, vCanvas.el);

        requestAnimationFrame(update);
    }

    update();
}

start_renderer(camera);

vCanvas.el.addEventListener(MouseEvents.Click, (evt) => {
    const ray = new Ray3();

    const ray_origin = new Vector3();
    const ray_direction = new Vector3();

    const pointer = getPointer(vCanvas.el, evt);

    const _x = pointer.x;
    const _y = pointer.y;

    Camera.projectRay(camera,
        _x,
        _y,
        ray_origin, ray_direction
    );

    ray3_array_compose(
        ray,
        ray_origin.x, ray_origin.y, ray_origin.z,
        ray_direction.x, ray_direction.y, ray_direction.z
    );
    ray.tMin = 0;
    ray.tMax = Infinity;

    const out = [];

    scene.trace(out, ray);

    console.log(pointer, out, ray, pt);

});

vCanvas.css({
    pointerEvents: 'auto'
});

function getPointer(domElement, event) {

    if (domElement.ownerDocument.pointerLockElement) {

        return {
            x: 0,
            y: 0,
            button: event.button
        };

    } else {

        const rect = domElement.getBoundingClientRect();

        return {
            x: (event.clientX - rect.left) / rect.width * 2 - 1,
            y: -(event.clientY - rect.top) / rect.height * 2 + 1,
            button: event.button
        };

    }

}

// link
document.body.appendChild(vContainer.el);
vContainer.link();
