/**
 * @module meteoJS/thermodynamicDiagram/coordinateSystem
 */
import { tempCelsiusToKelvin,
  tempByPotentialTempAndPres,
  tempByEquiPotTempAndPres,
  potentialTempByTempAndPres,
  dewpointByHMRAndPres,
  saturationHMRByTempAndPres } from '../calc.js';
import addEventFunctions from '../Events.js';

/**
 * Options change event.
 * 
 * @event module:meteoJS/thermodynamicDiagram/coordinateSystem#change:options
 */

/**
 * Pressure options.
 * 
 * @typedef {Object} module:meteoJS/thermodynamicDiagram/coordinateSystem~pressureOptions
 * @property {number} [min=100] - Minimum pressure on the diagram.
 * @property {number} [max=1050] - Maximum pressure on the diagram.
 */

/**
 * Temperature options.
 * 
 * @typedef {Object} module:meteoJS/thermodynamicDiagram/coordinateSystem~temperatureOptions
 * @property {number} [min=-40°C]
 *   Temperature either on bottom-left on the diagram (if reference equals
 *   'base') or on the left of an isobar (if reference is a number).
 * @property {number} [max=45°C]
 *   Temperature either on bottom-right on the diagram (if reference equals
 *   'base') or on the right of an isobar (if reference is a number).
 * @property {'base'|integer} [reference='base']
 *   Reference for 'min' and 'max' values. Allowed values: 'base' or number.
 * @property {integer} [inclinationAngle=45]
 *   Angle of inclination to the right of the isotherms. Allowed values between
 *   0 and 90 (exclusive), in degrees.
 */

/**
 * Options for the constructor.
 * 
 * @typedef {Object} module:meteoJS/thermodynamicDiagram/coordinateSystem~options
 * @param {integer} [width=100] - Width of the diagram.
 * @param {integer} [height=100] - Height of the diagram.
 * @param {module:meteoJS/thermodynamicDiagram/coordinateSystem~pressureOptions}
 *   [pressure] - Pressure options.
 * @param {module:meteoJS/thermodynamicDiagram/coordinateSystem~temperatureOptions}
 *   [temperature] - Temperature options.
 */

/**
 * Abstract class to specify the coordinate system of the thermodynamicDiagram.
 * 
 * Child classes define the explicit coordinate system.
 * This class defines already: (can be overridden by child classes)
 * * log-P y-axes with horizontal isobars
 * * straight isotherms, inclinated to the right
 * 
 * <pre><code>import CoordinateSystem from 'meteojs/thermodynamicDiagram/CoordinateSystem';</code></pre>
 * 
 * @abstract
 * @fires module:meteoJS/thermodynamicDiagram/coordinateSystem#change:options
 */
export class CoordinateSystem {
  
  /**
   * @param {module:meteoJS/thermodynamicDiagram/coordinateSystem~options} options
   */
  constructor({
    width = 100,
    height = 100,
    pressure = {},
    temperature = {}
  } = {}) {
    /**
     * @type integer
     * @private
     */
    this._width = width;
    
    /**
     * @type integer
     * @private
     */
    this._height = height;
    
    /**
     * @type number
     * @private
     */
    this.temperatureBottomLeft;
    
    /**
     * @type number
     * @private
     */
    this.temperatureBottomRight;
    
    /**
     * @type number
     * @private
     */
    this.inclinationTan;
    
    /**
     * @type Object
     * @private
     */
    this.options = {
      pressure: {},
      temperature: {}
    };
    
    this._initPressureOptions(pressure);
    this._initTemperatureOptions(temperature);
  }
  
  /**
   * Visible width, in pixels.
   * 
   * @type integer
   * @public
   */
  get width() {
    return this._width;
  }
  set width(width) {
    const oldWidth = this._width;
    this._width = width;
    if (oldWidth != this._width)
      this.trigger('change:options');
  }
  
  /**
   * Visible height, in pixels.
   * 
   * @type integer
   * @public
   */
  get height() {
    return this._height;
  }
  set height(height) {
    const oldHeight = this._height;
    this._height = height;
    if (oldHeight != this._height)
      this.trigger('change:options');
  }
  
  /**
   * Returns if isobars are straight lines in the defined coordinate system.
   * 
   * @returns {boolean}
   */
  isIsobarsStraightLine() {
    return true;
  }

  /**
   * Returns if the dry adiabats are straight lines
   * in the defined coordinate system.
   * 
   * @returns {boolean}
   */
  isDryAdiabatStraightLine() {
    return false;
  }
  
  /**
   * @returns {boolean}
   */
  isIsothermsVertical() {
    return (this.options.temperature.inclinationAngle !== undefined) &&
         (this.options.temperature.inclinationAngle == 0);
  }

