import { LOD as ThreeLOD, Object3D, Vector3 } from "three";

import { serializable } from "../engine/engine_serialization_decorator.js";
import { getParam } from "../engine/engine_utils.js";
import { Behaviour, GameObject } from "./Component.js";
import { Renderer } from "./Renderer.js";

const debug = getParam("debuglods");
const noLods = getParam("nolods");

enum LODFadeMode {
    None = 0,
    CrossFade = 1,
    SpeedTree = 2,
}

export class LODModel {
    @serializable()
    screenRelativeTransitionHeight!: number;
    @serializable()
    distance!: number;
    @serializable(Renderer)
    renderers!: Renderer[];
}

class LOD {
    readonly model: LODModel;
    get renderers(): Renderer[] { return this.model.renderers; }

    constructor(model: LODModel) {
        this.model = model;
    }
}

declare class LODSetting {
    lod: ThreeLOD;
    levelIndex: number;
    distance: number;
}

/**
 * LODGroup allows to create a group of LOD levels for an object.
 * @category Rendering
 * @group Components
 */
export class LODGroup extends Behaviour {

    fadeMode: LODFadeMode = LODFadeMode.None;
    @serializable(Vector3)
    localReferencePoint: Vector3 | undefined = undefined;
    lodCount: number = 0;
    size: number = 0;
    animateCrossFading: boolean = false;

    @serializable(LODModel)
    lodModels?: LODModel[];

    private _lods: LOD[] = [];
    private _settings: LODSetting[] = [];

    // https://threejs.org/docs/#api/en/objects/LOD
    private _lodsHandler?: Array<ThreeLOD>;

    start(): void {
        if (debug)
            console.log("LODGROUP", this.name, this.lodModels, this);
        if (noLods) return;
        if (this._lodsHandler) return;
        if (!this.gameObject) return;

        if (this.lodModels && Array.isArray(this.lodModels)) {
            const renderers: Renderer[] = [];
            for (const model of this.lodModels) {
                const lod = new LOD(model);
                this._lods.push(lod);
                for (const rend of lod.renderers) {
                    if (!renderers.includes(rend))
                        renderers.push(rend);
                }
            }
            this._lodsHandler = new Array<ThreeLOD>();
            for (let i = 0; i < renderers.length; i++) {
                const handler = new ThreeLOD();
                this._lodsHandler.push(handler);
                this.gameObject.add(handler);
            }
            const empty = new Object3D();
            empty.name = "Cull " + this.name;
            for (let i = 0; i < renderers.length; i++) {
                const rend = renderers[i];
                const handler = this._lodsHandler[i];
                const obj = rend.gameObject;
                if (debug)
                    console.log(i, obj.name);
                for (const lod of this._lods) {
                    const dist = lod.model.distance;

                    // get object to be lodded, it can be empty
                    let object: Object3D | null = null;
                    if (lod.renderers.includes(rend)) {
                        object = obj;
                    }
                    else {
                        object = empty;
                    }

                    if (object.type === "Group") {
                        console.warn(`LODGroup ${this.name}: Group or MultiMaterial object's are not supported as LOD object: ${object.name}`);
                        continue;
                    }
                    if (debug)
                        console.log("LEVEL", object.name, dist);
                    handler.autoUpdate = false;
                    this.onAddLodLevel(handler, object, lod.model.distance);
                }
            }
        }
    }

    onAfterRender() {
        if (!this.gameObject) return;
        if (!this._lodsHandler) return;
        const cam = this.context.mainCamera;
        if (!cam) return;

        for (const h of this._lodsHandler) {
            h.update(cam);
            const levelIndex = h.getCurrentLevel();
            const level = h.levels[levelIndex];
            h.layers.mask = level.object.layers.mask;
        }
    }

    private onAddLodLevel(lod: ThreeLOD, obj: Object3D, dist: number) {
        if(obj === this.gameObject) {
            console.warn("LODGroup component must be on parent object and not mesh directly at the moment", obj.name, obj)
            return;
        }
        lod.addLevel(obj, dist * this._distanceFactor, .01);
        const setting = { lod: lod, levelIndex: lod.levels.length - 1, distance: dist };
        this._settings.push(setting)
    }

    private _distanceFactor = 1;

    distanceFactor(factor: number) {
        if (factor === this._distanceFactor) return;
        this._distanceFactor = factor;
        for (const setting of this._settings) {
            const level = setting.lod.levels[setting.levelIndex];
            level.distance = setting.distance * factor;
        }
    }
}