# PropertyReference.java

    /*
    ** Module   : PropertyReference.java
    ** Abstract : OO property reference which allows deferred read/write access
    **
    ** Copyright (c) 2019-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
    ** 001 CA  20190524 Created initial version.
    ** 002 CA  20190604 Added missing javadocs.
    ** 003 CA  20190620 Fixed initialization order.
    ** 004 CA  20190724 When calling the setter, the value must be converted to the property's type,
    **                 if is not the same type.
    ** 005 CA  20210609 Reworked INPUT/INPUT-OUTPUT parameters to a new approach, where they are explicitly
    **                  initialized at the method's execution, and not at the caller's arguments.
    ** 006 CA  20240215 Fixed issues using CALL property/field references as arguments, associated with a
    **                  DATASET/TABLE-HANDLE parameter.
    ** 007 CA  20240520 Fixed validation of the DYNAMIC-PROPERTY arguments for statement and function.
    ** 008 PBB 20250509 Modified implementation of DYNAMIC-PROPERTY function to introduce support for
    **                  DYNAMIC-INVOKE builtin function that returns or might return an extent.
    ** 009 PBB 20250522 Added support for DYNAMIC-PROPERTY statement (setter) mostly related to extents.
    */

    /*
    ** 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.lang.reflect.*;
    import java.util.*;

    import com.goldencode.p2j.oo.lang.*;

    /**
     * Dynamic access to a legacy property.
     */
    public final class PropertyReference
    implements Resolvable,
               Accessor
    {
       /** The fully qualified legacy class name for this property, in case of static properties. */
       private String className = null;

       /** The object instance, in case of instance properties. */
       private object<? extends _BaseObject_> instance = null;

       /** The legacy property name to access. */
       private String property = null;

       /** The index, in case of extent properties. */
       private int64 index = null;

       /** The property's extent. */
       private Integer extent = null;

       /** The legacy type to which the property belongs. */
       private Class<? extends _BaseObject_> type;

       /** The property's data type. */
       private Class<? extends BaseDataType> propType;

       /** The associated getter for the property. */
       private InternalEntry defaultGetter = null;

       /** The associated setter for the property. */
       private InternalEntry defaultSetter = null;

       /** The list of all associated setters for the property */
       private List<InternalEntry> settersList = null;

       /** In case of an instance property, the legacy reference. */
       private _BaseObject_ reference;

       /**
        * Constructor.
        *
        * @param   className
        *          The class name in case of static property access.
        * @param   property
        *          Name of class property member.
        */
       public PropertyReference(String className, String property)
       {
          this.className = className;
          this.property = property;

          init();
       }

       /**
        * Constructor.
        *
        * @param   instance
        *          The class instance, for instance property access.
        * @param   property
        *          Name of class property member.
        */
       public PropertyReference(object<? extends _BaseObject_> instance, String property)
       {
          this.instance = new object<>(instance);
          this.property = property;

          init();
       }

       /**
        * Constructor.
        *
        * @param   className
        *          The class name in case of static property access.
        * @param   property
        *          Name of class property member.
        * @param   index
        *          Index to pass to property's getter method at resolve time.
        */
       public PropertyReference(String className, String property, NumberType index)
       {
          this.className = className;
          this.property = property;
          this.index = new int64(index);

          init();
       }

       /**
        * Constructor.
        *
        * @param   className
        *          The class name in case of static property access.
        * @param   property
        *          Name of class property member.
        * @param   index
        *          Index to pass to property's getter method at resolve time.
        */
       public PropertyReference(String className, String property, int index)
       {
          this.className = className;
          this.property = property;
          this.index = new int64(index);

          init();
       }

       /**
        * Constructor.
        *
        * @param   instance
        *          The class instance, for instance property access.
        * @param   property
        *          Name of class property member.
        * @param   index
        *          Index to pass to property's getter method at resolve time.
        */
       public PropertyReference(object<? extends _BaseObject_> instance, String property, int index)
       {
          this.instance = new object<>(instance);
          this.property = property;
          this.index = new int64(index);

          init();
       }

       /**
        * Constructor.
        *
        * @param   instance
        *          The class instance, for instance property access.
        * @param   property
        *          Name of class property member.
        * @param   index
        *          Index to pass to property's getter method at resolve time.
        */
       public PropertyReference(object<? extends _BaseObject_> instance, String property, NumberType index)
       {
          this.instance = new object<>(instance);
          this.property = property;
          this.index = new int64(index);

          init();
       }

       /**
        * Get the property accessor index.
        *
        * @return   The {@link #index}.
        */
       public Integer getIndex()
       {
          return index.isUnknown() ? null : index.toJavaIntegerType();
       }

       /**
        * Get the property's extent.
        *
        * @return   The {@link #extent}.
        */
       public Integer getPropertyExtent()
       {
          return extent;
       }

       /**
        * Resolve the property's type, the legacy (default) getter and setter,
        * and the list of all the setters.
        */
       private void init()
       {
          this.type = className == null ? instance.ref.getClass() : ObjectOps.resolveClass(className);
          this.reference = instance == null ? null : instance.ref();

          this.extent = SourceNameMapper.findLegacyPropertyExtent(type, property);
          this.defaultGetter = SourceNameMapper.findLegacyAccessor(type, property, true, extent != null, index != null);
          this.defaultSetter = SourceNameMapper.findLegacyAccessor(type, property, false, extent != null, index != null);

          if (this.extent != null)
          {
             // Save all the setters for an extent property for choosing the right one at runtime
             this.settersList = SourceNameMapper.findLegacySetters(type, property);
          }

          if (this.defaultSetter == null && index != null && extent == null)
          {
             // defaultSetter calls with a specified index for a non-extent property will ignore the index
             this.defaultSetter = SourceNameMapper.findLegacyAccessor(type, property, false, false, false);
             this.index = null;
          }

          if (this.defaultSetter != null)
          {
             this.defaultSetter.getMethod().setAccessible(true);
          }

          if (this.defaultGetter != null)
          {
             this.defaultGetter.getMethod().setAccessible(true);
             this.propType = (Class<? extends BaseDataType>) this.defaultGetter.getMethod().getReturnType();
          }
          else if (this.defaultSetter != null)
          {
             Parameter p = this.defaultSetter.getParameter(0);
             this.propType = BaseDataType.fromTypeName(p.getType());
          }

       }

       /**
        * Get a copy of the referenced object's current runtime value (this does not return the
        * original instance itself).
        *
        * @return   The copy of the current value.
        */
       @Override
       public BaseDataType get()
       {
          OutputParameterAssigner opa = TransactionManager.deregisterOutputParameterAssigner();

          try
          {
             Object result = (index == null ? defaultGetter.getMethod().invoke(reference)
                                            : defaultGetter.getMethod().invoke(reference, index));

             if (result.getClass().isArray())
             {
                String errMsg = "An array was specified in an expression, " +
                               "on the right-hand side of an assignment, " +
                               "or as a parameter when no array is appropriate or expected";
                ErrorManager.recordOrThrowError(361, errMsg, true);
                return unknown.UNKNOWN;
             }

             return (BaseDataType) result;
          }
          catch (InvocationTargetException exc)
          {
             // TODO: ignore ? Check for errors?

             return BaseDataType.generateUnknown(propType);
          }
          catch (IllegalAccessException exc)
          {
             // Represents programming or JVM configuration error.
             throw new RuntimeException(exc);
          }
          finally
          {
             TransactionManager.registerOutputParameterAssigner(opa);
          }
       }

       /**
        * Get a copy of the referenced object's current runtime extent value (this does not return the
        * original instance itself).
        *
        * @return   The copy of the current extent value.
        */
       public BaseDataType[] getExtent()
       {
          OutputParameterAssigner opa = TransactionManager.deregisterOutputParameterAssigner();

          try
          {
             return (BaseDataType[]) (index == null ? defaultGetter.getMethod().invoke(reference)
                      : defaultGetter.getMethod().invoke(reference, index));
          }
          catch (Exception e)
          {
             // Represents programming or JVM configuration error.
             throw new RuntimeException(e);
          }
          finally
          {
             TransactionManager.registerOutputParameterAssigner(opa);
          }
       }

       /**
        * Set the referenced object's current runtime value to the given value.
        *
        * @param    val
        *           The current value to be assigned into the referenced object.
        */
       @Override
       public void set(BaseDataType val)
       {
          try
          {
             if (extent != null)
             {
                if (index == null)
                {
                   Object currentState = defaultGetter.getMethod().invoke(reference);
                   if (extent == SourceNameMapper.DYNAMIC_EXTENT && Array.getLength(currentState) == 0)
                   {
                      String errMsg = "Invalid assignment to an unfixed indeterminate extent";
                      ErrorManager.recordOrThrowError(11389, errMsg, false);
                   }
                   for (InternalEntry ie : this.settersList)
                   {
                      if (ie.getParameterList().size() == 1 && !ie.getParameter(0).isExtent())
                      {
                         ie.getMethod().invoke(reference, val);
                         return;
                      }
                   }
                   String errMsg = "No proper setter found for '%s' extent property of %s class.";
                   throw new RuntimeException(String.format(errMsg,
                                                            this.property,
                                                            this.className != null ?
                                                                  this.className :
                                                                  this.instance.type().getName()));
                }
                else
                {
                   for (InternalEntry ie : this.settersList)
                   {
                      if (ie.getParameterList().size() == 2)
                      {
                         ie.getMethod().invoke(reference, val, index);
                      }
                   }
                }
             }
             else if (val.getClass() != propType)
             {
                // TODO: Assigning a non-extent value to an extent property
                BaseDataType v2 = BaseDataType.generateDefault(propType);
                v2.assign(val);
                val = v2;
             }

             if (index == null)
             {
                this.defaultSetter.getMethod().invoke(reference, val);
             }
             else
             {
                this.defaultSetter.getMethod().invoke(reference, val, index);
             }
          }
          catch (InvocationTargetException exc)
          {
             // TODO: ignore ? Check for errors?
          }
          catch (IllegalAccessException exc)
          {
             // Represents programming or JVM configuration error.
             throw new RuntimeException(exc);
          }
       }

       /**
        *  Set the referenced object's current runtime extent value to the given extent value.
        *
        * @param    source
        *           The extent values to be assigned to the referenced extent object.
        */
       public void setExtent(BaseDataType[] source)
       {
          if (extent == null || (extent != source.length && extent != SourceNameMapper.DYNAMIC_EXTENT))
          {
             String errMsg = "Whole-array assignment target and source must have the same extent " +
                      "unless the target is indeterminate";
             ErrorManager.recordOrThrowError(14905, errMsg, false);
             return;
          }
          if (source.length > 0 && source.getClass().getComponentType() == propType.getComponentType())
          {
             try
             {
                Object currentState = defaultGetter.getMethod().invoke(reference);
                if (extent == SourceNameMapper.DYNAMIC_EXTENT &&
                    Array.getLength(currentState) > 0         &&
                    Array.getLength(currentState) != source.length)
                {
                   String errMsg = String.format("Indeterminate extent is already fixed to a dimension" +
                            " of %d", Array.getLength(currentState));
                   ErrorManager.recordOrThrowError(13738, errMsg, false);
                   return;
                }
                defaultSetter.getMethod().invoke(reference, (Object) source);
             }
             catch (InvocationTargetException e)
             {
                // TODO: ignore ? Check for errors?
             }
             catch (IllegalAccessException e)
             {
                throw new RuntimeException(e);
             }
          }
          else
          {
             String[] errMsgs = new String[]
             {
                "Incompatible datatypes found during runtime conversion",
                "Could not convert data for array assignment"
             };
             ErrorManager.recordOrThrowError(new int[] { 5729, 14907 }, errMsgs, false);
          }
       }

       /**
        * Resolve this object to its current runtime value.
        *
        * @return  Resolved value.
        */
       @Override
       public BaseDataType resolve()
       {
          return get();
       }

       /**
        * Report the return type expected from resolving the object.
        *
        * @return  Class indicating the type which will be returned by the
        *          object.  Must be a variant of {@link BaseDataType}.
        */
       @Override
       public Class getType()
       {
          return propType;
       }

       /**
        * Wrap a class property (used as an OUTPUT or INPUT-OUTPUT argument), so that it can be
        * updated when the call returns.
        *
        * @param    io
        *           Flag indicating the argument is INPUT-OUTPUT or just OUTPUT.
        *
        * @return   A new BDT value, initialized depending on the argument mode.
        */
       public <T extends BaseDataType> T wrap(boolean io)
       {
          BaseDataType init = io ? resolve() : null;
          T wrapper = (T) OutputParameter.createVariable(propType,
                                                         init,
                                                         get(),
                                                         (bdt) -> new PropertyAssigner(bdt));

          return wrapper;
       }

       /**
        * Get the {@link #instance} for resolving this property.
        *
        * @return   The {@link #instance} field.
        */
       object<? extends _BaseObject_> getInstance()
       {
          return instance;
       }

       /**
        * A simple assigner to update the backing class property, when the call returns.
        */
       private class PropertyAssigner
       extends AbstractSimpleParameter
       {
          /**
           * Create a new instance using the variable reference as argument.
           */
          protected PropertyAssigner(BaseDataType variable)
          {
             super(variable);
          }

          /**
           * Assign the current value of the parameter variable back to the associated class property.
           */
          @Override
          protected void assign()
          {
             set(outputValue);
          }
       }
    }