//
// Copyright (c) 2008, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   12 Jun 2008  Brian Frank  Creation
//   10 Apr 2017  Brian Frank  Refactor to model CSS color
//

**
** Models an CSS4 RGB color with alpha
**
@Js
@Serializable { simple = true }
const final class Color : Paint
{

//////////////////////////////////////////////////////////////////////////
// Construction
//////////////////////////////////////////////////////////////////////////

  ** Transparent constant with opacity set to zero
  const static Color transparent := make(0, 0f)

  ** Black is #000
  const static Color black := make(0, 1.0f)

  ** White is #FFF
  const static Color white := make(0xFFFFFF, 1.0f)

  ** Make a new instance with the RGB components masked
  ** together: bits 16-23 red; bits 8-15 green; bits 0-7 blue.
  ** Alpha should be a float between 1.0 and 0.0.
  new make(Int rgb := 0, Float a := 1.0f)
  {
    this.rgb = rgb.and(0xff_ff_ff)
    this.a = a.max(0f).min(1.0f)
  }

  ** Make a new instance with the RGB individual
  ** components as integers between 0 and 255 and alpha
  ** as float between 1.0 and 0.0.
  static Color makeRgb(Int r, Int g, Int b, Float a := 1.0f)
  {
    return make((r.and(0xff).shiftl(16))
             .or(g.and(0xff).shiftl(8))
             .or(b.and(0xff)), a)
  }

  ** Construct a color using HSL model (hue, saturation, lightness):
  **   - hue as 0.0 to 360.0
  **   - saturation as 0.0 to 1.0
  **   - lightness (or brightness) as 0.0 to 1.0
  **   - alpha as 0.0 to 1.0
  ** Also see `h`, `s`, `l`.
  static Color makeHsl(Float h, Float s, Float l, Float a := 1.0f)
  {
    c := (1f - (2f * l - 1f).abs) * s
    x := c * (1f - ((h / 60f) % 2f - 1f).abs)
    m := l - c / 2f
    r := 0f
    g := 0f
    b := 0f
         if (h < 60f)               { r=c;  g=x;  b=0f }
    else if (h >= 60f  && h < 120f) { r=x;  g=c;  b=0f }
    else if (h >= 120f && h < 180f) { r=0f; g=c;  b=x  }
    else if (h >= 180f && h < 240f) { r=0f; g=x;  b=c  }
    else if (h >= 240f && h < 300f) { r=x;  g=0f; b=c  }
    else if (h >= 300f && h < 360f) { r=c;  g=0f; b=x  }
    return make(((r+m) * 255f).round.toInt.shiftl(16)
                .or(((g+m) * 255f).round.toInt.shiftl(8))
                .or(((b+m) * 255f).round.toInt), a)
  }

//////////////////////////////////////////////////////////////////////////
// Parsing
//////////////////////////////////////////////////////////////////////////

  ** Parse color from CSS 4 string.  If invalid
  ** and checked is true then throw ParseErr otherwise
  ** return null.  The following formats are supported:
  **   - CSS keyword color
  **   - #RRGGBB
  **   - #RRGGBBAA
  **   - #RGB
  **   - #RGBA
  **   - rgb(r, g b)
  **   - rgba(r, g, b, a)
  **   - hsl(h, s, l)
  **   - hsla(h, s, l, a)
  **
  ** Functional notation works with comma or space separated
  ** arguments.
  **
  ** Examples:
  **   Color.fromStr("red")
  **   Color.fromStr("#8A0")
  **   Color.fromStr("#88AA00")
  **   Color.fromStr("rgba(255, 0, 0, 0.3)")
  **   Color.fromStr("rgb(100% 0% 0% 25%)")
  static new fromStr(Str s, Bool checked := true)
  {
    try
    {
      // #xxx syntax
      if (s.startsWith("#")) return parseHex(s)

      // keyword
      k := byKeyword[s]
      if (k != null) return k

      // try functional notation
      paren := s.index("(")
      if (paren != null)
      {
        if (s[-1] != ')') throw Err()
        return parseFunc(s[0..<paren], GeomUtil.split(s[paren+1..-2]))
      }

      // bad format
      throw Err()
    }
    catch (Err e) {}
    if (checked) throw ParseErr("Invalid Color: $s")
    return null
  }

  ** Parse comma separated list from string
  @NoDoc
  static Color[]? listFromStr(Str s, Bool checked := true)
  {
    try
    {
      toks := s.split(',')
      if (s.contains("("))
      {
        acc := StrBuf[,]
        inParen := false
        toks.each |tok, i|
        {
          if (inParen) acc.last.addChar(',').add(tok)
          else acc.add(StrBuf().add(tok))
          if (tok.contains("(")) inParen = true
          if (tok.contains(")")) inParen = false
        }
        toks = acc.map |buf->Str| { buf.toStr }
      }
      return toks.map |tok->Color| { Color.fromStr(tok) }
    }
    catch (Err e) e.trace
    if (checked) throw ParseErr("Invalid color list: $s")
    return null
  }

  private static Color parseHex(Str s)
  {
    sub := s[1..-1]
    hex := sub.toInt(16)
    switch (sub.size)
    {
      case 3:
        r := hex.shiftr(8).and(0xf); r = r.shiftl(4).or(r)
        g := hex.shiftr(4).and(0xf); g = g.shiftl(4).or(g)
        b := hex.shiftr(0).and(0xf); b = b.shiftl(4).or(b)
        return make(r.shiftl(16).or(g.shiftl(8)).or(b))
      case 4:
        r := hex.shiftr(12).and(0xf); r = r.shiftl(4).or(r)
        g := hex.shiftr(8).and(0xf);  g = g.shiftl(4).or(g)
        b := hex.shiftr(4).and(0xf);  b = b.shiftl(4).or(b)
        a := hex.shiftr(0).and(0xf);  a = a.shiftl(4).or(a)
        return makeRgb(r, g, b, a/255f)
      case 6:
        return make(hex)
      case 8:
        return make(hex.shiftr(8), GeomUtil.formatFloat(hex.and(0xff)/255f).toFloat)
      default:
        throw Err()
    }
  }

  private static Color parseFunc(Str func, Str[] args)
  {
    switch (func)
    {
      case "rgb":
      case "rgba":
        return makeRgb(parseRgbArg(args[0]), parseRgbArg(args[1]), parseRgbArg(args[2]), parsePercentArg(args.getSafe(3)))
      case "hsl":
      case "hsla":
        return makeHsl(parseDegArg(args[0]), parsePercentArg(args[1]), parsePercentArg(args[2]), parsePercentArg(args.getSafe(3)))
      default:
        throw Err()
    }
  }

  private static Int parseRgbArg(Str s)
  {
    if (s[-1] == '%') return (255f * s[0..-2].toFloat / 100f).toInt
    return s.toInt
  }

  private static Float parseDegArg(Str s)
  {
    if (s.endsWith("deg")) s = s[0..-4]
    f := s.toFloat
    if (f > 360f) f = f.toInt.mod(360).toFloat
    return f
  }

  private static Float parsePercentArg(Str? s)
  {
    if (s == null) return 1.0f
    if (s[-1] == '%') return s[0..-2].toFloat / 100f
    return s.toFloat
  }

//////////////////////////////////////////////////////////////////////////
// Color Model
//////////////////////////////////////////////////////////////////////////

  ** The RGB components masked together: bits 16-23 red;
  ** bits 8-15 green; bits 0-7 blue.
  const Int rgb

  ** The alpha component from 0.0 to 1.0
  const Float a

  ** The red component from 0 to 255.
  Int r() { rgb.shiftr(16).and(0xff) }

  ** The green component from 0 to 255.
  Int g() { rgb.shiftr(8).and(0xff) }

  ** The blue component from 0 to 255.
  Int b() { rgb.and(0xff) }

  ** Hue as a float between 0.0 and 360.0 of the HSL model (hue,
  ** saturation, lightness).  Also see `makeHsl`, `s`, `l`.
  Float h()
  {
    r := this.r.toFloat
    b := this.b.toFloat
    g := this.g.toFloat
    min := r.min(b.min(g))
    max := r.max(b.max(g))
    delta := max - min
    s := max == 0f ? 0f : delta / max
    h := 0f
    if (s != 0f)
    {
      if (r == max) h = (g - b) / delta
      else if (g == max) h = 2f + (b - r) / delta
      else if (b == max) h = 4f + (r - g) / delta
      h *= 60f
      if (h < 0f) h += 360f
    }
    return h
  }

  ** Saturation as a float between 0.0 and 1.0 of the HSL model (hue,
  ** saturation, lightness).  Also see `makeHsl`, `h`, `l`.
  Float s()
  {
    min := r.min(b.min(g)).toFloat
    max := r.max(b.max(g)).toFloat
    c   := max - min
    if (c == 0f) return 0f
    return c / (1f - (2f * l - 1f).abs) / 255f
  }

  ** Lightness (brightness) as a float between 0.0 and 1.0 of the HSL
  ** model (hue, saturation, lightness). Also see `makeHsl`, `h`, `s`.
  Float l()
  {
    max := r.max(b.max(g)).toFloat
    min := r.min(b.min(g)).toFloat
    return (max + min) * 0.5f / 255f
  }

//////////////////////////////////////////////////////////////////////////
// Identity
//////////////////////////////////////////////////////////////////////////

  ** Return the hash code.
  override Int hash() { rgb.xor(a.hash.shiftl(24)) }

  ** Equality
  override Bool equals(Obj? that)
  {
    x := that as Color
    if (x == null) return false
    return x.rgb == rgb && x.a == a
  }

  ** If the alpha component is 1.0, then format as '"#RRGGBB"' hex
  ** string, otherwise format as '"rbga()"' notation.
  override Str toStr()
  {
    if (a >= 1.0f) return toHexStr
    aStr := a.toLocale("0.##", Locale.en)
    return "rgba($r,$g,$b,$aStr)"
  }

  ** Format as #RGB, #RRGGBB or #RRGGBBAA syntax
  Str toHexStr()
  {
    hex := rgb.toHex(6)
    if (a >= 1f)
    {
      if (hex[0] == hex[1] && hex[2] == hex[3] && hex[4] == hex[5])
        return "#" + hex[0].toChar + hex[2].toChar + hex[4].toChar
      else
        return "#" + hex
    }
    ahex := (255f * a).toInt.min(255).max(0).toHex(2)
    return "#" + hex + ahex
  }

//////////////////////////////////////////////////////////////////////////
// Paint
//////////////////////////////////////////////////////////////////////////

  ** Always return true
  override Bool isColorPaint() { true }

  ** Return this
  override Color asColorPaint() { this }

//////////////////////////////////////////////////////////////////////////
// Utils
//////////////////////////////////////////////////////////////////////////

  ** Return if `a` is zero, fully transparent
  Bool isTransparent() { a <= 0f }

  ** Adjust the opacity of this color and return new instance,
  ** where 'opacity' is between 0.0  and 1.0.
  Color opacity(Float opacity := 1f)
  {
    make(rgb, a * opacity)
  }

  ** Get a color which is a lighter shade of this color.
  ** This increases the brightness by the given percentage
  ** which is a float between 0.0 and 1.0.
  Color lighter(Float percentage := 0.2f)
  {
    // adjust value (brighness)
    l := (this.l + percentage).max(0f).min(1f)
    return makeHsl(h, s, l)
  }

  ** Get a color which is a dark shade of this color.
  ** This decreases the brightness by the given percentage
  ** which is a float between 0.0 and 1.0.
  Color darker(Float percentage := 0.2f)
  {
    lighter(-percentage)
  }

  ** Adjust saturation as percentage between -1..1.
  Color saturate(Float percentage := 0.2f)
  {
    s := (this.s + percentage).max(0f).min(1f)
    return makeHsl(h, s, l)
  }

  ** Convenience for 'saturate(-percentage)'.
  Color desaturate(Float percentage := 0.2f)
  {
    saturate(-percentage)
  }

//////////////////////////////////////////////////////////////////////////
// Interpolate
//////////////////////////////////////////////////////////////////////////

  ** Interpolate between a and b where t is 0.0 to 1.0 using RGB color model.
  static Color interpolateRgb(Color a, Color b, Float t)
  {
    return Color.makeRgb(interpolateByte(a.r, b.r, t),
                         interpolateByte(a.g, b.g, t),
                         interpolateByte(a.b, b.b, t),
                         interpolatePercent(a.a, b.a, t))
  }

  ** Interpolate between a and b where t is 0.0 to 1.0 using HSL color model.
  static Color interpolateHsl(Color a, Color b, Float t)
  {
    return Color.makeHsl(interpolateDeg(a.h, b.h, t),
                         interpolatePercent(a.s, b.s, t),
                         interpolatePercent(a.l, b.l, t),
                         interpolatePercent(a.a, b.a, t))
  }

  private static Float interpolateDeg(Float a, Float b, Float t)
  {
    (a + (b-a) * t).min(360f).max(0f)
  }

  private static Int interpolateByte(Int a, Int b, Float t)
  {
    (a + (b-a) * t).toInt.min(255).max(0)
  }

  private static Float interpolatePercent(Float a, Float b, Float t)
  {
    (a + (b-a) * t).min(1f).max(0f)
  }

//////////////////////////////////////////////////////////////////////////
// Predefined
//////////////////////////////////////////////////////////////////////////

  @NoDoc static Str[] keywords() { byKeyword.keys }

  private static const Str:Color byKeyword
  static
  {
    // CSS 1, 2, 3, and 4 keywords
    acc := Str:Color[:] { caseInsensitive = true }
    acc["black"] = Color(0x000000)
    acc["silver"] = Color(0xc0c0c0)
    acc["gray"] = Color(0x808080)
    acc["white"] = Color(0xffffff)
    acc["maroon"] = Color(0x800000)
    acc["red"] = Color(0xff0000)
    acc["purple"] = Color(0x800080)
    acc["fuchsia"] = Color(0xff00ff)
    acc["green"] = Color(0x008000)
    acc["lime"] = Color(0x00ff00)
    acc["olive"] = Color(0x808000)
    acc["yellow"] = Color(0xffff00)
    acc["navy"] = Color(0x000080)
    acc["blue"] = Color(0x0000ff)
    acc["teal"] = Color(0x008080)
    acc["aqua"] = Color(0x00ffff)
    acc["orange"] = Color(0xffa500)
    acc["aliceblue"] = Color(0xf0f8ff)
    acc["antiquewhite"] = Color(0xfaebd7)
    acc["aquamarine"] = Color(0x7fffd4)
    acc["azure"] = Color(0xf0ffff)
    acc["beige"] = Color(0xf5f5dc)
    acc["bisque"] = Color(0xffe4c4)
    acc["blanchedalmond"] = Color(0xffebcd)
    acc["blueviolet"] = Color(0x8a2be2)
    acc["brown"] = Color(0xa52a2a)
    acc["burlywood"] = Color(0xdeb887)
    acc["cadetblue"] = Color(0x5f9ea0)
    acc["chartreuse"] = Color(0x7fff00)
    acc["chocolate"] = Color(0xd2691e)
    acc["coral"] = Color(0xff7f50)
    acc["cornflowerblue"] = Color(0x6495ed)
    acc["cornsilk"] = Color(0xfff8dc)
    acc["crimson"] = Color(0xdc143c)
    acc["cyan"] = Color(0x00ffff)
    acc["darkblue"] = Color(0x00008b)
    acc["darkcyan"] = Color(0x008b8b)
    acc["darkgoldenrod"] = Color(0xb8860b)
    acc["darkgray"] = Color(0xa9a9a9)
    acc["darkgreen"] = Color(0x006400)
    acc["darkgrey"] = Color(0xa9a9a9)
    acc["darkkhaki"] = Color(0xbdb76b)
    acc["darkmagenta"] = Color(0x8b008b)
    acc["darkolivegreen"] = Color(0x556b2f)
    acc["darkorange"] = Color(0xff8c00)
    acc["darkorchid"] = Color(0x9932cc)
    acc["darkred"] = Color(0x8b0000)
    acc["darksalmon"] = Color(0xe9967a)
    acc["darkseagreen"] = Color(0x8fbc8f)
    acc["darkslateblue"] = Color(0x483d8b)
    acc["darkslategray"] = Color(0x2f4f4f)
    acc["darkslategrey"] = Color(0x2f4f4f)
    acc["darkturquoise"] = Color(0x00ced1)
    acc["darkviolet"] = Color(0x9400d3)
    acc["deeppink"] = Color(0xff1493)
    acc["deepskyblue"] = Color(0x00bfff)
    acc["dimgray"] = Color(0x696969)
    acc["dimgrey"] = Color(0x696969)
    acc["dodgerblue"] = Color(0x1e90ff)
    acc["firebrick"] = Color(0xb22222)
    acc["floralwhite"] = Color(0xfffaf0)
    acc["forestgreen"] = Color(0x228b22)
    acc["gainsboro"] = Color(0xdcdcdc)
    acc["ghostwhite"] = Color(0xf8f8ff)
    acc["gold"] = Color(0xffd700)
    acc["goldenrod"] = Color(0xdaa520)
    acc["greenyellow"] = Color(0xadff2f)
    acc["grey"] = Color(0x808080)
    acc["honeydew"] = Color(0xf0fff0)
    acc["hotpink"] = Color(0xff69b4)
    acc["indianred"] = Color(0xcd5c5c)
    acc["indigo"] = Color(0x4b0082)
    acc["ivory"] = Color(0xfffff0)
    acc["khaki"] = Color(0xf0e68c)
    acc["lavender"] = Color(0xe6e6fa)
    acc["lavenderblush"] = Color(0xfff0f5)
    acc["lawngreen"] = Color(0x7cfc00)
    acc["lemonchiffon"] = Color(0xfffacd)
    acc["lightblue"] = Color(0xadd8e6)
    acc["lightcoral"] = Color(0xf08080)
    acc["lightcyan"] = Color(0xe0ffff)
    acc["lightgoldenrodyellow"] = Color(0xfafad2)
    acc["lightgray"] = Color(0xd3d3d3)
    acc["lightgreen"] = Color(0x90ee90)
    acc["lightgrey"] = Color(0xd3d3d3)
    acc["lightpink"] = Color(0xffb6c1)
    acc["lightsalmon"] = Color(0xffa07a)
    acc["lightseagreen"] = Color(0x20b2aa)
    acc["lightskyblue"] = Color(0x87cefa)
    acc["lightslategray"] = Color(0x778899)
    acc["lightslategrey"] = Color(0x778899)
    acc["lightsteelblue"] = Color(0xb0c4de)
    acc["lightyellow"] = Color(0xffffe0)
    acc["limegreen"] = Color(0x32cd32)
    acc["linen"] = Color(0xfaf0e6)
    acc["mediumaquamarine"] = Color(0x66cdaa)
    acc["mediumblue"] = Color(0x0000cd)
    acc["mediumorchid"] = Color(0xba55d3)
    acc["mediumpurple"] = Color(0x9370db)
    acc["mediumseagreen"] = Color(0x3cb371)
    acc["mediumslateblue"] = Color(0x7b68ee)
    acc["mediumspringgreen"] = Color(0x00fa9a)
    acc["mediumturquoise"] = Color(0x48d1cc)
    acc["mediumvioletred"] = Color(0xc71585)
    acc["midnightblue"] = Color(0x191970)
    acc["mintcream"] = Color(0xf5fffa)
    acc["mistyrose"] = Color(0xffe4e1)
    acc["moccasin"] = Color(0xffe4b5)
    acc["navajowhite"] = Color(0xffdead)
    acc["oldlace"] = Color(0xfdf5e6)
    acc["olivedrab"] = Color(0x6b8e23)
    acc["orangered"] = Color(0xff4500)
    acc["orchid"] = Color(0xda70d6)
    acc["palegoldenrod"] = Color(0xeee8aa)
    acc["palegreen"] = Color(0x98fb98)
    acc["paleturquoise"] = Color(0xafeeee)
    acc["palevioletred"] = Color(0xdb7093)
    acc["papayawhip"] = Color(0xffefd5)
    acc["peachpuff"] = Color(0xffdab9)
    acc["peru"] = Color(0xcd853f)
    acc["pink"] = Color(0xffc0cb)
    acc["plum"] = Color(0xdda0dd)
    acc["powderblue"] = Color(0xb0e0e6)
    acc["rosybrown"] = Color(0xbc8f8f)
    acc["royalblue"] = Color(0x4169e1)
    acc["saddlebrown"] = Color(0x8b4513)
    acc["salmon"] = Color(0xfa8072)
    acc["sandybrown"] = Color(0xf4a460)
    acc["seagreen"] = Color(0x2e8b57)
    acc["seashell"] = Color(0xfff5ee)
    acc["sienna"] = Color(0xa0522d)
    acc["skyblue"] = Color(0x87ceeb)
    acc["slateblue"] = Color(0x6a5acd)
    acc["slategray"] = Color(0x708090)
    acc["slategrey"] = Color(0x708090)
    acc["snow"] = Color(0xfffafa)
    acc["springgreen"] = Color(0x00ff7f)
    acc["steelblue"] = Color(0x4682b4)
    acc["tan"] = Color(0xd2b48c)
    acc["thistle"] = Color(0xd8bfd8)
    acc["tomato"] = Color(0xff6347)
    acc["transparent"] = transparent
    acc["turquoise"] = Color(0x40e0d0)
    acc["violet"] = Color(0xee82ee)
    acc["wheat"] = Color(0xf5deb3)
    acc["whitesmoke"] = Color(0xf5f5f5)
    acc["yellowgreen"] = Color(0x9acd32)
    acc["rebeccapurple"] = Color(0x663399)
    byKeyword = acc
  }
}