import React from 'react';
import * as d3Scale from 'd3-scale';
import * as d3Array from 'd3-array';
import * as d3Color from 'd3-color';
import * as d3Format from 'd3-format';
import * as d3Interpolate from 'd3-interpolate';
import * as d3Shape from 'd3-shape';
import * as d3Timer from 'd3-timer';
import * as d3Ease from 'd3-ease';
import {
  extractBidPrices,
  extractBidVolumes,
  extractMaxTradedVolume,
  extractAvgTradedVolume,
  extractMaxVolume,
  zoomTimeFormat,
  extractMaxAskBidVolume
} from "./utils";

export const d3 = Object.assign(
  Object.assign(
    Object.assign({}, d3Scale, d3Array, d3Color)
    , d3Format, d3Interpolate, d3Shape
  )
  , d3Ease, d3Timer
);

/**
 * Stock Heatmap
 * @author Rounak Saha
 *
 * © Copyright 2020, Rounak Saha
 */
export default class StockHeatmap extends React.Component {

  /** @type {React.RefObject<HTMLCanvasElement>} */
  canvasRef = React.createRef();
  /** @type {CanvasRenderingContext2D} */
  drawingContext = null;

  data = [];
  windowedData = [];
  windowLength = 20;
  windowPosition = 0;

  mouse = {
    x: 0,
    y: 0
  }

  circles = []

  /** Default Theme colors and dimensions */
  defaults = {
    borderPadding: [5, 5, 0, 0],
    bidAskWidth: 100,
    axisYWidth: 50,
    axisXHeight: 50,
    buyColor: '#388e3c',
    textOnBuyColor: '#ffffff',
    sellColor: '#d32f2f',
    textOnSellColor: '#ffffff',
    textOnBackground: '#000000',
    textHighlightOnBackground: '#ff0000',
    tradeColor: '#7434eb',
    axisTickSize: 6,
    axisColor: '#000000',
    xAxisTextPadding: 6,
    yAxisTextPadding: 6,
    bidAskGraphPaddingLeft: 10,
    bidAskTransitionDuration: 500,
    volumeCircleMaxRadius: 1,
    runningRatioSeconds: 5,
    hmWidth: () => (this.props.width - this.defaults.borderPadding[1] - this.defaults.borderPadding[3] - this.defaults.bidAskWidth - this.defaults.axisYWidth),
    hmHeight: () => (this.props.height - this.defaults.borderPadding[0] - this.defaults.borderPadding[2] - this.defaults.axisXHeight),
    clearColor: '#ffffff',
  };

  shouldComponentUpdate(nextProps, nextState) {
    // console.log('shouldComponentUpdate', nextProps);
    const shouldUpdate = this.props.width !== nextProps.width
      || this.props.height !== nextProps.height;
    if (shouldUpdate) {
      this.detachMouseListeners();
    }
    return shouldUpdate;
  }

  // -------------------START:: Lifecycle methods to retrive 2d context from updated dom-------------------------
  componentDidMount() {
    // console.log('component mouted');
    if (this.canvasRef.current !== null) {
      this.drawingContext = this.canvasRef.current.getContext('2d');
      this.updateHeatmap();
      this.attachMouseListeners();
    }

    const panel = this
    setInterval(() => {
      panel.updateHeatmap()
    }, 10)
  }
  componentDidUpdate() {
    // console.log('component updtated');
    if (this.canvasRef.current !== null) {
      this.drawingContext = this.canvasRef.current.getContext('2d');
      this.updateHeatmap();
      this.attachMouseListeners();
    }
  }
  componentWillUnmount() {
    this.detachMouseListeners();
  }
  // -------------------END:: Lifecycle methods to retrive 2d context from updated dom---------------------------


  // ------------------ START:: Mouse Event listeners -------------------
  isMouseDown = false;
  mouseDownX = 0;

