<template>
    <div class="table-box waveform-page">
        <WaveformToolbar
            :selected-waveform-file-name="selectedWaveformFileName"
            :is-parsing="isParsing"
            :waveform-file-accept="waveformFileAccept"
            :channel-options="channelOptions"
            :active-channel-index="activeChannelIndex"
            :display-mode-options="displayModeOptions"
            :active-display-mode="activeDisplayMode"
            :value-mode-options="valueModeOptions"
            :active-value-mode="activeValueMode"
            @update:active-channel-index="activeChannelIndex = $event"
            @update:active-display-mode="activeDisplayMode = $event"
            @update:active-value-mode="activeValueMode = $event"
            @waveform-file-change="handleWaveformFileChange"
        />

        <div class="waveform-layout">
            <WaveformTrendPanel
                :has-waveform-data="hasWaveformData"
                :active-display-mode="activeDisplayMode"
                :active-trend-tab="activeTrendTab"
                :trend-tabs="trendTabs"
                :active-trend-options="activeTrendOptions"
                :single-channel-trend-options-list="singleChannelTrendOptionsList"
                :all-channel-trend-groups="allChannelTrendGroups"
                :is-all-channels-active="isAllChannelsActive"
                :last-parse-error-message="lastParseErrorMessage"
                :active-trend-tool-states="activeTrendToolStates"
                :disabled-trend-tool-states="disabledTrendToolStates"
                @update:active-trend-tab="activeTrendTab = $event"
                @trend-tool-action="handleTrendToolAction"
                @chart-data-zoom="handleTrendChartDataZoom"
            />

            <WaveformInfoPanel
                :has-parsed-waveform="hasParsedWaveform"
                :summary-items="summaryItems"
                :vector-parse-result="vectorParseResult"
                :last-parse-error-message="lastParseErrorMessage"
                :last-vector-parse-error-message="lastVectorParseErrorMessage"
                :active-vector-channel-name="activeWaveDetail?.channelName || ''"
            />
        </div>
    </div>
</template>

<script setup lang="ts">
import { computed, nextTick, ref, watch } from 'vue'
import dayjs from 'dayjs'
import html2canvas from 'html2canvas'
import { ElMessage } from 'element-plus'
import { parseComtradeApi, parseComtradeVectorApi } from '@/api/tools/waveform'
import type { Waveform } from '@/api/tools/waveform/interface'
import WaveformInfoPanel from './components/WaveformInfoPanel.vue'
import WaveformToolbar from './components/WaveformToolbar.vue'
import WaveformTrendPanel from './components/WaveformTrendPanel.vue'
import type {
    AllChannelTrendGroup,
    ChannelSelectValue,
    DisplayMode,
    LabelValueOption,
    SingleChannelTrendOption,
    SummaryItem,
    TrendChartZoomPayload,
    TrendToolAction,
    TrendTabValue,
    ValueMode,
    WaveformDetailOption
} from './components/types'

defineOptions({
    name: 'WaveformView'
})

interface WaveformSeriesItem {
    name: string
    data: number[]
}

interface WaveformTrendPayload {
    timeLabels: string[]
    unit: string
    min?: number
    max?: number
    series: WaveformSeriesItem[]
}

interface TrendChartLayoutOptions {
    showTimeAxis?: boolean
    yAxisSplitCount?: number
}

interface TrendZoomRange {
    start: number
    end: number
}

const activeTrendTab = ref<TrendTabValue>('instant')
const activeValueMode = ref<ValueMode>('primary')
const activeDisplayMode = ref<DisplayMode>('single-channel')
const activeChannelIndex = ref<ChannelSelectValue>('all')
const singleChannelTrendChartGroup = 'waveform-single-channel-sync'
const allChannelTrendChartGroup = 'waveform-all-channel-sync'
const isParsing = ref(false)
const selectedCfgFile = ref<File | null>(null)
const selectedDatFile = ref<File | null>(null)
const waveformParseResult = ref<Waveform.WaveComtradeResultVO | null>(null)
const vectorParseResult = ref<Waveform.WaveComtradeVectorResultVO | null>(null)
const lastParseErrorMessage = ref('')
const lastVectorParseErrorMessage = ref('')
const waveformFileAccept = '.cfg,.dat'
const trendXZoomRange = ref<TrendZoomRange>({ start: 0, end: 100 })
const trendYZoomScale = ref(1)
const activeTrendInteractionMode = ref<'none' | 'box-zoom' | 'pan'>('none')

const trendTabs: LabelValueOption<TrendTabValue>[] = [
    { value: 'instant', label: '瞬时波形' },
    { value: 'rms', label: 'RMS 波形' }
]

const valueModeOptions: LabelValueOption<ValueMode>[] = [
    { label: '一次值', value: 'primary' },
    { label: '二次值', value: 'secondary' }
]

const displayModeOptions: LabelValueOption<DisplayMode>[] = [
    { label: '单通道', value: 'single-channel' },
    { label: '多通道', value: 'multi-channel' }
]

const trendLabelMap: Record<TrendTabValue, string> = {
    instant: '瞬时波形',
    rms: 'RMS 波形'
}

const valueModeLabelMap: Record<ValueMode, string> = {
    primary: '一次值',
    secondary: '二次值'
}

const readThemeColor = (name: string, fallback: string) => {
    if (typeof window === 'undefined') return fallback
    const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim()
    return value || fallback
}

