<!--
  - CoolerControl - monitor and control your cooling and other devices
  - Copyright (c) 2021-2025  Guy Boldon and contributors
  -
  - This program is free software: you can redistribute it and/or modify
  - it under the terms of the GNU General Public License as published by
  - the Free Software Foundation, either version 3 of the License, or
  - (at your option) any later version.
  -
  - This program is distributed in the hope that it will be useful,
  - but WITHOUT ANY WARRANTY; without even the implied warranty of
  - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  - GNU General Public License for more details.
  -
  - You should have received a copy of the GNU General Public License
  - along with this program.  If not, see <https://www.gnu.org/licenses/>.
  -->

<script setup lang="ts">
import { useDeviceStore } from '@/stores/DeviceStore'
import { useSettingsStore } from '@/stores/SettingsStore'
import { onMounted, onUnmounted } from 'vue'
import { Device, UID } from '@/models/Device'
import uPlot from 'uplot'
import { useThemeColorsStore } from '@/stores/ThemeColorsStore'
import {
    columnHighlightPlugin,
    DeviceLineProperties,
    mouseWheelZoomPlugin,
    SCALE_KEY_PERCENT,
    SCALE_KEY_RPM,
    SCALE_KEY_WATTS,
    tooltipPlugin,
} from '@/components/u-plot-plugins.ts'
import { Dashboard, DataType } from '@/models/Dashboard.ts'
import { useI18n } from 'vue-i18n'

const deviceStore = useDeviceStore()
const settingsStore = useSettingsStore()
const { t } = useI18n()
// const yCrosshair = computed(
//     () =>
//         `${settingsStore.systemOverviewOptions.timeChartLineScale}px solid color-mix(in srgb, var(--primary-color) 30%, transparent)`,
// )
const colors = useThemeColorsStore()
const uSeriesData: uPlot.AlignedData = []
const uLineNames: Array<string> = []

interface Props {
    dashboard: Dashboard
}

const props = defineProps<Props>()
const includesTemps: boolean =
    props.dashboard.dataTypes.length === 0 || props.dashboard.dataTypes.includes(DataType.TEMP)
const includedDuties: boolean =
    props.dashboard.dataTypes.length === 0 || props.dashboard.dataTypes.includes(DataType.DUTY)
const includesLoads: boolean =
    props.dashboard.dataTypes.length === 0 || props.dashboard.dataTypes.includes(DataType.LOAD)
const includesFreqs: boolean =
    props.dashboard.dataTypes.length === 0 || props.dashboard.dataTypes.includes(DataType.FREQ)
const includesRPMs: boolean =
    props.dashboard.dataTypes.length === 0 || props.dashboard.dataTypes.includes(DataType.RPM)
const includesWatts: boolean =
    props.dashboard.dataTypes.length === 0 || props.dashboard.dataTypes.includes(DataType.WATTS)
const includesDevice = (deviceUID: UID): boolean =>
    props.dashboard.deviceChannelNames.length === 0 ||
    props.dashboard.deviceChannelNames.some(
        (deviceChannel) => deviceChannel.deviceUID === deviceUID,
    )
const includesDeviceChannel = (deviceUID: UID, channelName: string): boolean =>
    props.dashboard.deviceChannelNames.length === 0 ||
    props.dashboard.deviceChannelNames.some(
        (deviceChannel) =>
            deviceChannel.deviceUID === deviceUID && deviceChannel.channelName === channelName,
    )
const timeRangeSeconds = props.dashboard.timeRangeSeconds

const allDevicesLineProperties = new Map<string, DeviceLineProperties>()

/**
 * Line Names should be unique for our Series Data.
 * @param device
 * @param statusName
 */
const createLineName = (device: Device, statusName: string): string =>
    `${device.type}_${device.type_index}_${statusName}`

/**
 * Converts our internal Device objects and statuses into the format required by uPlot
 */
