// Copyright (c) 2012, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

/// Representations of CSS styles.

part of '../parser.dart';

// TODO(terry): Prune down this file we do need some of the code in this file
//              for darker, lighter, how to represent a Font, etc but alot of
//              the complexity can be removed.
//              See https://github.com/dart-lang/csslib/issues/7

/// Base for all style properties (e.g., Color, Font, Border, Margin, etc.)
abstract class _StyleProperty {
  /// Returns the expression part of a CSS declaration.  Declaration is:
  ///
  ///     property:expression;
  ///
  /// E.g., if property is color then expression could be rgba(255,255,0) the
  ///       CSS declaration would be 'color:rgba(255,255,0);'.
  ///
  /// then _cssExpression would return 'rgba(255,255,0)'.  See
  /// <http://www.w3.org/TR/CSS21/grammar.html>
  String? get cssExpression;
}

/// Base interface for Color, HSL and RGB.
abstract class ColorBase {
  /// Canonical form for color #rrggbb with alpha blending (0.0 == full
  /// transparency and 1.0 == fully opaque). If _argb length is 6 it's an
  /// rrggbb otherwise it's aarrggbb.
  String toHexArgbString();

  /// Return argb as a value (int).
  int get argbValue;
}

/// General purpose Color class.  Represent a color as an ARGB value that can be
/// converted to and from num, hex string, hsl, hsla, rgb, rgba and SVG pre-
/// defined color constant.
class Color implements _StyleProperty, ColorBase {
  // If _argb length is 6 it's an rrggbb otherwise it's aarrggbb.
  final String _argb;

  // TODO(terry): Look at reducing Rgba and Hsla classes as factories for
  //              converting from Color to an Rgba or Hsla for reading only.
  //              Usefulness of creating an Rgba or Hsla is limited.

  /// Create a color with an integer representing the rgb value of red, green,
  /// and blue.  The value 0xffffff is the color white #ffffff (CSS style).
  /// The [rgb] value of 0xffd700 would map to #ffd700 or the constant
  /// Color.gold, where ff is red intensity, d7 is green intensity, and 00 is
  /// blue intensity.
  Color(int rgb, [num? alpha]) : _argb = Color._rgbToArgbString(rgb, alpha);

  /// RGB takes three values. The [red], [green], and [blue] parameters are
  /// the intensity of those components where '0' is the least and '256' is the
  /// greatest.
  ///
  /// If [alpha] is provided, it is the level of translucency which ranges from
  /// '0' (completely transparent) to '1.0' (completely opaque).  It will
  /// internally be mapped to an int between '0' and '255' like the other color
  /// components.
  Color.createRgba(int red, int green, int blue, [num? alpha])
      : _argb = Color.convertToHexString(
            Color._clamp(red, 0, 255),
            Color._clamp(green, 0, 255),
            Color._clamp(blue, 0, 255),
            alpha != null ? Color._clamp(alpha, 0, 1) : alpha);

  /// Creates a new color from a CSS color string. For more information, see
  /// <https://developer.mozilla.org/en/CSS/color>.
  Color.css(String color) : _argb = Color._convertCssToArgb(color)!;

  // TODO(jmesserly): I found the use of percents a bit suprising.
  /// HSL takes three values.  The [hueDegree] degree on the color wheel; '0' is
  /// the least and '100' is the greatest.  The value '0' or '360' is red, '120'
  /// is green, '240' is blue. Numbers in between reflect different shades.
  /// The [saturationPercent] percentage; where'0' is the least and '100' is the
  /// greatest (100 represents full color).  The [lightnessPercent] percentage;
  /// where'0' is the least and '100' is the greatest.  The value 0 is dark or
  /// black, 100 is light or white and 50 is a medium lightness.
  ///
  /// If [alpha] is provided, it is the level of translucency which ranges from
  /// '0' (completely transparent foreground) to '1.0' (completely opaque
  /// foreground).
  Color.createHsla(num hueDegree, num saturationPercent, num lightnessPercent,
      [num? alpha])
      : _argb = Hsla(
                Color._clamp(hueDegree, 0, 360) / 360,
                Color._clamp(saturationPercent, 0, 100) / 100,
                Color._clamp(lightnessPercent, 0, 100) / 100,
                alpha != null ? Color._clamp(alpha, 0, 1) : alpha)
            .toHexArgbString();

  /// The hslaRaw takes three values.  The [hue] degree on the color wheel; '0'
  /// is the least and '1' is the greatest.  The value '0' or '1' is red, the
  /// ratio of 120/360 is green, and the ratio of 240/360 is blue.  Numbers in
  /// between reflect different shades.  The [saturation] is a percentage; '0'
  /// is the least and '1' is the greatest.  The value of '1' is equivalent to
  /// 100% (full colour).  The [lightness] is a percentage; '0' is the least and
  /// '1' is the greatest.  The value of '0' is dark (black), the value of '1'
  /// is light (white), and the value of '.50' is a medium lightness.
  ///
  /// The fourth optional parameter is:
  ///   [alpha]      level of translucency range of values is 0..1, zero is a
  ///                completely transparent foreground and 1 is a completely
  ///                opaque foreground.
  Color.hslaRaw(num hue, num saturation, num lightness, [num? alpha])
      : _argb = Hsla(
                Color._clamp(hue, 0, 1),
                Color._clamp(saturation, 0, 1),
                Color._clamp(lightness, 0, 1),
                alpha != null ? Color._clamp(alpha, 0, 1) : alpha)
            .toHexArgbString();

  /// Generate a real constant for pre-defined colors (no leading #).
  const Color.hex(this._argb);

  // TODO(jmesserly): this is needed by the example so leave it exposed for now.
  @override
  String toString() => cssExpression;