  /**
   * Attaches mouse interaction event listeners
   */
  attachMouseListeners = () => {
    if (this.canvasRef.current !== null) {
      this.canvasRef.current.addEventListener('mousedown', this.eventMouseDown);
      this.canvasRef.current.addEventListener('mousemove', this.eventMouseMove);
      this.canvasRef.current.addEventListener('mouseup', this.eventMouseUp);
      this.canvasRef.current.addEventListener('wheel', this.eventZoomWheel);
      this.canvasRef.current.addEventListener('keydown', this.eventKeyDown);
    }
  }

  /**
   * Detaches mouse interaction event listeners
   */
  detachMouseListeners = () => {
    if (this.canvasRef.current !== null) {
      this.canvasRef.current.removeEventListener('mousedown', this.eventMouseDown);
      this.canvasRef.current.removeEventListener('mousemove', this.eventMouseMove);
      this.canvasRef.current.removeEventListener('mouseup', this.eventMouseUp);
      this.canvasRef.current.removeEventListener('wheel', this.eventZoomWheel);
      this.canvasRef.current.removeEventListener('keydown', this.eventKeyDown);
    }
  }

  /**
   * Mouse down event on canvas
   * @param {MouseEvent} e
   */
  eventMouseDown = (e) => {
    // console.log('eventMouseDown', e);
    if (!this.isMouseDown) {
      this.isMouseDown = true;
      this.mouseDownX = e.clientX;
    }
  }

  /**
   * Mouse move event on canvas
   * @param {MouseEvent} e
   */
  eventMouseMove = (e) => {
    // 判断鼠标拖拽距离，这个只是判断拖拽是否满足阈值，使其画面滚动暂停
    const downDragLength = Math.abs(e.clientX - this.mouseDownX);
    if (this.isMouseDown && downDragLength > 200) {
      this.props.toggleAutoScroll(false)
    }

    // 其他事件处理
    if (this.isMouseDown && this.xScale) {
      // Mouse drag, scroll the time series，距离上一次移动视野的鼠标拖拽距离
      const dragX = e.clientX - this.mouse.x;
      const moveDataPointsCount = Math.floor(Math.abs(dragX) / this.xScale.bandwidth());
      if (moveDataPointsCount > 0) this.mouse.x = e.x
      // const moveDataPointDirection = dragLength >= 0 ? 'right' : 'left';
      // console.log('drag x=', dragLength, moveDataPointsCount, this.windowPosition);
      this.moveDataWindow(this.windowPosition + moveDataPointsCount * (dragX >= 0 ? -1 : 1));
    } else {
      if (e.clientX < this.defaults.hmWidth() && e.clientY < this.defaults.hmHeight()) {
        this.mouse = {
          x: e.clientX,
          y: e.clientY
        };
      }
    }
  }

  /**
   * Mouse up event on canvas
   * @param {MouseEvent} e
   */
  eventMouseUp = (e) => {
    // console.log('eventMouseUp',e);
    this.isMouseDown = false;
    this.mouseDownX = 0;
  }

  /**
   * Wheel event on canvas to zoom
   * @param {WheelEvent} e
   */
  eventZoomWheel = (e) => {
    const direction = e.deltaY < 0 ? 'zoom-in' : 'zoom-out';
    let l = 0, l2 = 0;
    switch (direction) {
      case 'zoom-in':
        l = Math.max(this.windowLength - 16, 3);
        break;
      case 'zoom-out':
        l = Math.min(this.windowLength + 16, this.data.length - 16);
        break;
    }
    l2 = this.windowLength - l;
    this.windowLength = l;
    this.moveDataWindow(this.windowPosition + l2);
    // console.log('zoom Level=', this.windowLength);
  }

  /**
   * Event to be triggered when keyboard key is pressed
   * @param {KeyboardEvent} e
   */
  eventKeyDown = (e) => {
    e.preventDefault();
    console.log('key event', e.isComposing, e.key, e.ctrlKey);
    switch (e.key) {
      case 'ArrowLeft':
        this.moveDataWindow(this.windowPosition - (e.ctrlKey ? 10 : 1));
        break;
      case 'ArrowRight':
        this.moveDataWindow(this.windowPosition + (e.ctrlKey ? 10 : 1));
        break;
    }
  }

  // ------------------ END:: Mouse Event listeners ---------------------


