<!--
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>
  <div class="cl-histogram-container">
    <div class="data-show-container">
      <div :id="itemId"
           v-show="!!fullData.length"
           class="data-item"></div>
    </div>
  </div>
</template>

<script>
import echarts from 'echarts';
import CommonProperty from '../common/common-property';
import {format, precisionRound} from 'd3';
const d3 = {format, precisionRound};
export default {
  props: {
    // histogram data
    fullData: {
      type: Array,
      default() {
        return [];
      },
    },
    // view name
    viewName: {
      type: Number,
      default: 1,
    },
    // axis name
    axisName: {
      type: Number,
      default: 0,
    },
    // Display full screen
    fullScreen: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    return {
      itemId: '', // Dom id
      oriData: {}, // Original data
      charOption: {}, // Chart configuration
      charObj: null, // chart Object
      updated: false, // Updated
      zrDrawElement: {hoverDots: []},
      zr: null,
      chartTipFlag: false, // Wheather to display tips of the histogram
    };
  },
  computed: {},
  watch: {},
  mounted() {
    this.init();
  },
  methods: {
    /**
     * Initialize
     */
    init() {
      this.itemId =
        `${new Date().getTime()}` + `${this.$store.state.componentsCount}`;
      this.$store.commit('componentsNum');
    },
    /**
     * Update the view Size
     */
    resizeView() {
      if (this.charObj) {
        this.charObj.resize();
      }
    },
    /**
     * Convert to chart data
     */
    formatDataToChar() {
      const chartData = this.fullData;
      const seriesData = [];
      let maxX = -Infinity;
      let minX = Infinity;
      let maxZ = -Infinity;
      let minZ = Infinity;
      const gridData = [];
      if (chartData && chartData.length) {
        chartData.forEach((histogram) => {
          const seriesItem = [];
          gridData.push(histogram.step);
          histogram.items.forEach((bucket) => {
            if (this.viewName === 0) {
              seriesItem.push([bucket[2], bucket[3]]);
            } else if (this.viewName === 1) {
              seriesItem.push(bucket[2], histogram.step, bucket[3]);
            }
            maxX = Math.max(maxX, bucket[2]);
            minX = Math.min(minX, bucket[2]);
            minZ = Math.min(minZ, bucket[3]);
            maxZ = Math.max(maxZ, bucket[3]);
          });
          seriesData.push(seriesItem);
        });
      }
      this.oriData = {
        seriesData,
        maxX,
        minX,
        maxZ,
        minZ,
        gridData,
      };
    },
    /**
     * update sample data
     */
    updateSampleData() {
      this.charOption = this.formatCharOption();
      if (!this.charObj) {
        const chartItem = document.getElementById(this.itemId);
        if (!chartItem) {
          return;
        }
        this.charObj = echarts.init(chartItem, null);
      }
      this.removeTooltip();
      this.charObj.setOption(this.charOption, true);
    },
    /**
     * Binding interaction event
     */
    sampleEventBind() {
      if (!this.zr) {
        this.zr = this.charObj.getZr();
        this.zr.off('mouseout', 'mousemove');
        this.zr.on('mouseout', (e) => {
          this.removeTooltip();
          this.chartTipFlag = false;
          this.$emit('chartTipFlagChange', this.chartTipFlag);
        });
        this.zr.on('mousemove', (e) => {
          this.removeTooltip();
          this.mousemoveEvent(e);
        });
      }
    },
    /**
     * Formate chart option
     * @return {Object} chatr option
     */
    formatCharOption() {
      const colorMin = '#346E69';
      const colorMax = '#EBFFFD';
      const oriData = this.oriData;
      const colorArr = this.getGrientColor(
          colorMin,
          colorMax,
          oriData.seriesData.length,
      );
      const fullScreenFun = this.toggleFullScreen;
      const axisName = this.axisName;
      const that = this;
      const option = {
        grid: {
          left: 24,
          top: 60,
          right: 80,
          bottom: 60,
        },
        xAxis: {
          max: oriData.maxX,
          min: oriData.minX,
          axisLine: {onZero: false},
          axisLabel: {
            fontSize: '11',
            formatter: function(value) {
              return that.formateNUmber(value);
            },
          },
          splitLine: {show: false},
        },
        yAxis: {
          position: 'right',
          axisLine: {onZero: false, show: false},
          splitLine: {show: true},
          axisTick: {show: false},
          boundaryGap: false,
          axisLabel: {
            fontSize: '11',
            formatter: function(value) {
              return that.formateNUmber(value);
            },
          },
        },
        toolbox: {
          top: 20,
          right: 20,
          emphasis: {
            iconStyle: {
              textPosition: 'top',
            },
          },
          // toolbox
          feature: {
            // fullScreen
            myToolFullScreen: {
              show: true,
              title: this.$t('histogram.fullScreen'),
              iconStyle: {
                borderColor: this.fullScreen ? '#3E98C5' : '#6D7278',
              },
              icon: CommonProperty.fullScreenIcon,
              onclick() {
                fullScreenFun();
              },
            },
          },
        },
      };
      if (this.viewName === 1) {
        const seriesData = [];
        oriData.seriesData.forEach((item, dataIndex) => {
          const dataItem = {
            name: item[1],
            value: item,
            itemStyle: {
              color: colorArr[dataIndex],
            },
          };
          seriesData.push(dataItem);
        });
        option.series = [
          {
            type: 'custom',
            dimensions: ['x', 'y'],
            renderItem: (params, api) => {
              const points = this.makePolyPoints(
                  params.dataIndex,
                  api.coord,
                  params.coordSys.y - 10,
              );

              return {
                type: 'polyline',
                silent: true,
                shape: {
                  points,
                },
                style: api.style({
                  stroke: '#bbb',
                  lineWidth: 1,
                }),
              };
            },
            data: seriesData,
          },
        ];
        option.yAxis.data = oriData.gridData;
        option.yAxis.type = 'category';
        option.grid.top = 126;
        if (axisName === 2 && this.fullScreen) {
          option.grid.right = 140;
        }
        option.yAxis.inverse = true;
        option.yAxis.axisLabel.formatter = function(value) {
          return that.yAxisFormatter(value);
        };
      } else if (this.viewName === 0) {
        option.color = colorArr;
        option.series = [];
        oriData.seriesData.forEach((k) => {
          option.series.push({
            type: 'line',
            symbol: 'none',
            lineStyle: {
              width: 1,
            },
            data: k.slice(1, -1),
          });
        });
      }
      return option;
    },
    /**
     * Expand/Collapse in full screen
     */
    toggleFullScreen() {
      this.removeTooltip();
      if (!this.fullScreen) {
        if (this.axisName === 2) {
          this.charOption.grid.right = 140;
        }
        this.charOption.toolbox.feature.myToolFullScreen.iconStyle.borderColor =
          '#3E98C5';
      } else {
        this.charOption.grid.right = 80;
        this.charOption.toolbox.feature.myToolFullScreen.iconStyle.borderColor =
          '#6D7278';
      }
      this.charObj.setOption(this.charOption);
      this.$emit('toggleFullScreen');
    },
    /**
     * remove tooltip
     */
    removeTooltip() {
      if (this.zr) {
        if (this.zrDrawElement.hoverDots) {
          this.zrDrawElement.hoverDots.forEach((dot) => this.zr.remove(dot));
        }
        if (this.zrDrawElement.hoverLine) {
          this.zr.remove(this.zrDrawElement.hoverLine);
        }
        if (this.zrDrawElement.tooltip) {
          this.zr.remove(this.zrDrawElement.tooltip);
        }
        if (this.zrDrawElement.tooltipY) {
          this.zr.remove(this.zrDrawElement.tooltipY);
        }
        if (this.zrDrawElement.tooltipX) {
          this.zr.remove(this.zrDrawElement.tooltipX);
        }
      }
    },
    /**
     * Calculate polygon points
     * @param {Number} dataIndex
     * @param {Object} getCoord
     * @param {Number} yValueMapHeight
     * @return {Array} Array of ploygon points
     */
    makePolyPoints(dataIndex, getCoord, yValueMapHeight) {
      const points = [];
      const rawData = this.oriData.seriesData;
      const maxZ = this.oriData.maxZ;
      const minZ = this.oriData.minZ;
      for (let i = 0; i < rawData[dataIndex].length; ) {
        const x = this.getValue(rawData, dataIndex, i++);
        const y = this.getValue(rawData, dataIndex, i++);
        const z = this.getValue(rawData, dataIndex, i++);
        const pt = getCoord([x, y]);
        // linear map in z axis
        if (maxZ !== minZ) {
          pt[1] -= ((z - minZ) / (maxZ - minZ)) * yValueMapHeight;
        }
        points.push(pt);
      }
      return points;
    },
    /**
     * get convert point
     * @param {Array} pt value
     * @return {Array}
     */
    getCoord(pt) {
      return this.charObj.convertToPixel('grid', pt);
    },
    /**
     * Formate Y coordinate display
     * @param {Number} value
     * @return {Object}
     */
    yAxisFormatter(value) {
      let data = '';
      const filter = this.fullData.filter((k) => k.step === value);
      if (filter.length) {
        if (this.axisName === 2) {
          data = this.fullScreen
            ? this.dealrelativeTime(
                new Date(filter[0].wall_time * 1000).toString(),
            )
            : [];
        } else if (this.axisName === 1) {
          data = `${this.formateNUmber(
              (filter[0].relative_time / 3600).toFixed(3),
          )}h`;
        } else {
          data = this.formateNUmber(filter[0].step);
        }
      }
      return data;
    },
    /**
     * Formate time display
     * @param {Number} value
     * @return {Number} Formatted number
     */
    formateNUmber(value) {
      value = Number(value);
      if (value.toString().length > 6) {
        return value.toExponential(3);
      } else {
        return Math.round(value * 1000) / 1000;
      }
    },
    /**
     * Formate time display
     * @param {Object} time
     * @return {String} Formatted time
     */
    dealrelativeTime(time) {
      const arr = time.split(' ');
      const str = arr[0] + ' ' + arr[1] + ' ' + arr[2] + ',' + ' ' + arr[4];
      return str;
    },
    /**
     * Mouse move event
     * @param {Object} e Original event
     */
    mousemoveEvent(e) {
      const unit = 's';
      const nearestIndex = this.findNearestValue([e.offsetX, e.offsetY]);
      if (
        nearestIndex &&
        nearestIndex.yIndex !== null &&
        nearestIndex.binIndex !== null
      ) {
        const {binIndex, yIndex} = nearestIndex;
        const chartData = this.fullData;
        const hoveredItem = chartData[yIndex];
        const p = Math.max(0, d3.precisionRound(0.01, 1.01) - 1);
        const yValueFormat = d3.format(`.${p}e`);
        const gridRect = this.charObj
            .getModel()
            .getComponent('grid', 0)
            .coordinateSystem.getRect();
        const gridRectY = gridRect.y - 10;
        let linePoints = [];
        if (!hoveredItem || !hoveredItem.items[binIndex]) {
          return;
        }
        if (!this.chartTipFlag) {
          this.chartTipFlag = true;
          this.$emit('chartTipFlagChange', this.chartTipFlag);
        }
        if (this.viewName === 1 && yIndex !== null) {
          linePoints = this.makePolyPoints(yIndex, this.getCoord, gridRectY);
        } else if (this.viewName === 0 && hoveredItem.items) {
          hoveredItem.items.forEach((item) => {
            linePoints.push(this.getCoord([item[2], item[3]]));
          });
        }

        this.zrDrawElement.hoverLine = new echarts.graphic.Polyline({
          silent: true,
          shape: {
            points: linePoints.slice(1, -1),
          },
          z: 999,
        });
        this.zr.add(this.zrDrawElement.hoverLine);

        this.zrDrawElement.tooltip = new echarts.graphic.Text({});
        let itemX;
        const x = hoveredItem.items[binIndex][2];
        let z = 0;
        chartData.forEach((dataItem, index) => {
          const y = dataItem.step;
          const pt = this.getCoord([x, y]);
          if (index === yIndex) {
            z = hoveredItem.items[binIndex][3];
          } else {
            const items = dataItem.items;
            for (let k = 1; k < items.length - 1; k++) {
              const nextX = items[k + 1][2];
              const nextZ = items[k + 1][3];
              if (items[k][2] === x) {
                z = items[k][3];
                break;
              } else if (items[k][2] < x && nextX > x) {
                const proportionX = (x - items[k][2]) / (nextX - items[k][2]);
                z = (nextZ - items[k][3]) * proportionX + items[k][3];
                break;
              }
            }
          }
          itemX = pt[0];
          const circleOption = {
            z: 1000,
          };
          if (this.viewName === 1) {
            pt[1] -=
              ((z - this.oriData.minZ) /
                (this.oriData.maxZ - this.oriData.minZ)) *
              gridRectY;
            circleOption.shape = {
              cx: itemX,
              cy: pt[1],
              r: 1.5,
            };
          } else {
            circleOption.shape = {
              cx: 0,
              cy: 0,
              r: 1.5,
            };
            circleOption.position = this.charObj.convertToPixel('grid', [x, z]);
          }
          const dot = new echarts.graphic.Circle(circleOption);
          this.zr.add(dot);
          this.zrDrawElement.hoverDots.push(dot);
        });
        this.zrDrawElement.tooltip = new echarts.graphic.Text({});

        let htmlStr = '';
        const hoveredAxis = hoveredItem.items[binIndex][3];
        htmlStr = `<td>${
          hoveredAxis.toString().length >= 6
            ? yValueFormat(hoveredAxis)
            : hoveredAxis
        }</td><td style="text-align:center;">${this.formateNUmber(
            hoveredItem.step,
        )}</td><td>${this.formateNUmber(
            (hoveredItem.relative_time / 1000).toFixed(3),
        )}${unit}</td><td>${this.dealrelativeTime(
            new Date(hoveredItem.wall_time * 1000).toString(),
        )}</td>`;
        const dom = document.querySelector('#tipTr');
        dom.innerHTML = htmlStr;
        const chartElement = document.getElementById(this.itemId);
        if (chartElement) {
          if (!this.fullScreen) {
            const chartWidth =
              chartElement.parentNode.parentNode.parentNode.parentNode
                  .clientWidth;
            const chartHeight =
              chartElement.parentNode.parentNode.parentNode.parentNode
                  .clientHeight;
            const left =
              chartElement.parentNode.parentNode.parentNode.parentNode
                  .offsetLeft;
            const top =
              chartElement.parentNode.parentNode.parentNode.parentNode
                  .offsetTop;
            const echartTip = document.querySelector('#echartTip');
            echartTip.style.top = `${top + chartHeight - 60}px`;
            if (left > echartTip.clientWidth) {
              echartTip.style.left = `${left - echartTip.clientWidth}px`;
            } else {
              echartTip.style.left = `${left + chartWidth}px`;
            }
          } else {
            const width = document.querySelector('#echartTip').clientWidth;
            const height = document.querySelector('#echartTip').clientHeight;
            const screenWidth = document.body.scrollWidth;
            const screenHeight = document.body.scrollHeight;
            const scrollTop = document.querySelector('.cl-show-data-content')
                .scrollTop;
            const offsetTop = document.querySelector('.cl-show-data-content')
                .offsetTop;
            if (
              height + e.event.y + 20 > screenHeight &&
              screenHeight > height
            ) {
              document.querySelector('#echartTip').style.top = `${e.event.y +
                scrollTop -
                height -
                20 -
                offsetTop}px`;
            } else {
              document.querySelector('#echartTip').style.top = `${e.event.y +
                scrollTop +
                20 -
                offsetTop}px`;
            }
            // Blank area on the right of the chart is 80
            if (width + e.event.x + 80 > screenWidth && screenWidth > width) {
              document.querySelector('#echartTip').style.left = `${e.event.x -
                width -
                20}px`;
            } else {
              document.querySelector('#echartTip').style.left = `${e.event.x +
                20}px`;
            }
          }
        }

        this.zrDrawElement.tooltipX = new echarts.graphic.Text({
          position: [itemX, gridRect.y + gridRect.height],
          style: {
            text:
              x.toString().length >= 6
                ? x.toExponential(3)
                : Math.round(x * 1000) / 1000,
            textFill: '#fff',
            textAlign: 'center',
            fontSize: 12,
            textBackgroundColor: '#333',
            textBorderWidth: 2,
            textPadding: [5, 7],
            rich: {},
          },
          z: 2000,
        });
        this.zr.add(this.zrDrawElement.tooltipX);
        if (this.viewName === 1 && linePoints && linePoints.length) {
          let text = '';
          if (yIndex !== null) {
            text = this.yAxisFormatter(hoveredItem.step);
          }
          this.zrDrawElement.tooltipY = new echarts.graphic.Text({
            position: [
              gridRect.x + gridRect.width,
              linePoints[linePoints.length - 1][1],
            ],
            style: {
              text: text,
              textFill: '#fff',
              textVerticalAlign: 'middle',
              fontSize: 12,
              textBackgroundColor: '#333',
              textBorderWidth: 2,
              textPadding: [5, 7],
              rich: {},
            },
            z: 2000,
          });
          this.zr.add(this.zrDrawElement.tooltipY);
        }
      }
    },
    /**
     * find nearest value
     * @param {Array} eventPoint value
     * @return {Object}
     */
    findNearestValue(eventPoint) {
      if (!eventPoint || !eventPoint.length || !this.charObj || !this.oriData) {
        return;
      }
      const value = this.charObj.convertFromPixel('grid', eventPoint);
      if (!value || !value.length) {
        return;
      }
      let binIndex = null;
      let yIndex = null;
      let nearestX = Infinity;
      let nearestY = -Infinity;
      let nearestYData = Infinity;
      const gridRect = this.charObj
          .getModel()
          .getComponent('grid', 0)
          .coordinateSystem.getRect();
      const gridRectY = gridRect.y - 10;
      const x = value[0];
      this.fullData.forEach((dataItem, i) => {
        let distY;
        let yAxis;
        for (let k = 0; k < dataItem.items.length - 1; k++) {
          const item = dataItem.items[k];
          const itemNext = dataItem.items[k + 1];
          const nextX = itemNext[2];
          const nextZ = itemNext[3];
          if (item.length >= 4) {
            if (item[2] < x && nextX >= x) {
              const proportionX = (x - item[2]) / (nextX - item[2]);
              yAxis = (nextZ - item[3]) * proportionX + item[3];
              distY = Math.abs(value[1] - yAxis);
              break;
            }
          }
        }
        if (this.viewName === 0 && distY < nearestYData) {
          nearestYData = distY;
          yIndex = i;
        } else if (this.viewName === 1) {
          const pt = this.getCoord([x, dataItem.step]);
          const ptStep = pt[1];
          pt[1] -=
            ((yAxis - this.oriData.minZ) /
              (this.oriData.maxZ - this.oriData.minZ)) *
            gridRectY;
          if (
            eventPoint[1] > pt[1] &&
            eventPoint[1] < ptStep &&
            ptStep > nearestY
          ) {
            nearestY = ptStep;
            yIndex = i;
          }
        }
      });
      if (yIndex === null && this.viewName === 1) {
        this.fullData.forEach((item, index) => {
          if (index >= value[1]) {
            yIndex = yIndex === null ? index : Math.min(yIndex, index);
          }
        });
      }
      if (yIndex !== null) {
        const yData = this.fullData[yIndex].items;
        yData.forEach((ele, index) => {
          const distX = Math.abs(ele[2] - value[0]);
          if (distX < nearestX) {
            nearestX = distX;
            binIndex = index;
          }
        });
        binIndex =
          binIndex === 0
            ? 1
            : binIndex === yData.length - 1
            ? yData.length - 2
            : binIndex;
      }
      return {
        binIndex,
        yIndex,
      };
    },
    /**
     * Calculate gradient color
     * @param {String} startColor
     * @param {String} endColor
     * @param {Number} step
     * @return {Array} Array of gradient color
     */
    getGrientColor(startColor, endColor, step) {
      const startRgb = this.formatColor(startColor);
      const endRgb = this.formatColor(endColor);
      const gapRgbR = (endRgb[0] - startRgb[0]) / step;
      const gapRgbG = (endRgb[1] - startRgb[1]) / step;
      const gapRgbB = (endRgb[2] - startRgb[2]) / step;
      const colorResult = [];
      for (let i = 0; i < step; i++) {
        const sR = parseInt(gapRgbR * i + startRgb[0]);
        const sG = parseInt(gapRgbG * i + startRgb[1]);
        const sB = parseInt(gapRgbB * i + startRgb[2]);
        const hex = this.formatColorToHex(`rgb(${sR},${sG},${sB})`);
        colorResult.push(hex);
      }
      return colorResult;
    },
    /**
     * Converts a color string to recognizable format
     * @param {String} str Color string
     * @return {String}
     */
    formatColor(str) {
      if (!str) {
        return;
      }
      const colorReg = /^([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
      let colorStr = str.toLowerCase().slice(1);
      if (colorReg.test(colorStr)) {
        let colorStrNew = '';
        if (colorStr.length === 3) {
          for (let i = 0; i < 3; i++) {
            colorStrNew += colorStrNew
                .slice(i, i + 1)
                .concat(colorStrNew.slice(i, i + 1));
          }
          colorStr = colorStrNew;
        }
        const colorFormat = [];
        for (let i = 0; i < 6; i += 2) {
          colorFormat.push(parseInt(`0x${colorStr.slice(i, i + 2)}`));
        }
        return colorFormat;
      } else {
        return colorStr;
      }
    },
    /**
     * Converts rgb color string to hex
     * @param {String} rgb Rgb color
     * @return {String} Hex color
     */
    formatColorToHex(rgb) {
      const regRgb = /^(rgb|RGB)/g;
      if (regRgb.test(rgb)) {
        const colorSplit = rgb.replace(/(?:(|)|rgb|RGB)*/g, '').split(',');
        let hexStr = '';
        for (let i = 0; i < colorSplit.length; i++) {
          let hexItem = Number(colorSplit[i]).toString(16);
          hexItem = hexItem < 10 ? `0${hexItem}` : hexItem;
          if (hexItem === '0') {
            hexItem += hexItem;
          }
          hexStr += hexItem;
        }
        if (hexStr.length !== 6) {
          hexStr = rgb;
        }
        return hexStr;
      }
    },
    /**
     * Get value
     * @param {Object} seriesData
     * @param {Number} dataIndex
     * @param {Number} i
     * @return {Number}
     */
    getValue(seriesData, dataIndex, i) {
      return seriesData[dataIndex][i];
    },
    /**
     * Unbing event
     */
    clearZrData() {
      if (this.zr) {
        this.removeTooltip();
        this.zr.off('mouseout', 'mousemove');
        this.zr = null;
      }
    },
    /**
     * Update histogram data
     */
    updateHistogramData() {
      this.$nextTick(() => {
        this.formatDataToChar();
        this.updateSampleData();
        this.sampleEventBind();
      });
    },
  },
  destroyed() {
    this.clearZrData();
  },
};
</script>
<style lang="scss">
.cl-histogram-container {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  .data-show-container {
    width: 100%;
    flex: 1;
    .data-item {
      width: 100%;
      height: 100%;
    }
  }
}
</style>