const phaseColors = {
    a: readThemeColor('--cn-color-phase-a', '#daa520'),
    b: readThemeColor('--cn-color-phase-b', '#2e8b57'),
    c: readThemeColor('--cn-color-phase-c', '#a52a2a')
}

const defaultPhaseColors = [phaseColors.a, phaseColors.b, phaseColors.c]
const axisTextColor = readThemeColor('--el-text-color-regular', '#606266')
const axisLineColor = readThemeColor('--el-border-color', '#dcdfe6')

const selectedWaveformFileName = computed(() => {
    const fileNames = [selectedCfgFile.value?.name, selectedDatFile.value?.name].filter(Boolean)
    return fileNames.join(' / ')
})

const getWaveformParseErrorMessage = (error: unknown) => {
    if (!error || typeof error !== 'object') {
        return '波形解析失败，请检查 cfg 和 dat 文件内容'
    }

    const businessError = error as {
        message?: string
        response?: {
            data?: {
                message?: string
            }
        }
    }

    return businessError.response?.data?.message || businessError.message || '波形解析失败，请检查 cfg 和 dat 文件内容'
}

const hasParsedWaveform = computed(() => !!waveformParseResult.value?.waveData)

const buildSeriesPoints = (list: number[][] | undefined, valueIndex: number) => {
    return (list || []).map(item => [Number(item[0]), Number(item[valueIndex] ?? 0)])
}

const normalizedWaveDetails = computed<Waveform.WaveDataDetail[]>(() => {
    const waveData = waveformParseResult.value?.waveData
    const detailList = waveformParseResult.value?.waveDataDetails || []
    const instantList = waveData?.listWaveData
    const rmsList = waveData?.listRmsData
    const waveTitles = waveData?.waveTitle || []
    const groupCount = Math.max(Math.floor(Math.max(waveTitles.length - 1, 0) / 3), detailList.length)

    if (!groupCount) return []

    return Array.from({ length: groupCount }, (_, index) => {
        const startColumnIndex = index * 3 + 1
        const detail = detailList[index]
        const titleSlice = waveTitles.slice(startColumnIndex, startColumnIndex + 3)

        return {
            channelName: detail?.channelName || waveData?.channelNames?.[startColumnIndex] || `通道${index + 1}`,
            title: detail?.title || titleSlice.join(' / ') || `三相波形 ${index + 1}`,
            unit: detail?.unit || '',
            a: detail?.a || waveTitles[startColumnIndex] || 'A相',
            b: detail?.b || waveTitles[startColumnIndex + 1] || 'B相',
            c: detail?.c || waveTitles[startColumnIndex + 2] || 'C相',
            isOpen: detail?.isOpen || false,
            // 三相颜色统一读取全局主题变量，避免被接口返回的局部颜色覆盖。
            colors: [...defaultPhaseColors],
            instantData: {
                aValue: buildSeriesPoints(instantList, startColumnIndex),
                bValue: buildSeriesPoints(instantList, startColumnIndex + 1),
                cValue: buildSeriesPoints(instantList, startColumnIndex + 2)
            },
            rmsData: {
                aValue: buildSeriesPoints(rmsList, startColumnIndex),
                bValue: buildSeriesPoints(rmsList, startColumnIndex + 1),
                cValue: buildSeriesPoints(rmsList, startColumnIndex + 2)
            }
        }
    })
})

const channelOptions = computed<WaveformDetailOption[]>(() => {
    const detailOptions = normalizedWaveDetails.value.map((item, index) => ({
        label: buildChannelLabel(item, index),
        value: index
    }))

    return detailOptions.length ? [{ label: '全部', value: 'all' }, ...detailOptions] : []
})

const isAllChannelsActive = computed(() => activeChannelIndex.value === 'all')

const activeWaveDetail = computed(() => {
    if (typeof activeChannelIndex.value !== 'number') return null

    return normalizedWaveDetails.value[activeChannelIndex.value] || normalizedWaveDetails.value[0] || null
})

const activeWaveData = computed(() => waveformParseResult.value?.waveData)

const normalizeRatio = (value?: number) => {
    const ratio = Number(value)
    return Number.isFinite(ratio) && ratio > 0 ? ratio : 1
}

const isCurrentChannel = (channelName?: string) => {
    return (channelName || '').toUpperCase().startsWith('I')
}

const getValueScale = (detail: Waveform.WaveDataDetail | null) => {
    if (activeValueMode.value === 'secondary') return 1

    const waveData = activeWaveData.value
    const ratio = isCurrentChannel(detail?.channelName) ? waveData?.ct : waveData?.pt
    return normalizeRatio(ratio)
}

const activeValueScale = computed(() => getValueScale(activeWaveDetail.value))

const safeNumber = (value: unknown) => {
    const numberValue = Number(value)
    return Number.isFinite(numberValue) ? numberValue : 0
}

const formatNumber = (value: unknown, fractionDigits = 3) => {
    const numberValue = Number(value)

    if (!Number.isFinite(numberValue)) return '--'
    if (Number.isInteger(numberValue)) return `${numberValue}`

    return `${Number(numberValue.toFixed(fractionDigits))}`
}

const formatWaveformTime = (value?: string) => {
    if (!value) return '--'

    const parsedValue = dayjs(value)
    return parsedValue.isValid() ? parsedValue.format('YYYY-MM-DD HH:mm:ss.SSS') : value
}

