/**
 * Copyright (c) 2025 3Dconnexion. All rights reserved.
 * License:
 *   This file in licensed under the terms of the '3Dconnexion
 *   Software Development Kit' license agreement found in the
 *   'LicenseAgreementSDK.txt' file.
 *   All rights not expressly granted by 3Dconnexion are reserved.
 *
 */

/**
 * Viewport
 *
 * The Viewport class owns the canvas-based view. It loads an image,
 * computes view scaling, and maintains view state (zoom, width / height).
 *
 * This class provides simple pan/roll/scale operations against the 2D
 * canvas transform and implements helper methods for converting canvas
 * coordinates to image coordinates.
 *
 * @example
 * const vp = new Viewport({ canvasId: 'theImage' });
 * vp.loadImage('Images/SpaceMouseWireless-Kit.jpg');
 *
 * @class
 */
class Viewport {
    #mousePosElement;
    #imagePosElement;
    #context;
    #image = null;
    #imageBounds = new DOMRect();
    #isDragging = false;

    constructor({ canvasId = "theImage" } = {}) {
        let canvas = document.getElementById(canvasId);
        if (!canvas) {
            throw new Error(`Canvas #${canvasId} not found`);
        }

        canvas.addEventListener("mouseleave", (e) => this.#onMouseLeave(e));
        canvas.addEventListener("mousemove", (e) => this.#onMouseMove(e));
        canvas.addEventListener("mouseup", (e) => this.#onMouseUp(e));
        canvas.addEventListener("mousedown", (e) => this.#onMouseDown(e));
        canvas.addEventListener("wheel", (e) => this.#onWheel(e), {
            passive: false,
        });

        this.#context = canvas.getContext("2d");
        // See individual pixels when zooming
        this.#context.imageSmoothingEnabled = false;

        this.#image = new Image();
        this.#image.onload = () => {
            this.#onImageLoaded();
        };

        // Used to display the mouse position in canvas and image coordinates.
        this.#mousePosElement = document.getElementById("canvas-pos");
        this.#imagePosElement = document.getElementById("image-pos");
    }

    /**
     * Viewport / canvas width in device pixels.
     * @returns {number}
     */
    get width() {
        return this.#context.canvas.width;
    }

    /**
     * Viewport / canvas height in device pixels.
     * @returns {number}
     */
    get height() {
        return this.#context.canvas.height;
    }

    /**
     * Returns the current 2D transform applied to the viewport (canvas context).
     *
     * The returned object is a DOMMatrix representing the currrent transform matrix
     * used for subsequent draw operations.
     *
     * @returns {DOMMatrix}
     */
    getTransform() {
        return this.#context.getTransform();
    }

    /**
     * Returns the bounds of the loaded image in image coordinates.
     *
     * The rectangle contains { x, y, width, height } corresponding to the
     * loaded image natural dimensions.
     *
     * @returns {DOMRect}
     */
    getImageBounds() {
        return this.#imageBounds;
    }

    /**
     * Translate the current view by the specified delta in image coordinates.
     *
     * @param {number} dx - Delta X in image coordinates.
     * @param {number} dy - Delta Y in image coordinates.
     * @returns {void}
     */
    pan(dx, dy) {
        console.log(`pan dx: ${dx}, dy: ${dy}`);

        this.#context.translate(dx, dy);
    }

    /**
     * Rotate the view about the specified center point.
     *
     * @param {number} angle - Rotation angle in radians.
     * @param {number} centerX - X coordinate of the rotation center in image coordinates.
     * @param {number} centerY - Y coordinate of the rotation center in image coordinates.
     * @returns {void}
     */
    roll(angle, centerX, centerY) {
        this.#context.translate(centerX, centerY);
        this.#context.rotate(angle);
        this.#context.translate(-centerX, -centerY);
    }

    /**
     * Scale the view uniformly about the specified center point.
     *
     * @param {number} factor - Scale factor (e.g. 1.1 to zoom in, 0.9 to zoom out).
     * @param {number} centerX - X coordinate of the scale center in image coordinates.
     * @param {number} centerY - Y coordinate of the scale center in image coordinates.
     * @returns {void}
     */
    scale(factor, centerX, centerY) {
        this.#context.translate(centerX, centerY);
        this.#context.scale(factor, factor);
        this.#context.translate(-centerX, -centerY);
    }

    /**
     * Fit the loaded image into the canvas (zoom to extents).
     *
     * The method computes an appropriate uniform scale and translation so that
     * the image fits the canvas while preserving aspect ratio, then resets the
     * canvas transform and applies the computed transform before issuing a draw.
     *
     * @returns {void}
     */
    zoomExtents() {
        const { naturalWidth: w, naturalHeight: h } = this.#image;
        const aspect = w / h;
        let zoom = 1;
        let x = 0,
            y = 0;
        const canvas = this.#context.canvas;
        if (canvas.height * aspect > canvas.width) {
            zoom = canvas.width / w;
            y = (canvas.height / zoom - h) / 2;
        } else {
            zoom = canvas.height / h;
            x = (canvas.width / zoom - w) / 2;
        }

        this.#context.resetTransform();
        this.#context.scale(zoom, zoom);
        this.#context.translate(x, y);

        this.draw();
    }

    /**
     * Draws the loaded image to the canvas using the current transform.
     *
     * This method clears the canvas preserving the current transform stack,
     * and then draws the image using the coordinates stored in {@link #imageBounds}.
     *
     * @returns {void}
     */
    draw() {
        this.#context.save();
        this.#context.setTransform(1, 0, 0, 1, 0, 0);
        this.#context.clearRect(0, 0, this.#context.canvas.width, this.#context.canvas.height);
        this.#context.restore();
        this.#context.drawImage(
            this.#image,
            this.#imageBounds.x,
            this.#imageBounds.y,
            this.#imageBounds.width,
            this.#imageBounds.height
        );
    }

    /**
     * Starts loading an image into the viewport.
     *
     * Sets the internal Image object's `src`, beginning an asynchronous load.
     * When the image finishes loading the private `#onImageLoaded()` handler
     * will compute view scaling and trigger an initial draw.
     *
     * @param {string} imageSrc - URL or path of the image to load.
     * @returns {void}
     * @example
     * // begin loading and let the viewport handle onload
     * viewport.loadImage('Images/SpaceMouseWireless-Kit.jpg');
     */
    loadImage(imageSrc) {
        this.#image.src = imageSrc;
        this.zoomExtents();
    }

    /**
     * Convert a canvas pixel coordinate into image coordinates using the current transform.
     *
     * @private
     * @param {number} x - X coordinate in canvas pixels.
     * @param {number} y - Y coordinate in canvas pixels.
     * @returns {DOMPoint} Point in image coordinates.
     */
    #getImagePoint(x, y) {
        const canvasPoint = new DOMPoint(x, y);
        return this.#context.getTransform().invertSelf().transformPoint(canvasPoint);
    }