  /**
   * Pressure for a x-y coordinate.
   * Implementation valid for horizontal isobars, log-P y-axes.
   * 
   * @param {number} x - Pixels from the left.
   * @param {number} y - Pixels from bottom.
   * @returns {number} Pressure in hPa.
   */
  getPByXY(x, y) {
    return Math.pow(this.options.pressure.min, y / this.height) *
         Math.pow(this.options.pressure.max,
           (this.height - y)/this.height);
  }

  /**
   * Temperature for x-y coordinate.
   * Implementation valid for straight isotherms.
   * 
   * @param {number} x - Pixels from the left.
   * @param {number} y - Pixels from bottom.
   * @returns {number} Temperature in Kelvin.
   */
  getTByXY(x, y) {
  // bottom x coordinate of isotherm
    let x0 = x - y * this.inclinationTan;
    return this.temperatureBottomLeft +
    x0 *
    (this.temperatureBottomRight-this.temperatureBottomLeft) / this.width;
  }

  /**
   * y coordinate for pressure and x coordinate.
   * Implementation valid for horizontal isobars, log-P y-axes.
   * 
   * @param {number} x - Pixels from the left.
   * @param {number} p - Pressure in hPa.
   * @returns {number} Pixels from bottom.
   */
  getYByXP(x, p) {
    return this.height *
    Math.log(this.options.pressure.max / p) /
    Math.log(this.options.pressure.max / this.options.pressure.min);
  }

  /**
   * Temperature for pressure and x coordinate.
   * Implementation valid for horizontal isobars, log-P y-axes and straight
   * isotherms.
   * 
   * @param {number} x - Pixels from the left.
   * @param {number} p - Pressure in hPa.
   * @returns {number} Temperature in Kelvin.
   */
  getTByXP(x, p) {
    return this.getTByXY(x, this.getYByXP(x, p));
  }

  /**
   * x coordinate for temperature and y coordinate.
   * Implementation valid for straight isotherms.
   * 
   * @param {number} y - Pixels from bottom.
   * @param {number} T - Temperature in Kelvin.
   * @returns {number} Pixels from the left.
   */
  getXByYT(y, T) {
  // bottom x coordinate 
    let x0 =
    (T-this.temperatureBottomLeft) *
    this.width / (this.temperatureBottomRight-this.temperatureBottomLeft);
    return x0 + y * this.inclinationTan;
  }

  /**
   * y coordinate for temperature and x coordinate.
   * Implementation valid for straight isotherms.
   * 
   * @param {number} x - Pixels from the left.
   * @param {number} T - Temperature in Kelvin.
   * @returns {number|undefined} Pixels from bottom.
   */
  getYByXT(x, T) {
    return (this.inclinationTan != 0) ?
      (x - this.getXByYT(0, T)) / this.inclinationTan :
      undefined;
  }

  /**
   * x coordinate for pressure and temperature.
   * Implementation valid for horizontal isobars, log-P y-axes and straight
   * isotherms.
   * 
   * @param {number} p - Pressure in hPa.
   * @param {number} T - Temperature in Kelvin.
   * @returns {number} Pixels from the left.
   */
  getXByPT(p, T) {
    return this.getXByYT(this.getYByXP(0, p), T);
  }

  /**
   * y coordinate for pressure and temperature.
   * Implementation valid for horizontal isobars, log-P y-axes and straight
   * isotherms.
   * 
   * @param {number} p - Pressure in hPa.
   * @param {number} T - Temperature in Kelvin.
   * @returns {number} Pixels from bottom.
   */
  getYByPT(p) {
    return this.getYByXP(0, p);
  }

  /**
   * x coordinate for potential temperature and y coordinate.
   * Implementation valid for horizontal isobars, log-P y-axes and straight
   * isotherms.
   * 
   * @param {number} y - Pixels from bottom.
   * @param {number} T - Potential temperature in Kelvin.
   * @returns {number} Pixels from the left.
   */
  getXByYPotentialTemperature(y, T) {
    T = tempByPotentialTempAndPres(T, this.getPByXY(0, y));
    return this.getXByYT(y, T);
  }