const buildChannelLabel = (detail: Waveform.WaveDataDetail, index: number) => {
    if (detail.title) return detail.title
    if (detail.channelName && detail.unit) return `${detail.channelName} (${detail.unit})`
    if (detail.channelName) return detail.channelName
    return `通道 ${index + 1}`
}

const buildTrendPayload = (
    detail: Waveform.WaveDataDetail | null,
    trendTab: TrendTabValue,
    scale: number
): WaveformTrendPayload => {
    const trendData = trendTab === 'instant' ? detail?.instantData : detail?.rmsData
    const aName = detail?.a || 'A相'
    const bName = detail?.b || 'B相'
    const cName = detail?.c || 'C相'
    const aValue = trendData?.aValue || []
    const bValue = trendData?.bValue || []
    const cValue = trendData?.cValue || []
    const timeSource = aValue.length ? aValue : bValue.length ? bValue : cValue
    const seriesConfigs = [
        { name: aName, source: aValue },
        { name: bName, source: bValue },
        { name: cName, source: cValue }
    ]

    const series = seriesConfigs
        .filter(item => item.source.length)
        .map(item => ({
            name: item.name,
            data: item.source.map(point => safeNumber(point[1]) * scale)
        }))

    const flatSeriesData = series.flatMap(item => item.data)
    const min = flatSeriesData.length ? Math.min(...flatSeriesData) : undefined
    const max = flatSeriesData.length ? Math.max(...flatSeriesData) : undefined

    return {
        timeLabels: timeSource.map(point => formatNumber(point[0])),
        unit: detail?.unit || '',
        min,
        max,
        series
    }
}

const activeTrendPayload = computed(() => {
    return buildTrendPayload(activeWaveDetail.value, activeTrendTab.value, activeValueScale.value)
})

const getTrendColors = (detail: Waveform.WaveDataDetail | null) => {
    const customColors = detail?.colors?.filter(Boolean) || []
    return customColors.length >= 3 ? customColors.slice(0, 3) : defaultPhaseColors
}

const currentTrendColors = computed(() => getTrendColors(activeWaveDetail.value))

const hasWaveformData = computed(() => {
    if (isAllChannelsActive.value) return allChannelTrendGroups.value.length > 0

    return activeTrendPayload.value.series.length > 0
})

const canPanTrendChart = computed(() => {
    const { start, end } = trendXZoomRange.value

    return hasWaveformData.value && (start > 0 || end < 100)
})

const activeTrendToolStates = computed<Partial<Record<TrendToolAction, boolean>>>(() => ({
    'box-zoom': activeTrendInteractionMode.value === 'box-zoom',
    pan: activeTrendInteractionMode.value === 'pan'
}))

const disabledTrendToolStates = computed<Partial<Record<TrendToolAction, boolean>>>(() => ({
    pan: !canPanTrendChart.value
}))

const TREND_AXIS_EXPAND_RATIO = 1.2
const TREND_AXIS_SHRINK_RATIO = 0.8
const TREND_AXIS_BALANCED_RATIO = 0.9
const TREND_AXIS_DEFAULT_SPLIT_COUNT = 4
const TREND_AXIS_COMPACT_SPLIT_COUNT = 2
const TREND_AXIS_SMALL_INTERVAL_THRESHOLD = 1
const TREND_AXIS_EXTRA_SPLIT_SCORE = 0.25
const TREND_AXIS_READABLE_INTERVAL_STEPS = [1, 2, 2.5, 5, 10]
const TREND_GRID_TOP = '6px'
const TREND_GRID_LEFT = '52px'
const TREND_GRID_RIGHT = '18px'
const TREND_GRID_BOTTOM = {
    withTimeAxis: '30px',
    withoutTimeAxis: '6px'
}
const TREND_LINE_MAX_WIDTH = 1.6

const getAxisPrecision = (step: number) => {
    const absStep = Math.abs(step)

    if (!Number.isFinite(absStep) || absStep >= 1) return 0

    const stepText = `${absStep}`
    if (stepText.includes('e-')) {
        return Number(stepText.split('e-')[1] || 0)
    }

    return Math.min(stepText.split('.')[1]?.length || 0, 4)
}

const getAxisBoundaryPrecision = (axisMin: number, axisMax: number, interval: number) => {
    const boundaryPrecision = Math.max(Math.abs(axisMin), Math.abs(axisMax)) < 1 ? 2 : 0

    return Math.max(getAxisPrecision(interval), boundaryPrecision)
}

const getReadableAxisInterval = (value: number) => {
    if (!Number.isFinite(value) || value <= 0) return 1

    const magnitude = 10 ** Math.floor(Math.log10(value))
    const normalizedValue = value / magnitude
    const step = TREND_AXIS_READABLE_INTERVAL_STEPS.find(item => normalizedValue <= item) || 10

    return step * magnitude
}

const normalizeAxisValue = (value: number, precision: number) => {
    const factor = 10 ** precision
    const normalizedValue = Math.round(value * factor) / factor
    return Object.is(normalizedValue, -0) ? 0 : normalizedValue
}

const roundAxisValueUp = (value: number) => {
    const absValue = Math.abs(value)

    if (!Number.isFinite(absValue) || absValue === 0) return 0

    const magnitude = 10 ** Math.floor(Math.log10(absValue))
    const step = magnitude >= 10 ? magnitude / 10 : magnitude
    const precision = getAxisPrecision(step)

    return normalizeAxisValue(Math.ceil(absValue / step) * step, precision)
}

