<template>
  <div id="contextMap" class="chart" :height="height">
    <resize-observer @notify="handleResize" />
    <svg
      id="contextMap_SVG"
      :height="height"
      :width="width"
      style="position: absolute;"
    >
      <g id="contextMap_XAxis" />
      <g id="contextMap_Brush" />
    </svg>
    <canvas id="contextMap_Canvas" :height="height" :width="width" />
  </div>
</template>

<script>
import * as d3 from 'd3'

export default {
  data() {
    return {
      width: 1000,
      height: 60,
      margin: { top: 10, right: 20, bottom: 20, left: 20 }
    }
  },
  computed: {
    $chart() {
      return d3.select('#contextMap')
    },
    $svg() {
      return d3.select('#contextMap_SVG')
    },
    $canvas() {
      return d3.select('#contextMap_Canvas')
    },
    $xAxis() {
      return d3.select('#contextMap_XAxis')
    },
    $brush() {
      return d3.select('#contextMap_Brush')
    },

    stickID() {
      return this.$store.getters.activeStick
    },
    cameraID() {
      return this.$store.getters.activeCamera
    },

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

    isVisible() {
      return this.stickID !== null
    },
    zoomState() {
      return this.$store.getters.zoomState
    },
    redrawTrigger() {
      return this.$store.state.redrawTrigger
    },
    childrenID() {
      const childrenID = this.$chart
        .selectChildren()
        .nodes()
        .map(x => x.id)
      childrenID.push(this.$chart.node().id)
      return childrenID
    },
    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]
    },
    xScale() {
      return d3
        .scaleUtc()
        .domain(this.timespan)
        .range(this.rangeX)
    },
    yScale() {
      return d3
        .scaleLinear()
        .domain([0, Math.round(this.maxSnowHeight / 10) * 10])
        .range(this.rangeY)
    },
    // TODO: Change chartData.
    chartData() {
      return this.cameraID !== null
        ? this.$store.getters.chartData(this.cameraID)
        : {}
    },
    timespan() {
      return [
        this.$store.getters.timespanStart,
        this.$store.getters.timespanEnd
      ]
    },
    maxSnowHeight() {
      return this.$store.getters.maxSnowHeight
    },
    brush() {
      const rangeX =
        this.rangeX[1] > this.rangeX[0]
          ? this.rangeX
          : [this.rangeX[0], this.rangeX[0]]
      return d3
        .brushX()
        .extent([
          [rangeX[0], 0],
          [rangeX[1], this.height]
        ])
        .on('brush end', this.brushed)
    },
    filterIsDay() {
      return this.$store.state.filters.isDay
    },
    filterSnowDetected() {
      return this.$store.state.filters.snowDetected
    },
    filterStickFound() {
      return this.$store.state.filters.stickFound
    },
    filterMinSnowHeight() {
      return this.$store.state.filters.minSnowHeight
    },
    filterMaxSnowHeight() {
      return this.$store.state.filters.maxSnowHeight
    },
    filterImageSticksEdited() {
      return this.$store.state.filters.imageSticksEdited
    },
    filterImageDiscarded() {
      return this.$store.state.filters.imageDiscarded
    }
  },
  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 (this.childrenID.includes(state.zoomingID)) {
        return
      }
      // update zoom of the chart
      this.updateBrush(state.selection)
    },
    width() {
      this.$brush.call(this.brush)
      this.$xAxis.call(this.xAxis, this.xScale)
      this.$nextTick(() => {
        this.draw()
        this.updateBrush(this.zoomState.selection)
      })
    },
    stickID() {
      this.$xAxis.call(this.xAxis, this.xScale)
      this.draw()
    },
    cameraID() {
      this.$xAxis.call(this.xAxis, this.xScale)
      this.draw()
    },
    redrawTrigger() {
      if (this.isVisible) {
        this.draw()
      }
    },
    isVisible() {
      this.updateBrush(this.zoomState.selection)
    },
    filterIsDay() {
      if (this.isVisible) {
        this.draw()
      }
    },
    filterSnowDetected() {
      if (this.isVisible) {
        this.draw()
      }
    },
    filterStickFound() {
      if (this.isVisible) {
        this.draw()
      }
    },
    filterMinSnowHeight() {
      if (this.isVisible) {
        this.draw()
      }
    },
    filterMaxSnowHeight() {
      if (this.isVisible) {
        this.draw()
      }
    },
    filterImageSticksEdited() {
      if (this.isVisible) {
        this.draw()
      }
    },
    filterImageDiscarded() {
      if (this.isVisible) {
        this.draw()
      }
    }
    // stickID() {
    //   if (this.isVisible) {
    //     this.$nextTick(() => {
    //       this.draw()
    //       this.updateBrush(this.zoomState.selection)
    //     })
    //   }
    // }
  },
  methods: {
    handleResize({ width }) {
      this.width = width
    },
    brushed(event) {
      if (
        (event.type === 'brush' || event.type === 'end') &&
        !event.sourceEvent
      ) {
        return
      }
      if (event.selection === null) {
        return
      }
      const normSel = this.selection2NormalizedSelection(event.selection)

      // Update the transform for this component.
      this.updateBrush(normSel)

      // Update the store event.
      const state = { selection: normSel, zoomingID: 'contextMap' }
      this.$store.dispatch('setZoomState', state)
    },
    wheeled(event) {
      event.preventDefault()

      const factor = event.deltaY < 0 ? Math.pow(2, -1 / 5) : Math.pow(2, 1 / 5)
      var length = this.zoomState.selection[1] - this.zoomState.selection[0]
      const midpoint = this.zoomState.selection[0] + length / 2
      length = length * factor
      const newStart = midpoint - length / 2 >= 0 ? midpoint - length / 2 : 0
      const newEnd = midpoint + length / 2 <= 1 ? midpoint + length / 2 : 1
      const normSel = [newStart, newEnd]

      // Update the transform for this component.
      this.updateBrush(normSel)

      // Update the store event.
      const state = { selection: normSel, zoomingID: 'contextMap' }
      this.$store.dispatch('setZoomState', state)
    },
    draw() {
      if (!this.isVisible) {
        return // No stick selected, nothing to redraw.
      }
      // Clear whole canvas.
      this.context.clearRect(0, 0, this.width, this.height)
      // Draw points.
      this.drawChartPoints(this.context, this.xScale, this.yScale)
      // Clear left and right sides of charts.
      this.context.clearRect(0, 0, this.margin.left, this.height)
      this.context.clearRect(this.rangeX[1], 0, this.margin.right, this.height)
    },
    updateBrush(normalizedSelection) {
      const selection = this.normalizedSelection2Selection(normalizedSelection)

      // Update brush on this chart.
      this.$brush.call(this.brush.move, selection)
    },
    selection2NormalizedSelection(selection) {
      return [
        (selection[0] - this.rangeX[0]) / this.spanX >= 0
          ? (selection[0] - this.rangeX[0]) / this.spanX
          : 0,
        (selection[1] - this.rangeX[0]) / this.spanX <= 1
          ? (selection[1] - this.rangeX[0]) / this.spanX
          : 1
      ]
    },
    normalizedSelection2Selection(normalizedSelection) {
      return [
        normalizedSelection[0] * this.spanX + this.rangeX[0],
        normalizedSelection[1] * this.spanX + this.rangeX[0]
      ]
    },

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

    drawChartPoints(context, x, y) {
      // context: Canvas context, used for drawing into canvas.
      // x: D3 scale for x axis.
      // y: D3 scale for y axis.
      for (const imageName in this.chartData) {
        if (Object.hasOwnProperty.call(this.chartData, imageName)) {
          let opacity = 1
          if (
            this.$store.getters.isFilteredOut(
              this.chartData[imageName],
              this.stickID
            )
          ) {
            opacity = 0.2
          }
          const timestamp = this.chartData[imageName].dateTime
          const snowHeight = this.chartData[imageName].sticks[this.stickID]
            .snowHeight
          const type = this.stickType(imageName)
          if (type.type === 'cross') {
            this.drawCross(
              context,
              timestamp,
              snowHeight,
              x,
              y,
              type.color,
              opacity
            )
          }
          if (type.type === 'point') {
            this.drawPoint(
              context,
              timestamp,
              snowHeight,
              x,
              y,
              type.color,
              opacity
            )
          }
        }
      }
    },

    stickType(imageName) {
      // Select type of a datapoint, based on the attributes.

      const image = this.chartData[imageName]
      const stick = image.sticks[this.stickID]
      const result = { type: '', color: '', opacity: 1 }

      // Stick discarded
      if (image.discardedByUser || stick.discardedByUser) {
        result.type = 'cross'
        result.color = '#888888'
        result.opacity = 1
        return result
      }
      // Stick manually edited
      if (stick.correctedByUser || image.approvedByUser) {
        result.type = 'point'
        result.color = '#88a509'
        result.opacity = 1
        return result
      }
      // No snow detected
      if (image.weatherConditions === 0) {
        result.type = 'point'
        result.color = '#6696c3'
        result.opacity = 1
        return result
      }
      // Stick not found
      if (!stick.visible) {
        result.type = 'point'
        result.color = '#d4aca9'
        result.opacity = 1
        return result
      }
      // Stick normal/skipped
      result.type = 'point'
      result.color = '#555555'
      result.opacity = 1
      return result
      // Outlier
      // Custom user tag
    },

    drawPoint(context, timestamp, snowHeight, x, y, color, opacity) {
      // context: Canvas context, used for drawing into canvas.
      // timestamp: (x axis) string, containing standard time format.
      // snowHeight: (y axis) number.
      // x: D3 scale for x axis.
      // y: D3 scale for y axis.
      // color: Color of the point and the line.
      const px = Math.floor(x(new Date(timestamp)))
      const py = Math.floor(y(snowHeight))

      context.beginPath()
      context.fillStyle = color || '#555555'
      context.strokeStyle = color || '#555555'
      context.globalAlpha = opacity || 1
      context.lineWidth = 1
      context.arc(px, py, 2, 0, 2 * Math.PI, true)
      context.fill()
    },
    drawCross(context, timestamp, snowHeight, x, y, color, opacity) {
      // context: Canvas context, used for drawing into canvas.
      // timestamp: (x axis) string, containing standard time format.
      // snowHeight: (y axis) number.
      // x: D3 scale for x axis.
      // y: D3 scale for y axis.
      // color: Color of the point and the line.
      const px = Math.floor(x(new Date(timestamp)))
      const py = Math.floor(y(snowHeight))

      context.strokeStyle = color || '#555555'
      context.lineWidth = 2
      context.lineCap = 'round'
      context.globalAlpha = opacity || 1
      context.beginPath()
      context.moveTo(px - 1.4, py - 1.4)
      context.lineTo(px + 1.4, py + 1.4)
      context.moveTo(px - 1.4, py + 1.4)
      context.lineTo(px + 1.4, py - 1.4)
      context.stroke()
    }
  },
  mounted() {
    this.$chart.on('wheel', this.wheeled)
    this.width = this.$chart.node().clientWidth
  }
}
</script>

<style lang="scss">
.chart {
  position: relative;
}
</style>
