﻿<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"
            :has-waveform-data="hasWaveformData"
            @update:active-channel-index="activeChannelIndex = $event"
            @update:active-display-mode="activeDisplayMode = $event"
            @update:active-value-mode="activeValueMode = $event"
            @waveform-file-change="handleWaveformFileChange"
            @download="downloadTrendData"
        />

        <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"
                :last-parse-error-message="lastParseErrorMessage"
                @update:active-trend-tab="activeTrendTab = $event"
            />

            <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, ref } from 'vue'
import dayjs from 'dayjs'
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 {
    DisplayMode,
    LabelValueOption,
    SingleChannelTrendOption,
    SummaryItem,
    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
}

const activeTrendTab = ref<TrendTabValue>('instant')
const activeValueMode = ref<ValueMode>('primary')
const activeDisplayMode = ref<DisplayMode>('single-channel')
const activeChannelIndex = ref(0)
const singleChannelTrendChartGroup = 'waveform-single-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 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[]>(() => {
    return normalizedWaveDetails.value.map((item, index) => ({
        label: buildChannelLabel(item, index),
        value: index
    }))
})

const activeWaveDetail = computed(() => {
    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 activeValueScale = computed(() => {
    if (activeValueMode.value === 'secondary') return 1

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

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 currentTrendColors = computed(() => {
    const customColors = activeWaveDetail.value?.colors?.filter(Boolean) || []
    return customColors.length >= 3 ? customColors.slice(0, 3) : defaultPhaseColors
})

const hasWaveformData = computed(() => activeTrendPayload.value.series.length > 0)

const SYMMETRIC_AXIS_SPLIT_COUNT = 6
const REGULAR_AXIS_SPLIT_COUNT = 5

const getAxisPrecision = (step: number) => {
    if (!Number.isFinite(step) || step >= 1) return 0

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

    return stepText.split('.')[1]?.length || 0
}

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 getNiceAxisInterval = (value: number) => {
    if (!Number.isFinite(value) || value <= 0) return 1

    const exponent = Math.floor(Math.log10(value))
    const magnitude = 10 ** exponent
    const normalized = value / magnitude
    const candidates = [1, 2, 2.5, 5, 10]
    const closestCandidate = candidates.reduce((currentBest, currentValue) => {
        return Math.abs(currentValue - normalized) < Math.abs(currentBest - normalized) ? currentValue : currentBest
    }, candidates[0])

    return closestCandidate * magnitude
}

const roundAxisBoundary = (value: number, interval: number, isMin: boolean) => {
    const precision = getAxisPrecision(interval)
    const stepCount = isMin ? Math.floor(value / interval) : Math.ceil(value / interval)
    return normalizeAxisValue(stepCount * interval, precision)
}

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

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

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

    const crossesZero = min < 0 && max > 0
    const splitCount = crossesZero ? SYMMETRIC_AXIS_SPLIT_COUNT : REGULAR_AXIS_SPLIT_COUNT
    const baseValue = crossesZero ? Math.max(Math.abs(min), Math.abs(max)) : max - min
    const interval = getNiceAxisInterval(baseValue / (crossesZero ? splitCount / 2 : splitCount))
    const precision = getAxisPrecision(interval)

    let axisMin = roundAxisBoundary(min, interval, true)
    let axisMax = roundAxisBoundary(max, interval, false)

    if (crossesZero) {
        const axisBound = Math.max(Math.abs(axisMin), Math.abs(axisMax))
        axisMin = normalizeAxisValue(-axisBound, precision)
        axisMax = normalizeAxisValue(axisBound, precision)
    }

    if (axisMin === axisMax) {
        axisMin = normalizeAxisValue(axisMin - interval, precision)
        axisMax = normalizeAxisValue(axisMax + interval, precision)
    }

    return {
        name: trendPayload.unit || '',
        min: axisMin,
        max: axisMax,
        interval,
        splitNumber: crossesZero ? SYMMETRIC_AXIS_SPLIT_COUNT : Math.max(Math.round((axisMax - axisMin) / interval), 1),
        minInterval: interval,
        maxInterval: interval,
        // 跨零波形按 0 对称出刻度，避免边界出现 164 / -165 这类不均匀标签。
        axisLabel: {
            hideOverlap: true,
            formatter: (value: number) => formatAxisLabel(value, precision)
        }
    }
}
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)
    }
}

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

    return {
        tooltip: {
            trigger: 'axis',
            axisPointer: {
                type: 'line',
                snap: true,
                lineStyle: {
                    color: 'rgba(24, 144, 255, 0.55)',
                    width: 1
                }
            },
            valueFormatter: (value: number) => {
                return trendPayload.unit ? `${formatNumber(value)} ${trendPayload.unit}` : formatNumber(value)
            }
        },
        legend: {
            top: 0,
            right: 12
        },
        grid: {
            top: '18px',
            left: '24px',
            right: showTimeAxis ? '32px' : '24px',
            bottom: showTimeAxis ? '16px' : '6px'
        },
        xAxis: {
            data: trendPayload.timeLabels,
            boundaryGap: false,
            // 仅最后一张图显示时间轴，前两张图不再额外预留占位，尽量放大趋势内容区。
            name: showTimeAxis ? 'ms' : '',
            nameLocation: 'end',
            nameGap: showTimeAxis ? 10 : 0,
            nameTextStyle: {
                color: axisTextColor
            },
            axisLine: {
                show: showTimeAxis,
                // 时间轴固定在图底部，避免 y 轴包含 0 时横轴穿过波形区域显得过粗。
                onZero: false,
                lineStyle: {
                    color: axisLineColor
                }
            },
            axisLabel: {
                show: showTimeAxis,
                hideOverlap: false,
                interval: 0,
                margin: showTimeAxis ? 9 : 0,
                color: axisTextColor,
                formatter: buildTimeAxisLabelFormatter(trendPayload.timeLabels)
            },
            axisTick: {
                show: showTimeAxis,
                lineStyle: {
                    color: axisLineColor
                }
            }
        },
        yAxis: buildTrendAxisConfig(trendPayload),
        dataZoom: [],
        color: chartColors,
        series: seriesList.map(item => ({
            name: item.name,
            type: 'line',
            smooth: true,
            symbol: 'none',
            data: item.data
        }))
    }
}

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

