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

    <ul ref="list" class="option-list">
      <li v-for="(item, i) in optionList" :key="i" class="list-item">
        <span class="serial-number" :style="{ fontSize: data.property.option_question_number_font_size }">
          {{
            isEnable(isEnableManualModify)
              ? item[0].custom_number
                ? `${item[0].custom_number}.`
                : ''
              : computeOptionMethods[data.option_number_show_mode](i)
          }}
        </span>
        <div
          v-for="({ content, mark }, j) in item"
          :key="mark"
          :class="['item-wrapper', `item-${mark}`, computedAnswerClass(mark), computedActiveClass(mark)]"
          :style="{ cursor: disabled ? 'default' : 'pointer' }"
          @mousedown="mousedown($event, i, j, mark)"
          @mouseup="mouseup($event, i, j, mark)"
          @click="handleClickConnection($event, i, j, mark)"
        >
          <span class="content rich-text" v-html="sanitizeHTML(content)"></span>
        </div>
      </li>
    </ul>

    <div v-if="isShowRightAnswer" class="right-answer">
      <div class="title">正确答案</div>
      <ul ref="answerList" class="option-list">
        <li v-for="(item, i) in optionList" :key="i" class="list-item">
          <div v-for="{ content, mark } in item" :key="mark" :class="['item-wrapper', `answer-item-${mark}`]">
            <span class="content rich-text" v-html="sanitizeHTML(content)"></span>
          </div>
        </li>
      </ul>
    </div>

    <div v-if="isEnable(data.property.is_enable_analysis) && isShowRightAnswer" class="analysis">
      <span class="analysis-title">解析</span>
      <div class="analysis-content" v-html="sanitizeHTML(data.analysis)"></div>
    </div>
  </div>
</template>

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

import PreviewMixin from './components/PreviewMixin';

