
package uk.co.wingpath.modbus;

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

/**
* Provides a model for storage of values in registers.
* <p>A {@code ModbusModel} provides methods for packing register
* values into Modbus messages, and unpacking Modbus messages into register
* values.
* The model uses an {@link AddressMap} and a {@link BigValueFlags} to control
* the packing and unpacking.
* Note that many of the provided methods have package scope.
* <p>This class is abstract and does not provide an implementation of the
* registers themselves. A concrete extension will need to override
* some or all of the following methods in order to provide a register
* implementation:
* <ul>
* <li>{@link #isFloat(int)}
* <li>{@link #setValue(int,double)}
* <li>{@link #setValue(int,long)}
* <li>{@link #getDoubleValue(int)}
* <li>{@link #getLongValue(int)}
* <li>{@link #getValueSize(int)}
* <li>{@link #isSigned(int)}
* <li>{@link #getRadix(int)}
* <li>{@link #getAddresses()}
* <li>{@link #getValueSizes()}
* <li>{@link #writable(int)}
* </ul>
* <p>All these methods have default implementations - see the
* method descriptions for details.
*/
public abstract class ModbusModel
{
    private BigValueFlags bigValueFlags;
    private AddressMap map;

    /**
    * Constructs a ModbusModel with the specified big-value flags and
    * address map.
    * @param bigValueFlags the big-value flags.
    * @param map the address map.
    */
    public ModbusModel (BigValueFlags bigValueFlags, AddressMap map)
    {
        if (bigValueFlags == null)
            bigValueFlags = new BigValueFlags ();
        if (map == null)
            map = new AddressMap ();
        this.bigValueFlags = bigValueFlags;
        this.map = map;
    }

    /**
    * Constructs a ModbusModel with the specified big-value flags and a null
    * address map.
    * @param bigValueFlags the big-value flags.
    */
    public ModbusModel (BigValueFlags bigValueFlags)
    {
        this (bigValueFlags, null);
    }

    /**
    * Does the register at the specified address contain a floating-point
    * value?
    * <p>The default implementation of this method returns {@code false}.
    * @param address "model" address of the register.
    * @return {@code true} if the register value is floating-point,
    * {@code false} if the register value is integral.
    * @throws ModbusException if the register address is invalid.
    * This exception does not need a help ID or message as it is always
    * caught within the modbus package.
    */
    public boolean isFloat (int address)
        throws ModbusException
    {
        return false;
    }

    /**
    * Sets the value of the register at the specified address to the
    * specified long value.
    * <p>The default implementation of this method calls
    * {@link #setValue(int,double) }.
    * @param address "model" address of the register.
    * @param value the value to stored in the register.
    * @throws ModbusException if the register address is invalid.
    * This exception does not need a help ID or message as it is always
    * caught within the modbus package.
    */
    public void setValue (int address, long value)
        throws ModbusException
    {
        setValue (address, (double) value);
    }

    /**
    * Sets the value of the register at the specified address to the
    * specified double value.
    * <p>The default implementation of this method throws an
    * {@link IllegalStateException}.
    * @param address "model" address of the register.
    * @param value the value to stored in the register.
    * @throws ModbusException if the register address is invalid.
    * This exception does not need a help ID or message as it is always
    * caught within the modbus package.
    */
    public void setValue (int address, double value)
        throws ModbusException
    {
        throw new IllegalStateException ("Method setValue not implemented");
    }

    /**
    * Gets the value of the register at the specified address as a long.
    * <p>The default implementation of this method calls
    * {@link #getDoubleValue(int)} and converts the returned value to a long.
    * @param address "model" address of the register.
    * @return The register value.
    * @throws ModbusException if the register address is invalid.
    * This exception does not need a help ID or message as it is always
    * caught within the modbus package.
    */
    public long getLongValue (int address)
        throws ModbusException
    {
        return (long) getDoubleValue (address);
    }

    /**
    * Gets the value of the register at the specified address as a double.
    * <p>The default implementation of this method throws an
    * {@link IllegalStateException}.
    * @param address "model" address of the register.
    * @return The register value.
    * @throws ModbusException if the register address is invalid.
    * This exception does not need a help ID or message as it is always
    * caught within the modbus package.
    */
    public double getDoubleValue (int address)
        throws ModbusException
    {
        throw new IllegalStateException (
            "Method getDoubleValue not implemented");
    }

    /**
    * Gets the size of the register at the specifed address.
    * <p>The default implementation of this method returns 2.
    * @param address "model" address of the register.
    * @return Register size in bytes. A size of 0 means 1-bit.
    * @throws ModbusException if no register at the specified address.
    * This exception does not need a help ID or message as it is always
    * caught within the modbus package.
    */
    public int getValueSize (int address)
        throws ModbusException
    {
        return 2;
    }

    /**
    * Tests whether the value of the register at the specifed address is
    * signed or unsigned.
    * This is used only for register value tracing.
    * <p>The default implementation of this method returns {@code true}.
    * @param address "model" address of the register.
    * @return true if the value is signed, false if it is unsigned.
    * @throws ModbusException if no register at the specified address.
    * This exception does not need a help ID or message as it is always
    * caught within the modbus package.
    */
    public boolean isSigned (int address)
        throws ModbusException
    {
        return true;
    }

