/* Copyright (c) 2012 Tobias Wolf, All Rights Reserved
 *
 * The contents of this file is dual-licensed under 2 
 * alternative Open Source/Free licenses: LGPL 2.1 or later and 
 * Apache License 2.0. (starting with JNA version 4.0.0).
 * 
 * You can freely decide which license you want to apply to 
 * the project.
 * 
 * You may obtain a copy of the LGPL License at:
 * 
 * http://www.gnu.org/licenses/licenses.html
 * 
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "LGPL2.1".
 * 
 * You may obtain a copy of the Apache License at:
 * 
 * http://www.apache.org/licenses/
 * 
 * A copy is also included in the downloadable source code package
 * containing JNA, in file "AL2.0".
 */
package com.sun.jna.platform.win32.COM;

import com.sun.jna.WString;
import com.sun.jna.platform.win32.Guid;
import com.sun.jna.platform.win32.Guid.CLSID;
import com.sun.jna.platform.win32.Guid.GUID;
import com.sun.jna.platform.win32.Guid.REFIID;
import com.sun.jna.platform.win32.Kernel32;
import com.sun.jna.platform.win32.OaIdl;
import com.sun.jna.platform.win32.OaIdl.DISPID;
import com.sun.jna.platform.win32.OaIdl.DISPIDByReference;
import com.sun.jna.platform.win32.OaIdl.EXCEPINFO;
import com.sun.jna.platform.win32.Ole32;
import com.sun.jna.platform.win32.OleAuto;
import com.sun.jna.platform.win32.OleAuto.DISPPARAMS;
import com.sun.jna.platform.win32.Variant.VARIANT;
import com.sun.jna.platform.win32.Variant.VariantArg;
import com.sun.jna.platform.win32.WTypes;
import com.sun.jna.platform.win32.WinDef;
import com.sun.jna.platform.win32.WinDef.LCID;
import com.sun.jna.platform.win32.WinDef.UINT;
import com.sun.jna.platform.win32.WinNT.HRESULT;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.PointerByReference;

/**
 * Helper class to provide basic COM support.
 *
 * @author Tobias Wolf, wolf.tobias@gmx.net
 */
public class COMBindingBaseObject extends COMInvoker {

    /** The Constant LOCALE_USER_DEFAULT. */
    public final static LCID LOCALE_USER_DEFAULT = Kernel32.INSTANCE
            .GetUserDefaultLCID();

    /** The Constant LOCALE_SYSTEM_DEFAULT. */
    public final static LCID LOCALE_SYSTEM_DEFAULT = Kernel32.INSTANCE
            .GetSystemDefaultLCID();

    /** The i unknown. */
    private IUnknown iUnknown;

    /** The i dispatch. */
    private IDispatch iDispatch;

    /** IDispatch interface reference. */
    private PointerByReference pDispatch = new PointerByReference();

    /** IUnknown interface reference. */
    private PointerByReference pUnknown = new PointerByReference();

    public COMBindingBaseObject(IDispatch dispatch) {
        // transfer the value
        this.iDispatch = dispatch;
    }

    public COMBindingBaseObject(CLSID clsid, boolean useActiveInstance) {
        this(clsid, useActiveInstance, WTypes.CLSCTX_SERVER);
    }

    public COMBindingBaseObject(CLSID clsid, boolean useActiveInstance,
            int dwClsContext) {
        assert COMUtils.comIsInitialized() : "COM not initialized";
        
        init(useActiveInstance, clsid, dwClsContext);
    }

    public COMBindingBaseObject(String progId, boolean useActiveInstance,
            int dwClsContext) throws COMException {
        assert COMUtils.comIsInitialized() : "COM not initialized";

        CLSID.ByReference clsid = new CLSID.ByReference();
        HRESULT hr = Ole32.INSTANCE.CLSIDFromProgID(progId, clsid);

        COMUtils.checkRC(hr);
        
        init(useActiveInstance, clsid, dwClsContext);
    }