export default {
  name: 'MatchingPreview',
  mixins: [PreviewMixin],
  props: {
    isEnableManualModify: {
      type: String,
      default: 'false',
    },
  },
  data() {
    return {
      computeOptionMethods,
      answerList: [], // 答案列表
      curConnectionPoint: { i: -1, j: -1, mark: '' }, // 当前连线点
      // 拖拽相关
      drag: false,
      mouseEvent: {
        clientX: 0,
        clientY: 0,
      },
      mousePointer: { i: -1, j: -1, 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,
      },
      {
        handler(val) {
          if (!val) return;
          this.$nextTick(() => {
            this.judgeRichTextIsImgAndText();
          });
        },
        deep: 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) || _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,
    },
    isJudgingRightWrong(cur) {
      if (cur) {
        this.clearLine();
        this.circulateAnswerList();
      } else {
        this.clearLine();
      }
    },
    isShowRightAnswer(cur) {
      if (cur) {
        this.$nextTick(() => {
          this.circulateAnswerList(true);
          this.judgeRichTextIsImgAndText(true);
        });
      }
    },
  },
  mounted() {
    document.addEventListener('click', this.handleEventConnection);
    document.addEventListener('mousemove', this.documentMousemouse);
    document.addEventListener('mouseup', this.documentMouseup);
    this.judgeRichTextIsImgAndText();
    this.judgeRichTextIsImgAndText(true);
  },
  beforeDestroy() {
    document.removeEventListener('click', this.handleEventConnection);
    document.removeEventListener('mousemove', this.documentMousemouse);
    document.removeEventListener('mouseup', this.documentMouseup);
  },
  methods: {
    /* 用 mouse 事件模拟拖拽 开始*/
    documentMousemouse(e) {
      if (!this.drag) return;
      if (this.disabled) return;
      // 使用 svg 绘制跟随鼠标移动的连接线
      let svg = document.querySelector('.move-connection');
      let list = this.$refs.list.getBoundingClientRect(); // 列表的位置
      let { clientX, clientY } = e;
      let isLeft = clientX < this.mouseEvent.clientX; // 鼠标是否向左移动
      // 计算 svg 的宽度，宽度不能超过列表的宽度
      let width = Math.min(list.width, isLeft ? list.width - clientX + list.left - 1 : clientX - list.left - 1);
      if (svg) {
        svg.setAttribute(
          'style',
          `position: absolute; ${isLeft ? 'right: 0;' : 'left: 0;'} width: ${width}px; height: 100%;`,
        );
      } else {
        svg = document.createElementNS(svgNS, 'svg');
        svg.classList.add('move-connection');
        svg.setAttribute('style', `position: absolute; width: ${width}px; height: 100%;`);
        let path = document.createElementNS(svgNS, 'path');
        this.setPathAttr(path);
        svg.appendChild(path);
        this.$refs.list.appendChild(svg);
      }
      let top = this.mouseEvent.clientY - list.top;
      let left = isLeft
        ? this.mouseEvent.clientX - list.left - Math.abs(width - list.width)
        : this.mouseEvent.clientX - list.left;
      let mouseX = isLeft ? clientX - list.left - Math.abs(width - list.width) : 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, mark: '' };
      this.mouseEvent = { clientX: 0, clientY: 0 };
    },
    /**
     * 鼠标按下事件，设置当前连线点
     * @param {PointerEvent} e 事件对象
     * @param {number} i 选项列表索引
     * @param {number} j 选项索引
     * @param {string} mark 选项标识
     */
    mousedown(e, i, j, mark) {
      this.drag = true;
      document.body.style.userSelect = 'none'; // 禁止选中文本
      this.mouseEvent = { clientX: e.clientX, clientY: e.clientY };
      this.mousePointer = { i, j, mark };
    },
    /**
     * 鼠标抬起事件，如果是一个合适的连接点，则创建连接线
     * @param {PointerEvent} e 事件对象
     * @param {Number} i 选项列表索引
     * @param {Number} j 选项索引
     * @param {String} mark 选项标识
     */
    mouseup(e, i, j, mark) {
      let { i: curI, j: curJ, mark: curMark } = this.mousePointer;
      if (curI === -1 && curJ === -1) return;
      if (Math.abs(curJ - j) > 1 || mark === curMark) return;
      this.changeConnectionList(mark, j, true);
      this.createLine(mark, true);
    },
    /* 用 mouse 事件模拟拖拽 结束 */

    // 重置当前连线点
    resetCurConnectionPoint() {
      this.curConnectionPoint = { i: -1, j: -1, mark: '' };
    },
    /**
     * 当点击的不是连线点时，清除所有连线点的选中状态
     * @param {PointerEvent} e
     */
    handleEventConnection(e) {
      let currentNode = e.target;
      while (currentNode !== null) {
        if (currentNode.classList && currentNode.classList.contains('item-wrapper')) {
          break;
        }
        currentNode = currentNode.parentNode;
      }
      if (currentNode) return;
      Array.from(document.getElementsByClassName('item-wrapper')).forEach((item) => {
        item.classList.remove('focus');
      });
      this.resetCurConnectionPoint();
    },

    /**
     * 处理点击连线
     * @param {PointerEvent} e 事件对象
     * @param {Number} i 选项列表索引
     * @param {Number} j 选项索引
     * @param {String} mark 选项标识
     */
    handleClickConnection(e, i, j, mark) {
      if (this.disabled) return;
      let { i: curI, j: curJ, mark: curMark } = this.curConnectionPoint;
      // 获取 item-wrapper 元素
      let currentNode = e.target;
      while (currentNode !== null) {
        if (currentNode.classList && currentNode.classList.contains('item-wrapper')) {
          break;
        }
        currentNode = currentNode.parentNode;
      }
      // 如果当前连线点不存在或就是当前连线点，则设置当前连线点
      if ((curI === -1 && curJ === -1) || mark === curMark) {
        this.curConnectionPoint = { i, j, mark };
        currentNode.classList.add('focus');
        return;
      }
      // 如果当前连线点存在，清除所有连线点的选中状态
      Array.from(this.$refs.list.getElementsByClassName('item-wrapper')).forEach((item) => {
        item.classList.remove('focus');
      });
      // 如果当前连线点与上一个连线点不在相邻的位置，则设置点击的连接点为当前连线点
      if (Math.abs(curJ - j) > 1 || mark === curMark || (curJ === j && curI !== i)) {
        this.curConnectionPoint = { i, j, mark };
        currentNode.classList.add('focus');
        return;
      }
      this.changeConnectionList(mark, j);
      // 如果当前连线点与上一个连线点在相邻的位置，则创建连接线
      this.createLine(mark);
    },

    // 循环答案列表
    circulateAnswerList(isShowRightAnswer = false) {
      let answer_list = isShowRightAnswer ? this.data.answer.answer_list : this.answer.answer_list;
      answer_list.forEach((item) => {
        item.forEach((mark, j) => {
          if (mark.length <= 0 || j >= item.length - 1) return;

          let cur = { i: -1, j: -1 }; // 当前连线点
          this.answerList.findIndex((item, i) => {
            return item.some((li, j) => {
              if (li.mark === mark) {
                cur = { i, j };
                return true;
              }
            });
          });
          this.curConnectionPoint = { i: cur.i, j: cur.j, mark };
          this.createLine(item[j + 1], false, isShowRightAnswer);
        });
      });
    },

    /**
     * 创建连接线
     * @param {String} mark 选项标识
     * @param {Boolean} isDrag 是否是拖拽
     * @param {Boolean} isShowRightAnswer 是否是显示正确答案
     */
    createLine(mark, isDrag = false, isShowRightAnswer = false) {
      let { offsetWidth, offsetLeft, offsetTop, offsetHeight } = document.getElementsByClassName(
        `${isShowRightAnswer ? 'answer-' : ''}item-${mark}`,
      )[0];
      const { curOffsetWidth, curOffsetLeft, curOffsetTop, curOffsetHeight, curMark } = this.computedCurConnectionPoint(
        isDrag,
        isShowRightAnswer,
      );
      let top = Math.min(offsetTop + offsetHeight / 2, curOffsetTop + offsetHeight / 2);
      let left = Math.min(offsetLeft + offsetWidth, curOffsetLeft + curOffsetWidth);
      let width = Math.abs(
        offsetLeft > curOffsetLeft
          ? curOffsetLeft - offsetLeft + offsetWidth
          : offsetLeft - curOffsetLeft + curOffsetWidth,
      );
      let height =
        curOffsetTop > offsetTop
          ? Math.abs(offsetTop + offsetHeight / 2 - (curOffsetTop + curOffsetHeight / 2))
          : 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: 62px; height: ${Math.max(8, height)}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[isShowRightAnswer ? 'answerList' : '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 是否是拖拽
     * @param {Boolean} isShowRightAnswer 是否是显示正确答案
     */
    computedCurConnectionPoint(isDrag = false, isShowRightAnswer = false) {
      const { mark } = isDrag ? this.mousePointer : this.curConnectionPoint;
      let dom = document.getElementsByClassName(`${isShowRightAnswer ? 'answer-' : ''}item-${mark}`)[0];
      return {
        curOffsetWidth: dom.offsetWidth,
        curOffsetLeft: dom.offsetLeft,
        curOffsetTop: dom.offsetTop,
        curOffsetHeight: dom.offsetHeight,
        curMark: mark,
      };
    },

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

    /**
     * 修改连接列表
     * @param {String} mark 选项标识
     * @param {Boolean} isDrag 是否是拖拽
     */
    changeConnectionList(mark, j, isDrag = false) {
      const { mark: curMark, j: curJ } = isDrag ? this.mousePointer : this.curConnectionPoint;
      this.changeAnswerList(curMark, mark, curJ < j);
      this.changeAnswerList(mark, curMark, curJ > j);
    },
    /**
     * 改变答案列表
     * @param {String} curMark 当前选项标识
     * @param {String} mark 选项标识
     */
    changeAnswerList(curMark, mark, isPre) {
      let oldPointer = { mark: '', position: '' };
      // 找到当前选项，修改 preMark 或 nextMark
      this.answerList.find((item) =>
        item.find((li) => {
          if (li.mark === curMark) {
            if (isPre) {
              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;
    },
    // 计算激活状态 class
    computedActiveClass(mark) {
      const find = this.answer.answer_list.find((item) => {
        if (item.some((li) => li === mark)) return true;
      });
      return find ? 'active' : '';
    },
    /**
     * 计算答题对错选项class
     * @param {string} mark 选项标识
     */
    computedAnswerClass(mark) {
      if (!this.isJudgingRightWrong) return '';
      let answer = this.data.answer.answer_list.find((item) => {
        return item.some((li) => li === mark);
      });
      return this.is2DArrayContains1DArray(this.answer.answer_list, answer) ? 'right' : 'wrong';
    },
    /**
     * 判断二维数组是否包含一维数组
     * @param {array} arr2D 二维数组
     * @param {array} arr1D 一维数组
     */
    is2DArrayContains1DArray(arr2D, arr1D) {
      for (let i = 0; i < arr2D.length; i++) {
        const currentArray = arr2D[i];
        if (currentArray.length !== arr1D.length) {
          continue;
        }
        let found = true;
        for (let j = 0; j < currentArray.length; j++) {
          if (currentArray[j] !== arr1D[j]) {
            found = false;
            break;
          }
        }
        if (found) {
          return true;
        }
      }
      return false;
    },
    /**
     * 判断富文本是否包含图片和文本，如果图片和文本都存在，则居中显示
     * @param {boolean} isAnswer 是否是答案
     */
    judgeRichTextIsImgAndText(isAnswer) {
      let list = isAnswer ? this.$refs.answerList : this.$refs.list;
      if (!list) return;
      list.querySelectorAll('.content.rich-text > p').forEach((el) => {
        let hasTextNode = Array.from(el.childNodes).some((node) => node.nodeType === Node.TEXT_NODE); // 判断是否有文本节点
        let hasImgNode = Array.from(el.childNodes).some((node) => node.nodeName === 'IMG'); // 判断是否有图片节点
        if (hasTextNode && hasImgNode) {
          el.style.textAlign = 'center';
        }
      });
    },
  },
};
</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: 8px;
      align-items: center;

      .serial-number {
        font-size: 16pt;
      }

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

        &:not(:last-child) {
          margin-right: 52px;
        }

        &.focus,
        &.active {
          background-color: #dcdbdd;
        }

        &.right {
          background-color: $right-bc-color;
        }

        &.wrong {
          box-shadow: 0 0 0 1px $error-color;
        }

        .content {
          flex: 1;
        }
      }
    }
  }

  .right-answer {
    .title {
      margin-bottom: 24px;
    }
  }
}
</style>
