import { setMax, setMin } from '../math';
import { FastTransform } from '../math/FastTransform';
import { Writeable } from '../Types';
import { BoundingInfo, DeepImmutable, Matrix, Vector3 } from './babylonjs-import';
import { BimChangeIfc } from './bim-api-client';
import { BimProductMeshDescriptor, BimVertexData } from './bim-format-types';
import type { FlatSegment3, VertexData } from '@twinfinity/geometry';
import { BimProductMesh } from './BimProductMesh';
import { IBimIfcLoaderElement } from './bim-ifc-loader-element';
import { BimIfcMesh } from './bim-ifc-mesh';
import { BimIfcObject } from './bim-ifc-object';
import { BimIfcStyle } from './bim-ifc-style';
import { IfcMaterialHighlightIndex, IfcMaterialRenderProperty } from './CustomBabylonMaterials/IfcMaterial';
import { Materials } from './materials';
import { RgbaComponent } from './rgba';
import { MergedIfcProductMeshesRenderInfo } from './MergedIfcProductMeshesRenderInfo';
import { Uint32Bits } from '../Uint32Bits';
import { MergeId } from './MergeId';
import { ModelProductClass } from '@twinfinity/t8-client-model';

/**
 * Represents the mesh of an IFC product. An IFC product may
 * consist of `0..N` meshes. A door is often represented as two meshes: a
 * rectangle and a door handle.
 */
export class IfcProductMesh implements BimProductMesh {
    private static readonly _tmp = {
        vectorA: Vector3.Zero(),
        vectorB: Vector3.Zero(),
        vectorC: Vector3.Zero()
    };

    readonly colorTexturePixelIndex: number = -1;

    /** Determine which babylonjs mesh this ifc product mesh shall belong to. */
    public mergeId = '';

    /** @inheritDoc */
    public cullingDistance = -1;

    /** @inheritDoc */
    public edges?: FlatSegment3[];

    /** Center of the cube, this mesh will be a part of, in IFC space (not worldspace) */
    public centerOfParentIfcCube?: DeepImmutable<Vector3>;

    public _renderPropertyInfo?: MergedIfcProductMeshesRenderInfo;

    private readonly _loaderElement: IBimIfcLoaderElement;

    constructor(
        public readonly ifcProduct: BimIfcObject,
        public readonly mi: number,
        public readonly r: number,
        public readonly si: number,
        public readonly wt: number
    ) {
        this._loaderElement = ifcProduct.ifcLoaderElement;
    }

    public static from(o: BimIfcObject, dto: BimProductMesh): BimProductMesh {
        // TODO: Fix better override of space colors. Currently we know that the last element of
        // o.ifcLoaderElement.styles is a specially created style that should be used for all
        // IFC spaces. The only reason is that requirements stated that they should be green, which they
        // are not always in the IFC files.
        const si = o.class === ModelProductClass.predefined.space ? o.ifcLoaderElement.styles.length - 1 : dto.si;
        return new IfcProductMesh(o, dto.mi ?? 0, dto.r ?? 0, si ?? 0, dto.wt ?? 0);
    }

    public get vertexCount(): number {
        return this.descriptor.p;
    }

    public get indexCount(): number {
        return this.descriptor.i;
    }

    public getVertexData(): Promise<VertexData> {
        return Promise.resolve(this.vertexData);
    }

    public async getEdges(): Promise<FlatSegment3[]> {
        const cached = this.edges;
        if (cached) return cached;

        await this._loaderElement.loadGeometricFeatures();

        const resolved = this._loaderElement.geometricFeaturesRepository.resolveEdgesForMesh(this);
        return resolved ?? [];
    }

    public get isOnGpu(): boolean {
        // Empty string mergeid means that mesh is not on gpu
        return !!this.mergeId;
    }

    public get visible(): boolean {
        return this.renderProperties.has(IfcMaterialRenderProperty.visibility);
    }

    public set visible(visible: boolean) {
        this.renderProperties.set(IfcMaterialRenderProperty.visibility, visible);
    }

    public get outline(): boolean {
        return this.renderProperties.has(IfcMaterialRenderProperty.outline);
    }

    public set outline(value: boolean) {
        this.renderProperties.set(IfcMaterialRenderProperty.outline, value);
    }

    public get highlight(): IfcMaterialHighlightIndex {
        return this.renderProperties.getNumber(IfcMaterialRenderProperty.highlight, 2);
    }

    public set highlight(value: IfcMaterialHighlightIndex) {
        this.renderProperties.setNumber(IfcMaterialRenderProperty.highlight, 2, value);
    }

    public get ignoreClipPlanes(): boolean {
        return this.materials.renderPropertiesFor(this).has(IfcMaterialRenderProperty.ignoreClipPlanes);
    }

    public set ignoreClipPlanes(value: boolean) {
        this.materials.renderPropertiesFor(this).set(IfcMaterialRenderProperty.ignoreClipPlanes, value);
    }

    public get depthWriteIgnoresVisibility(): boolean {
        return this.renderProperties.has(IfcMaterialRenderProperty.depthWriteIgnoresVisibility);
    }

    public set depthWriteIgnoresVisibility(value: boolean) {
        this.renderProperties.set(IfcMaterialRenderProperty.depthWriteIgnoresVisibility, value);
    }

