<template>
  <div class="flex">
    <div class="left">
      <codemirror
        v-model="codeVal"
        :options="cmOption"
        class="code-mirror"
        @ready="ready"
        ref="codemirror"
      ></codemirror>
      <div v-if="isSubmit" class="code-mask"></div>
      <el-button
        class="run btn"
        type="primary"
        @click="runCode(false)"
        :disabled="runEnable"
      >运行</el-button>
    </div>
    <div class="code-right answer">
      <p class="text-wrapper">{{ runResult }}</p>
      <div class="pic-wrap" v-if="picSrc">
        <div style="margin-bottom: 5px;text-align: center">
          <img class="pic" :src="picSrc" alt="">
        </div>
        <el-button class="download-btn btn" type="primary" size="mini" @click="$refs.picLink.click()">下载图片</el-button>
        <a ref="picLink" style="display: none;" download="运行结果.png" :href="picSrc">下载图片</a>
      </div>
      <div class="result-right t-color" v-show="isError">
        <img :src="require(`@/assets/images/system/${systemId}/yes.png`)" alt />运行成功
      </div>
      <div class="result-wrong" v-show="isError === 0">
        <img src="@/assets/images/error.png" alt />
        第{{errLine}}行出现错误
        <el-button class="tips-btn" @click="getTips" v-show="showTips">提示</el-button>
        <el-dialog title="答案提示" center :close-on-click-modal="false" :visible.sync="tipsVisible">
          <el-tabs>
            <el-tab-pane label="参考答案">
              <div class="answer-wrap" v-html="answer"></div>
            </el-tab-pane>
          </el-tabs>
        </el-dialog>
      </div>
    </div>
  </div>
</template>

