# CallParameter.java

    /*
    ** Module   : CallParameter.java
    ** Abstract : Specification for a parameter, when adding it via a CALL:SET-PARAMETER or legacy
    **            ParameterList class.
    **
    ** Copyright (c) 2019-2024, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
    ** 001 CA  20190710 Extracted from Call.java.
    ** 002 ME  20201009 Save initial value for input parameters to avoid variable mutation between
    **                  Call.setParameter and Call.invoke. Delay validation for input table/dataset
    **                  handle until Call.invoke is executed.
    **     OM  20210309 Removed double display of error message number.
    **     CA  20210609 Fixed OO property and field references used as arguments (extent or not).
    **     GES 20210430 Reworked table and dataset parameter processing.
    **     CA  20220910 Fixed 'copyOutput' for DataSetParameter.
    **     OM  20221205 Implemented toString() for easier debugging.
    **     OM  20221216 Reworked table and dataset handle parameters.
    **     CA  20230207 Fixed TABLE-/DATASET-HANDLE case when the handle is unknown.
    ** 003 CA  20240215 Fixed issues using CALL property/field references as arguments, associated with a
    **                  DATASET/TABLE-HANDLE parameter.
    ** 004 CA  20240331 Use logical constants for TRUE, FALSE, UNKNOWN, within internal FWD runtime, so logical
    **                  type checks must include sub-classes, too.
    ** 005 CA  20240409 The parameter mode needs to be set at the dataset/table parameter explicitly.
    ** 006 DDF 20240408 Pass the type of the parameter and use it to wrap it directly to avoid generating
    **                  a constructor for that type.
    ** 007 CA  20241030 Added immutable character constant support.
    ** 008 CA  20241107 Added character constant support.
    */

    /*
    ** 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.util;

    import java.io.*;
    import java.lang.reflect.*;
    import java.util.EnumSet;
    import java.util.logging.*;

    import com.goldencode.p2j.persist.*;
    import com.goldencode.util.*;

    /**
     * A wrapper for call parameters.
     */
    public class CallParameter
    {
       /** The original value passed to {@link Call#setParameter}. */
       public final Object value;

       /** The original input for value parameter passed to {@link Call#setParameter}. */
       public final Object initialValue;

       /** The call mode passed to {@link Call#setParameter}. */
       public final CallMode mode;

       /** The data type passed to {@link Call#setParameter}. */
       public final String dataType;

       /** The prepared argument which will be passed to the remote call. */
       protected Object argument = null;

       /**
        * Create a new wrapper for a parameter which will be used for a native OS API call.
        *
        * @param    dataType
        *           The parameter's data type.
        * @param    isClass
        *           Flag indicating that the datatype is a legacy OO class.
        * @param    mode
        *           The call mode.
        * @param    value
        *           The parameter's value.
        */
       public CallParameter(String dataType, boolean isClass, CallMode mode, Object value)
       {
          this.mode = mode;

          if ("TABLE-HANDLE".equalsIgnoreCase(dataType) && value instanceof handle)
          {
             TempTable tempTable = (TempTable) ((handle) value).getResource();
             if (tempTable != null && tempTable.valid())
             {
                this.value = new TableParameter(tempTable.defaultBufferHandle(), mode.mode);

                this.dataType = "TABLE";
                this.initialValue = convertValue("TABLE", isClass, mode, this.value);
                return;
             }
          }
          else if ("DATASET-HANDLE".equalsIgnoreCase(dataType) && value instanceof handle)
          {
             DataSet dataSet = (DataSet) ((handle) value).getResource();
             if (dataSet != null && dataSet.valid())
             {
                this.value = new DataSetParameter(dataSet, mode.mode);

                this.dataType = "DATASET";
                this.initialValue = convertValue("DATASET", isClass, mode, this.value);
                return;
             }
          }

          this.dataType = dataType;
          this.value = value;
          // save the current value for input/input-output so the argument have the right reference on invoke
          this.initialValue = convertValue(dataType, isClass, mode, value);
       }

       /**
        * Get the argument.  It is assumed {@link #convertTo(Class)} was previously called.
        *
        * @return    The {@link #argument}.
        */
       public Object getArgument()
       {
          if (argument == null)
          {
             argument = convertTo();
          }

          return argument;
       }

       /**
        * If this parameter is an OUTPUT or INPUT-OUTPUT non-table parameter, copy the value to
        * the variable or field, as specified via {@link Call#setParameter SET-PARAMETER}.
        *
        * @return   <code>true</code> if the value could be copied; <code>false</code> otherwise.
        */
       public boolean copyOutput()
       {
          if (mode.input || argument instanceof TableParameter || argument instanceof DataSetParameter)
          {
             return true;
          }

          if (value instanceof PropertyReference)
          {
             PropertyReference pr = (PropertyReference) value;

             if (pr.getType().isAssignableFrom(argument.getClass()))
             {
                pr.set((BaseDataType) argument);
             }
             else
             {
                pr.set(convertTo((BaseDataType) argument, pr.getType()));
             }

             return true;
          }
          else if (value instanceof FieldReference)
          {
             FieldReference fr = (FieldReference) value;

             Buffer buf = (Buffer) fr.getParentBuffer().getDMOProxy();
             if (!buf._available())
             {
                ErrorManager.recordOrShowError(10088,
                                               "Record  not available for dynamic output parameter",
                                               false, false, false);
                return false;
             }

             if (fr.getType().isAssignableFrom(argument.getClass()))
             {
                fr.set((BaseDataType) argument);
             }
             else
             {
                fr.set(convertTo((BaseDataType) argument, fr.getType()));
             }
          }
          else if (value instanceof BaseDataType)
          {
             BaseDataType bdt = (BaseDataType) value;

             if (bdt instanceof decimal || bdt instanceof int64)
             {
                if (bdt.getClass() == decimal.class)
                {
                   bdt = new decimal()
                   {
                      @Override
                      boolean isAssignDirect()
                      {
                         return true;
                      }
                   };
                }
                else if (bdt.getClass() == int64.class)
                {
                   bdt = new int64()
                   {
                      @Override
                      boolean isAssignDirect()
                      {
                         return true;
                      }
                   };
                }
                else if (bdt.getClass() == integer.class)
                {
                   bdt = new integer()
                   {
                      @Override
                      boolean isAssignDirect()
                      {
                         return true;
                      }
                   };
                }
                else if (bdt.getClass() == recid.class)
                {
                   bdt = new recid()
                   {
                      @Override
                      boolean isAssignDirect()
                      {
                         return true;
                      }
                   };
                }

                bdt.assign((BaseDataType) value);
             }

             // documentation states that if the variable is out of scope, then terminate with 10088
             // actually, 4GL abends.  we let it assign, to not add the scope-overhead to each and
             // every variable

             if (bdt.getClass().isAssignableFrom(argument.getClass()))
             {
                bdt.assign((BaseDataType) argument);
             }
             else
             {
                bdt.assign(convertTo((BaseDataType) argument, value.getClass()));

                if (bdt.isAssignDirect() && CompareOps._isNotEqual(bdt, (BaseDataType) value))
                {
                   try
                   {
                      // copy the data from the wrapped reference to the other, if they are unequal
                      ByteArrayOutputStream baos = new ByteArrayOutputStream();
                      ObjectOutputStream out = new ObjectOutputStream(baos);
                      bdt.writeExternal(out);
                      out.close();

                      ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
                      ((BaseDataType) value).readExternal(new ObjectInputStream(bais));

                      // force this to be registered as undo
                      ((BaseDataType) value).checkUndoable(true);
                   }
                   catch (Exception e)
                   {
                      throw new IllegalStateException("Problem copying output data", e);
                   }
                }
             }
          }
          else
          {
             throw new IllegalStateException("Can not use " + value.getClass() + " for OUTPUT.");
          }

          // reset argument so it gets recreated from initial value on next call
          if (mode.inputOutput)
             argument = null;

          return true;
       }

       /**
        * Convert this parameter's {@link CallParameter#value value} to be usable as an argument.
        * <p>
        * Once converted, it will be cached in the {@link #argument} variable. The FWD base data type
        * class is inferred from parameter data type.
        *
        * @return   The {@link #argument}.
        */
       public Object convertTo()
       {
          return convertTo(BaseDataType.fromTypeName(dataType));
       }

       /**
        * Convert this parameter's {@link CallParameter#value value} to be usable as an argument.
        * <p>
        * Once converted, it will be cached in the {@link #argument} variable.
        *
        * @param cls The FWD base data type class for the parameter.
        *
        * @return   The {@link #argument}.
        */
       public Object convertTo(Class<?> cls)
       {
          boolean silent = ErrorManager.isSilent();
          if (!silent)
          {
             // assume a silent mode, so that errors can be captured and re-raised
             ErrorManager.setSilent(true);
          }
          boolean forceError = false;

          try
          {
             if (argument != null)
             {
                return argument;
             }

             boolean input = mode.input || mode.inputOutput;
             boolean output = mode.output || mode.inputOutput;

             Object argVal = value;
             Class<?> valueCls = argVal == null ? null : argVal.getClass();
             if (valueCls == FieldReference.class)
             {
                valueCls = ((FieldReference) value).getType();
                argVal = ((FieldReference) value).get();
             }
             else if (valueCls == PropertyReference.class)
             {
                valueCls = ((PropertyReference) value).getType();
                argVal = ((PropertyReference) value).get();
             }

             if (handle.class.isAssignableFrom(valueCls))
             {
                if (((handle) argVal).isUnknown())
                {
                   // in this case, leave the reference (property or field), which will need to be updated
                }
                else
                {
                   WrappedResource res = (WrappedResource) ((handle) argVal).getResource();
                   if (!res.valid() && "DATASET-HANDLE".equalsIgnoreCase(dataType))
                   {
                      // this needs to 'fall through' and not handlded in this method.
                      forceError = true;
                      ErrorManager.setSilent(silent);

                      ErrorManager.recordOrThrowError(12314,
                                                      "Invalid DATASET-HANDLE parameter given",
                                                      false,
                                                      false);

                      return null;
                   }

                   // if a valid dataset is passed, then use that instance exclusively, and discard the
                   // field/property reference

                   // for a temp-table arg, then again we work with the handle directly, regardless if is valid
                   // or not

                   argVal = new handle(res);
                }
             }

             if ("TABLE-HANDLE".equalsIgnoreCase(dataType) || "DATASET-HANDLE".equalsIgnoreCase(dataType))
             {
                if (argVal instanceof FieldReference)
                {
                   argVal = new HandleFieldRef((FieldReference) argVal);
                }
                else if (argVal instanceof PropertyReference)
                {
                   argVal = new HandlePropertyRef((PropertyReference) argVal);
                }
             }

             // for table/dataset handle input/input-output validation is delayed till invoke
             if ("TABLE-HANDLE".equalsIgnoreCase(dataType))
             {
                argument = new TableParameter((handle) argVal, mode.mode, input, output);
                return argument;
             }
             else if ("DATASET-HANDLE".equalsIgnoreCase(dataType))
             {
                argument = new DataSetParameter((handle) argVal, mode.mode, input, output);
                return argument;
             }
             else if (initialValue instanceof BaseDataType)
             {
                argument = TypeFactory.initInput((BaseDataType) initialValue);
             }
             else
             {
                argument = initialValue;
             }

             return argument;
          }
          catch (ErrorConditionException ece)
          {
             if (forceError)
             {
                throw ece;
             }

             ErrorManager.setSilent(silent);

             boolean pendingError = ErrorManager.isPendingError();
             ErrorManager.clearPending();

             int[] nums =
             {
                ece.getProgressErrorCode(),
                5729,
                10059
             };

             String[] texts =
             {
                ece.getMessage(),
                "Incompatible datatypes found during runtime conversion",
                "Unable to convert SET-PARAMETER value to datatype passed"
             };

             if (!pendingError)
             {
                ErrorManager.recordOrShowError(nums, texts, false, false, false, true);
             }
             else
             {
                ErrorManager.recordOrThrowError(nums, texts, false, true);
             }

             return null;
          }
          finally
          {
             ErrorManager.setSilent(silent);
          }
       }

       /**
        * Make a copy of this parameter's {@link CallParameter#value value} for input/input-output parameters.
        * <p>
        * For input and input-output the value saved in the {@link #value} variable points to the value/reference
        * used when the parameter was created although that might change in between when the {@link Call#invoke invoke}
        * method is called.
        *
        * @param    dataType
        *           The parameter's data type.
        * @param    isClass
        *           Flag indicating that the datatype is a legacy OO class.
        * @param    mode
        *           The call mode.
        * @param    value
        *           The parameter's value as set in ctor.
        *
        * @return   The current {@link #value} reference for input/input-output.
        */
       private Object convertValue(String dataType, boolean isClass, CallMode mode, Object value)
       {
          if (value instanceof AbstractExtentParameter)
          {
             // nothing to do here
             return value;
          }

          if (mode.input || mode.inputOutput)
          {
             Class<? extends BaseDataType> cls = isClass ? object.class : BaseDataType.fromTypeName(dataType);

             if (cls != null)
             {
                BaseDataType val = BaseDataType.generateUnknown(cls);

                if (value instanceof BaseDataType)
                {
                   val.assign((BaseDataType) value);
                }
                else if (value instanceof FieldReference)
                {
                   FieldReference ref = (FieldReference) value;
                   if (ref.getExtent() != null && ref.getIndex() == -1)
                   {
                      // this is an array, the work is not here.
                      val = null;
                   }
                   else
                   {
                      val.assign(ref.get());
                   }
                }
                else if (value instanceof PropertyReference)
                {
                   PropertyReference ref = (PropertyReference) value;
                   val.assign(ref.get());
                }
                else
                {
                   boolean assigned = false;
                   try
                   {
                      Constructor<?> ctor = cls.getConstructor(value.getClass());
                      if (ctor != null)
                      {
                         val.assign((BaseDataType) ctor.newInstance(value));
                         assigned = true;
                      }
                   }
                   catch (Exception e)
                   {
                      // fallback
                   }

                   if (!assigned)
                   {
                      try
                      {
                         Constructor<?> ctor = cls.getConstructor(String.class);
                         if (ctor != null)
                         {
                            val.assign((BaseDataType) ctor.newInstance(value.toString()));
                         }
                      }
                      catch (Exception e)
                      {
                         val = null;
                      }
                   }
                }

                return val != null ? val : value;
             }
          }

          // input and input-output need to be wrapped

          if (value instanceof BaseDataType)
          {
             value = OutputParameter.wrap((BaseDataType) value, mode.inputOutput);
          }
          else if (value instanceof FieldReference)
          {
             FieldReference ref = (FieldReference) value;
             if (ref.getExtent() != null && ref.getIndex() == -1)
             {
                // this is an array, the work is not here.
             }
             else
             {
                value = OutputParameter.wrap((FieldReference) value, mode.inputOutput, ref.getType());
             }
          }
          else if (value instanceof PropertyReference)
          {
             value = OutputParameter.wrap((PropertyReference) value, mode.inputOutput);
          }

          if (mode.mode != null && mode.mode != ParameterOption.NONE)
          {
             if (value instanceof DataSetParameter)
             {
                ((DataSetParameter) value).setParameterOptions(EnumSet.of(mode.mode));
             }
             else if (value instanceof TableParameter)
             {
                ((TableParameter) value).setParameterOptions(EnumSet.of(mode.mode));
             }
          }

          return value;
       }

       /**
        * Convert the given BDT instance to the specified type.
        * <p>
        * This will follow explicit rules to convert from the variable's data-type (passed to
        * {@link Call#setParameter} and the value received from the emulated CALL invoke.
        * <p>
        * At this time, the following data types were not explored and can't be converted unless
        * there variable's and value's data-type match:
        * <ul>
        * <li>decimal - this was explored only for converting from character values.  Even with
        *     this type, the conversion process is indeterminate in 4GL - if not all required bytes
        *     exist so they can be used, random values are used.  With other data types and using
        *     certain values, 4GL even crashes.</li>
        * <li>longchar - at the time of this writing, no rules could be extracted for converting
        *     a longchar to or from another data-type - consecutive runs of the same program
        *     gives different output.</li>
        * <li>raw - was not explored</li>
        * <li>rowid - was not explored</li>
        * <li>handle - 4GL allows a handle to be initiated with a random value, even if that value
        *     is not a valid resource ID.  Was not explored further, as this requires changes in
        *     how FWD can initialize a handle.</li>
        * <li>datetimetz - could not determine rules to convert a BDT value to a datetimetz data
        *     type.  There might be some constant date or time used as a reference, same as
        *     date uses the default date offset 2433405 (in days).
        * </ul>
        *
        * @param    val
        *           The instance configured as OUTPUT/INPUT-OUTPUT with the data type as specified
        *           by {@link Call#setParameter}, and saved in {@link #dataType}.
        * @param    type
        *           The data type of the actual instance passed to the {@link Call#setParameter}.
        *
        * @return   The converted value.
        */
       private BaseDataType convertTo(BaseDataType val, Class<?> type)
       {
          if (val.isUnknown() || val.getClass() == type)
          {
             return val;
          }

          Runnable logWarning = () ->
          {
             if (Call.LOG.isLoggable(Level.WARNING))
             {
                Call.LOG.log(Level.WARNING,
                        "Attention - converting " + val.getClass() + " to " + type +
                        " is not handled explicitly by FWD.");
             }
          };

          long defDate = 2433405;

          if (int64.class.isAssignableFrom(type))
          {
             int64 res = new int64();

             if (val instanceof int64)
             {
                res.assign(val);

                return res;
             }
             else if (val instanceof character)
             {
                String sval = val.toStringMessage();

                // converting a char to int/int64 just takes ASCII code for each character
                long rval = 0;
                if (sval.length() <= 8)
                {
                   // if the value is over 64 bit, it will overflow and 4GL uses 0
                   for (int i = 0; i < sval.length(); i++)
                   {
                      rval = (rval << 8) + sval.charAt(i);
                   }
                }

                res.assign(rval);

                return res;
             }
             else if (val.getClass() == date.class)
             {
                res.assign(-2433405 + ((date) val).longValue());

                return res;
             }
             else if (val.getClass() == datetime.class)
             {
                // compute the numeric representation of this datetime:
                // - upper bytes: date(val) - defDate
                // - lower bytes: mtime(val)
                raw r = new raw();
                r.setLength(8);
                r.setLong(datetime.millisecondsSinceMidnight((datetime) val), 1);
                r.setLong(((datetime) val).longValue() - defDate, 5);
                byte[] bytes = r.getByteArray();
                long v = 0;
                for (int i = 0; i < bytes.length; i++)
                {
                   int b = bytes[(bytes.length - 1 - i)] & 0xff;
                   v = v << 8;
                   v = v + b;
                }
                res.assign(v);

                return res;
             }
             else if (val.getClass() == datetimetz.class)
             {
                // this can't convert
                res.assign(0);

                return res;
             }
             else if (val instanceof decimal)
             {
                if (CompareOps._isEqual(val, 0.0d))
                {
                   res.assign(0);
                   return res;
                }

                // get the string representation of this decimal value and:
                // - if the number of fractional and non-fractional digits is more than 14, then -1
                // - leftmost bit is "0x8#", where "#" is the number of fractional digits in hex
                // - concatenate the fractional and non-fractional digits
                // - push them to the left, in pairs, i.e. 12345 becomes 5f3412 (use F if odd
                // number of digits)
                String sval = ((decimal) val).toStringMessage();
                int dot = sval.indexOf('.');
                String fract = dot < 0 ? "" : sval.substring(dot + 1);
                String dec = dot < 0 ? sval : sval.substring(0, dot);
                String all = dec + fract;

                if (all.length() > 14)
                {
                   res.assign(-1);
                   return res;
                }

                long v = 0xffffffffffffffffl;
                v = (v << 8) + ((0x8 << 4) + fract.length());

                while (!all.isEmpty())
                {
                   String chars = all.length() == 1 ? (all + "F") : all.substring(0,  2);
                   v = (v << 8) + Long.parseLong(chars, 16);

                   all = all.length() == 1 ? all.substring(1) : all.substring(2);
                }

                res.assign(v);

                return res;
             }
          }
          else if (type == decimal.class)
          {
             decimal res = new decimal();

             if (val instanceof character)
             {
                String sval = val.toStringMessage();

                // this type is a little tricky; it fills the following, by walking the string from
                // left to right:
                // - first char is ignored
                // - always negative
                // - first populate 49 fractional digits
                // - from what is left, populate 23 non-fractional digits
                // - if the length of the string (without first char) is more than 36 chars
                //   (288 bits), then set it to 0

                if (sval.length() > 37 || sval.isEmpty() || sval.length() == 1)
                {
                   res.assign(0);
                }
                else
                {
                   int maxFraction = 49;
                   String left = "-0";
                   String right = "";
                   for (int i = 1; i < sval.length(); i++)
                   {
                      String hex = Integer.toHexString(Byte.toUnsignedInt((byte) sval.charAt(i)));
                      if (hex.length() < 2)
                      {
                         hex = "0" + hex;
                      }

                      right = right + hex;

                      if (right.length() > maxFraction)
                      {
                         int diff = right.length() - maxFraction;
                         left = left + right.substring(0, diff);
                         right = right.substring(diff);
                      }
                   }

                   if (right.length() < maxFraction)
                   {
                      // here, we just pad the fractional part with 0 digits; in 4GL, if the
                      // fractional parts can't be computed (the string is less than 25 chars), it
                      // will populate it with random values
                      right = right + StringHelper.repeatChar('0', maxFraction - right.length());
                   }

                   // this 49 fraction is always forced to the internal representation; but this
                   // can't be used in 4GL unless you want to output this value - any arithmetic
                   // operation done on this value will abort the process. So, we just keep it
                   // internally, and still honor the variable's specified 'DECIMALS' clause.
                   res.setValue(left+right, maxFraction);
                }

                return res;
             }

             // TODO: other types
          }
          else if (type == date.class)
          {
             // the implicit date
             date res = new date(defDate);
             boolean skipZero = false;

             raw r = new raw();
             if (val instanceof character)
             {
                character ch = (character) val;
                if (TextOps.length(ch).intValue() > 0)
                {
                   r.setString(ch, 1, TextOps.byteLength(ch));
                }
                skipZero = true;
             }
             else if (val instanceof decimal)
             {
                if (CompareOps._isEqual(val, 0.0d))
                {
                   return res;
                }
                else
                {
                   byte[] bytes = Call.decimalBytes((decimal) val);

                   long v = 0xffffffffffffffffl;
                   for (int i = 0; i < bytes.length; i++)
                   {
                      v = (v << 8) + ((int) bytes[i] & 0xff);
                   }

                   res.assign(new int64(defDate + v));

                   return res;
                }
             }
             else if (val instanceof int64)
             {
                r.setLong((int64) val, 1);
             }
             else if (val instanceof logical)
             {
                r.setByte(((logical) val).getValue() ? 1 : 0, 1);
             }
             else if (val instanceof datetimetz)
             {
                logWarning.run();

                // TODO: log this, can't get a rule - return default date
                return res;
             }
             else if (val instanceof datetime)
             {
                long idate = ((datetime) val).longValue();
                if (idate == defDate)
                {
                   res = date.plusDays(res, datetime.millisecondsSinceMidnight((datetime) val));
                }

                return res;
             }
             else
             {
                logWarning.run();
             }

             // from the given byte representation, read only at most 4 non-zero bytes;
             // if after reading 4 non-zero bytes there are other bytes, then return implicit date
             long[] bytes = new long[4];
             int numBytes = 0;
             for (int i = 1; i <= r.length().intValue(); i++)
             {
                long b = r.getByte(i).getValue();
                if (skipZero && b == 0)
                {
                   continue;
                }

                if (numBytes == bytes.length)
                {
                   return res;
                }

                bytes[numBytes] = b;
                numBytes = numBytes + 1;
             }

             // compute the days to be added to the implicit date
             long days = 0;
             for (int i = 0; i < numBytes; i++)
             {
                long b = skipZero ? bytes[i] << ((numBytes - i - 1) * 8)
                                  : bytes[i] << (i * 8);

                days = days + b;
             }

             res = date.plusDays(res, days);

             return res;
          }
          else if (type == character.class || type == character.characterConstant.class)
          {
             int prefix00 = 0x80;

             character res = new character();
             boolean[] stripFF = new boolean[] { true };
             java.util.function.Function<byte[], Integer> reverseBytes = bytes ->
             {
                int lastNonNull = bytes.length - 1;
                while (lastNonNull >= 0 &&
                       (bytes[lastNonNull] == 0 ||
                        (stripFF[0] && bytes[lastNonNull] == (byte) 0xFF)))
                {
                   lastNonNull = lastNonNull - 1;
                }

                if (lastNonNull < 0)
                {
                   return 0;
                }

                int blength = lastNonNull + 1;
                for (int i = 0; i < blength / 2; i++)
                {
                   byte aux = bytes[i];
                   bytes[i] = bytes[blength - (i + 1)];
                   bytes[blength - (i + 1)] = aux;
                }

                return blength;
             };

             raw r = new raw();
             boolean reverse = false;
             if (val instanceof date)
             {
                r.setLong(((date) val).longValue() - defDate, 1);

                if (val instanceof datetime)
                {
                   byte[] bdate = r.getByteArray();
                   int dateNonNull = reverseBytes.apply(bdate);

                   r.setInt64(datetime.millisecondsSinceMidnight((datetime) val).longValue(), 1);
                   byte[] btime = r.getByteArray();
                   int timeNonNull = reverseBytes.apply(btime);

                   byte[] bytes;

                   if (val instanceof datetimetz)
                   {
                      // from high to low:
                      // - 6 bytes are mtime
                      // - 4 bytes are date
                      // - 2 bytes are the timezone (in minutes)

                      r.setLength(0);
                      r.setShort(((datetimetz) val).getTimeZoneOffset().intValue(), 1);
                      byte[] btz = r.getByteArray();
                      reverseBytes.apply(btz);

                      bytes = new byte[12];
                      System.arraycopy(btime, 0, bytes, 0, Math.min(btime.length, 6));
                      System.arraycopy(bdate, 0, bytes, 6, Math.min(bdate.length, 4));
                      System.arraycopy(btz,   0, bytes, 10, Math.min(btz.length, 2));
                   }
                   else
                   {
                      // 4 high-order bytes are the date, 4 low-order bytes are mtime
                      bytes = new byte[dateNonNull + 4];
                      System.arraycopy(bdate, 0, bytes, 0, dateNonNull);
                      System.arraycopy(btime, 0, bytes, bytes.length - timeNonNull, timeNonNull);
                   }

                   res.forceBytes(bytes);
                   return res;
                }
                else
                {
                   reverse = true;
                   stripFF[0] = true;
                }
             }
             else if (val instanceof decimal)
             {
                if (CompareOps._isEqual(val, 0.0d))
                {
                   res.assign(character.EMPTY_STRING);
                }
                else
                {
                   byte[] bytes = Call.decimalBytes((decimal) val);
                   res.forceBytes(bytes);
                }

                return res;
             }
             else if (val instanceof handle)
             {
                // handle is allowed, and it will try to interpret the value as a resource id
                // it will assign the resource id, but it will not be valid if the resource
                // doesn't exist
                handle h = (handle) val;
                Long resId = h.getResourceId();
                if (resId != null)
                {
                   r.setLong(resId, 1);
                   reverse = true;
                }
                else
                {
                   // TODO: log this, a resource ID was not set for this handle
                   res.assign(h);

                   return res;
                }
             }
             else if (val instanceof int64)
             {
                // int64, integer and recid
                r.setLong((int64) val, 1);
                stripFF[0] = false;
                reverse = true;
             }
             else if (val instanceof logical)
             {
                r.setByte(((logical) val).booleanValue() ? 1 : 0, 1);
             }
             else if (val instanceof rowid)
             {
                res.assign(val);

                return res;
             }
             else
             {
                logWarning.run();

                res.assign(val);

                return res;
             }

             byte[] bytes = r.getByteArray();
             int blength;

             if (reverse)
             {
                blength = reverseBytes.apply(bytes);
             }
             else
             {
                int lastNonNull = bytes.length - 1;
                while (lastNonNull >= 0 && bytes[lastNonNull] == 0)
                {
                   lastNonNull = lastNonNull - 1;
                }
                blength = lastNonNull < 0 ? 0 : (lastNonNull + 1);
             }

             if (blength == 0)
             {
                res.assign(character.EMPTY_STRING);
             }
             else
             {
                // TODO: this NULL prefix is not always added... for date/datetime, 0x7f is used
                // as a limit, and for some cases is not even added.
                if ((int) (bytes[0] & 0xFF) >= prefix00)
                {
                   // force a null byte...
                   byte[] b2 = new byte[blength + 1];
                   System.arraycopy(bytes, 0, b2, 1, blength);
                   bytes = b2;
                   blength = bytes.length;
                }
                res.forceBytes(bytes, blength);
             }

             return res;
          }
          else if (type == logical.class || type == logical.logicalConstant.class)
          {
             logical res = new logical();

             if (val instanceof handle)
             {
                Long resId = handle.resourceId(((handle) val).getResource());
                res.assign(resId != null && resId != 0);
             }
             else if (val instanceof int64)
             {
                res.assign(CompareOps._isNotEqual(val, new integer(0)));
                return res;
             }
             else if (val instanceof decimal)
             {
                res.assign(CompareOps._isNotEqual(val, new integer(0)));
                return res;
             }
             else if (val instanceof character)
             {
                raw r = new raw();
                r.setString((character) val, 1);
                res.assign(r.getByte(1).intValue() != 0);
                return res;
             }
             else if (val instanceof date)
             {
                if (val instanceof datetime)
                {
                   res.assign(datetime.millisecondsSinceMidnight((datetime) val).longValue() != 0);
                }
                else
                {
                   res.assign(((date) val).longValue() != defDate);
                }

                return res;
             }
             else
             {
                logWarning.run();
             }
             // TODO: other types
          }
          else if (type == datetime.class)
          {
             datetime res = new datetime();

             // lower 32 bits, signed
             //    - if negative, defaults to 23:59:59.999 ??????
             //    - if positive, millis (modulo 24 * 60 * 60 * 1000)
             // upper 32 bits - date
             if (val instanceof character)
             {
                // lowest 4 bytes are taken to compute the mtime
                String v = ((character) val).toJavaType();
                byte[] vb = v.getBytes();
                long t = 0;
                for (int i = Math.max(0, vb.length - 4); i < vb.length; i++)
                {
                   t = (t << 8) + (byte) (vb[i] & 0xFF);
                }

                res.setTime(t);

                // from what remains, the date is computed
                long d = 0;
                for (int i = 0; i < vb.length - 4; i++)
                {
                   d = (d << 8) + (byte) (vb[i] & 0xFF);
                }
                res.setDayNumber(defDate + d);

                return res;
             }
             else if (val.getClass() == date.class)
             {
                long t = ((date) val).longValue() - defDate;

                res.setTime(t);
                res.setDayNumber(defDate);

                return res;
             }
             else if (val.getClass() == datetimetz.class)
             {
                long t = ((datetimetz) val).getTimeZoneOffset().longValue();

                res.setTime(t);
                res.setDayNumber(defDate);

                return res;
             }
             else if (val.getClass() == decimal.class)
             {
                byte[] bytes = Call.decimalBytes((decimal) val);
                long t = 0;
                for (int i = 0; i < bytes.length; i++)
                {
                   t = (t << 8) + (byte) (bytes[i] & 0xff);
                }

                res.setTime(t);
                res.setDayNumber(defDate - 1);

                return res;
             }
             else if (val instanceof int64)
             {
                long l = ((int64) val).longValue();
                res.setTime(l & 0xffffffffl);
                res.setDayNumber(defDate + (l & 0xffffffff00000000l));

                return res;
             }
             else
             {
                logWarning.run();
             }
          }
          else if (type == datetimetz.class)
          {
             // TODO: find the rules
          }
          else
          {
             logWarning.run();
          }

          BaseDataType res = BaseDataType.generateUnknown(type);
          res.assign(val);

          return res;
       }

       /**
        * Obtain a brief description of this object. That include: the passing mode, the parameter type, and the
        * argument value (or its description).
        *
        * @return  a short string representation of this object.
        */
       @Override
       public String toString()
       {
          return "CallParameter{" + mode + "(" + dataType + "):" + argument + "}";
       }
    }