
package uk.co.wingpath.util;

import java.io.*;
import java.util.*;
import java.text.*;
import java.math.*;
import uk.co.wingpath.event.*;

/**
* This class supports numeric values that may be floating-point or integral,
* and that may be of various sizes.
* <p>The enum {@link Type Numeric.Type} specifies the possible types and sizes.
* Actual numeric values are represented by immutable instances of private
* classes that implement the {@link Value Numeric.Value} interface.
* <p>Each type has an {@link Type#undefined undefined} value, which converts to and from
* an empty string. An undefined value is equal to an undefined value of the
* same type, but not equal to anything else.
* The {@link Numeric#matches Numeric.matches} and
* {@link Numeric.Value#matches Numeric.Value.matches} methods
* interpret an undefined value as a pattern that will match any value.
* <p>Instances of the class {@link Variable Numeric.Variable} may be used to
* store numeric values and monitor changes to them.
*/
public final class Numeric
{
    /**
    * Unsigned decimal radix.
    */
    public static final int RADIX_UNSIGNED_DECIMAL = 0;

    /**
    * Special "radix" used for packing up to 8 UTF-8 characters in an
    * integer value.
    */
    public static final int RADIX_CHAR = -1;

    private static final BigInteger MIN_SIGNED_LONG =
        BigInteger.valueOf (Long.MIN_VALUE);
    private static final BigInteger MAX_SIGNED_LONG =
        BigInteger.valueOf (Long.MAX_VALUE);
    private static final BigInteger TWO_POWER_64 =
        MAX_SIGNED_LONG.add (BigInteger.valueOf (1)).
            multiply (BigInteger.valueOf (2));

    private Numeric ()
    {
    }

    /**
    * This provides a uniform interface to numeric values of various types.
    */
    public interface Value
    {
        /**
        * Gets the type of the numeric value.
        * @return the numeric type.
        */
        Type getType ();

        /**
        * Gets the numeric value as a <code>long</code>.
        * @return the numeric value converted to a <code>long</code>.
        */
        long getLongValue ();

        /**
        * Gets the numeric value as a <code>double</code>.
        * @return the numeric value converted to a <code>double</code>.
        */
        double getDoubleValue ();

        /**
        * Tests whether the value is defined.
        * @return true if the value is defined, false otherwise.
        */
        boolean isDefined ();

        /**
        * Converts the numeric value to a string using the specifed radix.
        * <p>A radix of 10 is interpreted as signed decimal.
        * A radix of -1 is interpreted as UTF-8 characters.
        * All other radixes are interpreted as unsigned.
        * <p>The radix may be 0, which is interpreted to mean unsigned decimal.
        * <p>If the numeric value is of a floating-point type, the radix is
        * ignored and the conversion is done using signed decimal.
        * An undefined value converts to an empty string.
        * @param radix the radix to be used for the conversion.
        * @return the converted value.
        */
        String toString (int radix);

        /**
        * Converts the numeric value to a signed decimal string using the
        * specifed offset and scale.
        * <p>The conversion uses the formula:
        * <blockquote>result = (signed_value + offset) / scale</blockquote>
        * or:
        * <blockquote>result = (unsigned_value + offset) / scale</blockquote>
        * depending on whether the initial value is to be interpreted as signed
        * or unsigned.
        * An undefined value converts to an empty string.
        * @param offset offset to be used for the conversion.
        * @param scale scale to be used for the conversion.
        * @param signed whether the initial value is to be interpreted as
        * signed.
        * @return the converted value.
        */
        String toString (double offset, double scale, boolean signed);

        /**
        * Tests whether this value matches the specified value.
        * An undefined value will match any value of the same type.
        * @param value the value to be matched.
        * @return true if the value matches, false otherwise.
        */
        boolean matches (Value value);
    }

    /**
    * Tests whether a Numeric.Value array matches another Numeric.Value array.
    * <p>Matching of corresponding pairs of values is done using the
    * {@link Numeric.Value#matches Numeric.Value.matches} method.
    * <p>The arrays will not match if they are different sizes.
    * @param a1 first array of values to be matched.
    * @param a2 second array of values to be matched.
    * @return true if the value arrays match, false otherwise.
    */
    public static boolean matches (Value [] a1, Value [] a2)
    {
        if (a1.length != a2.length)
            return false;

        for (int i = 0 ; i < a1.length ; ++i)
        {
            if (!a1 [i].matches (a2 [i]))
                return false;
        }

        return true;
    }