  // ------------------ D3 Variables ---------------------
  /** @type {d3Scale.ScaleBand<string>} */
  xScale = null;
  /** @type {d3Scale.ScaleLinear<number, number>} */
  bidAskScale = null;
  /** @type {d3Scale.ScaleBand<string>} */
  yScale = null;
  /** @type {number[]} */
  yDomainValues = null;
  /** @type {d3Timer.Timer} */
  bidAskAnimTimer = null;
  // ------------------ D3 Variables ---------------------

  /**
   * This function will be called if there is any dimension change on heatmap
   * This function changes the d3 scales based on windowed data
   */
  updateHeatmapDimensions = () => {
    const { width, height } = this.props;
    if (width > 0 && height > 0 && this.windowedData.length > 0) {
      // setup x-scale
      this.xScale = d3.scaleBand()
        .range([0, this.defaults.hmWidth()])
        .domain(this.windowedData.map(d => d.ts));
      // setup y-scale
      this.yDomainValues = extractBidPrices(this.windowedData).sort((a, b) => a - b);
      this.yScale = d3.scaleBand()
        .range([this.defaults.hmHeight(), 0])
        .domain(this.yDomainValues);
      // setup bid ask scale
      this.bidAskScale = d3.scaleLinear()
        .range([0, this.defaults.bidAskWidth])
        .domain([0, d3.max(extractBidVolumes(this.windowedData[this.windowedData.length - 1]))]);
    }
  }


  /**
   * This method will be called after an update of internal data is performed.
   */
  updateHeatmap = () => {
    if (this.drawingContext !== null) {
      // 1. update scale and dimensions
      this.updateHeatmapDimensions();

      // 2. Draw the bid ask spread heatmap
      this.clearCanvas(this.defaults.borderPadding[3], this.defaults.borderPadding[0],
        this.defaults.hmWidth(), this.defaults.hmHeight(), this.defaults.clearColor);
      this.drawMainGraph();

      // 3. Draw xy Axis
      this.drawXAxis();
      this.drawYAxisAndBidAskGraph();

      // 4. Draw buy-to-sell ratio
      this.drawBuy2SellRatio();

      // 5. Draw mouse
      this.drawMouse();

      // console.log('heatmap draw update');
      // this.clearCanvas(0, 0, this.defaults.hmWidth(), this.defaults.hmHeight(), '#aaaaaa');
    }
  }

  // ------------------------------ START: Canvas draw functions ---------------------------------------
  getMousePos = (canvas, x, y) =>  {
    var rect = canvas.getBoundingClientRect();

    // 使用容器的内部宽度进行计算，排除 padding 影响
    var style = window.getComputedStyle(canvas.parentNode);
    var paddingLeft = parseFloat(style.paddingLeft) || 0;
    var paddingRight = parseFloat(style.paddingRight) || 0;
    var effectiveWidth = rect.width - paddingLeft - paddingRight;

    return {
      x: (x - rect.left - paddingLeft) * (canvas.width / effectiveWidth) * 1.03,
      y: (y - rect.top) * (canvas.height / rect.height)
    };
  }
  drawMouse = () => {
    const canvas = this.canvasRef.current;
    if (!canvas) return;

    const context = canvas.getContext('2d');
    if (!context) return;

    let mouse = this.getMousePos(canvas, this.mouse.x, this.mouse.y)

    const x = mouse.x;
    const y = mouse.y;

    // 移动事件处理，指上圆球事件处理
    let drawPending = []
    this.circles.forEach(circle => {
      const distance = Math.sqrt(Math.pow(x - circle.x, 2) + Math.pow(y - circle.y, 2));
      if (distance <= circle.radius) {
        let d = circle.data
        drawPending.push(d)
      }
    });
    if (drawPending.length > 0) {
      const WIDTH = 100
      const HEIGHT = drawPending.length * 45

      // 绘制方块
      let color = d3.color('#fff').rgb();
      color.opacity = 0.7
      this.drawingContext.fillStyle = color.toString();
      this.drawingContext.fillRect(
        x + 2,
        y + 2,
        WIDTH,
        HEIGHT
      );

      // 绘制方块内数据
      drawPending.map((d, index) => {
        let depth = d.marketDepth;
        let text = `${depth.side === 'sell' ? '卖出' : '买入'} ${depth.lastTradedQty} 在 ${depth.lastTradedPrice} `;
        context.fillStyle = d3.color(depth.side == 'sell' ? '#222' : '#222').rgb();
        context.fillText(`${d.ts}`, x + 10, y + 7 + 15 + index * 45);
        context.fillText(text, x + 10, y + 7 + 30 + index * 45);
        if (index < drawPending.length - 1) {
          context.fillText("--------------", x + 10, y + 7 + 45 + index * 45);
        }
      })
    }
    //
    // // 清除canvas并重新绘制
    // context.clearRect(0, 0, canvas.width, canvas.height);

    // 绘制交叉线和文本
    context.beginPath();
    context.moveTo(x, 30);
    context.lineTo(x, this.defaults.hmHeight());
    context.moveTo(30, y);
    context.lineTo(this.defaults.hmWidth(), y);
    context.strokeStyle = '#000';
    context.stroke();
  }