<script>
import newmain from "../util/newMain";
import { codemirror } from "vue-codemirror";
import "codemirror/theme/ambiance.css"; // 这里引入的是主题样式
import "codemirror/mode/javascript/javascript.js";
import "codemirror/mode/python/python.js";
import "codemirror/lib/codemirror.css";
// require active-line.js
import "codemirror/addon/selection/active-line.js";
// styleSelectedText
import "codemirror/addon/selection/mark-selection.js";
// hint
import "codemirror/addon/hint/show-hint.js";
import "codemirror/addon/hint/sql-hint.js";
import "codemirror/addon/hint/show-hint.css";
import "codemirror/addon/hint/javascript-hint.js";
// highlightSelectionMatches
import "codemirror/addon/scroll/annotatescrollbar.js";
import "codemirror/addon/search/matchesonscrollbar.js";
import "codemirror/addon/search/match-highlighter.js";
// keyMap
import "codemirror/mode/clike/clike.js";
import "codemirror/mode/sql/sql.js";
import "codemirror/addon/edit/matchbrackets.js";
import "codemirror/addon/comment/comment.js";
import "codemirror/addon/dialog/dialog.js";
import "codemirror/addon/dialog/dialog.css";
import "codemirror/addon/search/searchcursor.js";
import "codemirror/addon/search/search.js";
import "codemirror/keymap/sublime.js";
// foldGutter
import "codemirror/addon/fold/foldgutter.css";
import "codemirror/addon/fold/brace-fold.js";
import "codemirror/addon/fold/comment-fold.js";
import "codemirror/addon/fold/foldcode.js";
import "codemirror/addon/fold/foldgutter.js";
import "codemirror/addon/fold/indent-fold.js";
import "codemirror/addon/fold/markdown-fold.js";
import "codemirror/addon/fold/xml-fold.js";
// 编辑的主题文件
import "codemirror/theme/monokai.css";
import "codemirror/theme/base16-light.css";
import { Loading } from 'element-ui';
import axios from 'axios';
import util from '@/util'
import config from '@/config'
const CANCEL_TOKEN = axios.CancelToken // 用于input中中断请求
export default {
  props: ['judgmentId', 'code', 'codeId', 'projectId'],
  data() {
    return {
      token: util.getCookie('admin-token'),
      assessmentId: util.getCookie('assessmentId'), // 考核id
      courseId: util.getCookie('courseId'), // 课程id
      systemId: util.getCookie('systemId'),
      showTips: false, // 显示隐藏提示按钮
      answer: '', // 正确答案
      codeVal: this.code,
      runResult: '', // 运行结果
      isError: false, // 运行正确与否
      errLine: '', // 错误代码的位置
      isSubmit: false, // 是否提交了
      runEnable: false, // 是否禁用运行按钮
      tipsVisible: false, // 答案提示弹框显示标识
      picSrc: '', // 代码运行出来的图片url
      loadIns: null, // loading实例
      cmOption: {
        scrollbarStyle: "native",
        tabSize: 2, // tab
        styleActiveLine: true, // 高亮选中行
        lineNumbers: true, // 显示行号
        styleSelectedText: true,
        line: true,
        foldGutter: true, // 块槽
        gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
        highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: true }, // 可以启用该选项来突出显示当前选中的内容的所有实例
        mode: 'python',
        lineWrapping: true, //代码折叠
        autoCloseTags: true,// 自动闭合标签
        autoCloseBrackets: true,// 自动闭合括号
        // hint.js options
        hintOptions: {
          // 当匹配只有一项的时候是否自动补全
          completeSingle: false
        },
        // 快捷键 可提供三种模式 sublime、emacs、vim
        keyMap: "sublime",
        matchBrackets: true,
        showCursorWhenSelecting: true,
        theme: "monokai" // 主题
      },
      inputTextReg: /^((?!#).*?(,|\(|\[|\{|\s)+)?input(?!\w)\(['|"]([\s\S]+?)['|"]\)/m, // 匹配input()
      requestList: [], // 有input的情况下，保存每个axios的对象，用于中断请求
      sourceCode: '', // 把input替换成exit函数后的代码
      requestTimer: null // 用于中断请求的定时器
    };
  },
  components: {
    codemirror
  },
  watch: {
    codeVal(val) {
      this.$emit("update:code", val)
    }
  },
  mounted() {
    if (!this.assessmentId) this.showTips = true
    //兄弟组件传值
    newmain.$on("isSubmit", isSubmit => {
      this.isSubmit = isSubmit
    })
  },

  methods: {
    // 页面加载完后重置编辑框大小
    ready() {
      const code = this.$refs.codemirror.codemirror
      code.setSize("auto", "calc(100vh - 167px)");
      code.on('keypress', function() {
        // 显示智能提示
        code.showHint()
      });
    },
    /**
     * python代码里如果有input函数的话，是做了单独的处理的，原理是先把所有input函数都替换成exit函数，再在exit函数里加上特定标识，再通过接口传给后端去执行
     * 因为exit函数是跟input有类似的效果，就是都会产生阻塞，所以python引擎一旦遇到exit，进程就会被停止，然后返回exit函数里面的值，而这个值，就是上面说的特定标识加上原本这个input函数里的值
     * 然后就可以通过这个返回的值来提示给用户，让用户继续输入
     * 下面这个函数就是递归执行这个input输入过程的函数
     */
    confirmInput(msg){
      const receiveResult = msg.replace('validing:', '')
      this.runResult += receiveResult
      this.$prompt(receiveResult, '提示', {
        confirmButtonText: "确定"
      }).then(({ value }) => {
        this.runResult += `${value}\n`
        // 把exit函数替换成用户输入的值
        this.sourceCode = this.sourceCode.replace(`exit('validing:${receiveResult.replace(/[\r\n]*/g,'')}')`, _ => {
          return `'${value}'`
        })
        this.sourceCode = this.sourceCode.replace(`exit("validing:${receiveResult.replace(/[\r\n]*/g,'')}")`, _ => {
          return `'${value}'`
        })
        clearTimeout(this.requestTimer)
        
        // 一秒钟后还没请求成功的话，就直接中断请求，因为一般是陷入死循环了才会需要这么长时间的，中断了后就再次执行AnswerTips（运行按钮的执行函数）函数，再次进入循环
        this.requestTimer = setTimeout(() => {
          this.requestList.map(n => n('interrupt'))
        }, 1000)

        axios.post(config.host + this.api.runPythonCode, {
          code: this.sourceCode,
          bcId: this.judgmentId,
          cid: this.courseId,
          projectId: this.projectId
        }, {
          headers: {
            token: this.token
          },
          cancelToken: new CANCEL_TOKEN(c => { //强行中断请求要用到的，记录请求信息
            this.requestList.push(c)
          })
        }).then(response => {
          let res = response.data
          const data = res.code
          const result = data.runResult
          if (result.includes('File ')) {
            this.runResult = result
            this.errLine = parseInt(result.substring(result.indexOf('line') + 4, result.length))
            this.isError = data.retResult
          } else if (result.includes('validing:')) {
            this.isError = 1
            this.confirmInput(result)
          } else if(data.retResult) {
            this.isError = 1
            this.runResult += result
          }
          this.$emit('update:codeId', res.codeId) // 更新coddeId
          this.$emit('update:answer', this.runResult) // 更新运行结果
        }).catch(e => {
          if(e && e.message == 'interrupt'){
            this.runCode(true)
            this.requestList = []
          }
        })

      }).catch(err => {})
    },
    // 运行代码
    runCode(isWhile) { // isWhile为true表示代码里有while循环，右边的运行结果需要拼接展示，而不是直接覆盖
      if (!this.isSubmit) {
        let code = this.codeVal
        if (!code) {
          this.$message({
            message: "警告哦，内容为空不可运行",
            type: "warning"
          })
        } else {
          const inputTextReg = this.inputTextReg
          const inputFuncReg = /input\(['|"]/g
          const bcId = this.judgmentId
          const cid = this.courseId
          const projectId = this.projectId
          // 该正则是验证代码里是否有input，如果有，就要另外做处理，而不是直接传给后端执行
          if (inputTextReg.test(code)) {
            code = code.replace(inputTextReg, val => {
              return val.replace(/\\n/g,"")
            })
            this.codeVal = code

            // 把input函数替换成exit函数，加上"validing:"作为特定标识，好方便后面的识别
            code = code.replace(inputFuncReg,val => {
              return `exit(${val[val.length - 1]}validing:`
            })
            this.sourceCode = code

            if(!isWhile) this.runResult = ''
            this.$post(this.api.runPythonCode, {
              code,
              bcId,
              cid,
              projectId
            }).then(res => {
              const data = res.code
              const result = data.runResult
              if(result.includes('File ')){
                if (isWhile) {
                  this.runResult += result
                } else {
                  this.runResult = result
                }
                this.errLine = parseInt(result.substring(result.indexOf('line') + 4, result.length))
                this.isError = data.retResult
              }else if(result.includes('validing:')){
                this.isError = 1
                this.confirmInput(result)
              }
              this.$emit('update:codeId', res.codeId) // 更新coddeId
              this.$emit('update:answer', this.runResult) // 更新运行结果
            }).catch(err => {})
          } else {
            this.loadIns = Loading.service({
              background: 'transparent'
            })
            // 如果代码有savefig(python里的保存图片的方法),则要把savefig里的图片名字加上个时间戳，防止后台保存图片到服务器的时候图片名字重复导致覆盖
            code = code.replace(/\.savefig\(([\u4e00-\u9fa5\w]*?['"])/mg, str => {
              return str + Date.now()
            })
            // 把代码传给后端，在后端运行Python代码
            this.$post(this.api.runPythonCode, {
              code,
              bcId,
              cid,
              projectId
            }).then(res => {
              const data = res.code
              const photo = data.photoUrl
              const result = data.runResult
              this.$emit('cache') // 每次运行代码都要把代码传给后端做缓存
              this.loadIns.close()
              this.picSrc = ''
              this.$emit('update:codeId', res.codeId) // 更新coddeId
              this.$emit('update:answer', result) // 更新运行结果
              let imgList = ''
              let firtImg = ''
              try {
                imgList = eval(result)
              } catch (error) {}
              if (imgList && imgList.length) firtImg = imgList[0]
              // 如果是下载图片的代码，则要显示图片和运行结果，不用显示对错，换成显示下载图片
              if (photo) {
                this.isError = '' // 对错隐藏
                const text = result.replace(photo, '') // 结果里包含了图片路径，所以要把图片路径给去掉
                this.runResult = text
                this.picSrc = photo
              } else if (imgList instanceof Array && imgList.length && typeof firtImg === 'string' && (firtImg.includes('.jpg') || firtImg.includes('.png') || firtImg.includes('img'))) {
                /**
                 * 这段是为要下载图片的项目案例写的，后端会返回图片名称的数组，前端负责循环这个数组，然后下载下来
                 * 只有该系统有这段代码，因为其他7个系统没有下载图片的项目，后续如果加了，直接把这段代码复制过去即可
                 */
                imgList.map((n,i) => {
                  util.downloadFile(`${i+1}.jpg`,n)
                })
                this.isError = 0
                this.runResult = '下载完成'
              } else {
                this.isError = data.retResult
                this.runResult = result
                this.errLine = parseInt(result.substring(result.indexOf("line") + 4, result.length))
              }
            }).catch(res => {
              res.status == 500 && this.$message.error('检测到代码里有非法代码，请检查是否有调用系统命令。')
              this.loadIns.close()
            })
          }
        }
      } else {
        this.runEnable = true
        this.$message({
          message: "警告哦，已提交不可再运行",
          type: "warning"
        })
      }
    },
    // 获取正式答案
    getTips() {
      this.tipsVisible = true
      this.$get(this.api.queryBcJudgmentByBcId, {
        bcId: this.judgmentId
      }).then(res => {
        this.answer = res.experimentCode
      }).catch(err => {})
    }
  }
};
</script>

<style lang="scss" scoped>
.left{
  position: relative;
  width: calc(100% - 400px);
}
.text-wrapper {
  white-space: pre-wrap;
}
/deep/.answer {
  .el-tab-pane {
    padding: 0 10px;
    height: 340px;
    overflow: hidden;
    overflow-y: auto;
    white-space: pre-wrap;
  }
  .el-dialog--center {
    width: 500px;
    height: 500px;
  }
  .el-dialog__title {
    font-size: 22px;
    font-weight: 500;
  }
  .el-tabs__nav-wrap::after {
    background-color: #333;
  }
  .el-tabs__active-bar {
    height: 0;
    background-color: #fff;
  }
  .el-tabs__header {
    background-color: #333;
  }
  .el-dialog--center .el-dialog__body {
    padding: 0;
  }
  .el-tabs__item {
    width: 80px;
    color: #fff;
  }
  .el-tabs--top .el-tabs__item.is-top:nth-child(2) {
    padding-left: 20px;
  }
  .el-tabs__item.is-active {
    color: #fff !important;
    background-color: #333 !important;
  }
  .tips-btn {
    margin-top: 10px;
    height: 40px;
    width: 90px;
    border: none;
    position: absolute;
    right: 0;
    background: rgba(255, 49, 49, 1);
    color: rgba(255, 255, 255, 1);
    &:hover,
    &:focus,
    &:active {
      background: rgba(255, 49, 49, 1);
      color: rgba(255, 255, 255, 1);
    }
  }
}
/deep/.CodeMirror-wrap pre.CodeMirror-line,
.CodeMirror-wrap pre.CodeMirror-line-like {
  height: 30px;
  line-height: 30px;
}
.result-right {
  background-color: rgba(43, 40, 22, 1);
}
.result-wrong {
  background-color: rgba(43, 22, 22, 1);
  color: #f00;
}
.result-wrong,
.result-right {
  position: absolute;
  left: 20px;
  right: 20px;
  display: flex;
  bottom: 10px;
  padding: 0 10px;
  img {
    width: 40px;
    height: 40px;
    margin-top: 5px;
    margin-right: 10px;
  }
}
.code-right {
  width: 500px;
  color: #fff;
  background: #1b1b1b;
  display: inline-block;
  position: relative;
  overflow-x: auto;
  p {
    font-size: 18px;
    margin: 10px;
    position: absolute;
    width: calc(100% - 14px);
    height: calc(100vh - 247px);
    overflow: auto;
  }
}
.pic-wrap{
  position: absolute;
  left: 0;
  right: 0;
  bottom: 5px;
  text-align: center;
  .pic{
    max-width: 80%;
    vertical-align: middle;
  }
}
.code-mask{
  z-index: 2;
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
}
.run{
  z-index:99;
  position:absolute;
  right: 50px; 
  bottom:15px;
  width:100px;
  color:#fff;
}
.download-btn{
  color:#fff;
}
/deep/.answer-wrap{
  pre{
    width: 100%;
    white-space: pre-wrap;
  }
  img{
    max-width: 100%;
  }
}
</style>