    /**
    * Tests whether integral operations can be used to convert values.
    * @param offset offset to be used to convert values.
    * @param scale scale to be used to convert values.
    * @return {@code true} if integral operations can be used.
    */
    private static boolean canUseIntegral (double offset, double scale)
    {
        double recipScale = 1.0 / scale;
        return
            Math.floor (recipScale) == recipScale &&
            Math.floor (offset) == offset &&
            recipScale >= Long.MIN_VALUE && recipScale <= Long.MAX_VALUE &&
            offset >= Long.MIN_VALUE && offset <= Long.MAX_VALUE;
    }

    private static String radixString (int radix)
    {
        String radixStr = "a";
        switch (radix)
        {
        case 2:
            radixStr = "a binary";
            break;
        case 8:
            radixStr = "an octal";
            break;
        case 10:
            radixStr = "a decimal";
            break;
        case RADIX_UNSIGNED_DECIMAL:
            radixStr = "an unsigned decimal";
            break;
        case 16:
            radixStr = "a hexadecimal";
            break;
        }
        return radixStr;
    }

    private static String typeString (double offset, double scale)
    {
        if (canUseIntegral (offset, scale))
            return "an integer";
        return "a number";
    }

    /**
    * This enum specifies the possible types of numeric values.
    */
    public enum Type
    {
        /**
        * 1-bit integer.
        */
        int1 (false, 0)
        {
            @Override
            public Value createValue (long value)
            {
                if (value == 0)
                    return zero;
                if (value == 1)
                    return one;
                throw new IllegalArgumentException ("Value out of range");
            }

            @Override
            public Value createValue (double value)
            {
                return createValue (longValue (value));
            }

            @Override
            protected boolean checkSignedRange (long val)
            {
                return val == 0 || val == 1;
            }

            @Override
            protected boolean checkUnsignedRange (long val)
            {
                return val == 0 || val == 1;
            }

            @Override
            protected long signedVal (long val)
            {
                return val;
            }

            @Override
            protected void valueError (int radix)
                throws ValueException
            {
                throw new ValueException ("Must be 0 or 1");
            }

            @Override
            protected void valueError (double offset, double scale,
                    boolean signed)
                throws ValueException
            {
                throw new ValueException ("Must be " +
                    toString (0, offset, scale) + " or " +
                    toString (1, offset, scale));
            }
        },

        /**
        * 8-bit integer.
        */
        int8 (false, 1)
        {
            @Override
            public Value createValue (long value)
            {
                if (value == 0)
                    return zero;
                if (value == 1)
                    return one;
                if (value < Byte.MIN_VALUE || value > 0xff)
                    throw new IllegalArgumentException ("Value out of range");
                return new LongValue (this, value & 0xff);
            }

            @Override
            public Value createValue (double value)
            {
                return createValue (longValue (value));
            }

            @Override
            protected boolean checkSignedRange (long val)
            {
                return val >= Byte.MIN_VALUE && val <= Byte.MAX_VALUE;
            }

            @Override
            protected boolean checkUnsignedRange (long val)
            {
                return val >= 0 && val <= 0xff;
            }

            @Override
            protected long signedVal (long val)
            {
                return (byte) val;
            }

            @Override
            protected void valueError (int radix)
                throws ValueException
            {
                String range;
                if (radix == 10)
                {
                    range = Long.toString (Byte.MIN_VALUE) + " to " +
                        Long.toString (Byte.MAX_VALUE);
                }
                else
                {
                    range = "0 to " + toString (0xff, radix);
                }
                throw new ValueException ("Must be " +
                    radixString (radix) + " integer in the range " + range);
            }

            @Override
            protected void valueError (double offset, double scale,
                    boolean signed)
                throws ValueException
            {
                String range;
                if (signed)
                {
                    range = toString (Byte.MIN_VALUE, offset, scale) + " to " +
                        toString (Byte.MAX_VALUE, offset, scale);
                }
                else
                {
                    range = toString (0, offset, scale) + " to " +
                        toString (0xff, offset, scale);
                }
                throw new ValueException ("Must be " +
                    typeString (offset, scale) + " in the range " + range);
            }
        },