  /**
   * Draw buy/sell ratio at bottom right corner
   */
  drawBuy2SellRatio = () => {
    if (this.windowedData.length > 0) {
      // dimension
      const d = this.windowedData[this.windowedData.length - 1];
      const x = this.defaults.borderPadding[3] + this.defaults.hmWidth() + this.defaults.axisTickSize;
      const y = this.defaults.borderPadding[0] + this.defaults.hmHeight() + this.defaults.axisTickSize;
      const w = this.props.width - x;
      const h = this.props.height - y;
      this.clearCanvas(x, y, w, h, this.defaults.clearColor);
      let textHeight = (h - 10) / 2;
      this.drawingContext.save();
      this.drawingContext.textAlign = 'center';
      this.drawingContext.textBaseline = 'middle';
      // this.drawingContext.font = `bold ${textHeight}px sans-serif`;
      // this.drawingContext.fillText(, x + w * 1/2, y + textHeight / 2);
      // // Runing average ratio
      // if(this.windowedData.length >= this.defaults.runningRatioSeconds) {
      //   let sellT20RunningSum = 0;
      //   let buyT20RunningSum = 0;
      //   for (let i = this.windowedData.length - 1; i >= this.windowedData.length - this.defaults.runningRatioSeconds; i--) {
      //     sellT20RunningSum += (this.windowedData[i].marketDepth.sells || []).reduce((vol, s) => vol + s.qty,0);
      //     buyT20RunningSum += (this.windowedData[i].marketDepth.buys || []).reduce((vol, s) => vol + s.qty,0);
      //   }
      //   const newBSTPFactor = (buyT20RunningSum / sellT20RunningSum);
      //   this.drawingContext.fillText(newBSTPFactor.toFixed(2), x + w /4, y + textHeight *0.5);
      // }
      this.drawingContext.font = `bold ${13}px sans-serif`;
      this.drawingContext.textBaseline = 'bottom';
      this.drawingContext.fillText(`买卖比: ${(d.marketDepth.buyOrderVolume / d.marketDepth.sellOrderVolume).toFixed(2)}`, x + w / 2, y + textHeight * 2 + 5);
      this.drawingContext.restore();
    }
  }

