/**
 * This file is part of VILLASweb.
 *
 * VILLASweb is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * VILLASweb is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with VILLASweb. If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/

import React from 'react';
import { scaleLinear, scaleTime, scaleOrdinal} from 'd3-scale';
import { schemeCategory10 } from 'd3-scale-chromatic'
import { extent } from 'd3-array';
import { line } from 'd3-shape';
import { axisBottom, axisLeft } from  'd3-axis';
import { select } from 'd3-selection';
import { timeFormat } from 'd3-time-format';
import { format } from 'd3';

const topMargin = 10;
const bottomMargin = 25;
const leftMargin = 40;
const rightMargin = 10;

let uniqueIdentifier = 0;

class Plot extends React.Component {
  constructor(props) {
    super(props);

    // create dummy axes
    let labelMargin = 0;
    if (props.yLabel !== '') {
      labelMargin = 30;
    }

    const xScale = scaleTime().domain([Date.now() - props.time * 1000, Date.now()]).range([0, props.width - leftMargin - labelMargin - rightMargin]);
    let yScale;

    if (props.yUseMinMax) {
      yScale = scaleLinear().domain([props.yMin, props.yMax]).range([props.height + topMargin - bottomMargin, topMargin]);
    } else {
      yScale = scaleLinear().domain([0, 10]).range([props.height + topMargin - bottomMargin, topMargin]);
    }

    const xAxis = axisBottom().scale(xScale).ticks(5).tickFormat(timeFormat("%M:%S"));
    const yAxis = axisLeft().scale(yScale).ticks(5).tickFormat(format(".3s"));

    this.state = {
      data: null,
      lines: null,
      xAxis,
      yAxis,
      labelMargin,
      identifier: uniqueIdentifier++,
      stopTime: null,
    };
  }

  componentDidMount() {
    this.createInterval();
  }

  componentWillUnmount() {
    this.removeInterval();
  }

  static getDerivedStateFromProps(props, state){
    let labelMargin = 0;
    if (props.yLabel !== '') {
      labelMargin = 30;
    }

    // check if data is invalid
    if (props.data == null || props.data.length === 0) {
      // create empty plot axes
      let xScale;
      let yScale;
      let stopTime;

      if(!props.paused){
        xScale = scaleTime().domain([Date.now() -  props.time * 1000, Date.now()]).range([0, props.width - leftMargin - labelMargin - rightMargin]);
        stopTime = Date.now();
      }else{
        stopTime = state.stopTime;
        xScale = scaleLinear().domain([state.stopTime -  props.time * 1000, state.stopTime]).range([0, props.width - leftMargin - labelMargin - rightMargin]);
      }

      if (props.yUseMinMax) {
        yScale = scaleLinear().domain([props.yMin, props.yMax]).range([props.height + topMargin - bottomMargin, topMargin]);
      } else {
        yScale = scaleLinear().domain([0, 10]).range([props.height + topMargin - bottomMargin, topMargin]);
      }

      const xAxis = axisBottom().scale(xScale).ticks(5).tickFormat(timeFormat("%M:%S"));
      const yAxis = axisLeft().scale(yScale).ticks(5).tickFormat(format(".3s"));

      return{
        stopTime: stopTime,
        data: null,
        xAxis,
        yAxis,
        labelMargin
      };
    }

    // only show data in requested time
    let data = props.data;
    let icDataset = data.find(function(element) {
      return element !== undefined;
    })

    const firstTimestamp = icDataset[icDataset.length - 1].x - (props.time + 1) * 1000;
    if (icDataset[0].x < firstTimestamp) {
      // only show data in range (+100 ms)
      const index = icDataset.findIndex(value => value.x >= firstTimestamp - 100);
      data = data.map(values => values.slice(index));
    }

    return {
      data,
      labelMargin
    };

  }

  componentDidUpdate(prevProps: Readonly<P>, prevState: Readonly<S>, snapshot: SS): void {
    if (prevProps.time !== this.props.time) {
      this.createInterval();
    }
  }

  createInterval() {
    this.removeInterval();

    if (this.props.time < 30) {
      this.interval = setInterval(this.tick, 50);
    } else if (this.props.time < 120) {
      this.interval = setInterval(this.tick, 100);
    } else if (this.props.time < 300) {
      this.interval = setInterval(this.tick, 200);
    } else {
      this.interval = setInterval(this.tick, 1000);
    }
  }

  removeInterval() {
    if (this.interval != null) {
      clearInterval(this.interval);

      this.interval = null;
    }
  }

  tick = () => {
    if (this.state.data == null) {
      this.setState({ lines: null });
      return;
    }

    if (this.props.paused === true) {
      return;
    }

    // calculate yRange
    let yRange = [0, 0];

    if (this.props.yUseMinMax) {
      yRange = [this.props.yMin, this.props.yMax];
    } else if (this.props.data.length > 0) {
      let icDataset = this.props.data.find(function(element) {
        return element !== undefined;
      })

      yRange = [icDataset[0].y, icDataset[0].y];

      this.props.data.forEach(values => {
        const range = extent(values, p => p.y);

        if (range[0] < yRange[0]) yRange[0] = range[0];
        if (range[1] > yRange[1]) yRange[1] = range[1];
      });
    }

    // create scale functions for both axes
    const xScale = scaleTime().domain([Date.now() - this.props.time * 1000, Date.now()]).range([0, this.props.width - leftMargin - this.state.labelMargin - rightMargin]);
    const yScale = scaleLinear().domain(yRange).range([this.props.height + topMargin - bottomMargin, topMargin]);

    const xAxis = axisBottom().scale(xScale).ticks(5).tickFormat(timeFormat("%M:%S"));
    const yAxis = axisLeft().scale(yScale).ticks(5).tickFormat(format(".3s"));

    // generate paths from data
    const sparkLine = line().x(p => xScale(p.x)).y(p => yScale(p.y));
    const newLineColor = scaleOrdinal(schemeCategory10);

    const lines = this.state.data.map((values, index) => {
      let signalID = this.props.signalIDs[index];

      if(this.props.lineColors === undefined || this.props.lineColors === null){
        this.props.lineColors = [] // for backwards compatibility
      }

      if (typeof this.props.lineColors[signalID] === "undefined") {
        this.props.lineColors[signalID] = newLineColor(signalID);
      }
      return <path d={sparkLine(values)} key={index} style={{ fill: 'none', stroke: this.props.lineColors[signalID] }} />
    });

    this.setState({ lines, xAxis, yAxis });
  }

  render() {
    const yLabelPos = {
      x: 12,
      y: this.props.height / 2
    }

    return <svg width={this.props.width - rightMargin + 1} height={this.props.height + topMargin + bottomMargin}>
      <g ref={node => select(node).call(this.state.xAxis)} style={{ transform: `translateX(${leftMargin + this.state.labelMargin}px) translateY(${this.props.height + topMargin - bottomMargin}px)` }} />
      <g ref={node => select(node).call(this.state.yAxis)} style={{ transform: `translateX(${leftMargin + this.state.labelMargin}px)` }} />

      <text strokeWidth="0.005" textAnchor="middle" x={yLabelPos.x} y={yLabelPos.y} transform={`rotate(270 ${yLabelPos.x} ${yLabelPos.y})`}>{this.props.yLabel}</text>
      <text strokeWidth="0.005" textAnchor="end" x={this.props.width - rightMargin} y={this.props.height + topMargin + bottomMargin - 10}>Time [s]</text>

      <defs>
        <clipPath id={"lineClipPath" + this.state.identifier}>
          <rect x={leftMargin + this.state.labelMargin} y={topMargin} width={this.props.width - leftMargin - this.state.labelMargin - rightMargin} height={this.props.height - bottomMargin} />
        </clipPath>
      </defs>

      <g style={{ clipPath: 'url(#lineClipPath' + this.state.identifier + ')' }}>
        {this.state.lines}
      </g>
    </svg>;
  }
}

export default Plot;