//-------------------------------------------------------------------------------------------------------
// Project: NodeActiveX
// Author: Yuri Dursin
// Description: DispObject class declarations. This class incapsulates COM IDispatch interface to Node JS Object
//-------------------------------------------------------------------------------------------------------

#pragma once

#include "utils.h"

enum options_t { 
    option_none = 0, 
    option_async = 0x0001, 
    option_type = 0x0002,
    option_activate = 0x0004,
    option_prepared = 0x0100,
    option_owned = 0x0200,
    option_property = 0x0400,
    option_function_simple = 0x0800,
    option_mask = 0x00FF,
    option_auto = (option_async | option_type)
};

inline bool TypeInfoGetName(ITypeInfo *info, DISPID dispid, BSTR *name) {
    HRESULT hrcode = info->GetDocumentation(dispid, name, NULL, NULL, NULL);
    if SUCCEEDED(hrcode) return true;
    UINT cnt_ret;
    return info->GetNames(dispid, name, 1, &cnt_ret) == S_OK && cnt_ret > 0;
}

template<typename T>
bool TypeInfoPrepareFunc(ITypeInfo *info, UINT n, T process) {
    FUNCDESC *desc;
    if (info->GetFuncDesc(n, &desc) != S_OK) return false;
    process(info, desc, nullptr);
    info->ReleaseFuncDesc(desc);
    return true;
}

template<typename T>
bool TypeInfoPrepareVar(ITypeInfo *info, UINT n, T process) {
    VARDESC *desc;
    if (info->GetVarDesc(n, &desc) != S_OK) return false;
    process(info, nullptr, desc);
    info->ReleaseVarDesc(desc);
    return true;
}

template<typename T>
void TypeInfoPrepare(ITypeInfo *info, int mode, T process) {
    UINT cFuncs = 0, cVars = 0;
    TYPEATTR *pattr = NULL;
    if (info->GetTypeAttr(&pattr) == S_OK) {
        cFuncs = pattr->cFuncs;
        cVars = pattr->cVars;
        info->ReleaseTypeAttr(pattr);
    }
    if ((mode & 1) != 0) {
        for (UINT n = 0; n < cFuncs; n++) {
            TypeInfoPrepareFunc<T>(info, n, process);
        }
    }
    if ((mode & 2) != 0) {
        for (UINT n = 0; n < cVars; n++) {
            TypeInfoPrepareVar<T>(info, n, process);
        }
    }
}

template<typename T>
bool TypeLibEnumerate(ITypeLib *typelib, int mode, T process) {
    UINT i, cnt = typelib ? typelib->GetTypeInfoCount() : 0;
    for (i = 0; i < cnt; i++) {
        CComPtr<ITypeInfo> info;
        if (typelib->GetTypeInfo(i, &info) != S_OK) continue;
        TypeInfoPrepare<T>(info, mode, process);
    }
    return cnt > 0;
}

class DispInfo {
public:
    std::weak_ptr<DispInfo> parent;
    CComPtr<IDispatch> ptr;
    std::wstring name;
    int options;
    bool bManaged;

    struct type_t { 
        DISPID dispid; 
        int kind; 
        int argcnt_get; 
        inline type_t(DISPID dispid_, int kind_) : dispid(dispid_), kind(kind_), argcnt_get(0) {}
        inline bool is_property() const { return ((kind & INVOKE_FUNC) == 0); }
        inline bool is_property_simple() const { return (((kind & (INVOKE_PROPERTYGET | INVOKE_FUNC))) == INVOKE_PROPERTYGET) && (argcnt_get == 0); }
        inline bool is_function_simple() const { return (((kind & (INVOKE_PROPERTYGET | INVOKE_FUNC))) == INVOKE_FUNC) && (argcnt_get == 0); }
        inline bool is_property_advanced() const { return kind == (INVOKE_PROPERTYGET | INVOKE_PROPERTYPUT) && (argcnt_get == 1); }
    };
    typedef std::shared_ptr<type_t> type_ptr;
    typedef std::map<DISPID, type_ptr> types_by_dispid_t;
    types_by_dispid_t types_by_dispid;

    inline DispInfo(IDispatch *disp, const std::wstring &nm, int opt, std::shared_ptr<DispInfo> *parnt = nullptr)
        : ptr(disp), options(opt), name(nm), bManaged(false)
    { 
        if (parnt) parent = *parnt;
        if ((options & option_type) != 0)
            Prepare(disp);
    }