  /**
   * Draws X Axis
   */
  drawXAxis = () => {
    // clear canvas before axis draw
    this.clearCanvas(
      this.defaults.borderPadding[3], this.defaults.borderPadding[0] + this.defaults.hmHeight(),
      this.defaults.hmWidth(), this.defaults.axisXHeight, this.defaults.clearColor
    );
    // draw axis
    this.drawingContext.save();
    this.drawingContext.beginPath();
    this.drawingContext.translate(this.defaults.borderPadding[3], this.defaults.borderPadding[0] + this.defaults.hmHeight());
    this.drawingContext.moveTo(0, 0);
    this.drawingContext.lineTo(this.defaults.hmWidth(), 0);
    this.drawingContext.textAlign = 'center';
    this.drawingContext.textBaseline = 'top';
    // const assumedTextWidth = this.drawingContext.measureText('77:77:77').width + 20;
    // const maxTickInterval = this.defaults.hmWidth() / assumedTextWidth;
    const bandInterval = Math.max(1, parseInt(this.windowedData.length / (this.defaults.hmWidth() / 102)));
    // console.log('bandInterval=', bandInterval);
    let panel = this;
    this.windowedData.map((d, i) => {
      if (i !=0 && i % bandInterval === 0) {
        let x = this.xScale(d.ts);
        if (x + d.ts.length * 5 < panel.defaults.hmWidth()) {
          this.drawingContext.moveTo(x, 0);
          this.drawingContext.lineTo(x, this.defaults.axisTickSize);
          this.drawingContext.fillText(d.ts, x, this.defaults.axisTickSize + this.defaults.xAxisTextPadding);
        }
      }
    });
    this.drawingContext.textAlign = 'left';
    this.drawingContext.font = '12px Arial';

    let zoomLevelText = `当前视域:  ${zoomTimeFormat(this.windowLength)}`
    this.drawingContext.fillText(zoomLevelText, 20, this.defaults.axisTickSize + this.defaults.xAxisTextPadding + 20);
    let w = this.drawingContext.measureText(zoomLevelText).width;
    const maxVolumeInWindowData = extractMaxTradedVolume(this.windowedData);

    const maxVolumeText = `最近${zoomTimeFormat(this.windowLength, 1)}内最大交易量:  `;
    this.drawingContext.fillText(maxVolumeText, 20 + w + 20, this.defaults.axisTickSize + this.defaults.xAxisTextPadding + 20);
    this.drawingContext.fillStyle = this.defaults.textHighlightOnBackground;
    w += this.drawingContext.measureText(maxVolumeText).width;
    this.drawingContext.font = 'bold 12px Arial';
    this.drawingContext.fillText(`${maxVolumeInWindowData}`, 20 + w + 20, this.defaults.axisTickSize + this.defaults.xAxisTextPadding + 20);
    w += this.drawingContext.measureText(`${maxVolumeInWindowData}`).width;

    if (this.windowedData.length > 0) {
      this.drawingContext.fillStyle = this.defaults.textOnBackground;
      this.drawingContext.font = '12px Arial';
      this.drawingContext.fillText(`
        最后交易价格:  ${this.windowedData[this.windowedData.length - 1].marketDepth.lastTradedPrice}
        最后交易数量:  ${this.windowedData[this.windowedData.length - 1].marketDepth.lastTradedQty}
        最后交易时间:  ${this.windowedData[this.windowedData.length - 1].ts}
        `, 20 + w + 40, this.defaults.axisTickSize + this.defaults.xAxisTextPadding + 20);
    }
    this.drawingContext.fillStyle = this.defaults.textOnBackground;
    this.drawingContext.lineWidth = 1.2;
    this.drawingContext.strokeStyle = this.defaults.axisColor;
    this.drawingContext.stroke();
    this.drawingContext.restore();
  }

