import { BimIfcIndexDTO, IfcFileIndex, BimIfcIndex } from './BimIfcIndex';
import { BimCoreApiClient } from './client/BimCoreApiClient';
import { HttpResponseType } from '../http';
import { Discipline } from './discipline';
import { Fail, Failure } from '../fail';
import { Matrix, Quaternion, Vector3 } from './babylonjs-maths-import';
import { setMax, setMin, Vertex3 } from '../math';
import { coordinateSystems } from './babylon-coordinate-system';
import { BimTransformsRepository } from './bim-transforms-repository';

/**
 * Represents the failure result when reading an IFC file index fails.
 */
export type IfcFileIndexReadFailure = Failure<{
    /**
     * Status of the HTTP request that failed.
     */
    httpStatus: number;
    /**
     * Gives the reason for the failure.
     */
    reason: string;
    /**
     * The URL that was being fetched when the failure occurred.
     */
    url: string;
}>;

interface TmpVars {
    scale: Vector3;
    rotation: Quaternion;
    translate: Vector3;
    matrix: Matrix;
    points: [Vector3, Vector3, Vector3, Vector3, Vector3, Vector3, Vector3, Vector3];
    min: Vector3;
    max: Vector3;
}

/**
 * Handles fetching and parsing .idx files to create IfcFileIndex instances.
 */
export class IfcFileIndexReader {
    private static readonly _tmp: TmpVars = {
        scale: Vector3.Zero(),
        rotation: Quaternion.Identity(),
        translate: Vector3.Zero(),
        matrix: Matrix.Identity(),
        points: [
            Vector3.Zero(),
            Vector3.Zero(),
            Vector3.Zero(),
            Vector3.Zero(),
            Vector3.Zero(),
            Vector3.Zero(),
            Vector3.Zero(),
            Vector3.Zero()
        ],
        min: Vector3.Zero(),
        max: Vector3.Zero()
    };

    constructor(private readonly bimApi: BimCoreApiClient) {}

    /**
     * Fetches an .idx file from the given URL and creates an IfcFileIndex instance.
     *
     * @param indexUrl - The URL of the .idx file to fetch
     * @param discipline - The discipline to associate with the index
     * @param transform - Optional transform to apply to all transformations in the IFC
     * @returns A Promise that resolves to an IfcFileIndex instance or a failure result
     */
    async read(
        indexUrl: URL,
        discipline: Discipline,
        transform?: Matrix
    ): Promise<IfcFileIndex | IfcFileIndexReadFailure> {
        const ifcIndexResponse = await this.bimApi.get<BimIfcIndexDTO>(indexUrl, HttpResponseType.json);

        if (ifcIndexResponse.status !== 200) {
            return Fail({
                httpStatus: ifcIndexResponse.status,
                reason: 'Failed to fetch .idx file.',
                url: indexUrl.toString()
            });
        }

        const ifcIndexDTO = await ifcIndexResponse.value;
        const ifcIndex = new IfcFileIndex(ifcIndexDTO, discipline);

        // Process transforms and regions with defaults and coordinate system conversions
        this.processIndexData(ifcIndex, transform);

        // Clean up the DTO to free memory since IfcFileIndex has copied its references
        Object.keys(ifcIndexDTO).forEach((key) => delete (ifcIndexDTO as any)[key]);

        return ifcIndex;
    }

    private processIndexData(index: BimIfcIndex, transform?: Matrix): void {
        const k = 10000.0;
        const toMeterScale = 1.0 / index.modelFactors.oneMeter;
        const tmp = IfcFileIndexReader._tmp;

        // Create a temporary transforms repository to use for matrix conversions
        const tempTransformsRepo = {
            convertBimTransformToMatrixToRef: BimTransformsRepository.prototype.convertBimTransformToMatrixToRef
        };

        for (const wt of index.transforms) {
            if (!wt) continue;

            // Apply default values and scaling
            (wt.x as number) = (wt.x ?? 0) * toMeterScale;
            (wt.y as number) = (wt.y ?? 0) * toMeterScale;
            (wt.z as number) = (wt.z ?? 0) * toMeterScale;

            (wt.qx as number) = (wt.qx ?? 0) / k;
            (wt.qy as number) = (wt.qy ?? 0) / k;
            (wt.qz as number) = (wt.qz ?? 0) / k;
            (wt.qw as number) = (wt.qw ?? k) / k;

            (wt.sx as number) = (wt.sx ?? 1) * toMeterScale;
            (wt.sy as number) = (wt.sy ?? 1) * toMeterScale;
            (wt.sz as number) = (wt.sz ?? 1) * toMeterScale;

            const ifcTransform = tempTransformsRepo.convertBimTransformToMatrixToRef.call({ index }, wt, tmp.matrix);

            // Convert transforms from IFC space to Babylon.js space
            const bjsTransform = coordinateSystems.convertIfcTransformToBabylonTransformInPlace(ifcTransform);

            if (transform) {
                // bjsTransform = transform * bjsTransform
                bjsTransform.multiplyToRef(transform, bjsTransform);
            }
            bjsTransform.decompose(tmp.scale, tmp.rotation, tmp.translate);

            (wt.x as number) = tmp.translate.x;
            (wt.y as number) = tmp.translate.y;
            (wt.z as number) = tmp.translate.z;

            (wt.qx as number) = tmp.rotation.x;
            (wt.qy as number) = tmp.rotation.y;
            (wt.qz as number) = tmp.rotation.z;
            (wt.qw as number) = tmp.rotation.w;

            (wt.sx as number) = tmp.scale.x;
            (wt.sy as number) = tmp.scale.y;
            (wt.sz as number) = tmp.scale.z;
        }

        // Process region bounding boxes
        for (const region of index.regions) {
            this.getBabylonBboxPointsFromIfcMinMaxToRef(
                { x: region.bbox[0], y: region.bbox[1], z: region.bbox[2] },
                { x: region.bbox[3], y: region.bbox[4], z: region.bbox[5] },
                tmp.points,
                toMeterScale,
                transform
            );

            setMax(tmp.min);
            setMin(tmp.max);

            tmp.points.forEach((point) => {
                tmp.min.minimizeInPlace(point);
                tmp.max.maximizeInPlace(point);
            });

            region.bbox[0] = tmp.min.x;
            region.bbox[1] = tmp.min.y;
            region.bbox[2] = tmp.min.z;

            region.bbox[3] = tmp.max.x;
            region.bbox[4] = tmp.max.y;
            region.bbox[5] = tmp.max.z;
        }
    }

    private getBabylonBboxPointsFromIfcMinMaxToRef(
        min: Vertex3,
        max: Vertex3,
        out: [Vector3, Vector3, Vector3, Vector3, Vector3, Vector3, Vector3, Vector3],
        scale = 1,
        transform?: Matrix
    ): void {
        out[0].set(min.x, min.z, min.y).scaleInPlace(scale);
        out[1].set(max.x, min.z, min.y).scaleInPlace(scale);
        out[2].set(min.x, min.z, max.y).scaleInPlace(scale);
        out[3].set(min.x, max.z, min.y).scaleInPlace(scale);
        out[4].set(max.x, min.z, max.y).scaleInPlace(scale);
        out[5].set(max.x, max.z, min.y).scaleInPlace(scale);
        out[6].set(min.x, max.z, max.y).scaleInPlace(scale);
        out[7].set(max.x, max.z, max.y).scaleInPlace(scale);

        if (transform) {
            out.forEach((p) => {
                Vector3.TransformCoordinatesToRef(p, transform, p);
            });
        }
    }
}
