/**
 * @module meteoJS/thermodynamicDiagram/hodograph
 */
import {
  windspeedKMHToMS,
  windspeedKNToMS,
  windspeedMSToKMH,
  windspeedMSToKN } from '../calc.js';
import {
  getNormalizedLineOptions,
  getNormalizedTextOptions,
  getNormalizedFontOptions,
  drawTextInto
} from './Functions.js';
import CoordinateSystem from './CoordinateSystem.js';
import PlotDataArea from './PlotDataArea.js';

/**
 * Options for the circle grid.
 * 
 * @typedef {module:meteoJS/thermodynamicDiagram~lineOptions}
 *   module:meteoJS/thermodynamicDiagram/hodograph~gridCirclesOptions
 * @param {number} [interval=13.89]
 *   Interval between grid circles (and value for the first grid circle).
 *   In m/s.
 */

/**
 * Options for a text backdrop.
 * 
 * @typedef {Object}
 *   module:meteoJS/thermodynamicDiagram/hodograph~backdropOptions
 * @property {boolean} [visible=true] - Visibility.
 * @property {mixed} [color='white'] - Color.
 */

/**
 * Options for the grid labels.
 * 
 * @typedef {module:meteoJS/thermodynamicDiagram~textOptions}
 *   module:meteoJS/thermodynamicDiagram/hodograph~gridLabelsOptions
 * @property {number} [angle=225]
 *   Angle of the labels starting from the origin
 *   (in degrees, 0 relates to North).
 * @property {string} [unit='km/h']
 *   Unit of the label values. Allowed values: 'm/s', 'kn', 'km/h'
 * @property {string} [prefix=''] - Prefix of the label text.
 * @property {integer} [decimalPlaces=0] - Number of digits to appear after
 *   the decimal point of the label values.
 * @property {module:meteoJS/thermodynamicDiagram/hodograph~backdropOptions}
 *   [backdrop] - Options for the backdrop of the grid labels.
 */

/**
 * Options for the hover labels in the hodograph.
 * 
 * @typedef {module:meteoJS/thermodynamicDiagram/tdDiagram~labelsOptions}
 *   module:meteoJS/thermodynamicDiagram/hodograph~labelsOptions
 * @property {Object} [pressure] - Options for the output of the pressure value.
 * @property {boolean} [pressure.visible=true] - Visibility.
 * @property {integer} [pressure.decimalPlaces=0]
 *   Number of digits to appear after the decimal point.
 * @property {string} [pressure.prefix=' hPa'] - Prefix of the value text.
 * @property {Object} [windspeed]
 *   Options for the output of the windspeed value.
 * @property {boolean} [windspeed.visible=true] - Visibility.
 * @property {string} [windspeed.unit='kn']
 *   Unit of the value text. Allowed values: 'm/s', 'kn', 'km/h'
 * @property {integer} [windspeed.decimalPlaces=0]
 *   Number of digits to appear after the decimal point.
 * @property {string} [windspeed.prefix=' kn'] - Prefix of the value text.
 * @property {Object} [winddir] - Options for the output of the winddir value.
 * @property {boolean} [winddir.visible=true] - Visibility.
 * @property {integer} [winddir.decimalPlaces=0]
 *   Number of digits to appear after the decimal point.
 * @property {string} [winddir.prefix='°'] - Prefix of the value text.
 */

/**
 * Options for the hover labels.
 * 
 * @typedef {module:meteoJS/thermodynamicDiagram/plotDataArea~hoverLabelsOptions}
 *   module:meteoJS/thermodynamicDiagram/hodograph~hoverLabelsOptions
 * @property {number} [maxDistance=20]
 *   Maximum distance to a data point to show a hover label in pixels.
 *   If undefined, always a hover label to the nearest point is shown.
 * @property {module:meteoJS/thermodynamicDiagram/hodograph~labelsOptions}
 *   [hodograph] - Options for hodograph label.
 */

