/*
** Module   : Variable.java
** Abstract : mechanism for indirect data storage
**
** Copyright (c) 2009-2021, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description----------------------------------------
** 001 GES 20090226 Mechanism for indirect data storage.
** 002 CA  20210906 Allow override via command line arguments, for: web endpoint, variable initial values and 
**                  oauth1 parameters. 
*/

/*
** 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 should have received a copy of the GNU Affero General Public License
** along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

package com.goldencode.harness;

import java.util.*;

/**
 * Mechanism for indirect data storage.
 */
public class Variable
{
   /** Map with overrides for the variable's initial value. */
   public static final Map<String, String> overrideVariables = new HashMap<>();

   /** Data value. */
   private Object value = null;
   
   /** Data type. */
   private Class<?> type = null;
   
   /** Variable name (case-sensitive). */
   private String name = null;
   
   /**
    * Define a new instance without a value (the value is <code>null</code>).
    *
    * @param    name
    *           Variable name (case-sensitive).
    * @param    type
    *           The data type of the variable.
    */
   public Variable(String name, Class<?> type)
   {
      this.name = name;
      this.type = type;
      
      String txt = overrideVariables.get(name);
      if (txt != null)
      {
         fromString(txt);
      }
   }
   
   /**
    * Define a new instance without a value (the value is <code>null</code>).
    *
    * @param    name
    *           Variable name (case-sensitive).
    * @param    type
    *           The data type of the variable.
    * @param    txt
    *           Text representation of the data value.
    */
   public Variable(String name, Class<?> type, String txt)
   {
      this.name = name;
      this.type = type;
      txt = overrideVariables.getOrDefault(name, txt);
      fromString(txt);
   }
   
   /**
    * Define a new instance with a value and an inferred data type.
    *
    * @param    name
    *           Variable name (case-sensitive).
    * @param    value
    *           The data value to store.
    */
   public Variable(String name, Object value)
   {
      this.name  = name;
      this.type  = value.getClass();
      
      String txt = overrideVariables.get(name);
      if (txt != null)
      {
         fromString(txt);
      }
      else
      {
         this.value = value;
      }
   }
   
   /**
    * Get the data value.
    *
    * @return   The current value.
    */
   public synchronized Object getValue()
   {
      return value;
   }
   
   /**
    * Set the data value (type safely).
    *
    * @param    value
    *           The new value.
    *
    * @throws   ClassCastException
    *           If types are not assignment compatible.
    */
   public synchronized void setValue(Object value)
   {
      if (value != null && !type.isInstance(value))
      {
         genClassCastException(value.getClass());
      }
      
      this.value = value;
   }
   
   /**
    * Set the data value.
    *
    * @param    value
    *           The new value.
    *
    * @throws   ClassCastException
    *           If types are not assignment compatible.
    */
   public synchronized void setValue(int value)
   {
      if (type == Integer.TYPE || type == Integer.class)
      {
         this.value = new Integer(value);
      }
      else if (type == Long.TYPE || type == Long.class)
      {
         this.value = new Long(value);
      }
      else if (type == Double.TYPE || type == Double.class)
      {
         this.value = new Double(value);
      }
      else
      {
         genClassCastException(Integer.TYPE);
      }
   }
   
   /**
    * Set the data value.
    *
    * @param    value
    *           The new value.
    *
    * @throws   ClassCastException
    *           If types are not assignment compatible.
    */
   public synchronized void setValue(long value)
   {
      if (type == Integer.TYPE || type == Integer.class)
      {
         this.value = new Integer((int) value);
      }
      else if (type == Long.TYPE || type == Long.class)
      {
         this.value = new Long(value);
      }
      else if (type == Double.TYPE || type == Double.class)
      {
         this.value = new Double(value);
      }
      else
      {
         genClassCastException(Long.TYPE);
      }
   }
   
   /**
    * Set the data value.
    *
    * @param    value
    *           The new value.
    *
    * @throws   ClassCastException
    *           If types are not assignment compatible.
    */
   public synchronized void setValue(double value)
   {
      if (type == Integer.TYPE || type == Integer.class)
      {
         this.value = new Integer((int) value);
      }
      else if (type == Long.TYPE || type == Long.class)
      {
         this.value = new Long((long) value);
      }
      else if (type == Double.TYPE || type == Double.class)
      {
         this.value = new Double(value);
      }
      else
      {
         genClassCastException(Double.TYPE);
      }
   }
   
