//
// Copyright (c) 2009, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   10 Aug 09  Brian Frank  Creation
//

**
** Modes width, style, color, and radius of a rectangular border.
**
@Js
@Serializable { simple = true }
const class Border
{

  ** Width in pixels of top side, default is 1.
  const Int widthTop := 1
  ** Width in pixels of right side, default is 1.
  const Int widthRight := 1
  ** Width in pixels of bottom side, default is 1.
  const Int widthBottom := 1
  ** Width in pixels of left side, default is 1.
  const Int widthLeft := 1

  ** Style of top side as one of styleX constants, default is solid.
  const Int styleTop := styleSolid
  ** Style of right side as one of styleX constants, default is solid.
  const Int styleRight := styleSolid
  ** Style of bottom side as one of styleX constants, default is solid.
  const Int styleBottom := styleSolid
  ** Style of left side as one of styleX constants, default is solid.
  const Int styleLeft := styleSolid

  ** Constant identifier for solid style.
  static const Int styleSolid  := 0
  ** Constant identifier for inset style.
  static const Int styleInset  := 1
  ** Constant identifier for outside style.
  static const Int styleOutset := 2

  ** Color of top side, default is black.
  const Color colorTop := black
  ** Color of right side, default is black.
  const Color colorRight := black
  ** Color of bottom side, default is black.
  const Color colorBottom := black
  ** Color of left side, default is black.
  const Color colorLeft := black

  ** Radius in pixels of top-left corner, default is 0.
  const Int radiusTopLeft := 0
  ** Radius in pixels of top-right corner, default is 0.
  const Int radiusTopRight := 0
  ** Radius in pixels of bottom-right corner, default is 0.
  const Int radiusBottomRight := 0
  ** Radius in pixels of bottom-left corner, default is 0.
  const Int radiusBottomLeft := 0

  // to avoid Color javascript initializer dependency
  private static const Color black := Color(0)

  **
  ** Default is zero pixel border.
  **
  static const Border defVal := Border.fromStr("0")

  **
  ** Construct with it-block
  **
  new make(|This| f)
  {
    f(this)
    toStr = formatStr
  }

  **
  ** Copy border with some modifications
  **
  new copy(Border orig, |This| f)
  {
    this.widthTop    = orig.widthTop
    this.widthRight  = orig.widthRight
    this.widthBottom = orig.widthBottom
    this.widthLeft   = orig.widthLeft

    this.styleTop    = orig.styleTop
    this.styleRight  = orig.styleRight
    this.styleBottom = orig.styleBottom
    this.styleLeft   = orig.styleLeft

    this.colorTop    = orig.colorTop
    this.colorRight  = orig.colorRight
    this.colorBottom = orig.colorBottom
    this.colorLeft   = orig.colorLeft

    this.radiusTopLeft     = orig.radiusTopLeft
    this.radiusTopRight    = orig.radiusTopRight
    this.radiusBottomRight = orig.radiusBottomRight
    this.radiusBottomLeft  = orig.radiusBottomLeft

    f(this)
    toStr = formatStr
  }

  **
  ** Parse a border from string (see `toStr`).  If invalid
  ** and checked is true then throw ParseErr otherwise
  ** return null.  The string formatted as four optional
  ** parts, where each part may have 1 to 4 values:
  **   border := [width] [style] [color] [radius]
  **   width  := top ["," right ["," bottom ["," left]]]
  **   style  := top ["," right ["," bottom ["," left]]]
  **   color  := top ["," right ["," bottom ["," left]]]
  **   radius := top-left ["," top-right ["," bottom-right ["," bottom-left]]]
  **
  ** Width and radius must be integers, color must match `Color` string
  ** format, and style must be "solid", "inset", or "outset".  If one side
  ** is not specified, it is reflected from the opposite side:
  **   a      =>  a,a,a,a
  **   a,b    =>  a,b,a,b
  **   a,b,c  =>  a,b,c,b
  **
  ** Examples:
  **   Border("2")          =>  2 solid #000000 0
  **   Border("#abc")       =>  1 solid #aabbcc 0
  **   Border("2 inset 3")  =>  2 inset #000000 3
  **   Border("0,1,2,3")    =>  0,1,2,3 solid #000000 0
  **   Border("0,1,2 #00f") =>  0,1,2 solid #0000ff 0
  **
  static new fromStr(Str str, Bool checked := true)
  {
    try
    {
      if (str.isEmpty) return defVal
      return makeStr(str)
    }
    catch {}
    if (checked) throw ParseErr("Invalid Border: $str")
    return null
  }

  private new makeStr(Str str)
  {
    p := BorderParser(str)

    p.parseGroup(1) |s| { Int.fromStr(s, 10, false) }
    this.widthTop    = p.top
    this.widthRight  = p.right
    this.widthBottom = p.bottom
    this.widthLeft   = p.left

    p.parseGroup(styleSolid) |s| { Border.styleFromStr(s, false) }
    this.styleTop    = p.top
    this.styleRight  = p.right
    this.styleBottom = p.bottom
    this.styleLeft   = p.left

    p.parseGroup(black) |s| { Color.fromStr(s, false) }
    this.colorTop    = p.top
    this.colorRight  = p.right
    this.colorBottom = p.bottom
    this.colorLeft   = p.left

    p.parseGroup(0) |s| { Int.fromStr(s, 10, false) }
    this.radiusTopLeft     = p.top
    this.radiusTopRight    = p.right
    this.radiusBottomRight = p.bottom
    this.radiusBottomLeft  = p.left

    if (p.tok != null) throw Err()
    this.toStr = formatStr
  }

  **
  ** Hash is based on string format.
  **
  override Int hash() { toStr.hash }

  **
  ** Equality is based on string format.
  **
  override Bool equals(Obj? obj)
  {
    that := obj as Border
    if (that == null) return false
    return toStr == that.toStr
  }

  **
  ** Return "solid", "inset", "outset" for int constant.
  **
  static Str styleToStr(Int s)
  {
    switch (s)
    {
      case styleSolid:  return "solid"
      case styleInset:  return "inset"
      case styleOutset: return "outset"
      default:          throw ArgErr()
    }
  }

  **
  ** Parse style string into int constant - see `styleToStr`.
  **
  static Int? styleFromStr(Str s, Bool checked := true)
  {
    switch (s)
    {
      case "solid":  return styleSolid
      case "inset":  return styleInset
      case "outset": return styleOutset
      default:
        if (checked) throw ParseErr(s)
        return null
    }
  }

  **
  ** String format - see `fromStr` for format.
  **
  override const Str toStr

  private Str formatStr()
  {
    s := StrBuf()
    formatPart(s, widthTop, widthRight, widthBottom, widthLeft) { it.toStr }
    formatPart(s, styleTop, styleRight, styleBottom, styleLeft) { styleToStr(it) }
    formatPart(s, colorTop, colorRight, colorBottom, colorLeft) { it.toStr }
    formatPart(s, radiusTopLeft, radiusTopRight, radiusBottomRight, radiusBottomLeft) { it.toStr }
    return s.toStr
  }

  private StrBuf formatPart(StrBuf s, Obj t, Obj r, Obj b, Obj l, |Obj->Str| f)
  {
    if (!s.isEmpty) s.addChar(' ')
    if (l == r)
    {
      if (t == b)
      {
        if (t == l) return s.add(f(t))
        return s.add(f(t)).addChar(',').add(f(l))
      }
      return s.add(f(t)).addChar(',').add(f(r)).addChar(',').add(f(b))
    }
    return s.add(f(t)).addChar(',').add(f(r)).addChar(',').add(f(b)).addChar(',').add(f(l))
  }

  **
  ** Return widthRight+widthLeft, widthTop+widthBottom
  **
  Size toSize() { Size(widthRight+widthLeft, widthTop+widthBottom) }

}