/**
 * Options for the constructor.
 * 
 * @typedef {module:meteoJS/thermodynamicDiagram/plotDataArea~options}
 *   module:meteoJS/thermodynamicDiagram/hodograph~options
 * @param {Object} [grid] - Options for the hodograph grid.
 * @param {module:meteoJS/thermodynamicDiagram~lineOptions} [grid.axes]
 *   Options for the hodograph's x- and y-axes.
 * @param {module:meteoJS/thermodynamicDiagram/hodograph~gridCirclesOptions}
 *   [grid.circles] - Options for the hodograph circle grid.
 * @param {module:meteoJS/thermodynamicDiagram/hodograph~gridLabelsOptions}
 *   [grid.labels] - Options for the hodograph grid labels.
 * @param {number|undefined} [grid.max=undefined]
 *   Maximum value for the grid axes and circles. If undefined, determined from
 *   'windspeedMax'.
 * @param {number} [windspeedMax=41.67]
 *   The maximum windspeed [m/s], that should be visible on the plot. This
 *   refers to the x- or y-direction with the origin in the middle of the plot,
 *   because in these directions, a polar plot has the least extent concerning
 *   distance.
 * @param {number[]|undefined} [origin=undefined]
 *   Move origin of polar plot. If 'undefined' the origin is in the center. To
 *   move, use an array with 2 elements. The first element moves the origin in
 *   x direction, the second in y direction. The values are interpreted as
 *   relative length (relating to the half width resp. height). Positive values
 *   to move in North-East direction. E.g. to move the origin the half way to
 *   the upper right corner, use [0.5, 0.5].
 * @param {module:meteoJS/thermodynamicDiagram/hodograph~hoverLabelsOptions}
 *   [hoverLabels] - Hover labels options.
 */

/**
 * Class to draw the hodograph.
 * 
 * <pre><code>import Hodograph from 'meteojs/thermodynamicDiagram/Hodograph';</code></pre>
 * 
 * @extends module:meteoJS/thermodynamicDiagram/plotDataArea.PlotDataArea
 */
export class Hodograph extends PlotDataArea {
  