  /**
   * Draws Y Axis and Bid Ask graph at the same time
   */
  drawYAxisAndBidAskGraph = () => {
    if (this.yDomainValues !== null) {
      const yh2 = this.yScale.bandwidth() * 0.5;
      // clear canvas before axis draw
      this.clearCanvas(
        this.defaults.borderPadding[3] + this.defaults.hmWidth(), this.defaults.borderPadding[0],
        this.defaults.axisYWidth, this.defaults.hmHeight() + this.defaults.axisTickSize, this.defaults.clearColor
      );
      // translate and draw
      this.drawingContext.save();
      this.drawingContext.beginPath();
      this.drawingContext.translate(this.defaults.borderPadding[3] + this.defaults.hmWidth(), this.defaults.borderPadding[0]);
      this.drawingContext.moveTo(0, 0);
      this.drawingContext.lineTo(0, this.defaults.hmHeight() + this.defaults.axisTickSize);
      this.drawingContext.textAlign = 'start';
      this.drawingContext.textBaseline = 'top';
      let maxTextWidth = 0;
      this.yDomainValues.map(d => {
        let y = this.yScale(d) + yh2;
        this.drawingContext.moveTo(0, y);
        this.drawingContext.lineTo(this.defaults.axisTickSize, y);

        // 大于7位，换行绘制
        if (String(d).length <= 7) {
          y -= 5
          this.drawingContext.fillText(d, this.defaults.axisTickSize + this.defaults.yAxisTextPadding, y, this.defaults.axisYWidth - this.defaults.axisTickSize + this.defaults.yAxisTextPadding);
        } else {
          y -= 10
          let text = String(d)
          let [t0, t1] = text.split('.')

          this.drawingContext.fillText(t0, this.defaults.axisTickSize + this.defaults.yAxisTextPadding, y, this.defaults.axisYWidth - this.defaults.axisTickSize + this.defaults.yAxisTextPadding);
          this.drawingContext.fillText('.'.concat(t1), this.defaults.axisTickSize + this.defaults.yAxisTextPadding, y + 10, this.defaults.axisYWidth - this.defaults.axisTickSize + this.defaults.yAxisTextPadding);
        }

        let tw = this.drawingContext.measureText(d).width;
        maxTextWidth = maxTextWidth >= tw ? maxTextWidth : tw;
      });
      this.drawingContext.lineWidth = 1.2;
      this.drawingContext.strokeStyle = this.defaults.axisColor;
      this.drawingContext.stroke();
      this.drawingContext.restore();

      // Now I will draw the bid ask strength graph,
      const x = this.defaults.borderPadding[3] + this.defaults.hmWidth() + maxTextWidth + this.defaults.axisTickSize + this.defaults.yAxisTextPadding + this.defaults.bidAskGraphPaddingLeft;
      const y = this.defaults.borderPadding[0];
      this.drawBidAskGraph(x, y);
    }
  }

  /**
   * Draw and animate Bid Ask graph
   * @param {number} x
   * @param {number} y
   */
  drawBidAskGraph = (x, y) => {
    if (this.windowedData.length > 0) {
      if (this.bidAskAnimTimer !== null) {
        this.bidAskAnimTimer.stop();
        this.bidAskAnimTimer = null;
      }
      this.bidAskAnimTimer = d3.timer(elapsed => {
        // compute how far through the animation we are (0 to 1)
        const t = Math.min(1, d3.easeCubic(elapsed / this.defaults.bidAskTransitionDuration));

        // ----------------draw--------------------
        // console.log('drawing bid ask graph');
        this.clearCanvas(
          x, y, this.defaults.bidAskWidth, this.defaults.hmHeight() + this.defaults.axisTickSize, this.defaults.clearColor
        );
        const h = this.yScale.bandwidth() - 2;
        const d = this.windowedData[this.windowedData.length - 1];

        if (!d) {
          return
        }

        const maxBidAskVol = extractMaxVolume(d);
        this.drawingContext.save();
        this.drawingContext.translate(x, y);
        this.drawingContext.lineWidth = 0;
        this.drawingContext.textBaseline = 'middle';
        const drawBars = (arr, color, textColor) => {
          arr.map(v => {
            this.drawingContext.fillStyle = color;
            const l = this.defaults.bidAskWidth * (+v.qty / maxBidAskVol);
            // save v bars length
            this.drawingContext.fillRect(0, this.yScale(v.rate), l, h);
            let textWidth = this.drawingContext.measureText(v.qty).width;

            if (this.defaults.bidAskWidth - l - textWidth >= textWidth) {
              // text outside bar
              this.drawingContext.textAlign = 'start';
              this.drawingContext.fillStyle = this.defaults.textOnBackground;
              this.drawingContext.fillText(v.qty, l + 1, this.yScale(v.rate) + h / 2 + 1);
            } else {
              this.drawingContext.textAlign = 'end';
              this.drawingContext.fillStyle = textColor;
              this.drawingContext.fillText(v.qty, l - textWidth, this.yScale(v.rate) + h / 2 + 1);
            }
          });
        }
        drawBars(d.marketDepth.buys, this.defaults.buyColor, this.defaults.textOnBuyColor);
        drawBars(d.marketDepth.sells, this.defaults.sellColor, this.defaults.textOnSellColor);
        this.drawingContext.restore();
        // ----------------draw--------------------

        // if this animation is over
        if (t === 1) this.bidAskAnimTimer.stop();
      });
    }
  }

