/** @internal NOTE: Internal APIs. Subject to change. Use of these APIs in production applications is not supported. */ /** */
import { Extensions } from '../Extensions';
import { Camera, Mesh, Ray, TargetCamera, Vector3 } from './babylonjs-import';
import { Materials } from './materials';
import { MergedIfcProductMeshesRenderInfo } from './MergedIfcProductMeshesRenderInfo';
import { Geometry3d, BimIfcProductsAndMeshes } from './vertex-data-merge';

/**
 * Additional data that decorates {@link Mesh} instances with geometry
 * that represents IFC objects.
 */
export class TwinfinityIfcMeshExtension {
    private static readonly _orthoFrustumSize = { x: 0, y: 0 };
    private static readonly _tmp = {
        vectorA: Vector3.Zero(),
        vectorB: Vector3.Zero(),
        ray: new Ray(Vector3.Zero(), Vector3.Up(), 1000)
    };

    /** Number of indices this {@link Mesh} contains. */
    public readonly indiceCount: number;

    /** Number of vertices, uv's and normals this {@link Mesh} contains. */
    public readonly primitiveCount: number;

    /** `true` if this {@link Mesh} contains transparent objects
     * (in which case it cannot contain opaque objects and `false` if it does) */
    public readonly isTransparent: boolean;

    /** All {@link BimIfcObject}'s and their {@link BimProductMesh}'es that
     * makes up the {@link Mesh} geometry.' */
    public readonly productsAndMeshes: BimIfcProductsAndMeshes;

    /** Distance from camera where `_mesh` will become visibile. If < 0 it is always visible. */
    public readonly cullingDistance: number;

    public cullOnCameraMove: boolean;

    /**
     * Info about render properties of all {@link BimIfcProduct} instances that have this `BJS mesh` handles.
     * Allows us to do quick checks if any of the ifc products in this mesh is visible, outlined or ghostoutlined.
     * @hidden
     * @internal
     */
    public readonly productMeshRenderInfo: MergedIfcProductMeshesRenderInfo;

    /**
     * Constructor
     * @param _mesh {@link Mesh} instance to connect the extension to.
     * @param _materials {@link Materials} instance
     * @param geometry3D {@link Geometry3dHandle} instance representing the geometry which will be applied to `_mesh`.
     */
    public constructor(
        private readonly _mesh: Mesh,
        private readonly _materials: Materials,
        geometry3D: Geometry3d
    ) {
        this.indiceCount = geometry3D.indices.length;
        this.primitiveCount = geometry3D.positions.length;
        this.isTransparent = geometry3D.isTransparent;

        this.productsAndMeshes = geometry3D.productsAndMeshes;
        this.cullingDistance = geometry3D.cullingDistance;
        this.cullOnCameraMove = geometry3D.cullOnCameraMove;

        this.productMeshRenderInfo = new MergedIfcProductMeshesRenderInfo(this._materials.globalRenderState);

        this.productMeshRenderInfo.assign(this.productsAndMeshes);
    }

    private _pivotTargetSphereIntersectionTest = (camera: TargetCamera): boolean => {
        const viewer = camera.twinfinity.viewer;
        if (!viewer) {
            throw new Error('Viewer is undefined.');
        }

        const bInfo = this._mesh.getBoundingInfo();
        const pivotPointSphereRadius = viewer.performance.ifc.hideExpensiveMeshesOnCameraMove.pickBoundingSphereRadius;

        const boundingSphereCenterDistanceToCameraTarget = bInfo.boundingSphere.centerWorld
            .subtractToRef(camera.twinfinity.pivot.target, TwinfinityIfcMeshExtension._tmp.vectorA)
            .length();
        return boundingSphereCenterDistanceToCameraTarget < pivotPointSphereRadius + bInfo.boundingSphere.radiusWorld;
    };

    public dispose(): void {
        this.productMeshRenderInfo.detach(this.productsAndMeshes);
    }