  // TODO(terry): Regardless of how color is set (rgb, num, css or hsl) we'll
  //              always return a rgb or rgba loses fidelity when debugging in
  //              CSS if user uses hsl and would like to edit as hsl, etc.  If
  //              this is an issue we should keep the original value and not re-
  //              create the CSS from the normalized value.
  @override
  String get cssExpression {
    if (_argb.length == 6) {
      return '#$_argb'; // RGB only, no alpha blending.
    } else {
      num alpha = Color.hexToInt(_argb.substring(0, 2));
      var a = (alpha / 255).toStringAsPrecision(2);
      var r = Color.hexToInt(_argb.substring(2, 4));
      var g = Color.hexToInt(_argb.substring(4, 6));
      var b = Color.hexToInt(_argb.substring(6, 8));
      return 'rgba($r,$g,$b,$a)';
    }
  }

  Rgba get rgba {
    var nextIndex = 0;
    num? a;
    if (_argb.length == 8) {
      // Get alpha blending value 0..255
      var alpha = Color.hexToInt(_argb.substring(nextIndex, nextIndex + 2));
      // Convert to value from 0..1
      a = double.parse((alpha / 255).toStringAsPrecision(2));
      nextIndex += 2;
    }
    var r = Color.hexToInt(_argb.substring(nextIndex, nextIndex + 2));
    nextIndex += 2;
    var g = Color.hexToInt(_argb.substring(nextIndex, nextIndex + 2));
    nextIndex += 2;
    var b = Color.hexToInt(_argb.substring(nextIndex, nextIndex + 2));
    return Rgba(r, g, b, a);
  }

  Hsla get hsla => Hsla.fromRgba(rgba);

  @override
  int get argbValue => Color.hexToInt(_argb);

  @override
  bool operator ==(Object other) => Color.equal(this, other);

  @override
  String toHexArgbString() => _argb;

  Color darker(num amount) {
    var newRgba = Color._createNewTintShadeFromRgba(rgba, -amount);
    return Color.hex(newRgba.toHexArgbString());
  }

  Color lighter(num amount) {
    var newRgba = Color._createNewTintShadeFromRgba(rgba, amount);
    return Color.hex(newRgba.toHexArgbString());
  }

  static bool equal(ColorBase curr, Object other) {
    if (other is Color) {
      var o = other;
      return o.toHexArgbString() == curr.toHexArgbString();
    } else if (other is Rgba) {
      var rgb = other;
      return rgb.toHexArgbString() == curr.toHexArgbString();
    } else if (other is Hsla) {
      var hsla = other;
      return hsla.toHexArgbString() == curr.toHexArgbString();
    } else {
      return false;
    }
  }

  @override
  int get hashCode => _argb.hashCode;

  // Conversion routines:

  static String _rgbToArgbString(int rgba, num? alpha) {
    num? a;
    // If alpha is defined then adjust from 0..1 to 0..255 value, if not set
    // then a is left as undefined and passed to convertToHexString.
    if (alpha != null) {
      a = (Color._clamp(alpha, 0, 1) * 255).round();
    }

    var r = (rgba & 0xff0000) >> 0x10;
    var g = (rgba & 0xff00) >> 8;
    var b = rgba & 0xff;

    return Color.convertToHexString(r, g, b, a);
  }

  static const int _rgbCss = 1;
  static const int _rgbaCss = 2;
  static const int _hslCss = 3;
  static const int _hslaCss = 4;

  /// Parse CSS expressions of the from #rgb, rgb(r,g,b), rgba(r,g,b,a),
  /// hsl(h,s,l), hsla(h,s,l,a) and SVG colors (e.g., darkSlateblue, etc.) and
  /// convert to argb.
  static String? _convertCssToArgb(String value) {
    // TODO(terry): Better parser/regex for converting CSS properties.
    var color = value.trim().replaceAll('\\s', '');
    if (color[0] == '#') {
      var v = color.substring(1);
      Color.hexToInt(v); // Valid hexadecimal, throws if not.
      return v;
    } else if (color.isNotEmpty && color[color.length - 1] == ')') {
      int type;
      if (color.indexOf('rgb(') == 0 || color.indexOf('RGB(') == 0) {
        color = color.substring(4);
        type = _rgbCss;
      } else if (color.indexOf('rgba(') == 0 || color.indexOf('RGBA(') == 0) {
        type = _rgbaCss;
        color = color.substring(5);
      } else if (color.indexOf('hsl(') == 0 || color.indexOf('HSL(') == 0) {
        type = _hslCss;
        color = color.substring(4);
      } else if (color.indexOf('hsla(') == 0 || color.indexOf('HSLA(') == 0) {
        type = _hslaCss;
        color = color.substring(5);
      } else {
        throw UnsupportedError('CSS property not implemented');
      }

      color = color.substring(0, color.length - 1); // Strip close paren.

      var args = <num>[];
      var params = color.split(',');
      for (var param in params) {
        args.add(double.parse(param));
      }
      switch (type) {
        case _rgbCss:
          return Color.convertToHexString(
              args[0].toInt(), args[1].toInt(), args[2].toInt());
        case _rgbaCss:
          return Color.convertToHexString(
              args[0].toInt(), args[1].toInt(), args[2].toInt(), args[3]);
        case _hslCss:
          return Hsla(args[0], args[1], args[2]).toHexArgbString();
        case _hslaCss:
          return Hsla(args[0], args[1], args[2], args[3]).toHexArgbString();
        default:
          // Type not defined UnsupportedOperationException should have thrown.
          assert(false);
          break;
      }
    }
    return null;
  }

  static int hexToInt(String hex) => int.parse(hex, radix: 16);