    /**
    * Gets the radix of the value of the register at the specifed address.
    * This is used only for register value tracing.
    * <p>The default implementation of this method returns 10.
    * @param address "model" address of the register.
    * @return the radix of the value.
    * @throws ModbusException if no register at the specified address.
    * This exception does not need a help ID or message as it is always
    * caught within the modbus package.
    */
    public int getRadix (int address)
        throws ModbusException
    {
        return 10;
    }

    /**
    * Gets all valid "model" addresses.
    * <p>The default implementation of this method throws an
    * {@link IllegalStateException}.
    * <p>This method is only called by the {@code ModbusModel} "check" methods.
    * If you do not want to call any of the "check" methods, then you do not
    * need to override this method.
    * @return Array of all valid register addresses, sorted into increasing
    * order.
    */
    public int [] getAddresses ()
    {
        throw new IllegalStateException ("Method getAddresses not implemented");
    }

    /**
    * Gets all register sizes.
    * <p>Each element of the array returned by this method should be the same
    * as the size that would be returned by {@link #getValueSize} for the
    * corresponding element of the array returned by {@link #getAddresses}.
    * <p>The default implementation of this method throws an
    * {@link IllegalStateException}.
    * <p>This method is only called by the {@code ModbusModel} "check" methods.
    * If you do not want to call any of the "check" methods, then you do not
    * need to override this method.
    * @return array of all register sizes.
    */
    public int [] getValueSizes ()
    {
        throw new IllegalStateException (
            "Method getValueSizes not implemented");
    }

    /**
    * Are Modbus masters allowed to write to the register at the specified
    * model address?
    * <p>The default implementation of this method returns {@code true}.
    * @param address address of register.
    * @return {@code true} if the register may be written to.
    * @throws ModbusException if the register address is invalid.
    * This exception does not need a help ID or message as it is always
    * caught within the modbus package.
    */
    public boolean writable (int address)
        throws ModbusException
    {
        return true;
    }

    private int nextAddress (int address, int size)
    {
        return address + bigValueFlags.addressIncrement (size);
    }

    private int nextAddress (BigValueFlags bigValueFlags, int address, int size)
    {
        return address + bigValueFlags.addressIncrement (size);
    }

    /**
    * This class provides "iterators" for stepping through Modbus addresses
    * one value at a time.
    */
    private class Stepper
    {
        int address;                // Model address
        final AddressMap.Map space; // Address space
        int offset;                 // Byte offset in message data
        int shift;                  // Bit offset within data byte
        int count;                  // Count to be used in message

        /**
        * Creates a {@code Stepper} starting at the specified model address in
        * the specified address space.
        * <p>The starting address must be in the specified address space,
        * but there need not be a defined register at the address.
        * @param address the starting model address.
        * @param space the address space.
        */
        Stepper (int address, AddressMap.Map space)
        {
            this.address = address;
            this.space = space;
            assert space.inRange (address);
            offset = 0;
            shift = 0;
            count = 0;
        }

        /**
        * Gets the current model address.
        * @return the current model address.
        */
        int getAddress ()
        {
            return address;
        }

        /**
        * Gets the size of the value at the current address.
        * @return size of the current value in bytes, or 0 for a 1-bit value.
        * @throws ModbusException if the current address is not in the
        * address space (the stepper has been stepped off the end of the
        * space), or if there is no defined register at the current address.
        */
        int getSize ()
            throws ModbusException
        {
            if (!space.inRange (address))
            {
                // Have gone off the end of the address space.
                Modbus.addressError ("M001",
                    "Message address " +
                    space.fromModel (new ModelAddress (address, shift)) +
                    " is not in the " + space.getName () + " range");
            }
            int size;
            try
            {
                size = getValueSize (address);
            }
            catch (ModbusException e)
            {
                // No register at the current address.
                Modbus.addressError ("M013",
                    "No " + space.getName () + " at model address " + address +
                    " (message address " +
                    space.fromModel (new ModelAddress (address, shift)) + ")");
                throw new AssertionError ("Unreachable");
            }
            if (size != 0)
            {
                if (shift != 0)
                {
                    shift = 0;
                    offset++;
                }
            }

            return size;
        }

        /**
        * Checks that the register at the current address is writable.
        * @throws ModbusException if there is no register at the current
        * address or if the register is not writable.
        */
        void checkWritable ()
            throws ModbusException
        {
            getSize ();     // Check address for validity
            if (!writable (address))
            {
                Modbus.addressError ("M002",
                    "Register " + address +
                    " (message address " +
                    space.fromModel (new ModelAddress (address, shift)) +
                    ") is not writable");
            }
        }

        /**
        * Gets the total number of bytes that have been stepped over.
        * @return total size in bytes.
        */
        int getTotalSize ()
        {
            return shift == 0 ? offset : offset + 1;
        }

        /**
        * Gets the count that should be in the Modbus message for the
        * values that have been stepped over.
        * <p>If the {@link BigValueFlags "word count"} flag is set,
        * this will be the number of 16-bit words,
        * otherwise it will be the number of values.
        * @return message count.
        */
        int getCount ()
        {
            if (bigValueFlags.getWordCount ())
                return (getTotalSize () + 1) / 2;
            else
                return count;
        }

