<template>
  <div style="display:flex;">
    <div class="left">
      <codemirror
        v-model="codeVal"
        :options="cmOption"
        class="code-mirror"
        @ready="ready"
        ref="myCmGenerate"
      ></codemirror>
      <div v-if="isSubmit" class="code-mask"></div>
      <el-button
        type="warning"
        @click="runCode"
        :disabled="runEnable"
        style="width:100px;position:absolute;z-index:99;background:#FDCA17;color:black;right: 50px;bottom:15px;"
      >运行</el-button>
    </div>
    <div class="code-right answer">
      <p class="text-wrapper" v-html="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" type="primary" size="mini" @click="$refs.picLink.click()">下载图片</el-button>
        <a ref="picLink" style="display: none;" download="运行结果.png" :href="picSrc">下载图片</a>
      </div>
      <div class="code_yes" v-show="this.isError">
        <img src="../assets/img/yes.png" alt />运行成功
      </div>
      <div class="code_error" v-show="this.isError === 0">
        <img src="../assets/img/error.png" alt />
        第{{errLine}}行出现错误
        <el-button class="tips-btn" @click="getTips" v-show="showTips">提示</el-button>
        <el-dialog title="答案提示" center :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/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'
const CANCEL_TOKEN = axios.CancelToken // 用于input中中断请求
export default {
  props: ['workbench1', 'code', 'codeId', 'projectId'],
  data() {
    return {
      assessmentId: util.getCookie(';assessmentId'), // 考核id
      courseId: util.getCookie('courseId'), // 课程id
      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: {
          // 模式, 可查看 codemirror/mode 中的所有模式
          name: "javascript",
          json: true
        },
        lineWrapping: true, //代码折叠
        // hint.js options
        hintOptions: {
          // 当匹配只有一项的时候是否自动补全
          completeSingle: false
        },
        // 快捷键 可提供三种模式 sublime、emacs、vim
        keyMap: "sublime",
        matchBrackets: true,
        showCursorWhenSelecting: true,
        theme: "monokai", // 主题
        extraKeys: { Ctrl: "autocomplete" } // 可以用于为编辑器指定额外的键绑定，以及keyMap定义的键绑定
      },
      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() {
      this.$refs.myCmGenerate.codemirror.setSize("auto", "calc(100vh - 149px)");
    },
    // 下载文件
    downloadFile(fileName,url) {
      var x = new XMLHttpRequest()
      x.open("GET", url, true)
      x.responseType = 'blob'
      x.onload=function(e) {
          var url = window.URL.createObjectURL(x.response)
          var a = document.createElement('a')
          a.href = url
          a.download = fileName
          a.click()
      }
      x.send()
    },
    // python代码里如果有input函数的话，是做了单独的处理的，原理是先把所有input函数都替换成exit函数，再在exit函数里加上特定标识，再通过接口传给后端去执行
    // 因为exit函数是跟input有类似的效果，就是都会产生阻塞，所以python引擎一旦遇到exit，进程就会被停止，然后返回exit函数里面的值，而这个值，就是上面说的特定标识加上原本这个input函数里的值
    // 然后就可以通过这个返回的值来提示给用户，让用户继续输入
    // 下面这个函数就是递归执行这个input输入过程的函数
    confirmInput(msg){
      let receiveResult = msg.replace('validing:','')
      this.runResult += receiveResult
      this.$prompt(receiveResult, "提示", {
        confirmButtonText: "确定"
      }).then(({ value }) => {
        this.runResult += `${value}<br>`
        // 把exit函数替换成用户输入的值
        this.sourceCode = this.sourceCode.replace(`exit('validing:${receiveResult.replace(/[\r\n]*/g,'')}')`,val => {
          return `'${value}'`
        })
        this.sourceCode = this.sourceCode.replace(`exit("validing:${receiveResult.replace(/[\r\n]*/g,'')}")`,val => {
          return `'${value}'`
        })
        clearTimeout(this.requestTimer)
        
        // 一秒钟后还没请求成功的话，就直接中断请求，因为一般是陷入死循环了才会需要这么长时间的，中断了后就再次执行AnswerTips（运行按钮的执行函数）函数，再次进入循环
        this.requestTimer = setTimeout(() => {
          this.requestList.map(n => n('interrupt'))
        },1000)

        axios.post(this.api.runPythonCode,{
          code: this.codeVal,
          bcId: this.workbench1,
          cid: this.courseId,
          projectId: this.projectId
        },{
          cancelToken: new CANCEL_TOKEN(c => { //强行中断请求要用到的，记录请求信息
            this.requestList.push(c)
          })
        }).then(response => {
          let res = response.data
          let result = res.message.result
          if(result.includes('File ')){
            let modify = res.message.result
            this.runResult = modify
            this.errLine = parseInt(modify.substring(modify.indexOf("line") + 4, modify.length))
            this.isError = res.message.isError
          }else if(result.includes('validing:')){
            this.isError = 0
            this.confirmInput(result)
          }else if(!res.message.isError){
            this.isError = 0
            this.runResult += result
          }
        }).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 {
          let inputTextReg = this.inputTextReg
          let inputFuncReg = /input\(['|"]/g
          // 该正则是验证代码里是否有input，如果有，就要另外做处理，而不是直接传给后端执行
          if (inputTextReg.test(code)) {
            let sourceCode = this.codeVal
            sourceCode = sourceCode.replace(inputTextReg,val => {
              return val.replace(/\\n/g,"")
            })
            this.codeVal = sourceCode

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

            if(!isWhile) this.runResult = ''
            this.$post(this.api.runPythonCode, {
              code: this.codeVal,
              bcId: this.workbench1,
              cid: this.courseId,
              projectId: this.projectId
            }).then(res => {
              let result = res.message.result
              if(result.includes('File ')){
                let modify = res.message.result
                if(isWhile){
                  this.runResult += modify
                }else{
                  this.runResult = modify
                }
                this.errLine = parseInt(modify.substring(modify.indexOf("line") + 4, modify.length))
                this.isError = res.message.isError
              }else if(result.includes('validing:')){
                this.isError = 0
                this.confirmInput(result)
              }
            }).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: this.workbench1,
              cid: this.courseId,
              projectId: this.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)
              this.$emit('update:answer', result)
              if(photo){
                this.isError = ''
                const text = result.replace(photo, '')
                this.runResult = text
                this.picSrc = photo
              }else{
                // 这段是为要下载图片的项目案例写的，后端会返回图片名称的数组，前端负责循环这个数组，然后下载下来
                // 只有该系统有这段代码，因为其他7个系统没有下载图片的项目，后续如果加了，直接把这段代码复制过去即可
                if(0){
                  if(result instanceof Array && result.length && (result[0].includes('.jpg') || result[0].includes('.png') || result[0].includes('.gif'))){
                    result.map((n,i) => {
                      this.downloadFile(`${i+1}.jpg`,n)
                    })
                    this.isError = 0
                    this.runResult = '下载完成'
                  }else{
                    this.isError = 0
                    this.runResult = data.runResult
                    this.errLine = parseInt(result.substring(modify.indexOf("line") + 4, modify.length))
                  }
                } 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.workbench1
      }).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;
}
//答案提示
.answer ::v-deep .el-tab-pane {
  padding: 0 10px;
  height: 340px;
  overflow: hidden;
  overflow-y: auto;
  white-space: pre-wrap;
}
.answer ::v-deep .el-dialog--center {
  width: 500px;
  height: 500px;
}
.answer ::v-deep .el-dialog__title {
  font-size: 22px;
  font-weight: 500;
}
.answer ::v-deep .el-tabs__nav-wrap::after {
  background-color: #333;
}
.answer ::v-deep .el-tabs__active-bar {
  background-color: #fff;
  height: 0;
}
.answer ::v-deep .el-tabs__header {
  background-color: #333;
}
.answer ::v-deep .el-dialog--center .el-dialog__body {
  padding: 0;
}
.answer ::v-deep .el-tabs__item {
  color: #fff;
  width: 80px;
}
.answer ::v-deep .el-tabs--top .el-tabs__item.is-top:nth-child(2) {
  padding-left: 20px;
}
.answer ::v-deep .el-tabs__item.is-active {
  color: #fff !important;
  background-color: #333 !important;
}

::v-deep .CodeMirror-wrap pre.CodeMirror-line,
.CodeMirror-wrap pre.CodeMirror-line-like {
  height: 30px;
  line-height: 30px;
}

.cm-s-monokai .CodeMirror-linenumber {
  height: 30px;
  line-height: 30px;
}
.code-right ::v-deep .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);
}
.code-right ::v-deep .tips-btn:hover,
.tips-btn:focus,
.tips-btn:active {
  background: rgba(255, 49, 49, 1);
  color: rgba(255, 255, 255, 1);
}
.code_error img {
  width: 40px;
  height: 40px;
  margin-top: 5px;
  margin-right: 10px;
}
.code_yes img {
  width: 40px;
  height: 40px;
  margin-top: 5px;
  margin-right: 10px;
}
.code_yes {
  color: #fdca17;
  background-color: rgba(43, 40, 22, 1);
}
.code_error {
  background-color: rgba(43, 22, 22, 1);
  color: red;
}
.code_error,
.code_yes {
  display: flex;
  bottom: 10px;
  position: absolute;
  left: 20px;
  right: 20px;
  padding: 0 10px;
  // padding-right: 20px;
  box-sizing: border-box;
}
.code-right p {
  font-size: 18px;
  margin: 10px;
  position: absolute;
  width: calc(100% - 14px);
  height: calc(100vh - 230px);
  overflow: auto;
}
.code-right {
  width: 500px;
  color: #ffffff;
  background: #1b1b1b;
  display: inline-block;
  position: relative;
  overflow-x: auto;
}
.button ::v-deep .el-button--warning:hover,
.el-button--warning:focus,
.el-button--warning:active {
  background: #fdca17;
  color: #333;
}
.flex {
  display: flex;
}
// 滚动条的宽度
.scrollbar ::-webkit-scrollbar {
  width: 2px; // 横向滚动条
  height: 6px; // 纵向滚动条 必写
}
// 滚动条的滑块
.scrollbar ::-webkit-scrollbar-thumb {
  background-color: #fdca17;
  border-radius: 3px;
  box-shadow: inset 0 0 5px #dddddd;
}
.scrollbar ::-webkit-scrollbar-track {
  /*滚动条里面轨道*/
  box-shadow: inset 0 0 5px #dddddd;
  border-radius: 0;
  background: #dddddd;
}
.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;
}
.func-btn{
  width:100px;
  position:absolute;
  z-index:99;
  background:$main-color;
  color:#fff !important;
  right: 50px;
  bottom:15px;
  border-color: $main-color;
}
.download-btn{
  background:$main-color;
  color:#fff;
  border-color: $main-color
}
/deep/.answer-wrap{
  pre{
    width: 100%;
    white-space: pre-wrap;
    // height: 320px;
    // overflow-x: scroll;
  }
  img{
    max-width: 100%;
  }
}
</style>