<!--
Copyright 2020 Huawei Technologies Co., Ltd.All Rights Reserved.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->

<template>
  <!--cl-data-map-manage -->
  <div class="cl-data-map-manage">
    <div class='data-map-p32'>
      <div class="cl-title cl-data-map-title">
        <div class="cl-title-left">{{$t('dataMap.titleText')}}</div>
        <div class="cl-title-right">
          <div class="cl-close-btn"
               @click="jumpToTrainDashboard">
            <img src="@/assets/images/close-page.png">
          </div>
        </div>
      </div>
      <div class="cl-content">
        <div id="data-maps">
          <div class="cl-data-map"
               :class="fullScreen? 'full-screen':''">
            <!-- data-map -->
            <div class="data-map-container"
                 :class="rightShow?'':'all'">
              <div id="graph"></div>
              <div class="image-noData"
                   v-if="noData">
                <div>
                  <img :src="require('@/assets/images/nodata.png')"
                       alt="" />
                </div>
                <div class="noData-text">{{$t("public.noData")}}</div>
              </div>
              <div :title="$t('graph.fullScreen')"
                   class="full-screen-button"
                   @click="fullScreen = !fullScreen"></div>
              <div :title="$t('graph.fitScreen')"
                   class="fit-screen"
                   @click="fit()"></div>
              <div :title="$t('graph.downloadPic')"
                   class="download-button"
                   @click="downLoadSVG"></div>
            </div>
            <!-- Right column -->
            <div id="sidebar"
                 :class="rightShow ? '' : 'right-hide'">
              <div class="toggle-right"
                   @click="toggleRight">
                <i :class="rightShow?'icon-toggle':'icon-toggle icon-left'"></i>
              </div>
              <!-- Node information -->
              <div class="node-info"
                   :class="showLegend?'node-info-con node-info-container':'node-info-con node-info-container-long'">
                <div class="title">{{$t('graph.nodeInfo')}}</div>
                <div class="node-info-list"
                     v-if="selectedNode && selectedNode.length">
                  <div class="item"
                       v-for="item in selectedNode"
                       :key="item.key">
                    <div class="label">{{item.key}}</div>
                    <div class="value">{{item.value}}</div>
                  </div>
                </div>
              </div>
              <!-- Legend -->
              <div class="legend"
                   v-if="!fullScreen">
                <div class="title">
                  {{ $t('graph.legend') }}
                  <img :src="require('@/assets/images/all-drop-down.png')"
                       v-show="!showLegend"
                       @click="foldLegend"
                       alt="" />
                  <img :src="require('@/assets/images/all-uptake.png')"
                       v-show="showLegend"
                       @click="foldLegend"
                       alt="" />
                </div>
                <div v-show="showLegend"
                     class="legend-content">
                  <div class="legend-item">
                    <div class="pic">
                      <img :src="require('@/assets/images/creat-dataset.png')"
                           alt="" />
                    </div>
                    <div>Create</div>
                  </div>
                  <div class="legend-item">
                    <div class="pic">
                      <img :src="require('@/assets/images/map-dataset.png')"
                           alt="" />
                    </div>
                    <div>Map</div>
                  </div>
                  <div class="legend-item">
                    <div class="pic">
                      <img :src="require('@/assets/images/operator-node.png')"
                           alt="" />
                    </div>
                    <div>Operator</div>
                  </div>
                  <div class="legend-item">
                    <div class="pic">
                      <img :src="require('@/assets/images/shuffle-dataset.png')"
                           alt="" />
                    </div>
                    <div>Shuffle</div>
                  </div>
                  <div class="legend-item">
                    <div class="pic">
                      <img :src="require('@/assets/images/name-scope.png')"
                           alt="" />
                    </div>
                    <div>Batch</div>
                  </div>
                  <div class="legend-item">
                    <div class="pic">
                      <img :src="require('@/assets/images/repeat-dataset.png')"
                           alt="" />
                    </div>
                    <div>Repeat</div>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import RequestService from '../../services/request-service';