        /**
        * Moves the stepper on to the next address.
        * <p>In order to step, the stepper must get the size of the value
        * at the <emphasis>current</emphasis> adddress, which means that
        * there must be a defined register at the current address that is
        * in the address space. There is no requirement that the
        * <emphasis>next</emphasis> address is in the space or has a defined
        * register.
        * @throws ModbusException if the current address is not in the
        * address space (the stepper has been stepped off the end of the
        * space), or if there is no defined register at the current address.
        */
        void step ()
            throws ModbusException
        {
            int size = getSize ();
            if (size == 0)
            {
                shift++;
                if (shift == 8)
                {
                    shift = 0;
                    offset++;
                }
                address++;
            }
            else
            {
                offset += size;
                address = nextAddress (address, size);
            }
            count++;
        }
    }

    /**
    * Converts number of values to count (as passed in Modbus message).
    * @param address model address of first register.
    * @param nvalues number of values to be transferred.
    * @param space address space of register
    * @return count to be inserted into Modbus message.
    * @throws ModbusException if the address/nvalues is invalid.
    */
    public int getCount (int address, int nvalues, AddressMap.Map space)
        throws ModbusException
    {
        Stepper stepper = new Stepper (address, space);

        while (nvalues > 0)
        {
            stepper.step ();
            nvalues--;
        }

        return stepper.getCount ();
    }

    /**
    * Converts number of values to number of bytes in Modbus message.
    * @param address model address of first register.
    * @param nvalues number of values to be transferred.
    * @param space address space of register
    * @return number of data bytes.
    * @throws ModbusException if the address/nvalues is invalid.
    */
    public int getTotalBytes (int address, int nvalues, AddressMap.Map space)
        throws ModbusException
    {
        Stepper stepper = new Stepper (address, space);

        while (nvalues > 0)
        {
            stepper.step ();
            nvalues--;
        }

        return stepper.getTotalSize ();
    }

    /**
    * Converts count (as passed in Modbus message) to number of values.
    * @param address model address of first register.
    * @param count count extracted from Modbus message.
    * @param space address space of register
    * @return number of values to be transferred.
    * @throws ModbusException if the address/count is invalid.
    */
    public int getNumValues (int address, int count, AddressMap.Map space)
        throws ModbusException
    {
        Stepper stepper = new Stepper (address, space);
        int nvalues = 0;

        while (stepper.getCount () < count)
        {
            stepper.step ();
            nvalues++;
        }

        if (stepper.getCount () != count)
        {
            int addr = stepper.getAddress ();
            Modbus.addressError ("M003",
                "Transfer ends in middle of register " + addr +
                " (message address " +
                space.fromModel (new ModelAddress (addr, 0)) + ")");
        }

        return nvalues;
    }

    void packValue (int size, double value, byte [] buf, int offset)
    {
        byte [] data = new byte [size];

        switch (size)
        {
        case 4:
            Bytes.fromFloat ((float) value, data, 0);
            break;

        case 8:
            Bytes.fromDouble (value, data, 0);
            break;

        default:
            throw new IllegalStateException ("Bad size: " + size);
        }

        storeBytes (data, buf, offset);
    }

    void packValue (int size, long value, byte [] buf, int offset, int shift)
    {
        if (size == 0)
        {
            if (value != 0)
                buf [offset] |= 1 << shift;
        }
        else
        {
            byte [] data = new byte [size];

            switch (size)
            {
            case 1:
                data [0] = (byte) value;
                break;

            case 2:
                Bytes.fromShort ((short) value, data, 0);
                break;

            case 4:
                Bytes.fromInt ((int) value, data, 0);
                break;

            case 8:
                Bytes.fromLong (value, data, 0);
                break;

            default:
                throw new IllegalStateException ("Bad size: " + size);
            }

            storeBytes (data, buf, offset);
        }
    }

    double unpackDoubleValue (int size, byte [] buf, int offset)
    {
        byte [] data = extractBytes (buf, offset, size);
        switch (size)
        {
        case 4:
            return Bytes.toFloat (data, 0);

        case 8:
            return Bytes.toDouble (data, 0);

        default:
            throw new IllegalStateException ("Bad size: " + size);
        }
    }

    long unpackLongValue (int size, byte [] buf, int offset, int shift)
    {
        if (size == 0)
        {
            if ((buf [offset] & (1 << shift)) != 0)
                return 1;
            return 0;
        }
        else
        {
            byte [] data = extractBytes (buf, offset, size);
            switch (size)
            {
            case 1:
                return data [0] & 0xff;

            case 2:
                return Bytes.toShort (data, 0);

            case 4:
                return Bytes.toInt (data, 0);

            case 8:
                return Bytes.toLong (data, 0);

            default:
                throw new IllegalStateException ("Bad size: " + size);
            }
        }
    }

    public void trace (Tracer.Tracing tracing,
        int slaveId, boolean isWrite, int address, double value)
    {
        if (tracing == null)
            return;
        tracing.trace (slaveId, isWrite, 0, address,
            String.format ("%.8g", value));
    }

    private static long unsignedValue (long value, int size)
    {
        switch (size)
        {
        case 0:
            return value & 1;
        case 1:
            return value & 0xff;
        case 2:
            return value & 0xffff;
        case 4:
            return value & 0xffffffffL;
        case 8:
            return value;
        default:
            throw new IllegalStateException ("Bad size: " + size);
        }
    }

