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

**
** ObixObj models an 'obix:obj' element.
**
class ObixObj
{

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

  **
  ** Programatic name of the object which defines role of
  ** this object in its parent.  Throw UnsupportedErr if an
  ** attempt is made to set the name once mounted under a parent.
  **
  Str? name
  {
    set
    {
      if (parent != null) throw UnsupportedErr("cannot set name while parented")
      this.&name = it
    }
  }

  **
  ** URI of this object. The root object of a document must have
  ** an absolute URI, other objects may have a URI relative to
  ** the document root.  See `normalizedHref` to get this href
  ** normalized against the root object.
  **
  Uri? href

  **
  ** Get this objects `href` normalized against the root object's
  ** URI.  Return null no href defined.
  **
  Uri? normalizedHref()
  {
    if (href == null) return null
    r := root
    if (r.href == null) return null
    return r.href + href
  }

  **
  ** The XML element name to use for this object.  If not
  ** one of the valid oBIX element names then throw ArgErr.
  ** Valid element names are:
  **   obj, bool, int, real, str, enum, uri, abstime,
  **   reltime, date, time, list, op, feed, ref, err
  **
  Str elemName := "obj"
  {
    set
    {
      if (!ObixUtil.elemNames[it]) throw ArgErr("Invalid elemName: $it")
      this.&elemName = it
    }
  }

  **
  ** Return string representation.
  **
  override Str toStr()
  {
    s := StrBuf()
    s.add("<").add(elemName)
    if (name != null) s.add(" name='").add(name).add("'")
    if (href != null) s.add(" href='").add(href).add("'")
    if (val != null)  s.add(" val='").add(ObixUtil.valToStr(val)).add("'")
    s.add(">");
    return s.toStr
  }

//////////////////////////////////////////////////////////////////////////
// Contracts
//////////////////////////////////////////////////////////////////////////

  **
  ** The list of contract URIs this object implemented
  ** as specified by 'is' attribute.
  **
  Contract contract := Contract.empty

  **
  ** The 'of' contract for lists and feeds.
  **
  Contract? of

  **
  ** The 'in' contract for operations and feeds.
  **
  Contract? in

  **
  ** The 'out' contract for operations.
  **
  Contract? out

//////////////////////////////////////////////////////////////////////////
// Value
//////////////////////////////////////////////////////////////////////////

  **
  ** The null flag indicates the absense of a value.
  **
  Bool isNull

  **
  ** Object value for value object types:
  **   - obix:bool    => sys::Bool
  **   - obix:int     => sys::Int
  **   - obix:real    => sys::Float
  **   - obix:str     => sys::Str
  **   - obix:enum    => sys::Str
  **   - obix:uri     => sys::Uri
  **   - obix:abstime => sys::DateTime
  **   - obix:reltime => sys::Duration
  **   - obix:date    => sys::Date
  **   - obix:time    => sys::Time
  **
  ** If the value is not one of the types listed above, then ArgErr is
  ** thrown.  If the value is set to non-null, then the `elemName` is
  ** automatically updated.
  **
  Obj? val
  {
    set
    {
      // TODO: clean this up
      if (elemName == "enum" && it is Str) { &val = it; return }
      if (it != null)
      {
        elem := ObixUtil.valTypeToElemName[Type.of(it)]
        if (elem == null) throw ArgErr("Invalid val type: ${Type.of(it)}")
        this.&elemName = elem
        if (it is DateTime && tz == null)
        {
          tz := ((DateTime)it).tz
          if (!tz.fullName.startsWith("Etc/")) this.tz = tz
        }
      }
      &val = it
    }
  }

  **
  ** Return this element type's Fantom value type or null if this
  ** is a non-value type such as 'obj'.
  **
  Type? valType() {  ObixUtil.elemNameToValType[elemName] }

  **
  ** Get the value encoded as a string.  The string is *not*
  ** XML escaped.  If value is null return "null".
  **
  Str valToStr()
  {
    return ObixUtil.valToStr(val)
  }

//////////////////////////////////////////////////////////////////////////
// Parent/Child Tree
//////////////////////////////////////////////////////////////////////////

  **
  ** Get the root ancestor of this object, or
  ** return 'this' if no parent.
  **
  ObixObj root()
  {
    x := this
    while (x.parent != null) x = x.parent
    return x
  }