  /**
   * @param {module:meteoJS/thermodynamicDiagram/hodograph~options} options
   *   Options.
   */
  constructor({
    svgNode = undefined,
    coordinateSystem = new CoordinateSystem(),
    x,
    y,
    width,
    height,
    style = {},
    visible = true,
    events = {},
    hoverLabels = {},
    dataGroupIds = ['windbarbs'],
    getCoordinatesByLevelData = (dataGroupId, sounding, levelData, plotArea) => {
      let x = undefined;
      let y = undefined;
      if (levelData.wspd !== undefined &&
        levelData.wdir !== undefined) {
        x = levelData.wspd * -Math.sin(levelData.wdir / 180 * Math.PI);
        y = levelData.wspd * Math.cos(levelData.wdir / 180 * Math.PI);
      }
      else if (levelData.u !== undefined &&
        levelData.v !== undefined) {
        x = levelData.u;
        y = -levelData.v;
      }
      if (x === undefined ||
          y === undefined)
        return {};
      return {
        x: plotArea.center[0] + x * plotArea.pixelPerSpeed,
        y: plotArea.center[1] + y * plotArea.pixelPerSpeed
      };
    },
    insertDataGroupInto = (svgNode, dataGroupId, sounding, data) => {
      const basePolylines = [data
        .filter(level => {
          if (sounding.options.hodograph.minPressure !== undefined
            && level.levelData.pres !== undefined
            && level.levelData.pres < sounding.options.hodograph.minPressure)
            return false;
          if (sounding.options.hodograph.maxPressure !== undefined
            && level.levelData.pres !== undefined
            && level.levelData.pres > sounding.options.hodograph.maxPressure)
            return false;
          return true;
        })];
      basePolylines[0].sort((a,b) => b.levelData.pres-a.levelData.pres);
      const segmentPolylines = [];
      for (const segment of sounding.options.hodograph.segments) {
        const def = {
          levels: [],
          visible: segment.visible,
          style: segment.style
        };
        basePolylines.map((basePolyline, i) => {
          let lowSplit = undefined;
          let highSplit = undefined;
          basePolyline.map(l => {
            if ((segment.minPressure !== undefined && segment.minPressure <= l.levelData.pres
              && segment.maxPressure !== undefined && segment.maxPressure >= l.levelData.pres)
              || (segment.minPressure === undefined
              && segment.maxPressure !== undefined && segment.maxPressure >= l.levelData.pres)
              || (segment.minPressure !== undefined && segment.minPressure <= l.levelData.pres
              && segment.maxPressure === undefined)) {
              def.levels.push(l);
              if (highSplit === undefined)
                highSplit = l;
              lowSplit = l;
            }
          });
          if (highSplit !== undefined && lowSplit !== undefined && highSplit !== lowSplit) {
            const indexLow = basePolyline
              .findIndex(l => l.levelData.pres === lowSplit.levelData.pres);
            const indexHigh = basePolyline
              .findIndex(l => l.levelData.pres === highSplit.levelData.pres);
            const newBaseLine = basePolyline.slice(indexLow);
            basePolylines[i] = basePolyline.slice(0, indexHigh+1);
            basePolylines.push(newBaseLine);
          }
        });
        if (def.levels.length > 0)
          segmentPolylines.push(def);
      }
      basePolylines.map(basePolyline => {
        if (basePolyline.length < 2)
          return;
        svgNode
          .polyline(basePolyline.map(level => [ level.x, level.y ]))
          .fill('none').stroke(sounding.options.hodograph.style);
      });
      segmentPolylines.map(segmentPolyline => {
        svgNode
          .polyline(segmentPolyline.levels.map(level => [ level.x, level.y ]))
          .fill('none').stroke(segmentPolyline.style);
      });
    },
    grid = {},
    windspeedMax = windspeedKNToMS(150),
    origin = undefined,
    filterDataPoint = undefined,
    minDataPointsDistance = 0
  } = {}) {
    super({
      svgNode,
      coordinateSystem,
      x,
      y,
      width,
      height,
      style,
      visible,
      events,
      hoverLabels,
      dataGroupIds,
      getCoordinatesByLevelData,
      insertDataGroupInto,
      getSoundingVisibility:
        sounding => sounding.visible && sounding.options.hodograph.visible,
      filterDataPoint,
      minDataPointsDistance
    });

    /**
     * @type number[]|undefined
     * @private
     */
    this._origin = origin;

    /**
     * @type number
     * @private
     */
    this._windspeedMax = windspeedMax;
    
    this._gridOptions = this.getNormalizedGridOptions(grid);

    if (this._gridOptions.max === undefined)
      this._gridOptions.max = windspeedMax;
    this.init();
  }

  /**
   * Origin of the hodograph relative to the plot area. If not undefined, it
   * has to be a 2-element array. The first element moves the origin in
   * x direction, the second in y direction. The values are interpreted as
   * relative length (relating to the half width resp. height). Positive values
   * to move in North-East direction. E.g. to move the origin the half way to
   * the upper right corner, use [0.5, 0.5].
   * 
   * @type number[]|undefined
   * @public
   */
  get origin() {
    return this._origin;
  }
  set origin(origin) {
    const oldOrigin = this._origin;
    this._origin = origin;
    this._hoverLabelsGroup.clear();
    if (oldOrigin === undefined && this._origin !== undefined
      || oldOrigin !== undefined && this._origin === undefined
      || (oldOrigin !== undefined && this._origin !== undefined
      && (oldOrigin[0] != this._origin[0]
      || oldOrigin[1] != this._origin[1])))
      this.onCoordinateSystemChange();
  }

  /**
   * The origin of the hodograph in pixel coordinates.
   * 
   * @type number[]
   * @public
   * @readonly
   */
  get center() {
    const center = [this.width/2, this.height/2];
    if (this._origin !== undefined) {
      center[0] += this._origin[0] * this.minExtentLength/2;
      center[1] -= this._origin[1] * this.minExtentLength/2;
    }
    return center;
  }