import CommonProperty from '@/common/common-property.js';
import {select, selectAll, zoom} from 'd3';
import 'd3-graphviz';
const d3 = {select, selectAll, zoom};
export default {
  data() {
    return {
      allGraphData: {},
      graphviz: null,
      totalMemory: 16777216 * 2, // Memory size of the graph plug-in
      scaleRange: [0.0001, 10000], // graph zooms in and zooms out.
      rightShow: true,
      showLegend: true,
      fullScreen: false,
      trainJobID: '',
      selectedNode: [],
      noData: false,
    };
  },
  mounted() {
    // Judging from the training job overview.
    if (!this.$route.query || !this.$route.query.train_id) {
      this.trainJobID = '';
      this.$message.error(this.$t('trainingDashboard.invalidId'));
      document.title = `${this.$t('trainingDashboard.dataMap')}-MindInsight`;
      return;
    }
    this.trainJobID = this.$route.query.train_id;
    document.title = `${decodeURIComponent(this.trainJobID)}-${this.$t(
        'trainingDashboard.dataMap',
    )}-MindInsight`;
    this.$nextTick(() => {
      this.queryGraphData();
    });
  },
  destroyed() {},
  methods: {
    /**
     * To obtain graph data.
     */
    queryGraphData() {
      const params = {
        train_id: this.trainJobID,
      };
      RequestService.queryDatasetGraph(params).then((res) => {
        if (res && res.data && res.data.dataset_graph) {
          const data = JSON.parse(JSON.stringify(res.data.dataset_graph));
          this.dealResponseData(data);
          if (Object.keys(this.allGraphData).length) {
            this.noData = false;
            const dot = this.packageGraphData();
            this.initGraph(dot);
          } else {
            this.noData = true;
          }
        }
      });
    },
    /**
     * Processing Graph Data
     * @param {Object} data Data of the graph
     * @param {String} parentKey Key value of the parent-level data.
     * @param {Number} index Index of a node.
     */
    dealResponseData(data, parentKey = '', index = 0) {
      if (!data) {
        return;
      }
      const key = `${parentKey ? parentKey + '/' : ''}${data.op_type ||
        ''}_${index}`;
      const obj = {
        key: key,
        id: '',
      };
      Object.keys(data).forEach((k) => {
        {
          if (k !== 'children') {
            obj[k] = JSON.parse(JSON.stringify(data[k]));
          } else {
            obj.children = [];
            if (data.children && data.children.length) {
              data.children.forEach((data, ind) => {
                obj.children.push(`${obj.key}/${data.op_type}_${ind}`);
                this.dealResponseData(data, obj.key, ind);
              });
            }
          }
        }
      });
      this.allGraphData[key] = obj;
    },
    /**
     * Encapsulates graph data into dot data.
     * @return {String} dot string for packing graph data
     */
    packageGraphData() {
      const nodeType = [
        'BatchDataset',
        'ShuffleDataset',
        'RepeatDataset',
        'MapDataset',
      ];
      let nodeStr = '';
      let edgeStr = '';
      Object.keys(this.allGraphData).forEach((key) => {
        const node = this.allGraphData[key];
        if (node.op_type === 'MapDataset') {
          nodeStr += this.packageSubGraph(key);
        } else {
          node.id = key;
          nodeStr +=
            `<${node.key}>[id="${node.key}";label="${node.op_type}";` +
            `class=${
              nodeType.includes(node.op_type) ? node.op_type : 'CreatDataset'
            };shape=rect;fillcolor="#9cc3e5";];`;
        }
      });

      Object.keys(this.allGraphData).forEach((key) => {
        const node = this.allGraphData[key];
        node.children.forEach((k) => {
          const child = this.allGraphData[k];
          edgeStr += `<${child.id}>-><${node.id}>[${
            child.op_type === 'MapDataset'
              ? `ltail=<cluster_${child.key}>;`
              : ''
          }${
            node.op_type === 'MapDataset' ? `lhead=<cluster_${node.key}>;` : ''
          }];`;
        });
      });
      const initSetting =
        'node[style="filled";fontsize="10px"];edge[fontsize="6px";];';
      return `digraph {compound=true;rankdir=LR;${initSetting}${nodeStr}${edgeStr}}`;
    },

    /**
     * Encapsulates the data of a subgraph.
     * @param {String} key Key value of a node.
     * @return {String} dot string
     */
    packageSubGraph(key) {
      const node = this.allGraphData[key];
      let strTemp = '';
      if (node.operations && node.operations.length) {
        let nodeStr = '';
        node.operations.forEach((op, ind) => {
          const id = `${node.key}/${op.tensor_op_name}_${ind}`;
          op.key = id;
          nodeStr += `<${id}>[id="${id}";class=Operator;label="${op.tensor_op_name}";fillcolor="#c5e0b3"];`;
          if (!node.id) {
            node.id = id;
          }
        });
        strTemp +=
          `subgraph <cluster_${key}>{style="filled";id="${key}";` +
          `label="${node.op_type}";fillcolor="#9cc3e5";${nodeStr}};`;
      }
      return strTemp;
    },
    /**
     * Initializing the dataset graph
     * @param {String} dot dot statement encapsulated in graph data
     */
    initGraph(dot) {
      this.graphviz = d3
          .select('#graph')
          .graphviz({useWorker: false, totalMemory: this.totalMemory})
          .zoomScaleExtent(this.scaleRange)
          .dot(dot)
          .attributer(this.attributer)
          .render(this.afterInitGraph);
    },
    /**
     * Process other data after the dataset graph is initialized.
     */
    afterInitGraph() {
      setTimeout(() => {
        if (this.graphviz) {
          this.graphviz._data = null;
          this.graphviz._dictionary = null;
          this.graphviz = null;
        }
      }, 100);
      d3.select('#graph')
          .selectAll('title')
          .remove();
      this.startApp();
    },
    /**
     * Default method of the graph rendering adjustment. Set the node format.
     * @param {Object} datum Object of the current rendering element.
     * @param {Number} index Indicates the subscript of the current rendering element.
     * @param {Array} nodes An array encapsulated with the current rendering element.
     */
    attributer(datum, index, nodes) {
      if (datum.tag === 'svg') {
        const width = '100%';
        const height = '100%';
        datum.attributes.width = width;
        datum.attributes.height = height;
      }
    },
    /**
     * Initialization method executed after the graph rendering is complete
     */
    startApp() {
      this.initZooming();
      const nodes = d3.selectAll('g.node, g.cluster');
      nodes.on('click', (target, index, nodesList) => {
        this.selectNodeInfo(target);
        const selectedNode = nodesList[index];
        nodes.classed('selected', false);
        d3.select(`g[id="${selectedNode.id}"]`).classed('selected', true);
      });
    },

    /**
     * Initializing the Zoom Function of a Graph
     */
    initZooming() {
      const svgDom = document.querySelector('#graph svg');
      const svgRect = svgDom.getBoundingClientRect();

      const graphDom = document.querySelector('#graph #graph0');
      const graphBox = graphDom.getBBox();
      const graphRect = graphDom.getBoundingClientRect();
      let graphTransform = {};

      const minScale = Math.min(
          svgRect.width / 2 / graphRect.width,
          svgRect.height / 2 / graphRect.height,
      );

      const padding = 4;
      const minDistance = 50;
      const pointer = {start: {x: 0, y: 0}, end: {x: 0, y: 0}};

      const zoom = d3
          .zoom()
          .on('start', () => {
            pointer.start.x = event.x;
            pointer.start.y = event.y;
          })
          .on('zoom', () => {
            const transformData = this.getTransformData(graphDom);
            if (!Object.keys(graphTransform).length) {
              graphTransform = {
                x: transformData.translate[0],
                y: transformData.translate[1],
                k: transformData.scale[0],
              };
            }

            let tempStr = '';
            let change = {};
            let scale = transformData.scale[0];
            const graphRect = graphDom.getBoundingClientRect();
            const transRate = graphBox.width / graphRect.width;
            if (event.type === 'mousemove') {
              pointer.end.x = event.x;
              pointer.end.y = event.y;
              let tempX = pointer.end.x - pointer.start.x;
              let tempY = pointer.end.y - pointer.start.y;
              const paddingTrans = Math.max(
                  (padding / transRate) * scale,
                  minDistance,
              );
              if (
                graphRect.left + paddingTrans + tempX >=
              svgRect.left + svgRect.width
              ) {
                tempX = Math.min(tempX, 0);
              }
              if (
                graphRect.left + graphRect.width - paddingTrans + tempX <=
              svgRect.left
              ) {
                tempX = Math.max(tempX, 0);
              }
              if (
                graphRect.top + paddingTrans + tempY >=
              svgRect.top + svgRect.height
              ) {
                tempY = Math.min(tempY, 0);
              }
              if (
                graphRect.top + graphRect.height - paddingTrans + tempY <=
              svgRect.top
              ) {
                tempY = Math.max(tempY, 0);
              }

              change = {
                x: tempX * transRate * scale,
                y: tempY * transRate * scale,
              };
              pointer.start.x = pointer.end.x;
              pointer.start.y = pointer.end.y;
            } else if (event.type === 'wheel') {
              const wheelDelta = event.wheelDelta;
              const rate = Math.abs(wheelDelta / 100);
              scale =
              wheelDelta > 0
                ? transformData.scale[0] * rate
                : transformData.scale[0] / rate;

              scale = Math.max(this.scaleRange[0], scale, minScale);
              scale = Math.min(this.scaleRange[1], scale);
              change = {
                x:
                (graphRect.x + padding / transRate - event.x) *
                transRate *
                (scale - transformData.scale[0]),
                y:
                (graphRect.bottom - padding / transRate - event.y) *
                transRate *
                (scale - transformData.scale[0]),
              };
            }

            graphTransform = {
              x: transformData.translate[0] + change.x,
              y: transformData.translate[1] + change.y,
              k: scale,
            };

            tempStr = `translate(${graphTransform.x},${graphTransform.y}) scale(${graphTransform.k})`;
            graphDom.setAttribute('transform', tempStr);
            event.stopPropagation();
            event.preventDefault();
          });

      const svg = d3.select('#graph svg');
      svg.on('.zoom', null);
      svg.call(zoom);
      svg.on('dblclick.zoom', null);
      svg.on('wheel.zoom', null);

      const graph0 = d3.select('#graph #graph0');
      graph0.on('.zoom', null);
      graph0.call(zoom);
    },
    /**
     * Obtains the transform data of a node.
     * @param {Object} node Node dom data
     * @return {Object} transform data of a node
     */
    getTransformData(node) {
      if (!node) {
        return [];
      }
      const transformData = node.getAttribute('transform');
      const attrObj = {};
      if (transformData) {
        const lists = transformData.trim().split(' ');
        lists.forEach((item) => {
          const index1 = item.indexOf('(');
          const index2 = item.indexOf(')');
          const name = item.substring(0, index1);
          const params = item
              .substring(index1 + 1, index2)
              .split(',')
              .map((i) => {
                return parseFloat(i) || 0;
              });
          attrObj[name] = params;
        });
      }
      return attrObj;
    },
    /**
     * Process the selected node information.
     * @param {Object} target Selected Object
     */
    selectNodeInfo(target) {
      this.selectedNode = [];
      if (!target || !target.key) {
        return;
      }
      let id = '';
      let select = '';
      if (target.attributes.class.indexOf('Operator') !== -1) {
        id = target.attributes.id.slice(
            0,
            target.attributes.id.lastIndexOf('/'),
        );
        let index = -1;
        if (target.attributes.id.match(/\d+$/)) {
          index = parseInt(target.attributes.id.match(/\d+$/)[0]);
        }
        if (this.allGraphData[id] && index > -1) {
          select = this.allGraphData[id].operations[index];
        }
      } else {
        id = target.attributes.id;
        select = this.allGraphData[id];
      }
      if (select) {
        const ignoreKeys = [
          'op_module',
          'op_type',
          'children',
          'operations',
          'id',
          'key',
        ];
        Object.keys(select).forEach((item) => {
          if (!ignoreKeys.includes(item)) {
            const value =
              select[item] instanceof Array
                ? select[item].join(', ')
                : select[item] === null
                ? 'None'
                : select[item];
            this.selectedNode.push({key: item, value: value});
          }
        });
      }
    },
    /**
     * Adapt to the screen
     */
    fit() {
      const graphDom = document.getElementById('graph0');
      const box = graphDom.getBBox();
      const str = `translate(${-box.x},${-box.y}) scale(1)`;
      graphDom.setAttribute('transform', str);
    },
    /**
     * Download svg
     */
    downLoadSVG() {
      const svgXml = document.querySelector('#graph #graph0').innerHTML;
      const bbox = document.getElementById('graph0').getBBox();
      const viewBoxSize = `${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}`;
      const encodeStr =
        `<svg xmlns="http://www.w3.org/2000/svg" ` +
        `xmlns:xlink="http://www.w3.org/1999/xlink" ` +
        `width="${bbox.width}" height="${bbox.height}" ` +
        `viewBox="${viewBoxSize}">${CommonProperty.dataMapDownloadStyle}<g>${svgXml}</g></svg>`;

      // Write the svg stream encoded by base64 to the image object.
      const src = `data:image/svg+xml;base64,
      ${window.btoa(unescape(encodeURIComponent(encodeStr)))}`;
      const a = document.createElement('a');
      a.href = src; // Export the information in the canvas as image data.
      a.download = 'dataMap'; // Set the download name.
      a.click(); // Click to trigger download.
    },
    /**
     * Collapse on the right
     */
    toggleRight() {
      this.rightShow = !this.rightShow;
    },
    /**
     * Fold the legend area.
     */
    foldLegend() {
      this.showLegend = !this.showLegend;
    },
    /**
     * jump back to train dashboard
     */
    jumpToTrainDashboard() {
      this.$router.push({
        path: '/train-manage/training-dashboard',
        query: {
          id: this.trainJobID,
        },
      });
    },
  },
};
</script>
<style lang="scss">
.cl-data-map-manage {
  height: 100%;
  .cl-data-map-title {
    height: 56px;
    line-height: 56px;
  }
  .data-map-p32 {
    height: 100%;
  }
  .cl-content {
    height: calc(100% - 50px);
    overflow: auto;
  }
  #data-maps {
    width: 100%;
    height: 100%;
    font-size: 0;
    background: #f0f2f5;
    .cl-data-map {
      position: relative;
      width: 100%;
      height: 100%;
      background-color: #fff;
      padding: 0 32px 24px;
      min-height: 700px;
      overflow: hidden;
      .data-map-container {
        height: 100%;
        width: calc(100% - 442px);
        position: relative;
        #graph {
          height: 100%;
          width: 100%;
          padding: 16px;
          background-color: #f7faff;
          #graph0 > polygon {
            fill: transparent;
          }
          .node,
          .cluster {
            cursor: pointer;
          }
          .selected {
            polygon,
            ellipse {
              stroke: red !important;
              stroke-width: 2px;
            }
          }
          .CreatDataset > polygon,
          .Operator > ellipse {
            stroke: #58a4e0;
            fill: #d1ebff;
          }
          .cluster > polygon {
            fill: #c1f5d5;
            stroke: #56b077;
          }
          .RepeatDataset > polygon {
            stroke: #fdca5a;
            fill: #fff2d4;
          }
          .ShuffleDataset > polygon {
            stroke: #f79666;
            fill: #fed78e;
          }
          .BatchDataset > polygon {
            stroke: #fa8e5a;
            fill: #ffcfb8;
          }
          .edge {
            path {
              stroke: rgb(167, 167, 167);
            }
            polygon {
              fill: rgb(167, 167, 167);
              stroke: rgb(167, 167, 167);
            }
          }
        }
        .full-screen-button {
          position: absolute;
          right: 10px;
          top: 10px;
          cursor: pointer;
          width: 12px;
          height: 12px;
          z-index: 999;
          display: inline-block;
          background-image: url('../../assets/images/full-screen.png');
        }
        .fit-screen {
          position: absolute;
          width: 16px;
          height: 14px;
          right: 32px;
          top: 10px;
          z-index: 999;
          cursor: pointer;
          display: inline-block;
          background-image: url('../../assets/images/fit.png');
        }
        .download-button {
          position: absolute;
          width: 16px;
          height: 14px;
          right: 54px;
          top: 10px;
          z-index: 999;
          cursor: pointer;
          display: inline-block;
          background-image: url('../../assets/images/download.png');
          background-size: 14px 14px;
          background-repeat: no-repeat;
        }
      }
    }
    .cl-data-map.full-screen {
      position: absolute;
      top: 0;
      bottom: 0;
      left: 0;
      right: 0;
      width: auto;
      height: auto;
      padding: 0;
      .data-map-container {
        width: 100%;
      }
      #sidebar {
        .node-info-con {
          height: calc(100% - 280px);
        }
      }
    }
    #sidebar.right-hide {
      right: -442px;
    }
    #sidebar {
      position: absolute;
      right: 0;
      top: 0;
      width: 442px;
      height: 100%;
      border-radius: 6px;
      text-align: left;
      background-color: #ffffff;
      display: inline-block;
      box-shadow: 0 1px 3px 0px rgba(0, 0, 0, 0.1);
      color: #333333;
      font-size: 14px;
      line-height: 14px;
      padding: 24px 32px;
      div,
      span,
      pre {
        font-size: 14px;
      }
      .title {
        padding: 24px 0;
        font-size: 14px;
        color: #333333;
        img {
          float: right;
          margin-right: 10px;
          cursor: pointer;
        }
      }
      .node-info-container {
        height: calc(100% - 156px);
      }
      .node-info-container-long {
        height: calc(100% - 62px);
      }
      .node-info {
        .title {
          padding: 0 0 24px;
          font-size: 14px;
          color: #333;
        }
        .node-info-list {
          height: calc(100% - 62px);
          overflow-y: auto;
        }
        .item {
          line-height: 20px;
          padding: 5px 0 5px 20px;
          background-color: #f2f2f2;
          .label {
            vertical-align: top;
            width: 30%;
            word-break: break-all;
            display: inline-block;
          }
          .value {
            padding: 0 10px;
            vertical-align: top;
            display: inline-block;
            width: 70%;
            word-break: break-all;
          }
        }
      }
      .legend {
        .legend-content {
          background-color: #f7faff;
          padding: 0 32px;
          height: 94px;
          overflow-y: auto;
        }
        .legend-item {
          padding: 5px 0;
          display: inline-block;
          width: 50%;
          font-size: 14px;
          line-height: 20px;
          .pic {
            width: 45px;
            text-align: center;
            img {
              width: 45px;
              height: 15px;
              margin-left: -20px;
              vertical-align: middle;
            }
          }
          div {
            display: inline-block;
            padding-left: 20px;
            vertical-align: middle;
          }
        }
      }
      .toggle-right {
        position: absolute;
        top: calc(50% - 43px);
        left: -16px;
        width: 18px;
        height: 86px;
        cursor: pointer;
        background-image: url('../../assets/images/toggle-right-bg.png');
      }
      .icon-toggle {
        width: 6px;
        height: 9px;
        background-image: url('../../assets/images/toggle-right-icon.png');
        position: absolute;
        top: calc(50% - 4.5px);
        left: calc(50% - 3px);
      }
      .icon-toggle.icon-left {
        transform: rotateY(180deg);
      }
    }
    .data-map-container.all {
      width: 100%;
    }
    // No data available.
    .image-noData {
      // Set the width and white on the right.
      width: 100%;
      height: 100%;
      background: #fff;
      position: absolute;
      top: 0;
      left: 0;
      display: flex;
      justify-content: center;
      align-items: center;
      flex-direction: column;
      z-index: 200;
    }
    .noData-text {
      margin-top: 33px;
      font-size: 18px;
    }
  }
  .cl-close-btn {
    width: 20px;
    height: 20px;
    vertical-align: -3px;
    cursor: pointer;
    display: inline-block;
  }
  .cl-title-right {
    padding-right: 32px;
  }
}
</style>