  static String convertToHexString(int r, int g, int b, [num? a]) {
    var rHex = Color._numAs2DigitHex(Color._clamp(r, 0, 255));
    var gHex = Color._numAs2DigitHex(Color._clamp(g, 0, 255));
    var bHex = Color._numAs2DigitHex(Color._clamp(b, 0, 255));
    var aHex = (a != null)
        ? Color._numAs2DigitHex((Color._clamp(a, 0, 1) * 255).round())
        : '';

    // TODO(terry) 15.toRadixString(16) return 'F' on Dartium not f as in JS.
    //             bug: <http://code.google.com/p/dart/issues/detail?id=2670>
    return '$aHex$rHex$gHex$bHex'.toLowerCase();
  }

  static String _numAs2DigitHex(int v) => v.toRadixString(16).padLeft(2, '0');

  static T _clamp<T extends num>(T value, T min, T max) =>
      math.max(math.min(max, value), min);

  /// Change the tint (make color lighter) or shade (make color darker) of all
  /// parts of [rgba] (r, g and b).  The [amount] is percentage darker between
  /// -1 to 0 for darker and 0 to 1 for lighter; '0' is no change.  The [amount]
  /// will darken or lighten the rgb values; it will not change the alpha value.
  /// If [amount] is outside of the value -1 to +1 then [amount] is changed to
  /// either the min or max direction -1 or 1.
  ///
  /// Darker will approach the color #000000 (black) and lighter will approach
  /// the color #ffffff (white).
  static Rgba _createNewTintShadeFromRgba(Rgba rgba, num amount) {
    int r, g, b;
    var tintShade = Color._clamp(amount, -1, 1);
    if (amount < 0 && rgba.r == 255 && rgba.g == 255 && rgba.b == 255) {
      // TODO(terry): See TODO in _changeTintShadeColor; eliminate this test
      //              by converting to HSL and adjust lightness although this
      //              is fastest lighter/darker algorithm.
      // Darkening white special handling.
      r = Color._clamp((255 + (255 * tintShade)).round().toInt(), 0, 255);
      g = Color._clamp((255 + (255 * tintShade)).round().toInt(), 0, 255);
      b = Color._clamp((255 + (255 * tintShade)).round().toInt(), 0, 255);
    } else {
      // All other colors then darkening white go here.
      r = Color._changeTintShadeColor(rgba.r, tintShade).round().toInt();
      g = Color._changeTintShadeColor(rgba.g, tintShade).round().toInt();
      b = Color._changeTintShadeColor(rgba.b, tintShade).round().toInt();
    }
    return Rgba(r, g, b, rgba.a);
  }

  // TODO(terry): This does an okay lighter/darker; better would be convert to
  //              HSL then change the lightness.
  /// The parameter [v] is the color to change (r, g, or b) in the range '0' to
  /// '255'. The parameter [delta] is a number between '-1' and '1'.  A value
  /// between '-1' and '0' is darker and a value between '0' and '1' is lighter
  /// ('0' imples no change).
  static num _changeTintShadeColor(num v, num delta) =>
      Color._clamp(((1 - delta) * v + (delta * 255)).round(), 0, 255);