const initUSeriesData = () => {
    uSeriesData.length = 0
    uLineNames.length = 0

    const firstDevice: Device = deviceStore.allDevices().next().value
    const currentStatusLength = timeRangeSeconds / settingsStore.ccSettings.poll_rate
    const uTimeData = new Float64Array(currentStatusLength)
    for (const [statusIndex, status] of firstDevice.status_history
        .slice(-currentStatusLength)
        .entries()) {
        uTimeData[statusIndex] = new Date(status.timestamp).getTime() / 1000 // Status' Unix timestamp
    }

    // We need to use decimal values for at least temps, so Float32.
    // TypedArrays have a fixed length, so we need to manage this ourselves
    const uLineData = new Map<string, Float32Array>()

    for (const device of deviceStore.allDevices()) {
        if (!includesDevice(device.uid)) continue
        const deviceSettings = settingsStore.allUIDeviceSettings.get(device.uid)!
        for (const [statusIndex, status] of device.status_history
            .slice(-currentStatusLength)
            .entries()) {
            for (const tempStatus of status.temps) {
                if (!includesTemps) break
                if (!includesDeviceChannel(device.uid, tempStatus.name)) continue
                const tempSettings = deviceSettings.sensorsAndChannels.get(tempStatus.name)!
                const lineName = createLineName(device, tempStatus.name + '_temp')
                if (!uLineNames.includes(lineName)) {
                    uLineNames.push(lineName)
                }
                if (!allDevicesLineProperties.has(lineName)) {
                    allDevicesLineProperties.set(lineName, {
                        color: tempSettings.color,
                        name: tempSettings.name,
                    })
                }
                let floatArray = uLineData.get(lineName)
                if (floatArray == null) {
                    floatArray = new Float32Array(currentStatusLength)
                    uLineData.set(lineName, floatArray)
                }
                floatArray[statusIndex] = tempStatus.temp
            }
            for (const channelStatus of status.channels) {
                if (!includesDeviceChannel(device.uid, channelStatus.name)) continue
                if (channelStatus.duty != null) {
                    const isLoadChannel = includesLoads && channelStatus.name.endsWith('Load')
                    const isFanDutyChannel = includedDuties && !channelStatus.name.endsWith('Load')
                    if (isLoadChannel || isFanDutyChannel) {
                        const channelSettings = deviceSettings.sensorsAndChannels.get(
                            channelStatus.name,
                        )!
                        const lineNameExt: string = isLoadChannel ? '_load' : '_duty'
                        const lineName = createLineName(device, channelStatus.name + lineNameExt)
                        if (!uLineNames.includes(lineName)) {
                            uLineNames.push(lineName)
                        }
                        if (!allDevicesLineProperties.has(lineName)) {
                            allDevicesLineProperties.set(lineName, {
                                color: channelSettings.color,
                                name: channelSettings.name,
                            })
                        }
                        let floatArray = uLineData.get(lineName)
                        if (floatArray == null) {
                            floatArray = new Float32Array(currentStatusLength)
                            uLineData.set(lineName, floatArray)
                        }
                        floatArray[statusIndex] = channelStatus.duty
                    }
                }
                if (includesRPMs && channelStatus.rpm != null) {
                    const channelSettings = deviceSettings.sensorsAndChannels.get(
                        channelStatus.name,
                    )!
                    const lineName = createLineName(device, channelStatus.name + '_rpm')
                    if (!uLineNames.includes(lineName)) {
                        uLineNames.push(lineName)
                    }
                    if (!allDevicesLineProperties.has(lineName)) {
                        allDevicesLineProperties.set(lineName, {
                            color: channelSettings.color,
                            name: channelSettings.name,
                        })
                    }
                    let floatArray = uLineData.get(lineName)
                    if (floatArray == null) {
                        floatArray = new Float32Array(currentStatusLength)
                        uLineData.set(lineName, floatArray)
                    }
                    floatArray[statusIndex] = channelStatus.rpm / settingsStore.frequencyPrecision
                }
                if (includesFreqs && channelStatus.freq != null) {
                    const channelSettings = deviceSettings.sensorsAndChannels.get(
                        channelStatus.name,
                    )!
                    const lineName = createLineName(device, channelStatus.name + '_freq')
                    if (!uLineNames.includes(lineName)) {
                        uLineNames.push(lineName)
                    }
                    if (!allDevicesLineProperties.has(lineName)) {
                        allDevicesLineProperties.set(lineName, {
                            color: channelSettings.color,
                            name: channelSettings.name,
                        })
                    }
                    let floatArray = uLineData.get(lineName)
                    if (floatArray == null) {
                        floatArray = new Float32Array(currentStatusLength)
                        uLineData.set(lineName, floatArray)
                    }
                    floatArray[statusIndex] = channelStatus.freq / settingsStore.frequencyPrecision
                }
                if (includesWatts && channelStatus.watts != null) {
                    const channelSettings = deviceSettings.sensorsAndChannels.get(
                        channelStatus.name,
                    )!
                    const lineName = createLineName(device, channelStatus.name + '_watts')
                    if (!uLineNames.includes(lineName)) {
                        uLineNames.push(lineName)
                    }
                    if (!allDevicesLineProperties.has(lineName)) {
                        allDevicesLineProperties.set(lineName, {
                            color: channelSettings.color,
                            name: channelSettings.name,
                        })
                    }
                    let floatArray = uLineData.get(lineName)
                    if (floatArray == null) {
                        floatArray = new Float32Array(currentStatusLength)
                        uLineData.set(lineName, floatArray)
                    }
                    floatArray[statusIndex] = channelStatus.watts
                }
            }
        }
    }

    for (const lineName of uLineNames) {
        // the uLineNames Array keeps our LineData arrays in order
        uSeriesData.push(uLineData.get(lineName)!)
    }
    uSeriesData.splice(0, 0, uTimeData) // 'inserts' time values as the first array, where uPlot expects it
    console.debug('Initialized uPlot Series Data')
}

