<!-- eslint-disable vue/no-v-html -->
<template>
  <div class="matching-preview">
    <div class="stem">
      <span class="question-number">{{ data.property.question_number }}.</span>
      <span v-html="sanitizeHTML(data.stem)"></span>
    </div>
    <div v-if="isEnable(data.property.is_enable_description)" class="description">{{ data.description }}</div>

    <ul ref="list" class="option-list">
      <li v-for="(item, i) in optionList" :key="i" class="list-item">
        <div v-for="({ content, mark }, j) in item" :key="mark" class="item-wrapper">
          <span
            v-if="j > 0 && j < item.length"
            :class="['connection', `pre-line-${i}-${j}`]"
            @mousedown="mousedown($event, i, j, 'pre', mark)"
            @mouseup="mouseup($event, i, j, 'pre', mark)"
            @click="handleClickConnection($event, i, j, 'pre', mark)"
          ></span>
          <span class="content" v-html="sanitizeHTML(content)"></span>
          <span
            v-if="j < item.length - 1"
            :class="['connection', `next-line-${i}-${j}`]"
            @mousedown="mousedown($event, i, j, 'next', mark)"
            @mouseup="mouseup($event, i, j, 'next', mark)"
            @click="handleClickConnection($event, i, j, 'next', mark)"
          ></span>
        </div>
      </li>
    </ul>
  </div>
</template>

<script>
import { svgNS } from '@/views/exercise_questions/data/common';

import PreviewMixin from './components/PreviewMixin';

