import { mat4, vec3 } from 'gl-matrix';
import vtkPlane from '@kitware/vtk.js/Common/DataModel/Plane';
import cache from '../cache/cache';
import { EPSILON, MPR_CAMERA_VALUES, RENDERING_DEFAULTS } from '../constants';
import { OrientationAxis, Events } from '../enums';
import { actorIsA, isImageActor } from '../utilities/actorCheck';
import getClosestImageId from '../utilities/getClosestImageId';
import getSliceRange from '../utilities/getSliceRange';
import getSpacingInNormalDirection from '../utilities/getSpacingInNormalDirection';
import snapFocalPointToSlice from '../utilities/snapFocalPointToSlice';
import triggerEvent from '../utilities/triggerEvent';
import BaseVolumeViewport from './BaseVolumeViewport';
import setDefaultVolumeVOI from './helpers/setDefaultVolumeVOI';
import { setTransferFunctionNodes } from '../utilities/transferFunctionUtils';
import getImageSliceDataForVolumeViewport from '../utilities/getImageSliceDataForVolumeViewport';
import { transformCanvasToIJK } from '../utilities/transformCanvasToIJK';
import { transformIJKToCanvas } from '../utilities/transformIJKToCanvas';
import getVolumeViewportScrollInfo from '../utilities/getVolumeViewportScrollInfo';
class VolumeViewport extends BaseVolumeViewport {
    constructor(props) {
        super(props);
        this._useAcquisitionPlaneForViewPlane = false;
        this.getNumberOfSlices = () => {
            const { numberOfSlices } = getImageSliceDataForVolumeViewport(this) || {};
            return numberOfSlices;
        };
        this.resetCameraForResize = () => {
            return this.resetCamera({
                resetPan: true,
                resetZoom: true,
                resetToCenter: true,
                resetRotation: false,
                suppressEvents: true,
            });
        };
        this.getCurrentImageIdIndex = (volumeId, useSlabThickness = true) => {
            const { currentStepIndex } = getVolumeViewportScrollInfo(this, volumeId || this.getVolumeId(), useSlabThickness);
            return currentStepIndex;
        };
        this.getSliceIndex = () => {
            const { imageIndex } = getImageSliceDataForVolumeViewport(this) || {};
            return imageIndex;
        };
        this.getCurrentImageId = () => {
            const actorEntry = this.getDefaultActor();
            if (!actorEntry || !actorIsA(actorEntry, 'vtkVolume')) {
                return;
            }
            const volume = cache.getVolume(this.getVolumeId());
            if (!volume) {
                return;
            }
            const { viewPlaneNormal, focalPoint } = this.getCamera();
            return getClosestImageId(volume, focalPoint, viewPlaneNormal);
        };
        this.getSlicePlaneCoordinates = () => {
            const actorEntry = this.getDefaultActor();
            if (!actorEntry?.actor) {
                console.warn('No image data found for calculating vtkPlanes.');
                return [];
            }
            const volumeId = this.getVolumeId();
            const imageVolume = cache.getVolume(volumeId);
            const camera = this.getCamera();
            const { focalPoint, position, viewPlaneNormal } = camera;
            const spacingInNormalDirection = getSpacingInNormalDirection(imageVolume, viewPlaneNormal);
            const sliceRange = getSliceRange(actorEntry.actor, viewPlaneNormal, focalPoint);
            const numSlicesBackward = Math.round((sliceRange.current - sliceRange.min) / spacingInNormalDirection);
            const numSlicesForward = Math.round((sliceRange.max - sliceRange.current) / spacingInNormalDirection);
            const currentSliceIndex = this.getSliceIndex();
            const focalPoints = [];
            for (let i = -numSlicesBackward; i <= numSlicesForward; i++) {
                const { newFocalPoint: point } = snapFocalPointToSlice(focalPoint, position, sliceRange, viewPlaneNormal, spacingInNormalDirection, i);
                focalPoints.push({ sliceIndex: currentSliceIndex + i, point });
            }
            return focalPoints;
        };
        const { orientation } = this.options;
        if (orientation && orientation !== OrientationAxis.ACQUISITION) {
            this.applyViewOrientation(orientation);
            return;
        }
        this._useAcquisitionPlaneForViewPlane = true;
    }
    async setVolumes(volumeInputArray, immediate = false, suppressEvents = false) {
        const firstImageVolume = cache.getVolume(volumeInputArray[0].volumeId);
        if (!firstImageVolume) {
            throw new Error(`imageVolume with id: ${firstImageVolume.volumeId} does not exist`);
        }
        if (this._useAcquisitionPlaneForViewPlane) {
            this._setViewPlaneToAcquisitionPlane(firstImageVolume);
            this._useAcquisitionPlaneForViewPlane = false;
        }
        return super.setVolumes(volumeInputArray, immediate, suppressEvents);
    }
    async addVolumes(volumeInputArray, immediate = false, suppressEvents = false) {
        const firstImageVolume = cache.getVolume(volumeInputArray[0].volumeId);
        if (!firstImageVolume) {
            throw new Error(`imageVolume with id: ${firstImageVolume.volumeId} does not exist`);
        }
        if (this._useAcquisitionPlaneForViewPlane) {
            this._setViewPlaneToAcquisitionPlane(firstImageVolume);
            this._useAcquisitionPlaneForViewPlane = false;
        }
        return super.addVolumes(volumeInputArray, immediate, suppressEvents);
    }
    jumpToWorld(worldPos) {
        const { focalPoint } = this.getCamera();
        const delta = [0, 0, 0];
        vec3.sub(delta, worldPos, focalPoint);
        const camera = this.getCamera();
        const normal = camera.viewPlaneNormal;
        const dotProd = vec3.dot(delta, normal);
        const projectedDelta = vec3.fromValues(normal[0], normal[1], normal[2]);
        vec3.scale(projectedDelta, projectedDelta, dotProd);
        if (Math.abs(projectedDelta[0]) > 1e-3 ||
            Math.abs(projectedDelta[1]) > 1e-3 ||
            Math.abs(projectedDelta[2]) > 1e-3) {
            const newFocalPoint = [0, 0, 0];
            const newPosition = [0, 0, 0];
            vec3.add(newFocalPoint, camera.focalPoint, projectedDelta);
            vec3.add(newPosition, camera.position, projectedDelta);
            this.setCamera({
                focalPoint: newFocalPoint,
                position: newPosition,
            });
            this.render();
        }
        return true;
    }
    setOrientation(orientation, immediate = true) {
        let viewPlaneNormal, viewUp;
        if (typeof orientation === 'string') {
            if (MPR_CAMERA_VALUES[orientation]) {
                ({ viewPlaneNormal, viewUp } = MPR_CAMERA_VALUES[orientation]);
            }
            else if (orientation === 'acquisition') {
                ({ viewPlaneNormal, viewUp } = this._getAcquisitionPlaneOrientation());
            }
            else {
                throw new Error(`Invalid orientation: ${orientation}. Use Enums.OrientationAxis instead.`);
            }
            this.setCamera({
                viewPlaneNormal,
                viewUp,
            });
            this.viewportProperties.orientation = orientation;
            this.resetCamera();
        }
        else {
            ({ viewPlaneNormal, viewUp } = orientation);
            this.applyViewOrientation(orientation);
        }
        if (immediate) {
            this.render();
        }
    }
    setCameraClippingRange() {
        const activeCamera = this.getVtkActiveCamera();
        if (!activeCamera) {
            console.warn('No active camera found');
            return;
        }
        if (activeCamera.getParallelProjection()) {
            activeCamera.setClippingRange(-RENDERING_DEFAULTS.MAXIMUM_RAY_DISTANCE, RENDERING_DEFAULTS.MAXIMUM_RAY_DISTANCE);
        }
        else {
            activeCamera.setClippingRange(RENDERING_DEFAULTS.MINIMUM_SLAB_THICKNESS, RENDERING_DEFAULTS.MAXIMUM_RAY_DISTANCE);
        }
    }
    _getAcquisitionPlaneOrientation() {
        const actorEntry = this.getDefaultActor();
        if (!actorEntry) {
            return;
        }
        const volumeId = this.getVolumeId();
        const imageVolume = cache.getVolume(volumeId);
        if (!imageVolume) {
            throw new Error(`imageVolume with id: ${volumeId} does not exist in cache`);
        }
        const { direction } = imageVolume;
        const viewPlaneNormal = direction.slice(6, 9).map((x) => -x);
        const viewUp = direction.slice(3, 6).map((x) => -x);
        return {
            viewPlaneNormal,
            viewUp,
        };
    }
    _setViewPlaneToAcquisitionPlane(imageVolume) {
        let viewPlaneNormal, viewUp;
        if (imageVolume) {
            const { direction } = imageVolume;
            viewPlaneNormal = direction.slice(6, 9).map((x) => -x);
            viewUp = direction.slice(3, 6).map((x) => -x);
        }
        else {
            ({ viewPlaneNormal, viewUp } = this._getAcquisitionPlaneOrientation());
        }
        this.setCamera({
            viewPlaneNormal,
            viewUp,
        });
        this.initialViewUp = viewUp;
        this.resetCamera();
    }
    getBlendMode(filterActorUIDs) {
        const actorEntries = this.getActors();
        const actorForBlend = filterActorUIDs?.length > 0
            ? actorEntries.find((actorEntry) => filterActorUIDs.includes(actorEntry.uid))
            : actorEntries[0];
        return (actorForBlend?.blendMode ||
            actorForBlend?.actor.getMapper().getBlendMode());
    }
    setBlendMode(blendMode, filterActorUIDs = [], immediate = false) {
        let actorEntries = this.getActors();
        if (filterActorUIDs?.length > 0) {
            actorEntries = actorEntries.filter((actorEntry) => {
                return filterActorUIDs.includes(actorEntry.uid);
            });
        }
        actorEntries.forEach((actorEntry) => {
            const { actor } = actorEntry;
            const mapper = actor.getMapper();
            mapper.setBlendMode?.(blendMode);
            actorEntry.blendMode = blendMode;
        });
        if (immediate) {
            this.render();
        }
    }
    resetCamera(options) {
        const { resetPan = true, resetZoom = true, resetRotation = true, resetToCenter = true, suppressEvents = false, resetOrientation = true, } = options || {};
        const { orientation } = this.viewportProperties;
        if (orientation && resetOrientation) {
            this.applyViewOrientation(orientation, false);
        }
        super.resetCamera({ resetPan, resetZoom, resetToCenter });
        const activeCamera = this.getVtkActiveCamera();
        const viewPlaneNormal = activeCamera.getViewPlaneNormal();
        const focalPoint = activeCamera.getFocalPoint();
        const actorEntries = this.getActors();
        actorEntries.forEach((actorEntry) => {
            if (!actorEntry.actor) {
                return;
            }
            const mapper = actorEntry.actor.getMapper();
            const vtkPlanes = mapper.getClippingPlanes();
            if (vtkPlanes.length === 0 && !actorEntry?.clippingFilter) {
                const clipPlane1 = vtkPlane.newInstance();
                const clipPlane2 = vtkPlane.newInstance();
                const newVtkPlanes = [clipPlane1, clipPlane2];
                let slabThickness = RENDERING_DEFAULTS.MINIMUM_SLAB_THICKNESS;
                if (actorEntry.slabThickness) {
                    slabThickness = actorEntry.slabThickness;
                }
                this.setOrientationOfClippingPlanes(newVtkPlanes, slabThickness, viewPlaneNormal, focalPoint);
                mapper.addClippingPlane(clipPlane1);
                mapper.addClippingPlane(clipPlane2);
            }
        });
        if (resetRotation &&
            MPR_CAMERA_VALUES[this.viewportProperties.orientation] !== undefined) {
            const viewToReset = MPR_CAMERA_VALUES[this.viewportProperties.orientation];
            this.setCameraNoEvent({
                viewUp: viewToReset.viewUp,
                viewPlaneNormal: viewToReset.viewPlaneNormal,
            });
        }
        if (!suppressEvents) {
            const eventDetail = {
                viewportId: this.id,
                camera: this.getCamera(),
                renderingEngineId: this.renderingEngineId,
                element: this.element,
            };
            triggerEvent(this.element, Events.CAMERA_RESET, eventDetail);
        }
        return true;
    }
    setSlabThickness(slabThickness, filterActorUIDs = []) {
        if (slabThickness < 0.1) {
            slabThickness = 0.1;
        }
        let actorEntries = this.getActors();
        if (filterActorUIDs?.length > 0) {
            actorEntries = actorEntries.filter((actorEntry) => {
                return filterActorUIDs.includes(actorEntry.uid);
            });
        }
        actorEntries.forEach((actorEntry) => {
            if (actorIsA(actorEntry, 'vtkVolume')) {
                actorEntry.slabThickness = slabThickness;
            }
        });
        const currentCamera = this.getCamera();
        this.updateClippingPlanesForActors(currentCamera);
        this.triggerCameraModifiedEventIfNecessary(currentCamera, currentCamera);
        this.viewportProperties.slabThickness = slabThickness;
    }
    resetSlabThickness() {
        const actorEntries = this.getActors();
        actorEntries.forEach((actorEntry) => {
            if (actorIsA(actorEntry, 'vtkVolume')) {
                actorEntry.slabThickness = RENDERING_DEFAULTS.MINIMUM_SLAB_THICKNESS;
            }
        });
        const currentCamera = this.getCamera();
        this.updateClippingPlanesForActors(currentCamera);
        this.triggerCameraModifiedEventIfNecessary(currentCamera, currentCamera);
        this.viewportProperties.slabThickness = undefined;
    }
    getSliceViewInfo() {
        const { width: canvasWidth, height: canvasHeight } = this.getCanvas();
        const ijkOriginPoint = transformCanvasToIJK(this, [0, 0]);
        const ijkRowPoint = transformCanvasToIJK(this, [canvasWidth - 1, 0]);
        const ijkColPoint = transformCanvasToIJK(this, [0, canvasHeight - 1]);
        const ijkRowVec = vec3.sub(vec3.create(), ijkRowPoint, ijkOriginPoint);
        const ijkColVec = vec3.sub(vec3.create(), ijkColPoint, ijkOriginPoint);
        const ijkSliceVec = vec3.cross(vec3.create(), ijkRowVec, ijkColVec);
        vec3.normalize(ijkRowVec, ijkRowVec);
        vec3.normalize(ijkColVec, ijkColVec);
        vec3.normalize(ijkSliceVec, ijkSliceVec);
        const { dimensions } = this.getImageData();
        const [sx, sy, sz] = dimensions;
        const ijkCorners = [
            [0, 0, 0],
            [sx - 1, 0, 0],
            [0, sy - 1, 0],
            [sx - 1, sy - 1, 0],
            [0, 0, sz - 1],
            [sx - 1, 0, sz - 1],
            [0, sy - 1, sz - 1],
            [sx - 1, sy - 1, sz - 1],
        ];
        const canvasCorners = ijkCorners.map((ijkCorner) => transformIJKToCanvas(this, ijkCorner));
        const canvasAABB = canvasCorners.reduce((aabb, canvasPoint) => {
            aabb.minX = Math.min(aabb.minX, canvasPoint[0]);
            aabb.minY = Math.min(aabb.minY, canvasPoint[1]);
            aabb.maxX = Math.max(aabb.maxX, canvasPoint[0]);
            aabb.maxY = Math.max(aabb.maxY, canvasPoint[1]);
            return aabb;
        }, { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity });
        const ijkTopLeft = transformCanvasToIJK(this, [
            canvasAABB.minX,
            canvasAABB.minY,
        ]);
        const sliceToIndexMatrix = mat4.fromValues(ijkRowVec[0], ijkRowVec[1], ijkRowVec[2], 0, ijkColVec[0], ijkColVec[1], ijkColVec[2], 0, ijkSliceVec[0], ijkSliceVec[1], ijkSliceVec[2], 0, ijkTopLeft[0], ijkTopLeft[1], ijkTopLeft[2], 1);
        const ijkBottomRight = transformCanvasToIJK(this, [
            canvasAABB.maxX,
            canvasAABB.maxY,
        ]);
        const ijkDiagonal = vec3.sub(vec3.create(), ijkBottomRight, ijkTopLeft);
        const indexToSliceMatrix = mat4.invert(mat4.create(), sliceToIndexMatrix);
        const { viewPlaneNormal } = this.getCamera();
        const isOblique = viewPlaneNormal.filter((component) => Math.abs(component) > EPSILON)
            .length > 1;
        if (isOblique) {
            throw new Error('getSliceInfo is not supported for oblique views');
        }
        const sliceAxis = viewPlaneNormal.findIndex((component) => Math.abs(component) > 1 - EPSILON);
        if (sliceAxis === -1) {
            throw new Error('Unable to determine slice axis');
        }
        const sliceWidth = vec3.dot(ijkRowVec, ijkDiagonal) + 1;
        const sliceHeight = vec3.dot(ijkColVec, ijkDiagonal) + 1;
        return {
            sliceIndex: this.getSliceIndex(),
            width: sliceWidth,
            height: sliceHeight,
            slicePlane: sliceAxis,
            sliceToIndexMatrix,
            indexToSliceMatrix,
        };
    }
    getCurrentSlicePixelData() {
        const { voxelManager } = this.getImageData();
        const sliceData = voxelManager.getSliceData(this.getSliceViewInfo());
        return sliceData;
    }
    getViewReference(viewRefSpecifier = {}) {
        const viewRef = super.getViewReference(viewRefSpecifier);
        if (!viewRef?.volumeId) {
            return;
        }
        const volume = cache.getVolume(viewRef.volumeId);
        viewRef.referencedImageId = getClosestImageId(volume, viewRef.cameraFocalPoint, viewRef.viewPlaneNormal);
        return viewRef;
    }
    resetProperties(volumeId) {
        this._resetProperties(volumeId);
    }
    _resetProperties(volumeId) {
        const volumeActor = volumeId
            ? this.getActor(volumeId)
            : this.getDefaultActor();
        if (!volumeActor) {
            throw new Error(`No actor found for the given volumeId: ${volumeId}`);
        }
        if (volumeActor.slabThickness) {
            volumeActor.slabThickness = RENDERING_DEFAULTS.MINIMUM_SLAB_THICKNESS;
            this.viewportProperties.slabThickness = undefined;
            this.updateClippingPlanesForActors(this.getCamera());
        }
        volumeId ||= this.getVolumeId();
        const imageVolume = cache.getVolume(volumeId);
        if (!imageVolume) {
            throw new Error(`imageVolume with id: ${volumeId} does not exist in cache`);
        }
        setDefaultVolumeVOI(volumeActor.actor, imageVolume);
        if (isImageActor(volumeActor)) {
            const transferFunction = volumeActor.actor
                .getProperty()
                .getRGBTransferFunction(0);
            setTransferFunctionNodes(transferFunction, this.initialTransferFunctionNodes);
        }
        const eventDetails = {
            ...super.getVOIModifiedEventDetail(volumeId),
        };
        const resetPan = true;
        const resetZoom = true;
        const resetToCenter = true;
        const resetCameraRotation = true;
        this.resetCamera({
            resetPan,
            resetZoom,
            resetToCenter,
            resetCameraRotation,
        });
        triggerEvent(this.element, Events.VOI_MODIFIED, eventDetails);
    }
    getSlicesClippingPlanes() {
        const focalPoints = this.getSlicePlaneCoordinates();
        const { viewPlaneNormal } = this.getCamera();
        const slabThickness = RENDERING_DEFAULTS.MINIMUM_SLAB_THICKNESS;
        return focalPoints.map(({ point, sliceIndex }) => {
            const vtkPlanes = [vtkPlane.newInstance(), vtkPlane.newInstance()];
            this.setOrientationOfClippingPlanes(vtkPlanes, slabThickness, viewPlaneNormal, point);
            return {
                sliceIndex,
                planes: vtkPlanes.map((plane) => ({
                    normal: plane.getNormal(),
                    origin: plane.getOrigin(),
                })),
            };
        });
    }
}
export default VolumeViewport;