  // Predefined CSS colors see <http://www.w3.org/TR/css3-color/>
  static final Color transparent = const Color.hex('00ffffff'); // Alpha 0.0
  static final Color aliceBlue = const Color.hex('0f08ff');
  static final Color antiqueWhite = const Color.hex('0faebd7');
  static final Color aqua = const Color.hex('00ffff');
  static final Color aquaMarine = const Color.hex('7fffd4');
  static final Color azure = const Color.hex('f0ffff');
  static final Color beige = const Color.hex('f5f5dc');
  static final Color bisque = const Color.hex('ffe4c4');
  static final Color black = const Color.hex('000000');
  static final Color blanchedAlmond = const Color.hex('ffebcd');
  static final Color blue = const Color.hex('0000ff');
  static final Color blueViolet = const Color.hex('8a2be2');
  static final Color brown = const Color.hex('a52a2a');
  static final Color burlyWood = const Color.hex('deb887');
  static final Color cadetBlue = const Color.hex('5f9ea0');
  static final Color chartreuse = const Color.hex('7fff00');
  static final Color chocolate = const Color.hex('d2691e');
  static final Color coral = const Color.hex('ff7f50');
  static final Color cornFlowerBlue = const Color.hex('6495ed');
  static final Color cornSilk = const Color.hex('fff8dc');
  static final Color crimson = const Color.hex('dc143c');
  static final Color cyan = const Color.hex('00ffff');
  static final Color darkBlue = const Color.hex('00008b');
  static final Color darkCyan = const Color.hex('008b8b');
  static final Color darkGoldenRod = const Color.hex('b8860b');
  static final Color darkGray = const Color.hex('a9a9a9');
  static final Color darkGreen = const Color.hex('006400');
  static final Color darkGrey = const Color.hex('a9a9a9');
  static final Color darkKhaki = const Color.hex('bdb76b');
  static final Color darkMagenta = const Color.hex('8b008b');
  static final Color darkOliveGreen = const Color.hex('556b2f');
  static final Color darkOrange = const Color.hex('ff8c00');
  static final Color darkOrchid = const Color.hex('9932cc');
  static final Color darkRed = const Color.hex('8b0000');
  static final Color darkSalmon = const Color.hex('e9967a');
  static final Color darkSeaGreen = const Color.hex('8fbc8f');
  static final Color darkSlateBlue = const Color.hex('483d8b');
  static final Color darkSlateGray = const Color.hex('2f4f4f');
  static final Color darkSlateGrey = const Color.hex('2f4f4f');
  static final Color darkTurquoise = const Color.hex('00ced1');
  static final Color darkViolet = const Color.hex('9400d3');
  static final Color deepPink = const Color.hex('ff1493');
  static final Color deepSkyBlue = const Color.hex('00bfff');
  static final Color dimGray = const Color.hex('696969');
  static final Color dimGrey = const Color.hex('696969');
  static final Color dodgerBlue = const Color.hex('1e90ff');
  static final Color fireBrick = const Color.hex('b22222');
  static final Color floralWhite = const Color.hex('fffaf0');
  static final Color forestGreen = const Color.hex('228b22');
  static final Color fuchsia = const Color.hex('ff00ff');
  static final Color gainsboro = const Color.hex('dcdcdc');
  static final Color ghostWhite = const Color.hex('f8f8ff');
  static final Color gold = const Color.hex('ffd700');
  static final Color goldenRod = const Color.hex('daa520');
  static final Color gray = const Color.hex('808080');
  static final Color green = const Color.hex('008000');
  static final Color greenYellow = const Color.hex('adff2f');
  static final Color grey = const Color.hex('808080');
  static final Color honeydew = const Color.hex('f0fff0');
  static final Color hotPink = const Color.hex('ff69b4');
  static final Color indianRed = const Color.hex('cd5c5c');
  static final Color indigo = const Color.hex('4b0082');
  static final Color ivory = const Color.hex('fffff0');
  static final Color khaki = const Color.hex('f0e68c');
  static final Color lavender = const Color.hex('e6e6fa');
  static final Color lavenderBlush = const Color.hex('fff0f5');
  static final Color lawnGreen = const Color.hex('7cfc00');
  static final Color lemonChiffon = const Color.hex('fffacd');
  static final Color lightBlue = const Color.hex('add8e6');
  static final Color lightCoral = const Color.hex('f08080');
  static final Color lightCyan = const Color.hex('e0ffff');
  static final Color lightGoldenRodYellow = const Color.hex('fafad2');
  static final Color lightGray = const Color.hex('d3d3d3');
  static final Color lightGreen = const Color.hex('90ee90');
  static final Color lightGrey = const Color.hex('d3d3d3');
  static final Color lightPink = const Color.hex('ffb6c1');
  static final Color lightSalmon = const Color.hex('ffa07a');
  static final Color lightSeaGreen = const Color.hex('20b2aa');
  static final Color lightSkyBlue = const Color.hex('87cefa');
  static final Color lightSlateGray = const Color.hex('778899');
  static final Color lightSlateGrey = const Color.hex('778899');
  static final Color lightSteelBlue = const Color.hex('b0c4de');
  static final Color lightYellow = const Color.hex('ffffe0');
  static final Color lime = const Color.hex('00ff00');
  static final Color limeGreen = const Color.hex('32cd32');
  static final Color linen = const Color.hex('faf0e6');
  static final Color magenta = const Color.hex('ff00ff');
  static final Color maroon = const Color.hex('800000');
  static final Color mediumAquaMarine = const Color.hex('66cdaa');
  static final Color mediumBlue = const Color.hex('0000cd');
  static final Color mediumOrchid = const Color.hex('ba55d3');
  static final Color mediumPurple = const Color.hex('9370db');
  static final Color mediumSeaGreen = const Color.hex('3cb371');
  static final Color mediumSlateBlue = const Color.hex('7b68ee');
  static final Color mediumSpringGreen = const Color.hex('00fa9a');
  static final Color mediumTurquoise = const Color.hex('48d1cc');
  static final Color mediumVioletRed = const Color.hex('c71585');
  static final Color midnightBlue = const Color.hex('191970');
  static final Color mintCream = const Color.hex('f5fffa');
  static final Color mistyRose = const Color.hex('ffe4e1');
  static final Color moccasin = const Color.hex('ffe4b5');
  static final Color navajoWhite = const Color.hex('ffdead');
  static final Color navy = const Color.hex('000080');
  static final Color oldLace = const Color.hex('fdf5e6');
  static final Color olive = const Color.hex('808000');
  static final Color oliveDrab = const Color.hex('6b8e23');
  static final Color orange = const Color.hex('ffa500');
  static final Color orangeRed = const Color.hex('ff4500');
  static final Color orchid = const Color.hex('da70d6');
  static final Color paleGoldenRod = const Color.hex('eee8aa');
  static final Color paleGreen = const Color.hex('98fb98');
  static final Color paleTurquoise = const Color.hex('afeeee');
  static final Color paleVioletRed = const Color.hex('db7093');
  static final Color papayaWhip = const Color.hex('ffefd5');
  static final Color peachPuff = const Color.hex('ffdab9');
  static final Color peru = const Color.hex('cd85ef');
  static final Color pink = const Color.hex('ffc0cb');
  static final Color plum = const Color.hex('dda0dd');
  static final Color powderBlue = const Color.hex('b0e0e6');
  static final Color purple = const Color.hex('800080');
  static final Color red = const Color.hex('ff0000');
  static final Color rosyBrown = const Color.hex('bc8f8f');
  static final Color royalBlue = const Color.hex('4169e1');
  static final Color saddleBrown = const Color.hex('8b4513');
  static final Color salmon = const Color.hex('fa8072');
  static final Color sandyBrown = const Color.hex('f4a460');
  static final Color seaGreen = const Color.hex('2e8b57');
  static final Color seashell = const Color.hex('fff5ee');
  static final Color sienna = const Color.hex('a0522d');
  static final Color silver = const Color.hex('c0c0c0');
  static final Color skyBlue = const Color.hex('87ceeb');
  static final Color slateBlue = const Color.hex('6a5acd');
  static final Color slateGray = const Color.hex('708090');
  static final Color slateGrey = const Color.hex('708090');
  static final Color snow = const Color.hex('fffafa');
  static final Color springGreen = const Color.hex('00ff7f');
  static final Color steelBlue = const Color.hex('4682b4');
  static final Color tan = const Color.hex('d2b48c');
  static final Color teal = const Color.hex('008080');
  static final Color thistle = const Color.hex('d8bfd8');
  static final Color tomato = const Color.hex('ff6347');
  static final Color turquoise = const Color.hex('40e0d0');
  static final Color violet = const Color.hex('ee82ee');
  static final Color wheat = const Color.hex('f5deb3');
  static final Color white = const Color.hex('ffffff');
  static final Color whiteSmoke = const Color.hex('f5f5f5');
  static final Color yellow = const Color.hex('ffff00');
  static final Color yellowGreen = const Color.hex('9acd32');
}