    private static String toString (long value, int size, boolean signed,
        int radix)
    {
        switch (radix)
        {
        case Numeric.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
                {
                    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 (unsignedValue (value, size));
            while (s.length () < size * 8)
                s = "0" + s;
            return s;

        case 8:
            return Long.toOctalString (unsignedValue (value, size));

        case 16:
            return Long.toHexString (unsignedValue (value, size));

        case 10:
            if (signed)
                return "" + value;

            // Unsigned decimal

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

            // 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 (Numeric.TWO_POWER_64);
            return bi.toString ();

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

    public void trace (Tracer.Tracing tracing,
        int slaveId, boolean isWrite, int address, long value)
    {
        if (tracing == null)
            return;
        String valstr;
        try
        {
            int size = getValueSize (address);
            boolean signed = isSigned (address);
            int radix = getRadix (address);
            valstr = toString (value, size, signed, radix);
        }
        catch (ModbusException e)
        {
            valstr = "" + value;
        }
        tracing.trace (slaveId, isWrite, 0, address, valstr);
    }

    /**
    * Packs register values into a byte array.
    * It is up to the caller to check that the number of bytes will fit
    * in a legal Modbus message.
    * @param address model address of first register.
    * @param nvalues number of values to pack.
    * @param space address space of registers.
    * @param tracing for returning register trace lines. May be
    * null if no tracing required.
    * @param slaveId slave ID (only used for tracing).
    * @param isWrite whether a write request is being handled (only used for
    * tracing).
    * @return byte array of packed values.
    */
    byte [] pack (int address, int nvalues, AddressMap.Map space,
            Tracer.Tracing tracing, int slaveId, boolean isWrite)
        throws ModbusException
    {
        int totalSize = getTotalBytes (address, nvalues, space);
        byte [] result = new byte [totalSize];
        Stepper stepper = new Stepper (address, space);

        for (int i = 0 ; i < nvalues ; i++)
        {
            int size = stepper.getSize ();
            address = stepper.getAddress ();
            if (isFloat (address))
            {
                double value = getDoubleValue (address);
                packValue (size, value, result, stepper.offset);
                trace (tracing, slaveId, isWrite, address, value);
            }
            else
            {
                long value = getLongValue (address);
                packValue (size, value, result, stepper.offset, stepper.shift);
                trace (tracing, slaveId, isWrite, address, value);
            }
            stepper.step ();
        }

        return result;
    }

    /**
    * Unpacks a byte array into registers
    * @param array array to be unpacked
    * @param address model address of first register
    * @param nvalues number of values to pack
    * @param space address space of registers
    * @param slaveId slave ID (only used for tracing)
    * @param isWrite whether a write request is being handled (only used for
    * tracing).
    * @throws ModbusException if the address/nvalues is invalid or if
    * {@link #setValue} throws a ModbusException.
    */
    void unpack (byte [] array, int address, int nvalues, AddressMap.Map space,
            boolean strictChecking, int slaveId, boolean isWrite,
            Tracer tracer)
        throws ModbusException
    {
        // Check size of data, so we can send an error response before
        // modifying any register.

        Stepper stepper = new Stepper (address, space);

        for (int i = 0 ; i < nvalues ; i++)
        {
            int size = stepper.getSize ();
            int addr = stepper.getAddress ();
            stepper.step ();
        }

        if (stepper.getTotalSize () > array.length)
        {
            Modbus.dataError ("M004",
                "Not enough data in message: " +
                array.length + " bytes when expecting " +
                stepper.getTotalSize ());
        }
        if (strictChecking && stepper.getTotalSize () < array.length)
        {
            Modbus.dataError ("M005",
                "Excess data in message: " +
                array.length + " bytes when expecting " +
                stepper.getTotalSize ());
        }

        // Do the actual unpacking

        Tracer.Tracing tracing = new Tracer.Tracing ();
        stepper = new Stepper (address, space);

        for (int i = 0 ; i < nvalues ; i++)
        {
            int size = stepper.getSize ();
            int addr = stepper.getAddress ();
            if (isFloat (addr))
            {
                double value = unpackDoubleValue (size, array, stepper.offset);
                setValue (addr, value);
                trace (tracing, slaveId, isWrite, addr, value);
            }
            else
            {
                long value = unpackLongValue (size, array, stepper.offset,
                    stepper.shift);
                setValue (addr, value);
                trace (tracing, slaveId, isWrite, addr, value);
            }
            stepper.step ();
        }

        tracer.trace (tracing);
    }

    private byte [] extractBytes (byte [] a, int offset, int size)
    {
        byte [] result = new byte [size];

        switch (size)
        {
        case 1:
            result [0] = a [offset + 0];
            break;

        case 2:
            result [0] = a [offset + 0];
            result [1] = a [offset + 1];
            break;

        case 4:
            if (bigValueFlags.getLittleEndian ())
            {
                result [0] = a [offset + 2];
                result [1] = a [offset + 3];
                result [2] = a [offset + 0];
                result [3] = a [offset + 1];
            }
            else
            {
                result [0] = a [offset + 0];
                result [1] = a [offset + 1];
                result [2] = a [offset + 2];
                result [3] = a [offset + 3];
            }
            break;

        case 8:
            if (bigValueFlags.getLittleEndian ())
            {
                result [0] = a [offset + 6];
                result [1] = a [offset + 7];
                result [2] = a [offset + 4];
                result [3] = a [offset + 5];
                result [4] = a [offset + 2];
                result [5] = a [offset + 3];
                result [6] = a [offset + 0];
                result [7] = a [offset + 1];
            }
            else
            {
                result [0] = a [offset + 0];
                result [1] = a [offset + 1];
                result [2] = a [offset + 2];
                result [3] = a [offset + 3];
                result [4] = a [offset + 4];
                result [5] = a [offset + 5];
                result [6] = a [offset + 6];
                result [7] = a [offset + 7];
            }
            break;

        default:
            throw new IllegalStateException ("Bad size: " + size);
        }

        return result;
    }

    private void storeBytes (byte [] data, byte [] buf, int offset)
    {
        switch (data.length)
        {
        case 1:
            buf [offset + 0] = data [0];
            break;

        case 2:
            buf [offset + 0] = data [0];
            buf [offset + 1] = data [1];
            break;

        case 4:
            if (bigValueFlags.getLittleEndian ())
            {
                buf [offset + 2] = data [0];
                buf [offset + 3] = data [1];
                buf [offset + 0] = data [2];
                buf [offset + 1] = data [3];
            }
            else
            {
                buf [offset + 0] = data [0];
                buf [offset + 1] = data [1];
                buf [offset + 2] = data [2];
                buf [offset + 3] = data [3];
            }
            break;

        case 8:
            if (bigValueFlags.getLittleEndian ())
            {
                buf [offset + 6] = data [0];
                buf [offset + 7] = data [1];
                buf [offset + 4] = data [2];
                buf [offset + 5] = data [3];
                buf [offset + 2] = data [4];
                buf [offset + 3] = data [5];
                buf [offset + 0] = data [6];
                buf [offset + 1] = data [7];
            }
            else
            {
                buf [offset + 0] = data [0];
                buf [offset + 1] = data [1];
                buf [offset + 2] = data [2];
                buf [offset + 3] = data [3];
                buf [offset + 4] = data [4];
                buf [offset + 5] = data [5];
                buf [offset + 6] = data [6];
                buf [offset + 7] = data [7];
            }
            break;

        default:
            throw new IllegalStateException ("Bad size: " + data.length);
        }
    }

    void checkWritable (int address, int nvalues, AddressMap.Map space)
        throws ModbusException
    {
        Stepper stepper = new Stepper (address, space);

        while (nvalues > 0)
        {
            stepper.checkWritable ();
            stepper.step ();
            nvalues--;
        }
    }

    /**
    * Finds largest group of values that can be read or written
    * in a single message starting at the specified register address.
    * <p>The size of the group is limited by how many data bytes will fit
    * in a message, by the constraint that the registers must be
    * contiguous, and by the constraint that reads and writes cannot be
    * mixed in the same message.
    * @param address model address of first register to be transferred.
    * @param maxDataBytes maximum number of data bytes that may be transferred.
    * @param allowMixed allow writable and non-writable registers in the
    * same group.
    * @return Number of values in largest group.
    */
    public int getGroup (int address, int maxDataBytes, boolean allowMixed)
    {
        int nvalues = 0;

        try
        {
            AddressMap.Map as = map.getAddressSpace (address);
            if (as == null)
                return nvalues;
            Stepper stepper = new Stepper (address, as);
            boolean w = writable (address);

            for (;;)
            {
                address = stepper.getAddress ();
                if ((!allowMixed && writable (address) != w) ||
                    map.getAddressSpace (address) != as)
                {
                    break;
                }
                stepper.step ();
                if (stepper.getTotalSize () > maxDataBytes)
                    break;
                if (stepper.getCount () > 65535)
                    break;
                nvalues++;
            }
        }
        catch (ModbusException e)
        {
        }

        return nvalues;
    }

    /**
    * Converts a holding-register message address to a model address.
    * @param address the message address.
    * @return the corresponding model address.
    * @throws ModbusException if the message address is out of range.
    */
    int fromHoldingRegister (int address)
        throws ModbusException
    {
        return map.getHoldingRegisterMap ().toModel (address).getAddress ();
    }

    /**
    * Converts a model address to a holding-register message address.
    * @param address the model address.
    * @return the corresponding message address.
    */
    int toHoldingRegister (int address)
    {
        return map.getHoldingRegisterMap ().fromModel (
            new ModelAddress (address, 0));
    }

    /**
    * Converts a input-register message address to a model address.
    * @param address the message address.
    * @return the corresponding model address.
    * @throws ModbusException if the message address is out of range.
    */
    int fromInputRegister (int address)
        throws ModbusException
    {
        return map.getInputRegisterMap ().toModel (address).getAddress ();
    }

    /**
    * Converts a model address to an input-register message address.
    * @param address the model address.
    * @return the corresponding message address.
    */
    int toInputRegister (int address)
    {
        return map.getInputRegisterMap ().fromModel (
            new ModelAddress (address, 0));
    }

    /**
    * Converts a discrete-input message address to a model address.
    * @param address the message address.
    * @return the corresponding model address.
    * @throws ModbusException if the message address is out of range.
    */
    ModelAddress fromDiscreteInput (int address)
        throws ModbusException
    {
        return map.getDiscreteInputMap ().toModel (address);
    }

    /**
    * Converts a model address to a discrete-input message address.
    * @param address the model address.
    * @return the corresponding message address.
    */
    int toDiscreteInput (ModelAddress address)
    {
        return map.getDiscreteInputMap ().fromModel (address);
    }

    /**
    * Converts a coil message address to a model address.
    * @param address the message address.
    * @return the corresponding model address.
    * @throws ModbusException if the message address is out of range.
    */
    ModelAddress fromCoil (int address)
        throws ModbusException
    {
        return map.getCoilMap ().toModel (address);
    }

    /**
    * Converts a model address to a coil message address.
    * @param address the model address.
    * @return the corresponding message address.
    */
    int toCoil (ModelAddress address)
    {
        return map.getCoilMap ().fromModel (address);
    }

    int getExceptionStatusRegister ()
    {
        return map.getExceptionStatusRegister ();
    }

    int getDiagnosticRegister ()
    {
        return map.getDiagnosticRegister ();
    }

    private void setDiscreteMultiplier (AddressMap.Map map, int [] addresses,
        int defaultMultiplier)
    {
        try
        {
            map.setMultiplier (defaultMultiplier);

            for (int i = 0 ; i < addresses.length ; i++)
            {
                int addr = addresses [i];
                if (map.inRange (addr))
                {
                    int sz = getValueSize (addr);
                    map.setMultiplier (sz == 0 ? 1 : sz * 8);
                    break;
                }
            }
        }
        catch (ModbusException e)
        {
            throw new IllegalStateException (
                "Unexpected " + Exceptions.getName (e) + ": " +
                    Exceptions.getMessage (e));
        }
    }

    private void setExceptionStatusRegisterSize (AddressMap map)
    {
        try
        {
            int addr = map.getExceptionStatusRegister ();
            if (addr < 0)
                return;
            map.setExceptionStatusRegisterSize (getValueSize (addr));
        }
        catch (ModbusException e)
        {
            map.setExceptionStatusRegisterSize (1);
        }
    }

    private void setDiagnosticRegisterSize (AddressMap map)
    {
        try
        {
            int addr = map.getDiagnosticRegister ();
            if (addr < 0)
                return;
            map.setDiagnosticRegisterSize (getValueSize (addr));
        }
        catch (ModbusException e)
        {
            map.setDiagnosticRegisterSize (2);
        }
    }

    private void setDiscreteMultipliers (AddressMap map, int [] addresses,
        int defaultMultiplier)
    {
        setDiscreteMultiplier (map.getCoilMap (), addresses, defaultMultiplier);
        setDiscreteMultiplier (map.getDiscreteInputMap (), addresses,
            defaultMultiplier);
        setExceptionStatusRegisterSize (map);
        setDiagnosticRegisterSize (map);
    }

    private int getCoilValueSize (AddressMap map)
    {
        return map.getCoilMap ().getMultiplier () / 8;
    }

    /**
    * Gets the size of registers in the coil address space.
    * @return Coil register size.
    */
    public int getCoilValueSize ()
    {
        return getCoilValueSize (map);
    }

    private int getDiscreteInputValueSize (AddressMap map)
    {
        return map.getDiscreteInputMap ().getMultiplier () / 8;
    }

    /**
    * Gets the size of registers in the discrete input address space.
    * @return Discrete input register size.
    */
    public int getDiscreteInputValueSize ()
    {
        return getDiscreteInputValueSize (map);
    }

    boolean byteSwapCoils ()
        throws ModbusException
    {
        return map.getByteSwap () && getCoilValueSize () > 1;
    }

    boolean byteSwapDiscreteInputs ()
        throws ModbusException
    {
        return map.getByteSwap () && getDiscreteInputValueSize () > 1;
    }

    boolean getByteSwap ()
    {
        return map.getByteSwap ();
    }

    boolean bitReverseCoils ()
        throws ModbusException
    {
        return map.getBitReverse () && getCoilValueSize () > 1;
    }

    boolean bitReverseDiscreteInputs ()
        throws ModbusException
    {
        return map.getBitReverse () && getDiscreteInputValueSize () > 1;
    }

    boolean getBitReverse ()
    {
        return map.getBitReverse ();
    }

    /**
    * Gets the big-value flags.
    * @return The big-value flags.
    */
    public BigValueFlags getBigValueFlags ()
    {
        return bigValueFlags;
    }

    /**
    * Gets the address map.
    * @return The address map.
    */
    public AddressMap getAddressMap ()
    {
        return map;
    }

    private class Range
    {
        int first;
        int last;

        Range (int first, int last)
        {
            this.first = first;
            this.last = last;
        }
    }

    // Check that all registers are mapped into at least one
    // address space

    private void checkMapped (AddressMap map, int [] addresses, int [] sizes)
        throws ValueException
    {
        List<Range> addrs = new LinkedList<Range> ();
        int first = -1;
        int last = -1;

        for (int i = 0 ; i < addresses.length ; i++)
        {
            int address = addresses [i];
            if (map.isExceptionStatusRegister (address))
                continue;
            if (map.isDiagnosticRegister (address))
                continue;
            AddressMap.Map space = map.getAddressSpace (address);
            if (space == null)
            {
                if (first < 0)
                {
                    first = i;
                    last = i;
                }
                else if (nextAddress (addresses [last], sizes [last]) >=
                    address)
                {
                    last = i;
                }
                else
                {
                    addrs.add (new Range (addresses [first], addresses [last]));
                    first = i;
                    last = i;
                }
            }
        }

        if (first >= 0)
            addrs.add (new Range (addresses [first], addresses [last]));

        if (addrs.size () != 0)
        {
            String str = "";
            int count = 0;

            for (Range r : addrs)
            {
                if (count != 0)
                    str += ", ";
                count += r.last - r.first + 1;
                if (str.length () > 20)
                {
                    str += "...";
                    break;
                }
                str += r.first;
                if (r.last != r.first)
                    str += "-" + r.last;
            }

            if (count > 1)
            {
                throw new ValueException ("M011",
                    "Addresses " + str + " are not in any address range");
            }
            else
            {
                throw new ValueException ("M010",
                    "Address " + str + " is not in any address range");
            }
        }
    }

    // Check that registers don't overlap

    private void checkOverlap (BigValueFlags bigValueFlags, int [] addresses,
            int [] sizes)
        throws ValueException
    {
        for (int i = 0 ; i < addresses.length - 1 ; i++)
        {
            int address = addresses [i];
            int size = sizes [i];
            int nextAddr = addresses [i + 1];

            if (nextAddress (bigValueFlags, address, size) > nextAddr)
            {
                throw new ValueException (
                    "Registers " + address + " and " + nextAddr +
                        " overlap");
            }
        }
    }

    // Check that all coils are same size

    private void checkCoilSizes (AddressMap map, int [] addresses, int [] sizes)
        throws ValueException
    {
        int coilSize = getCoilValueSize (map);

        for (int i = 0 ; i < addresses.length ; i++)
        {
            int address = addresses [i];
            int size = sizes [i];
            if (map.isCoil (address) && size != coilSize)
            {
                throw new ValueException ("M006",
                    "Inconsistent sizes for coils");
            }
        }
    }

    // Check that all discrete inputs are same size

    private void checkDiscreteInputSizes (AddressMap map, int [] addresses,
            int [] sizes)
        throws ValueException
    {
        int discreteInputSize = getDiscreteInputValueSize (map);

        for (int i = 0 ; i < addresses.length ; i++)
        {
            int address = addresses [i];
            int size = sizes [i];
            if (map.isDiscreteInput (address) && size != discreteInputSize)
            {
                throw new ValueException ("M007",
                    "Inconsistent sizes for discrete inputs");
            }
        }
    }

    // Check that consistent size used for exception status register

    private void checkExceptionStatusRegisterSizes (AddressMap map,
            int [] addresses, int [] sizes)
        throws ValueException
    {
        if (map.getExceptionStatusRegister () < 0)
            return;
        int regSize = map.getExceptionStatusRegisterSize ();

        for (int i = 0 ; i < addresses.length ; i++)
        {
            int address = addresses [i];
            int size = sizes [i];
            if (map.isExceptionStatusRegister (address) && size != regSize)
            {
                throw new ValueException (
                    "Inconsistent sizes for exception status register");
            }
        }
    }

    // Check that consistent size used for diagnostic register

    private void checkDiagnosticRegisterSizes (AddressMap map,
            int [] addresses, int [] sizes)
        throws ValueException
    {
        if (map.getDiagnosticRegister () < 0)
            return;
        int regSize = map.getDiagnosticRegisterSize ();

        for (int i = 0 ; i < addresses.length ; i++)
        {
            int address = addresses [i];
            int size = sizes [i];
            if (map.isDiagnosticRegister (address) && size != regSize)
            {
                throw new ValueException (
                    "Inconsistent sizes for diagnostic register");
            }
        }
    }

    /**
    * Checks the supplied big-value flags for consistency with
    * the current register definitions and address map.
    * @throws ValueException if the flags are inconsistent.
    */
    public void checkBigValueFlags (BigValueFlags bigValueFlags)
        throws ValueException
    {
        int [] addresses = getAddresses ();
        int [] sizes = getValueSizes ();
        setDiscreteMultipliers (map, addresses, 1);
        checkOverlap (bigValueFlags, addresses, sizes);
    }

    /**
    * Sets the big-value flags.
    * @param bigValueFlags the new big-value flags.
    */
    public void setBigValueFlags (BigValueFlags bigValueFlags)
    {
        this.bigValueFlags = bigValueFlags;
    }

    /**
    * Checks the supplied address map for consistency with the current
    * register definitions and big-value flags.
    * @throws ValueException if the address map is inconsistent.
    */
    public void checkAddressMap (AddressMap map)
        throws ValueException
    {
        int [] addresses = getAddresses ();
        int [] sizes = getValueSizes ();
        map.check ();
        setDiscreteMultipliers (map, addresses, 1);
        checkCoilSizes (map, addresses, sizes);
        checkDiscreteInputSizes (map, addresses, sizes);
        checkExceptionStatusRegisterSizes (map, addresses, sizes);
        checkDiagnosticRegisterSizes (map, addresses, sizes);
        checkMapped (map, addresses, sizes);
    }

    /**
    * Sets the address map.
    * @param map the new address map.
    */
    public void setAddressMap (AddressMap map)
    {
        this.map = map;
    }

    /**
    * Checks the register definitions, the address map, and the big-value flags
    * for consistency.
    * @throws ValueException if there is an inconsistency.
    */
    public void checkModel ()
        throws ValueException
    {
        int [] addresses = getAddresses ();
        int [] sizes = getValueSizes ();
        map.check ();
        setDiscreteMultipliers (map, addresses, 1);
        checkCoilSizes (map, addresses, sizes);
        checkDiscreteInputSizes (map, addresses, sizes);
        checkExceptionStatusRegisterSizes (map, addresses, sizes);
        checkDiagnosticRegisterSizes (map, addresses, sizes);
        checkMapped (map, addresses, sizes);
        checkOverlap (bigValueFlags, addresses, sizes);
    }

    /**
    * Checks a proposed register size for consistency with the current
    * register definitions, address map and big-value flags.
    * @param address model address of register.
    * @param size proposed size of register.
    * @throws ValueException if there is an inconsistency.
    */
    public void checkRegisterSize (int address, int size)
        throws ValueException
    {
        int [] addresses = getAddresses ();
        int [] sizes = getValueSizes ();
        setDiscreteMultipliers (map, addresses, size == 0 ? 1 : size * 8);

        // Check that register does not overlap following address

        for (int i = 0 ; i < addresses.length ; i++)
        {
            int addr = addresses [i];
            if (address < addr && nextAddress (address, size) > addr)
            {
                throw new ValueException (
                    "Registers " + address + " and " + addr + " overlap");
            }
        }

        // Check that consistent size used for coils and discrete inputs

        if (map.isCoil (address))
        {
            for (int i = 0 ; i < addresses.length ; i++)
            {
                int addr = addresses [i];
                if (map.isCoil (addr))
                {
                    if (sizes [i] != size)
                    {
                        throw new ValueException ("M008",
                            "Inconsistent size for coil");
                    }
                    break;
                }
            }
        }
        if (map.isDiscreteInput (address))
        {
            for (int i = 0 ; i < addresses.length ; i++)
            {
                int addr = addresses [i];
                if (map.isDiscreteInput (addr))
                {
                    if (sizes [i] != size)
                    {
                        throw new ValueException ("M009",
                            "Inconsistent size for discrete input register");
                    }
                    break;
                }
            }
        }
    }

    /**
    * Checks a proposed block of register definitions for consistency with
    * the current register definitions, address map and big-value flags.
    * @param startaddr model address of first proposed register.
    * @param size size of each proposed register.
    * @param count number of registers in the block.
    * @throws ValueException if there is an inconsistency.
    */
    public void checkNewRegisters (int startaddr, int size, int count)
        throws ValueException
    {
        int inc = bigValueFlags.addressIncrement (size);
        int endaddr = startaddr + count * inc - 1;
        if (endaddr < startaddr)
        {
            throw new ValueException (
                "Address must be in range 0 to " + Integer.MAX_VALUE);
        }
        int [] addresses = getAddresses ();
        int [] sizes = getValueSizes ();
        setDiscreteMultipliers (map, addresses, size == 0 ? 1 : size * 8);

        // Check that address not already defined within new register range

        for (int i = 0 ; i < addresses.length ; i++)
        {
            int addr = addresses [i];
            if (addr >= startaddr && addr <= endaddr)
            {
                if ((addr - startaddr) % inc == 0)
                {
                    throw new ValueException (
                        "Address " + addr + " is already used");
                }
                else
                {
                    throw new ValueException (
                        "Registers " +
                        (addr - (addr - startaddr) % inc) +
                        " and " + addr + " overlap");
                }
            }
        }

        // Check that all addresses are mapped

        for (int addr = startaddr ; addr <= endaddr && addr >= 0 ; addr += inc)
        {
            AddressMap.Map space = map.getAddressSpace (addr);
            if (space == null)
            {
                throw new ValueException ("M010",
                    "Address " + addr + " is not in any address range");
            }
        }

        // Check that start address does not overlap preceding register

        for (int i = 0 ; i < addresses.length ; i++)
        {
            int addr = addresses [i];
            if (addr < startaddr &&
                nextAddress (addr, sizes [i]) > startaddr)
            {
                throw new ValueException ("M012",
                    "Registers " + addr + " and " + startaddr +
                        " overlap");
            }
        }

        // Check that consistent size used for coils and discrete inputs

        for (int addr = startaddr ; addr <= endaddr && addr >= 0 ; addr += inc)
        {
            if (map.isCoil (addr))
            {
                for (int i = 0 ; i < addresses.length ; i++)
                {
                    int addr1 = addresses [i];
                    if (map.isCoil (addr1))
                    {
                        if (sizes [i] != size)
                        {
                            throw new ValueException ("M008",
                                "Inconsistent size for coil");
                        }
                        break;
                    }
                }
                break;
            }
        }

        for (int addr = startaddr ; addr <= endaddr && addr >= 0 ; addr += inc)
        {
            if (map.isDiscreteInput (addr))
            {
                for (int i = 0 ; i < addresses.length ; i++)
                {
                    int addr1 = addresses [i];
                    if (map.isDiscreteInput (addr1))
                    {
                        if (sizes [i] != size)
                        {
                            throw new ValueException ("M009",
                                "Inconsistent size for discrete input");
                        }
                        break;
                    }
                }
                break;
            }
        }
    }
}