  /**
   * y coordinate for potential temperature and x coordinate.
   * Implementation valid for horizontal isobars, log-P y-axes and straight
   * isotherms.
   * 
   * @param {number} x - Pixels from the left.
   * @param {number} T - Potential temperature in Kelvin.
   * @returns {number|undefined} Pixels from bottom.
   */
  getYByXPotentialTemperature(x, T) {
    let a = this.getPByXY(x, 0);
    let b = this.getPByXY(x, this.height);
    if (potentialTempByTempAndPres(this.getTByXP(x, b), b) < T ||
      T < potentialTempByTempAndPres(this.getTByXP(x, a), a))
      return undefined;
    while (a-b > 10) {
      let p = b+(a-b)/2;
      let tBin = this.getTByXP(x, p);
      let potTemp = potentialTempByTempAndPres(tBin, p);
      if (potTemp === undefined)
        return undefined;
      if (potTemp < T)
        a = p;
      else
        b = p;
    }
    let y = this.getYByXP(x, b+(a-b)/2);
    return y;
  }

  /**
   * x coordinate for pressure and potential temperature.
   * Implementation valid for horizontal isobars, log-P y-axes and straight
   * isotherms.
   * 
   * @param {number} p - Pressure in hPa.
   * @param {number} T - Potential temperature in Kelvin.
   * @returns {number} Pixels from the left.
   */
  getXByPPotentialTemperatur(p, T) {
    T = tempByPotentialTempAndPres(T, p);
    return this.getXByPT(p, T);
  }

  /**
   * y coordinate for pressure and potential temperature.
   * Implementation valid for horizontal isobars, log-P y-axes and straight
   * isotherms.
   * 
   * @param {number} p - Pressure in hPa.
   * @param {number} T - Potential temperature in Kelvin.
   * @returns {number} Pixels from bottom.
   */
  getYByPPotentialTemperatur(p, T) {
    let x = this.getXByPPotentialTemperatur(p, T);
    return this.getYByXPotentialTemperature(x, T);
  }

  /**
   * x coordinate for humid mixing ratio and y coordinate.
   * Implementation valid for horizontal isobars, log-P y-axes and straight
   * isotherms.
   * 
   * @param {number} y - Pixels from bottom.
   * @param {number} hmr - Humid mixing ratio. []
   * @returns {number} Pixels from the left.
   */
  getXByYHMR(y, hmr) {
    let p = this.getPByXY(0, y); // horizontal isobars
    return this.getXByYT(y, dewpointByHMRAndPres(hmr, p));
  }

  /**
   * y coordinate for humid mixing ratio and x coordinate.
   * Implementation valid for horizontal isobars, log-P y-axes and straight
   * isotherms.
   * 
   * @param {number} x - Pixels from the left.
   * @param {number} hmr - Humid mixing ratio. []
   * @returns {number|undefined} Pixels from bottom.
   */
  getYByXHMR(x, hmr) {
    let a = this.getPByXY(x, 0);
    let b = this.getPByXY(x, this.height);
    while (a-b > 10) {
      let p = b+(a-b)/2;
      let hmrp = saturationHMRByTempAndPres(this.getTByXP(x, p), p);
      if (hmrp === undefined)
        return undefined;
      if (hmrp < hmr)
        b = p;
      else
        a = p;
    }
    let y = this.getYByXP(x, b+(a-b)/2);
    return y;
  }

  /**
   * x coordinate for pressure and humid mixing ratio.
   * Implementation valid for horizontal isobars, log-P y-axes and straight
   * isotherms.
   * 
   * @param {number} p - Pressure in hPa.
   * @param {number} hmr - Humid mixing ratio. []
   * @returns {number} Pixels from the left.
   */
  getXByPHMR(p, hmr) {
    let dewpoint = dewpointByHMRAndPres(hmr, p);
    return this.getXByPT(p, dewpoint);
  }

  /**
   * y coordinate for pressure and humid mixing ratio.
   * Implementation valid for horizontal isobars, log-P y-axes and straight
   * isotherms.
   * 
   * @param {number} p - Pressure in hPa.
   * @param {number} hmr - Humid mixing ratio. []
   * @returns {number|undefined} Pixels from bottom.
   */
  getYByPHMR(p, hmr) {
    let dewpoint = dewpointByHMRAndPres(hmr, p);
    return this.getYByPT(p, dewpoint);
  }

  /**
   * x coordinate for equipotential temperature and y coordainte.
   * Implementation valid for horizontal isobars, log-P y-axes and straight
   * isotherms.
   * 
   * @param {number} y - Pixels from bottom.
   * @param {number} thetae - Equipotential temperaturen in Kelvin.
   * @returns {number} Pixels from the left.
   */
  getXByYEquiPotTemp(y, thetae) {
    let T = tempByEquiPotTempAndPres(thetae, this.getPByXY(0, y));
    return this.getXByYT(y, T);
  }