  **
  ** Parent object or null if unparented.
  **
  ObixObj? parent { private set }

  **
  ** Return is size is zero.
  **
  Bool isEmpty() { return kidsCount == 0 }

  **
  ** Return number of child objects.
  **
  Int size() { return kidsCount }

  **
  ** Return if there is child object by the specified name.
  **
  Bool has(Str name)
  {
    if (kidsByName == null) return false
    return kidsByName.containsKey(name)
  }

  **
  ** Get a child by name.  If not found and checked is true
  ** then throw NameErr, otherwise null.
  **
  @Operator ObixObj? get(Str name, Bool checked := true)
  {
    child := kidsByName?.get(name)
    if (child != null) return child
    if (checked) throw NameErr("Missing obix child '$name'")
    return null
  }

  **
  ** If the name maps to a child object, then return that
  ** child's value.  Otherwise route to 'Obj.trap'.
  **
  override Obj? trap(Str name, Obj?[]? args := null)
  {
    child := kidsByName?.get(name)
    if (child != null) return child.val
    return super.trap(name, args)
  }

  **
  ** Get a readonly list of the children objects or empty
  ** list if no children.  If iterating the children it is
  ** more efficient to use `each`.
  **
  ObixObj[] list()
  {
    if (kidsCount == 0) return noChildren
    list := ObixObj[,] { capacity = kidsCount }
    for (ObixObj? p := kidsHead; p != null; p = p.next) list.add(p)
    return list.ro
  }

  **
  ** Get the first child returned by `list` or null.
  **
  ObixObj? first() { kidsHead }

  **
  ** Get the last child returned by `list` or null.
  **
  ObixObj? last() { kidsTail }

  **
  ** Iterate each of the children objects.
  **
  Void each(|ObixObj child| f)
  {
    for (ObixObj? p := kidsHead; p != null; p = p.next) f(p)
  }

  **
  ** Add a child object.  Throw ArgErr if this child is
  ** already parented or has a duplicate name.  Return this.
  **
  @Operator This add(ObixObj kid)
  {
    // sanity checks
    if (kid.parent != null || kid.prev != null || kid.next != null)
      throw ArgErr("Child is already parented")
    if (kid.name != null && kidsByName != null && kidsByName.containsKey(kid.name) && elemName != "list")
      throw ArgErr("Duplicate child name '$kid.name'")

    // if named, add to name map
    if (kid.name != null)
    {
      if (kidsByName == null) kidsByName = Str:ObixObj[:]
      kidsByName[kid.name] = kid
    }

    // add to ordered linked list
    if (kidsTail == null) { kidsHead = kidsTail = kid }
    else { kidsTail.next = kid; kid.prev = kidsTail; kidsTail = kid }

    // update kid's references and count
    kidsCount++
    kid.parent = this
    return this
  }

  **
  ** Remove the specified child object by reference.
  ** Throw ArgErr if not my child.  Return this
  **
  This remove(ObixObj kid)
  {
    // sanity checks
    if (kid.parent != this) throw ArgErr("Not parented by me")

    // remove from name map if applicable
    if (kid.name != null) kidsByName.remove(kid.name)

    // remove from linked list
    if (kidsHead == kid) { kidsHead = kid.next }
    else { kid.prev.next = kid.next }
    if (kidsTail == kid) { kidsTail = kid.prev }
    else { kid.next.prev = kid.prev }

    // clear kid's references and count
    kidsCount--
    kid.parent = null
    kid.prev   = null
    kid.next   = null
    return this
  }

  **
  ** Remove all children objects.  Return this.
  **
  This clear()
  {
    ObixObj? p := kidsHead
    while (p != null)
    {
      x := p.next
      p.parent = p.prev = p.next = null
      p = x
    }

    kidsByName.clear
    kidsHead = kidsTail = null
    kidsCount = 0
    return this
  }

//////////////////////////////////////////////////////////////////////////
// Facets
//////////////////////////////////////////////////////////////////////////

  **
  ** Localized human readable version of the name attribute.
  **
  Str? displayName

  **
  ** Localized human readable string summary of the object.
  **
  Str? display

  **
  ** Reference to the graphical icon.
  **
  Uri? icon

  **
  ** Inclusive minium for value.
  **
  Obj? min

  **
  ** Inclusive maximum for value.
  **
  Obj? max

