<script lang="ts">
    import ButtonWithTooltip from '$lib/components/ButtonWithTooltip.svelte';
    import * as Popover from '$lib/components/ui/popover/index.js';
    import * as ToggleGroup from '$lib/components/ui/toggle-group/index.js';
    import Separator from '$lib/components/ui/separator/separator.svelte';
    import Chart from 'chart.js/auto';
    import mapboxgl from 'mapbox-gl';
    import { onDestroy, onMount } from 'svelte';
    import {
        BrickWall,
        TriangleRight,
        HeartPulse,
        Orbit,
        SquareActivity,
        Thermometer,
        Zap,
        Circle,
        Check,
        ChartNoAxesColumn,
        Construction,
    } from '@lucide/svelte';
    import { getSlopeColor, getSurfaceColor, getHighwayColor } from '$lib/assets/colors';
    import {
        getCadenceWithUnits,
        getConvertedDistance,
        getConvertedElevation,
        getConvertedTemperature,
        getConvertedVelocity,
        getDistanceUnits,
        getDistanceWithUnits,
        getElevationWithUnits,
        getHeartRateWithUnits,
        getPowerWithUnits,
        getTemperatureWithUnits,
        getVelocityWithUnits,
    } from '$lib/units';
    import type { Readable, Writable } from 'svelte/store';
    import type { GPXStatistics } from 'gpx';
    import { mode } from 'mode-watcher';
    import { settings } from '$lib/logic/settings';
    import { map } from '$lib/components/map/map';
    import { i18n } from '$lib/i18n.svelte';

    const { distanceUnits, velocityUnits, temperatureUnits } = settings;

    let {
        gpxStatistics,
        slicedGPXStatistics,
        additionalDatasets = $bindable(),
        elevationFill = $bindable(),
        showControls = true,
    }: {
        gpxStatistics: Readable<GPXStatistics>;
        slicedGPXStatistics: Writable<[GPXStatistics, number, number] | undefined>;
        additionalDatasets: string[];
        elevationFill: 'slope' | 'surface' | 'highway' | undefined;
        showControls?: boolean;
    } = $props();

    let canvas: HTMLCanvasElement;
    let overlay: HTMLCanvasElement;
    let chart: Chart;

    Chart.defaults.font.family =
        'ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'; // Tailwind CSS font

    let marker: mapboxgl.Marker | null = null;
    let dragging = false;
    let panning = false;

    let options = {
        animation: false,
        parsing: false,
        maintainAspectRatio: false,
        scales: {
            x: {
                type: 'linear',
                ticks: {
                    callback: function (value: number) {
                        return `${value.toFixed(1).replace(/\.0+$/, '')} ${getDistanceUnits()}`;
                    },
                    align: 'inner',
                    maxRotation: 0,
                },
            },
            y: {
                type: 'linear',
                ticks: {
                    callback: function (value: number) {
                        return getElevationWithUnits(value, false);
                    },
                },
            },
        },
        datasets: {
            line: {
                pointRadius: 0,
                tension: 0.4,
                borderWidth: 2,
                cubicInterpolationMode: 'monotone',
            },
        },
        interaction: {
            mode: 'nearest',
            axis: 'x',
            intersect: false,
        },
        plugins: {
            legend: {
                display: false,
            },
            decimation: {
                enabled: true,
            },
            tooltip: {
                enabled: () => !dragging && !panning,
                callbacks: {
                    title: function () {
                        return '';
                    },
                    label: function (context: Chart.TooltipContext) {
                        let point = context.raw;
                        if (context.datasetIndex === 0) {
                            if ($map && marker) {
                                if (dragging) {
                                    marker.remove();
                                } else {
                                    marker.setLngLat(point.coordinates);
                                    marker.addTo($map);
                                }
                            }
                            return `${i18n._('quantities.elevation')}: ${getElevationWithUnits(point.y, false)}`;
                        } else if (context.datasetIndex === 1) {
                            return `${$velocityUnits === 'speed' ? i18n._('quantities.speed') : i18n._('quantities.pace')}: ${getVelocityWithUnits(point.y, false)}`;
                        } else if (context.datasetIndex === 2) {
                            return `${i18n._('quantities.heartrate')}: ${getHeartRateWithUnits(point.y)}`;
                        } else if (context.datasetIndex === 3) {
                            return `${i18n._('quantities.cadence')}: ${getCadenceWithUnits(point.y)}`;
                        } else if (context.datasetIndex === 4) {
                            return `${i18n._('quantities.temperature')}: ${getTemperatureWithUnits(point.y, false)}`;
                        } else if (context.datasetIndex === 5) {
                            return `${i18n._('quantities.power')}: ${getPowerWithUnits(point.y)}`;
                        }
                    },
                    afterBody: function (contexts: Chart.TooltipContext[]) {
                        let context = contexts.filter((context) => context.datasetIndex === 0);
                        if (context.length === 0) return;
                        let point = context[0].raw;
                        let slope = {
                            at: point.slope.at.toFixed(1),
                            segment: point.slope.segment.toFixed(1),
                            length: getDistanceWithUnits(point.slope.length),
                        };
                        let surface = point.extensions.surface
                            ? point.extensions.surface
                            : 'unknown';
                        let highway = point.extensions.highway
                            ? point.extensions.highway
                            : 'unknown';
                        let sacScale = point.extensions.sac_scale;
                        let mtbScale = point.extensions.mtb_scale;

                        let labels = [
                            `    ${i18n._('quantities.distance')}: ${getDistanceWithUnits(point.x, false)}`,
                            `    ${i18n._('quantities.slope')}: ${slope.at} %${elevationFill === 'slope' ? ` (${slope.length} @${slope.segment} %)` : ''}`,
                        ];

                        if (elevationFill === 'surface') {
                            labels.push(
                                `    ${i18n._('quantities.surface')}: ${i18n._(`toolbar.routing.surface.${surface}`)}`
                            );
                        }

                        if (elevationFill === 'highway') {
                            labels.push(
                                `    ${i18n._('quantities.highway')}: ${i18n._(`toolbar.routing.highway.${highway}`)}${
                                    sacScale
                                        ? ` (${i18n._(`toolbar.routing.sac_scale.${sacScale}`)})`
                                        : ''
                                }`
                            );
                            if (mtbScale) {
                                labels.push(
                                    `    ${i18n._('toolbar.routing.mtb_scale')}: ${mtbScale}`
                                );
                            }
                        }

                        if (point.time) {
                            labels.push(
                                `    ${i18n._('quantities.time')}: ${i18n.df.format(point.time)}`
                            );
                        }

                        return labels;
                    },
                },
            },
            zoom: {
                pan: {
                    enabled: true,
                    mode: 'x',
                    modifierKey: 'shift',
                    onPanStart: function () {
                        // hide tooltip
                        panning = true;
                        $slicedGPXStatistics = undefined;
                    },
                    onPanComplete: function () {
                        panning = false;
                    },
                },
                zoom: {
                    wheel: {
                        enabled: true,
                    },
                    mode: 'x',
                    onZoomStart: function ({ chart, event }: { chart: Chart; event: any }) {
                        if (
                            event.deltaY < 0 &&
                            Math.abs(
                                chart.getInitialScaleBounds().x.max /
                                    chart.options.plugins.zoom.limits.x.minRange -
                                    chart.getZoomLevel()
                            ) < 0.01
                        ) {
                            // Disable wheel pan if zoomed in to the max, and zooming in
                            return false;
                        }

                        $slicedGPXStatistics = undefined;
                    },
                },
                limits: {
                    x: {
                        min: 'original',
                        max: 'original',
                        minRange: 1,
                    },
                },
            },
        },
        stacked: false,
        onResize: function () {
            updateOverlay();
        },
    };

    let datasets: string[] = ['speed', 'hr', 'cad', 'atemp', 'power'];
    datasets.forEach((id) => {
        options.scales[`y${id}`] = {
            type: 'linear',
            position: 'right',
            grid: {
                display: false,
            },
            reverse: () => id === 'speed' && $velocityUnits === 'pace',
            display: false,
        };
    });

    onMount(async () => {
        Chart.register((await import('chartjs-plugin-zoom')).default); // dynamic import to avoid SSR and 'window is not defined' error

        chart = new Chart(canvas, {
            type: 'line',
            data: {
                datasets: [],
            },
            options,
            plugins: [
                {
                    id: 'toggleMarker',
                    events: ['mouseout'],
                    afterEvent: function (chart: Chart, args: { event: Chart.ChartEvent }) {
                        if (args.event.type === 'mouseout') {
                            if ($map && marker) {
                                marker.remove();
                            }
                        }
                    },
                },
            ],
        });

        // Map marker to show on hover
        let element = document.createElement('div');
        element.className = 'h-4 w-4 rounded-full bg-cyan-500 border-2 border-white';
        marker = new mapboxgl.Marker({
            element,
        });

        let startIndex = 0;
        let endIndex = 0;
        function getIndex(evt) {
            const points = chart.getElementsAtEventForMode(
                evt,
                'x',
                {
                    intersect: false,
                },
                true
            );

            if (points.length === 0) {
                const rect = canvas.getBoundingClientRect();
                if (evt.x - rect.left <= chart.chartArea.left) {
                    return 0;
                } else if (evt.x - rect.left >= chart.chartArea.right) {
                    return $gpxStatistics.local.points.length - 1;
                } else {
                    return undefined;
                }
            }

            let point = points.find((point) => point.element.raw);
            if (point) {
                return point.element.raw.index;
            } else {
                return points[0].index;
            }
        }

        let dragStarted = false;
        function onMouseDown(evt) {
            if (evt.shiftKey) {
                // Panning interaction
                return;
            }
            dragStarted = true;
            canvas.style.cursor = 'col-resize';
            startIndex = getIndex(evt);
        }
        function onMouseMove(evt) {
            if (dragStarted) {
                dragging = true;
                endIndex = getIndex(evt);
                if (endIndex !== undefined) {
                    if (startIndex === undefined) {
                        startIndex = endIndex;
                    } else if (startIndex !== endIndex) {
                        $slicedGPXStatistics = [
                            $gpxStatistics.slice(
                                Math.min(startIndex, endIndex),
                                Math.max(startIndex, endIndex)
                            ),
                            Math.min(startIndex, endIndex),
                            Math.max(startIndex, endIndex),
                        ];
                    }
                }
            }
        }
        function onMouseUp(evt) {
            dragStarted = false;
            dragging = false;
            canvas.style.cursor = '';
            endIndex = getIndex(evt);
            if (startIndex === endIndex) {
                $slicedGPXStatistics = undefined;
            }
        }
        canvas.addEventListener('pointerdown', onMouseDown);
        canvas.addEventListener('pointermove', onMouseMove);
        canvas.addEventListener('pointerup', onMouseUp);
    });

    $effect(() => {
        let data = $gpxStatistics;
        if (chart && $distanceUnits && $velocityUnits && $temperatureUnits) {
            // update data
            chart.data.datasets[0] = {
                label: i18n._('quantities.elevation'),
                data: data.local.points.map((point, index) => {
                    return {
                        x: getConvertedDistance(data.local.distance.total[index]),
                        y: point.ele ? getConvertedElevation(point.ele) : 0,
                        time: point.time,
                        slope: {
                            at: data.local.slope.at[index],
                            segment: data.local.slope.segment[index],
                            length: data.local.slope.length[index],
                        },
                        extensions: point.getExtensions(),
                        coordinates: point.getCoordinates(),
                        index: index,
                    };
                }),
                normalized: true,
                fill: 'start',
                order: 1,
            };
            chart.data.datasets[1] = {
                data: data.local.points.map((point, index) => {
                    return {
                        x: getConvertedDistance(data.local.distance.total[index]),
                        y: getConvertedVelocity(data.local.speed[index]),
                        index: index,
                    };
                }),
                normalized: true,
                yAxisID: 'yspeed',
                hidden: true,
            };
            chart.data.datasets[2] = {
                data: data.local.points.map((point, index) => {
                    return {
                        x: getConvertedDistance(data.local.distance.total[index]),
                        y: point.getHeartRate(),
                        index: index,
                    };
                }),
                normalized: true,
                yAxisID: 'yhr',
                hidden: true,
            };
            chart.data.datasets[3] = {
                data: data.local.points.map((point, index) => {
                    return {
                        x: getConvertedDistance(data.local.distance.total[index]),
                        y: point.getCadence(),
                        index: index,
                    };
                }),
                normalized: true,
                yAxisID: 'ycad',
                hidden: true,
            };
            chart.data.datasets[4] = {
                data: data.local.points.map((point, index) => {
                    return {
                        x: getConvertedDistance(data.local.distance.total[index]),
                        y: getConvertedTemperature(point.getTemperature()),
                        index: index,
                    };
                }),
                normalized: true,
                yAxisID: 'yatemp',
                hidden: true,
            };
            chart.data.datasets[5] = {
                data: data.local.points.map((point, index) => {
                    return {
                        x: getConvertedDistance(data.local.distance.total[index]),
                        y: point.getPower(),
                        index: index,
                    };
                }),
                normalized: true,
                yAxisID: 'ypower',
                hidden: true,
            };
            chart.options.scales.x['min'] = 0;
            chart.options.scales.x['max'] = getConvertedDistance(data.global.distance.total);

            chart.update();
        }
    });

    function slopeFillCallback(context) {
        return getSlopeColor(context.p0.raw.slope.segment);
    }

    function surfaceFillCallback(context) {
        return getSurfaceColor(context.p0.raw.extensions.surface);
    }

    function highwayFillCallback(context) {
        return getHighwayColor(
            context.p0.raw.extensions.highway,
            context.p0.raw.extensions.sac_scale,
            context.p0.raw.extensions.mtb_scale
        );
    }

    $effect(() => {
        if (elevationFill && chart) {
            if (elevationFill === 'slope') {
                chart.data.datasets[0]['segment'] = {
                    backgroundColor: slopeFillCallback,
                };
            } else if (elevationFill === 'surface') {
                chart.data.datasets[0]['segment'] = {
                    backgroundColor: surfaceFillCallback,
                };
            } else if (elevationFill === 'highway') {
                chart.data.datasets[0]['segment'] = {
                    backgroundColor: highwayFillCallback,
                };
            } else {
                chart.data.datasets[0]['segment'] = {};
            }
            chart.update();
        }
    });

    $effect(() => {
        if (additionalDatasets && chart) {
            let includeSpeed = additionalDatasets.includes('speed');
            let includeHeartRate = additionalDatasets.includes('hr');
            let includeCadence = additionalDatasets.includes('cad');
            let includeTemperature = additionalDatasets.includes('atemp');
            let includePower = additionalDatasets.includes('power');
            if (chart.data.datasets.length > 0) {
                chart.data.datasets[1].hidden = !includeSpeed;
                chart.data.datasets[2].hidden = !includeHeartRate;
                chart.data.datasets[3].hidden = !includeCadence;
                chart.data.datasets[4].hidden = !includeTemperature;
                chart.data.datasets[5].hidden = !includePower;
            }
            chart.update();
        }
    });

    function updateOverlay() {
        if (!canvas) {
            return;
        }

        overlay.width = canvas.width / window.devicePixelRatio;
        overlay.height = canvas.height / window.devicePixelRatio;
        overlay.style.width = `${overlay.width}px`;
        overlay.style.height = `${overlay.height}px`;

        if ($slicedGPXStatistics) {
            let startIndex = $slicedGPXStatistics[1];
            let endIndex = $slicedGPXStatistics[2];

            // Draw selection rectangle
            let selectionContext = overlay.getContext('2d');
            if (selectionContext) {
                selectionContext.fillStyle = mode.current === 'dark' ? 'white' : 'black';
                selectionContext.globalAlpha = mode.current === 'dark' ? 0.2 : 0.1;
                selectionContext.clearRect(0, 0, overlay.width, overlay.height);

                let startPixel = chart.scales.x.getPixelForValue(
                    getConvertedDistance($gpxStatistics.local.distance.total[startIndex])
                );
                let endPixel = chart.scales.x.getPixelForValue(
                    getConvertedDistance($gpxStatistics.local.distance.total[endIndex])
                );

                selectionContext.fillRect(
                    startPixel,
                    chart.chartArea.top,
                    endPixel - startPixel,
                    chart.chartArea.height
                );
            }
        } else if (overlay) {
            let selectionContext = overlay.getContext('2d');
            if (selectionContext) {
                selectionContext.clearRect(0, 0, overlay.width, overlay.height);
            }
        }
    }

    $effect(() => {
        if ($slicedGPXStatistics || mode.current) {
            updateOverlay();
        }
    });

    onDestroy(() => {
        if (chart) {
            chart.destroy();
        }
    });