    void Prepare(IDispatch *disp) {
        Enumerate(1/*functions only*/, &bManaged, [this](ITypeInfo *info, FUNCDESC *func, VARDESC *var) {
            type_ptr &ptr = this->types_by_dispid[func->memid];
            if (!ptr) ptr.reset(new type_t(func->memid, func->invkind));
            else ptr->kind |= func->invkind;
            if ((func->invkind & INVOKE_PROPERTYGET) != 0) {
                if (func->cParams > ptr->argcnt_get)
                    ptr->argcnt_get = func->cParams;
            }
        });
        bool prepared = types_by_dispid.size() > 3; // QueryInterface, AddRef, Release
        if (prepared) options |= option_prepared;
    }

    inline bool GetTypeInfo(const DISPID dispid, type_ptr &info) {
        if ((options & option_prepared) == 0) return false;
        types_by_dispid_t::const_iterator it = types_by_dispid.find(dispid);
        if (it == types_by_dispid.end()) return false;
        info = it->second;
        return true;
    }

    HRESULT FindProperty(LPOLESTR name, DISPID *dispid) {
        return DispFind(ptr, name, dispid);
    }

    HRESULT GetProperty(DISPID dispid, LONG argcnt, VARIANT *args, VARIANT *value, EXCEPINFO *except = 0) {
        HRESULT hrcode = DispInvoke(ptr, dispid, argcnt, args, value, DISPATCH_PROPERTYGET, except);
        return hrcode;
    }

    HRESULT GetProperty(DISPID dispid, LONG index, VARIANT *value, EXCEPINFO *except = 0) {
        CComVariant arg(index);
        LONG argcnt = (index >= 0) ? 1 : 0;
        return DispInvoke(ptr, dispid, argcnt, &arg, value, DISPATCH_PROPERTYGET, except);
    }

    HRESULT SetProperty(DISPID dispid, LONG argcnt, VARIANT *args, VARIANT *value, EXCEPINFO *except = 0) {
        HRESULT hrcode = DispInvoke(ptr, dispid, argcnt, args, value, DISPATCH_PROPERTYPUT, except);
        if FAILED(hrcode) value->vt = VT_EMPTY;
        return hrcode;
    }

    HRESULT ExecuteMethod(DISPID dispid, LONG argcnt, VARIANT *args, VARIANT *value, EXCEPINFO *except = 0) {
        HRESULT hrcode = DispInvoke(ptr, dispid, argcnt, args, value, DISPATCH_METHOD, except);
        return hrcode;
    }

    template<typename T>
    bool Enumerate(int mode, bool* checkManaged, T process = nullptr) {
        UINT i, cnt;
        CComPtr<ITypeLib> prevtypelib;
        if (!ptr || FAILED(ptr->GetTypeInfoCount(&cnt))) cnt = 0;
        else for (i = 0; i < cnt; i++) {
            CComPtr<ITypeInfo> info;
            if (ptr->GetTypeInfo(i, 0, &info) != S_OK) continue;

            // Query type library
            UINT typeindex;
            CComPtr<ITypeLib> typelib;
            if (info->GetContainingTypeLib(&typelib, &typeindex) == S_OK) {

                // Check if typelib is managed
                if (checkManaged != nullptr && !*checkManaged) {
                    CComPtr<ITypeLib2> typeLib2;
                    if (SUCCEEDED(typelib->QueryInterface(IID_ITypeLib2, (void**)&typeLib2))) {

                        // {90883F05-3D28-11D2-8F17-00A0C9A6186D}
                        static const GUID GUID_ExportedFromComPlus = { 0x90883F05, 0x3D28, 0x11D2, { 0x8F, 0x17, 0x00, 0xA0, 0xC9, 0xA6, 0x18, 0x6D } };

                        CComVariant cv;
                        if (SUCCEEDED(typeLib2->GetCustData(GUID_ExportedFromComPlus, &cv))) {
                            *checkManaged = true;
                        }
                    }
                }

                // Enumerate all types in library types
                // May be very slow! need a special method
                // if (typelib != prevtypelib) {
                //     TypeLibEnumerate<T>(typelib, mode, process);
                //	   prevtypelib.Attach(typelib.Detach());
                // }

                CComPtr<ITypeInfo> tinfo;
                if (typelib->GetTypeInfo(typeindex, &tinfo) == S_OK) {
                    TypeInfoPrepare<T>(tinfo, mode, process);
                }
            }

            // Process types
            else {
                TypeInfoPrepare<T>(info, mode, process);
            }
        }
        return cnt > 0;
    }
};

typedef std::shared_ptr<DispInfo> DispInfoPtr;

class DispObject: public NodeObject
{
public:
    DispObject(const DispInfoPtr &ptr, const std::wstring &name, DISPID id = DISPID_UNKNOWN, LONG indx = -1, int opt = 0);
    ~DispObject();

    static Persistent<ObjectTemplate> inst_template;
    static Persistent<FunctionTemplate> clazz_template;
    static NodeMethods clazz_methods;