**************************************************************************
** BorderParser
**************************************************************************

@Js
internal class BorderParser
{
  new make(Str str) { this.str = str; next }

  Void parseGroup(Obj def, |Str s->Obj?| f)
  {
    top = tok != null ? f(tok) : null
    if (top == null) { top = right = bottom = left = def; return }
    right  = comma ? parse(f) : top
    bottom = comma ? parse(f) : top
    left   = comma ? parse(f) : right
    if (comma) throw Err()
    next
  }

  private Obj parse(|Str s->Obj?| f)
  {
    next
    val := f(tok)
    if (val == null) throw Err()
    return val
  }

  private Void next()
  {
    // if no more tokens
    size := str.size
    if (n >= size) { tok = null; comma = false; return }

    // strip leading whitespace
    while (n < size && str[n] == ' ') ++n

    // parse token
    s := n
    for (; n<size; ++n)
      if (str[n] == ' ' || str[n] == ',') break
    tok = str[s ..< n]

    // strip trailing whitespace
    while (n < size && str[n] == ' ') ++n

    // check if we have a comma, if so skip it and return true
    comma = n < size && str[n] == ','
    if (comma) ++n
  }

  Str str
  Int n
  Str? tok := "?"
  Bool comma
  Obj? top; Obj? right; Obj? bottom; Obj? left;
}