<template>
  <div
    :id="cameraID + '_AveragesMap'"
    class="chart"
    :style="cssVars"
    :height="height"
  >
    <resize-observer @notify="handleResize" />
    <canvas
      :id="this.cameraID + '_AveragesMap_Canvas'"
      class="chart-lines"
      :height="height"
      :width="width"
    />
    <svg
      :id="this.cameraID + '_AveragesMap_SVGAxes'"
      class="chart-axes"
      :height="height"
    >
      <g :id="this.cameraID + '_AveragesMap_XAxis'" />
    </svg>
  </div>
</template>

<script>
import * as d3 from 'd3'

export default {
  props: {
    cameraID: {
      type: String,
      required: true
    }
  },
  data() {
    return {
      width: 1000,
      height: 100,
      margin: { top: 10, right: 20, bottom: 20, left: 30 },
      hoveredStickID: null,
      scaleExtent: [1, Infinity],
      lineWidth: 1,
      rectangleColor: '#dddddd'
    }
  },
  computed: {
    // --- D3 chart element accessors ---
    $chart() {
      return d3.select('#' + this.cameraID + '_AveragesMap')
    },
    $svgAxesActive() {
      return d3.select('#' + this.cameraID + '_AveragesMap_SVGAxes')
    },
    $canvas() {
      return d3.select('#' + this.cameraID + '_AveragesMap_Canvas')
    },
    $xAxis() {
      return d3.select('#' + this.cameraID + '_AveragesMap_XAxis')
    },
    // ---

    chartID() {
      return this.cameraID + '_AveragesMap'
    },

    // --- SCSS variables ---
    cssVars() {
      return {
        '--chart-margin-left': this.margin.left + 'px',
        '--chart-margin-top': this.margin.top + 'px',
        '--height': this.height + 'px'
      }
    },
    // ---

    // --- Canvas context ---
    context() {
      return this.$canvas.node().getContext('2d')
    },
    // ---

    // --- Precomputed helpers ---
    rangeX() {
      return [this.margin.left, this.width - this.margin.right]
    },
    spanX() {
      return this.rangeX[1] - this.rangeX[0]
    },
    rangeY() {
      return [this.height - this.margin.bottom, this.margin.top]
    },
    spanY() {
      return this.rangeY[0] - this.rangeY[1]
    },
    maxSnowHeightRounded() {
      return Math.round(Math.ceil(this.maxSnowHeight / 10)) * 10
    },
    // ---

    // --- Scales ---
    xScale() {
      return d3
        .scaleUtc()
        .domain(this.timespan)
        .range(this.rangeX)
    },
    xScaleZoomed() {
      const transform = this.normalizedSelection2Transform(
        this.zoomState.selection
      )
      return transform.rescaleX(this.xScale)
    },
    yScale() {
      return d3
        .scaleLinear()
        .domain([0, this.maxSnowHeightRounded])
        .range(this.rangeY)
    },
    // ---

    // --- Vuex store data ---
    chartData() {
      return this.$store.getters.chartData(this.cameraID)
    },
    stickIDs() {
      return this.$store.getters.stickIDs
    },
    timespan() {
      return [
        this.$store.getters.timespanStart,
        this.$store.getters.timespanEnd
      ]
    },
    maxSnowHeight() {
      return this.$store.getters.maxSnowHeight
    },
    zoomState() {
      // Returns:
      // zoomState - {selection: [0, 1], zoomingID: cameraID_stickID}
      // selection: Start and end points of the viewport.
      return this.$store.getters.zoomState
    },
    redrawTrigger() {
      return this.$store.state.redrawTrigger
    },
    cameraChronology() {
      return this.$store.state.chronology[this.cameraID]
    },
    activeStick: {
      get() {
        return this.$store.getters.activeStick
      },
      set({ stickID, imageName }) {
        this.$store.dispatch('setActivePoint', {
          stickID,
          cameraID: this.cameraID,
          imageName
        })
        this.$store.dispatch('setStickStatusGUI', {
          statusGUI: 'full',
          stickID
        })
      }
    },
    isVisible() {
      return this.$store.getters.cameraStatusGUI(this.cameraID) === 'full'
    },
    // ---

    // --- Zoom ---
    zoom() {
      return d3
        .zoom()
        .scaleExtent(this.scaleExtent)
        .extent([
          [this.margin.left, 0],
          [this.width - this.margin.right, this.height]
        ])
        .translateExtent([
          [this.margin.left, 0],
          [this.width - this.margin.right, 0]
        ])
        .on('zoom', this.zoomed)
    },
    // ---

    // --- Quadtree ---
    quadtreeImageName() {
      if (this.chartData) {
        return (
          d3
            .quadtree()
            .x(d => {
              // Ask for x coordinate in the canvas coordinate system.
              let dateTime = this.chartData[d].dateTime
              return this.xScaleZoomed(new Date(dateTime))
            })
            .y(() => 0)
            // .y(() => {
            //   return 0
            //   // return this.yScale(
            //   //   this.chartData[d].sticks[this.stickID].snowHeightAverage
            //   // )
            // })
            .extent([
              [this.margin.left, 0],
              [this.width - this.margin.right, this.height]
            ])
            .addAll(Object.keys(this.chartData))
        )
      }
      return null
    }
    // ---
  },
  watch: {
    zoomState(state) {
      // check if sourceEvent exists and if this is the same component
      // - if true, don't update the zoom (this is to prevent the infinite loop)

      if (state.zoomingID === this.chartID) {
        return
      }
      if (this.isVisible) {
        this.updateZoom(state.selection)
      }

      const transform = this.normalizedSelection2Transform(state.selection)

      // then update the zoom transform internally with the new transform settings of the other components
      this.$chart.node().__zoom = transform
    },
    width() {
      this.$chart.call(this.zoom)
      this.$nextTick(() => {
        if (this.isVisible) {
          this.updateZoom(this.zoomState.selection)
        }
      })
    },
    redrawTrigger() {
      if (this.isVisible) {
        this.redrawChart(this.xScaleZoomed)
      }
    },
    activeStick() {
      if (this.isVisible) {
        this.redrawChart(this.xScaleZoomed)
      }
    },
    hoveredStickID() {
      this.redrawChart(this.xScaleZoomed)
    },
    isVisible() {
      this.redrawChart(this.xScaleZoomed)
    },
    cameraChronology() {
      this.redrawChart(this.xScaleZoomed)
    }
  },
  methods: {
    handleResize({ width }) {
      this.width = width
    },
    redrawChart(x) {
      // Clear canvas and draw chart and gradient again.
      // x: Scale of the x coordinate.

      // Clear whole canvas.
      this.context.clearRect(0, 0, this.width, this.height)
      this.drawSnowHeightAverages(this.context, x)
      // Clear left and right sides of charts.
      this.context.clearRect(0, 0, this.margin.left, this.height)
      this.context.clearRect(
        this.width - this.margin.right,
        0,
        this.margin.right,
        this.height
      )
    },
    zoomed(event) {
      // Generate normalized selection from transform object.
      const normSel = this.transform2NormalizedSelection(event.transform)
      // Update zoom of this component.
      if (this.isVisible) {
        this.updateZoom(normSel)
      }

      // Create state object describing zoom and save it to the Vuex store.
      const state = { selection: normSel, zoomingID: this.chartID }
      this.$store.dispatch('setZoomState', state)
    },
    updateZoom(normalizedSelection) {
      // Update zoom on this chart, using the transform.
      // Apply transformation to the x axis and the chart points.
      const transform = this.normalizedSelection2Transform(normalizedSelection)
      const xz = transform.rescaleX(this.xScale)

      // Redraw x axis.
      this.$xAxis.call(this.xAxis, xz)

      // Redraw canvas with points.
      this.redrawChart(xz)
    },
    transform2NormalizedSelection(transform) {
      const x = transform.x
      const k = transform.k
      if (k === 1) {
        return [0, 1]
      }
      const s = (x + (k - 1) * this.rangeX[0]) / (-k * this.spanX)
      const e = s + 1 / k
      return [s, e]
    },
    normalizedSelection2Transform(normalizedSelection) {
      const start = normalizedSelection[0]
      const end = normalizedSelection[1]
      const k = 1 / (end - start)
      const x = -(start * k * this.spanX + (k - 1) * this.rangeX[0])
      return d3.zoomIdentity.translate(x, 0).scale(k)
    },

    mousemoved(event) {
      const mouseX = event.offsetX
      const mouseY = event.offsetY
      const imageName = this.quadtreeImageName.find(mouseX, 0)

      const quadtreeStick = d3
        .quadtree()
        .x(() => 0)
        .y(d => this.chartData[imageName].sticks[d].snowHeightAverage)
        .addAll(Object.keys(this.chartData[imageName].sticks))

      this.hoveredStickID = quadtreeStick.find(0, this.yScale(mouseY))
    },
    clicked(event) {
      const mouseX = event.offsetX
      const mouseY = event.offsetY
      const imageName = this.quadtreeImageName.find(mouseX, 0)

      const quadtreeStick = d3
        .quadtree()
        .x(() => 0)
        .y(d => this.chartData[imageName].sticks[d].snowHeightAverage)
        .addAll(Object.keys(this.chartData[imageName].sticks))

      this.activeStick = {
        stickID: quadtreeStick.find(0, this.yScale(mouseY)),
        imageName
      }
    },

    xAxis(g, x) {
      g.attr(
        'transform',
        `translate(0,${this.height - this.margin.bottom})`
      ).call(
        d3
          .axisBottom(x)
          .ticks(10)
          .tickSizeOuter(0)
      )
    },

    drawSnowHeightAverages(context, x) {
      // context: Canvas context, used for drawing into canvas.
      // x: D3 scale for x axis.
      // y: D3 scale for y axis.

      this.drawMissingDataRectangles(context, x)

      for (const stickID of this.stickIDs) {
        let color = '#000000' // Default color.
        if (this.activeStick === stickID) {
          color = '#ad0101' // Stick is active.
        }
        if (this.hoveredStickID === stickID) {
          color = '#00a6a6' // Stick is hovered on.
        }
        this.drawSnowHeightAverage(context, x, stickID, color)
      }
    },
    drawMissingDataRectangles(context, x) {
      // context: Canvas context, used for drawing into canvas.
      // x: D3 scale for x axis.

      const color = d3
        .scaleLinear()
        .domain([0, this.stickIDs.length])
        .range([this.rectangleColor, '#ffffff'])

      let lastTimestamp = null

      if (!this.cameraChronology) {
        return
      }

      this.cameraChronology.order.forEach(imageName => {
        const timestamp = this.chartData[imageName].dateTime
        const sticks = this.chartData[imageName].sticks

        let visibleCount = 0

        for (const stick in sticks) {
          if (Object.hasOwnProperty.call(sticks, stick)) {
            if (
              sticks[stick].visible ||
              this.chartData[imageName].weatherConditions === 0
            ) {
              visibleCount += 1
            }
          }
        }

        if (lastTimestamp != null) {
          this.drawRectangle(
            context,
            x,
            lastTimestamp,
            timestamp,
            color(visibleCount)
          )
        }
        lastTimestamp = timestamp
      })
    },
    drawSnowHeightAverage(context, x, stickID, color) {
      // context: Canvas context, used for drawing into canvas.
      // x: D3 scale for x axis.
      // y: D3 scale for y axis.

      let lastTimestamp = null
      let lastSnowHeightAverage = null

      if (!this.cameraChronology) {
        return
      }

      this.cameraChronology.order.forEach(imageName => {
        const timestamp = this.chartData[imageName].dateTime
        const stick = this.chartData[imageName].sticks[stickID]

        if (typeof stick === 'undefined') {
          return // The stick 'stickID' is outside the camera's 'cameraID' view.
        }

        const snowHeightAverage = stick.snowHeightAverage

        // if (stick.snowHeightAverage === -1) {
        //   return // Draw a straight line between identified points, ignoring the undefined ones.
        // }

        if (lastTimestamp != null) {
          this.drawLine(
            context,
            lastTimestamp,
            lastSnowHeightAverage,
            timestamp,
            snowHeightAverage,
            x,
            color
          )
        }
        lastTimestamp = timestamp
        lastSnowHeightAverage = snowHeightAverage
      })
    },
    drawLine(
      context,
      lastTimestamp,
      lastSnowHeightAverage,
      timestamp,
      snowHeightAverage,
      x,
      color
    ) {
      // context: Canvas context, used for drawing into canvas.
      // lastTimestamp, timestamp: (x axis) string, containing standard time format.
      // lastSnowHeightAverage, snowHeightAverage: (y axis) number.
      // x: D3 scale for x axis.
      // y: D3 scale for y axis.
      // color: Color of the point and the line.
      const lastPx = Math.floor(x(new Date(lastTimestamp)))
      const px = Math.floor(x(new Date(timestamp)))
      // If both points are out of bounds, don't draw the line.
      if (
        (px >= 0 && px <= this.width) ||
        (lastPx >= 0 && lastPx <= this.width)
      ) {
        if (lastSnowHeightAverage === -1 || snowHeightAverage === -1) {
          return
        }
        const lastPy = Math.floor(this.yScale(lastSnowHeightAverage))
        const py = Math.floor(this.yScale(snowHeightAverage))
        context.beginPath()
        context.strokeStyle = color || '#555555'
        context.lineWidth = 2
        context.globalAlpha = 0.3
        context.moveTo(lastPx, lastPy)
        context.lineTo(px, py)
        context.stroke()
      }
    },

    drawRectangle(context, x, lastTimestamp, timestamp, color, opacity) {
      const x1 = Math.floor(x(new Date(lastTimestamp)))
      const x2 = Math.floor(x(new Date(timestamp))) + 1
      // If start or end are out of bounds, don't draw the shape.
      if (x1 >= this.width || x2 <= 0) {
        return
      }

      context.strokeStyle = color || '#555555'
      context.fillStyle = color || '#555555'
      context.lineWidth = 1
      context.globalAlpha = opacity || 1
      context.beginPath()
      context.rect(x1, this.rangeY[1], x2 - x1, this.spanY)
      context.fill()
    }
  },
  mounted() {
    this.$chart
      .call(this.zoom)
      .on('wheel', event => {
        // Prevent scrolling the whole page when using mouse wheel on chart.
        if (
          event.deltaY > 0 &&
          this.zoomState.selection[0] === 0 &&
          this.zoomState.selection[1] === 1
        ) {
          event.preventDefault()
        }
      })
      .on('mousemove', this.mousemoved)
      .on('click', this.clicked)
      .on('mouseleave', () => (this.hoveredStickID = null))
    this.$xAxis.call(this.xAxis, this.xScale)

    this.width = this.$chart.node().clientWidth
  }
}
</script>

<style lang="scss" scoped>
.chart {
  position: relative;
  background: var(--color-background);
  height: var(--height);
  padding: 0px;
}

.chart-axes {
  position: absolute;
  left: 0px;
  top: 0px;
  z-index: 2;
  width: 100%;
}

.chart-lines {
  position: absolute;
  left: 0px;
  top: 0px;
  z-index: 1;
}
</style>
