/**
 * Represents a loader for chart data and settings.
 */
class ChartLoader {
    /**
     * Creates an instance of ChartLoader.
     * @param {HTMLCanvasElement} canvas - The canvas element for rendering the chart.
     * @param {HTMLCanvasElement} effectCanvas - The canvas element for rendering visual effects.
     * @param {HTMLCanvasElement} detectionCanvas - The canvas element for detecting mouse interactions.
     * @param {HTMLElement} parent - The parent element that contains the chart.
     * @param {HTMLElement} legend - The legend element associated with the chart.
     * @param {HTMLElement} tooltip - The tooltip element used for displaying additional information.
     */
    constructor(canvas, effectCanvas, detectionCanvas, parent, legend, tooltip) {
        this.canvas = canvas
        this.effectCanvas = effectCanvas
        this.detectionCanvas = detectionCanvas
        this.parent = parent
        this.legend = legend
        this.tooltip = tooltip
        this.clickedShapeIndex = null
        this.selectedShapeIndex = null
    }

    /**  Function for loading chart data from the server
     * @param {String} id - The id used to identify the chart
     **/
    loadData(id) {
        // Fetch chart data from the server
        fetch("/api/charts/" + id, {
            method: 'GET',
            headers: {
                'Content-Type': 'json'
            }
        })
            // Parse the JSON response
            .then(response => response.json())
            // Process the retrieved data
            .then(data => {
                // Extract metadata and table data from the response
                let metadata = data.metadata
                //metadata.custom_x_values = ""
                let table = data.table

                // Draw the chart it contains data
                if (data !== null) {
                    this.drawChart(metadata, table)
                    this.parent.style.backgroundColor = metadata.backgroundColor
                }
                // Display an error message if chart data is not found
                else
                    this.parent.innerHTML = "graph not found."
            })
    }

    /**
     * Calculates the mode (most common value) in an array.
     * @param {Array} array - The array of values.
     * @returns {any} The mode of the array.
     */
    mode(array) {
        // Check if the array is empty
        if (array.length == 0)
            return null

        // Create a map to store the count of each element
        let modeMap = {}

        // Initialize variables to track the most common element and its count
        let maxEl = array[0], maxCount = 1

        // Loop through the array to count occurrences of each element
        for (let i = 0; i < array.length; i++) {
            let el = array[i]

            // Increment the count for the current element in the map
            if (modeMap[el] == null)
                modeMap[el] = 1
            else
                modeMap[el]++

            // Update the most common element and its count if needed
            if (modeMap[el] > maxCount) {
                maxEl = el
                maxCount = modeMap[el]
            }
        }

        // Return the most common element (mode)
        return maxEl
    }


    /**
     * Retrieves the index of the shape at a given position on the canvas.
     * @param {HTMLCanvasElement} canvas - The canvas element containing the shapes.
     * @param {Object} pos - The position on the canvas with x and y coordinates.
     * @returns {number} The index of the shape at the specified position.
     */
    getShapeIndex(canvas, pos) {
        let ctx = canvas.getContext('2d')

        // Get neigboring pixels
        let imageData = Array.from(ctx.getImageData(pos.x - 2, pos.y - 2, 5, 5).data)

        // Convert into indexes
        let indexes = []
        while (imageData.length > 0) {
            let pixel = imageData.splice(0, 4)

            // Only consider pixels with full opacity (alpha = 255)
            if (pixel[3] === 255) {
                let index = (pixel[0] * 256 + pixel[1]) * 256 + pixel[2]
                indexes.push(index)
            }
        }

        // To avoid edge cases because of anti aliasing
        let mostCommonIndex = this.mode(indexes)

        return mostCommonIndex
    }