  /**
   * Returns the pixel per speed unit. Mainly for internal usage.
   * 
   * @type number
   * @public
   * @readonly
   */
  get pixelPerSpeed() {
    const center = this.center;
    return Math.min(
      Math.max(this.width - center[0], center[0]),
      Math.max(this.height - center[1], center[1])
    ) / this._windspeedMax;
  }
  
  /**
   * Plots hodograph background.
   * 
   * @override
   */
  _drawBackground(svgNode) {
    super._drawBackground(svgNode);
    
    const center = this.center;
    const pixelPerSpeed = this.pixelPerSpeed;
    // x-/y-axes
    if (this._gridOptions.axes.visible) {
      svgNode
        .line(0, center[1], this.width, center[1])
        .stroke(this._gridOptions.axes.style);
      svgNode
        .line(center[0], 0, center[0], this.height)
        .stroke(this._gridOptions.axes.style);
    }
    
    // circles and labels
    for (let v = this._gridOptions.circles.interval;
      v <= this._gridOptions.max;
      v += this._gridOptions.circles.interval) {
      let radius = v * pixelPerSpeed;
      svgNode
        .circle(2*radius)
        .attr({
          cx: center[0],
          cy: center[1]
        })
        .fill('none')
        .stroke(this._gridOptions.circles.style);
      if (this._gridOptions.labels.visible) {
        let xText =
          radius *
          Math.cos((this._gridOptions.labels.angle - 90) / 180 * Math.PI);
        let yText =
          radius *
          Math.sin((this._gridOptions.labels.angle - 90) / 180 * Math.PI);
        let text = '';
        switch (this._gridOptions.labels.unit) {
        case 'm/s':
          text = Number.parseFloat(v)
            .toFixed(this._gridOptions.labels.decimalPlaces);
          break;
        case 'kn':
          text = windspeedMSToKN(v)
            .toFixed(this._gridOptions.labels.decimalPlaces);
          break;
        default:
          text = windspeedMSToKMH(v)
            .toFixed(this._gridOptions.labels.decimalPlaces);
          break;
        }
        text += this._gridOptions.labels.prefix;
        let fontColor = undefined;
        const font = {...this._gridOptions.labels.font};
        if ('color' in font) {
          fontColor = font.color;
          delete font.color;
        }
        const textNode = svgNode
          .plain(text)
          .font(this._gridOptions.labels.font)
          .center(center[0] + xText, center[1] + yText);
        if (fontColor !== undefined)
          textNode.fill(fontColor);
        if (font['text-anchor'] == 'end')
          textNode.dx(-textNode.bbox().width/2-3);
        else if (font['text-anchor'] == 'start')
          textNode.dx(+textNode.bbox().width/2+3);
        if (this._gridOptions.labels.angle == 90
          || this._gridOptions.labels.angle == 270)
          textNode.dy(textNode.bbox().height/2+3);

        if (this._gridOptions.labels.backdrop.visible) {
          const bbox = textNode.bbox();
          textNode.before(
            svgNode
              .rect(bbox.width, bbox.height)
              .move(bbox.x, bbox.y)
              .fill({ color: this._gridOptions.labels.backdrop.color })
          );
        }
      }
    }
  }
  
  /**
   * Normalizes options for grid.
   * 
   * @private
   */
  getNormalizedGridOptions({
    axes = {},
    circles = {},
    labels = {},
    max = undefined
  }) {
    axes = getNormalizedLineOptions(axes);
    circles = getNormalizedLineOptions(circles);
    if (!('interval' in circles) ||
        circles.interval === undefined)
      circles.interval = windspeedKMHToMS(50);
    labels = getNormalizedTextOptions(labels);
    if (!('angle' in labels) ||
        labels.angle === undefined)
      labels.angle = 225;
    if (!('unit' in labels) ||
        labels.unit === undefined)
      labels.unit = 'km/h';
    if (!('prefix' in labels) ||
        labels.prefix === undefined)
      labels.prefix = '';
    if (!('decimalPlaces' in labels) ||
        labels.decimalPlaces === undefined)
      labels.decimalPlaces = 0;
    if (!('backdrop' in labels) ||
      labels.backdrop === undefined)
      labels.backdrop = {};
    if (!('color' in labels.backdrop))
      labels.backdrop.color = 'white';
    if (!('visible' in labels.backdrop))
      labels.backdrop.visible = true;
    if (labels.font.size === undefined)
      labels.font.size = 10;
    
    return {
      axes,
      circles,
      labels,
      max
    };
  }