        /**
        * 16-bit integer.
        */
        int16 (false, 2)
        {
            @Override
            public Value createValue (long value)
            {
                if (value == 0)
                    return zero;
                if (value == 1)
                    return one;
                if (value < Short.MIN_VALUE || value > 0xffff)
                    throw new IllegalArgumentException ("Value out of range");
                return new LongValue (this, value & 0xffff);
            }

            @Override
            public Value createValue (double value)
            {
                return createValue (longValue (value));
            }

            @Override
            protected boolean checkSignedRange (long val)
            {
                return val >= Short.MIN_VALUE && val <= Short.MAX_VALUE;
            }

            @Override
            protected boolean checkUnsignedRange (long val)
            {
                return val >= 0 && val <= 0xffff;
            }

            @Override
            protected long signedVal (long val)
            {
                return (short) val;
            }

            @Override
            protected void valueError (int radix)
                throws ValueException
            {
                String range;
                if (radix == 10)
                {
                    range = Long.toString (Short.MIN_VALUE) + " to " +
                        Long.toString (Short.MAX_VALUE);
                }
                else
                {
                    range = "0 to " + toString (0xffff, radix);
                }
                throw new ValueException ("Must be " +
                    radixString (radix) + " integer in the range " + range);
            }

            @Override
            protected void valueError (double offset, double scale,
                    boolean signed)
                throws ValueException
            {
                String range;
                if (signed)
                {
                    range = toString (Short.MIN_VALUE, offset, scale) + " to " +
                        toString (Short.MAX_VALUE, offset, scale);
                }
                else
                {
                    range = toString (0, offset, scale) + " to " +
                        toString (0xffff, offset, scale);
                }
                throw new ValueException ("Must be " +
                    typeString (offset, scale) + " in the range " + range);
            }
        },

        /**
        * 32-bit integer
        */
        int32 (false, 4)
        {
            @Override
            public Value createValue (long value)
            {
                if (value == 0)
                    return zero;
                if (value == 1)
                    return one;
                if (value < Integer.MIN_VALUE || value > 0xffffffffL)
                    throw new IllegalArgumentException ("Value out of range");
                return new LongValue (this, value & 0xffffffffL);
            }

            @Override
            public Value createValue (double value)
            {
                return createValue (longValue (value));
            }

            @Override
            protected boolean checkSignedRange (long val)
            {
                return val >= Integer.MIN_VALUE && val <= Integer.MAX_VALUE;
            }

            @Override
            protected boolean checkUnsignedRange (long val)
            {
                return val >= 0 && val <= 0xffffffffL;
            }

            @Override
            protected long signedVal (long val)
            {
                return (int) val;
            }

            @Override
            protected void valueError (int radix)
                throws ValueException
            {
                String range;
                if (radix == 10)
                {
                    range = Long.toString (Integer.MIN_VALUE) + " to " +
                        Integer.toString (Integer.MAX_VALUE);
                }
                else
                {
                    range = "0 to " + toString (0xffffffffL, radix);
                }
                throw new ValueException ("Must be " +
                    radixString (radix) + " integer in the range " + range);
            }

            @Override
            protected void valueError (double offset, double scale,
                    boolean signed)
                throws ValueException
            {
                String range;
                if (signed)
                {
                    range = toString (Integer.MIN_VALUE, offset, scale) +
                        " to " + toString (Integer.MAX_VALUE, offset, scale);
                }
                else
                {
                    range = toString (0, offset, scale) + " to " +
                        toString (0xffffffffL, offset, scale);
                }
                throw new ValueException ("Must be " +
                    typeString (offset, scale) + " in the range " + range);
            }
        },