    public COMBindingBaseObject(String progId, boolean useActiveInstance)
            throws COMException {
        this(progId, useActiveInstance, WTypes.CLSCTX_SERVER);
    }

    private void init(boolean useActiveInstance, GUID clsid, int dwClsContext) throws COMException {
        HRESULT hr;
        if (useActiveInstance) {
            hr = OleAuto.INSTANCE.GetActiveObject(clsid, null, this.pUnknown);

            if (COMUtils.SUCCEEDED(hr)) {
                this.iUnknown = new Unknown(this.pUnknown.getValue());
                hr = iUnknown.QueryInterface(new REFIID( IDispatch.IID_IDISPATCH),
                        this.pDispatch);
            } else {
                hr = Ole32.INSTANCE.CoCreateInstance(clsid, null, dwClsContext,
                        IDispatch.IID_IDISPATCH, this.pDispatch);
            }
        } else {
            hr = Ole32.INSTANCE.CoCreateInstance(clsid, null, dwClsContext,
                    IDispatch.IID_IDISPATCH, this.pDispatch);
        }
        
        COMUtils.checkRC(hr);
        
        this.iDispatch = new Dispatch(this.pDispatch.getValue());
    }
    
    /**
     * Gets the i dispatch.
     *
     * @return the i dispatch
     */
    public IDispatch getIDispatch() {
        return iDispatch;
    }

    /**
     * Gets the i dispatch pointer.
     *
     * @return the i dispatch pointer
     */
    public PointerByReference getIDispatchPointer() {
        return pDispatch;
    }

    /**
     * Gets the i unknown.
     *
     * @return the i unknown
     */
    public IUnknown getIUnknown() {
        return iUnknown;
    }

    /**
     * Gets the i unknown pointer.
     *
     * @return the i unknown pointer
     */
    public PointerByReference getIUnknownPointer() {
        return pUnknown;
    }

    /**
     * Release.
     */
    public void release() {
        if (this.iDispatch != null) {
            this.iDispatch.Release();
        }
    }

    protected HRESULT oleMethod(int nType, VARIANT.ByReference pvResult,
            IDispatch pDisp, String name, VARIANT[] pArgs) throws COMException {

        if (pDisp == null)
            throw new COMException("pDisp (IDispatch) parameter is null!");

        // variable declaration
        WString[] ptName = new WString[] { new WString(name) };
        DISPIDByReference pdispID = new DISPIDByReference();

        // Get DISPID for name passed...
        HRESULT hr = pDisp.GetIDsOfNames(new REFIID(Guid.IID_NULL), ptName, 1,
                LOCALE_USER_DEFAULT, pdispID);

        COMUtils.checkRC(hr);

        return this
                .oleMethod(nType, pvResult, pDisp, pdispID.getValue(), pArgs);
    }