const shiftSeriesData = (shiftLength: number) => {
    for (const arr of uSeriesData) {
        for (let i = 0; i < arr.length - shiftLength; i++) {
            arr[i] = arr[i + shiftLength] // Shift left
        }
    }
}

const updateUSeriesData = () => {
    const firstDevice: Device = deviceStore.allDevices().next().value
    const currentStatusLength = timeRangeSeconds / settingsStore.ccSettings.poll_rate
    shiftSeriesData(1)

    const newTimestamp = firstDevice.status.timestamp
    uSeriesData[0][currentStatusLength - 1] = new Date(newTimestamp).getTime() / 1000

    for (const device of deviceStore.allDevices()) {
        if (!includesDevice(device.uid)) continue
        const newStatus = device.status
        for (const tempStatus of newStatus.temps) {
            if (!includesTemps) break
            if (!includesDeviceChannel(device.uid, tempStatus.name)) continue
            const lineName = createLineName(device, tempStatus.name + '_temp')
            uSeriesData[uLineNames.indexOf(lineName) + 1][currentStatusLength - 1] = tempStatus.temp
        }
        for (const channelStatus of newStatus.channels) {
            if (!includesDeviceChannel(device.uid, channelStatus.name)) continue
            if (channelStatus.duty != null) {
                const isLoadChannel = includesLoads && channelStatus.name.endsWith('Load')
                const isFanDutyChannel = includedDuties && !channelStatus.name.endsWith('Load')
                if (isLoadChannel || isFanDutyChannel) {
                    const lineNameExt: string = isLoadChannel ? '_load' : '_duty'
                    const lineName = createLineName(device, channelStatus.name + lineNameExt)
                    uSeriesData[uLineNames.indexOf(lineName) + 1][currentStatusLength - 1] =
                        channelStatus.duty
                }
            }
            if (includesRPMs && channelStatus.rpm != null) {
                const lineName = createLineName(device, channelStatus.name + '_rpm')
                uSeriesData[uLineNames.indexOf(lineName) + 1][currentStatusLength - 1] =
                    channelStatus.rpm / settingsStore.frequencyPrecision
            }
            if (includesFreqs && channelStatus.freq != null) {
                const lineName = createLineName(device, channelStatus.name + '_freq')
                uSeriesData[uLineNames.indexOf(lineName) + 1][currentStatusLength - 1] =
                    channelStatus.freq / settingsStore.frequencyPrecision
            }
            if (includesWatts && channelStatus.watts != null) {
                const lineName = createLineName(device, channelStatus.name + '_watts')
                uSeriesData[uLineNames.indexOf(lineName) + 1][currentStatusLength - 1] =
                    channelStatus.watts
            }
        }
    }
    console.debug('Updated uPlot Data')
}