    public get ghostOutline(): boolean {
        return this.depthWriteIgnoresVisibility && !this.visible;
    }

    public set ghostOutline(enabled: boolean) {
        // NOTE: I believe this is a bug. ghostOutline should only be a getter.
        // It is depthWriteIgnoresVisibility that controls if a non-visible mesh is rendered as a ghost outline or not.
        // We should probably remove ghostOutline altogether and rename depthWriteIgnoresVisibility to ghostOutline.
        // Perhaps call it ghost visibility or something?
        let renderProperty = this.renderProperties.value;
        renderProperty = Uint32Bits.set(renderProperty, IfcMaterialRenderProperty.depthWriteIgnoresVisibility, enabled);
        // this.renderProperties._onChanged is only executed once even if we set two properties.
        this.renderProperties.value = Uint32Bits.set(renderProperty, IfcMaterialRenderProperty.visibility, !enabled);
    }

    public get descriptor(): BimProductMeshDescriptor {
        return this.loaderElement.index.meshDescriptors[this.mi]!;
    }

    public get style(): BimIfcStyle {
        return this.loaderElement.styles[this.si];
    }

    public get ifc(): BimChangeIfc {
        return this.loaderElement.ifc;
    }

    public get loaderElement(): IBimIfcLoaderElement {
        return this._loaderElement;
    }

    public get vertexData(): BimVertexData {
        return this.loaderElement.getVertexData(this.descriptor);
    }

    private get renderProperties(): Uint32Bits {
        return this.materials.renderPropertiesFor(this);
    }

    public aabb(min: Vector3, max: Vector3, noCache = true): void {
        const tmpTransform = this.loaderElement.transformsRepository.getTransform(this, noCache);
        const tmpBbox = this.descriptor.bbox;
        const tmpVector = IfcProductMesh._tmp.vectorA;
        tmpTransform.transformInPlaceVector3(tmpBbox[0], tmpBbox[1], tmpBbox[2], tmpVector);
        min.minimizeInPlace(tmpVector);
        max.maximizeInPlace(tmpVector);
        tmpTransform.transformInPlaceVector3(tmpBbox[3], tmpBbox[4], tmpBbox[5], tmpVector);
        min.minimizeInPlace(tmpVector);
        max.maximizeInPlace(tmpVector);
    }

    public _clearMergeId(): boolean {
        if (!this.mergeId) {
            return false;
        }
        this.mergeId = '';
        this.cullingDistance = -1;
        this.centerOfParentIfcCube = undefined;
        this._renderPropertyInfo = undefined;
        return true;
    }

    // Divides the world into aabbs with a set side length. Basically, we subdivide IFC space into
    // aabbs (cubes) with equal length. We then look at each IFC mesh and assign it to one of the aabbs.
    // All meshes which end up in the aabb cube will be merged for performance reasons. We actually
    // have two types of aabbs: one for large objects and one for small objects. Aabbs containing
    // small objects are distance culled, i.e., they are not visible if the camera is further away than
    // N units. This can give a large performance boost (fps) since small objects can be very complex.
    // NOTE: Cube sizes should probably not be hardcoded if possible.
    // NOTE: Empirical studies show that we get good performance with around 2000 meshes.
    // We should attempt to mesh so we get a maximum of that. If we look at the total number of
    // triangles, we would get an indication of how many triangles we should attempt to put in each mesh.
    public _setMergeId(mergeId: MergeId): boolean {
        this.mergeId = mergeId.id;
        this.cullingDistance = mergeId.cullingDistance;
        this.centerOfParentIfcCube = mergeId.cubeCellCenter;
        return true;
    }

    public boundingInfo(bI?: BoundingInfo, noCache = true): BoundingInfo {
        const tmpMin = IfcProductMesh._tmp.vectorB;
        const tmpMax = IfcProductMesh._tmp.vectorC;
        if (bI === undefined) {
            this.aabb(setMax(tmpMin), setMin(tmpMax), noCache);
            return new BoundingInfo(tmpMin, tmpMax);
        }

        this.aabb(tmpMin.copyFrom(bI.minimum), tmpMax.copyFrom(bI.maximum), noCache);
        bI.reConstruct(tmpMin, tmpMax);
        return bI;
    }

    public defaultColor(): boolean {
        return this.setColor(this.style.color);
    }

    public transform(noCache = true): Matrix {
        return this.loaderElement.transformsRepository.getMatrix(this, noCache);
    }

    public fastTransform(noCache = true): FastTransform {
        return this.loaderElement.transformsRepository.getTransform(this, noCache);
    }

    public mesh(noCache = true): BimIfcMesh {
        return new BimIfcMesh(
            this.vertexData,
            this.transform(noCache),
            1.0 / this.loaderElement.index.modelFactors.oneMeter
        );
    }

    public copyColorTo(dst: Writeable<ArrayLike<number>>, dstOffset = 0): void {
        return this.materials.copyColorTo(this, dst, dstOffset);
    }

    public setColor(color: ArrayLike<number>): boolean {
        return this.materials.setColor(this, color);
    }

    public getColorComponent(colorComponent: RgbaComponent): number {
        return this.materials.getColorComponent(this, colorComponent);
    }

    private get materials(): Materials {
        return this.loaderElement.materials;
    }
}