    protected HRESULT oleMethod(int nType, VARIANT.ByReference pvResult,
            IDispatch pDisp, DISPID dispId, VARIANT[] pArgs)
            throws COMException {

        if (pDisp == null)
            throw new COMException("pDisp (IDispatch) parameter is null!");

        // variable declaration
        int _argsLen = 0;
        VARIANT[] _args = null;
        DISPPARAMS.ByReference dp = new DISPPARAMS.ByReference();
        EXCEPINFO.ByReference pExcepInfo = new EXCEPINFO.ByReference();
        IntByReference puArgErr = new IntByReference();

        // make parameter reverse ordering as expected by COM runtime
        if ((pArgs != null) && (pArgs.length > 0)) {
            _argsLen = pArgs.length;
            _args = new VARIANT[_argsLen];

            int revCount = _argsLen;
            for (int i = 0; i < _argsLen; i++) {
                _args[i] = pArgs[--revCount];
            }
        }

        // Handle special-case for property-puts!
        if (nType == OleAuto.DISPATCH_PROPERTYPUT) {
            dp.setRgdispidNamedArgs(new DISPID[] {OaIdl.DISPID_PROPERTYPUT});
        }

        // Build DISPPARAMS
        if (_argsLen > 0) {
            dp.setArgs(_args);

            // write 'DISPPARAMS' structure to memory
            dp.write();
        }

        // Apply "fix" according to
        // https://www.delphitools.info/2013/04/30/gaining-visual-basic-ole-super-powers/
        // https://msdn.microsoft.com/en-us/library/windows/desktop/ms221486(v=vs.85).aspx
        //
        // Summary: there are methods in the word typelibrary that require both
        // PROPERTYGET _and_ METHOD to be set. With only one of these set the call
        // fails.
        //
        // The article from delphitools argues, that automation compatible libraries
        // need to be compatible with VisualBasic which does not distingish methods
        // and property getters and will set both flags always.
        //
        // The MSDN article advises this behaviour: "[...] Some languages cannot 
        // distinguish between retrieving a property and calling a method. In this 
        //case, you should set the flags DISPATCH_PROPERTYGET and DISPATCH_METHOD.
        // [...]"))
        //
        // This was found when trying to bind InchesToPoints from the _Application 
        // dispatch interface of the MS Word 15 type library
        //
        // The signature according the ITypeLib Viewer (OLE/COM Object Viewer):
        // [id(0x00000172), helpcontext(0x09700172)]
        // single InchesToPoints([in] single Inches);

        final int finalNType;
        if (nType == OleAuto.DISPATCH_METHOD || nType == OleAuto.DISPATCH_PROPERTYGET) {
            finalNType = OleAuto.DISPATCH_METHOD | OleAuto.DISPATCH_PROPERTYGET;
        } else {
            finalNType = nType;
        }

        // Make the call!
        HRESULT hr = pDisp.Invoke(dispId, new REFIID(Guid.IID_NULL), LOCALE_SYSTEM_DEFAULT,
                new WinDef.WORD(finalNType), dp, pvResult, pExcepInfo, puArgErr);

        COMUtils.checkRC(hr, pExcepInfo, puArgErr);
        return hr;
    }

    /**
     * Ole method.
     *
     * @param nType
     *            the n type
     * @param pvResult
     *            the pv result
     * @param pDisp
     *            the disp
     * @param name
     *            the name
     * @param pArg
     *            the arg
     * @return the hresult
     * @throws COMException
     *             the cOM exception
     */
    protected HRESULT oleMethod(int nType, VARIANT.ByReference pvResult,
            IDispatch pDisp, String name, VARIANT pArg) throws COMException {

        return this.oleMethod(nType, pvResult, pDisp, name,
                new VARIANT[] { pArg });
    }

    protected HRESULT oleMethod(int nType, VARIANT.ByReference pvResult,
            IDispatch pDisp, DISPID dispId, VARIANT pArg) throws COMException {

        return this.oleMethod(nType, pvResult, pDisp, dispId,
                new VARIANT[] { pArg });
    }

    /**
     * Ole method.
     *
     * @param nType
     *            the n type
     * @param pvResult
     *            the pv result
     * @param pDisp
     *            the disp
     * @param name
     *            the name
     * @return the hresult
     * @throws COMException
     *             the cOM exception
     */
    protected HRESULT oleMethod(int nType, VARIANT.ByReference pvResult,
            IDispatch pDisp, String name) throws COMException {

        return this.oleMethod(nType, pvResult, pDisp, name, (VARIANT[]) null);
    }

    protected HRESULT oleMethod(int nType, VARIANT.ByReference pvResult,
            IDispatch pDisp, DISPID dispId) throws COMException {

        return this.oleMethod(nType, pvResult, pDisp, dispId, (VARIANT[]) null);
    }

    /**
     * Check failed.
     *
     * @param hr
     *            the hr
     */
    protected void checkFailed(HRESULT hr) {
        COMUtils.checkRC(hr, null, null);
    }
}
