/*
 * Value.java
 *
 * Created on January 4, 2001, 11:37 AM
 */

package org.freehep.graphicsbase.util;

import java.lang.reflect.Constructor;
import java.util.Date;
import java.text.SimpleDateFormat;
import java.text.ParseException;

/**
 * A class that can represent any Java object or primitive. Unlike the
 * built-in primitive proxies (Double, Integer etc) it is mutable. It is
 * used to allow values to be used without needing overloaded methods for
 * each primitive type, and without the overhead of object creation/deletion.
 *
 * When a value is returned by an Object method it should be assumed to be valid
 * only until the next method call to that Object. The use of Value should be
 * avoided in multi-threaded environments.
 *
 * @author  tonyj
 * @version $Id: Value.java 9133 2006-10-13 16:25:43Z turri $
 */
public class Value {

    private int     intValue;
    private short   shortValue;
    private long    longValue;
    private float   floatValue;
    private double  doubleValue;
    private boolean boolValue;
    private byte    byteValue;
    private char    charValue;
    private Object  obj;
    private Class<?>   type;

    public final static Class<?> TYPE_INTEGER = Integer.TYPE;
    public final static Class<?> TYPE_SHORT   = Short.TYPE;
    public final static Class<?> TYPE_LONG    = Long.TYPE;
    public final static Class<?> TYPE_FLOAT   = Float.TYPE;
    public final static Class<?> TYPE_DOUBLE  = Double.TYPE;
    public final static Class<?> TYPE_BOOLEAN = Boolean.TYPE;
    public final static Class<?> TYPE_BYTE    = Byte.TYPE;
    public final static Class<?> TYPE_CHAR    = Character.TYPE;
    public final static Class<?> TYPE_STRING  = String.class;
    public final static Class<?> TYPE_DATE    = Date.class;

    public Value() {}
    
    public Value( Value v ) {
        setValue(v);
    }
    
    public Value setValue(Value v) {
        this.type        = v.getType();
        this.intValue    = v.intValue;
        this.shortValue  = v.shortValue;
        this.longValue   = v.longValue;
        this.floatValue  = v.floatValue;
        this.doubleValue = v.doubleValue;
        this.boolValue   = v.boolValue;
        this.byteValue   = v.byteValue;
        this.charValue   = v.charValue;
        this.obj         = v.obj;
        return this;
    }
    
    /**
     * Get the Value's type
     * @return The Class of this Value.
     *
     */
    public Class<?> getType() {
        return type;
    }

    /**
     * Set the Value's internal value to an integer.
     * @param val The integer value.
     * @return The Value object with the given internal value.
     *
     */
    public Value set(int val)    { intValue    = val; type = TYPE_INTEGER; return this; }

    /**
     * Set the Value's internal value to a short.
     * @param val The short value.
     * @return The Value object with the given internal value.
     *
     */
    public Value set(short val)  { shortValue  = val; type = TYPE_SHORT;   return this; }

    /**
     * Set the Value's internal value to a long.
     * @param val The long value.
     * @return The Value object with the given internal value.
     *
     */
    public Value set(long val)   { longValue   = val; type = TYPE_LONG;    return this; }

    /**
     * Set the Value's internal value to a float.
     * @param val The float value.
     * @return The Value object with the given internal value.
     *
     */
    public Value set(float val)  { floatValue  = val; type = TYPE_FLOAT;   return this; }

    /**
     * Set the Value's internal value to a double.
     * @param val The double value.
     * @return The Value object with the given internal value.
     *
     */
    public Value set(double val) { doubleValue = val; type = TYPE_DOUBLE;  return this; }

    /**
     * Set the Value's internal value to a boolean.
     * @param val The boolean value.
     * @return The Value object with the given internal value.
     *
     */
    public Value set(boolean val){ boolValue   = val; type = TYPE_BOOLEAN; return this; }

    /**
     * Set the Value's internal value to a byte.
     * @param val The byte value.
     * @return The Value object with the given internal value.
     *
     */
    public Value set(byte val)   { byteValue   = val; type = TYPE_BYTE;    return this; }

    /**
     * Set the Value's internal value to a char.
     * @param val The char value.
     * @return The Value object with the given internal value.
     *
     */
    public Value set(char val)   { charValue   = val; type = TYPE_CHAR;    return this; }