</script>

<div class="h-full grow min-w-0 relative py-2">
    <canvas bind:this={overlay} class="w-full h-full absolute pointer-events-none"></canvas>
    <canvas bind:this={canvas} class="w-full h-full absolute"></canvas>
    {#if showControls}
        <div class="absolute bottom-10 right-1.5">
            <Popover.Root>
                <Popover.Trigger>
                    <ButtonWithTooltip
                        label={i18n._('chart.settings')}
                        variant="outline"
                        side="left"
                        class="w-7 h-7 p-0 flex justify-center opacity-70 hover:opacity-100 transition-opacity duration-300 hover:bg-background"
                    >
                        <ChartNoAxesColumn size="18" />
                    </ButtonWithTooltip>
                </Popover.Trigger>
                <Popover.Content
                    class="w-fit p-0 flex flex-col"
                    side="top"
                    align="end"
                    sideOffset={-32}
                >
                    <ToggleGroup.Root
                        class="flex flex-col items-start gap-0 p-1 w-full border-none"
                        type="single"
                        bind:value={elevationFill}
                    >
                        <ToggleGroup.Item
                            class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
                            value="slope"
                        >
                            <div class="w-6 flex justify-center items-center">
                                {#if elevationFill === 'slope'}
                                    <Circle class="size-1.5 fill-current text-current" />
                                {/if}
                            </div>
                            <TriangleRight size="15" />
                            {i18n._('quantities.slope')}
                        </ToggleGroup.Item>
                        <ToggleGroup.Item
                            class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
                            value="surface"
                            variant="outline"
                        >
                            <div class="w-6 flex justify-center items-center">
                                {#if elevationFill === 'surface'}
                                    <Circle class="size-1.5 fill-current text-current" />
                                {/if}
                            </div>
                            <BrickWall size="15" />
                            {i18n._('quantities.surface')}
                        </ToggleGroup.Item>
                        <ToggleGroup.Item
                            class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
                            value="highway"
                            variant="outline"
                        >
                            <div class="w-6 flex justify-center items-center">
                                {#if elevationFill === 'highway'}
                                    <Circle class="size-1.5 fill-current text-current" />
                                {/if}
                            </div>
                            <Construction size="15" />
                            {i18n._('quantities.highway')}
                        </ToggleGroup.Item>
                    </ToggleGroup.Root>
                    <Separator />
                    <ToggleGroup.Root
                        class="flex flex-col items-start gap-0 p-1"
                        type="multiple"
                        bind:value={additionalDatasets}
                    >
                        <ToggleGroup.Item
                            class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
                            value="speed"
                        >
                            <div class="w-6 flex justify-center items-center">
                                {#if additionalDatasets.includes('speed')}
                                    <Check size="14" />
                                {/if}
                            </div>
                            <Zap size="15" />
                            {$velocityUnits === 'speed'
                                ? i18n._('quantities.speed')
                                : i18n._('quantities.pace')}
                        </ToggleGroup.Item>
                        <ToggleGroup.Item
                            class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
                            value="hr"
                        >
                            <div class="w-6 flex justify-center items-center">
                                {#if additionalDatasets.includes('hr')}
                                    <Check size="14" />
                                {/if}
                            </div>
                            <HeartPulse size="15" />
                            {i18n._('quantities.heartrate')}
                        </ToggleGroup.Item>
                        <ToggleGroup.Item
                            class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
                            value="cad"
                        >
                            <div class="w-6 flex justify-center items-center">
                                {#if additionalDatasets.includes('cad')}
                                    <Check size="14" />
                                {/if}
                            </div>
                            <Orbit size="15" />
                            {i18n._('quantities.cadence')}
                        </ToggleGroup.Item>
                        <ToggleGroup.Item
                            class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
                            value="atemp"
                        >
                            <div class="w-6 flex justify-center items-center">
                                {#if additionalDatasets.includes('atemp')}
                                    <Check size="14" />
                                {/if}
                            </div>
                            <Thermometer size="15" />
                            {i18n._('quantities.temperature')}
                        </ToggleGroup.Item>
                        <ToggleGroup.Item
                            class="p-0 pr-1.5 h-6 w-full gap-1.5 rounded flex justify-start data-[state=on]:bg-background data-[state=on]:hover:bg-accent hover:bg-accent hover:text-foreground"
                            value="power"
                        >
                            <div class="w-6 flex justify-center items-center">
                                {#if additionalDatasets.includes('power')}
                                    <Check size="14" />
                                {/if}
                            </div>
                            <SquareActivity size="15" />
                            {i18n._('quantities.power')}
                        </ToggleGroup.Item>
                    </ToggleGroup.Root>
                </Popover.Content>
            </Popover.Root>
        </div>
    {/if}
</div>