  /**
   * Initialize hover labels options.
   * 
   * @param {module:meteoJS/thermodynamicDiagram/hodograph~hoverLabelsOptions}
   *   options - Hover labels options.
   */
  _initHoverLabels({
    visible = true,
    type = 'mousemove',
    maxDistance = 20,
    insertLabelsFunc = undefined,
    getLevelData = ({ hoverLabelsSounding, e, maxDistance }) => {
      const sounding = hoverLabelsSounding.sounding;

      let smallestDistanceSquare = undefined;
      let nearestLevelData = undefined;
      sounding.getLevels()
        .filter(pres => 
          (hoverLabelsSounding.options.hodograph.minPressure === undefined
          || hoverLabelsSounding.options.hodograph.minPressure <= pres)
          && (hoverLabelsSounding.options.hodograph.maxPressure === undefined
          || pres <= hoverLabelsSounding.options.hodograph.maxPressure))
        .map(pres => {
          const levelData = sounding.getData(pres);
          if (levelData.wspd === undefined || levelData.wdir === undefined)
            return;
          const { x, y } =
            this._getCoordinatesByLevelData('windbarbs',
              sounding, levelData, this);
          const distanceSquare =
            Math.pow(e.elementX - x, 2)
            + Math.pow(e.elementY - y, 2);
          if (nearestLevelData === undefined
            || distanceSquare < smallestDistanceSquare) {
            smallestDistanceSquare = distanceSquare;
            nearestLevelData = levelData;
          }
        });

      if (maxDistance !== undefined
        && Math.pow(maxDistance, 2) < smallestDistanceSquare)
        nearestLevelData = {};
      return nearestLevelData;
    },
    getHoverSounding = undefined,
    hodograph = {}
  }) {
    if (!('visible' in hodograph))
      hodograph.visible = true;
    if (!('style' in hodograph))
      hodograph.style = {};
    hodograph.font = getNormalizedFontOptions(hodograph.font, {
      anchor: 'end',
      'alignment-baseline': 'bottom'
    });
    if (!('fill' in hodograph))
      hodograph.fill = {};
    if (hodograph.fill.opacity === undefined)
      hodograph.fill.opacity = 0.7;
    if (hodograph.fill.color === undefined)
      hodograph.fill.color = 'white';
    if (insertLabelsFunc === undefined)
      insertLabelsFunc = this._makeInsertLabelsFunc(hodograph);

    super._initHoverLabels({
      visible,
      type,
      maxDistance,
      insertLabelsFunc,
      getLevelData,
      getHoverSounding
    });
  }