    /**
     * `true` if visible from the supplied cameras point of view. Otherwise `false`.
     * @param camera Camera to use when checking visibility
     */
    public isVisible(camera: Camera, distance?: number): boolean {
        const viewer = camera.twinfinity.viewer;
        if (!viewer) {
            throw new Error('Viewer is undefined.');
        }

        // No visible IFC objects in this mesh. Exit early.
        const nothingVisible =
            this.productMeshRenderInfo.state.visibleCount === 0 &&
            this.productMeshRenderInfo.state.ghostOutlineCount === 0;
        if (nothingVisible) {
            return false;
        }

        // Mesh is either distance culled or not possible to cull when camera moves. Exit early
        if (this.isDistanceCulled(camera, distance ?? this.cullingDistance)) {
            return false;
        }

        // Mesh cannot be culled on camera move. Hence mesh is visible. Exit early
        if (!this._mesh.twinfinity.ifc!.cullOnCameraMove) {
            return true;
        }

        const { hideExpensiveMeshesOnCameraMove } = viewer.performance.ifc;
        const lastCheck = viewer._cameraStateSnapshot?.lastCheckTimestamp;
        const lastChange = viewer._cameraStateSnapshot?.lastChangeTimestamp;
        const checkCameraMoveCulling =
            hideExpensiveMeshesOnCameraMove.isEnabled &&
            lastCheck &&
            lastChange &&
            lastCheck - lastChange < hideExpensiveMeshesOnCameraMove.delayUntilCullingStops;

        // We shall not cull on camera move at this time. Hence mesh is visible. Exit early
        if (!checkCameraMoveCulling) {
            return true;
        }

        // Time to check if camera shall be culled now that we know that camera is moving and culling on camera move is enabled.

        // Check if the pivot point target sphere intersects the mesh bounding sphere. If so the mesh is visible.
        const pivotPointTargetBoundingSphereIntersectsMesh =
            hideExpensiveMeshesOnCameraMove.keepMeshesNearPivotPoint && camera instanceof TargetCamera
                ? this._pivotTargetSphereIntersectionTest(camera)
                : false;

        if (pivotPointTargetBoundingSphereIntersectsMesh) {
            return true;
        }

        // Check if point of interest ray intersects the mesh bounding box. If so the mesh is visible. (We do not want to cull
        // meshes near the point of interest, ie where we "look").
        const poiRay = camera.getForwardRayToRef(TwinfinityIfcMeshExtension._tmp.ray);
        const { boundingBox } = this._mesh.getBoundingInfo();
        const poiRayIntersectsMesh =
            hideExpensiveMeshesOnCameraMove.keepMeshesNearPointOfInterest &&
            poiRay.intersectsBoxMinMax(boundingBox.minimumWorld, boundingBox.maximumWorld);

        return poiRayIntersectsMesh;
    }

    /**
     * Gets the distance from an mesh to the camera
     * @param camera Camera to use when checking distance
     */
    public distanceToCamera(camera: Camera): number {
        const bSphere = this._mesh.getBoundingInfo().boundingSphere;
        if (camera.mode === Camera.PERSPECTIVE_CAMERA) {
            const tmp = bSphere.centerWorld.subtractToRef(
                camera.globalPosition,
                TwinfinityIfcMeshExtension._tmp.vectorA
            );
            return tmp.length() - bSphere.radius;
        } else {
            // Ortographic camera
            camera.twinfinity.getOrthoFrustumSizeToRef(TwinfinityIfcMeshExtension._orthoFrustumSize);
            return TwinfinityIfcMeshExtension._orthoFrustumSize.x;
        }
    }