   /**
    * Set the data value.
    *
    * @param    value
    *           The new value.
    *
    * @throws   ClassCastException
    *           If types are not assignment compatible.
    */
   public synchronized void setValue(boolean value)
   {
      if (type == Boolean.TYPE || type == Boolean.class)
      {
         this.value = new Boolean(value);
      }
      else
      {
         genClassCastException(Boolean.TYPE);
      }
   }
   
   /**
    * Report if the data type is one of the primitive types.
    *
    * @return   <code>true</code> if this is an <code>int</code>, 
    *           <code>long</code>, <code>double</code> or <code>boolean</code>.
    */
   public boolean isPrimitive()
   {
      return type.isPrimitive();
   }
   
   /**
    * Convert the current value into an <code>int</code> (if possible) and 
    * return it.
    *
    * @return   The value or 0 if the value is <code>null</code>.
    *
    * @throws   ClassCastException
    *           If the conversion is from a type which is not supported.
    */
   public int asInt()
   {
      Object value = getValue();
      
      if (value == null)
      {
         return 0;
      }
      
      int result = 0;
      
      if (value instanceof Integer)
      {
         result = ((Integer) value).intValue();
      }
      else if (value instanceof Long)
      {
         result = ((Long) value).intValue();
      }
      else if (value instanceof Double)
      {
         result = ((Double) value).intValue();
      }
      else
      {
         genClassCastException(Integer.TYPE);
      }
      
      return result;
   }   
   
   /**
    * Convert the current value into a <code>long</code> (if possible) and 
    * return it.
    *
    * @return   The value or 0 if the value is <code>null</code>.
    *
    * @throws   ClassCastException
    *           If the conversion is from a type which is not supported.
    */
   public long asLong()
   {
      Object value = getValue();
      
      if (value == null)
      {
         return 0;
      }
      
      long result = 0;
      
      if (value instanceof Integer)
      {
         result = ((Integer) value).intValue();
      }
      else if (value instanceof Long)
      {
         result = ((Long) value).longValue();
      }
      else if (value instanceof Double)
      {
         result = ((Double) value).longValue();
      }
      else
      {
         genClassCastException(Long.TYPE);
      }
      
      return result;
   }   
   
   /**
    * Convert the current value into a <code>double</code> (if possible) and 
    * return it.
    *
    * @return   The value or 0.0 if the value is <code>null</code>.
    *
    * @throws   ClassCastException
    *           If the conversion is from a type which is not supported.
    */
   public double asDouble()
   {
      Object value = getValue();
      
      if (value == null)
      {
         return 0.0;
      }
      
      double result = 0;
      
      if (value instanceof Integer)
      {
         result = ((Integer) value).intValue();
      }
      else if (value instanceof Long)
      {
         result = ((Long) value).longValue();
      }
      else if (value instanceof Double)
      {
         result = ((Double) value).doubleValue();
      }
      else
      {
         genClassCastException(Double.TYPE);
      }
      
      return result;
   }   
   
   /**
    * Convert the current value into a <code>boolean</code> (if possible) and 
    * return it.
    *
    * @return   The value or <code>false</code> if the value is
    *           <code>null</code>.
    *
    * @throws   ClassCastException
    *           If the conversion is from a type which is not supported.
    */
   public boolean asBoolean()
   {
      Object value = getValue();
      
      if (value == null)
      {
         return false;
      }
      
      boolean result = false;
      
      if (value instanceof Boolean)
      {
         result = ((Boolean) value).booleanValue();
      }
      else
      {
         genClassCastException(Boolean.TYPE);
      }
      
      return result;
   }   
   
   /**
    * Convert the given string representation into an instance of the proper
    * data type and assign it into this instance.
    * <p>
    * The following data types are supported:
    * <p>
    * <ul>
    *    <li> String
    *    <li> boolean or Boolean
    *    <li> int     or Integer
    *    <li> long    or Long
    *    <li> double  or Double
    * </ul>
    *
    * @param    txt
    *           Text representation of the data.
    *
    * @throws   RuntimeException
    *           If the conversion is from a type which is not supported.
    */
   public synchronized void fromString(String txt)
   {
      if (type.equals(String.class))
      {
         value = txt;
      }
      else if (type.equals(Boolean.TYPE) || type.equals(Boolean.class))
      {
         value = Boolean.valueOf(txt);
      }
      else if (type.equals(Integer.TYPE) || type.equals(Integer.class))
      {
         value = Integer.valueOf(txt);
      }
      else if (type.equals(Long.TYPE) || type.equals(Long.class))
      {
         value = Long.valueOf(txt);
      }
      else if (type.equals(Double.TYPE) || type.equals(Double.class))
      {
         value = Double.valueOf(txt);
      }
      else
      {
         String spec = "Unknown conversion of '%s' into type '%s'.";
         throw new RuntimeException(String.format(spec, txt, type.getName()));
      }
   }
   