        /**
        * 64-bit integer.
        */
        int64 (false, 8)
        {
            @Override
            public Value createValue (long value)
            {
                if (value == 0)
                    return zero;
                if (value == 1)
                    return one;
                return new LongValue (this, value);
            }

            @Override
            public Value createValue (double value)
            {
                return createValue (longValue (value));
            }

            @Override
            protected boolean checkSignedRange (long val)
            {
                return true;
            }

            @Override
            protected boolean checkUnsignedRange (long val)
            {
                return true;
            }

            @Override
            protected long signedVal (long val)
            {
                return val;
            }

            @Override
            protected void valueError (int radix)
                throws ValueException
            {
                throw new ValueException ("Must be " +
                    radixString (radix) + " integer");
            }

            @Override
            protected void valueError (double offset, double scale,
                    boolean signed)
                throws ValueException
            {
                if (scale == 1.0 && offset == 0.0)
                    valueError (signed ? 10 : RADIX_UNSIGNED_DECIMAL);
                String range;
                if (signed)
                {
                    range = toString (Long.MIN_VALUE, offset, scale) + " to " +
                        toString (Long.MAX_VALUE, offset, scale);
                }
                else
                {
                    range = toString (0, offset, scale) + " to " +
                        toString (Long.MAX_VALUE - (double) Long.MIN_VALUE,
                            offset, scale);
                }
                throw new ValueException ("Must be " +
                    typeString (offset, scale) + " in the range " + range);
            }
        },

        /**
        * 32-bit float.
        */
        float32 (true, 4)
        {
            @Override
            public Value createValue (long value)
            {
                return createValue ((double) value);
            }

            @Override
            public Value createValue (double value)
            {
                if (value == 0.0)
                    return zero;
                if (value == 1.0)
                    return one;
                // Don't bother with range check - if value is too big, it will
                // convert to an infinity.
                return new DoubleValue (this, (float) value);
            }

            @Override
            protected boolean checkSignedRange (long val)
            {
                return true;
            }

            @Override
            protected boolean checkUnsignedRange (long val)
            {
                return true;
            }

            @Override
            protected long signedVal (long val)
            {
                return val;
            }

            @Override
            protected void valueError (int radix)
                throws ValueException
            {
                // Don't bother telling user the range - we aren't checking it!
                // Values out of range will convert to infinity.
                throw new ValueException ("Must be a decimal number");
            }

            @Override
            protected void valueError (double offset, double scale,
                    boolean signed)
                throws ValueException
            {
                // Don't bother telling user the range - we aren't checking it!
                // Values out of range will convert to infinity.
                throw new ValueException ("Must be a decimal number");
            }

            @Override
            public Numeric.Value fromString (String value, int radix)
                throws ValueException
            {
                if (value.equals (""))
                    return undef;

                try
                {
                    float f = Float.parseFloat (value.trim ());
                    return createValue (f);
                }
                catch (NumberFormatException e)
                {
                    valueError (radix);
                }

                throw new AssertionError ("Unreachable");
            }

            @Override
            protected String toString (double value, double offset,
                double scale)
            {
                if (offset == 0.0 && scale == 1.0)
                    return Float.toString ((float) value);
                double d = (value + offset) / scale;
                return Double.toString (d);
            }

            @Override
            protected String toString (double value)
            {
                return Float.toString ((float) value);
            }
        },

        /**
        * 64-bit float.
        */
        float64 (true, 8)
        {
            @Override
            public Value createValue (long value)
            {
                return createValue ((double) value);
            }

            @Override
            public Value createValue (double value)
            {
                if (value == 0.0)
                    return zero;
                if (value == 1.0)
                    return one;
                return new DoubleValue (this, value);
            }

            @Override
            protected boolean checkSignedRange (long val)
            {
                return true;
            }

            @Override
            protected boolean checkUnsignedRange (long val)
            {
                return true;
            }

            @Override
            protected long signedVal (long val)
            {
                return val;
            }

            @Override
            protected void valueError (int radix)
                throws ValueException
            {
                // Don't bother telling user the range - we aren't checking it!
                // Values out of range will convert to infinity.
                throw new ValueException ("Must be a decimal number");
            }

            @Override
            protected void valueError (double offset, double scale,
                    boolean signed)
                throws ValueException
            {
                // Don't bother telling user the range - we aren't checking it!
                // Values out of range will convert to infinity.
                throw new ValueException ("Must be a decimal number");
            }
        };