    /**
     * Set the Value's internal value to a String.
     * @param val The String value.
     * @return The Value object with the given internal value.
     *
     */
    public Value set(String val) { obj         = val; type = TYPE_STRING;  return this; }

    /**
     * Set the Value's internal value to a Date.
     * @param val The Date value.
     * @return The Value object with the given internal value.
     *
     */
    public Value set(Date val)   { obj         = val; type = TYPE_DATE;    return this; }

    /**
     * Set the Value's internal value to an Object.
     * @param val The Object value.
     * @return The Value object with the given internal value.
     *
     */
    public Value set(Object val) { obj         = val; type = obj == null ? Object.class : obj.getClass(); return this; }

    /**
     * Get the integer value.
     * @return The int value.
     * @exception A ClassCastException is thrown if this Value has incompatible type.
     *
     */
    public int getInt() {
        if (type == TYPE_INTEGER)    return intValue;
        else if (type == TYPE_SHORT) return shortValue;
        else if (type == TYPE_BYTE)  return byteValue;
        else throw new ClassCastException( "getInt cannot be called for type "+type.toString());
    }

    /**
     * Get the short value.
     * @return The short value.
     * @exception A ClassCastException is thrown if this Value has incompatible type.
     *
     */
    public short getShort() {
        if (type == TYPE_SHORT)     return shortValue;
        else if (type == TYPE_BYTE) return byteValue;
        else throw new ClassCastException( "getShort cannot be called for type "+type.toString());
    }

    /**
     * Get the long value.
     * @return The long value.
     * @exception A ClassCastException is thrown if this Value has incompatible type.
     *
     */
    public long getLong() {
        if (type == TYPE_LONG)         return longValue;
        else if (type == TYPE_INTEGER) return intValue;
        else if (type == TYPE_SHORT)   return shortValue;
        else if (type == TYPE_BYTE)    return byteValue;
        else throw new ClassCastException( "getLong cannot be called for type "+type.toString());
    }

    /**
     * Get the float value.
     * @return The float value.
     * @exception A ClassCastException is thrown if this Value has incompatible type.
     *
     */
    public float getFloat() {
        if (type == TYPE_FLOAT)        return floatValue;
        else if (type == TYPE_INTEGER) return intValue;
        else if (type == TYPE_SHORT)   return shortValue;
        else if (type == TYPE_LONG)    return longValue;
        else if (type == TYPE_BYTE)    return byteValue;
        else throw new ClassCastException( "getFloat cannot be called for type "+type.toString());
    }

    /**
     * Get the double value.
     * @return The double value.
     * @exception A ClassCastException is thrown if this Value has incompatible type.
     *
     */
    public double getDouble() {
        if (type == TYPE_DOUBLE)       return doubleValue;
        else if (type == TYPE_INTEGER) return intValue;
        else if (type == TYPE_SHORT)   return shortValue;
        else if (type == TYPE_LONG)    return longValue;
        else if (type == TYPE_FLOAT)   return floatValue;
        else if (type == TYPE_BYTE)    return byteValue;
        else if (type == TYPE_DATE)    return ((Date)obj).getTime();
        else throw new ClassCastException( "getDouble cannot be called for type "+type.toString());
    }

    /**
     * Get the boolean value.
     * @return The boolean value.
     * @exception A ClassCastException is thrown if this Value has incompatible type.
     *
     */
    public boolean getBoolean() {
        if (type == TYPE_BOOLEAN) return boolValue;
        else throw new ClassCastException( "getBoolean cannot be called for type "+type.toString());
    }

    /**
     * Get the byte value.
     * @return The byte value.
     * @exception A ClassCastException is thrown if this Value has incompatible type.
     *
     */
    public byte getByte() {
        if (type == TYPE_BYTE) return byteValue;
        else throw new ClassCastException( "getByte cannot be called for type "+type.toString());
    }

    /**
     * Get the char value.
     * @return The char value.
     * @exception A ClassCastException is thrown if this Value has incompatible type.
     *
     */
    public char getChar() {
        if (type == TYPE_CHAR) return charValue;
        else throw new ClassCastException( "getChar cannot be called for type "+type.toString());
    }