    static void NodeInit(const Local<Object> &target, Isolate* isolate, Local<Context> &ctx);
    static bool HasInstance(Isolate *isolate, const Local<Value> &obj) {
        Local<FunctionTemplate> clazz = clazz_template.Get(isolate);
        return !clazz.IsEmpty() && clazz->HasInstance(obj);
    }
    static DispObject *GetPtr(Isolate *isolate, const Local<Object> &obj) {
        Local<FunctionTemplate> clazz = clazz_template.Get(isolate);
        if (clazz.IsEmpty() || !clazz->HasInstance(obj)) return nullptr;
        return Unwrap<DispObject>(obj);
    }
    static IDispatch *GetDispPtr(Isolate *isolate, const Local<Object> &obj) {
        DispObject *self = GetPtr(isolate, obj);
        return (self && self->disp) ? self->disp->ptr : nullptr;
    }
    static bool GetValueOf(Isolate *isolate, const Local<Object> &obj, VARIANT &value) {
        DispObject *self = GetPtr(isolate, obj);
        return self && SUCCEEDED(self->valueOf(isolate, value, false));
    }
    static Local<Object> NodeCreate(Isolate *isolate, IDispatch *disp, const std::wstring &name, int opt) {
        Local<Object> parent;
        DispInfoPtr ptr(new DispInfo(disp, name, opt));
        return DispObject::NodeCreate(isolate, parent, ptr, name);
    }

private:
    static Local<Object> NodeCreate(Isolate *isolate, const Local<Object> &parent, const DispInfoPtr &ptr, const std::wstring &name, DISPID id = DISPID_UNKNOWN, LONG indx = -1, int opt = 0);

    static void NodeCreate(const FunctionCallbackInfo<Value> &args);
    static void NodeValueOf(const FunctionCallbackInfo<Value> &args);
    static void NodeToString(const FunctionCallbackInfo<Value> &args);
    static void NodeRelease(const FunctionCallbackInfo<Value> &args);
    static void NodeCast(const FunctionCallbackInfo<Value> &args);
    static void NodeGet(Local<Name> name, const PropertyCallbackInfoGetter &args);
    static void NodeSet(Local<Name> name, Local<Value> value, const PropertyCallbackInfoSetter &args);
    static void NodeGetByIndex(uint32_t index, const PropertyCallbackInfoGetter&args);
    static void NodeSetByIndex(uint32_t index, Local<Value> value, const PropertyCallbackInfoSetter &args);
    static void NodeCall(const FunctionCallbackInfo<Value> &args);

#ifdef NODE_INTERCEPTED
    static inline Intercepted InterceptedNodeGet(Local<Name> name, const PropertyCallbackInfoGetter& args) {
        NodeGet(name, args);
        return Intercepted::kYes;
    }
    static inline Intercepted InterceptedNodeSet(Local<Name> name, Local<Value> value, const PropertyCallbackInfoSetter& args) {
        NodeSet(name, value, args);
        return Intercepted::kYes;
    }
    static inline Intercepted InterceptedNodeGetByIndex(uint32_t index, const PropertyCallbackInfoGetter& args) {
        NodeGetByIndex(index, args);
        return Intercepted::kYes;
    }
    static inline Intercepted InterceptedNodeSetByIndex(uint32_t index, Local<Value> value, const PropertyCallbackInfoSetter& args) {
        NodeSetByIndex(index, value, args);
        return Intercepted::kYes;
    }
#endif

    static void NodeConnectionPoints(const FunctionCallbackInfo<Value> &args);
    static void PeakAndDispatchMessages(const FunctionCallbackInfo<Value> &args);

protected:
    bool release();
    bool get(LPOLESTR tag, LONG index, const PropertyCallbackInfoGetter &args);
    bool set(LPOLESTR tag, LONG index, const Local<Value> &value, const PropertyCallbackInfoSetter &args);
    void call(Isolate *isolate, const FunctionCallbackInfo<Value> &args);

    HRESULT valueOf(Isolate *isolate, VARIANT &value, bool simple);
    HRESULT valueOf(Isolate *isolate, const Local<Object> &self, Local<Value> &value);
    void toString(const FunctionCallbackInfo<Value> &args);
    Local<Value> getIdentity(Isolate *isolate);

private:
    int options;
    inline bool is_null() { return !disp; }
    inline bool is_prepared() { return (options & option_prepared) != 0; }
    inline bool is_object() { return dispid == DISPID_VALUE /*&& index < 0*/; }
    inline bool is_owned() { return (options & option_owned) != 0; }

    Persistent<Value> items, methods, vars;
    void initTypeInfo(Isolate *isolate);