        public final Value zero;
        public final Value one;
        public final Value undef;
        private final boolean isFloat;
        private final int size;

        private Type (boolean isFloat, int size)
        {
            this.isFloat = isFloat;
            this.size = size;
            zero = isFloat ? new DoubleValue (this, 0) : new LongValue (this, 0);
            one = isFloat ? new DoubleValue (this, 1) : new LongValue (this, 1);
            undef = isFloat ? new DoubleValue (this) : new LongValue (this);
        }

        /**
        * Gets the number of bytes used by values of this type.
        * <p>For example, 4 is returned for the <code>int32</code> type, and
        * 8 is returned for the <code>float64</code> type.
        * As a special case, 0 is returned for the <code>int1</code> type.
        * @return size in bytes.
        */
        public int getSize ()
        {
            return size;
        }

        /**
        * Returns whether this is a floating-point type.
        * @return <code>true</code> for floating-point types, <code>false</code>
        * for integral types.
        * @return whether type is floating-point.
        */
        public boolean isFloat ()
        {
            return isFloat;
        }

        /**
        * For internal use only.
        */
        protected abstract boolean checkSignedRange (long val);

        /**
        * For internal use only.
        */
        protected abstract boolean checkUnsignedRange (long val);

        /**
        * For internal use only.
        */
        protected boolean checkRange (long val)
        {
            return checkSignedRange (val) || checkUnsignedRange (val);
        }

        /**
        * For internal use only.
        */
        protected abstract long signedVal (long val);

        /**
        * For internal use only.
        */
        protected abstract void valueError (int radix)
            throws ValueException;

        /**
        * For internal use only.
        */
        protected abstract void valueError (double offset, double scale,
                boolean signed)
            throws ValueException;

        /**
        * Constructs a numeric value of this type by parsing the specified
        * string using the specified radix.
        * @param value the string to be parsed.
        * @param radix the radix to be used for parsing the string.
        * @return the numeric value.
        * @throws ValueException if the string cannot be converted to a value
        * of this type.
        */
        public Numeric.Value fromString (String value, int radix)
            throws ValueException
        {
            try
            {
                if (isFloat)
                {
                    if (value.equals (""))
                        return undef;
                    double d = Double.parseDouble (value.trim ());
                    return createValue (d);
                }
                else
                {
                    if (radix == RADIX_CHAR)
                    {
                        try
                        {
                            byte [] data = value.getBytes ("UTF-8");
                            if (data.length > size)
                            {
                                throw new ValueException (
                                    "Must be character" +
                                    (size == 1 ? "" : "s") + 
                                    " representible in " +
                                    "UTF-8 using " + size +
                                    " byte" + (size == 1 ? "" : "s"));
                            }
                            long n = 0;
                            for (int i = 0 ; i < size ; i++)
                            {
                                n <<= 8;
                                if (i < data.length)
                                    n |= (data [i] & 0xff);
                                else
                                    n |= ' ';
                            }
                            return createValue (n);
                        }
                        catch (UnsupportedEncodingException e)
                        {
                            // Shouldn't happen - UTF-8 should be supported.
                            throw new AssertionError ("Unreachable");
                        }
                    }
                    if (value.equals (""))
                        return undef;
                    if (radix == RADIX_UNSIGNED_DECIMAL && size == 8)
                    {
                        // Unsigned decimal long - parseLong can't handle this
                        // case, so use BigInteger.

                        BigInteger bi = new BigInteger (value.trim ());
                        if (bi.compareTo (BigInteger.ZERO) < 0)
                            valueError (radix);
                        if (bi.compareTo (TWO_POWER_64) >= 0)
                            valueError (radix);
                        if (bi.compareTo (MAX_SIGNED_LONG) > 0)
                            bi = bi.subtract (TWO_POWER_64);
                        return createValue (bi.longValue ());
                    }

                    long val = Long.parseLong (value.trim (),
                        radix == RADIX_UNSIGNED_DECIMAL ? 10 : radix);

                    if (radix == 10)
                    {
                        if (!checkSignedRange (val))
                            valueError (radix);
                    }
                    else
                    {
                        if (!checkUnsignedRange (val))
                            valueError (radix);
                    }

                    return createValue (val);
                }
            }
            catch (NumberFormatException e)
            {
                valueError (radix);
            }

            throw new AssertionError ("Unreachable");
        }