/// Rgba class for users that want to interact with a color as a RGBA value.
class Rgba implements _StyleProperty, ColorBase {
  // TODO(terry): Consider consolidating rgba to a single 32-bit int, make sure
  //              it works under JS and Dart VM.
  final int r;
  final int g;
  final int b;
  final num? a;

  Rgba(int red, int green, int blue, [num? alpha])
      : r = Color._clamp(red, 0, 255),
        g = Color._clamp(green, 0, 255),
        b = Color._clamp(blue, 0, 255),
        a = (alpha != null) ? Color._clamp(alpha, 0, 1) : alpha;

  factory Rgba.fromString(String hexValue) =>
      Color.css('#${Color._convertCssToArgb(hexValue)}').rgba;

  factory Rgba.fromColor(Color color) => color.rgba;

  factory Rgba.fromArgbValue(num value) {
    return Rgba(
      (value.toInt() & 0xff000000) >> 0x18, // a
      (value.toInt() & 0xff0000) >> 0x10, // r
      (value.toInt() & 0xff00) >> 8, // g
      value.toInt() & 0xff,
    ); // b
  }

  factory Rgba.fromHsla(Hsla hsla) {
    // Convert to Rgba.
    // See site <http://easyrgb.com/index.php?X=MATH> for good documentation
    // and color conversion routines.

    var h = hsla.hue;
    var s = hsla.saturation;
    var l = hsla.lightness;
    var a = hsla.alpha;

    int r;
    int g;
    int b;

    if (s == 0) {
      r = (l * 255).round().toInt();
      g = r;
      b = r;
    } else {
      num var2;

      if (l < 0.5) {
        var2 = l * (1 + s);
      } else {
        var2 = (l + s) - (s * l);
      }
      var var1 = 2 * l - var2;

      r = (255 * Rgba._hueToRGB(var1, var2, h + (1 / 3))).round().toInt();
      g = (255 * Rgba._hueToRGB(var1, var2, h)).round().toInt();
      b = (255 * Rgba._hueToRGB(var1, var2, h - (1 / 3))).round().toInt();
    }

    return Rgba(r, g, b, a);
  }

  static num _hueToRGB(num v1, num v2, num vH) {
    if (vH < 0) {
      vH += 1;
    }

    if (vH > 1) {
      vH -= 1;
    }

    if ((6 * vH) < 1) {
      return v1 + (v2 - v1) * 6 * vH;
    }

    if ((2 * vH) < 1) {
      return v2;
    }

    if ((3 * vH) < 2) {
      return v1 + (v2 - v1) * ((2 / 3 - vH) * 6);
    }

    return v1;
  }

  @override
  bool operator ==(Object other) => Color.equal(this, other);

  @override
  String get cssExpression {
    if (a == null) {
      return '#${Color.convertToHexString(r, g, b)}';
    } else {
      return 'rgba($r,$g,$b,$a)';
    }
  }

  @override
  String toHexArgbString() => Color.convertToHexString(r, g, b, a);

  @override
  int get argbValue {
    var value = 0;
    if (a != null) {
      value = a!.toInt() << 0x18;
    }
    value += r << 0x10;
    value += g << 0x08;
    value += b;
    return value;
  }

  Color get color => Color.createRgba(r, g, b, a);
  Hsla get hsla => Hsla.fromRgba(this);

  Rgba darker(num amount) => Color._createNewTintShadeFromRgba(this, -amount);
  Rgba lighter(num amount) => Color._createNewTintShadeFromRgba(this, amount);

  @override
  int get hashCode => toHexArgbString().hashCode;
}

/// Hsl class support to interact with a color as a hsl with hue, saturation,
/// and lightness with optional alpha blending.  The hue is a ratio of 360
/// degrees 360° = 1 or 0, (1° == (1/360)), saturation and lightness is a 0..1
/// fraction (1 == 100%) and alpha is a 0..1 fraction.
class Hsla implements _StyleProperty, ColorBase {
  final num _h; // Value from 0..1
  final num _s; // Value from 0..1
  final num _l; // Value from 0..1
  final num? _a; // Value from 0..1

  /// [hue] is a 0..1 fraction of 360 degrees (360 == 0).
  /// [saturation] is a 0..1 fraction (100% == 1).
  /// [lightness] is a 0..1 fraction (100% == 1).
  /// [alpha] is a 0..1 fraction, alpha blending between 0..1, 1 == 100% opaque.
  Hsla(num hue, num saturation, num lightness, [num? alpha])
      : _h = (hue == 1) ? 0 : Color._clamp(hue, 0, 1),
        _s = Color._clamp(saturation, 0, 1),
        _l = Color._clamp(lightness, 0, 1),
        _a = (alpha != null) ? Color._clamp(alpha, 0, 1) : alpha;

  factory Hsla.fromString(String hexValue) {
    var rgba = Color.css('#${Color._convertCssToArgb(hexValue)}').rgba;
    return _createFromRgba(rgba.r, rgba.g, rgba.b, rgba.a);
  }

  factory Hsla.fromColor(Color color) {
    var rgba = color.rgba;
    return _createFromRgba(rgba.r, rgba.g, rgba.b, rgba.a);
  }

  factory Hsla.fromArgbValue(num value) {
    num a = (value.toInt() & 0xff000000) >> 0x18;
    var r = (value.toInt() & 0xff0000) >> 0x10;
    var g = (value.toInt() & 0xff00) >> 8;
    var b = value.toInt() & 0xff;

    // Convert alpha to 0..1 from (0..255).
    a = double.parse((a / 255).toStringAsPrecision(2));

    return _createFromRgba(r, g, b, a);
  }

  factory Hsla.fromRgba(Rgba rgba) =>
      _createFromRgba(rgba.r, rgba.g, rgba.b, rgba.a);