    DispInfoPtr disp;
    std::wstring name;
    DISPID dispid;
    LONG index;

    HRESULT prepare();
};

class VariantObject : public NodeObject
{
public:
    VariantObject() {};
    VariantObject(const VARIANT &v) : value(v) {};
    VariantObject(const FunctionCallbackInfo<Value> &args);

    static Persistent<ObjectTemplate> inst_template;
    static Persistent<FunctionTemplate> clazz_template;
    static NodeMethods clazz_methods;

    static void NodeInit(const Local<Object> &target, Isolate* isolate, Local<Context> &ctx);
    static bool HasInstance(Isolate *isolate, const Local<Value> &obj) {
        Local<FunctionTemplate> clazz = clazz_template.Get(isolate);
        return !clazz.IsEmpty() && clazz->HasInstance(obj);
    }
    static VariantObject *GetInstanceOf(Isolate *isolate, const Local<Object> &obj) {
        Local<FunctionTemplate> clazz = clazz_template.Get(isolate);
        if (clazz.IsEmpty() || !clazz->HasInstance(obj)) return nullptr;
        return Unwrap<VariantObject>(obj);
    }
    static bool GetValueOf(Isolate *isolate, const Local<Object> &obj, VARIANT &value) {
        Local<FunctionTemplate> clazz = clazz_template.Get(isolate);
        if (clazz.IsEmpty() || !clazz->HasInstance(obj)) return false;
        VariantObject *self = Unwrap<VariantObject>(obj);
        return self && SUCCEEDED(self->value.CopyTo(&value));
    }

    static Local<Object> NodeCreateInstance(const FunctionCallbackInfo<Value> &args);
    static void NodeCreate(const FunctionCallbackInfo<Value> &args);
    static Local<Object> NodeCreate(Isolate* isolate, const VARIANT& var);

    static void NodeClear(const FunctionCallbackInfo<Value> &args);
    static void NodeAssign(const FunctionCallbackInfo<Value> &args);
    static void NodeCast(const FunctionCallbackInfo<Value> &args);
    static void NodeValueOf(const FunctionCallbackInfo<Value> &args);
    static void NodeToString(const FunctionCallbackInfo<Value> &args);
    static void NodeGet(Local<Name> name, const PropertyCallbackInfoGetter &args);
    static void NodeSet(Local<Name> name, Local<Value> value, const PropertyCallbackInfoSetter&args);
    static void NodeGetByIndex(uint32_t index, const PropertyCallbackInfoGetter&args);
    static void NodeSetByIndex(uint32_t index, Local<Value> value, const PropertyCallbackInfoSetter&args);

#ifdef NODE_INTERCEPTED
    static inline Intercepted InterceptedNodeGet(Local<Name> name, const PropertyCallbackInfoGetter& args) {
        NodeGet(name, args);
        return Intercepted::kYes;
    }
    static inline Intercepted InterceptedNodeSet(Local<Name> name, Local<Value> value, const PropertyCallbackInfoSetter& args) {
        NodeSet(name, value, args);
        return Intercepted::kYes;
    }
    static inline Intercepted InterceptedNodeGetByIndex(uint32_t index, const PropertyCallbackInfoGetter& args) {
        NodeGetByIndex(index, args);
        return Intercepted::kYes;
    }
    static inline Intercepted InterceptedNodeSetByIndex(uint32_t index, Local<Value> value, const PropertyCallbackInfoSetter& args) {
        NodeSetByIndex(index, value, args);
        return Intercepted::kYes;
    }
#endif

private:
    CComVariant value, pvalue;
    bool assign(Isolate *isolate, Local<Value> &val, Local<Value> &type);
};

class ConnectionPointObject : public NodeObject
{
public:
    ConnectionPointObject(IConnectionPoint *p, IDispatch* d);
    ConnectionPointObject(const FunctionCallbackInfo<Value> &args) {}
    static Persistent<ObjectTemplate> inst_template;
    static Persistent<FunctionTemplate> clazz_template;
    static Local<Object> NodeCreateInstance(Isolate *isolate, IConnectionPoint *p, IDispatch* d);
    static void NodeInit(const Local<Object> &target, Isolate* isolate, Local<Context> &ctx);
    static void NodeCreate(const FunctionCallbackInfo<Value> &args);
    static void NodeAdvise(const FunctionCallbackInfo<Value> &args);
    static void NodeUnadvise(const FunctionCallbackInfo<Value> &args);
    static void NodeConnectionPointMethods(const FunctionCallbackInfo<Value> &args);

private:
    bool InitIndex();

    CComPtr<IConnectionPoint> ptr;
    CComPtr<IDispatch> disp;
    DispObjectImpl::index_t index;

    std::unordered_set<DWORD> cookies;

};
