# ComObject.java

    /*
    ** Module   : ComObject.java
    ** Abstract : The base class for all COM objects, with minimal management.
    **
    ** Copyright (c) 2017-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --------------------------------Description-----------------------------------
    ** 001 OM  20170530 First commit.
    ** 002 CA  20171026 Changes for native COM automation support.
    ** 003 CA  20171102 The comhandle must use the comId as its ID reported by the id() API.
    **     EVL 20171110 Fix for array out of index exception issue when merging OUTPUT or INPUT-OUTPUT
    **                  parameters back to Java side.
    ** 004 CA  20180517 Fixed comhandle FWD-specific resource management.
    ** 005 OM  20190223 Added support for indexed properties. Fixed properties signature lookup.
    **                  Added ref-counting support.
    ** 006 HC  20190816 Various fixes to COM extension objects - mostly loading and invocation.
    ** 007 CA  20191212 Fixed constructor lookup during marshal (look for super-class or interface
    **                  match).
    ** 008 MAG 20191024 Fix NPE bug in call() method when params argument is null.
    ** 009 VVT 20200203 The default implementations for the valid() and delete() methods were added;
    **                  An internal cache is used for method lookup.
    **                  The internal xcall() method introduced to handle all COM method- and property-
    **                  related calls.
    ** 010 VVT 20200501 convertInputParameter: javadoc added; input parameter renamed to better match
    **                  its purpose; arguments of the BaseDataType are now treated specially if the
    **                  destination type is Object.
    **     VVT 20200722 The decimal 4gl type can now be converted to Java native integer types.
    **     VVT 20200723 Direct output to stdout replaced by log calls in the xcall() method.
    ** 011 CA  20200924 Replaced Method.invoke with ReflectASM.
    **     VVT 20201015 Logging of remote calls has been optimized. Also the call times are also logged now.
    **     CA  20210218 Arithmetic operations with integer operands are evaluated as int64: changed to allow
    **                  conversion of integer arguments.
    **     VVT 20211125 convertInputParameter() now is complete and can be accessed from Unit tests.
    **     VVT 20211126 convertInputParameter(): conversion decimal -> Object support added.
    **     HC  20211201 Added support for runtime conversion of converted OCX object references to comhandle.
    **     HC  20220421 Added optional method and property resolution without the need to annotate methods with
    **                  ComMethod or ComProperty annotations.
    **     VVT 20220427 Javadocs minor fixes.
    **     VVT 20220720 Access to getComObjectInstance() is now public to allow COM parameter conversion.
    **                  See #6244.
    **     TJD 20220504 Java 11 compatibility minor changes
    **     VVT 20230203 marshal() method: added support for LocalDate. See #6953.
    ** 012 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
    ** 013 GBB 20230602 Lowered logging level of debug messages.
    ** 014 CA  20240331 Use logical constants for TRUE, FALSE, UNKNOWN, within internal FWD runtime, so logical
    **                  type checks must include sub-classes, too.
    ** 015 CA  20240918 Removed default methods from interfaces and moved them as concrete implementations, as
    **                  default methods cause the Java metaspace to spike one order of magnitude.
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.comauto;

    import java.lang.annotation.*;
    import java.lang.reflect.*;
    import java.time.*;
    import java.time.format.*;
    import java.util.*;
    import java.util.concurrent.atomic.*;
    import java.util.function.*;
    import java.util.logging.*;

    import com.goldencode.p2j.ui.*;
    import com.goldencode.p2j.util.*;
    import com.goldencode.p2j.util.logging.*;
    import com.google.common.collect.*;

    /**
     * Base class for COM objects that will be stored using {@link comhandle}s. Does minimal
     * management of the COM Object.
     */
    public abstract class ComObject
    implements WrappedResource,
               Deletable
    {
       /** Cache of Java methods implementing COM properties. */
       protected static final Map<String, Multimap<String, Method>> propertyTable = new HashMap<>();

       /** Cache of Java methods implementing COM methods. */
       protected static final Map<String, Multimap<String, Method>> methodTable = new HashMap<>();

       /**
        * When set to {@code true} the method and property resolution mechanism will include also methods not
        * annotated with {@linkplain ComMethod} or {@linkplain ComProperty}.
        */
       protected boolean includeUnannotatedMethods;

       /**
        * The resource id. Used by {@code WrappedResource} management.
        */
       private Long resourceId = null;

       /**
        * The parent {@code ControlFrameWidget} widget. This container is used during the dispatch of
        * the events generated by this COM Object.
        */
       private ControlFrameWidget parentControlFrame = null;

       /** The legacy name of this com object. */
       private String name = "unknown";

       /**
        * The creator of this object. Cannot be {@code null}. Used for locating the event callbacks
        * as its internal procedure. When becomes invalid, this object also gets invalid and should be
        * deleted.
        */
       private handle parentProcedure = null;

       /** Counter for {@link comhandle}s that refer this object. */
       private AtomicInteger refcount = new AtomicInteger(1);

       /** The logger for COM methods. Should be public. Used by a customer code. */
       public final static CentralLogger LOGGER = CentralLogger.get(ComObject.class.getName());

       /** The class for void Java type */
       private static final Class<?> VOID = Void.TYPE;

       /** Build a new COM Object. Calls {@code init()} to initialize internal data structure. */
       public ComObject()
       {
          init();
       }

       /**
        * Initialize internal data structure of a fresh COM: the parent procedure is detected.
        */
       protected void init()
       {
          parentProcedure = ProcedureManager.thisProcedure();
       }

       /**
        * Converts a value to a BDT object. The method does not check whether the value is already a
        * {@code BaseDataType}, in fact is assume it is not. Note that, because this method uses the
        * best matching constructor, this method may do some implicit conversions if the passed in
        * value is compatible with the class required (like from a {@code String}).
        *
        * @param   aVal
        *          The value to be converted.
        *
        * @return  A {@code BaseDataType} instance that represent the same value as {@code aVal}.
        */
       protected static BaseDataType marshal(Object aVal)
       {
          if (aVal != null)
          {
             if (aVal instanceof BaseDataType)
             {
                return (BaseDataType) aVal;
             }
             else if (aVal instanceof String)
             {
                return new character((String) aVal);
             }
             else if (aVal instanceof Boolean)
             {
                return new logical((Boolean) aVal);
             }
             else if (aVal instanceof Integer)
             {
                return new integer((Integer) aVal);
             }
             else if (aVal instanceof Long)
             {
                return new int64((Long) aVal);
             }
             else if (aVal instanceof Double)
             {
                return new decimal((Double) aVal);
             }
             else if (aVal instanceof Float)
             {
                return new decimal((Float) aVal);
             }
             else if (aVal instanceof Date)
             {
                return new date((Date) aVal);
             }
             else if (aVal instanceof Enum)
             {
                return new integer(((Enum<?>) aVal).ordinal());
             }
             else if (aVal instanceof LocalDate)
             {
                final LocalDate localDate = (LocalDate) aVal;
                return new date(localDate.getMonthValue(),
                         localDate.getDayOfMonth(),
                         localDate.getYear(),
                         false);
             }
          }

          return new comhandle();
       }

       /**
        * Return true by default.
        */
       @Override
       public boolean valid()
       {
          return true;
       }

       /**
        * Do nothing by default
        */
       @Override
       public void delete()
       {
          // no-op
       }

       /**
        * Check if the resource can veto the delete or not, before attempting the {@link #delete()} call.
        *
        * @param    ref
        *           The exact handle holding the reference, on which the delete is attempted.
        * @param    explicit
        *           Flag indicating this is called via <code>DELETE OBJECT</code> from the application.
        *
        * @return   <code>false</code> if the delete is vetoed.
        */
       @Override
       public boolean allowDelete(handle ref, boolean explicit)
       {
          return true;
       }

       /**
        * Converts a value from a {@link ComParameter}, {@link BaseDataType} or other Java type to
        * a {@link ComParameter} instance which can be passed as an argument to a COM method call.
        *
        * @param   aVal
        *          The value to be converted.
        *
        * @return  A {@code ComParameter} instance.
        *
        * @throws  IllegalArgumentException
        *          If the value can't be passed by FWD as an argument for a COM method call.
        */
       protected static ComParameter unmarshal(Object aVal)
       {
          if (aVal == comhandle.EMPTY)
          {
             return null;
          }

          boolean extent = false;

          if (aVal instanceof ComParameter)
          {
             ComParameter aparam = (ComParameter) aVal;
             extent = aparam.isExtent();

             if (aparam.isOutput())
             {
                // replace the BDT instance for OUTPUT parameters and set it to default value
                ComParameter oparam = new ComParameter(aparam);

                Object value;

                if (extent)
                {
                   Class<?> cls = oparam.getValue().getClass().getComponentType();

                   value = Array.newInstance(cls, oparam.getLength());
                   for (int i = 0; i < oparam.getLength(); i++)
                   {
                      Array.set(value, i, BaseDataType.generateDefault(cls));
                   }
                }
                else
                {
                   value = ((BaseDataType) aparam.getValue()).instantiateDefault();
                }
                oparam.setValue(value);

                aVal = oparam;
             }
          }

          String[] fwdDataType = new String[1];

          Function<Object, Object> process = (value) -> {
             if (value instanceof BaseDataType)
             {
                BaseDataType bdt = (BaseDataType) value;
                fwdDataType[0] = bdt.getTypeName();

                if (bdt.isUnknown() || bdt instanceof unknown)
                {
                   value = null;
                }
                else if (bdt instanceof memptr)
                {
                   value = ((memptr) bdt).getPointerValue().longValue();
                }
                else if (bdt instanceof raw)
                {
                   value = ((raw) bdt).getByteArray();
                }
                else if (bdt instanceof comhandle)
                {
                   ComObject com = ((comhandle) bdt).getResource();

                   if (com instanceof NativeComObject)
                   {
                      value = ((NativeComObject) com).getComId();
                   }
                   else
                   {
                      throw new IllegalArgumentException(
                               "FWD COM implementations can't be invoked on native side!");
                   }
                }
                else if (bdt instanceof date || bdt instanceof datetime || bdt instanceof datetimetz)
                {
                   value = ((date) bdt).dateValue();
                }
                else if (bdt instanceof handle)
                {
                   throw new IllegalArgumentException(
                            "HANDLE data-type is not supported by native COM arguments!");
                }
                else if (bdt instanceof logical)
                {
                   value = ((logical) bdt).booleanValue();
                }
                else if (bdt instanceof decimal)
                {
                   value = ((decimal) bdt).doubleValue();
                }
                else if (bdt instanceof recid)
                {
                   throw new IllegalArgumentException(
                            "RECID data-type is not supported by native COM arguments!");
                }
                else if (bdt instanceof integer)
                {
                   value = ((integer) bdt).intValue();
                }
                else if (bdt instanceof int64)
                {
                   value = ((int64) bdt).longValue();
                }
                else if (bdt instanceof rowid)
                {
                   throw new IllegalArgumentException(
                            "ROWID data-type is not supported by native COM arguments!");
                }
                else if (bdt instanceof Text)
                {
                   value = ((Text) bdt).getValue();
                }
                else
                {
                   throw new IllegalArgumentException(
                            fwdDataType[0] + " data-type is not supported by native COM arguments!");
                }
             }
             else
             {
                if (value == null)
                {
                   fwdDataType[0] = "unknown";
                }
                if (value instanceof String)
                {
                   fwdDataType[0] = "character";
                }
                else if (value instanceof Boolean)
                {
                   fwdDataType[0] = "logical";
                }
                else if (value instanceof Integer)
                {
                   fwdDataType[0] = "integer";
                }
                else if (value instanceof Long)
                {
                   fwdDataType[0] = "int64";
                }
                else if (value instanceof Double)
                {
                   fwdDataType[0] = "decimal";
                }
                else if (value instanceof Float)
                {
                   fwdDataType[0] = "decimal";
                }
                else if (value instanceof Date)
                {
                   fwdDataType[0] = "date";
                }
                else
                {
                   throw new IllegalArgumentException(value.getClass()
                            + " data-type is not supported by native COM arguments!");
                }
             }

             return value;
          };

          ComParameter ret;
          Object value;

          if (aVal instanceof ComParameter)
          {
             ComParameter cparam = (ComParameter) aVal;

             ret = new ComParameter(cparam);
             ret.setByPointer(cparam.isByPointer());
             ret.setByVariantPointer(cparam.isByVariantPointer());
             ret.setDataType(cparam.getDataType());
             ret.setMode(cparam.getMode());

             value = cparam.getValue();
          }
          else
          {
             ret = new ComParameter();
             ret.setByPointer(false);
             ret.setByVariantPointer(false);
             ret.setDataType(null);
             ret.setMode('I');

             value = aVal;
          }

          if (value != null && value.getClass().isArray())
          {
             ret.setExtent(true);
             ret.setLength(Array.getLength(value));
          }

          if (ret.isExtent())
          {
             Object avalue = Array.newInstance(Object.class, ret.getLength());

             for (int i = 0; i < ret.getLength(); i++)
             {
                Array.set(avalue, i, process.apply(Array.get(value, i)));
             }

             value = avalue;
          }
          else
          {
             value = process.apply(value);
          }

          ret.setFwdDataType(fwdDataType[0].toUpperCase());
          ret.setValue(value);

          return ret;
       }

       /**
        * Converts an array of values from a {@link ComParameter}, {@link BaseDataType} or other Java
        * type to a {@link ComParameter} instance which can be passed as an argument to a COM method
        * call.
        *
        * @param   aVals
        *          The values to be converted.
        *
        * @return  A {@code ComParameter} instance.
        *
        * @throws  IllegalArgumentException
        *          If the value can't be passed by FWD as an argument for a COM method call.
        */
       protected static ComParameter[] unmarshal(Object[] aVals)
       {
          // the parameter list can be empty
          if (aVals == null)
          {
             return null;
          }

          ComParameter[] result = new ComParameter[aVals.length];

          for (int i = 0; i < aVals.length; i++)
          {
             result[i] = unmarshal(aVals[i]);
          }

          return result;
       }

       /**
        * Check the parameters received from a COM method call and copy the values into OUTPUT or
        * INPUT-OUTPUT arguments.
        *
        * @param    vals
        *           The received values.  The arguments are 1-indexed (as on index 0 is the return
        *           value), and each argument has an entry in this array; {@code null} values
        *           represent INPUT arguments or {@link unknown} values.
        * @param    params
        *           The actual references received by FWD for this method call.  All OUTPUT or INPUT-
        *           OUTPUT arguments must be {@link ComParameter} instances.
        */
       protected static void checkParameters(BaseDataType[][] vals, Object[] params)
       {
          if (params == null)
          {
             return;
          }

          for (int i = 0, k = 0; i < params.length; i++)
          {
             if (!(params[i] instanceof ComParameter))
             {
                continue;
             }

             ComParameter comParam = (ComParameter) params[i];
             if (comParam.isOutput() || comParam.isInputOutput())
             {
                // we increase k counter before indexing the vals to account vals[0] is
                // not a parameter but result
                BaseDataType[] val = vals[++k];

                Object aval = comParam.getValue();

                if (comParam.isExtent())
                {
                   if (comParam.getLength() == 0)
                   {
                      aval = ArrayAssigner.resize((BaseDataType[]) aval, val.length);
                      // and also replace the reference...
                      comParam.getRefReplacement().accept((BaseDataType[]) aval);
                   }

                   if (comParam.getLength() != val.length)
                   {
                      throw new IllegalStateException("Incorrect source and dest array dimension!");
                   }

                   // now we can just copy the values - a dynamic extent was resized and its reference
                   // was properly switched
                   for (int j = 0; j < comParam.getLength(); j++)
                   {
                      ((BaseDataType) Array.get(aval, j)).assign(val[j]);
                   }
                }
                else
                {
                   if (!(aval instanceof BaseDataType))
                   {
                      throw new IllegalStateException(
                               "COM call sent an OUTPUT/INPUT-OUTPUT argument which was not passed"
                                        + " as a BDT from FWD side!");
                   }

                   BaseDataType bdt = (BaseDataType) aval;
                   if (val[0] == null)
                   {
                      bdt.setUnknown();
                   }
                   else
                   {
                      bdt.assign(val[0]);
                   }
                }
             }
          }
       }

       /**
        * Reports if this object is unknown.
        *
        * @return   <code>true</code> if object is unknown.
        */
       @Override
       public boolean unknown()
       {
          return false;
       }

       /**
        * Get this resource's ID, if is already set.
        *
        * @return   The resource's ID or <code>null</code> if not set.
        */
       @Override
       public Long id()
       {
          return resourceId;
       }

       /**
        * Set this resource's ID.
        *
        * @param    id
        *           The resource's ID.
        */
       @Override
       public void id(long id)
       {
          resourceId = id;
       }

       /**
        * Obtain the legacy name of this COM object.
        *
        * @return  the name of this COM object.
        */
       public String getName()
       {
          return name;
       }

       /**
        * Sets the legacy name of this COM object.
        *
        * @param   newName
        *          The new legacy name of this COM object.
        */
       public void setName(String newName)
       {
          this.name = newName;
       }

       /**
        * Obtain the name of this ActiveX object.
        *
        * @return  the name of this ActiveX object.
        */
       public abstract String getActivexName();

       /**
        * Destroy all resources kept by FWD related to this COM object.
        */
       public final void destroy()
       {
          comhandle.removeResource(this);
       }

       /**
        * Obtain the {@code ControlFrameWidget} container for this COM, if any.
        *
        * @return  container for this COM.
        */
       public ControlFrameWidget getParentControlFrame()
       {
          return parentControlFrame;
       }

       /**
        * Sets the {@code ControlFrameWidget} container for this COM.
        *
        * @param   pcf
        *          The container for this COM.
        */
       public void setParentControlFrame(ControlFrameWidget pcf)
       {
          this.parentControlFrame = pcf;
       }

       /**
        * Get the parent procedure where this COM was created.
        *
        * @return  the parent procedure of this object.
        */
       public handle getParentProcedure()
       {
          return parentProcedure;
       }

       /**
        * Sets a COM property for the COM object stored by the handle.
        *
        * @param   prop
        *          The legacy property name. Case insensitive. If no property is found then a warning
        *          is displayed and method returns without altering the stored object.
        * @param   newVal
        *          The new value for the property. It must be of a compatible type with the property.
        * @param   indices
        *          A variable number of indices used to access this property's element.
        *
        * @return  {@code true} if the property was set.
        *
        */
       public boolean setProperty(String prop, Object newVal, Object... indices)
       {
          int indexCount = indices == null ? 0 : indices.length;
          Object[] params = new Object[1 + indexCount];
          params[0] = newVal;
          if (indices != null && indexCount > 0)
          {
             System.arraycopy(indices, 0, params, 1, indexCount);
          }

          Collection<Method> matchingMethods = methodsFor(propertyTable,
                                                          ComProperty.class,
                                                          getComObjectClass(),
                                                          prop);

          if (includeUnannotatedMethods)
          {
             matchingMethods.addAll(methodsFor(methodTable, null, getComObjectClass(), "set" + prop));
          }

          xcall(prop, matchingMethods, params);
          return true;
       }

       /**
        * Obtain a property value from the COM object stored in this handle.
        *
        * @param   prop
        *          The legacy name of the property. Case insensitive. If no property is found then a
        *          warning is displayed and method returns {@code unknown} value.
        * @param   indices
        *          A variable number of indices used to access this property's element.
        *
        * @return  The value of the requested property or {@code unknown} on exceptions.
        */
       public BaseDataType getProperty(String prop, Object... indices)
       {
          int indexCount = indices.length;
          Collection<Method> matchingMethods = methodsFor(propertyTable,
                                                          ComProperty.class,
                                                          getComObjectClass(),
                                                          prop);

          if (includeUnannotatedMethods)
          {
             matchingMethods.addAll(methodsFor(methodTable, null, getComObjectClass(), "get" + prop));
             matchingMethods.addAll(methodsFor(methodTable, null, getComObjectClass(), "is" + prop));
          }

          /**
           * In 4gl, it is valid to refer a no-argument method call without parenthesis,
           * in this case the method call is taken for a get property call by the FWD conversion.
           *
           * So, if we found no property with the name given, try to search for a matching method too.
           */
          if (indexCount == 0)
          {
             matchingMethods.addAll(methodsFor(methodTable, ComMethod.class, getComObjectClass(), prop));

             if (includeUnannotatedMethods)
             {
                matchingMethods.addAll(methodsFor(methodTable, null, getComObjectClass(), prop));
             }
          }

          return xcall(prop, matchingMethods, indices);
       }

       /**
        * Returns the com object class.
        * This implementation simply returns {@code this.getClass}. To be overriden by classes
        * providing support for "dynamic" com object access.
        *
        * @return  See above.
        */
       protected Class getComObjectClass()
       {
          return getClass();
       }

       /**
        * Returns the com object instance.
        * This implementation simply returns {@code this}. To be overriden by classes
        * providing support for "dynamic" com object access.
        *
        * @return  See above.
        */
       public Object getComObjectInstance()
       {
          return this;
       }

       /**
        * Select all methods whose name, class and COM annotation match the argument.
        *
        * @param <T>
        *        the annotation type: either {@link ComMethod} or {@link ComProperty}.
        * @param table
        *        the method class+name - method cache table, lazily updated by this method
        * @param annClass
        *        If not {@code null}, then only methods annotated with this annotation class are tested,
        *        otherwise all methods are tested.
        *        The argument must match the {@code T} template argument.
        * @param clazz
        *        the class of the objects we are looking for the methods in
        * @param comName
        *        the method name in any case.
        *
        * @return the collection of the matching methods
        */
       private static <T extends Annotation> Collection<Method> methodsFor(
                Map<String, Multimap<String, Method>> table, Class<T> annClass,
                Class<? extends ComObject> clazz, String comName)
       {
          String className = clazz.getName();
          Multimap<String, Method> classTable = table.get(className);

          if (classTable == null)
          {
             classTable = MultimapBuilder.treeKeys().hashSetValues().build();
             table.put(className, classTable);

             if (annClass != null)
             {
                for (Class<?> cls = clazz; cls != Object.class; cls = cls.getSuperclass())
                {
                   Class<?>[] faces = cls.getInterfaces();
                   for (Class<?> face : faces)
                   {
                      for (Method m : face.getMethods())
                      {
                         testAndRegisterMethod(annClass, classTable, m);
                      }
                   }

                   for (Method m : cls.getMethods())
                   {
                      testAndRegisterMethod(annClass, classTable, m);
                   }
                }
             }
             else
             {
                for (Method m : clazz.getMethods())
                {
                   classTable.put(m.getName().toUpperCase(), m);
                }
             }
          }

          return classTable.get(comName.toUpperCase());
       }

       /**
        * Test if the method passed as the {@code method} argument is
        * annotated with the annotation of the type passed in the {@code annClass}
        * argument. If yes, then register the method in the
        * {@code classTable} under the name extracted from the annotation name()
        * field and converted to the upper case.
        *
        * @param annClass the annotation class, must be either {@link ComParameter} or {@link ComMethod}
        * @param classTable the map to register the matching method in
        * @param method the input method to test
        */
       private static void testAndRegisterMethod(Class<? extends Annotation> annClass,
                                                 Multimap<String, Method> classTable,
                                                 Method method)
       {
          Annotation ann = method.getAnnotation(annClass);
          if (ann != null)
          {
             String name;
             if (ann instanceof ComProperty)
                name = ((ComProperty) ann).name();
             else if (ann instanceof ComMethod)
                name = ((ComMethod) ann).name();
             else
                throw new IllegalArgumentException();
             classTable.put(name.toUpperCase(), method);
          }
       }

       /**
        * Try to convert a value to the given type.
        *
        * @param arg
        *        the value to convert
        * @param targetType
        *        the destination type to convert the value to
        * @param dest
        *        the output array to store the converted value
        * @param idx
        *        the index in the output array to store the successful conversion result
        *
        * @return {@code true} if conversion succeeded.
        */
       static boolean convertInputParameter(final Object arg, final Class<?> targetType, final Object[] dest,
                final int idx)
       {
          /**
           * If the value is null, and the target type is not a primitive type, convert to null.
           */
          if (arg == null)
          {
             if (targetType.isPrimitive())
             {
                return false;
             }
             dest[idx] = null;
             return true;
          }

          Class<? extends Object> argClass = arg.getClass();

          /**
           * If the value is compatible with the target type, just use it,
           * with one exception: if the target class is Object, and the value
           * is 4gl data type, then convert the value to some base Java type.
           */
          if (targetType.isAssignableFrom(argClass) && !(Object.class.equals(targetType)
                   && BaseDataType.class.isAssignableFrom(argClass)))
          {
             dest[idx] = arg;
             return true;
          }

          if ((arg instanceof BaseDataType) && ((BaseDataType) arg).isUnknown())
          {
             /**
              * Try to pass null as the value.
              */
             return convertInputParameter(null, targetType, dest, idx);
          }

          if (comhandle.class.equals(argClass))
          {
             return convertInputParameter(((comhandle) arg).getResource(), targetType, dest, idx);
          }

          if (ComObject.class.isAssignableFrom(targetType))
          {
             ComObject co = (ComObject) arg;
             if (!co.valid())
             {
                throw new IllegalArgumentException();
             }
             dest[idx] = arg;
             return true;
          }

          ///
          // Java -> Java
          ///

          if (arg instanceof String)
          {
             // String -> long
             // String -> Long
             // String -> Long -> int
             // String -> Long -> Integer
             // String -> Long -> int64
             // String -> Long -> integer
             if (Long.class.equals(targetType) || Long.TYPE.equals(targetType)
                      || Integer.class.equals(targetType) || Integer.TYPE.equals(targetType)
                      || int64.class.equals(targetType) || integer.class.equals(targetType))
             {
                try
                {
                   final long longValue = Long.parseLong(((String) arg).trim());
                   return convertInputParameter(longValue, targetType, dest, idx);
                }
                catch (@SuppressWarnings("unused") NumberFormatException e)
                {
                   return false;
                }
             }

             // String -> double
             // String -> Double
             // String -> Double -> decimal
             if (Double.class.equals(targetType) || decimal.class.equals(targetType)
                      || Double.TYPE.equals(targetType))
             {
                try
                {
                   final Double doubleValue = Double.parseDouble(((String) arg).trim());
                   return convertInputParameter(doubleValue, targetType, dest, idx);
                }
                catch (@SuppressWarnings("unused") NumberFormatException e)
                {
                   return false;
                }
             }

             // String -> Boolean
             // String -> boolean
             // String -> Boolean -> logical
             if (Boolean.class.equals(targetType) || Boolean.TYPE.equals(targetType)
                      || logical.class.isAssignableFrom(targetType))
             {
                return convertInputParameter(Boolean.parseBoolean((String) arg), targetType, dest,
                         idx);
             }

             // String -> LocalDate
             // String -> LocalDate -> date
             if (LocalDate.class.equals(targetType) || date.class.equals(targetType))
             {
                try
                {
                   final LocalDate value = LocalDate.parse(((String) arg).trim());
                   return convertInputParameter(value, targetType, dest, idx);
                }
                catch (@SuppressWarnings("unused") DateTimeParseException e)
                {
                   return false;
                }
             }

             // String -> LocalDateTime
             // String -> LocalDateTime -> datetime
             if (LocalDateTime.class.equals(targetType) || datetime.class.equals(targetType))
             {
                try
                {
                   final LocalDateTime value = LocalDateTime.parse(((String) arg).trim());
                   return convertInputParameter(value, targetType, dest, idx);
                }
                catch (@SuppressWarnings("unused") DateTimeParseException e)
                {
                   return false;
                }
             }
          }

          if (arg instanceof LocalDate)
          {
             // LocalDate -> date
             if (date.class.equals(targetType))
             {
                final LocalDate localDate = (LocalDate) arg;
                return convertInputParameter(new date(localDate.getMonthValue(),
                         localDate.getDayOfMonth(), localDate.getYear()), targetType, dest, idx);
             }

             // LocalDate -> LocalDateTime
             // LocalDate -> LocalDateTime -> datetime
             if (LocalDateTime.class.equals(targetType) || datetime.class.equals(targetType))
             {
                final LocalDate localDate = (LocalDate) arg;
                return convertInputParameter(LocalDateTime.of(localDate.getYear(),
                         localDate.getMonth(), localDate.getDayOfMonth(), 0, 0, 0), targetType, dest,
                         idx);
             }
          }

          if (arg instanceof LocalDateTime)
          {
             // LocalDateTime -> datetime
             if (datetime.class.equals(targetType))
             {
                final LocalDateTime localDateTime = (LocalDateTime) arg;
                return convertInputParameter(new datetime(localDateTime.getMonthValue(),
                         localDateTime.getDayOfMonth(), localDateTime.getYear(),
                         localDateTime.getHour(), localDateTime.getMinute(),
                         localDateTime.getSecond(), localDateTime.getNano() / 1000000), targetType,
                         dest, idx);
             }

             // LocalDateTime -> LocalDate
             // LocalDateTime -> LocalDate -> date
             if (LocalDate.class.equals(targetType) || date.class.equals(targetType))
             {
                final LocalDateTime localDateTime = (LocalDateTime) arg;
                return convertInputParameter(LocalDate.of(localDateTime.getYear(),
                         localDateTime.getMonth(), localDateTime.getDayOfMonth()), targetType, dest,
                         idx);
             }
          }

          if (arg instanceof Long)
          {
             // Long -> Double
             // Long -> Double -> decimal
             if (Double.class.equals(targetType) || Double.TYPE.equals(targetType)
                      || decimal.class.equals(targetType))
             {
                return convertInputParameter(Double.valueOf(((Long) arg)), targetType, dest, idx);
             }

             // Long -> Integer
             // Long -> int
             if (Integer.class.equals(targetType) || Integer.TYPE.equals(targetType))
             {
                dest[idx] = Integer.valueOf(((Long) arg).intValue());
                return true;
             }
          }

          // Integer -> long
          // Integer -> Long
          // Integer -> Long -> Double
          // Integer -> Long -> double
          // Integer -> Long -> decimal
          // Integer -> Long -> integer
          // Integer -> Long -> int64
          // FIXME: always convert Integer -> Long?
          if (arg instanceof Integer && (Double.class.equals(targetType)
                   || Double.TYPE.equals(targetType) || Long.class.equals(targetType)
                   || Long.TYPE.equals(targetType) || decimal.class.equals(targetType)
                   || int64.class.equals(targetType) || integer.class.equals(targetType)))
          {
             return convertInputParameter(Long.valueOf(((Integer) arg)), targetType, dest, idx);
          }

          // Integer -> String
          // Long -> String
          // Double -> String
          // Boolean -> String
          if ((Number.class.isAssignableFrom(argClass) || arg instanceof Boolean)
                   && String.class.equals(targetType))
          {
             dest[idx] = arg.toString();
             return true;
          }

          // Double -> long
          // Double -> Long
          // Double -> Long -> Integer
          // Double -> Long -> int
          // Double -> Long -> int64
          // Double -> Long -> integer
          if (arg instanceof Double && (Long.class.equals(targetType) || Long.TYPE.equals(targetType)
                   || Integer.class.equals(targetType) || Integer.TYPE.equals(targetType)
                   || int64.class.equals(targetType) || integer.class.equals(targetType)))
          {
             return convertInputParameter(((Double) arg).longValue(), targetType, dest, idx);
          }

          // Integer -> int
          // Long -> long
          // Double -> double
          // Boolean -> boolean
          if ((arg instanceof Integer && Integer.TYPE.equals(targetType))
                   || (arg instanceof Long && Long.TYPE.equals(targetType))
                   || (arg instanceof Double && Double.TYPE.equals(targetType))
                   || (arg instanceof Boolean && Boolean.TYPE.equals(targetType)))
          {
             dest[idx] = arg;
             return true;
          }

          // LocalDate -> String
          // LocalDate -> String -> character
          // LocalDateTime -> String
          // LocalDateTime -> String -> character
          if ((arg instanceof LocalDate || arg instanceof LocalDateTime)
                   && (String.class.equals(targetType) || character.class.equals(targetType)))
          {
             return convertInputParameter(arg.toString(), targetType, dest, idx);
          }

          ///
          // BaseDataType -> Java
          ///

          if (arg instanceof character)
          {
             // character -> String
             // character -> Object
             if (String.class.equals(targetType) || Object.class.equals(targetType))
             {
                // Note: we do not strip zero characters here: the OCX might depend on its presence
                dest[idx] = ((character) arg).getValue();
                return true;
             }

             // character -> String -> *
             return convertInputParameter(((character) arg).toStringMessage(), targetType, dest, idx);
          }

          // logical -> Boolean
          // logical -> Boolean -> *
          if (arg instanceof logical)
          {
             final Boolean booleanValue = Boolean.valueOf(((logical) arg).booleanValue());
             if (Boolean.class.equals(targetType) || Boolean.TYPE.equals(targetType))
             {
                dest[idx] = booleanValue;
                return true;
             }

             return convertInputParameter(booleanValue, targetType, dest, idx);
          }

          // int64 -> Long
          // int64 -> long
          // int64 -> Long -> *
          if (arg instanceof int64)
          {
             return convertInputParameter(((int64) arg).longValue(), targetType, dest, idx);
          }

          if (arg instanceof decimal)
          {
             // decimal -> Double
             // decimal -> double
             // decimal -> Double -> Integer
             // decimal -> Double -> int
             // decimal -> Double -> integer
             // decimal -> Double -> int64
             // decimal -> Double -> long
             // decimal -> Double -> Long
             if (Double.class.equals(targetType) || Double.TYPE.equals(targetType)
                      || Integer.class.equals(targetType) || Integer.TYPE.equals(targetType)
                      || Long.class.equals(targetType) || Long.TYPE.equals(targetType)
                      || int64.class.equals(targetType) || integer.class.equals(targetType)
                      || Object.class.equals(targetType))
             {
                return convertInputParameter(((decimal) arg).doubleValue(), targetType, dest, idx);
             }

             // decimal -> String
             // decimal -> String -> character
             if (String.class.equals(targetType) || character.class.equals(targetType))
             {
                return convertInputParameter(((decimal) arg).toString().trim(), targetType, dest,
                         idx);
             }
          }

          // datetime -> LocalDateTime -> *
          // Note: datetime class is more specific than date, so the datetime conversion branch
          //       must precede the date conversion branch
          if (arg instanceof datetime)
          {
             final datetime datetimeArg = (datetime) arg;
             return convertInputParameter(
                      LocalDateTime.of(datetimeArg.getYear(), datetimeArg.getMonth(),
                               datetimeArg.getDay(), datetimeArg.getHours(), datetimeArg.getMinutes(),
                               datetimeArg.getSeconds(), datetimeArg.getMilliseconds() * 1000000),
                      targetType, dest, idx);
          }

          // date -> LocalDate -> *
          if (arg instanceof date)
          {
             final date dateArg = (date) arg;
             return convertInputParameter(
                      LocalDate.of(dateArg.getYear(), dateArg.getMonth(), dateArg.getDay()),
                      targetType, dest, idx);
          }

          ///
          // Java -> BaseDataType
          ///

          // String -> character
          // Integer -> character
          // Long -> character
          // Double -> character
          if (character.class.equals(targetType))
          {
             if (arg instanceof String || arg instanceof Integer || arg instanceof Long
                      || arg instanceof Double || arg instanceof Boolean || arg instanceof date
                      || arg instanceof datetime || arg instanceof logical)
             {
                dest[idx] = character.valueOf(arg);
                return true;
             }
          }

          // Boolean -> logical
          if (logical.class.isAssignableFrom(targetType) && (arg instanceof Boolean))
          {
             dest[idx] = new logical((Boolean) arg);
             return true;
          }

          // Integer -> integer
          // Long -> integer
          // int64 -> integer
          if (integer.class.equals(targetType))
          {
             if (arg instanceof Integer)
             {
                dest[idx] = new integer((Integer) arg);
                return true;
             }

             if (arg instanceof Long)
             {
                dest[idx] = new integer(((Long) arg).intValue());
                return true;
             }

             if (arg instanceof int64)
             {
                final int64 i = (int64) arg;
                dest[idx] = new integer(i.intValue());
                return true;
             }

             return false;
          }

          // Integer -> int64
          // Long -> int64
          if (int64.class.equals(targetType) && arg instanceof Long)
          {
             dest[idx] = new int64(((Long) arg).intValue());
             return true;
          }

          // Number -> decimal
          if (decimal.class.equals(targetType) && arg instanceof Number)
          {
             dest[idx] = new decimal((Number) arg);
             return true;
          }

          if (NumberType.class.equals(targetType))
          {
             if (arg instanceof Integer)
             {
                dest[idx] = new integer((int) arg);
                return true;
             }

             if (arg instanceof Long)
             {
                dest[idx] = new integer((int) arg);
                return true;
             }

             if (arg instanceof Double)
             {
                dest[idx] = new decimal((double) arg);
                return true;
             }

             if (arg instanceof Float)
             {
                dest[idx] = new decimal((float) arg);
                return true;
             }
          }

          return false;
       }

       /**
        * Calls a COM-method without parameters on the COM-object stored in this com-handle.
        *
        * @param   _name
        *          The method name, case insensitive. If the COM-object does not declare such method
        *          an error message is displayed and this method returns an empty {@link comhandle}.
        * @param   params
        *          The list of actual parameters. Their types must be compatible with the parameters
        *          of called method.
        *
        * @return  The result of the called method, if any, or undefined {@link comhandle} if no compatible method was found.
        */
       public BaseDataType call(String _name, Object... params)
       {
          final Collection<Method> matchingMethods = methodsFor(methodTable,
                                                                ComMethod.class,
                                                                getComObjectClass(),
                                                                _name);

          if (includeUnannotatedMethods)
          {
             matchingMethods.addAll(methodsFor(methodTable, null, getComObjectClass(), _name));
          }

          /**
           * Note: we cannot really tell the method call from the indexed property,
           * so we will look the property dictionary either up.
           */
          matchingMethods.addAll(methodsFor(propertyTable, ComProperty.class, getComObjectClass(), _name));

          if (includeUnannotatedMethods)
          {
             matchingMethods.addAll(methodsFor(methodTable, null, getComObjectClass(), "get" + _name));
          }

          return xcall(_name, matchingMethods, params);
       }

       /**
        * Select a method from the given list, and call the method with the arguments specified.
        *
        * For the method to match, the provided argument list must coerce into the method parameter types.
        *
        * @param _name the method name to search for, used for "method not found" reporting only.
        * @param methods the list of all methods of the class matching the name
        * @param args the arguments to pass to the method call
        *
        * @return if the method was selected and called successfully, then
        *         return the value returned from the call coerced to the {@link BaseDataType}.
        *         Otherwise return {@code null}
        */
       private BaseDataType xcall(String _name, Collection<Method> methods, Object[] args)
       {
          // TODO: OUTPUT/INPUT-OUTPUT parameters
          if (!methods.isEmpty())
          {
             boolean isFinerLevelEnabled = LOGGER.isLoggable(Level.FINER);

             Object[] argValues = new Object[args.length];

             for (int i = 0; i < args.length; i++)
             {
                Object param = args[i];
                argValues[i] = (param instanceof ComParameter) ? ((ComParameter) param).getValue()
                         : param;
             }

             outer: for (Method m : methods)
             {
                int methodParamCount = m.getParameterCount();
                if (args.length != methodParamCount)
                {
                   continue outer;
                }
                Class<?>[] methodParamTypes = m.getParameterTypes();
                int j = 0;
                for (Object param : argValues)
                {
                   if (!convertInputParameter(param, methodParamTypes[j], argValues, j))
                   {
                      continue outer;
                   }
                   j++;
                }

                try
                {
                   final Class<?> returnType = m.getReturnType();

                   long beginNanos = 0;
                   if (isFinerLevelEnabled)
                   {
                      LOGGER.log(Level.FINER, () -> {
                         StringBuilder sb = new StringBuilder(100);
                         sb.append(this);
                         sb.append(' ');
                         sb.append(_name);
                         sb.append(' ');
                         for (Object arg : argValues)
                         {
                            sb.append(arg);
                            sb.append(' ');
                         }

                         return sb.toString();
                      });
                      beginNanos = System.nanoTime();
                   }
                   if (VOID.equals(returnType))
                   {
                      Utils.invoke(m, getComObjectInstance(), argValues);
                      if (isFinerLevelEnabled)
                      {
                         LOGGER.log(Level.FINER, "Completed in "
                                  + ((System.nanoTime() - beginNanos) / 1000.0) + " microseconds");
                      }

                      return new comhandle();
                   }

                   Object ret = Utils.invoke(m, getComObjectInstance(), argValues);

                   if (isFinerLevelEnabled)
                   {
                      LOGGER.log(Level.FINER,
                              "Completed in " + ((System.nanoTime() - beginNanos) / 1000.0)
                                        + " microseconds, return value: " + ret);
                   }

                   if (ret instanceof BaseDataType)
                   {
                      return (BaseDataType) ret;
                   }

                   if (ret instanceof ComObject)
                   {
                      return new comhandle(ret);
                   }

                   return marshal(ret);
                }
                catch (IllegalAccessException e)
                {
                   LOGGER.warning("", e);
                }
                catch (InvocationTargetException e)
                {
                   final Throwable cause = e.getCause();
                   if (cause instanceof ConditionException)
                   {
                      throw (ConditionException) cause;
                   }
                   LOGGER.warning("", e);
                }
             }
          }
          // No method resolved
          comhandle.showAccessError(_name.toUpperCase(), "0x80020006", "Unknown name.");
          return new comhandle();
       }

       /**
        * Calls a COM-method on the COM-object stored in this com-handle.
        *
        * @param   methodName
        *          The method name. Case insensitive. If the COM-object does not declare such method
        *          an error message is displayed and this method returns {@code unknown} value.
        * @param   params
        *          The list of actual parameters. Their types must be compatible with the parameters
        *          of called method.
        *          TODO: marshal to BDT (?) When code is generated [param] is already BDT.
        *
        * @return  The result of the called method, in a {@code comhandle}, so it can be used in a
        *          chained call or property access.
        */
       public comhandle chainCall(String methodName, Object... params)
       {
          // TODO: marshal parameters to BDT
          // TODO: OUTPUT/INPUT-OUTPUT parameters
          // TODO: if no compatible method found, display error message
          BaseDataType result = call(methodName, params);

          return result instanceof comhandle ? (comhandle) result : new comhandle(result);
       }

       /**
        * Increments the reference counter for this COM object.
        *
        * TODO: what if this is invalid?
        */
       public void ref()
       {
          if (refcount.get() <= 0)
          {
             System.out.println("Refcounting imbalanced (ref) for " + this);
             return;
          }
          refcount.incrementAndGet();
       }

       /**
        * Increments the reference counter for this COM object. Automatically deletes the object when
        * reference counter reaches 0 as there are no more reference to this object.
        */
       public void deref()
       {
          if (refcount.get() <= 0)
          {
             System.out.println("Refcounting imbalanced (deref) for " + this);
             return;
          }

          int count = refcount.decrementAndGet();
          if (count == 0)
          {
             delete();
             destroy();
          }
       }
    }