  static Hsla _createFromRgba(num r, num g, num b, num? a) {
    // Convert RGB to hsl.
    // See site <http://easyrgb.com/index.php?X=MATH> for good documentation
    // and color conversion routines.
    r /= 255;
    g /= 255;
    b /= 255;

    // Hue, saturation and lightness.
    num h;
    num s;
    num l;

    var minRgb = math.min(r, math.min(g, b));
    var maxRgb = math.max(r, math.max(g, b));
    l = (maxRgb + minRgb) / 2;
    if (l <= 0) {
      return Hsla(0, 0, l); // Black;
    }

    var vm = maxRgb - minRgb;
    s = vm;
    if (s > 0) {
      s /= (l < 0.5) ? (maxRgb + minRgb) : (2 - maxRgb - minRgb);
    } else {
      return Hsla(0, 0, l); // White
    }

    num r2, g2, b2;
    r2 = (maxRgb - r) / vm;
    g2 = (maxRgb - g) / vm;
    b2 = (maxRgb - b) / vm;
    if (r == maxRgb) {
      h = (g == minRgb) ? 5.0 + b2 : 1 - g2;
    } else if (g == maxRgb) {
      h = (b == minRgb) ? 1 + r2 : 3 - b2;
    } else {
      h = (r == minRgb) ? 3 + g2 : 5 - r2;
    }
    h /= 6;

    return Hsla(h, s, l, a);
  }

  /// Returns 0..1 fraction (ratio of 360°, e.g. 1° == 1/360).
  num get hue => _h;

  /// Returns 0..1 fraction (1 == 100%)
  num get saturation => _s;

  /// Returns 0..1 fraction (1 == 100%).
  num get lightness => _l;

  /// Returns number as degrees 0..360.
  num get hueDegrees => (_h * 360).round();

  /// Returns number as percentage 0..100
  num get saturationPercentage => (_s * 100).round();

  /// Returns number as percentage 0..100.
  num get lightnessPercentage => (_l * 100).round();

  /// Returns number as 0..1
  num? get alpha => _a;

  @override
  bool operator ==(Object other) => Color.equal(this, other);

  @override
  String get cssExpression => (_a == null)
      ? 'hsl($hueDegrees,$saturationPercentage,$lightnessPercentage)'
      : 'hsla($hueDegrees,$saturationPercentage,$lightnessPercentage,$_a)';

  @override
  String toHexArgbString() => Rgba.fromHsla(this).toHexArgbString();

  @override
  int get argbValue => Color.hexToInt(toHexArgbString());

  Color get color => Color.createHsla(_h, _s, _l, _a);
  Rgba get rgba => Rgba.fromHsla(this);

  Hsla darker(num amount) => Hsla.fromRgba(Rgba.fromHsla(this).darker(amount));

  Hsla lighter(num amount) =>
      Hsla.fromRgba(Rgba.fromHsla(this).lighter(amount));

  @override
  int get hashCode => toHexArgbString().hashCode;
}

/// X,Y position.
class PointXY implements _StyleProperty {
  final num x, y;
  const PointXY(this.x, this.y);

  @override
  String? get cssExpression {
    // TODO(terry): TBD
    return null;
  }
}

// TODO(terry): Implement style and color.
/// Supports border for measuring with layout.
class Border implements _StyleProperty {
  final int? top, left, bottom, right;

  // TODO(terry): Just like CSS, 1-arg -> set all properties, 2-args -> top and
  //               bottom are first arg, left and right are second, 3-args, and
  //               4-args -> tlbr or trbl.
  const Border([this.top, this.left, this.bottom, this.right]);

  // TODO(terry): Consider using Size or width and height.
  Border.uniform(int amount)
      : top = amount,
        left = amount,
        bottom = amount,
        right = amount;

  int get width => left! + right!;
  int get height => top! + bottom!;

  @override
  String get cssExpression {
    return (top == left && bottom == right && top == right)
        ? '${left}px'
        : "${top != null ? '$top' : '0'}px "
            "${right != null ? '$right' : '0'}px "
            "${bottom != null ? '$bottom' : '0'}px "
            "${left != null ? '$left' : '0'}px";
  }
}

/// Font style constants.
class FontStyle {
  /// Font style [normal] default.
  static const String normal = 'normal';

  /// Font style [italic] use explicity crafted italic font otherwise inclined
  /// on the fly like oblique.
  static const String italic = 'italic';

  /// Font style [oblique] is rarely used. The normal style of a font is
  /// inclined on the fly to the right by 8-12 degrees.
  static const String oblique = 'oblique';
}

/// Font variant constants.
class FontVariant {
  /// Font style [normal] default.
  static const String normal = 'normal';

  /// Font variant [smallCaps].
  static const String smallCaps = 'small-caps';
}

/// Font weight constants values 100, 200, 300, 400, 500, 600, 700, 800, 900.
class FontWeight {
  /// Font weight normal [default]
  static const int normal = 400;

  /// Font weight bold
  static const int bold = 700;

  static const int wt100 = 100;
  static const int wt200 = 200;
  static const int wt300 = 300;
  static const int wt400 = 400;
  static const int wt500 = 500;
  static const int wt600 = 600;
  static const int wt700 = 700;
  static const int wt800 = 800;
  static const int wt900 = 900;
}

/// Generic font family names.
class FontGeneric {
  /// Generic family sans-serif font (w/o serifs).
  static const String sansSerif = 'sans-serif';

  /// Generic family serif font.
  static const String serif = 'serif';

  /// Generic family fixed-width font.
  static const monospace = 'monospace';

  /// Generic family emulate handwriting font.
  static const String cursive = 'cursive';

  /// Generic family decorative font.
  static const String fantasy = 'fantasy';
}