export default {
  name: 'MatchingPreview',
  mixins: [PreviewMixin],
  data() {
    return {
      answerList: [], // 答案列表
      curConnectionPoint: { i: -1, j: -1, position: 'pre', mark: '' }, // 当前连线点
      // 拖拽相关
      drag: false,
      mousePointer: { i: -1, j: -1, position: 'pre', mark: '' },
    };
  },
  computed: {
    optionList() {
      // 生成一个与选项列表相同的二维数组，但没有内容
      let arr = Array.from(Array(this.data.option_list.length), () => Array(this.data.option_list[0].length).fill({}));
      for (let i = 0; i < this.data.property.column_number; i++) {
        let rowNumList = Array.from(Array(this.data.option_list.length).keys()); // 行数列表
        for (let j = 0; j < this.data.option_list.length; j++) {
          let random = Math.floor(Math.random() * rowNumList.length); // 随机行数
          let item = this.data.option_list[j][i]; // 当前选项
          arr[rowNumList[random]][i] = item; // 将当前选项添加到随机行数的列中
          rowNumList.splice(random, 1); // 删除已经添加过选项的行数
        }
      }
      return arr;
    },
  },
  watch: {
    optionList: {
      handler(val) {
        this.clearLine();
        if (!val) return;
        let list = this.optionList.map((item) => {
          return item.map(({ mark }) => {
            return { mark, preMark: '', nextMark: '' };
          });
        });
        this.$set(this, 'answerList', list);
      },
      immediate: true,
    },
    answerList: {
      handler(list) {
        let arr = [];
        let column_number = this.data.property.column_number;
        // 从前往后遍历，如果有 nextMark，就往后找，直到没有 nextMark
        // 如果第一个选项的 nextMark 为空，则整个行都为空，等待后面的 preArr 填充
        list.forEach((item, i) => {
          let { mark, nextMark } = item[0];
          arr[i] = nextMark ? [mark, nextMark] : new Array(column_number).fill('');
          let _nextMark = nextMark;
          while (_nextMark) {
            let fMark = this.findMark(list, _nextMark, 'next');
            if (column_number > arr[i].length) {
              arr[i].push(fMark);
            }
            _nextMark = fMark;
          }
        });
        let emptyStringArray = []; // 无数据的列的下标数组
        arr.forEach((item, i) => {
          item.every((li) => li.length <= 0) ? emptyStringArray.push(i) : '';
        });
        if (column_number === 2) {
          this.answer.answer_list = arr;
          return;
        }

        // 从后向前遍历，如果有 preMark，就往前找，直到没有 preMark，并过滤掉无数据的列
        let preArr = [];
        list.forEach((item) => {
          let { mark, preMark } = item[column_number - 1];
          let _arr = new Array(column_number).fill('');
          let back = column_number - 1;
          let _preMark = preMark;
          while (_preMark) {
            _arr[back] = mark;
            back -= 1;
            _arr[back] = _preMark;
            back -= 1;
            let fMark = this.findMark(list, _preMark, 'pre');
            if (back >= 0 && fMark) {
              _arr[back] = fMark;
            }
            _preMark = fMark;
          }
          if (_arr.every((li) => li.length <= 0)) return;
          preArr.push(_arr);
        });
        // 将 preArr 中的数据填充到 arr 无数据的位置中
        if (preArr.length > 0 && emptyStringArray.length > 0) {
          preArr.forEach((item, i) => {
            arr[emptyStringArray[i]] = item;
          });
        }
        this.answer.answer_list = arr;
      },
      deep: true,
    },
  },
  mounted() {
    document.addEventListener('click', this.handleEventConnection);
    document.addEventListener('mousemove', this.documentMousemouse);
    document.addEventListener('mouseup', this.documentMouseup);
  },
  beforeDestroy() {
    document.removeEventListener('click', this.handleEventConnection);
    document.removeEventListener('mousemove', this.documentMousemouse);
    document.removeEventListener('mouseup', this.documentMouseup);
  },
  methods: {
    /* 用 mouse 事件模拟拖拽 开始*/
    documentMousemouse(e) {
      if (!this.drag) return;
      // 使用 svg 绘制跟随鼠标移动的连接线
      let svg = document.querySelector('.move-connection');
      if (!svg) {
        svg = document.createElementNS(svgNS, 'svg');
        svg.classList.add('move-connection');
        svg.setAttribute('style', `position: absolute; width: 100%; height: 100%;`);
        let path = document.createElementNS(svgNS, 'path');
        this.setPathAttr(path);
        svg.appendChild(path);
        this.$refs.list.appendChild(svg);
      }
      let { clientX, clientY } = e;
      let { i, j, position } = this.mousePointer;
      let dom = document.getElementsByClassName(`${position}-line-${i}-${j}`)[0].getBoundingClientRect(); // 连线点的位置
      let list = this.$refs.list.getBoundingClientRect(); // 列表的位置
      let top = dom.top - list.top + 5; // 连线点距离列表顶部的距离 + 5 是为了让线条在连线点的中间
      let left = dom.left - list.left + 5; // 连线点距离列表左边的距离 + 5 是为了让线条在连线点的中间
      let mouseX = clientX - list.left; // 鼠标距离列表左边的距离
      let mouseY = clientY - list.top; // 鼠标距离列表顶部的距离
      let path = svg.querySelector('path');
      path.setAttribute('d', `M ${left} ${top} L ${mouseX} ${mouseY}`);
    },
    documentMouseup() {
      if (!this.drag) return;
      this.drag = false;
      document.querySelector('.move-connection')?.remove();
      document.body.style.userSelect = 'auto'; // 允许选中文本
      this.mousePointer = { i: -1, j: -1, position: 'pre', mark: '' };
    },
    mousedown(e, i, j, position, mark) {
      this.drag = true;
      document.body.style.userSelect = 'none'; // 禁止选中文本
      this.mousePointer = { i, j, position, mark };
    },
    /**
     * 鼠标抬起事件，如果是一个合适的连接点，则创建连接线
     * @param {PointerEvent} e 事件对象
     * @param {Number} i 选项列表索引
     * @param {Number} j 选项索引
     * @param {'pre'|'next'} position 连线点位置
     * @param {String} mark 选项标识
     */
    mouseup(e, i, j, position, mark) {
      let { i: curI, j: curJ, position: curPosition, mark: curMark } = this.mousePointer;
      if (curI === -1 && curJ === -1) return;
      if (Math.abs(curJ - j) > 1 || position === curPosition || mark === curMark) return;
      this.changeConnectionList(mark, position, true);
      this.createLine(i, j, position, mark, true);
    },
    /* 用 mouse 事件模拟拖拽 结束 */

    resetCurConnectionPoint() {
      this.curConnectionPoint = { i: -1, j: -1, position: 'pre', mark: '' };
    },
    /**
     * 当点击的不是连线点时，清除所有连线点的选中状态
     * @param {PointerEvent} e
     */
    handleEventConnection(e) {
      if (e.target.classList.contains('connection')) return;
      Array.from(document.getElementsByClassName('connection')).forEach((item) => {
        item.classList.remove('focus');
      });
      this.resetCurConnectionPoint();
    },

    /**
     * 处理点击连线
     * @param {PointerEvent} e 事件对象
     * @param {Number} i 选项列表索引
     * @param {Number} j 选项索引
     * @param {'pre'|'next'} position
     * @param {String} mark 选项标识
     */
    handleClickConnection(e, i, j, position, mark) {
      let { i: curI, j: curJ, position: curPosition, mark: curMark } = this.curConnectionPoint;
      // 如果当前连线点不存在，则设置当前连线点
      if (curI === -1 && curJ === -1) {
        this.curConnectionPoint = { i, j, mark, position };
        e.target.classList.add('focus');
        return;
      }
      // 如果当前连线点存在，清除所有连线点的选中状态
      Array.from(document.getElementsByClassName('connection')).forEach((item) => {
        item.classList.remove('focus');
      });
      // 如果当前连线点与上一个连线点不在相邻的位置，则设置点击的连接点为当前连线点
      if (Math.abs(curJ - j) > 1 || position === curPosition || mark === curMark) {
        this.curConnectionPoint = { i, j, mark, position };
        e.target.classList.add('focus');
        return;
      }
      this.changeConnectionList(mark, position);
      // 如果当前连线点与上一个连线点在相邻的位置，则创建连接线
      this.createLine(i, j, position, mark);
    },

    /**
     * 创建连接线
     * @param {Number} i 选项列表索引
     * @param {Number} j 选项索引
     * @param {'pre'|'next'} position
     * @param {String} mark 选项标识
     * @param {Boolean} isDrag 是否是拖拽
     */
    createLine(i, j, position, mark, isDrag = false) {
      let { offsetLeft, offsetTop } = document.getElementsByClassName(`${position}-line-${i}-${j}`)[0];
      const { curOffsetLeft, curOffsetTop, curMark } = this.computedCurConnectionPoint(isDrag);
      let top = Math.min(offsetTop, curOffsetTop) + 5;
      let left = Math.min(offsetLeft, curOffsetLeft) + 5;
      let width = Math.abs(offsetLeft - curOffsetLeft);
      let height = Math.abs(offsetTop - curOffsetTop);
      let size = offsetLeft > curOffsetLeft ? offsetTop > curOffsetTop : offsetTop < curOffsetTop;

      // 创建一个空的SVG元素
      let svg = document.createElementNS(svgNS, 'svg');
      svg.setAttribute(
        'style',
        `position:absolute; width: 128px; height: ${Math.max(8, height) + 4}px; top: ${top}px; left: ${left}px;`,
      );
      svg.classList.add('connection-line', `svg-${mark}-${curMark}`); // 添加类名
      // 向SVG元素添加 path 元素
      let path = document.createElementNS(svgNS, 'path');
      path.setAttribute('d', `M ${size ? 0 : width} 0 L ${size ? width : 0} ${height}`); // 设置路径数据
      this.setPathAttr(path);
      svg.appendChild(path);
      this.$refs.list.appendChild(svg); // 将SVG元素插入到文档中

      // 清除当前连线点
      this.resetCurConnectionPoint();
    },
    // 设置 path 公用属性
    setPathAttr(path) {
      path.setAttribute('stroke-width', '2'); // 设置线条宽度
      path.setAttribute('stroke-linecap', 'round'); // 设置线段的两端样式
      path.setAttribute('stroke', '#306eff'); // 设置填充颜色
      path.setAttribute('transform', `translate(2, 2)`); // 设置偏移量
    },
    /**
     * 计算当前连线点的位置
     * @param {Boolean} isDrag 是否是拖拽
     */
    computedCurConnectionPoint(isDrag = false) {
      const { i, j, position, mark } = isDrag ? this.mousePointer : this.curConnectionPoint;
      let dom = document.getElementsByClassName(`${position}-line-${i}-${j}`)[0];
      return { curOffsetLeft: dom.offsetLeft, curOffsetTop: dom.offsetTop, curMark: mark };
    },

    // 清除所有连接线
    clearLine() {
      document.querySelectorAll('svg.connection-line').forEach((item) => {
        item.remove();
      });
    },

    /**
     * 修改连接列表
     * @param {String} mark 选项标识
     * @param {'pre'|'next'} position
     * @param {Boolean} isDrag 是否是拖拽
     */
    changeConnectionList(mark, position, isDrag = false) {
      const { mark: curMark, position: curPosition } = isDrag ? this.mousePointer : this.curConnectionPoint;
      this.changeAnswerList(curMark, mark, position);
      this.changeAnswerList(mark, curMark, curPosition);
    },
    /**
     * 改变答案列表
     * @param {String} curMark 当前选项标识
     * @param {String} mark 选项标识
     * @param {'pre'|'next'} position
     */
    changeAnswerList(curMark, mark, position) {
      let oldPointer = { mark: '', position: '' };
      // 找到当前选项，修改 preMark 或 nextMark
      this.answerList.find((item) =>
        item.find((li) => {
          if (li.mark === curMark) {
            if (position === 'pre') {
              if (li.nextMark) {
                oldPointer = { mark: li.nextMark, position: 'next' };
              }
              li.nextMark = mark;
            } else {
              if (li.preMark) {
                oldPointer = { mark: li.preMark, position: 'pre' };
              }
              li.preMark = mark;
            }
            return true;
          }
        }),
      );
      // 如果当前选项有 preMark 或 nextMark，则清除原来的连线
      if (!oldPointer.mark) return;
      document.querySelector(`svg.connection-line.svg-${curMark}-${oldPointer.mark}`)?.remove();
      document.querySelector(`svg.connection-line.svg-${oldPointer.mark}-${curMark}`)?.remove();

      // 找到原来的选项，清除 preMark 或 nextMark
      this.answerList.find((item) =>
        item.find((li) => {
          if (li.mark === oldPointer.mark) {
            if (oldPointer.position === 'pre') {
              li.nextMark = '';
            } else {
              li.preMark = '';
            }
            return true;
          }
        }),
      );
    },

    /**
     * 根据 mark 查找 nextMark 或 preMark
     * @param {Array} list 答案列表
     * @param {String} mark 标记
     * @param {'pre'|'next'} type 类型
     * @returns {String} 返回 nextMark 或 preMark
     */
    findMark(list, mark, type) {
      let fMark = '';
      list.find((item) => {
        return item.find((li) => {
          if (mark === li.mark) {
            fMark = type === 'pre' ? li.preMark : li.nextMark;
            return true;
          }
        });
      });
      return fMark;
    },
  },
};
</script>

<style lang="scss" scoped>
@use '@/styles/mixin.scss' as *;

.matching-preview {
  @include preview;

  .option-list {
    position: relative;
    display: flex;
    flex-direction: column;
    row-gap: 16px;

    .list-item {
      display: flex;
      column-gap: 60px;

      .item-wrapper {
        display: flex;
        flex: 1;
        column-gap: 24px;
        align-items: center;
        min-height: 48px;
        padding: 12px 24px;
        background-color: $fill-color;
        border-radius: 40px;

        .content {
          flex: 1;
        }

        .connection {
          z-index: 1;
          width: 14px;
          height: 14px;
          cursor: pointer;
          border: 4px solid #306eff;
          border-radius: 50%;

          &.focus {
            background-color: #306eff;
            border-color: #fff;
            outline: 2px solid #306eff;
          }
        }
      }
    }
  }
}
</style>