// const callRefreshSeriesListData = () => {
//     // we use a wrapper function here so we can easily update the
//     // function reference after the onMount() below
//     refreshSeriesListData()
// }

let chart: uPlot | null = null
let isZoomed: boolean = false
let rafId: number | null = null
let rafPaused: boolean = false // rAF stopped due to user interaction
let userZoomed: boolean = false // user has made a zoom selection
const startRaf = () => {
    if (rafId !== null || chart === null) return
    const animate = () => {
        const now = Date.now() / 1000
        chart!.setData(uSeriesData, false)
        // Snap the right edge to a whole device-pixel boundary so the x-axis only
        // advances in integer-pixel steps. This stabilizes the dash/dot phase between
        // frames and eliminates the shaky/crawling appearance of dashed series.
        const pxPerSec = chart!.bbox.width / timeRangeSeconds
        const snappedNow = pxPerSec > 0 ? Math.round(now * pxPerSec) / pxPerSec : now
        chart!.setScale('x', { min: snappedNow - timeRangeSeconds, max: snappedNow })
        rafId = requestAnimationFrame(animate)
    }
    rafId = requestAnimationFrame(animate)
}

const stopRaf = () => {
    if (rafId !== null) {
        cancelAnimationFrame(rafId)
        rafId = null
    }
}

onUnmounted(() => {
    stopRaf()
})

// @ts-ignore
let refreshSeriesListData = () => {
    initUSeriesData()
}

initUSeriesData()

const uPlotSeries: Array<uPlot.Series> = [{}]

const getLineStyle = (lineName: string): Array<number> => {
    const lineLower = lineName.toLowerCase()
    if (lineLower.endsWith('rpm') || lineLower.endsWith('freq')) {
        return [1, 1]
    } else if (lineLower.endsWith('load') || lineLower.includes('pump')) {
        return [6, 3]
    } else if (lineLower.endsWith('duty')) {
        return [10, 3, 2, 3]
    } else if (lineLower.endsWith('watts')) {
        return [6, 3, 2, 6]
    } else {
        return []
    }
}

