sourceafFancom::Variant.fan

using [java] com.jacob.com::Variant as JVariant
using [java] java.util::Date as JDate

** A multi-format data type used for all COM communications.
** 
** A Variant's type may be queried using the 'isXXX()' methods and returned with the 'asXXX()' 
** methods. Variants have the notion of being null, the equivalent of VB Nothing. Whereas Variant 
** objects themselves are never null, their 'asXXX()' methods may return null.
** 
** The [asType()]`asType` method can be used to return any Fantom literal ('Bool', 'Int', 'Emum', 
** etc...) and also Fancom Surrogates. Surrogates are user defined classes which wrap either a 
** `Dispatch` or a `Variant` object. Surrogates make it easy to write boiler plate Fantom classes 
** that mimic the behaviour of COM components. 
** 
** 
** Variant Surrogates [#surrogate]
** ===============================
** A Variant Surrogate is any object that conforms to the following rules. (Think of it as an 
** *implied* interface.)
** 
** A Variant Surrogate **must** have either:
**  - a ctor with the signature 'new makeFromVariant(Variant variant)' or
**  - a static factory method with the signature 'static MyClass fromVariant(Variant variant)'
** 
** A Variant Surrogate **must** also have either:
**  - a method with the signature 'Variant toVariant()' or
**  - a field with the signature 'Variant variant'
**  
** This allows Variant Surrogates to be passed in as parameters to a Dispatch object, and to be 
** instantiated from the [asType()]`asType` method.
** 
** Enums are good candidates for converting into surrogates. Also see `Flag` as another example.
** 
** Note: Fancom Surrogate methods and fields may be of any visibility - which is useful if don't 
** wish to expose them.
**  
** Note: Fancom Surrogates don't require you to implement any Mixins, reflection is used to find 
** your wrapper methods.
**
**
** Variant Types [#variantTypes]
** =============================
** The conversion of types between the various underlying systems is given below:
** 
** pre>
**   Automation Type | JACOB Type | Fancom Type | Remarks
**   ---------------------------------------------------------------------
**    0 VT_EMPTY       null         null          Equivalent to VB Nothing
**    1 VT_NULL        null         null          Equivalent to VB Null
**    2 VT_I2          short        Int           
**    3 VT_I4          int          Int / Enum    A Long in VC
**    4 VT_R4          float        Float
**    5 VT_R8          double       Float
**    6 VT_CY          Currency     ---
**    7 VT_DATE        Date         DateTime
**    8 VT_BSTR        String       Str
**    9 VT_DISPATCH    Dispatch     Dispatch
**   10 VT_ERROR       int          throws Err
**   11 VT_BOOL        boolean      Bool
**   12 VT_VARIANT     Object       ---
**   14 VT_DECIMAL     BigDecimal   Decimal
**   16 VT_I1          ---          Int 
**   17 VT_UI1         byte         Int
**   18 VT_UI2         ---          Int 
**   19 VT_UI4         ---          Int 
**   20 VT_I8          long         Int
**   21 VT_UI8         ---          Int           Unsigned 64 bit - will throw err if out of range 
**   22 VT_INT         ---          Int           Signed 32 / 64 bit (dependent on OS) 
**   23 VT_UINT        ---          Int           Unsigned 32 / 64 bit - will throw err if out of range
**   25 VT_HRESULT     ---          throws Err
**   30 VT_LPSTR       ---          Str
**   31 VT_LPWSTR      ---          Str
** <pre
** 
** All other value types are unsupported by JACOB and hence unsupported by Fancom. For more information 
** on Automation Types see [VARENUM enumeration (Automation)]`http://msdn.microsoft.com/en-us/library/windows/desktop/ms221170%28v=vs.85%29.aspx` on MSDN. 
** 
class Variant {
    private static const Int VT_VECTOR  := 0x1000 
    private static const Int VT_ARRAY   := 0x2000 
    private static const Int VT_BYREF   := 0x4000
    
    ** The JACOB Variant this object wraps
    JVariant variant { private set }
    
    internal VarType varType

    // ---- Constructors --------------------------------------------------------------------------

    ** Make a 'null' Variant
    new make() {
        this.variant = JVariant()
        this.varType = VarType.fromVt(variant.getvt)
    }

    ** Make a Variant representing a 'Bool' value
    new makeFromBool(Bool value) {
        this.variant = JVariant(value)
        this.varType = VarType.fromVt(variant.getvt)
    }

    ** Make a Variant representing a 'DateTime' value
    new makeFromDateTime(DateTime value) {
        this.variant = JVariant(JDate(value.toJava))
        this.varType = VarType.fromVt(variant.getvt)
    }

    ** Make a Variant representing a 'Decimal' value
    new makeFromDecimal(Decimal value) {
        this.variant = JVariant(value)
        this.varType = VarType.fromVt(variant.getvt)
    }
    
    ** Make a Variant representing a 'Float' value
    new makeFromFloat(Float value) {
        this.variant = JVariant(value)
        this.varType = VarType.fromVt(variant.getvt)
    }
    
    ** Make a Variant representing an 'Int' value
    new makeFromInt(Int value) {
        this.variant = fromLong(value)
        this.varType = VarType.fromVt(variant.getvt)
    }

    ** Make a Variant representing a 'Str' value
    new makeFromStr(Str value) {
        this.variant = JVariant(value)
        this.varType = VarType.fromVt(variant.getvt)
    }
    
    ** Make a Variant wrapping the given JACOB Variant
    internal new makeFromVariant(JVariant variant) {
        this.variant = variant
        this.varType = VarType.fromVt(variant.getvt)
    }

    
    // ---- Is Methods ----------------------------------------------------------------------------
    
    ** Returns 'true' if this Variant represents a 'null' value
    Bool isNull() {
        variant.isNull
    }
    
    ** Returns 'true' if this Variant represents a 'Bool' value.
    ** Will return 'false' if the Variant represents 'null' as the type cannot be determined.
    Bool isBool() {
        if (isNull)
            return false
        return varType.isFantomBool
    }

    ** Returns 'true' if this Variant represents a `Dispatch` object.
    ** Will return 'false' if the Variant represents 'null' as the type cannot be determined.
    Bool isDispatch() {
        if (isNull)
            return false
        return varType.isFantomDispatch
    }

    ** Returns 'true' if this Variant represents a [DateTime]`sys::DateTime` object.
    ** Will return 'false' if the Variant represents 'null' as the type cannot be determined.
    Bool isDateTime() {
        if (isNull)
            return false
        return varType.isFantomDateTime
    }

    ** Returns 'true' if this Variant represents a [Decimal]`sys::Decimal` object.
    ** Will return 'false' if the Variant represents 'null' as the type cannot be determined.
    Bool isDecimal() {
        if (isNull)
            return false
        return varType.isFantomDecimal
    }

    ** Returns 'true' if this Variant can be converted into the given 'Enum' type, checking that 
    ** the Variant value is a valid ordinal for the type. 
    ** Throws 'ArgErr' if the given type is not a subclass of 'Enum'.
    Bool isEnum(Type enumType) {
        if (!enumType.isEnum)
            throw ArgErr(Messages.wrongType(enumType, Enum#))
        if (isNull)
            return false
        if (!varType.isFantomInt)
            return false
        
        // attempt to create an Enum, returning false if an Err is throw
        // yeah, this is a little 'orrid but when dealing with surrogates and ordinals it's the only 
        // way to be sure
        try {
            surrogate := Helper.toVariantSurrogate(enumType, this) 
            if (surrogate == null)
                Helper.toEnum(enumType, this)
            return true
        } catch (Err e) {
            return false
        }
    }

    ** Returns 'true' if this Variant represents a 'Float' value.
    ** Will return 'false' if the Variant represents 'null' as the type cannot be determined.
    Bool isFloat() {
        if (isNull)
            return false
        return varType.isFantomFloat
    }

    ** Returns 'true' if this Variant represents a 'Int' value.
    ** Will return 'false' if the Variant represents 'null' as the type cannot be determined.
    Bool isInt() {
        if (isNull)
            return false
        return varType.isFantomInt
    }
    
    ** Returns 'true' if this Variant represents a 'Str' value.
    ** Will return 'false' if the Variant represents 'null' as the type cannot be determined.
    Bool isStr() {
        if (isNull)
            return false
        return varType.isFantomStr
    }


    // ---- As Methods ----------------------------------------------------------------------------

    ** Returns the Variant value as a 'Bool' or 'null' if the Variant represents 'null'. 
    ** Throws `FancomErr` if the Variant is not a 'Bool' type.
    Bool? asBool() {
        if (isNull)
            return null
        if (!isBool)
            throw FancomErr(Messages.wrongVariantType(this, Bool#))
        return variant.changeType(VarType.VT_BOOL.vt).getBoolean
    }
    
    ** Returns the Variant value as a 'DateTime' or 'null' if the Variant represents 'null'. 
    ** Throws `FancomErr` if the Variant is not a 'DateTime' type.
    DateTime? asDateTime() {
        if (isNull)
            return null
        if (!isDateTime)
            throw FancomErr(Messages.wrongVariantType(this, DateTime#))
        date := variant.changeType(VarType.VT_DATE.vt).getJavaDate
        return DateTime.fromJava(date.getTime)
    }

    ** Returns the Variant value as a 'Decimal' or 'null' if the Variant represents 'null'. 
    ** Throws `FancomErr` if the Variant is not a 'Decimal' type.
    Decimal? asDecimal() {
        if (isNull)
            return null
        if (!isDecimal)
            throw FancomErr(Messages.wrongVariantType(this, Decimal#))
        return variant.changeType(VarType.VT_DECIMAL.vt).getDecimal
    }
    
    ** Returns the Variant value as a `Dispatch` object or 'null' if the Variant represents 'null'. 
    ** Throws `FancomErr` if the Variant is not a `Dispatch` type.
    Dispatch? asDispatch() {
        if (isNull)
            return null
        if (!isDispatch)
            throw FancomErr(Messages.wrongVariantType(this, Dispatch#))
        dispatch := variant.changeType(VarType.VT_DISPATCH.vt).getDispatch
        return Dispatch(dispatch)
    }
    
    ** Returns the Variant value as an instance of the supplied 'Enum' type or 'null' if the 
    ** Variant represents 'null'. 
    ** 
    ** Throws 'ArgErr' if the given type is not a subclass of 'Enum'.
    ** Throws `FancomErr` if the Variant is not an 'Enum' ('Int') type or if the 'Int' value is not a valid ordinal for the enum.
    Enum? asEnum(Type enumType) {
        if (isNull) 
            return null
        if (!isEnum(enumType))
            throw FancomErr(Messages.wrongVariantType(this, enumType))
        
        // check for variant surrogacy first, as enums often define their own values
        surrogate := Helper.toVariantSurrogate(enumType, this) 
        if (surrogate != null)
            return surrogate
        return Helper.toEnum(enumType, this) 
    }   
    
    ** Returns the Variant value as a 'Float' or 'null' if the Variant represents 'null'. 
    ** Throws `FancomErr` if the Variant is not a 'Float' type.
    Float? asFloat() {
        if (isNull)
            return null
        if (!isFloat)
            throw FancomErr(Messages.wrongVariantType(this, Float#))
        return variant.changeType(VarType.VT_R8.vt).getDouble
    }

    ** Returns the Variant value as an 'Int' or 'null' if the Variant represents 'null'. 
    ** Throws `FancomErr` if the Variant is not an 'Int' type.
    Int? asInt() {
        if (isNull)
            return null
        if (!isInt)
            throw FancomErr(Messages.wrongVariantType(this, Int#))
        return variant.changeType(VarType.VT_I8.vt).getLong
    }

    ** Returns the Variant value as a 'Str' or 'null' if the Variant represents 'null'. 
    ** Throws `FancomErr` if the Variant is not a 'Str' type.
    Str? asStr() {
        if (isNull)
            return null
        if (!isStr)
            throw FancomErr(Messages.wrongVariantType(this, Str#))
        return variant.changeType(VarType.VT_BSTR.vt).getString
    }

    ** A Swiss Army knife this method is; attempts to convert the Variant value into the given Type, be 
    ** it a 'Bool', 'Enum' or a Fancom wrapper. Throws `FancomErr` if unsuccessful.
    Obj? asType(Type type) {
        if (isNull)
            return null
        return Helper.toFantomObj(this, type) ?: throw FancomErr(Messages.wrongVariantType(this, type))
    }

    
    // ---- Misc Methods --------------------------------------------------------------------------

    ** Returns 'true' if this Variant is a 'Vector' value.
    Bool isVector() {
        variant.getvt.and(VT_VECTOR) > 0
    }

    ** Returns 'true' if this Variant represents an array.
    Bool isArray() {
        variant.getvt.and(VT_ARRAY) > 0
    }
    
    ** Returns 'true' if this Variant is a 'ByRef' value.
    Bool isByRef() {
        variant.getvt.and(VT_BYREF) > 0
    }
    
    ** Returns details of the underlying COM value.
    override Str toStr() {
        // toString() on a safe(!) byte array gave errors, so we just disp '...' instead
        typeName  := varType.name
        byRef     := isByRef ? "*" : ""
        vector    := isVector ? "()" : ""
        return isArray ? "(${varType.name}[])..." : "(${varType.name}${vector}${byRef})$variant.toString"
    }
    
    internal static native JVariant fromLong(Int value)
}