/// List of most common font families across different platforms.  Use the
/// collection names in the Font class (e.g., Font.SANS_SERIF, Font.FONT_SERIF,
/// Font.MONOSPACE, Font.CURSIVE or Font.FANTASY).  These work best on all
/// platforms using the fonts that best match availability on each platform.
/// See <http://www.angelfire.com/al4/rcollins/style/fonts.html> for a good
/// description of fonts available between platforms and browsers.
class FontFamily {
  /// Sans-Serif font for Windows similar to Helvetica on Mac bold/italic.
  static const String arial = 'arial';

  /// Sans-Serif font for Windows less common already bolded.
  static const String arialBlack = 'arial black';

  /// Sans-Serif font for Mac since 1984, similar to Arial/Helvetica.
  static const String geneva = 'geneva';

  /// Sans-Serif font for Windows most readable sans-serif font for displays.
  static const String verdana = 'verdana';

  /// Sans-Serif font for Mac since 1984 is identical to Arial.
  static const String helvetica = 'helvetica';

  /// Serif font for Windows traditional font with “old-style” numerals.
  static const String georgia = 'georgia';

  /// Serif font for Mac. PCs may have the non-scalable Times use Times New
  /// Roman instead.  Times is more compact than Times New Roman.
  static const String times = 'times';

  /// Serif font for Windows most common serif font and default serif font for
  /// most browsers.
  static const String timesNewRoman = 'times new roman';

  /// Monospace font for Mac/Windows most common. Scalable on Mac not scalable
  /// on Windows.
  static const String courier = 'courier';

  /// Monospace font for Mac/Windows scalable on both platforms.
  static const String courierNew = 'courier new';

  /// Cursive font for Windows and default cursive font for IE.
  static const String comicSansMs = 'comic sans ms';

  /// Cursive font for Mac on Macs 2000 and newer.
  static const String textile = 'textile';

  /// Cursive font for older Macs.
  static const String appleChancery = 'apple chancery';

  /// Cursive font for some PCs.
  static const String zaphChancery = 'zaph chancery';

  /// Fantasy font on most Mac/Windows/Linux platforms.
  static const String impact = 'impact';

  /// Fantasy font for Windows.
  static const String webdings = 'webdings';
}

class LineHeight {
  final num height;
  final bool inPixels;
  const LineHeight(this.height, {this.inPixels = true});
}

// TODO(terry): Support @font-face fule.
/// Font style support for size, family, weight, style, variant, and lineheight.
class Font implements _StyleProperty {
  /// Collection of most common sans-serif fonts in order.
  static const List<String> sansSerif = [
    FontFamily.arial,
    FontFamily.verdana,
    FontFamily.geneva,
    FontFamily.helvetica,
    FontGeneric.sansSerif
  ];

  /// Collection of most common serif fonts in order.
  static const List<String> serif = [
    FontFamily.georgia,
    FontFamily.timesNewRoman,
    FontFamily.times,
    FontGeneric.serif
  ];

  /// Collection of most common monospace fonts in order.
  static const List<String> monospace = [
    FontFamily.courierNew,
    FontFamily.courier,
    FontGeneric.monospace
  ];

  /// Collection of most common cursive fonts in order.
  static const List<String> cursive = [
    FontFamily.textile,
    FontFamily.appleChancery,
    FontFamily.zaphChancery,
    FontGeneric.fantasy
  ];

  /// Collection of most common fantasy fonts in order.
  static const List<String> fantasy = [
    FontFamily.comicSansMs,
    FontFamily.impact,
    FontFamily.webdings,
    FontGeneric.fantasy
  ];

  // TODO(terry): Should support the values xx-small, small, large, xx-large,
  //              etc. (mapped to a pixel sized font)?
  /// Font size in pixels.
  final num? size;

  // TODO(terry): _family should be an immutable list, wrapper class to do this
  //              should exist in Dart.
  /// Family specifies a list of fonts, the browser will sequentially select the
  /// the first known/supported font.  There are two types of font families the
  /// family-name (e.g., arial, times, courier, etc) or the generic-family
  /// (e.g., serif, sans-seric, etc.)
  final List<String>? family;

  /// Font weight from 100, 200, 300, 400, 500, 600, 700, 800, 900
  final int? weight;

  /// Style of a font normal, italic, oblique.
  final String? style;

  /// Font variant NORMAL (default) or SMALL_CAPS.  Different set of font glyph
  /// lower case letters designed to have to fit within the font-height and
  /// weight of the corresponding lowercase letters.
  final String? variant;

  final LineHeight? lineHeight;

  // TODO(terry): Size and computedLineHeight are in pixels.  Need to figure out
  //              how to handle in other units (specified in other units) like
  //              points, inches, etc.  Do we have helpers like Units.Points(12)
  //              where 12 is in points and that's converted to pixels?
  // TODO(terry): lineHeight is computed as 1.2 although CSS_RESET is 1.0 we
  //              need to be consistent some browsers use 1 others 1.2.
  // TODO(terry): There is a school of thought "Golden Ratio Typography".
  // Where width to display the text is also important in computing the line
  // height.  Classic typography suggest the ratio be 1.5.  See
  // <http://www.pearsonified.com/2011/12/golden-ratio-typography.php> and
  // <http://meyerweb.com/eric/thoughts/2008/05/06/line-height-abnormal/>.
  /// Create a font using [size] of font in pixels, [family] name of font(s)
  /// using [FontFamily], [style] of the font using [FontStyle], [variant] using
  /// [FontVariant], and [lineHeight] extra space (leading) around the font in
  /// pixels, if not specified it's 1.2 the font size.
  const Font(
      {this.size,
      this.family,
      this.weight,
      this.style,
      this.variant,
      this.lineHeight});

  /// Merge the two fonts and return the result. See [Style.merge] for
  /// more information.
  static Font? merge(Font? a, Font? b) {
    if (a == null) return b;
    if (b == null) return a;
    return Font._merge(a, b);
  }