    private isDistanceCulled(camera: Camera, cullingDistance: number): boolean {
        // GOOD IDEA? If camera is stationary then we could adjust lod distance dynamically so we show objects
        // further out? Could make things jerky when camera starts to move again.

        if (cullingDistance < 0) {
            return false;
        }

        const meshDistanceToCamera = this.distanceToCamera(camera);

        if (camera.mode === Camera.PERSPECTIVE_CAMERA) {
            // DO not show mesh if it is further away from the camera than its culling distance
            const isCulled = meshDistanceToCamera > cullingDistance;
            return isCulled;
        }

        // Ortographic camera
        // 2 is just a magic constant to give illusion of aprox the same culling behavior as in the perspective camera.
        const orthoCullingDistance = cullingDistance * 2 * camera.getEngine().getHardwareScalingLevel();
        const isCulled = meshDistanceToCamera > orthoCullingDistance;
        return isCulled;
    }
}
/**
 * Twinfinity extensions for {@link Mesh}.
 */
export interface TwinfinityMeshExtensions {
    /** If set then this mesh contains IFC data. */
    ifc?: TwinfinityIfcMeshExtension;

    /**
     * Clear cached vertex data of this mesh. Basically this removes the
     * cached vertex data from RAM (but not from GPU). This has the same
     * effects as calling {@link Scene.clearCachedVertexData} (except that it only
     * affects this particular {@link Mesh}).
     */
    clearCachedVertexData(): void;

    /*private createNormalsMesh(mesh: Mesh, size: number, color: Color3, sc: Scene): Mesh {
        const normals = mesh.getVerticesData(VertexBuffer.NormalKind)!;
        const positions = mesh.getVerticesData(VertexBuffer.PositionKind)!;

        //   const colors: Color4[][] = [];
        //   const color4 = color.toColor4();
        const lines = [];
        for (let i = 0; i < normals!.length; i += 3) {
            const v1 = Vector3.FromArray(positions!, i);
            const v2 = v1.add(Vector3.FromArray(normals!, i).scaleInPlace(size));
            lines.push([v1, v2]);
        }

        //   for (let i = 0; i < normals.length; i += 3) {
        //       const v1 = Vector3.FromArray(positions, i);
        //       const v2 = v1.add(Vector3.FromArray(normals, i).scaleInPlace(size));
        //       lines.push([v1.add(mesh.position), v2.add(mesh.position)]);
        //       colors.push([color4, color4]);
        //   }

        const normalLines = MeshBuilder.CreateLineSystem(
            `${mesh.id}.normalLines`,
            {
                lines,
            },
            sc,
        );
        normalLines.parent = mesh;
        normalLines.color = color;
        //   normalLines.mater
        normalLines.isPickable = false;
        return normalLines;
    }*/
}

declare module '@babylonjs/core/Meshes/mesh' {
    export interface Mesh {
        /** Twinfinity extensions. */
        readonly twinfinity: TwinfinityMeshExtensions;
    }
}

function clearCachedVertexData(mesh: Mesh): void {
    if (!mesh.geometry) {
        return;
    }
    if (mesh.geometry._indices) {
        mesh.geometry._indices = [];
    }
    if (mesh.geometry._vertexBuffers) {
        for (const vb of Object.values(mesh.geometry._vertexBuffers)) {
            // if (name === 'uv') {
            //     continue;
            // }
            vb._buffer._data = null;
        }
    }
}

Object.defineProperty(Mesh.prototype, Extensions.TWINFINITY_PROPERTY, {
    get: function (this: Mesh): TwinfinityMeshExtensions {
        const thisAny = this as unknown as { _twinfinity?: TwinfinityMeshExtensions };
        if (thisAny._twinfinity != null) {
            return thisAny._twinfinity;
        }
        thisAny._twinfinity = {
            ifc: undefined,
            clearCachedVertexData: () => clearCachedVertexData(this)
        };

        // Avoid a brutal memory leak. When mesh is disposed we must disconnect the additional twinfinity data.
        // Not sure why but perhaps BJS caches the mesh somewhere for reuse.
        this.onDisposeObservable.addOnce(() => {
            thisAny._twinfinity?.ifc?.dispose();
            thisAny._twinfinity = undefined;
        });
        return thisAny._twinfinity;
    },
    set: function (this: Mesh, p: TwinfinityIfcMeshExtension): void {}
});