  /**
   * Draws background heatmap for both buys and sells
   */
  drawMainGraph = () => {
    this.drawingContext.save();
    if (this.xScale && this.yScale && this.bidAskScale && this.drawingContext !== null) {
      const avgTradedVolume = extractAvgTradedVolume(this.windowedData);
      const maxBidAskVolume = extractMaxAskBidVolume(this.windowedData);
      const xh2 = this.xScale.bandwidth() * 0.5;
      const yh2 = this.yScale.bandwidth() * 0.5;
      this.drawingContext.translate(this.defaults.borderPadding[3], this.defaults.borderPadding[0]);
      this.windowedData.map(d => {
        const marketDepth = d.marketDepth;
        const ask1 = marketDepth.sells[0];
        const bid1 = marketDepth.buys[0];
        const ts = d.ts;
        // draw buys
        if (marketDepth.buys && marketDepth.buys.length > 0) {
          let color = d3.color('#1a506d').rgb();
          marketDepth.buys.map(buy => {
            color.opacity = buy.qty / maxBidAskVolume;
            this.drawingContext.fillStyle = color.toString();
            this.drawingContext.fillRect(
              this.xScale(ts),
              this.yScale(buy.rate),  // 减去半个方块的高度
              this.xScale.bandwidth(),
              this.yScale.bandwidth()
            );
          });
        }
        // draw sells
        if (marketDepth.sells && marketDepth.sells.length > 0) {
          let color = d3.color('#1a506d').rgb();
          marketDepth.sells.map(sell => {
            color.opacity = sell.qty / maxBidAskVolume;
            this.drawingContext.fillStyle = color.toString();
            this.drawingContext.fillRect(
              this.xScale(ts),
              this.yScale(sell.rate),  // 减去半个方块的高度
              this.xScale.bandwidth(),
              this.yScale.bandwidth()
            );
          });
        }
      });

      // draw trade size
      this.circles = []
      this.windowedData.map(d => {
        const marketDepth = d.marketDepth;
        const ask1 = marketDepth.sells[0];
        const bid1 = marketDepth.buys[0];
        const ts = d.ts;
        const maxBidAskVol = extractMaxVolume(d);

        let trade_color = d3.color(marketDepth.side == 'sell' ? '#cc5040' : '#44c98b').rgb();
        trade_color.opacity = 0.7;
        this.drawingContext.fillStyle = trade_color.toString();
        const r = /*xh2*/ Math.min(this.defaults.volumeCircleMaxRadius * (+marketDepth.lastTradedQty / avgTradedVolume) + 3, 50);
        this.drawingContext.beginPath();
        this.drawingContext.arc(
          this.xScale(ts) + xh2,
          this.yScale(+marketDepth.lastTradedPrice) + yh2,
          r, 0, 2 * Math.PI
        );
        this.drawingContext.strokeStyle = trade_color;
        this.drawingContext.fill();

        // 为球添加白色边框
        this.drawingContext.lineWidth = 1; // 设置线宽，可以根据需要调整
        this.drawingContext.strokeStyle = 'white'; // 设置描边颜色为白色
        this.drawingContext.stroke(); // 执行描边操作

        // 事件
        this.circles.push({
          x: this.xScale(ts) + xh2,
          y: this.yScale(+marketDepth.lastTradedPrice) + yh2,
          radius: r,
          data: d  // 存储与圆相关的数据，便于在事件处理时使用
        });
      })

      // draw buy line path
      let buy_line_color = d3.color(this.defaults.buyColor).rgb();
      this.drawingContext.fillStyle = buy_line_color.toString();
      this.drawingContext.beginPath();
      d3.line()
        .x(d => this.xScale(d.ts))
        .y(d => this.yScale(d.marketDepth.buys[0].rate) + yh2)
        // .curve(d3.curveLinear)
        .context(this.drawingContext)
        (this.windowedData);
      this.drawingContext.lineWidth = 2;
      this.drawingContext.strokeStyle = this.defaults.buyColor;
      this.drawingContext.stroke();

      // draw sell line path
      let sell_line_color = d3.color(this.defaults.sellColor).rgb();
      this.drawingContext.fillStyle = sell_line_color.toString();
      this.drawingContext.beginPath();
      d3.line()
        .x(d => this.xScale(d.ts))
        .y(d => this.yScale(d.marketDepth.sells[0].rate) + yh2)
        // .curve(d3.curveLinear)
        .context(this.drawingContext)
        (this.windowedData);
      this.drawingContext.lineWidth = 2;
      this.drawingContext.strokeStyle = this.defaults.sellColor;
      this.drawingContext.stroke();
    }
    this.drawingContext.restore();
  }