    /**
     * Handles mouse movement events for interactivity.
     * @param {MouseEvent} e - The mouse event object.
     * @param {Chart} chart - The chart object associated with the mouse movement.
     */
    mouseMoveFunc(e, chart) {
        // Get mouse position
        const pos = {
            x: e.clientX,
            y: e.clientY
        }

        let obj = []
        // Add selected shape to array if it exists
        if (this.selectedShapeIndex !== null)
            obj.push(chart.objects[this.selectedShapeIndex])

        // Get index of hovered shape and add it to the array if it exists
        let hoverShapeIndex = this.getShapeIndex(this.detectionCanvas, pos)
        if (hoverShapeIndex !== null)
            obj.push(chart.objects[hoverShapeIndex])

        // Display tooltip and effect if a shape is selected
        if (obj[0] !== undefined) {
            // Get the drawing context for the effect canvas
            let effectCtx = this.effectCanvas.getContext("2d")
            // Clear the effect canvas
            effectCtx.clearRect(0, 0, effectCanvas.width, effectCanvas.height)
            // Show the effect
            this.effectCanvas.style.opacity = 1
            chart.drawEffect(effectCtx, obj)

            // Show tooltip
            this.tooltip.style.display = "block"
            // Set tooltip content
            let name = chart.data[obj[0].colId.toString()].col_name
            this.tooltip.innerHTML = "<b>" + name + "</b><br><p>" + obj[0].value + "</p>"

            let tooltipPos = obj[0].getTooltipPos()

            // Adjust tooltip position to fit within the chart div
            if (tooltipPos.right.x + this.tooltip.clientWidth <= this.parent.clientWidth - 2) {
                this.tooltip.className = "tooltip leftArrow"
                this.tooltip.style.left = tooltipPos.right.x + 5 + "px"
            }
            else {
                this.tooltip.className = "tooltip rightArrow"
                this.tooltip.style.left = tooltipPos.left.x - this.tooltip.clientWidth + "px"
            }

            // Check if the tooltip extends beyond the bottom edge of the chart div
            if (tooltipPos.right.y + this.tooltip.clientHeight <= parent.clientHeight - 2)
                this.tooltip.style.top = tooltipPos.right.y + "px"
            else
                this.tooltip.style.top = tooltipPos.right.y - this.tooltip.clientHeight / 2 + "px"
        }
        else {
            // Hide tooltip and effect if no shape is selected
            this.tooltip.style.display = "none"
            this.effectCanvas.style.opacity = 0
        }
    }

    touchHandler(event) {
        // Skip when zooming
        if (event.touches.length === 2)
            return

        var touches = event.changedTouches,
            first = touches[0],
            type = "";
        switch (event.type) {
            case "touchstart": type = "mousedown"; break;
            case "touchmove": type = "mousemove"; break;
            case "touchend": type = "mouseup"; break;
            default: return;
        }

        var simulatedEvent = document.createEvent("MouseEvent");
        simulatedEvent.initMouseEvent(type, true, true, window, 1,
            first.screenX, first.screenY,
            first.clientX, first.clientY, false,
            false, false, false, 0/*left*/, null);

        first.target.dispatchEvent(simulatedEvent);
        event.preventDefault();
    }

    mapTouchToMouseWheel(element) {
        let lastDist = 0;
        let touchStarted = false;

        element.addEventListener('touchstart', (event) => {
            if (event.touches.length === 2) {
                let x = event.touches[0].clientX + event.touches[1].clientX
                let y = event.touches[0].clientY + event.touches[1].clientY
                lastDist = Math.sqrt(x * x + y * y)
                touchStarted = true;
                e.stopPropagation()
                event.preventDefault();
            }
        }, { passive: false });

        element.addEventListener('touchmove', (event) => {
            if (touchStarted && event.touches.length === 2) {
                let x = event.touches[0].clientX + event.touches[1].clientX
                let y = event.touches[0].clientY + event.touches[1].clientY
                const curDist = Math.sqrt(x * x + y * y)
                const deltaY = lastDist - curDist

                const wheelEvent = new WheelEvent('wheel', {
                    deltaY: deltaY,
                    bubbles: true,
                    cancelable: true,
                    view: window
                });

                element.dispatchEvent(wheelEvent);

                lastTouchY = currentTouchY;
                e.stopPropagation()
                event.preventDefault();
            }
        }, { passive: false });

        element.addEventListener('touchend', (event) => {
            touchStarted = false;
        });
    }