const formatAxisLabel = (value: number, precision: number) => {
    if (!Number.isFinite(value)) return ''
    return `${normalizeAxisValue(value, precision)}`
}

const clampPercent = (value: number) => Math.min(Math.max(value, 0), 100)

// 趋势图按当前可见点数调整线宽，避免大数据初始展示时线条过粗。
const resolveTrendVisiblePointCount = (pointCount: number) => {
    const { start, end } = trendXZoomRange.value
    const visibleRatio = Math.max((end - start) / 100, 0)

    return Math.ceil(pointCount * visibleRatio)
}

const resolveTrendLineWidth = (pointCount: number) => {
    if (pointCount >= 200000) return 0.35
    if (pointCount >= 100000) return 0.45
    if (pointCount >= 50000) return 0.55
    if (pointCount >= 20000) return 0.65
    if (pointCount >= 10000) return 0.75
    if (pointCount >= 5000) return 0.9
    if (pointCount >= 2000) return 1
    if (pointCount >= 800) return 1.2
    if (pointCount >= 200) return 1.4

    return TREND_LINE_MAX_WIDTH
}

const resetTrendToolState = () => {
    trendXZoomRange.value = { start: 0, end: 100 }
    trendYZoomScale.value = 1
    activeTrendInteractionMode.value = 'none'
}

const zoomTrendXAxis = (ratio: number) => {
    const { start, end } = trendXZoomRange.value
    const center = (start + end) / 2
    const nextWidth = Math.min(Math.max((end - start) * ratio, 1), 100)
    const nextStart = clampPercent(center - nextWidth / 2)
    const nextEnd = clampPercent(center + nextWidth / 2)

    if (nextStart === 0) {
        trendXZoomRange.value = { start: 0, end: nextWidth }
        return
    }

    if (nextEnd === 100) {
        trendXZoomRange.value = { start: 100 - nextWidth, end: 100 }
        return
    }

    trendXZoomRange.value = { start: nextStart, end: nextEnd }
}

const applyYAxisZoom = (yAxisConfig: Record<string, unknown>) => {
    const axisMin = Number(yAxisConfig.min)
    const axisMax = Number(yAxisConfig.max)
    const scale = trendYZoomScale.value

    if (!Number.isFinite(axisMin) || !Number.isFinite(axisMax) || axisMin === axisMax || scale === 1) {
        return yAxisConfig
    }

    const center = (axisMin + axisMax) / 2
    const halfRange = ((axisMax - axisMin) * scale) / 2
    const nextMin = center - halfRange
    const nextMax = center + halfRange
    const splitNumber = Number(yAxisConfig.splitNumber) || TREND_AXIS_DEFAULT_SPLIT_COUNT
    const interval = (nextMax - nextMin) / splitNumber
    const precision = getAxisBoundaryPrecision(nextMin, nextMax, interval)
    const normalizedInterval = normalizeAxisValue(interval, precision)

    return {
        ...yAxisConfig,
        min: normalizeAxisValue(nextMin, precision),
        max: normalizeAxisValue(nextMax, precision),
        interval: normalizedInterval,
        minInterval: normalizedInterval,
        maxInterval: normalizedInterval
    }
}

const resolveReadableAxisRange = (axisMin: number, axisMax: number, splitCount: number) => {
    const axisRange = axisMax - axisMin
    const rawInterval = axisRange / splitCount

    if (!Number.isFinite(axisRange) || axisRange <= 0 || rawInterval >= TREND_AXIS_SMALL_INTERVAL_THRESHOLD) {
        return {
            axisMin,
            axisMax,
            interval: rawInterval,
            splitCount
        }
    }

    const splitCountCandidates =
        splitCount >= TREND_AXIS_DEFAULT_SPLIT_COUNT ? [splitCount, splitCount + 1] : [splitCount]

    return splitCountCandidates.reduce(
        (currentBest, currentSplitCount) => {
            let interval = getReadableAxisInterval(axisRange / currentSplitCount)
            let readableMin = axisMin
            let readableMax = axisMax

            for (let index = 0; index < TREND_AXIS_READABLE_INTERVAL_STEPS.length; index += 1) {
                readableMin = Math.floor(axisMin / interval) * interval
                readableMax = readableMin + interval * currentSplitCount

                if (readableMax < axisMax) {
                    readableMax = Math.ceil(axisMax / interval) * interval
                    readableMin = readableMax - interval * currentSplitCount
                }

                if (readableMin <= axisMin && readableMax >= axisMax) break

                interval = getReadableAxisInterval(interval * 1.01)
            }

            const precision = getAxisPrecision(interval)
            const normalizedMin = normalizeAxisValue(readableMin, precision)
            const normalizedMax = normalizeAxisValue(readableMax, precision)
            const normalizedInterval = normalizeAxisValue(interval, precision)
            const extraSplitCost = Math.max(currentSplitCount - splitCount, 0) * TREND_AXIS_EXTRA_SPLIT_SCORE
            const wasteRatio = (normalizedMax - normalizedMin - axisRange) / axisRange
            const score = getAxisPrecision(normalizedInterval) * 10 + extraSplitCost + wasteRatio

            if (score >= currentBest.score) return currentBest

            return {
                axisMin: normalizedMin,
                axisMax: normalizedMax,
                interval: normalizedInterval,
                splitCount: currentSplitCount,
                score
            }
        },
        {
            axisMin,
            axisMax,
            interval: rawInterval,
            splitCount,
            score: Number.POSITIVE_INFINITY
        }
    )
}

