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 + "}";
   }
}