//
// Copyright (c) 2021, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   05 Aug 2021 Matthew Giannini Creation
//

using math

**
** A tagged ASN.1 value
**
const class AsnObj
{

//////////////////////////////////////////////////////////////////////////
// Constructor
//////////////////////////////////////////////////////////////////////////

  protected new make(AsnTag[] tags, Obj? val)
  {
    idx := tags.findIndex |tag| { tag.cls.isUniv }
    if (idx == -1) throw ArgErr("No UNIVERSAL tag specified")
    if (idx != tags.size - 1) throw ArgErr("UNIVERSAL tag must be last: ${idx} != ${tags.size-1}")
    this.tags = tags
    this.val  = val
  }

  ** The tags for this object.
  const AsnTag[] tags

  ** The value for this object.
  const Obj? val

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

  ** Get the value as a `sys::Bool`
  Bool bool() { val }

  ** Is this object's universal tag a 'Boolean'
  Bool isBool() { univTag == AsnTag.univBool }

  ** Get the value as an `sys::Int`. If the value is a `math::BigInt` you may lose
  ** both precision and sign. Use `bigInt` to get the value explicitly
  ** as a `math::BigInt`.
  Int int()
  {
    if (val is BigInt) return ((BigInt)val).toInt
    return val
  }

  ** Is this object's universal tag an 'Integer'
  Bool isInt() { univTag == AsnTag.univInt }

  ** Get the value as a `math::BigInt`.
  BigInt bigInt()
  {
    if (val is Int) return BigInt.makeInt(val)
    return val
  }

  ** Get any of the  binary values as a `sys::Buf`. The Buf will be a safe copy
  ** that can be modified. Throws `AsnErr` if the value is not a binary value.
  virtual Buf buf() { throw AsnErr("Not a binary type: ${this.typeof}") }

  ** Is this object's universal tag an 'Octet String'
  Bool isOcts() { univTag == AsnTag.univOcts }

  ** Is this an ASN.1 'Null' value
  Bool isNull() { val == null && univTag == AsnTag.univNull }

  ** Get this object as an `AsnOid`
  AsnOid oid() { this }

  ** Is this object's universal tag an 'Object Identifier'
  Bool isOid() { univTag == AsnTag.univOid }

  ** Get the value as a `sys::Str`
  Str str() { val }

  ** Get the value as a `sys::DateTime` timestamp
  DateTime ts() { val }

  ** Get this object as an `AsnColl`
  AsnColl coll() { this }

  ** Get this object as an `AsnSeq`
  AsnSeq seq() { this }

  @NoDoc virtual Bool isAny() { false }

//////////////////////////////////////////////////////////////////////////
// Tagging
//////////////////////////////////////////////////////////////////////////

  ** Push a tag to the front of the tag chain for this value. Returns
  ** a new instance of this object with the current value.
  **
  **   AsnObj.int(123).tag(AsnTag.implicit(TagClass.context, 0))
  **     => [0] IMPLICIT [UNIVERSAL 2]
  **   AsnObj.int(123).tag(AsnTag.explicit(TagClass.app, 1))
  **     => [APPLICATION 1] EXPLICIT [UNIVERSAL 2]
  virtual AsnObj push(AsnTag tag)
  {
    this.typeof.method("make").call([tag].addAll(this.tags), this.val)
  }

  ** Apply rules for 'EXPLICIT' and 'IMPLICIT' tags to obtain
  ** the set of effective tags for encoding this object.
  AsnTag[] effectiveTags()
  {
    acc := AsnTag[,]
    AsnTag? prev := null
    tags.each |tag|
    {
      if (prev == null) acc.add(tag)
      else if (prev.mode === AsnTagMode.explicit) acc.add(tag)
      prev = tag
    }
    return acc
  }

  ** Get the single effective tag for this object. Throws an error
  ** if there are multiple effective tags
  AsnTag tag()
  {
    etags := this.effectiveTags
    if (etags.size > 1) throw AsnErr("Multiple effective tags: $etags")
    return etags.first
  }

  ** Get the univ tag for this object
  AsnTag univTag() { tags.last }

  ** Is this a primitive type?
  Bool isPrimitive()
  {
    switch (univTag)
    {
      case AsnTag.univSeq:
      case AsnTag.univSet:
        return false
      default:
        return true
    }
  }

//////////////////////////////////////////////////////////////////////////
// Obj
//////////////////////////////////////////////////////////////////////////

  final override Int hash()
  {
    res := 31 + tags.hash
    res = (res * 31) + valHash
    return res
  }

  protected virtual Int valHash() { val?.hash ?: 0}

  final override Bool equals(Obj? obj)
  {
    if (this === obj) return true
    that := obj as AsnObj
    if (that == null) return false

    // for objects, tag equality is strict
    these := this.tags
    those := that.tags
    if (these.size != those.size) return false
    eq := these.all |t,i| { t.strictEquals(those[i]) }
    if (!eq) return false

    return valEquals(that)
  }

  protected virtual Bool valEquals(AsnObj that) { this.val == that.val }

  override Str toStr()
  {
    "${tags} ${valStr}"
  }

  protected virtual Str valStr() { "${val}" }
}