  /**
   * Makes a default insertLabelsFunc.
   * 
   * @param {module:meteoJS/thermodynamicDiagram/hodograph~labelsOptions}
   *   options - Style options for the hover labels.
   * @private
   */
  _makeInsertLabelsFunc({
    visible = true,
    style = {},
    font = {},
    fill = {},
    horizontalMargin = 10,
    verticalMargin = 0,
    radius = undefined,
    radiusPlus = 2,
    pressure = {},
    windspeed = {},
    winddir = {}
  }) {
    pressure = (({
      visible = true,
      decimalPlaces = 0,
      prefix = ' hPa'
    }) => { return { visible, decimalPlaces, prefix }; })(pressure);
    windspeed =  (({
      visible = true,
      unit = 'kn',
      decimalPlaces = 0,
      prefix = ' kn'
    }) => { return { visible, unit, decimalPlaces, prefix }; })(windspeed);
    winddir =  (({
      visible = true,
      decimalPlaces = 0,
      prefix = '°'
    }) => { return { visible, decimalPlaces, prefix }; })(winddir);
    return (sounding, levelData, group) => {
      group.clear();

      if (levelData === undefined
        || !visible)
        return;

      const { x, y } =
        this._getCoordinatesByLevelData('windbarbs',
          sounding, levelData, this);
      if (x === undefined ||
          y === undefined)
        return;

      let defaultStyle = sounding.options.hodograph.style;
      if (levelData.pres !== undefined)
        sounding.options.hodograph.segments.map(segment => {
          if ((segment.minPressure === undefined
            || segment.minPressure <= levelData.pres)
            && (segment.maxPressure === undefined
            || segment.maxPressure >= levelData.pres))
            defaultStyle = segment.style;
        });
      
      const dotRadius = (radius === undefined)
        ? defaultStyle.width / 2 + radiusPlus
        : radius;
      const fillOptions = {...style}; // Deep copy
      if (!('color' in fillOptions))
        fillOptions.color = defaultStyle.color;
      group
        .circle(2 * dotRadius)
        .attr({ cx: x, cy: y })
        .fill(fillOptions);
      const background = group.rect().fill(fill);
      const labelFont = {...font}; // Deep copy
      labelFont.anchor = 'start';
      if (labelFont.anchor == 'start' &&
          this.width - x < 45)
        labelFont.anchor = 'end';
      if (labelFont.anchor == 'end' &&
          x < 45)
        labelFont.anchor = 'start';
      let yDelta = 0;
      let textGroups = [];
      const texts = [];
      if (pressure.visible) {
        const text = Number.parseFloat(levelData.pres)
          .toFixed(pressure.decimalPlaces);
        texts.push(`${text}${pressure.prefix}`);
      }
      if (windspeed.visible) {
        let text = '';
        switch (windspeed.unit) {
        case 'm/s':
          text = Number.parseFloat(levelData.wspd)
            .toFixed(windspeed.decimalPlaces);
          break;
        case 'kn':
          text = windspeedMSToKN(levelData.wspd)
            .toFixed(windspeed.decimalPlaces);
          break;
        default:
          text = windspeedMSToKMH(levelData.wspd)
            .toFixed(windspeed.decimalPlaces);
          break;
        }
        texts.push(`${text}${windspeed.prefix}`);
      }
      if (winddir.visible) {
        const text = Number.parseFloat(levelData.wdir)
          .toFixed(winddir.decimalPlaces);
        texts.push(`${text}${winddir.prefix}`);
      }
      texts.map(text => {
        yDelta += labelFont.size * 5/4;
        textGroups.push(drawTextInto({
          node: group,
          text,
          x,
          y: y + yDelta,
          horizontalMargin,
          verticalMargin,
          font: labelFont
        }));
      });
      if (y+yDelta > this.height)
        textGroups.map(g => g.dy(-yDelta));
      const maxBBox = {
        x: undefined,
        y: undefined,
        x2: undefined,
        y2: undefined
      };
      textGroups.map(g => {
        g.children().map(el => {
          if (el.type != 'text')
            return;
          const bbox = el.bbox();
          if (maxBBox.x === undefined || bbox.x < maxBBox.x)
            maxBBox.x = bbox.x;
          if (maxBBox.y === undefined || bbox.y < maxBBox.y)
            maxBBox.y = bbox.y;
          if (maxBBox.x2 === undefined || maxBBox.x2 < bbox.x2)
            maxBBox.x2 = bbox.x2;
          if (maxBBox.y2 === undefined || maxBBox.y2 < bbox.y2)
            maxBBox.y2 = bbox.y2;
        });
      });
      background.attr({
        x: maxBBox.x,
        y: maxBBox.y,
        width: maxBBox.x2 - maxBBox.x,
        height: maxBBox.y2 - maxBBox.y
      });
    };
  }
}
export default Hodograph;