const resolveExpandedAxisBoundary = (value: number, isMin: boolean) => {
    if (value === 0) return 0

    const ratio = isMin
        ? value < 0
            ? TREND_AXIS_EXPAND_RATIO
            : TREND_AXIS_SHRINK_RATIO
        : value > 0
          ? TREND_AXIS_EXPAND_RATIO
          : TREND_AXIS_SHRINK_RATIO

    return value * ratio
}

const resolveTrendAxisSplitCount = (layoutOptions: TrendChartLayoutOptions) => {
    return layoutOptions.yAxisSplitCount || TREND_AXIS_DEFAULT_SPLIT_COUNT
}

const shouldUseBalancedAxisBoundary = (min: number, max: number) => {
    if (min >= 0 || max <= 0) return false

    const minAbs = Math.abs(min)
    const maxAbs = Math.abs(max)
    const smallerAbs = Math.min(minAbs, maxAbs)
    const largerAbs = Math.max(minAbs, maxAbs)

    return largerAbs > 0 && smallerAbs / largerAbs >= TREND_AXIS_BALANCED_RATIO
}

const buildTrendAxisConfig = (trendPayload: WaveformTrendPayload, splitCount = TREND_AXIS_DEFAULT_SPLIT_COUNT) => {
    const min = Number(trendPayload.min)
    const max = Number(trendPayload.max)

    if (!Number.isFinite(min) || !Number.isFinite(max)) {
        return {
            name: trendPayload.unit || ''
        }
    }

    let axisMin = resolveExpandedAxisBoundary(min, true)
    let axisMax = resolveExpandedAxisBoundary(max, false)

    if (shouldUseBalancedAxisBoundary(min, max)) {
        // 正负幅值接近时，对称边界必须基于已向外扩展后的范围，避免把峰值贴到坐标边界。
        const axisBoundary = roundAxisValueUp(Math.max(Math.abs(axisMin), Math.abs(axisMax)))
        axisMin = -axisBoundary
        axisMax = axisBoundary
    }

    if (axisMin === axisMax) {
        const fallbackBoundary = Math.max(Math.abs(axisMin), 1) * TREND_AXIS_EXPAND_RATIO
        axisMin = axisMin - fallbackBoundary
        axisMax = axisMax + fallbackBoundary
    }

    const safeSplitCount = Math.max(Math.round(splitCount), 1)
    const readableAxisRange = resolveReadableAxisRange(axisMin, axisMax, safeSplitCount)
    const precision = getAxisBoundaryPrecision(
        readableAxisRange.axisMin,
        readableAxisRange.axisMax,
        readableAxisRange.interval
    )

    axisMin = normalizeAxisValue(readableAxisRange.axisMin, precision)
    axisMax = normalizeAxisValue(readableAxisRange.axisMax, precision)
    const interval = normalizeAxisValue(readableAxisRange.interval, precision)
    const axisSplitCount = readableAxisRange.splitCount

    return {
        name: trendPayload.unit || '',
        nameLocation: 'middle',
        nameGap: 42,
        nameTextStyle: {
            color: axisTextColor,
            fontSize: 12,
            align: 'center'
        },
        min: axisMin,
        max: axisMax,
        interval,
        splitNumber: axisSplitCount,
        minInterval: interval,
        maxInterval: interval,
        // 纵坐标按数据极值留白后均分，小区间优先使用可读步长，避免标签出现冗长小数。
        axisLabel: {
            showMinLabel: true,
            showMaxLabel: true,
            hideOverlap: true,
            formatter: (value: number) => formatAxisLabel(value, precision)
        }
    }
}

const buildTrendSeries = (seriesList: WaveformSeriesItem[]) => {
    return seriesList.map(item => {
        const visiblePointCount = resolveTrendVisiblePointCount(item.data.length)

        return {
            name: item.name,
            type: 'line',
            smooth: true,
            symbol: 'none',
            symbolSize: 3,
            lineStyle: {
                width: resolveTrendLineWidth(visiblePointCount)
            },
            data: item.data
        }
    })
}

const buildTimeAxisLabelFormatter = (timeLabels: string[]) => {
    const lastIndex = timeLabels.length - 1

    return (value: string | number, index: number) => {
        // 横坐标只保留首尾标签，避免波形点位过多时底部文字拥挤。
        if (index !== 0 && index !== lastIndex) return ''
        return formatNumber(value, 2)
    }
}

interface TrendTooltipParam {
    axisValue?: string | number
    marker?: string
    seriesName?: string
    value?: number | string
}

const buildTrendTooltipFormatter = (unit: string, showTime = true) => {
    return (params: TrendTooltipParam | TrendTooltipParam[]) => {
        const paramList = Array.isArray(params) ? params : [params]
        const firstParam = paramList[0]
        const timeValue = firstParam?.axisValue
        const valueRows = paramList
            .map(item => {
                const marker = item.marker || ''
                const seriesName = item.seriesName || ''
                const valueText = unit ? `${formatNumber(item.value)} ${unit}` : formatNumber(item.value)

                return `<div>${marker}${seriesName}<span style="float:right;margin-left:12px;font-weight:600;">${valueText}</span></div>`
            })
            .join('')
        const timeText = timeValue === undefined ? '--' : `${formatNumber(timeValue, 2)} ms`

        if (!showTime) return valueRows

        return `${valueRows}<div style="margin-top:4px;">时间<span style="float:right;margin-left:12px;">${timeText}</span></div>`
    }
}