    /**
     * Get the String value.
     * @return The String representation of the internal value.
     *
     */
    public String getString() {
        if (type == TYPE_STRING)       return (String)obj;
        else if (type == TYPE_INTEGER) return String.valueOf(intValue);
        else if (type == TYPE_SHORT)   return String.valueOf(shortValue);
        else if (type == TYPE_LONG)    return String.valueOf(longValue);
        else if (type == TYPE_FLOAT)   return String.valueOf(floatValue);
        else if (type == TYPE_DOUBLE)  return String.valueOf(doubleValue);
        else if (type == TYPE_BOOLEAN) return String.valueOf(boolValue);
        else if (type == TYPE_BYTE)    return String.valueOf(byteValue);
        else if (type == TYPE_CHAR)    return String.valueOf(charValue);
        else if (type == TYPE_DATE)    return ((Date)obj).toString();
        else return obj != null ? obj.toString(): "null";
    }

    /**
     * Get the Date value.
     * @return The Date value.
     * @exception A ClassCastException is thrown if this Value has incompatible type.
     *
     */
    public Date getDate() {
        if (type == TYPE_DATE) return (Date)obj;
        else throw new ClassCastException( "getDate cannot be called for type "+type.toString());
    }

    /**
     * Get the Object value.
     * @return The Object value.
     *
     */
    public Object getObject() {
        if (obj != null)       return obj;
        else if (type == TYPE_INTEGER) return new Integer(intValue);
        else if (type == TYPE_SHORT)   return new Short(shortValue);
        else if (type == TYPE_LONG)    return new Long(longValue);
        else if (type == TYPE_FLOAT)   return new Float(floatValue);
        else if (type == TYPE_DOUBLE)  return new Double(doubleValue);
        else if (type == TYPE_BOOLEAN) return new Boolean(boolValue);
        else if (type == TYPE_BYTE)    return new Byte(byteValue);
        else if (type == TYPE_CHAR)    return new Character(charValue);
        else return null;
    }

    /**
     * Get the String value.
     * @return The String representation of the internal value.
     *
     */
    public String toString() {
        return getString();
    }
    
    /**
     * Returns an external representation of this value
     */
    public String toExternal() {
        // FIXME, does not work for arrays...
        return type.getName()+":"+getString();
    }
     
    /**
     * Set to value from the external respresentation
     */
    public Value fromExternal(String external) throws IllegalArgumentException {
        String[] part = external.split(":", 2);
        if (part.length != 2) 
            throw new IllegalArgumentException(getClass()+": External '"+external+
                                               "'does not contain ':' to separate type from value.");
        
        if (part[0].equals(TYPE_STRING.getName())) {
            return set(part[1]);
        } else if (part[0].equals(TYPE_SHORT.getName())) {
            return set(Short.parseShort(part[1]));
        } else if (part[0].equals(TYPE_LONG.getName())) {
            return set(Long.parseLong(part[1]));
        } else if (part[0].equals(TYPE_FLOAT.getName())) {
            return set(Float.parseFloat(part[1]));
        } else if (part[0].equals(TYPE_DOUBLE.getName())) {
            return set(Double.parseDouble(part[1]));
        } else if (part[0].equals(TYPE_BOOLEAN.getName())) {
            return set(Boolean.getBoolean(part[1]));
        } else if (part[0].equals(TYPE_BYTE.getName())) {
            return set(Byte.parseByte(part[1]));
        } else if (part[0].equals(TYPE_CHAR.getName())) {
            return set(part[1].charAt(0));
        } else if (part[0].equals(TYPE_INTEGER.getName())) {
            return set(Integer.parseInt(part[1]));
        } else if (part[0].equals(TYPE_DATE.getName())) {
            try {
                return set(new SimpleDateFormat().parse(part[1]));
            } catch (ParseException e) {
                throw new IllegalArgumentException(e.getMessage());
            }
        } else if (part[0].equals(Object.class.getName()) && part[1].equals("null")) {
            return set((Object)null);
        } else {
            // FIXME will not work for arrays, which are encoded as "[Lpackagename.classname;"
            try {
                Class<?> cls = Class.forName(part[0]);
                Constructor<?> ctor = cls.getDeclaredConstructor(new Class[] { String.class });
                ctor.setAccessible(true);
                return set(ctor.newInstance(new Object[] { part[1] }));
            } catch (Exception e) {
                throw new IllegalArgumentException(getClass()+": Cannot reconstruct value from type: "+part[0]+", "
                                                   +"and value "+part[1]+", due to "+e.getMessage());
            }
        }
    } 
}