    /** 
     * Adds event listeners to enable interactivity on the chart.
     * @param {Chart} chart - The chart object to which listeners will be added.
     */
    addListeners(chart) {

        document.addEventListener("touchstart", this.touchHandler, true);
        document.addEventListener("touchmove", this.touchHandler, true);
        document.addEventListener("touchend", this.touchHandler, true);
        document.addEventListener("touchcancel", this.touchHandler, true);

        this.mapTouchToMouseWheel(document);

        // Mousemove event listener for tracking mouse movement
        document.addEventListener("mousemove", (e) => {
            this.mouseMoveFunc(e, chart)

            if (this.panningEnabled === true) {
                this.chart.zoom.recalculate(e, true)

                // Update chart and redraw
                chart.updateBounds()
                chart.draw(false)
                this.addInteractivity()
            }
        })

        // Mousedown event listener for tracking mouse clicks
        document.addEventListener("mousedown", (e) => {
            const pos = {
                x: e.clientX,
                y: e.clientY
            }

            // Store the index of the latest clicked shape
            this.clickedShapeIndex = this.getShapeIndex(this.detectionCanvas, pos)

            this.chart.zoom.lastPos = pos
            this.panningEnabled = true
        })

        // Mouseup event listener for tracking mouse releases
        document.addEventListener("mouseup", (e) => {
            const pos = {
                x: e.clientX,
                y: e.clientY
            }

            // Determine if the selected shape has changed after mouse release
            if (this.clickedShapeIndex === null ||
                this.clickedShapeIndex !== this.getShapeIndex(this.detectionCanvas, pos))
                this.selectedShapeIndex = null
            else
                this.selectedShapeIndex = this.clickedShapeIndex

            // Perform mouse move action after mouse release to show the change immediately
            this.mouseMoveFunc(e, chart)

            this.panningEnabled = false
        })

        // Removes hover effects and tooltip when mouse leaves the chart
        document.addEventListener("mouseout", (e) => {
            if (this.selectedShapeIndex === null) {
                this.tooltip.style.display = "none"
            }
            else {
                let effectCtx = this.effectCanvas.getContext("2d")
                this.chart.drawEffect(effectCtx, [chart.objects[this.selectedShapeIndex]])
            }
        })

        // Window resize event listener for resizing the chart canvas
        window.addEventListener("resize", e => {
            // Resize the chart canvas and redraw the chart
            chart.resizeCanvas(this.parent, this.legend.offsetHeight)
            chart.draw(false)

            // Reapply interactivity
            this.addInteractivity()
        })

        // Wheel event listener for zooming the chart (if enabled)
        if (chart.settings.horizontalZoom || chart.settings.verticalZoom)
            window.addEventListener("wheel", e => {
                // Prevent default scrolling behavior
                e.stopPropagation()
                e.preventDefault()

                // Recalculate zoom based on the mouse wheel event
                chart.zoom.recalculate(e)

                // Update chart and redraw
                chart.updateBounds()
                chart.draw(false)
                this.addInteractivity()

                this.mouseMoveFunc(e, chart)
            },
                // Has to be active to stop default scroll behaviour
                { passive: false })
    }

    /* Asynchronously adds interactivity to the chart.
    */
    async addInteractivity() {
        setTimeout(() => {
            // Set dimensions of effect canvas
            this.effectCanvas.width = this.canvas.width
            this.effectCanvas.height = this.canvas.height
            // Set dimensions of detection canvas
            this.detectionCanvas.width = this.canvas.width
            this.detectionCanvas.height = this.canvas.height
            // Draw detection map on the detection canvas
            this.chart.drawDetectionMap(this.detectionCanvas.getContext("2d"))
        }, 0)
    }

    /**
     * Draws the chart based on the provided settings and data.
     * @param {Object} chartSettings - The settings for the chart.
     * @param {Array} data - The data to visualize on the chart.
     */
    drawChart(chartSettings, data) {
        let zoomManager = new ZoomManager(chartSettings.horizontalZoom, chartSettings.verticalZoom)

        // Choose the correct type of chart
        switch (chartSettings.type) {
            case "point":
                this.chart = new PointChart(this.canvas, data, chartSettings, zoomManager)
                break
            case "line":
                this.chart = new LineChart(this.canvas, data, chartSettings, zoomManager)
                break
            case "smoothline":
                this.chart = new SmoothLineChart(this.canvas, data, chartSettings, zoomManager)
                break
            case "pie":
                this.chart = new PieChart(this.canvas, data, chartSettings, zoomManager)
                break
            case "donut":
                this.chart = new DonutChart(this.canvas, data, chartSettings, zoomManager)
                break
            case "bar":
                this.chart = new BarChart(this.canvas, data, chartSettings, zoomManager)
                break
            case "area":
                this.chart = new AreaChart(this.canvas, data, chartSettings, zoomManager)
                break
            case "smootharea":
                this.chart = new SmoothAreaChart(this.canvas, data, chartSettings, zoomManager)
                break
            case "stacked":
                this.chart = new StackedChart(this.canvas, data, chartSettings, zoomManager)
                break
        }

        // Asynchronously update the legend
        setTimeout(this.chart.updateLegend(chartSettings.displayLegend, this.legend, this), 0)

        // Resize the canvas and chart based on the parent container and legend size
        this.chart.resizeCanvas(this.parent, this.legend.offsetHeight)

        this.chart.draw()

        this.addInteractivity()
        this.addListeners(this.chart)
    }
}
