<template>
  <div>
    <div ref="chart" class="waterfall-chart" id="waterfall-chart"></div>
    <div class="control-panel">
      <!-- 频率范围 -->
      <div class="box full-row">
        <div class="panel-block">
          <span class="label1">坐标轴</span>
          <el-select
            v-model="axisMode"
            size="mini"
            style="width: 120px"
            @change="handleAxisModeChange"
          >
            <el-option label="频率(Hz)" value="hz" />
            <el-option label="阶次(Order)" value="order" />
          </el-select>
          <span class="label1">{{ axisModeLabel }}</span>
          <el-input v-model="freqMin" size="mini" placeholder="下限" />
          <span>~</span>
          <el-input v-model="freqMax" size="mini" placeholder="上限" />
          <el-button size="mini" type="primary" @click="handleFreqRange">
            应用
          </el-button>
        </div>
        <!-- 手动标注 -->
        <div class="panel-block">
          <span class="label1">标注</span>
          <el-input v-model="manualFreq" size="mini" placeholder="频率/阶次" />
          <el-input v-model="multiple" size="mini" placeholder="倍频" />
          <el-button size="mini" type="success" @click="handleMark"
            >标注</el-button
          >
          <el-button size="mini" type="info" @click="removeMark"
            >清除</el-button
          >
        </div>
        <p class="hint-text">
          提示：点击某一采样时刻对应的谱线可高亮该时间（红色）；再点同一条谱线取消高亮。
        </p>
      </div>
    </div>
  </div>
</template>

<script>
import Plotly from "plotly.js-dist-min";