   /**
    * Get the variable's case-sensitive name.
    *
    * @return   The name.
    */
   public synchronized String getName()
   {
      return name;
   }
   
   /**
    * Get the variable's case-sensitive name.
    *
    * @param    name
    *           The name for the variable.
    */
   public synchronized void setName(String name)
   {
      this.name = name;
   }
   
   /**
    * Get the name of the getter method that most closely matches the data
    * type of the variable. Primitive data types need special getters and
    * all others use {@link #getValue}.
    *
    * @return   The name of the getter method for the value of this variable.
    */
   public synchronized String getGetterName()
   {
      String getter = "getValue";
      
      if (type.equals(Boolean.TYPE))
      {
         getter = "asBoolean";
      }
      else if (type.equals(Integer.TYPE))
      {
         getter = "asInt";
      }
      else if (type.equals(Long.TYPE))
      {
         getter = "asLong";
      }
      else if (type.equals(Double.TYPE))
      {
         getter = "asDouble";
      }
      
      return getter;
   }
   
   /**
    * Obtain the short name of the data type.
    *
    * @return   The short name of the data type, suitable for a variable
    *           definition in Java source code. If no short name is known,
    *           the fully qualified class name will be returned.
    */
   public synchronized String getTypeName()
   {
      String typeName = null;
      
      if (type.equals(String.class))
      {
         typeName = "String";
      }
      else if (type.equals(Boolean.TYPE))
      {
         typeName = "boolean";
      }
      else if (type.equals(Boolean.class))
      {
         typeName = "Boolean";
      }
      else if (type.equals(Integer.TYPE))
      {
         typeName = "int";
      }
      else if (type.equals(Integer.class))
      {
         typeName = "Integer";
      }
      else if (type.equals(Long.TYPE))
      {
         typeName = "long";
      }
      else if (type.equals(Long.class))
      {
         typeName = "Long";
      }
      else if (type.equals(Double.TYPE))
      {
         typeName = "double";
      }
      else if (type.equals(Double.class))
      {
         typeName = "Double";
      }
      else
      {
         typeName = type.getName();
      }
      
      return typeName;
   }
   
   /**
    * Obtain the class instance for the described type if it is supported.
    * <p>
    * The following data types are supported:
    * <p>
    * <ul>
    *    <li> String
    *    <li> boolean or Boolean
    *    <li> int     or Integer
    *    <li> long    or Long
    *    <li> double  or Double
    *    <li> Anything that can be obtained via <code>Class.forName()</code>.
    * </ul>
    *
    * @param    cls
    *           Text representation of the class name.
    *
    * @return   The class of the specified type or <code>null</code> if the
    *           type is not supported or recognized.
    */
   public static Class<?> getSupportedType(String cls)
   {
      Class<?> type = null;
      
      if (cls.equals("String"))
      {
         type = String.class;
      }
      else if (cls.equals("boolean"))
      {
         type = Boolean.TYPE;
      }
      else if (cls.equals("Boolean"))
      {
         type = Boolean.class;
      }
      else if (cls.equals("int"))
      {
         type = Integer.TYPE;
      }
      else if (cls.equals("Integer"))
      {
         type = Integer.class;
      }
      else if (cls.equals("long"))
      {
         type = Long.TYPE;
      }
      else if (cls.equals("Long"))
      {
         type = Long.class;
      }
      else if (cls.equals("double"))
      {
         type = Double.TYPE;
      }
      else if (cls.equals("Double"))
      {
         type = Double.class;
      }
      else
      {
         try
         {
            type = Class.forName(cls);
         }
         
         catch (ClassNotFoundException cnfe)
         {
            // ignore
         }
      }
      
      return type;
   }
   
   /**
    * Convert the given indirect variable references into an array of the
    * actual values (in the same order as the original variables).
    *
    * @param    vars
    *           The indirect variables.
    *
    * @return   The array of values.
    */
   public static Object[] resolve(Variable[] vars)
   {
      Object[] real = new Object[vars.length];
      
      for (int i = 0; i < vars.length; i++)
      {
         real[i] = vars[i].getValue();
      }
      
      return real;
   }
   
   /**
    * Generate a class casting exception.
    *
    * @param    source
    *           The type of data trying to be cast from.
    */
   private synchronized void genClassCastException(Class<?> source)
   {
      String spec  = "Cannot set/convert value of type %s to type %s."; 
      throw new ClassCastException(String.format(spec,
                                                 type.getName(),
                                                 source.getName()));
   }
}
