
package uk.co.wingpath.modbus;

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

public class ModbusFilePacker
{
    private final ModbusFiles files;

    /**
    * Constructs a ModbusFilePacker with the specified files.
    */
    public ModbusFilePacker (ModbusFiles files)
    {
        this.files = files;
    }

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

    /**
    * This class provides "iterators" for stepping through Modbus addresses
    * one value at a time.
    */
    private class Stepper
    {
        int fileNum;                // File number
        int address;                // Register number
        int offset;                 // Byte offset in message data
        int count;                  // Count to be used in message

        /**
        * Creates a {@code Stepper} starting at the specified address in the
        * specified file.
        * @param fileNum the file number.
        * @param address the starting address.
        */
        Stepper (int fileNum, int address)
        {
            this.fileNum = fileNum;
            this.address = address;
            offset = 0;
            count = 0;
        }

        /**
        * Gets the current address.
        * @return the current 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
        {
            try
            {
                ModbusRegisters registers = files.getRegisters (fileNum);
                ModbusRegister register = registers.getRegister (address);
                return register.getValueSize ();
            }
            catch (ModbusException e)
            {
                // No register at the current address.
                Modbus.addressError ("M013",
                    "No file register at address " + fileNum + ":" + address);
                throw new AssertionError ("Unreachable");
            }
        }

        /**
        * 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
        {
            ModbusRegisters registers = files.getRegisters (fileNum);
            ModbusRegister register = registers.getRegister (address);
            getSize ();     // Check address for validity
            if (!register.isWritable ())
            {
                Modbus.addressError ("M002",
                    "Register " + fileNum + ":" + address + " is not writable");
            }
        }

        /**
        * Gets the total number of bytes that have been stepped over.
        * @return total size in bytes.
        */
        int getTotalSize ()
        {
            return offset;
        }

        /**
        * 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 (files.getBigValueFlags ().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 ();
            assert size != 0;
            offset += size;
            address = nextAddress (address, size);
            count++;
        }
    }

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

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

        return stepper.getCount ();
    }

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

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

        return stepper.getTotalSize ();
    }

    /**
    * Converts count (as passed in Modbus message) to number of values.
    * @param fileNum the file number.
    * @param address address of first register.
    * @param count count extracted from Modbus message.
    * @return number of values to be transferred.
    * @throws ModbusException if the address/count is invalid.
    */
    public int getNumValues (int fileNum, int address, int count)
        throws ModbusException
    {
        Stepper stepper = new Stepper (fileNum, address);
        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);
        }

        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)
    {
        assert size != 0;
        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)
    {
        assert size != 0;
        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);
        }
    }

    private void trace (Tracer.Tracing tracing,
        int slaveId, boolean isWrite, int fileNum, int address, double value)
    {
        if (tracing == null)
            return;
        tracing.trace (slaveId, isWrite, fileNum, 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);
        }
    }

    private void trace (Tracer.Tracing tracing,
        int slaveId, boolean isWrite, int fileNum, int address, long value)
    {
        if (tracing == null)
            return;
        String valstr;
        try
        {
            ModbusRegisters registers = files.getRegisters (fileNum);
            ModbusRegister register = registers.getRegister (address);
            int size = register.getValueSize ();
            boolean signed = register.isSigned ();
            int radix = register.getRadix ();
            valstr = toString (value, size, signed, radix);
        }
        catch (ModbusException e)
        {
            valstr = "" + value;
        }
        tracing.trace (slaveId, isWrite, fileNum, 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 fileNum the file number.
    * @param address address of first register.
    * @param nvalues number of values to pack.
    * @param tracing list 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 fileNum, int address, int nvalues,
            Tracer.Tracing tracing, int slaveId, boolean isWrite)
        throws ModbusException
    {
        int totalSize = getTotalBytes (fileNum, address, nvalues);
        byte [] result = new byte [totalSize];
        Stepper stepper = new Stepper (fileNum, address);

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

        return result;
    }

    /**
    * Unpacks a byte array into registers
    * @param array array to be unpacked
    * @param fileNum the file number.
    * @param address address of first register
    * @param nvalues number of values to pack
    * @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 ModbusRegister#setValue} throws a ModbusException.
    */
    void unpack (byte [] array, int fileNum, int address, int nvalues,
            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 (fileNum, address);

        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 (fileNum, address);

        for (int i = 0 ; i < nvalues ; i++)
        {
            int size = stepper.getSize ();
            int addr = stepper.getAddress ();
            ModbusRegisters registers = files.getRegisters (fileNum);
            ModbusRegister register = registers.getRegister (addr);
            if (register.isFloat ())
            {
                double value = unpackDoubleValue (size, array, stepper.offset);
                register.setValue (value);
                trace (tracing, slaveId, isWrite, fileNum, addr, value);
            }
            else
            {
                long value = unpackLongValue (size, array, stepper.offset);
                register.setValue (value);
                trace (tracing, slaveId, isWrite, fileNum, 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 (files.getBigValueFlags ().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 (files.getBigValueFlags ().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 (files.getBigValueFlags ().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 (files.getBigValueFlags ().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 fileNum, int address, int nvalues)
        throws ModbusException
    {
        Stepper stepper = new Stepper (fileNum, address);

        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 fileNum the file number.
    * @param address address of first register to be transferred.
    * @param maxDataBytes maximum number of data bytes that may be transferred.
    * @return number of values in largest group.
    */
    public int getGroup (int fileNum, int address, int maxDataBytes)
    {
        int nvalues = 0;

        try
        {
            Stepper stepper = new Stepper (fileNum, address);
            ModbusRegisters registers = files.getRegisters (fileNum);
            ModbusRegister register = registers.getRegister (address);
            boolean w = register.isWritable ();

            for (;;)
            {
                int addr = stepper.getAddress ();
                register = registers.getRegister (addr);
                stepper.step ();
                if (stepper.getTotalSize () > maxDataBytes)
                    break;
                if (stepper.getCount () > 65535)
                    break;
                nvalues++;
            }
        }
        catch (ModbusException e)
        {
        }

        return nvalues;
    }

    private class Range
    {
        int first;
        int last;

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

