import { Icon } from '../tools/Icon';
import { Mesh } from './babylonjs-import';
import { Intersection } from './bim-format-types';
import { BimProductMesh } from './BimProductMesh';
import { PickOptionRay } from './Selectables';

/** What type of {@link PickResult} we have. */
export enum PickResultType {
    /** No pick result at all */
    Empty,

    /** A BabylonJS mesh was picked */
    Mesh,

    /** A {@link Icon} was picked */
    Icon,

    /** A {@link BimIfcObject} and one of its IFC mesh'es was picked */
    IfcProductMesh
}

/** Base class for some classes used in {@link PickResult}. */
abstract class PickResultBase {
    /**
     * Constructor
     * @param id Unique GPU picking id for the hit object
     */
    constructor(public readonly id: number) {}

    /** Exact intersection information. If no such information exists
     *  a empty array shall be returned.
     */
    abstract get hitInfo(): Intersection[];

    /**
     * Compare two {@link PickResult} instances.
     * @param pickResult
     */
    equals(pickResult: PickResult): boolean {
        return pickResult.type !== PickResultType.Empty && pickResult.id === this.id;
    }
}

/** Represents a pick operation where nothing was picked. */
export class PickResultEmpty {
    /** Reusable instance for use with pick operations yielding a empty result. */
    public static readonly instance = new PickResultEmpty();

    /** Discriminant union selector */
    public type: PickResultType.Empty = PickResultType.Empty;

    /** Always empty since its a empty pick operation. */
    public readonly hitInfo = Object.freeze<Intersection[]>([]) as Intersection[];

    /**
     * Compare two {@link PickResult} instances.
     * @param pickResult
     */
    equals(pickResult: PickResult): boolean {
        return pickResult.type === PickResultType.Empty;
    }
}

/** Represents a pick operation where a BabylonJS mesh was picked.
 *  Used when a picking operation hit a BabylonJS mesh that does not represent
 *  a Twinfinity object such as a {@link Icon} or {@link BimIfcObject}.
 */
export class PickResultMesh extends PickResultBase {
    /** Discriminant union selector */
    public type: PickResultType.Mesh = PickResultType.Mesh;

    /** Exact intersections of {@link mesh} that the pick operation gave. */
    public readonly hitInfo: Intersection[] = [];

    /** BabylonJS Mesh pick operation yielded. */
    public readonly mesh: Mesh;

    /**
     * Constructor
     * @param o - Options
     * @param o.id - Unique GPU picking id for the hit mesh.
     * @param o.mesh - BabylonJS Mesh that was hit by a pick operation
     * @param o.gpuPickMesh - The internal mesh (clone of `o.mesh`) that was used for the GPU pick operation
     * @param o.pickOptions - The picking ray and related options.
     */
    constructor(o: { id: number; mesh: Mesh; gpuPickMesh: Mesh; pickOptions: PickOptionRay }) {
        super(o.id);
        this.mesh = o.mesh;

        // Calculate hitInfo directly. Ideally it would have to be done lazily
        // But if we did it lazy (as we do of for other PickResults) then we
        // will refer to a gpuPickMesh that is no longer enabled, visible and might have
        // a incorrect transformatio matrix.
        if (o.gpuPickMesh.isPickable && o.pickOptions.isGeometryIntersectionEnabled) {
            const scene = o.mesh.getScene();
            const pI = scene.pickWithRay(o.pickOptions.ray, (m) => m === o.gpuPickMesh);
            const pickedNormal = pI?.getNormal(true);
            if (pI?.pickedPoint && pickedNormal) {
                this.hitInfo.push({
                    position: pI.pickedPoint,
                    distance: pI.distance,
                    normal: pickedNormal
                });
            }
        }
    }
}

/** Represents a pick operation where a {@link Icon} was picked. */
export class PickResultIcon extends PickResultBase {
    private readonly _pickOptions: PickOptionRay;
    private _hitInfo?: Intersection[];

    /** Discriminant union selector. */
    public type: PickResultType.Icon = PickResultType.Icon;

    /** Icon yielded by pick operation. */
    public readonly object: Icon;