        /**
        * Constructs a numeric value of this type by parsing the specified
        * string as signed decimal and converting it using the specified
        * offset and scale.
        * The conversion is done using the formula:
        * <blockquote>result = value * scale - offset</blockquote>
        * An empty string will convert to an undefined value.
        * @param value the string to be parsed.
        * @param offset offset to be used to convert the value
        * @param scale scale to be used to convert the value
        * @param signed whether the result is to be interpreted as signed
        * or unsigned. This is used for checking whether the result is in
        * the allowable range for this type.
        * @return the numeric value.
        * @throws ValueException if the string cannot be converted to a value
        * of this type.
        */
        public Numeric.Value fromString (String value, double offset,
                double scale, boolean signed)
            throws ValueException
        {
            if (value.equals (""))
                return undef;

            if (offset == 0.0 && scale == 1.0)
                return fromString (value, signed ? 10 : RADIX_UNSIGNED_DECIMAL);

            try
            {
                if (isFloat)
                {
                    double d = Double.parseDouble (value.trim ());
                    d = d * scale - offset;
                    return createValue (d);
                }
                if (canUseIntegral (offset, scale))
                {
                    // Entered value has to be integral, and in a range
                    // where we can convert it using integral operations.
                    BigInteger bi = new BigInteger (value.trim ());
                    bi = bi.divide (BigInteger.valueOf ((long) (1.0 / scale)));
                    bi = bi.subtract (BigInteger.valueOf ((long) offset));
                    long val = bi.longValue ();
                    if (signed)
                    {
                        if (bi.compareTo (MIN_SIGNED_LONG) < 0 ||
                            bi.compareTo (MAX_SIGNED_LONG) > 0 ||
                            !checkSignedRange (val))
                        {
                            valueError (offset, scale, signed);
                        }
                    }
                    else
                    {
                        if (bi.compareTo (BigInteger.ZERO) < 0 ||
                            bi.compareTo (TWO_POWER_64) >= 0 ||
                            !checkUnsignedRange (val))
                        {
                            valueError (offset, scale, signed);
                        }
                    }
                    return createValue (val);
                }

                double d = Double.parseDouble (value.trim ());
                d = d * scale - offset;
                if (d < Long.MIN_VALUE || d > Long.MAX_VALUE ||
                    Double.isNaN (d))
                {
                    valueError (offset, scale, signed);
                }
                long val = (long) d;

                if (!(signed ? checkSignedRange (val) :
                    checkUnsignedRange (val)))
                {
                    valueError (offset, scale, signed);
                }

                return createValue (val);
            }
            catch (NumberFormatException e)
            {
                valueError (offset, scale, signed);
            }

            throw new AssertionError ("Unreachable");
        }

        protected String toString (long value, int radix)
        {
            switch (radix)
            {
            case RADIX_CHAR:
                {
                    byte [] data = new byte [size];
                    long n = value;
                    for (int i = data.length - 1 ; i >= 0 ; i--)
                    {
                        data [i] = (byte) n;
                        n >>= 8;
                    }
                    try
                    {
                        // Not all byte sequences are valid UTF-8, so
                        // the string returned here is not guaranteed to
                        // convert back to the same sequence of bytes.
                        return new String (data, "UTF-8");
                    }
                    catch (UnsupportedEncodingException e)
                    {
                        // Shouldn't happen - UTF-8 should be supported.
                        throw new AssertionError ("Unreachable");
                    }
                }
            case 2:
                String s = Long.toBinaryString (value);
                while (s.length () < size * 8)
                    s = "0" + s;
                return s;

            case 8:
                return Long.toOctalString (value);

            case 16:
                return Long.toHexString (value);

            case 10:
                return "" + signedVal (value);

            case RADIX_UNSIGNED_DECIMAL:
                // Unsigned decimal

                if (size != 8 || value >= 0)
                    return "" + value;

                // We have a negative long that we want to convert to
                // unsigned decimal. Use BigInteger to do it.

                BigInteger bi = BigInteger.valueOf (value);
                bi = bi.add (TWO_POWER_64);
                return bi.toString ();

            default:
                throw new IllegalArgumentException (
                    "Invalid radix " + radix);
            }
        }