export default {
  name: "Waterfall3D",

  props: {
    // 数据格式：[freq, amp, time]
    data: {
      type: Array,
      default: () => [],
    },
  },
  data() {
    return {
      freqMin: "",
      freqMax: "",
      manualFreq: "",
      multiple: 1,
      axisMode: "hz", // hz | order

      freqRange: null, // 当前频率范围
      markLines: [], // 标注线
      /** 点击高亮的采样时间（时间戳 ms），null 表示未选中 */
      highlightedTime: null,
    };
  },
  computed: {
    axisModeLabel() {
      return this.axisMode === "order" ? "阶次" : "频率";
    },
  },

  watch: {
    data: {
      deep: true,
      immediate: true,
      handler() {
        this.highlightedTime = null;
        this.$nextTick(() => {
          this.renderChart();
        });
      },
    },
  },

  beforeDestroy() {
    const el = this.$refs.chart;
    if (el && typeof el.removeAllListeners === "function") {
      el.removeAllListeners("plotly_click");
      el.removeAllListeners("plotly_relayout");
    }
    if (el && typeof Plotly.purge === "function") {
      Plotly.purge(el);
    }
  },

  methods: {
    calcOrder(freq, rpm) {
      const f = Number(freq);
      const r = Number(rpm);
      if (!Number.isFinite(f) || !Number.isFinite(r) || r <= 0) return null;
      return (60 * f) / r;
    },
    toAxisValue(freq, rpm) {
      if (this.axisMode === "order") {
        return this.calcOrder(freq, rpm);
      }
      const f = Number(freq);
      return Number.isFinite(f) ? f : null;
    },
    handleAxisModeChange() {
      // 单位切换后，旧范围/标注的单位语义失效，清空避免误读
      this.freqRange = null;
      this.freqMin = "";
      this.freqMax = "";
      this.markLines = [];
      this.manualFreq = "";
      this.multiple = 1;
      this.highlightedTime = null;
      this.renderChart();
    },
    /** ✅ 时间解析（核心修复） */
    parseTime(time) {
      if (typeof time === "number") {
        return time < 1e12 ? time * 1000 : time;
      }
      if (typeof time === "string") {
        return new Date(time.replace(/-/g, "/")).getTime();
      }
      return new Date(time).getTime();
    },

    /** ✅ 时间格式化 */
    formatTime(t) {
      const d = new Date(t);
      const pad = (n) => String(n).padStart(2, "0");

      return `${d.getFullYear()}/${pad(d.getMonth() + 1)}/${pad(
        d.getDate(),
      )} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
    },

    /** ✅ 轴刻度：仅显示 月/日 时:分 */
    formatTimeTick(t) {
      const d = new Date(t);
      const pad = (n) => String(n).padStart(2, "0");
      const md = `${pad(d.getMonth() + 1)}/${pad(d.getDate())}`;
      const hh = pad(d.getHours());
      const mm = pad(d.getMinutes());
      // 00:00 不展示时间（避免“0点”刷屏）
      if (hh === "00" && mm === "00") return md;
      return `${md} ${hh}:${mm}`;
    },
    renderChart() {
      if (!this.data.length) return;

      /** =========================
   * 1. 分组（时间 + Hz）
   ========================== */
      const groupMap = new Map();

      this.data.forEach(([freq, amp, time, Hz, RPM]) => {
        const t = this.parseTime(time);
        const key = `${t}_${Hz}`;

        if (!groupMap.has(key)) {
          groupMap.set(key, {
            time: t,
            Hz,
            points: [],
          });
        }

        groupMap.get(key).points.push([freq, amp, RPM]);
      });

      /** =========================
   * 2. 转数组 + 排序
   ========================== */
      const groups = [...groupMap.values()];
      //   .sort((a, b) => a.time - b.time);

      if (
        this.highlightedTime != null &&
        !groups.some((g) => g.time === this.highlightedTime)
      ) {
        this.highlightedTime = null;
      }

      /** =========================
   * 3. 同时间分组（关键！！！）
   ========================== */
      const timeGroupMap = new Map();

      groups.forEach((g) => {
        if (!timeGroupMap.has(g.time)) {
          timeGroupMap.set(g.time, []);
        }
        timeGroupMap.get(g.time).push(g);
      });

      /** =========================
   * 4. traces
   ========================== */
      const traces = [];
      let maxAmp = 0;

      // ✅ 时间轴等间隔：用索引轴 + ticktext（避免时间戳间隔不均导致视觉“挤/拉”）
      const uniqueTimes = Array.from(new Set(groups.map((g) => g.time))).sort(
        (a, b) => a - b,
      );
      const timeToIndex = new Map(uniqueTimes.map((t, i) => [t, i]));

      groups.forEach((group, globalIndex) => {
        const { time, Hz, points } = group;

        const sameTimeGroups = timeGroupMap.get(time);
        const index = sameTimeGroups.indexOf(group);
        const total = sameTimeGroups.length;

        // 同一时间点若存在多组（不同采样频率 Hz），用极小偏移避免完全重叠（不影响刻度显示）
        const offsetX = total > 1 ? (index - (total - 1) / 2) * 0.08 : 0;
        const xBase = timeToIndex.get(time) ?? 0;

        const x = [];
        const y = [];
        const z = [];
        const text = [];
        const lineColor = [];

        points.sort((a, b) => a[0] - b[0]);

        points.forEach(([freq, amp, rpm]) => {
          const axisY = this.toAxisValue(freq, rpm);
          if (axisY == null) return;
          if (this.freqRange) {
            if (axisY < this.freqRange.min || axisY > this.freqRange.max)
              return;
          }

          // 轴映射：X=采样时间（等间隔），Y=频率(Hz)，Z=幅值
          x.push(xBase + offsetX);
          y.push(axisY);
          z.push(amp);
          if (Number.isFinite(amp)) {
            maxAmp = Math.max(maxAmp, Number(amp));
          }
          lineColor.push(amp);

          const order = this.calcOrder(freq, rpm);
          text.push(
            `时间: ${this.formatTime(time)}<br>` +
              `采样频率: ${Hz} Hz<br>` +
              `频率: ${freq.toFixed(2)} Hz<br>` +
              `阶次: ${order == null ? "-" : order.toFixed(3)}<br>` +
              `加速度(m/s^2): ${amp.toFixed(3)}`,
          );
        });

        traces.push({
          x,
          y,
          z,
          text,
          type: "scatter3d",
          mode: "lines",
          opacity: 0.78,
          line:
            this.highlightedTime != null && time === this.highlightedTime
              ? {
                  width: 3,
                  color: "#ff0000",
                }
              : {
                  width: 2,
                  color: lineColor,
                  colorscale: "Viridis",
                  cauto: true,
                },
          showlegend: false,
          hovertemplate: "%{text}<extra></extra>",
        });
      });

      this._dataTraceCount = groups.length;
      this._traceIndexToTime = groups.map((g) => g.time);

      /** =========================
   * 5. 采样时间刻度（仅 月/日 时:分）
   ========================== */
      const tickvals = uniqueTimes.map((_, i) => i);
      const ticktext = uniqueTimes.map((t) => this.formatTimeTick(t));
      // 仅用于 z 轴标签：去掉 0，避免原点出现两个“0”标签
      const zTickVals =
        maxAmp > 0
          ? Array.from({ length: 5 }, (_, i) => ((i + 1) * maxAmp) / 5)
          : [];

      /** =========================
   * 6. 标注线
   ========================== */
      if (this.markLines.length) {
        const minX = 0;
        const maxX = Math.max(0, uniqueTimes.length - 1);

        this.markLines.forEach((line) => {
          traces.push({
            x: [minX, maxX],
            y: [line.freq, line.freq],
            z: [0, 0],
            type: "scatter3d",
            mode: "lines+text",
            line: {
              color: "#ff0000",
              width: 4,
              dash: "dash",
            },
            text: ["", line.label],
            textposition: "top right",
            showlegend: false,
            hovertemplate: `${line.label}<extra></extra>`,
          });
        });
      }

      /** =========================
   * 7. layout
   ========================== */
      const layout = {
        // title: "3D 瀑布频谱图",
        paper_bgcolor: "#ffffff",
        plot_bgcolor: "#ffffff",
        margin: { l: 0, r: 0, b: 10, t: 26 },

        scene: {
          xaxis: {
            // title: "采样时间",
            title: "",
            tickvals,
            ticktext,
            tickangle: 45,
            gridcolor: "#e5e5e5",
            zeroline: false,
            showspikes: false,
            ticks: "outside",
            tickfont: { size: 10, color: "#606266" },
            titlefont: { size: 10, color: "#606266" },
            backgroundcolor: "#ffffff",
            showbackground: true,
            showline: true,
            // linecolor: "#dcdfe6",
            linecolor: "#e5e5e5",
            linewidth: 1,
          },
          yaxis: {
            title: this.axisMode === "order" ? "阶次 (Order)" : "频率 (Hz)",
            gridcolor: "#e5e5e5",
            zeroline: false,
            showspikes: false,
            ticks: "outside",
            tickfont: { size: 9, color: "#606266" },
            titlefont: { size: 10, color: "#606266" },
            backgroundcolor: "#fafafa",
            showbackground: true,
            showline: true,
            // linecolor: "#dcdfe6",
            linecolor: "#e5e5e5",
            linewidth: 1,
            // tickangle: 90,
          },
          zaxis: {
            title: "",
            gridcolor: "#e5e5e5",
            zeroline: false,
            showspikes: false,
            ticks: "outside",
            tickangle: 90,
            tickmode: "array",
            tickvals: zTickVals,
            tickformat: ".2f",
            tickfont: { size: 10, color: "#606266" },
            titlefont: {
              size: 10,
              color: "#606266",
            },
            backgroundcolor: "#fbfbfb",
            showbackground: true,
            showline: true,
            // linecolor: "#dcdfe6",
            linecolor: "#e5e5e5",
            linewidth: 1,
          },
          aspectmode: "manual",
          // X 采用等间隔索引后，整体更均衡
          aspectratio: { x: 2.4, y: 3.2, z: 1.1 },
          //   annotations: [
          //     {
          //       text: "加速度(m/s^2)",
          //       x: 0.92, // 右上角位置可微调
          //       y: 0.98,
          //       xref: "paper",
          //       yref: "paper",
          //       showarrow: false,
          //       font: { size: 10, color: "#606266" },
          //       align: "right",
          //     },
          //   ],
          camera: {
            up: { x: 0, y: 0, z: 1 },
            center: { x: 0, y: 0, z: 0 },
            // 让时间轴视觉更贴近底边
            // eye: { x: 2.6, y: 1.2, z: 0.8 },
            eye: {
              x: 2.8547815750982184,
              y: 0.3847735823828088,
              z: 0.7363229242526919,
            },
            projection: {
              type: "orthographic",
            },
          },
        },
      };

      /** =========================
   * 8. 渲染（先卸监听再 react，避免重绘过程中 plotly_click 重入 renderChart 栈溢出）
   ========================== */
      const plotEl = this.$refs.chart;
      if (!plotEl) return;

      if (typeof plotEl.removeAllListeners === "function") {
        plotEl.removeAllListeners("plotly_relayout");
        plotEl.removeAllListeners("plotly_click");
      }

      const plotOpts = {
        responsive: true,
        displayModeBar: true,
        displaylogo: false,
        modeBarButtonsToRemove: [
          "zoom3d",
          "pan3d",
          "orbitRotation",
          "tableRotation",
          "resetCameraDefault3d",
          "resetCameraLastSave3d",
          "hoverClosest3d",
        ],
      };

      const done = Plotly.react(plotEl, traces, layout, plotOpts);
      const bind = () => this._bindPlotlyEvents();
      if (done && typeof done.then === "function") {
        done.then(bind).catch(bind);
      } else {
        this.$nextTick(bind);
      }
    },

    _bindPlotlyEvents() {
      const plotElement = this.$refs.chart;
      if (!plotElement || typeof plotElement.on !== "function") return;

      plotElement.on("plotly_relayout", (eventData) => {
        if (eventData && eventData["scene.camera"]) {
          console.log("当前相机视角:", eventData["scene.camera"]);
        }
      });
      plotElement.on("plotly_click", (ev) => {
        const pt = ev.points && ev.points[0];
        if (!pt || pt.curveNumber == null) return;
        const n = this._dataTraceCount || 0;
        if (pt.curveNumber >= n) return;
        const t =
          this._traceIndexToTime && this._traceIndexToTime[pt.curveNumber];
        if (t == null) return;
        this.highlightedTime = this.highlightedTime === t ? null : t;
        this.$nextTick(() => {
          this.renderChart();
        });
      });
    },
    removeMark() {
      this.markLines = [];
      this.manualFreq = "";
      this.multiple = 1;
      this.renderChart();
    },
    handleFreqRange() {
      const min = Number(this.freqMin);
      const max = Number(this.freqMax);

      if (isNaN(min) || isNaN(max)) return;

      this.freqRange = { min, max };

      this.renderChart();
    },
    handleMark() {
      const base = Number(this.manualFreq);
      const multi = Number(this.multiple) || 1;

      if (isNaN(base)) return;

      const newLines = [];

      for (let i = 1; i <= multi; i++) {
        const val = base * i;
        newLines.push({
          freq: val,
          label:
            this.axisMode === "order"
              ? `${i}x (${val}Order)`
              : `${i}x (${val}Hz)`,
        });
      }

      this.markLines = [...this.markLines, ...newLines];

      this.renderChart();
    },
  },
};
</script>

<style scoped>
.waterfall-chart {
  width: 100%;
  height: 520px;
}
.control-panel {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 12px;
  padding: 8px 12px;
  background: #f5f7fa;
  border: 1px solid #ddd;
  border-radius: 6px;
  margin-bottom: 10px;
}
/* 🌟 关键：独占一行 */
.full-row {
  width: 100%;
  .panel-block {
    width: 65%;
    margin: 5px 0;
  }
}
.hint-text {
  width: 100%;
  margin: 4px 0 0;
  font-size: 12px;
  color: #909399;
  line-height: 1.45;
}
.panel-block {
  display: flex;
  align-items: center;
  gap: 6px;
}

.label {
  font-size: 12px;
  color: #666;
}
.label1 {
  font-size: 12px;
  color: #666;
  /* 父级是 flex：仅写 width 会被 flex-shrink 压缩，需固定 flex 基准且不收缩 */
  flex: 0 0 40px;
  box-sizing: border-box;
}

.btn-group {
  display: flex;
  gap: 6px;
}

.btn {
  padding: 2px 8px;
  font-size: 12px;
  border: 1px solid #ccc;
  border-radius: 3px;
  cursor: pointer;
}

.btn.active {
  background: #409eff;
  color: #fff;
}
.full-width {
  width: 100%;
}

.el-cascader {
  font-size: 12px;
}

.el-cascader__tags {
  max-width: 240px;
  overflow: hidden;
}

.feature-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 10px;
  width: 100%;
}

.feature-item {
  display: flex;
  align-items: center;
  gap: 6px;
}

/* 勿用 flex:1，否则会拉伸子项，内联 width 无法按预期生效 */
.el-select {
  flex: 0 0 auto;
}
</style>