const buildTrendChartOptions = (
    trendPayload: WaveformTrendPayload,
    seriesList: WaveformSeriesItem[],
    chartColors = currentTrendColors.value,
    layoutOptions: TrendChartLayoutOptions = {}
) => {
    const { showTimeAxis = true } = layoutOptions
    const yAxisSplitCount = resolveTrendAxisSplitCount(layoutOptions)
    const yAxisConfig = applyYAxisZoom(buildTrendAxisConfig(trendPayload, yAxisSplitCount))

    return {
        activeTool: activeTrendInteractionMode.value,
        tooltip: {
            trigger: 'axis',
            axisPointer: {
                type: 'line',
                snap: true,
                lineStyle: {
                    color: 'rgba(24, 144, 255, 0.55)',
                    width: 1
                }
            },
            formatter: buildTrendTooltipFormatter(trendPayload.unit, showTimeAxis)
        },
        legend: {
            show: false,
            top: 0,
            right: 12
        },
        grid: {
            top: TREND_GRID_TOP,
            // 多图趋势图固定绘图区左边界，避免纵坐标标签宽度不同导致 x=0 起点错位。
            left: TREND_GRID_LEFT,
            right: TREND_GRID_RIGHT,
            bottom: showTimeAxis ? TREND_GRID_BOTTOM.withTimeAxis : TREND_GRID_BOTTOM.withoutTimeAxis,
            containLabel: false
        },
        xAxis: {
            data: trendPayload.timeLabels,
            boundaryGap: false,
            // 仅最后一张图显示时间轴，前两张图不再额外预留占位，尽量放大趋势内容区。
            name: showTimeAxis ? 'ms' : '',
            nameLocation: 'end',
            nameGap: showTimeAxis ? 6 : 0,
            nameTextStyle: {
                color: axisTextColor
            },
            axisLine: {
                show: showTimeAxis,
                // 时间轴固定在图底部，避免 y 轴包含 0 时横轴穿过波形区域显得过粗。
                onZero: false,
                lineStyle: {
                    color: axisLineColor
                }
            },
            axisLabel: {
                show: showTimeAxis,
                hideOverlap: false,
                interval: 0,
                margin: showTimeAxis ? 10 : 0,
                color: axisTextColor,
                formatter: buildTimeAxisLabelFormatter(trendPayload.timeLabels)
            },
            axisTick: {
                show: showTimeAxis,
                lineStyle: {
                    color: axisLineColor
                }
            }
        },
        yAxis: yAxisConfig,
        dataZoom: [
            {
                type: 'inside',
                start: trendXZoomRange.value.start,
                end: trendXZoomRange.value.end,
                zoomOnMouseWheel: activeTrendInteractionMode.value !== 'pan',
                moveOnMouseMove: activeTrendInteractionMode.value === 'pan',
                moveOnMouseWheel: activeTrendInteractionMode.value === 'pan'
            }
        ],
        color: chartColors,
        series: buildTrendSeries(seriesList)
    }
}

const activeTrendOptions = computed<Record<string, unknown>>(() => {
    const trendPayload = activeTrendPayload.value
    return buildTrendChartOptions(trendPayload, trendPayload.series)
})

// 单通道模式按相别拆成多个图，便于分别观察各通道波形。
const buildSingleChannelTrendOptionsList = (
    detail: Waveform.WaveDataDetail | null,
    detailIndex: number,
    chartGroup: string,
    resolveShowTimeAxis = (seriesIndex: number, seriesCount: number) => seriesIndex === seriesCount - 1
): SingleChannelTrendOption[] => {
    const trendPayload = buildTrendPayload(detail, activeTrendTab.value, getValueScale(detail))
    const trendColors = getTrendColors(detail)

    // 单通道下每张图只保留一个 series，需要单独指定对应相色。
    return trendPayload.series.map((item, index) => {
        const showTimeAxis = resolveShowTimeAxis(index, trendPayload.series.length)
        const itemTrendPayload = {
            ...trendPayload,
            min: item.data.length ? Math.min(...item.data) : undefined,
            max: item.data.length ? Math.max(...item.data) : undefined,
            series: [item]
        }

        return {
            key: `${detailIndex}-${item.name}`,
            group: chartGroup,
            isLastChart: showTimeAxis,
            options: buildTrendChartOptions(
                itemTrendPayload,
                itemTrendPayload.series,
                [trendColors[index] || defaultPhaseColors[index]],
                {
                    showTimeAxis,
                    yAxisSplitCount: TREND_AXIS_COMPACT_SPLIT_COUNT
                }
            )
        }
    })
}

const singleChannelTrendOptionsList = computed<SingleChannelTrendOption[]>(() => {
    return buildSingleChannelTrendOptionsList(activeWaveDetail.value, -1, singleChannelTrendChartGroup)
})