// 单通道模式按相别拆成多个图，便于分别观察各通道波形。
const singleChannelTrendOptionsList = computed<SingleChannelTrendOption[]>(() => {
    const trendPayload = activeTrendPayload.value

    // 单通道下每张图只保留一个 series，需要单独指定对应相色。
    return trendPayload.series.map((item, index) => ({
        key: item.name,
        group: singleChannelTrendChartGroup,
        isLastChart: index === trendPayload.series.length - 1,
        options: buildTrendChartOptions(
            trendPayload,
            [item],
            [currentTrendColors.value[index] || defaultPhaseColors[index]],
            {
                // 仅最后一张图显示时间轴，并通过卡片高度微调补偿其额外占用空间。
                showTimeAxis: index === trendPayload.series.length - 1
            }
        )
    }))
})

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: detail ? buildChannelLabel(detail, activeChannelIndex.value) : '--' },
        { 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 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 = 0
        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 downloadTrendData = () => {
    if (!hasWaveformData.value) {
        ElMessage.warning('暂无可导出的波形数据')
        return
    }

    const trendPayload = activeTrendPayload.value
    const header = ['时间', ...trendPayload.series.map(item => item.name)]
    const rows = trendPayload.timeLabels.map((time, index) => {
        return [time, ...trendPayload.series.map(item => item.data[index] ?? '')]
    })

    const csvContent = [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 channelLabel = activeWaveDetail.value ? buildChannelLabel(activeWaveDetail.value, activeChannelIndex.value) : '波形'
    const fileName = `波形查看_${channelLabel}_${valueModeLabelMap[activeValueMode.value]}_${trendLabelMap[activeTrendTab.value]}.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>