    /**
     * Convert a canvas movement vector into an image-space vector using the current transform.
     *
     * @private
     * @param {number} x - Delta X in canvas pixels.
     * @param {number} y - Delta Y in canvas pixels.
     * @returns {DOMPoint} Vector in image coordinates.
     */
    #getImageVector(x, y) {
        const canvasVector = new DOMPoint(x, y, 0, 0);
        return this.#context.getTransform().invertSelf().transformPoint(canvasVector);
    }

    /**
     * Handles image load event and fits image to canvas.
     * @private
     * @sideEffects Updates view_scale, width, height, zoom, camera translation/rotation.
     * @remarks Triggers initial draw.
     */
    #onImageLoaded() {
        this.#imageBounds.width = this.#image.naturalWidth;
        this.#imageBounds.height = this.#image.naturalHeight;
        this.zoomExtents();
    }

    /**
     * Mouse down handler — starts dragging.
     * @private
     * @returns {void}
     */
    #onMouseDown() {
        this.#isDragging = true;
    }

    /**
     * Mouse move handler — updates displayed coordinates and performs drag panning when active.
     * @private
     * @param {MouseEvent} event - Mouse move event.
     * @returns {void}
     */
    #onMouseMove(event) {
        const imagePos = this.#getImagePoint(event.offsetX, event.offsetY);

        this.#mousePosElement.innerText = `Canvas X: ${event.offsetX}, Y: ${event.offsetY}`;
        this.#imagePosElement.innerText = `Image  X: ${imagePos.x}, Y: ${imagePos.y}`;

        if (this.#isDragging) {
            console.log(`Movement dx: ${event.movementX}, dy: ${event.movementY}`);
            const delta = this.#getImageVector(event.movementX, event.movementY);
            this.pan(delta.x, delta.y);
            this.draw();
        }
    }

    /**
     * Mouse leave handler — cancels drag.
     * @private
     * @returns {void}
     */
    #onMouseLeave() {
        console.log("onMouseLeave");
        this.#isDragging = false;
    }

    /**
     * Mouse up handler — cancels drag.
     * @private
     * @returns {void}
     */
    #onMouseUp() {
        this.#isDragging = false;
    }

    /**
     * Mouse wheel handler — performs zoom about the cursor position.
     * @private
     * @param {WheelEvent} event - Wheel event.
     * @returns {void}
     */
    #onWheel(event) {
        event.preventDefault();

        const factor = event.deltaY < 0 ? 1.1 : 0.9;
        const imagePos = this.#getImagePoint(event.offsetX, event.offsetY);

        this.#mousePosElement.innerText = `Canavs X: ${event.offsetX}, Y: ${event.offsetY}`;
        this.#imagePosElement.innerText = `Image  X: ${imagePos.x}, Y: ${imagePos.y}`;

        this.scale(factor, imagePos.x, imagePos.y);

        this.draw();
    }
}

export { Viewport };