const allChannelTrendGroups = computed<AllChannelTrendGroup[]>(() => {
    const availableDetails = normalizedWaveDetails.value
        .map((detail, index) => {
            const trendPayload = buildTrendPayload(detail, activeTrendTab.value, getValueScale(detail))

            return {
                detail,
                index,
                trendPayload
            }
        })
        .filter(item => item.trendPayload.series.length > 0)

    return availableDetails.map((item, groupIndex) => {
        const isLastGroup = groupIndex === availableDetails.length - 1

        return {
            key: `${item.index}-${buildChannelLabel(item.detail, item.index)}`,
            title: buildChannelLabel(item.detail, item.index),
            group: allChannelTrendChartGroup,
            isLastChart: isLastGroup,
            singleChannelOptionsList: buildSingleChannelTrendOptionsList(
                item.detail,
                item.index,
                allChannelTrendChartGroup,
                (seriesIndex, seriesCount) => isLastGroup && seriesIndex === seriesCount - 1
            ),
            multiChannelOptions: buildTrendChartOptions(
                item.trendPayload,
                item.trendPayload.series,
                getTrendColors(item.detail),
                {
                    showTimeAxis: isLastGroup,
                    yAxisSplitCount: TREND_AXIS_COMPACT_SPLIT_COUNT
                }
            )
        }
    })
})

const summaryItems = computed<SummaryItem[]>(() => {
    const waveData = activeWaveData.value
    const cfgData = waveData?.comtradeCfgDTO
    const detail = activeWaveDetail.value

    return [
        { label: '录波开始', value: formatWaveformTime(cfgData?.timeStart) },
        { label: '触发时间', value: formatWaveformTime(cfgData?.timeTrige) },
        { label: '采样率', value: cfgData?.finalSampleRate ? `${cfgData.finalSampleRate} Hz` : '--' },
        { label: '总通道数', value: cfgData?.nChannelNum ?? '--' },
        {
            label: '当前通道',
            value: isAllChannelsActive.value
                ? '全部'
                : detail
                  ? buildChannelLabel(detail, activeChannelIndex.value as number)
                  : '--'
        },
        { label: '单位', value: detail?.unit || '--' },
        { label: '相别数量', value: waveData?.iPhasic ?? '--' },
        { label: 'PT / CT', value: `${formatNumber(waveData?.pt, 2)} / ${formatNumber(waveData?.ct, 2)}` },
        { label: '数据类型', value: valueModeLabelMap[activeValueMode.value] }
    ]
})

const handleTrendToolAction = async (action: TrendToolAction) => {
    if (!hasWaveformData.value) return

    switch (action) {
        case 'x-zoom-in':
            zoomTrendXAxis(0.8)
            break
        case 'x-zoom-out':
            zoomTrendXAxis(1.25)
            break
        case 'y-zoom-in':
            trendYZoomScale.value = Math.max(trendYZoomScale.value * 0.8, 0.1)
            break
        case 'y-zoom-out':
            trendYZoomScale.value = Math.min(trendYZoomScale.value * 1.25, 10)
            break
        case 'box-zoom':
            activeTrendInteractionMode.value = activeTrendInteractionMode.value === 'box-zoom' ? 'none' : 'box-zoom'
            break
        case 'pan':
            if (!canPanTrendChart.value) {
                ElMessage.info('请先放大 X 轴或框选局部区域后再平移')
                activeTrendInteractionMode.value = 'none'
                break
            }
            activeTrendInteractionMode.value = activeTrendInteractionMode.value === 'pan' ? 'none' : 'pan'
            break
        case 'reset':
            resetTrendToolState()
            break
        case 'download-image':
            await downloadTrendImage()
            break
        case 'download-data':
            downloadTrendData()
            break
        default:
            break
    }
}

const handleTrendChartDataZoom = (value: TrendChartZoomPayload) => {
    trendXZoomRange.value = {
        start: clampPercent(value.start),
        end: clampPercent(value.end)
    }

    if (!canPanTrendChart.value && activeTrendInteractionMode.value === 'pan') {
        activeTrendInteractionMode.value = 'none'
    }
}

watch([activeTrendTab, activeValueMode, activeDisplayMode, activeChannelIndex], () => {
    resetTrendToolState()
})

const getFileBaseName = (fileName: string) => {
    return fileName.replace(/\.[^.]+$/, '').toLowerCase()
}

const resetSelectedWaveformFiles = () => {
    selectedCfgFile.value = null
    selectedDatFile.value = null
    waveformParseResult.value = null
    vectorParseResult.value = null
    lastParseErrorMessage.value = ''
    lastVectorParseErrorMessage.value = ''
}

const handleWaveformFileChange = async (event: Event) => {
    const input = event.target as HTMLInputElement
    const fileList = Array.from(input.files || [])

    if (!fileList.length) return

    const cfgFiles = fileList.filter(item => item.name.toLowerCase().endsWith('.cfg'))
    const datFiles = fileList.filter(item => item.name.toLowerCase().endsWith('.dat'))

    if (cfgFiles.length !== 1 || datFiles.length !== 1 || fileList.length !== 2) {
        resetSelectedWaveformFiles()
        ElMessage.warning('请选择一组同名的.cfg和.dat文件')
        return
    }

    const [cfgFile] = cfgFiles
    const [datFile] = datFiles

    if (!cfgFile || !datFile) {
        resetSelectedWaveformFiles()
        ElMessage.warning('请选择同一组.cfg和.dat文件')
        return
    }

    if (getFileBaseName(cfgFile.name) !== getFileBaseName(datFile.name)) {
        resetSelectedWaveformFiles()
        ElMessage.warning('请选择同名的.cfg和.dat文件')
        return
    }

    selectedCfgFile.value = cfgFile
    selectedDatFile.value = datFile

    await loadWaveformData(cfgFile, datFile)
}