let hasDegreeAxis: boolean = false
let hasFrequencyAxis: boolean = false
let hasWattsAxis: boolean = false
for (const lineName of uLineNames) {
    if (lineName.endsWith('_rpm') || lineName.endsWith('_freq')) {
        hasFrequencyAxis = true
        uPlotSeries.push({
            label: lineName,
            scale: SCALE_KEY_RPM,
            auto: props.dashboard.autoScaleFrequency,
            stroke: allDevicesLineProperties.get(lineName)?.color,
            points: {
                show: false,
            },
            dash: getLineStyle(lineName),
            spanGaps: true,
            width: settingsStore.chartLineScale,
            // min: 0,
            // max: 10000,
            // value: (_, rawValue) => (rawValue != null ? rawValue.toFixed(0) : rawValue),
            // value: (_, rawValue) => {
            //     // if (props.dashboard.frequencyPrecision === 1)
            //     //     return rawValue != null ? rawValue.toFixed(0) : rawValue
            //     // else
            //         return rawValue != null ? (rawValue / 1000).toFixed(1) : rawValue
            // },
        })
    } else if (lineName.endsWith('_watts')) {
        hasWattsAxis = true
        uPlotSeries.push({
            label: lineName,
            scale: SCALE_KEY_WATTS,
            auto: props.dashboard.autoScaleWatts,
            stroke: allDevicesLineProperties.get(lineName)?.color,
            points: {
                show: false,
            },
            dash: getLineStyle(lineName),
            spanGaps: true,
            width: settingsStore.chartLineScale,
        })
    } else {
        hasDegreeAxis = true
        uPlotSeries.push({
            label: lineName,
            scale: SCALE_KEY_PERCENT,
            auto: props.dashboard.autoScaleDegree,
            stroke: allDevicesLineProperties.get(lineName)?.color,
            points: {
                show: false,
            },
            dash: getLineStyle(lineName),
            spanGaps: true,
            width: settingsStore.chartLineScale,
            // min: 0,
            // max: 100,
            value: (_, rawValue) => (rawValue != null ? rawValue.toFixed(1) : rawValue),
        })
    }
}

