/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const SPECIALVALUES = new Set(["initial", "inherit", "unset"]);

const { getCSSLexer } = require("resource://devtools/shared/css/lexer.js");

loader.lazyRequireGetter(
  this,
  "CSS_ANGLEUNIT",
  "resource://devtools/shared/css/constants.js",
  true
);

/**
 * This module is used to convert between various angle units.
 *
 * Usage:
 *   let {angleUtils} = require("devtools/client/shared/css-angle");
 *   let angle = new angleUtils.CssAngle("180deg");
 *
 *   angle.authored === "180deg"
 *   angle.valid === true
 *   angle.rad === "3,14rad"
 *   angle.grad === "200grad"
 *   angle.turn === "0.5turn"
 *
 *   angle.toString() === "180deg"; // Outputs the angle value and its unit
 *   // Angle objects can be reused
 *   angle.newAngle("-1TURN") === "-1TURN"; // true
 */

function CssAngle(angleValue) {
  this.newAngle(angleValue);
}

module.exports.angleUtils = {
  CssAngle,
  classifyAngle,
};

CssAngle.prototype = {
  // Still keep trying to lazy load properties-db by lazily getting ANGLEUNIT
  get ANGLEUNIT() {
    return CSS_ANGLEUNIT;
  },

  _angleUnit: null,
  _angleUnitUppercase: false,

  // The value as-authored.
  authored: null,
  // A lower-cased copy of |authored|.
  lowerCased: null,

  get angleUnit() {
    if (this._angleUnit === null) {
      this._angleUnit = classifyAngle(this.authored);
    }
    return this._angleUnit;
  },

  set angleUnit(unit) {
    this._angleUnit = unit;
  },

  get valid() {
    const token = getCSSLexer(this.authored).nextToken();
    if (!token) {
      return false;
    }
    return (
      token.tokenType === "dimension" &&
      token.text.toLowerCase() in this.ANGLEUNIT
    );
  },

  get specialValue() {
    return SPECIALVALUES.has(this.lowerCased) ? this.authored : null;
  },

  get deg() {
    const invalidOrSpecialValue = this._getInvalidOrSpecialValue();
    if (invalidOrSpecialValue !== false) {
      return invalidOrSpecialValue;
    }

    const angleUnit = classifyAngle(this.authored);
    if (angleUnit === this.ANGLEUNIT.deg) {
      // The angle is valid and is in degree.
      return this.authored;
    }

    let degValue;
    if (angleUnit === this.ANGLEUNIT.rad) {
      // The angle is valid and is in radian.
      degValue = this.authoredAngleValue / (Math.PI / 180);
    }

    if (angleUnit === this.ANGLEUNIT.grad) {
      // The angle is valid and is in gradian.
      degValue = this.authoredAngleValue * 0.9;
    }

    if (angleUnit === this.ANGLEUNIT.turn) {
      // The angle is valid and is in turn.
      degValue = this.authoredAngleValue * 360;
    }

    let unitStr = this.ANGLEUNIT.deg;
    if (this._angleUnitUppercase === true) {
      unitStr = unitStr.toUpperCase();
    }
    return `${Math.round(degValue * 100) / 100}${unitStr}`;
  },

  get rad() {
    const invalidOrSpecialValue = this._getInvalidOrSpecialValue();
    if (invalidOrSpecialValue !== false) {
      return invalidOrSpecialValue;
    }

    const unit = classifyAngle(this.authored);
    if (unit === this.ANGLEUNIT.rad) {
      // The angle is valid and is in radian.
      return this.authored;
    }

    let radValue;
    if (unit === this.ANGLEUNIT.deg) {
      // The angle is valid and is in degree.
      radValue = this.authoredAngleValue * (Math.PI / 180);
    }

    if (unit === this.ANGLEUNIT.grad) {
      // The angle is valid and is in gradian.
      radValue = this.authoredAngleValue * 0.9 * (Math.PI / 180);
    }

    if (unit === this.ANGLEUNIT.turn) {
      // The angle is valid and is in turn.
      radValue = this.authoredAngleValue * 360 * (Math.PI / 180);
    }

    let unitStr = this.ANGLEUNIT.rad;
    if (this._angleUnitUppercase === true) {
      unitStr = unitStr.toUpperCase();
    }
    return `${Math.round(radValue * 10000) / 10000}${unitStr}`;
  },

  get grad() {
    const invalidOrSpecialValue = this._getInvalidOrSpecialValue();
    if (invalidOrSpecialValue !== false) {
      return invalidOrSpecialValue;
    }

    const unit = classifyAngle(this.authored);
    if (unit === this.ANGLEUNIT.grad) {
      // The angle is valid and is in gradian
      return this.authored;
    }

    let gradValue;
    if (unit === this.ANGLEUNIT.deg) {
      // The angle is valid and is in degree
      gradValue = this.authoredAngleValue / 0.9;
    }

    if (unit === this.ANGLEUNIT.rad) {
      // The angle is valid and is in radian
      gradValue = this.authoredAngleValue / 0.9 / (Math.PI / 180);
    }

    if (unit === this.ANGLEUNIT.turn) {
      // The angle is valid and is in turn
      gradValue = this.authoredAngleValue * 400;
    }

    let unitStr = this.ANGLEUNIT.grad;
    if (this._angleUnitUppercase === true) {
      unitStr = unitStr.toUpperCase();
    }
    return `${Math.round(gradValue * 100) / 100}${unitStr}`;
  },

  get turn() {
    const invalidOrSpecialValue = this._getInvalidOrSpecialValue();
    if (invalidOrSpecialValue !== false) {
      return invalidOrSpecialValue;
    }

    const unit = classifyAngle(this.authored);
    if (unit === this.ANGLEUNIT.turn) {
      // The angle is valid and is in turn
      return this.authored;
    }

    let turnValue;
    if (unit === this.ANGLEUNIT.deg) {
      // The angle is valid and is in degree
      turnValue = this.authoredAngleValue / 360;
    }

    if (unit === this.ANGLEUNIT.rad) {
      // The angle is valid and is in radian
      turnValue = this.authoredAngleValue / (Math.PI / 180) / 360;
    }

    if (unit === this.ANGLEUNIT.grad) {
      // The angle is valid and is in gradian
      turnValue = this.authoredAngleValue / 400;
    }

    let unitStr = this.ANGLEUNIT.turn;
    if (this._angleUnitUppercase === true) {
      unitStr = unitStr.toUpperCase();
    }
    return `${Math.round(turnValue * 100) / 100}${unitStr}`;
  },

  /**
   * Check whether the angle value is in the special list e.g.
   * inherit or invalid.
   *
   * @return {String|Boolean}
   *         - If the current angle is a special value e.g. "inherit" then
   *           return the angle.
   *         - If the angle is invalid return an empty string.
   *         - If the angle is a regular angle e.g. 90deg so we return false
   *           to indicate that the angle is neither invalid nor special.
   */
  _getInvalidOrSpecialValue() {
    if (this.specialValue) {
      return this.specialValue;
    }
    if (!this.valid) {
      return "";
    }
    return false;
  },

  /**
   * Change angle
   *
   * @param  {String} angle
   *         Any valid angle value + unit string
   */
  newAngle(angle) {
    // Store a lower-cased version of the angle to help with format
    // testing.  The original text is kept as well so it can be
    // returned when needed.
    this.lowerCased = angle.toLowerCase();
    this._angleUnitUppercase = angle === angle.toUpperCase();
    this.authored = angle;

    const reg = new RegExp(`(${Object.keys(this.ANGLEUNIT).join("|")})$`, "i");
    const unitStartIdx = angle.search(reg);
    this.authoredAngleValue = angle.substring(0, unitStartIdx);
    this.authoredAngleUnit = angle.substring(unitStartIdx, angle.length);

    return this;
  },

  nextAngleUnit() {
    // Get a reordered array from the formats object
    // to have the current format at the front so we can cycle through.
    let formats = Object.keys(this.ANGLEUNIT);
    const putOnEnd = formats.splice(0, formats.indexOf(this.angleUnit));
    formats = formats.concat(putOnEnd);
    const currentDisplayedValue = this[formats[0]];

    for (const format of formats) {
      if (this[format].toLowerCase() !== currentDisplayedValue.toLowerCase()) {
        this.angleUnit = this.ANGLEUNIT[format];
        break;
      }
    }
    return this.toString();
  },

  /**
   * Return a string representing a angle
   */
  toString() {
    let angle;

    switch (this.angleUnit) {
      case this.ANGLEUNIT.deg:
        angle = this.deg;
        break;
      case this.ANGLEUNIT.rad:
        angle = this.rad;
        break;
      case this.ANGLEUNIT.grad:
        angle = this.grad;
        break;
      case this.ANGLEUNIT.turn:
        angle = this.turn;
        break;
      default:
        angle = this.deg;
    }

    if (this._angleUnitUppercase && this.angleUnit != this.ANGLEUNIT.authored) {
      angle = angle.toUpperCase();
    }
    return angle;
  },

  /**
   * This method allows comparison of CssAngle objects using ===.
   */
  valueOf() {
    return this.deg;
  },
};

/**
 * Given a color, classify its type as one of the possible angle
 * units, as known by |CssAngle.angleUnit|.
 *
 * @param  {String} value
 *         The angle, in any form accepted by CSS.
 * @return {String}
 *         The angle classification, one of "deg", "rad", "grad", or "turn".
 */
function classifyAngle(value) {
  value = value.toLowerCase();
  if (value.endsWith("deg")) {
    return CSS_ANGLEUNIT.deg;
  }

  if (value.endsWith("grad")) {
    return CSS_ANGLEUNIT.grad;
  }

  if (value.endsWith("rad")) {
    return CSS_ANGLEUNIT.rad;
  }
  if (value.endsWith("turn")) {
    return CSS_ANGLEUNIT.turn;
  }

  return CSS_ANGLEUNIT.deg;
}