  /**
   * y coordinate for equipotential temperature and x coordinate.
   * Implementation valid for horizontal isobars, log-P y-axes and straight
   * isotherms.
   * 
   * @param {number} x - Pixels from the left.
   * @param {number} thetae - Equipotential temperaturen in Kelvin.
   * @returns {number|undefined} Pixels from bottom.
   */
  getYByXEquiPotTemp(x, thetae) {
    let a = 0;
    let b = this.height;
    let y = undefined;
    while (b-a > 10) {
      y = a+(b-a)/2;
      let thetaEY =
      this.getYByXT(x,
        tempByEquiPotTempAndPres(thetae, this.getPByXY(x, y)));
      if (thetaEY === undefined)
        return undefined;
      if (thetaEY < thetae)
        b = y;
      else
        a = y;
    }
    return y;
  }

  /**
   * x coordinate for pressure and equipotential temperature .
   * Implementation valid for horizontal isobars, log-P y-axes and straight
   * isotherms.
   * 
   * @param {number} p - Pressure in hPa.
   * @param {number} thetae - Equipotential temperaturen in Kelvin.
   * @returns {number} Pixels from the left.
   */
  getXByPEquiPotTemp(p, thetae) {
    let T = tempByEquiPotTempAndPres(thetae, p);
    return this.getXByPT(p, T);
  }

  /**
   * y coordinate for pressure and equipotential temperature .
   * Implementation valid for horizontal isobars, log-P y-axes and straight
   * isotherms.
   * 
   * @param {number} p - Pressure in hPa.
   * @param {number} thetae - Equipotential temperaturen in Kelvin.
   * @returns {number|undefined} Pixels from bottom.
   */
  getYByPEquiPotTemp(p, thetae) {
    let T = tempByEquiPotTempAndPres(thetae, p);
    return this.getYByPT(p, T);
  }
  
  /**
   * Updates options. To restore a default value, pass undefined.
   * 
   * @param {Object} [options] - Options.
   * @param {module:meteoJS/thermodynamicDiagram/coordinateSystem~pressureOptions}
   *   [options.pressure] - Pressure options.
   * @param {module:meteoJS/thermodynamicDiagram/coordinateSystem~temperatureOptions}
   *   [options.temperature] - Temperature options.
   */
  update({
    pressure = {},
    temperature = {}
  } = {}) {
    if ('min' in pressure)
      this.options.pressure.min =
        (pressure.min === undefined) ? 100 : pressure.min;
    if ('max' in pressure)
      this.options.pressure.max =
        (pressure.max === undefined) ? 1000 : pressure.max;
    
    if ('min' in temperature)
      this.options.temperature.min =
        (temperature.min === undefined)
          ? tempCelsiusToKelvin(-40) : temperature.min;
    if ('max' in temperature)
      this.options.temperature.max =
        (temperature.max === undefined)
          ? tempCelsiusToKelvin(-45) : temperature.max;
    if ('reference' in temperature)
      this.options.temperature.reference =
        (temperature.reference === undefined) ? 'base' : temperature.reference;
    if ('inclinationAngle' in temperature)
      this.options.temperature.inclinationAngle =
        (temperature.inclinationAngle === undefined)
          ? 45 : temperature.inclinationAngle;
    
    this._normalizeTemperatureRange();
    
    this.trigger('change:options');
  }
  
  /**
   * @private
   */
  _initPressureOptions({
    min = 100,
    max = 1050
  }) {
    this.options.pressure = {
      min,
      max
    };
  }
  
  /**
   * @private
   */
  _initTemperatureOptions({
    min = tempCelsiusToKelvin(-40),
    max = tempCelsiusToKelvin(45),
    reference = 'base',
    inclinationAngle = 45
  }) {
    this.options.temperature = {
      min,
      max,
      reference,
      inclinationAngle
    };
    
    this._normalizeTemperatureRange();
  }
  
  /**
   * @internal
   */
  _normalizeTemperatureRange() {
    this.temperatureBottomLeft = this.options.temperature.min;
    this.temperatureBottomRight = this.options.temperature.max;
    this.inclinationTan =
    (this.options.temperature.inclinationAngle == 45) ?
      1 :
      (this.options.temperature.inclinationAngle == 0) ?
        0 :
        Math.tan(this.options.temperature.inclinationAngle * Math.PI/180);
    
    // specific pressure level for temperature range
    if (/^[0-9]+$/.test(this.options.temperature.reference)) {
      let yReference = this.getYByXP(0, this.options.temperature.reference);
      let xTmin = this.inclinationTan * yReference;
      let deltaT =
      (this.temperatureBottomRight - this.temperatureBottomLeft) /
      this.width;
      this.temperatureBottomLeft += deltaT * xTmin;
      this.temperatureBottomRight += deltaT * xTmin;
    }
  }
}
addEventFunctions(CoordinateSystem.prototype);
export default CoordinateSystem;