  /**
   * Clear the canvas area
   * @param {number} x x coordinate
   * @param {number} y y xoordinate
   * @param {number} w width
   * @param {number} h height
   * @param {string} color color string
   */
  clearCanvas = (x, y, w, h, color) => {
    // console.log('clear canvas area', x, y, w, h, color);
    if (this.drawingContext !== null) {
      this.drawingContext.save();
      this.drawingContext.fillStyle = color || this.defaults.clearColor;
      this.drawingContext.fillRect(x, y, w, h);
      this.drawingContext.restore();
    }
  }

  // ------------------------------ END: Canvas draw functions ---------------------------------------

  /**
   * Set Data for the Heatmap to generate
   * @param {any[]} data The data to set
   */
  setData = (data) => {
    // console.log('setdata called=', data);
    if (data && data.length > 0) {
      this.data = data;
      this.updateWindowedData();
    }
  }

  /**
   * Add as extra data to existing data array.
   * @param {any} data
   */
  addData = (data) => {
    if (typeof (data) === 'object') {
      this.data.push(data);
      this.updateWindowedData();
    }
  }

  /**
   * This updates the data in array to be viewed in graph
   */
  updateWindowedData = () => {
    // console.log('window data updated');
    if (this.props.autoScroll) {
      this.moveDataWindow(this.data.length - this.windowLength - 1);
    }
  }

  /**
   * Move the position of data window within the main data.
   * @param {number} position The target position of the window to be moved to.
   */
  moveDataWindow = (position) => {
    if (position !== this.windowPosition && position > -1 && position < this.data.length - this.windowLength) {
      // move position only if within valid range
      this.windowedData = this.data.slice(position, position + this.windowLength + 1);

      // 延迟日志
      if (this.windowedData.length > 1) {
        let last = this.windowedData[this.windowedData.length - 1]
        console.log(new Date().getTime() - last.time, last.marketDepth.lastTradedPrice, last.marketDepth.lastTradedQty)
      }

      this.windowPosition = position;
      if (this.windowPosition === this.data.length - this.windowLength - 1) {
        // enable auto scroll
        this.props.toggleAutoScroll(true);
      }
      // console.log('moveDataWindow = ', position, this.windowPosition, this.windowLength, this.data.length, this.autoScroll, this.windowedData);
      // update the map
      this.updateHeatmap();
    }
  }

  /**
   * This sets the Heatmap Zoom level aka. window.
   * @param {number} zoom The seconds to zoom into
   */
  setZoomLevel = (zoom) => {
    let l = Math.min(Math.max(zoom * 4, 3), this.data.length - 1);
    let l2 = this.windowLength - l;
    this.windowLength = l;
    this.moveDataWindow(this.windowPosition + l2);
  }

  /**
   * Render Function
   */
  render() {
    const { width, height } = this.props;
    // console.log('heatmap rendered', width, height, this.data);
    return (
      <canvas ref={this.canvasRef} width={width || 300} height={height || 150} tabIndex={1}
              style={{
                display: 'block',
                width: '100%',
                height: '100%',
                cursor: 'crosshair',
                paddingRight: '3%',
                boxSizing: "border-box"
              }}></canvas>
    );
  }
}