        protected String toString (long value, double offset, double scale)
        {
            if (canUseIntegral (offset, scale))
            {
                BigInteger bi = BigInteger.valueOf (value);
                bi = bi.add (BigInteger.valueOf ((long) offset));
                bi = bi.multiply (BigInteger.valueOf ((long) (1.0 / scale)));
                return bi.toString ();
            }
            return toString ((double) value, offset, scale);
        }

        protected String toString (double value, double offset, double scale)
        {
            double d = (value + offset) / scale;
            return Double.toString (d);
        }

        protected String toString (double value)
        {
            return Double.toString (value);
        }

        /**
        * Constructs a numeric value of this type corresponding to the
        * specified long value.
        * @param value the long value to be used for constructing the numeric
        * value.
        * @return a numeric value corresponding to the long value.
        * @throws IllegalArgumentException if the value is out of range for
        * this type.
        */
        public abstract Value createValue (long value);

        /**
        * Constructs a numeric value of this type corresponding to the
        * specified double value.
        * @param value the double value to be used for constructing the numeric
        * value.
        * @return a numeric value corresponding to the double value.
        * @throws IllegalArgumentException if the value is out of range for
        * this type.
        */
        public abstract Value createValue (double value);

        /**
        * For internal use only.
        */
        protected long longValue (double value)
        {
            if (value < Long.MIN_VALUE ||
                value > Long.MAX_VALUE ||
                Double.isNaN (value))
            {
                throw new IllegalArgumentException ("Value out of range");
            }
            return (long) value;
        }

        /**
        * Constructs a numeric value of this type corresponding to the
        * specified numeric value.
        * @param value the numeric value to be used for constructing the numeric
        * value.
        * @return a numeric value corresponding to the supplied numeric value.
        * @throws IllegalArgumentException if the value is out of range for
        * this type.
        */
        public Value createValue (Value value)
        {
            if (isFloat ())
                return createValue (value.getDoubleValue ());
            return createValue (value.getLongValue ());
        }

        /**
        * Constructs an array of zero values of this type.
        * @param size array size.
        * @return array of zero numeric values.
        */
        public Value [] createZeroArray (int size)
        {
            Value [] a = new Value [size];

            for (int i = 0 ; i < size ; i++)
                a [i] = zero;

            return a;
        }

        /**
        * Constructs an array of undefined values of this type.
        * @param size array size.
        * @return array of undefined numeric values.
        */
        public Value [] createUndefArray (int size)
        {
            Value [] a = new Value [size];

            for (int i = 0 ; i < size ; i++)
                a [i] = undef;

            return a;
        }
    }

    private static class LongValue
        implements Value
    {
        private final Type type;
        private final long value;
        private final boolean defined;

        private LongValue (Type type, long value, boolean defined)
        {
            this.type = type;
            this.value = value;
            this.defined = defined;
        }

        private LongValue (Type type, long value)
        {
            this (type, value, true);
        }

        private LongValue (Type type)
        {
            this (type, 0, false);
        }

        @Override
        public Type getType ()
        {
            return type;
        }

        @Override
        public long getLongValue ()
        {
            return value;
        }

        @Override
        public double getDoubleValue ()
        {
            return type.signedVal (value);
        }

        @Override
        public boolean isDefined ()
        {
            return defined;
        }

        @Override
        public boolean equals (Object obj)
        {
            if (!(obj instanceof LongValue))
                return false;
            LongValue lv = (LongValue) obj;
            if (lv.getType () != type)
                return false;
            if (lv.defined != defined)
                return false;
            return lv.value == value;
        }

        @Override
        public boolean matches (Value v)
        {
            if (!(v instanceof LongValue))
                return false;
            LongValue lv = (LongValue) v;
            if (lv.getType () != type)
                return false;
            if (!defined || !lv.defined)
                return true;
            return lv.value == value;
        }

        @Override
        public int hashCode ()
        {
            return (int) value;
        }

        @Override
        public String toString (int radix)
        {
            if (!defined)
                return "";
            return type.toString (value, radix);
        }

        @Override
        public String toString (double offset, double scale,
            boolean signed)
        {
            if (!defined)
                return "";
            return type.toString (signed ? type.signedVal (value) : value,
                offset, scale);
        }

        @Override
        public String toString ()
        {
            return toString (10);
        }
    }