const hourFormat = settingsStore.time24 ? 'HH' : 'h'
const uOptions: uPlot.Options = {
    width: 200,
    height: 200,
    pxAlign: 0,
    series: uPlotSeries,
    axes: [
        {
            stroke: colors.themeColors.text_color,
            size: deviceStore.isSafariWebKit()
                ? Math.max(deviceStore.getREMSize(2.0), 38)
                : deviceStore.getREMSize(2.0),
            font: `${deviceStore.getREMSize(1)}px sans-serif`,
            ticks: {
                show: true,
                stroke: colors.themeColors.text_color,
                width: 1,
                size: 5,
            },
            space: deviceStore.getREMSize(6.25),
            incrs: [15, 60, 300, 900],
            values: [
                // min tick incr | default | year | month | day | hour | min | sec | mode
                [900, `{${hourFormat}}:{mm}`, null, null, null, null, null, null, 0],
                [300, `{${hourFormat}}:{mm}`, null, null, null, null, null, null, 0],
                [60, `{${hourFormat}}:{mm}`, null, null, null, null, null, null, 0],
                [15, `{${hourFormat}}:{mm}:{ss}`, null, null, null, null, null, null, 0],
            ],
            border: {
                show: true,
                width: 1,
                stroke: colors.themeColors.text_color_secondary,
            },
            grid: {
                show: true,
                stroke: colors.themeColors.border,
                width: 1,
                dash: [1, 2],
            },
        },
        {
            scale: SCALE_KEY_PERCENT,
            label: `${t('common.percentUnit')}  /  ${t('common.tempUnit')}`,
            labelGap: 0,
            labelSize: deviceStore.getREMSize(1.4),
            labelFont: `sans-serif`,
            stroke: colors.themeColors.text_color,
            size: deviceStore.getREMSize(2.5),
            font: `${deviceStore.getREMSize(1)}px sans-serif`,
            gap: 3,
            ticks: {
                show: true,
                stroke: colors.themeColors.text_color_secondary,
                width: 1,
                size: 5,
            },
            incrs: [10],
            // values: (_, ticks) => ticks.map((rawValue) => rawValue + '°/%'),
            border: {
                show: true,
                width: 1,
                stroke: colors.themeColors.text_color_secondary,
            },
            grid: {
                show: true,
                stroke: colors.themeColors.border,
                width: 1,
                dash: [1, 2],
            },
        },
        {
            side: 1,
            scale: SCALE_KEY_RPM,
            label:
                settingsStore.frequencyPrecision === 1
                    ? t('components.axisOptions.rpmMhz')
                    : t('components.axisOptions.krpmGhz'),
            labelGap: settingsStore.frequencyPrecision === 1 ? deviceStore.getREMSize(1.0) : 0,
            labelSize:
                settingsStore.frequencyPrecision === 1
                    ? deviceStore.getREMSize(2.9)
                    : hasWattsAxis
                      ? deviceStore.getREMSize(2.25)
                      : deviceStore.getREMSize(1.4),
            // labelFont, unlike font, seems to take rem values properly, and by is 1rem by default:
            labelFont: `sans-serif`,
            stroke: colors.themeColors.text_color,
            size: deviceStore.getREMSize(2.5),
            font: `${deviceStore.getREMSize(1)}px sans-serif`,
            ticks: {
                show: true,
                stroke: colors.themeColors.text_color_secondary,
                width: 1,
                size: 5,
            },
            values: (_, axisValues) =>
                axisValues.map((rawValue) =>
                    settingsStore.frequencyPrecision === 1
                        ? rawValue.toFixed(0)
                        : rawValue.toFixed(1),
                ),
            incrs: (_self: uPlot, _axisIdx: number, _scaleMin: number, scaleMax: number) => {
                if (settingsStore.frequencyPrecision === 1) {
                    if (scaleMax > 7000) {
                        return [1000]
                    } else if (scaleMax > 3000) {
                        return [500]
                    } else if (scaleMax > 1300) {
                        return [200]
                    } else if (scaleMax > 700) {
                        return [100]
                    } else {
                        return [50]
                    }
                } else {
                    if (scaleMax > 2) {
                        return [1]
                    } else if (scaleMax > 1) {
                        return [0.5]
                    } else {
                        return [0.2]
                    }
                }
            },
            border: {
                show: true,
                width: 1,
                stroke: colors.themeColors.text_color_secondary,
            },
            grid: {
                show: !hasDegreeAxis && hasFrequencyAxis,
                stroke: colors.themeColors.border,
                width: 1,
                dash: [1, 2],
            },
        },
        {
            side: 1,
            scale: SCALE_KEY_WATTS,
            label: t('components.axisOptions.watts'),
            labelGap: 0,
            labelSize: deviceStore.getREMSize(1.4),
            labelFont: `sans-serif`,
            stroke: colors.themeColors.text_color,
            size: deviceStore.getREMSize(2.5),
            font: `${deviceStore.getREMSize(1)}px sans-serif`,
            gap: 3,
            ticks: {
                show: true,
                stroke: colors.themeColors.text_color_secondary,
                width: 1,
                size: 5,
            },

            incrs: (_self: uPlot, _axisIdx: number, _scaleMin: number, scaleMax: number) => {
                if (scaleMax > 7000) {
                    return [1000]
                } else if (scaleMax > 3000) {
                    return [500]
                } else if (scaleMax > 1200) {
                    return [200]
                } else if (scaleMax > 500) {
                    return [100]
                } else if (scaleMax > 180) {
                    return [50]
                } else if (scaleMax > 120) {
                    return [20]
                } else if (scaleMax > 50) {
                    return [10]
                } else if (scaleMax > 23) {
                    return [5]
                } else if (scaleMax > 13) {
                    return [2]
                } else if (scaleMax > 7) {
                    return [1]
                } else if (scaleMax > 1) {
                    return [0.5]
                }
                return [0.1]
            },
            // values: (_, ticks) => ticks.map((rawValue) => rawValue + ' W'),
            border: {
                show: true,
                width: 1,
                stroke: colors.themeColors.text_color_secondary,
            },
            grid: {
                show: !hasDegreeAxis && !hasFrequencyAxis && hasWattsAxis,
                stroke: colors.themeColors.border,
                width: 1,
                dash: [1, 2],
            },
        },
    ],
    scales: {
        '%': {
            auto: props.dashboard.autoScaleDegree,
            range: (_self, _dataMin, dataMax) => {
                if (!hasDegreeAxis) return [null, null]
                return props.dashboard.autoScaleDegree
                    ? uPlot.rangeNum(0, dataMax || 10.5, 0.1, true)
                    : [props.dashboard.degreeMin, props.dashboard.degreeMax]
            },
        },
        rpm: {
            auto: props.dashboard.autoScaleFrequency,
            // @ts-ignore
            range: (_self, _dataMin, dataMax) => {
                if (!hasFrequencyAxis) return [null, null]
                return props.dashboard.autoScaleFrequency
                    ? uPlot.rangeNum(0, dataMax || 90.5, 0.1, true)
                    : [
                          props.dashboard.frequencyMin / settingsStore.frequencyPrecision,
                          props.dashboard.frequencyMax / settingsStore.frequencyPrecision,
                      ]
            },
        },
        W: {
            auto: props.dashboard.autoScaleWatts,
            range: (_self, _dataMin, dataMax) => {
                if (!hasWattsAxis) return [null, null]
                return props.dashboard.autoScaleWatts
                    ? uPlot.rangeNum(0, dataMax || 10.5, 0.1, true)
                    : [props.dashboard.wattsMin, props.dashboard.wattsMax]
            },
        },
        x: {
            auto: true,
            time: true,
            // @ts-ignore
            range: (self, newMin, newMax) => {
                let curMin = self.scales.x.min
                let curMax = self.scales.x.max
                // prevent zooming in too far, < 10 seconds
                if (newMax - newMin < 10) return [curMin, curMax]
                return [newMin, newMax]
            },
        },
    },
    legend: {
        show: false,
    },
    cursor: {
        show: true,
        x: false,
        // enable for crosshair on y-axis (in addition to css properties):
        y: false,
        points: {
            show: false,
        },
        drag: {
            // enable zoom selection of x-axis
            x: true,
            y: false,
            // distance to cover before starting selection
            dist: 10,
        },
        bind: {
            // @ts-ignore
            mousedown: (u, targ, handler) => {
                return (e) => {
                    if (e.button == 0) {
                        if (e.ctrlKey) {
                            // no drag-selection when ctrl is pressed
                            return
                        }
                        const pxPerXUnitSecond = u.valToPos(1, 'x') - u.valToPos(0, 'x')
                        // 10 seconds max zoom-in
                        u.cursor.drag!.dist = pxPerXUnitSecond * 10
                        handler(e)
                    }
                }
            },
        },
    },
    plugins: [
        tooltipPlugin(allDevicesLineProperties, t, settingsStore.frequencyPrecision),
        columnHighlightPlugin(),
        mouseWheelZoomPlugin(),
    ],
    hooks: {
        setScale: [
            (u: uPlot, key: string) => {
                // Only handle user-driven scale changes. Internal uPlot calls during init,
                // data updates, and rAF-driven setScale are all silenced by the rafPaused guard.
                if (key !== 'x' || !settingsStore.eyeCandy || !rafPaused) return
                // Compare visible span to the actual data span with 1% tolerance.
                // This is robust against floating-point drift and the fact that data[0][0]
                // may be zero-padded at startup.
                const scaleSpan = u.scales.x.max! - u.scales.x.min!
                const dataSpan = u.data[0][u.data[0].length - 1] - u.data[0][0]
                if (scaleSpan >= dataSpan * 0.99) {
                    userZoomed = false
                    rafPaused = false
                    initUSeriesData() // re-sync data skipped while zoomed
                    startRaf()
                } else if (!userZoomed) {
                    // First scale change that is a real zoom-in (drag or wheel).
                    userZoomed = true
                }
            },
        ],
    },
}
console.debug('Processed status data for System Overview')