const loadWaveformData = async (cfgFile: File, datFile: File) => {
    try {
        isParsing.value = true
        activeChannelIndex.value = 'all'
        resetTrendToolState()
        lastParseErrorMessage.value = ''
        lastVectorParseErrorMessage.value = ''

        // 波形与向量结果共用同一组文件，这里并行请求以缩短右侧联动展示等待时间。
        const [waveformResult, vectorResult] = await Promise.allSettled([
            parseComtradeApi({
                cfgFile,
                datFile
            }),
            parseComtradeVectorApi({
                cfgFile,
                datFile,
                parseType: 3
            })
        ])

        if (waveformResult.status === 'fulfilled') {
            waveformParseResult.value = waveformResult.value.data
        } else {
            waveformParseResult.value = null
            lastParseErrorMessage.value = getWaveformParseErrorMessage(waveformResult.reason)

            console.error('[waveform] parseComtrade failed', {
                cfgFileName: cfgFile.name,
                cfgFileSize: cfgFile.size,
                datFileName: datFile.name,
                datFileSize: datFile.size,
                error: waveformResult.reason
            })
        }

        if (vectorResult.status === 'fulfilled') {
            vectorParseResult.value = vectorResult.value.data
        } else {
            vectorParseResult.value = null
            lastVectorParseErrorMessage.value = getWaveformParseErrorMessage(vectorResult.reason)

            console.error('[waveform] parseComtradeVector failed', {
                cfgFileName: cfgFile.name,
                cfgFileSize: cfgFile.size,
                datFileName: datFile.name,
                datFileSize: datFile.size,
                error: vectorResult.reason
            })
        }
    } finally {
        isParsing.value = false
    }
}

const buildTrendExportFileName = (extension: string) => {
    const channelLabel = isAllChannelsActive.value
        ? '全部'
        : activeWaveDetail.value
          ? buildChannelLabel(activeWaveDetail.value, activeChannelIndex.value as number)
          : '波形'

    return `波形查看_${channelLabel}_${valueModeLabelMap[activeValueMode.value]}_${trendLabelMap[activeTrendTab.value]}.${extension}`
}

const downloadTrendImage = async () => {
    await nextTick()

    const targetElement = document.querySelector('.waveform-trend-export-target') as HTMLElement | null

    if (!targetElement) {
        ElMessage.warning('未找到可导出的趋势图区域')
        return
    }

    const canvas = await html2canvas(targetElement, {
        backgroundColor: '#ffffff',
        scale: window.devicePixelRatio || 1,
        useCORS: true
    })
    const imageUrl = canvas.toDataURL('image/png')
    const exportFile = document.createElement('a')

    exportFile.style.display = 'none'
    exportFile.download = buildTrendExportFileName('png')
    exportFile.href = imageUrl
    document.body.appendChild(exportFile)
    exportFile.click()
    document.body.removeChild(exportFile)

    ElMessage.success('趋势图图片下载成功')
}

const downloadTrendData = () => {
    if (!hasWaveformData.value) {
        ElMessage.warning('暂无可导出的波形数据')
        return
    }

    const trendPayload = activeTrendPayload.value
    const allChannelPayloads = normalizedWaveDetails.value
        .map((detail, index) => ({
            label: buildChannelLabel(detail, index),
            payload: buildTrendPayload(detail, activeTrendTab.value, getValueScale(detail))
        }))
        .filter(item => item.payload.series.length > 0)
    const exportPayload = isAllChannelsActive.value ? allChannelPayloads[0]?.payload : trendPayload

    if (!exportPayload) {
        ElMessage.warning('暂无可导出的波形数据')
        return
    }

    const header = ['时间', ...trendPayload.series.map(item => item.name)]
    const allChannelHeader = [
        '时间',
        ...allChannelPayloads.flatMap(item => item.payload.series.map(series => `${item.label}-${series.name}`))
    ]
    const rows = exportPayload.timeLabels.map((time, index) => {
        if (isAllChannelsActive.value) {
            return [
                time,
                ...allChannelPayloads.flatMap(item => item.payload.series.map(series => series.data[index] ?? ''))
            ]
        }

        return [time, ...trendPayload.series.map(item => item.data[index] ?? '')]
    })

    const csvContent = [isAllChannelsActive.value ? allChannelHeader : header, ...rows]
        .map(row => row.join(','))
        .join('\n')
    const blob = new Blob([`\uFEFF${csvContent}`], { type: 'text/csv;charset=utf-8;' })
    const blobUrl = URL.createObjectURL(blob)
    const exportFile = document.createElement('a')
    const fileName = buildTrendExportFileName('csv')

    exportFile.style.display = 'none'
    exportFile.download = fileName
    exportFile.href = blobUrl
    document.body.appendChild(exportFile)
    exportFile.click()
    document.body.removeChild(exportFile)
    URL.revokeObjectURL(blobUrl)

    ElMessage.success('趋势图数据下载成功')
}
</script>

<style scoped lang="scss">
.waveform-page {
    gap: 12px;
    overflow: hidden;
}

.waveform-layout {
    display: grid;
    grid-template-columns: minmax(0, 1.5fr) minmax(300px, 0.72fr);
    gap: 16px;
    width: 100%;
    height: 100%;
    overflow: hidden;
}

@media (max-width: 1280px) {
    .waveform-layout {
        grid-template-columns: 1fr;
    }
}
</style>