    /** BabylonJS Mesh pick operation yielded. {@link object} belongs to this mesh. */
    public readonly mesh: Mesh;

    /**
     * Constructor
     * @param o - Options
     * @param o.id - Unique GPU picking id for the hit `o.icon`.
     * @param o.mesh - BabylonJS mesh that `o.icon` belongs to.
     * @param o.icon - The {@link Icon} that was hit.
     * @param o.pickOptions - The picking ray and related options.
     */
    public constructor(o: { id: number; mesh: Mesh; icon: Icon; pickOptions: PickOptionRay }) {
        super(o.id);
        this.mesh = o.mesh;
        this.object = o.icon;
        this._pickOptions = o.pickOptions;
    }

    /** Exact intersections of {@link object} that the pick operation gave. */
    public override get hitInfo(): Intersection[] {
        // Lazy evaluate icon hitinfo only if requested to avoid
        // expensive intersection operation if its not really used
        if (this._hitInfo === undefined) {
            this._hitInfo = this._pickOptions.isGeometryIntersectionEnabled
                ? this.object.intersect(this._pickOptions)
                : [];
        }
        return this._hitInfo;
    }
}

/** Represents a pick operation where a ifc product mesh  was picked. */
export class PickResultIfcProductMesh extends PickResultBase {
    private readonly _pickOptions: PickOptionRay;
    private _hitInfo?: Intersection[];

    /** Discriminant union selector */
    public type: PickResultType.IfcProductMesh = PickResultType.IfcProductMesh;

    /**
     * {@link BimProductMesh} yielded by pick operation.
     * Use {@link BimProductMesh.ifcProduct} to get IFC product ({@link BimIfcObject} the mesh belongs to
     */
    public readonly ifcProductMesh: BimProductMesh;

    /** BabylonJS Mesh pick operation yielded. {@link mesh} belongs to this mesh. */
    public readonly mesh: Mesh;

    /**
     * Constructor
     * @param o - Options
     * @param o.id - Unique GPU picking id for the hit `o.icon`.
     * @param o.mesh - BabylonJS mesh that `o.ifcProductAndMesh` belongs to.
     * @param o.ifcProductAndMesh - The {@link BimProductAndMesh} that was hit.
     * @param o.pickOptions - The picking ray and related options.
     */
    public constructor(o: { id: number; mesh: Mesh; ifcProductAndMesh: BimProductMesh; pickOptions: PickOptionRay }) {
        super(o.id);
        this.mesh = o.mesh;
        this.ifcProductMesh = o.ifcProductAndMesh;
        this._pickOptions = o.pickOptions;
    }

    /** Exact intersections of {@link ifcProductMesh} that the pick operation gave. */
    public override get hitInfo(): Intersection[] {
        // Lazy evaluate icon hitinfo only if requested to avoid
        // expensive intersection operation if its not really used
        if (this._hitInfo === undefined) {
            if (!this._pickOptions.isGeometryIntersectionEnabled) {
                this._hitInfo = [];
            } else {
                const ifcMesh = this.ifcProductMesh.mesh();
                this._hitInfo = ifcMesh.intersect(this._pickOptions);
            }
        }
        return this._hitInfo;
    }
}

/**
 *  The result of a "pick" operation. For example if user clicks something on screen a pick
 *  operation can be issued to determine what the click, if anything, hit.
 *  It is a discriminant union. Use the `type` property or `instanceof` to determine what
 *  kind of result the pick operation gives. It can be
 *  * Empty.
 *  * A BabylonJS mesh
 *  * A {@link Icon}. Includes the BabylonJS mesh that the {@link Icon} belongs to.
 *  * A IFC product and the IFC mesh in it. Includes the BabylonJS mesh that the {@link Icon} belongs to.
 */
export type PickResult = PickResultMesh | PickResultIcon | PickResultIfcProductMesh | PickResultEmpty;

/** Alias for {@link PickResult} for backwards compatibility.
 *  @deprecated Use {@link PickResult} instead.
 */
export type BimProductAndMeshAndHitInfo = PickResult;