  **
  ** Number of decimal places to use for a real value.
  **
  Int? precision

  **
  ** Reference to the range definition of an enum or bool value.
  **
  Uri? range

  **
  ** Status facet indicates quality and state.
  **
  Status status := Status.ok

  **
  ** TimeZone facet assocaited with abstime, date, and time objects.
  ** This field is automatically updated when `val` is assigned a
  ** DateTime unless its timezone is UTC or starts with "Etc/".  After
  ** decoding this field is set only if an explicit "tz" attribute was
  ** specified.
  **
  TimeZone? tz

  **
  ** Unit of measurement for int and real values.  We only support units
  ** which are predefind in the oBIX unit database and specified using the
  ** URI "obix:units/".  These units are mapped to the `sys::Unit` API.
  ** If an unknown unit URI is decoded, then it is silently ignored and
  ** this field will be null.
  **
  Unit? unit

  **
  ** Specifies is this object can be written, or false if readonly.
  **
  Bool writable

//////////////////////////////////////////////////////////////////////////
// IO
//////////////////////////////////////////////////////////////////////////

  **
  ** Parse an XML document into memory as a tree of ObixObj.
  ** If close is true, then the input stream is guaranteed to
  ** be closed.
  **
  static ObixObj readXml(InStream in, Bool close := true)
  {
    return ObixXmlParser(in).parse(close)
  }

  **
  ** Write this ObixObj as an XML document to the specified stream.
  ** No XML prolog is specified so that this method can used to
  ** write a snippet of the overall document.
  **
  virtual Void writeXml(OutStream out, Int indent := 0)
  {
    // identity
    out.print(Str.spaces(indent)).print("<").print(elemName)
    if (name != null) out.print(" name='").writeXml(name, xmlEsc).print("'")
    if (href != null) out.print(" href='").print(href.encode).print("'")

    // contracts
    if (!contract.isEmpty) out.print(" is='").print(contract).print("'")
    if (of != null && !of.isEmpty) out.print(" of='").print(of).print("'")
    if (in != null && !in.isEmpty) out.print(" in='").print(in).print("'")
    if (this.out != null && !this.out.isEmpty) out.print(" out='").print(this.out).print("'")

    // value
    if (val != null) out.print(" val='").writeXml(valToStr, xmlEsc).print("'")
    if (isNull) out.print(" isNull='true'")

    // facets
    if (displayName != null) out.print(" displayName='").writeXml(displayName, xmlEsc).print("'")
    if (display != null) out.print(" display='").writeXml(display, xmlEsc).print("'")
    if (icon != null) out.print(" icon='").print(icon.encode).print("'")
    if (min != null) out.print(" min='").print(ObixUtil.valToStr(min)).print("'")
    if (max != null) out.print(" max='").print(ObixUtil.valToStr(max)).print("'")
    if (precision != null) out.print(" precision='").print(precision).print("'")
    if (range != null) out.print(" range='").print(range.encode).print("'")
    if (status !== Status.ok) out.print(" status='").print(status).print("'")
    if (tz != null) out.print(" tz='").print(tz.fullName).print("'")
    if (unit != null) out.print(" unit='obix:units/").print(unit.name).print("'")
    if (writable) out.print(" writable='true'")

    // children
    if (isEmpty) out.print("/>\n")
    else
    {
      out.print(">\n")
      each |ObixObj kid| { kid.writeXml(out, indent+1) }
      out.print(Str.spaces(indent)).print("</").print(elemName).print(">\n")
    }
    if (parent == null) out.flush
  }

  @NoDoc This dump()
  {
    out := Env.cur.out
    writeXml(out)
    out.flush
    return this
  }

//////////////////////////////////////////////////////////////////////////
// Fields
//////////////////////////////////////////////////////////////////////////

  internal static const Uri[] noUris := Uri[,]
  internal static const ObixObj[] noChildren := ObixObj[,]
  internal static const Int xmlEsc := OutStream.xmlEscNewlines.or(OutStream.xmlEscQuotes)

  private [Str:ObixObj]? kidsByName // map of children by name
  private ObixObj? kidsHead         // children linked list
  private ObixObj? kidsTail         // children linked list
  private Int kidsCount             // children linked list
  private ObixObj? prev             // sibling in parents linked list
  private ObixObj? next             // sibling in parents linked list

}