  Font._merge(Font a, Font b)
      : size = _mergeVal(a.size, b.size),
        family = _mergeVal(a.family, b.family),
        weight = _mergeVal(a.weight, b.weight),
        style = _mergeVal(a.style, b.style),
        variant = _mergeVal(a.variant, b.variant),
        lineHeight = _mergeVal(a.lineHeight, b.lineHeight);

  /// Shorthand CSS format for font is:
  ///
  ///    font-style font-variant font-weight font-size/line-height font-family
  ///
  /// The font-size and font-family values are required. If any of the other
  /// values are missing the default value is used.
  @override
  String get cssExpression {
    // TODO(jimhug): include variant, style, other options
    if (weight != null) {
      // TODO(jacobr): is this really correct for lineHeight?
      if (lineHeight != null) {
        return '$weight ${size}px/$lineHeightInPixels $_fontsAsString';
      }
      return '$weight ${size}px $_fontsAsString';
    }

    return '${size}px $_fontsAsString';
  }

  Font scale(num ratio) => Font(
      size: size! * ratio,
      family: family,
      weight: weight,
      style: style,
      variant: variant);

  /// The lineHeight, provides an indirect means to specify the leading. The
  /// leading is the difference between the font-size height and the (used)
  /// value of line height in pixels.  If lineHeight is not specified it's
  /// automatically computed as 1.2 of the font size.  Firefox is 1.2, Safari is
  /// ~1.2, and CSS suggest a ration from 1 to 1.2 of the font-size when
  /// computing line-height. The Font class constructor has the computation for
  /// _lineHeight.
  num? get lineHeightInPixels {
    if (lineHeight != null) {
      if (lineHeight!.inPixels) {
        return lineHeight!.height;
      } else {
        return (size != null) ? lineHeight!.height * size! : null;
      }
    } else {
      return (size != null) ? size! * 1.2 : null;
    }
  }

  @override
  int get hashCode {
    // TODO(jimhug): Lot's of potential collisions here. List of fonts, etc.
    return size!.toInt() % family![0].hashCode;
  }

  @override
  bool operator ==(Object other) {
    if (other is! Font) return false;
    return other.size == size &&
        other.family == family &&
        other.weight == weight &&
        other.lineHeight == lineHeight &&
        other.style == style &&
        other.variant == variant;
  }

  // TODO(terry): This is fragile should probably just iterate through the list
  //              of fonts construction the font-family string.
  /// Return fonts as a comma seperated list sans the square brackets.
  String get _fontsAsString {
    var fonts = family.toString();
    return fonts.length > 2 ? fonts.substring(1, fonts.length - 1) : '';
  }
}

/// This class stores the sizes of the box edges in the CSS [box model][]. Each
/// edge area is placed around the sides of the content box. The innermost area
/// is the [Style.padding] area which has a background and surrounds the
/// content.  The content and padding area is surrounded by the [Style.border],
/// which itself is surrounded by the transparent [Style.margin]. This box
/// represents the eges of padding, border, or margin depending on which
/// accessor was used to retrieve it.
///
/// [box model]: https://developer.mozilla.org/en/CSS/box_model
class BoxEdge {
  /// The size of the left edge, or null if the style has no edge.
  final num? left;

  /// The size of the top edge, or null if the style has no edge.
  final num? top;

  /// The size of the right edge, or null if the style has no edge.
  final num? right;

  /// The size of the bottom edge, or null if the style has no edge.
  final num? bottom;

  /// Creates a box edge with the specified [left], [top], [right], and
  /// [bottom] width.
  const BoxEdge([this.left, this.top, this.right, this.bottom]);

  /// Creates a box edge with the specified [top], [right], [bottom], and
  /// [left] width. This matches the typical CSS order:
  /// <https://developer.mozilla.org/en/CSS/margin>
  /// <https://developer.mozilla.org/en/CSS/border-width>
  /// <https://developer.mozilla.org/en/CSS/padding>.
  const BoxEdge.clockwiseFromTop(this.top, this.right, this.bottom, this.left);

  /// This is a helper to creates a box edge with the same [left], [top]
  /// [right], and [bottom] widths.
  const BoxEdge.uniform(num size)
      : top = size,
        left = size,
        bottom = size,
        right = size;

  /// Takes a possibly null box edge, with possibly null metrics, and fills
  /// them in with 0 instead.
  factory BoxEdge.nonNull(BoxEdge? other) {
    if (other == null) return const BoxEdge(0, 0, 0, 0);
    var left = other.left;
    var top = other.top;
    var right = other.right;
    var bottom = other.bottom;
    var make = false;
    if (left == null) {
      make = true;
      left = 0;
    }
    if (top == null) {
      make = true;
      top = 0;
    }
    if (right == null) {
      make = true;
      right = 0;
    }
    if (bottom == null) {
      make = true;
      bottom = 0;
    }
    return make ? BoxEdge(left, top, right, bottom) : other;
  }

  /// Merge the two box edge sizes and return the result. See [Style.merge] for
  /// more information.
  static BoxEdge? merge(BoxEdge? x, BoxEdge? y) {
    if (x == null) return y;
    if (y == null) return x;
    return BoxEdge._merge(x, y);
  }

  BoxEdge._merge(BoxEdge x, BoxEdge y)
      : left = _mergeVal(x.left, y.left),
        top = _mergeVal(x.top, y.top),
        right = _mergeVal(x.right, y.right),
        bottom = _mergeVal(x.bottom, y.bottom);

  /// The total size of the horizontal edges. Equal to [left] + [right], where
  /// null is interpreted as 0px.
  num get width => (left ?? 0) + (right ?? 0);

  /// The total size of the vertical edges. Equal to [top] + [bottom], where
  /// null is interpreted as 0px.
  num get height => (top ?? 0) + (bottom ?? 0);
}

T _mergeVal<T>(T x, T y) => y ?? x;