    private static class DoubleValue
        implements Value
    {
        private final Type type;
        private final double value;
        private final boolean defined;

        private DoubleValue (Type type, double value, boolean defined)
        {
            this.type = type;
            this.value = value;
            this.defined = defined;
        }

        private DoubleValue (Type type, double value)
        {
            this (type, value, true);
        }

        private DoubleValue (Type type)
        {
            this (type, 0.0, false);
        }

        @Override
        public Type getType ()
        {
            return type;
        }

        @Override
        public long getLongValue ()
        {
            if (value < Long.MIN_VALUE ||
                value > Long.MAX_VALUE ||
                Double.isNaN (value))
            {
                return 0;
            }
            return (long) value;
        }

        @Override
        public double getDoubleValue ()
        {
            return value;
        }

        @Override
        public boolean isDefined ()
        {
            return defined;
        }

        @Override
        public boolean equals (Object obj)
        {
            if (!(obj instanceof DoubleValue))
                return false;
            DoubleValue dv = (DoubleValue) obj;
            if (dv.getType () != type)
                return false;
            if (dv.defined != defined)
                return false;
            if (Double.isNaN (value))
                return Double.isNaN (dv.value);
            return dv.value == value;
        }

        @Override
        public boolean matches (Value v)
        {
            if (!(v instanceof DoubleValue))
                return false;
            DoubleValue dv = (DoubleValue) v;
            if (dv.getType () != type)
                return false;
            if (!defined || !dv.defined)
                return true;
            return dv.value == value;
        }

        @Override
        public int hashCode ()
        {
            return (int) value;
        }

        @Override
        public String toString (int radix)
        {
            if (!defined)
                return "";
            return type.toString (value);
        }

        @Override
        public String toString (double offset, double scale,
            boolean signed)
        {
            if (!defined)
                return "";
            return type.toString (value, offset, scale);
        }

        @Override
        public String toString ()
        {
            return toString (10);
        }
    }

    /**
    * <p>Instances of this class may be used to store numeric values and
    * monitor changes to them.
    */
    public static class Variable
        extends uk.co.wingpath.util.SimpleVariable<Value>
    {
        /**
        * Constructs a <code>Numeric.Variable</code> with initial type
        * <code>float64</code> and initial value 0.
        */
        public Variable ()
        {
            super (Type.float64.zero);
        }

        /**
        * Sets the value of the variable using the supplied long value.
        * The new value is constructed from the supplied long value
        * using the type of the old value.
        * @param value the long value to be used to construct the new numeric
        * value.
        */
        public void setValue (long value)
        {
            setValue (getValue ().getType ().createValue (value));
        }

        /**
        * Sets the value of the variable using the supplied double value.
        * The new value is constructed from the supplied double value
        * using the type of the old value.
        * @param value the double value to be used to construct the new numeric
        * value.
        */
        public void setValue (double value)
        {
            setValue (getValue ().getType ().createValue (value));
        }
    }

    /**
    * Converts a Numeric.Value array to a byte array.
    * @param a the array to be coenverted.
    * @return the converted array.
    */
    public static byte [] toByteArray (Numeric.Value [] a)
    {
        byte [] b = new byte [a.length];

        for (int i = 0 ; i < a.length ; ++i)
            b [i] = (byte) a [i].getLongValue ();

        return b;
    }

    /**
    * Converts a byte array to a Numeric.Value array.
    * @param a the array to be coenverted.
    * @return the converted array.
    */
    public static Numeric.Value [] fromByteArray (byte [] a)
    {
        Numeric.Value [] b = Type.int8.createUndefArray (a.length);

        for (int i = 0 ; i < a.length ; ++i)
            b [i] = Type.int8.createValue (a [i]);

        return b;
    }
}