//----------------------------------------------------------------------------------------------------------------------

onMounted(async () => {
    const uChartElement: HTMLElement = document.getElementById('u-plot-chart') ?? new HTMLElement()
    chart = new uPlot(uOptions, uSeriesData, uChartElement)
    const getChartSize = () => {
        const cwh = uChartElement.getBoundingClientRect()
        return { width: cwh.width, height: cwh.height }
    }
    chart.setSize(getChartSize())
    const resizeObserver = new ResizeObserver((_) => {
        chart!.setSize(getChartSize())
    })
    resizeObserver.observe(uChartElement)

    uChartElement.addEventListener('mousedown', (event: MouseEvent) => {
        if (
            settingsStore.eyeCandy &&
            rafId !== null &&
            (event.button === 0 || event.button === 2)
        ) {
            stopRaf()
            rafPaused = true
        }
    })
    // capture: true fires during the capture phase (top-down), before the wheel zoom
    // plugin's listener on the child u.over element (bubbling phase).
    uChartElement.addEventListener(
        'wheel',
        () => {
            if (settingsStore.eyeCandy && rafId !== null) {
                if (
                    chart!.scales.x.min != chart!.data[0][0] ||
                    chart!.scales.x.max != chart!.data[0][chart!.data[0].length - 1]
                ) {
                    stopRaf()
                    rafPaused = true
                }
            }
        },
        { capture: true, passive: true },
    )
    uChartElement.addEventListener('mouseup', () => {
        if (settingsStore.eyeCandy && rafPaused) {
            // Defer: uPlot listens on document for mouseup to finalize zoom, so setScale
            // fires after this element listener. Check userZoomed only after that happens.
            setTimeout(() => {
                if (rafPaused && !userZoomed) {
                    rafPaused = false
                    initUSeriesData() // re-sync data skipped while paused
                    startRaf()
                }
            }, 0)
        }
    })
    if (settingsStore.eyeCandy) {
        startRaf()
    }

    refreshSeriesListData = () => {
        initUSeriesData()
        chart!.setData(uSeriesData)
    }
    deviceStore.$onAction(({ name, after }) => {
        if (name === 'updateStatus') {
            after((onlyRecentStatus: boolean) => {
                if (!settingsStore.eyeCandy) {
                    // Non-animated mode: zoom detection via scale vs data bounds comparison
                    if (
                        chart!.scales.x.min != chart!.data[0][0] ||
                        chart!.scales.x.max != chart!.data[0][chart!.data[0].length - 1]
                    ) {
                        isZoomed = true
                        return
                    } else if (isZoomed) {
                        // zoom has been reset
                        isZoomed = false
                        initUSeriesData() // reinit everything
                    }
                }
                if (onlyRecentStatus) {
                    if (settingsStore.eyeCandy && rafId === null) {
                        // eyeCandy with rAF paused (user zoomed): do not shift data,
                        // as it causes the viewport contents to drift under the
                        // fixed zoom scale.
                        return
                    }
                    updateUSeriesData()
                } else {
                    initUSeriesData() // reinit everything
                }
                if (!settingsStore.eyeCandy) {
                    chart!.setData(uSeriesData, true)
                } else if (rafId === null) {
                    // eyeCandy, rAF paused (user zoomed): keep data live, preserve scale
                    chart!.setData(uSeriesData, false)
                }
                // if rAF is running, it calls setData each frame
            })
        }
    })
})
</script>

<template>
    <div class="p-2">
        <div id="u-plot-chart"></div>
    </div>
</template>

<style scoped lang="scss">
#u-plot-chart {
    width: 100%;
    height: calc(100vh - 5.75rem);
}

// zoom selection style
#u-plot-chart :deep(.u-select) {
    background: linear-gradient(
        0deg,
        rgba(var(--colors-accent) / 0.03) 0%,
        rgba(var(--colors-accent) / 0.4) 100%
    );
    position: absolute;
    pointer-events: none;
}
/** To add a crosshair to the y-axis:
.chart :deep(.u-hz .u-cursor-y) {
    border-bottom: v-bind(yCrosshair);
}
*/
</style>
