<template>
  <echart-base :options="[options]" :empty="isEmpty" v-model:loading="loading"></echart-base>
</template>

<script lang="ts">
import { defineComponent, PropType, ref, watch, watchEffect } from 'vue';
import { QueryHistoryDatasRsp, QueryQuoteDayRsp, QueryHistoryDatas, CycleType } from '@/services/go/quote/interface';
import { QueryHistoryDatas as queryHistoryDatas } from '@/services/go/quote';
import { debounce } from '@/utils/time';
import EchartBase from '../echart-base/index.vue';
import { handleEchart, Source } from './setup';
import moment from 'moment';

export default defineComponent({
    name: 'EchartKline',
    components: {
        EchartBase,
    },
    props: {
        // 实时行情数据
        quoteData: {
            type: Object as PropType<QueryQuoteDayRsp>,
            required: true,
        },
        // 周期类型
        cycleType: {
            type: Number as PropType<CycleType>,
            required: true,
        },
        // 指标类型
        seriesType: {
            type: String,
            default: 'MACD',
        },
    },
    setup(props) {
        const loading = ref(true);
        const isEmpty = ref(false);
        const historyIndexs: number[] = []; // 行情历史数据中所有非补充数据的索引位置(用于计算均线)
        const { chartData, options, updateOptions, initOptions } = handleEchart();

        // 处理图表数据
        const handleData = (rawData: QueryHistoryDatasRsp[]): void => {
            const { source } = chartData.value;
            source.length = 0;
            historyIndexs.length = 0;

            rawData.forEach((item, index) => {
                const { o, c, h, l, ts, tv } = item;
                source.push({
                    date: moment(ts).format('YYYY-MM-DD HH:mm:ss'),
                    open: o,
                    close: c,
                    lowest: l,
                    highest: h,
                    ma5: '-',
                    ma10: '-',
                    ma15: '-',
                    vol: tv,
                    macd: '-',
                    dif: '-',
                    dea: '-',
                    k: '-',
                    d: '-',
                    j: '-',
                    cci: '-',
                });
                if (!item.f) historyIndexs.push(index); // 排除补充数据
            });

            calcMA('ma5', 5);
            calcMA('ma10', 10);
            calcMA('ma15', 15);
            calcMACD();
            calcKDJ();
            clacCCI();
        };

        // 计算平均线
        const calcMA = (key: keyof Source, count: number) => {
            const { source } = chartData.value;
            let result: Source[keyof Source] = '-';

            if (source.length >= count) {
                // 均线起始位置
                const startIndex = historyIndexs[count - 1];
                for (let i = 0; i < source.length; i++) {
                    if (startIndex !== undefined && i > startIndex) {
                        const j = historyIndexs.findIndex((val) => val === i);
                        // 判断是否补充数据
                        if (j === -1) {
                            result = source[i - 1][key]; // 取上个平均值
                        } else {
                            // 向后取MA数
                            const maIndexs = historyIndexs.slice(j - (count - 1), j + 1);
                            // 计算总价
                            const total = maIndexs.reduce((sum, val) => sum + source[val].close, 0);
                            // 计算均线
                            result = (total / count).toFixed(2);
                        }
                    }
                    (<typeof result>source[i][key]) = result;
                }
            }
        };

        // 计算EMA
        const calcEMA = (close: number[], n: number) => {
            const ema: number[] = [],
                a = 2 / (n + 1); // 平滑系数
            for (let i = 0; i < close.length; i++) {
                if (i === 0) {
                    //第一个EMA(n)是前n个收盘价代数平均
                    const result = close.slice(0, n).reduce((sum, val) => sum + val, 0) / n;
                    ema.push(result);
                } else {
                    // EMA(n) = α × Close + (1 - α) × EMA(n - 1)
                    const result = a * close[i] + (1 - a) * ema[i - 1];
                    ema.push(result);
                }
            }
            return ema;
        };

        // 计算DEA
        const calcDEA = (dif: number[]) => {
            return calcEMA(dif, 9);
        };

        // 计算DIF
        const calcDIF = (close: number[]) => {
            const dif: number[] = [],
                emaShort = calcEMA(close, 12),
                emaLong = calcEMA(close, 26);

            for (let i = 0; i < close.length; i++) {
                const result = emaShort[i] - emaLong[i];
                dif.push(result);
            }
            return dif;
        };

        // 计算MACD
        const calcMACD = () => {
            const { source } = chartData.value,
                close = source.map((item) => item.close),
                dif = calcDIF(close),
                dea = calcDEA(dif);

            for (let i = 0; i < source.length; i++) {
                source[i].dif = dif[i].toFixed(2);
                source[i].dea = dea[i].toFixed(2);
                source[i].macd = ((dif[i] - dea[i]) * 2).toFixed(2);
            }
        };

        // 计算KDJ
        const calcKDJ = () => {
            const { source } = chartData.value;
            for (let i = 0; i < source.length; i++) {
                const item = source[i];
                if (i < 8) {
                    item.k = '-';
                    item.d = '-';
                    item.j = '-';
                } else {
                    let rsv = 50; // 如果最低价等于最高价，RSV默认值为50
                    if (item.lowest !== item.highest) {
                        const n9 = source.slice(i - 8, i + 1).map((item) => item.close), // 取前9个收盘价
                            max = Math.max(...n9),
                            min = Math.min(...n9);
                        // 计算RSV
                        rsv = ((item.close - min) / (max - min)) * 100;
                    }

                    const yestK = Number(source[i - 1].k); // 取前一日K值
                    const yestD = Number(source[i - 1].d); // 取前一日D值

                    if (isNaN(yestK) || isNaN(yestD)) {
                        // 如果前一日的K值或D值不存在则默认值为50
                        item.k = '50';
                        item.d = '50';
                        item.j = '50';
                    } else {
                        const k = (2 / 3) * yestK + (1 / 3) * rsv,
                            d = (2 / 3) * yestD + (1 / 3) * yestK,
                            j = 3 * k - 2 * d;

                        item.k = k.toFixed(2);
                        item.d = d.toFixed(2);
                        item.j = j.toFixed(2);
                    }
                }
            }
        };

        // 计算CCI
        const clacCCI = () => {
            const { source } = chartData.value;
            for (let i = 0; i < source.length; i++) {
                const item = source[i];
                if (i < 13) {
                    item.cci = '-';
                } else {
                    const tp = (item.close + item.lowest + item.highest) / 3, // (收盘价 + 最低价 + 最高价) / 3
                        n14 = source.slice(i - 13, i + 1), // 取前14条数据
                        ma = n14.reduce((sum, e) => sum + (e.close + e.lowest + e.highest) / 3, 0) / 14, // 计算前14条数据的(TP)价总和÷N
                        md = n14.reduce((sum, e) => sum + Math.abs(ma - (e.close + e.lowest + e.highest) / 3), 0) / 14, // 计算前14条数据的(MA-TP)价总和÷N
                        result = (tp - ma) / md / 0.015;

                    item.cci = result.toFixed(2);
                }
            }
        };

        // 更新图表K线数据
        const updateChartData = () => {
            const { source } = chartData.value,
                lastIndex = source.length - 1, // 历史行情最后索引位置
                lastTime = moment(source[lastIndex].date), // 历史行情最后时间
                newTime = moment(props.quoteData.lasttime), // 实时行情最新时间
                newPrice = props.quoteData.last; // 实时行情最新价

            let cycleMilliseconds = 60 * 1000; // 周期毫秒数

            switch (props.cycleType) {
                case CycleType.minutes5:
                    cycleMilliseconds *= 5;
                    break;
                case CycleType.minutes30:
                    cycleMilliseconds *= 30;
                    break;
                case CycleType.minutes60:
                    cycleMilliseconds *= 60;
                    break;
                case CycleType.hours2:
                    cycleMilliseconds *= 2 * 60;
                    break;
                case CycleType.Hours4:
                    cycleMilliseconds *= 4 * 60;
                    break;
                case CycleType.days:
                    cycleMilliseconds *= 24 * 60;
                    break;
            }

            const diffTime = newTime.valueOf() - lastTime.valueOf(); // 计算时间差

            if (diffTime > cycleMilliseconds * 2) {
                // 时间间隔超过两个周期，重新请求历史数据
            } else {
                // 判断时间差是否大于周期时间
                if (diffTime > cycleMilliseconds) {
                    lastTime.add(cycleMilliseconds, 'ms');
                    // 添加历史行情
                    source.push({
                        date: lastTime.format('YYYY-MM-DD HH:mm:ss'),
                        open: newPrice,
                        close: newPrice,
                        lowest: newPrice,
                        highest: newPrice,
                        ma5: '-',
                        ma10: '-',
                        ma15: '-',
                        vol: 0,
                        macd: '-',
                        dif: '-',
                        dea: '-',
                        k: '-',
                        d: '-',
                        j: '-',
                        cci: '-',
                    });
                    historyIndexs.push(lastIndex + 1); // 添加历史行情索引
                } else {
                    const lastData = source[lastIndex];
                    if (lastData.lowest > newPrice) {
                        lastData.lowest = newPrice; //更新最低价
                    }
                    if (lastData.highest < newPrice) {
                        lastData.highest = newPrice; //更新最高价
                    }
                    lastData.close = newPrice; //更新收盘价
                }

                calcMA('ma5', 5);
                calcMA('ma10', 10);
                calcMA('ma15', 15);
                calcMACD();
                calcKDJ();
                clacCCI();

                // 延迟图表更新，减少卡顿
                debounce(() => {
                    updateOptions(props.seriesType);
                }, 1000);
            }
        };

        // 监听行情最新价推送
        watch(
            () => props.quoteData.last,
            () => {
                if (!loading.value) {
                    updateChartData();
                }
            }
        );

        // 监听指标类型
        watch(
            () => props.seriesType,
            (val) => {
                if (!loading.value) {
                    updateOptions(val);
                }
            }
        );

        // 监听周期选择变化
        watchEffect(() => {
            loading.value = true;
            const params: QueryHistoryDatas = {
                cycleType: props.cycleType,
                goodsCode: props.quoteData.goodscode.toUpperCase(),
                count: 1440,
            };
            // 查询K线数据
            queryHistoryDatas(params)
                .then((res) => {
                    if (res.length) {
                        isEmpty.value = false;
                        // 日期升序排序
                        const kdata = res.sort((a, b) => moment(a.ts).valueOf() - moment(b.ts).valueOf());
                        handleData(kdata);
                    } else {
                        isEmpty.value = true;
                    }
                    initOptions(props.seriesType);
                })
                .catch(() => {
                    isEmpty.value = true;
                })
                .finally(() => {
                    loading.value = false;
                });
        });

        return {
            loading,
            isEmpty,
            options,
        };
    },
});
</script>