
package uk.co.wingpath.modbus;

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

/**
* This class provides methods that construct a {@code ModbusRequest},
* pass it to a {@code ModbusClient}, and interpret the
* {@code ModbusResponse} to update a {@code ModbusModel}.
*/

public class ModbusMaster
{
    private final ModbusModel model;
    private final ModbusClient client;
    private final int slaveId;
    private final int maxPdu;
    private final boolean enforceCountLimits;
    private final boolean strictChecking;
    private final boolean allowLongMessages;

    public ModbusMaster (ModbusModel model, ModbusClient client, int slaveId,
        int maxPdu, boolean enforceCountLimits,
        boolean strictChecking, boolean allowLongMessages)
    {
        this.model = model;
        this.client = client;
        this.slaveId = slaveId;
        this.maxPdu = maxPdu;
        this.enforceCountLimits = enforceCountLimits;
        this.strictChecking = strictChecking;
        this.allowLongMessages = allowLongMessages;
    }

    public ModbusMaster (ModbusModel model, ModbusClient client, int slaveId)
    {
        this (model, client, slaveId, Modbus.MAX_PDU_SIZE, true, true, false);
    }

    public int getSlaveId ()
    {
        return slaveId;
    }

    private void cantBroadcast ()
        throws ValueException
    {
        if (slaveId == 0)
        {
            throw new ValueException ("Can't broadcast a read request");
        }
    }

    public void read (int address, int nvalues)
        throws ModbusException, InterruptedException, IOException,
            ValueException
    {
        cantBroadcast ();
        AddressMap map = model.getAddressMap ();
        AddressMap.Map space = map.getAddressSpace (address);
        if (space == null)
        {
            throw new ValueException ("M010",
                "Address " + address + " is not in any address range");
        }
//System.out.println ("read: address " + address + ", space " + space.getName ());
        if (space == map.getHoldingRegisterMap ())
        {
            readHoldingRegisters (address, nvalues);
        }
        else if (space == map.getInputRegisterMap ())
        {
            readInputRegisters (address, nvalues);
        }
        else if (space == map.getCoilMap ())
        {
            int size = model.getCoilValueSize ();
            readCoils (address, size == 0 ? nvalues : nvalues * size * 8);
        }
        else if (space == map.getDiscreteInputMap ())
        {
            int size = model.getDiscreteInputValueSize ();
            readDiscreteInputs (address,
                size == 0 ? nvalues : nvalues * size * 8);
        }
    }

    public void write (int address, int nvalues)
        throws ModbusException, InterruptedException, IOException,
            ValueException
    {
        AddressMap map = model.getAddressMap ();
        AddressMap.Map space = map.getAddressSpace (address);
        if (space == null)
        {
            throw new ValueException ("M010",
                "Address " + address + " is not in any address range");
        }
//System.out.println ("read: address " + address + ", space " + space);
        if (space == map.getHoldingRegisterMap ())
        {
            writeMultipleRegisters (address, nvalues);
        }
        else if (space == map.getCoilMap ())
        {
            int size = model.getCoilValueSize ();
            writeCoils (address, size == 0 ? nvalues : nvalues * size * 8);
        }
        else if (space == map.getInputRegisterMap ())
        {
            throw new ValueException ("Writing to Input Register not allowed");
        }
        else if (space == map.getDiscreteInputMap ())
        {
            throw new ValueException ("Writing to Discrete Input not allowed");
        }
    }

    /**
    * Checks the reply size, and gets the number of data bytes in the
    * reply.
    * If long messages are allowed, the supplied byte count is ignored and
    * the number of data bytes is calculated as the reply length minus
    * the supplied overhead.
    * If long messages are not allowed, the supplied byte count is returned.
    * If strict checking is enabled, the reply length is checked to be
    * exactly the supplied overhead plus the supplied byte count.
    * If strict checking is disabled and long messages are not allowed,
    * the reply length is checked to at least the supplied overhead plus
    * the supplied byte count.
    * @param reply the reply message.
    * @param overhead the number of bytes used in the reply for fields
    * other than data.
    * @param byteCount the data byte count extracted from the reply, or 0 if
    * the reply does not contain a byte count.
    * @return the number of data bytes in the reply.
    */
    private int checkSize (ModbusResponse reply, int overhead, int byteCount)
        throws ModbusException
    {
        if (allowLongMessages)
        {
            reply.checkMinSize (overhead);
            return reply.size () - overhead;
        }
        if (strictChecking)
            reply.checkSize (overhead + byteCount);
        else
            reply.checkMinSize (overhead + byteCount);
        return byteCount;
    }

    /**
    * Reads into a group of contiguous holding registers.
    * @param address model address of first register.
    * @param nvalues number of values to read.
    * @throws ValueException if the count is invalid.
    * @throws InterruptedException if the thread is interrupted.
    * @throws IOException if an I/O error occurs.
    * @throws ModbusException if a Modbus error response is received, and
    * for various other errors relating to the construction of the request
    * and the interpretation of the reply.
    */
    public void readHoldingRegisters (int address, int nvalues)
        throws ModbusException, InterruptedException, IOException,
            ValueException
    {
        cantBroadcast ();
        AddressMap.Map space = model.getAddressMap ().getHoldingRegisterMap ();
        int nbytes = model.getTotalBytes (address, nvalues, space);
        if (!allowLongMessages && nbytes > 255)
        {
            throw new ValueException ("M102",
                "Invalid read count (" + nvalues +
                ") - response byte count (" + nbytes +
                ") would not fit in a byte");
        }
        if (nbytes + 2 > maxPdu)
        {
            throw new ValueException ("M103",
                "Invalid read count (" + nvalues +
                ") - response would exceed max PDU size (" +
                maxPdu + " bytes)");
        }
        if (enforceCountLimits && nbytes > 125 * 2)
        {
            throw new ValueException ("M104",
                "Invalid read count (" + nvalues +
                ") - response would exceed count limit (125 * 2 bytes)");
        }
        int count = model.getCount (address, nvalues, space);
        if (count > 65535)
        {
            throw new ValueException ("M114",
                "Invalid read count (" + count +
                ") - will not fit in 16 bits");
        }
        int addr = model.toHoldingRegister (address);
        MessageBuilder body = new MessageBuilder ();
        body.addInt (addr);
        body.addInt (count);
        ModbusRequest request = new ModbusRequest (slaveId,
            Modbus.FUNC_READ_HOLDING_REGISTERS, client.genTransId (),
            body.getData ());
        ModbusResponse reply = client.handleRequest (request);
        reply.checkMinSize (1 + nbytes);
        int byteCount = checkSize (reply, 1, reply.getByte (0));
        if (strictChecking && byteCount != nbytes)
        {
            Modbus.dataError ("M101",
                "Wrong byte count in response: " +
                byteCount + " when expecting " + nbytes);
        }
        byte [] data = reply.getData (1, nbytes);
        model.unpack (data, address, nvalues, space, strictChecking,
            slaveId, false);
    }

    /** Read into a group of contiguous input registers */

    public void readInputRegisters (int address, int nvalues)
        throws ModbusException, InterruptedException, IOException,
            ValueException
    {
        cantBroadcast ();
        AddressMap.Map space = model.getAddressMap ().getInputRegisterMap ();
        int nbytes = model.getTotalBytes (address, nvalues, space);
        if (!allowLongMessages && nbytes > 255)
        {
            throw new ValueException ("M102",
                "Invalid read count (" + nvalues +
                ") - response byte count (" + nbytes +
                ") would not fit in a byte");
        }
        if (nbytes + 2 > maxPdu)
        {
            throw new ValueException ("M103",
                "Invalid read count (" + nvalues +
                ") - response would exceed max PDU size (" +
                maxPdu + " bytes)");
        }
        if (enforceCountLimits && nbytes > 125 * 2)
        {
            throw new ValueException ("M104",
                "Invalid read count (" + nvalues +
                ") - response would exceed count limit (125 * 2 bytes)");
        }
        int count = model.getCount (address, nvalues, space);
        if (count > 65535)
        {
            throw new ValueException ("M114",
                "Invalid read count (" + count +
                ") - will not fit in 16 bits");
        }
        int addr = model.toInputRegister (address);
        MessageBuilder body = new MessageBuilder ();
        body.addInt (addr);
        body.addInt (count);
        ModbusRequest request = new ModbusRequest (slaveId,
            Modbus.FUNC_READ_INPUT_REGISTERS, client.genTransId (),
            body.getData ());
        ModbusResponse reply = client.handleRequest (request);
        reply.checkMinSize (1 + nbytes);
        int byteCount = checkSize (reply, 1, reply.getByte (0));
        if (strictChecking && byteCount != nbytes)
        {
            Modbus.dataError ("M101",
                "Wrong byte count in response: " +
                byteCount + " when expecting " + nbytes);
        }
        byte [] data = reply.getData (1, nbytes);
        model.unpack (data, address, nvalues, space, strictChecking,
            slaveId, false);
    }

    /** Write from a group of contiguous holding registers */

    public void writeMultipleRegisters (int address, int nvalues)
        throws ModbusException, InterruptedException, IOException,
            ValueException
    {
        AddressMap.Map space = model.getAddressMap ().getHoldingRegisterMap ();
        int count = model.getCount (address, nvalues, space);
        int addr = model.toHoldingRegister (address);
        byte [] data = model.pack (address, nvalues, space, true,
            slaveId, true);
        if (!allowLongMessages && data.length > 255)
        {
            throw new ValueException ("M105",
                "Invalid write count (" + nvalues +
                ") - byte count (" + data.length +
                ") would not fit in a byte");
        }
        if (data.length + 6 > maxPdu)
        {
            throw new ValueException ("M106",
                "Invalid write count (" + nvalues +
                ") - request would exceed max PDU size (" +
                maxPdu + " bytes)");
        }
        if (enforceCountLimits && data.length > 123 * 2)
        {
            throw new ValueException ("M107",
                "Invalid write count (" + nvalues +
                ") - request would exceed count limit (123 * 2 bytes)");
        }
        if (count > 65535)
        {
            throw new ValueException ("M115",
                "Invalid write count (" + count +
                ") - will not fit in 16 bits");
        }
        MessageBuilder body = new MessageBuilder ();
        body.addInt (addr);
        body.addInt (count);
        body.addByte (data.length > 255 ? 0 : data.length);
        body.addData (data);
        ModbusRequest request = new ModbusRequest (slaveId,
            Modbus.FUNC_WRITE_MULTIPLE_REGISTERS, client.genTransId (),
            body.getData ());
        ModbusResponse reply = client.handleRequest (request);
        if (strictChecking)
        {
            reply.checkSize (4);
            if (reply.getInt (0) != addr)
            {
                Modbus.dataError ("M109",
                    "Wrong address in response: " + reply.getInt (0) +
                    " instead of " + addr);
            }
            if (reply.getInt (2) != count)
            {
                Modbus.dataError ("M110",
                    "Wrong count in response: " + reply.getInt (2) +
                    " instead of " + count);
            }
        }
    }

    /** Write from a single holding register */

    public void writeSingleRegister (int address)
        throws ModbusException, InterruptedException, IOException
    {
        AddressMap.Map space = model.getAddressMap ().getHoldingRegisterMap ();
        int addr = model.toHoldingRegister (address);
        byte [] data = model.pack (address, 1, space, true, slaveId, true);
        MessageBuilder body = new MessageBuilder ();
        body.addInt (addr);
        body.addData (data);
        ModbusRequest request = new ModbusRequest (slaveId,
            Modbus.FUNC_WRITE_SINGLE_REGISTER, client.genTransId (),
            body.getData ());
        ModbusResponse reply = client.handleRequest (request);
        if (strictChecking)
        {
            reply.checkSize (request.size ());
            if (reply.getInt (0) != addr)
            {
                Modbus.dataError ("M109",
                    "Wrong address in response: " + reply.getInt (0) +
                    " instead of " + addr);
            }
            if (!Arrays.equals (request.getData (), reply.getData ()))
            {
                Modbus.dataError ("M111", "Wrong value in response");
            }
        }
    }

    public void maskWriteRegister (int address, long andMask, long orMask)
        throws ModbusException, InterruptedException, IOException
    {
        int addr = model.toHoldingRegister (address);
        int size = model.getValueSize (address);
        byte [] data = new byte [size * 2];
        model.packValue (size, andMask, data, 0, 0);
        model.packValue (size, orMask, data, size, 0);  
        MessageBuilder body = new MessageBuilder ();
        body.addInt (addr);
        body.addData (data);
        ModbusRequest request = new ModbusRequest (slaveId,
            Modbus.FUNC_MASK_WRITE_REGISTER, client.genTransId (),
            body.getData ());
        ModbusResponse reply = client.handleRequest (request);
        if (strictChecking)
        {
            reply.checkSize (request.size ());
            if (reply.getInt (0) != addr)
            {
                Modbus.dataError ("M109",
                    "Wrong address in response: " + reply.getInt (0) +
                    " instead of " + addr);
            }
            if (!Arrays.equals (request.getData (), reply.getData ()))
            {
                Modbus.dataError ("M112", "Wrong masks in response");
            }
        }
    }

    public void readCoils (int address, int nbits)
        throws ModbusException, InterruptedException, IOException,
            ValueException
    {
        cantBroadcast ();
        int valueSize = model.getCoilValueSize ();
        int nbytes = (nbits + 7) / 8;
        if (!allowLongMessages && nbytes > 255)
        {
            throw new ValueException ("M102",
                "Invalid read count (" + nbits +
                ") - response byte count (" + nbytes +
                ") would not fit in a byte");
        }
        if (nbytes + 2 > maxPdu)
        {
            throw new ValueException ("M103",
                "Invalid read count (" + nbits +
                ") - response would exceed max PDU size (" +
                maxPdu + " bytes)");
        }
        if (enforceCountLimits && nbits > 2000)
        {
            throw new ValueException ("M108",
                "Count (" + nbits + ") exceeds count limit (2000)");
        }
        if (nbits > 65535)
        {
            throw new ValueException ("M114",
                "Invalid read count (" + nbits + ") - will not fit in 16 bits");
        }
        AddressMap.Map space = model.getAddressMap ().getCoilMap ();
        int multiplier = valueSize == 0 ? 1 : valueSize * 8;
        int addr = model.toCoil (new ModelAddress (address, 0));
        int nvalues = (nbits + multiplier - 1) / multiplier;
        int size = valueSize == 0 ? (nvalues + 7) / 8 : nvalues * valueSize;
        MessageBuilder body = new MessageBuilder ();
        body.addInt (addr);
        body.addInt (nbits);
        ModbusRequest request = new ModbusRequest (slaveId,
            Modbus.FUNC_READ_COILS, client.genTransId (), body.getData ());
        ModbusResponse reply = client.handleRequest (request);
        checkSize (reply, 1, nbytes);
        if (strictChecking && reply.getByte (0) != nbytes)
        {
            Modbus.dataError ("M101",
                "Wrong byte count in response: " +
                reply.getByte (0) + " when expecting " + nbytes);
        }
        byte [] data = reply.getData (1, nbytes);
        if (model.bitReverseCoils ())
            Bytes.reverseBits (data);
        if (model.byteSwapCoils ())
            Bytes.byteSwap (data);
        model.unpack (data, address, nvalues, space, strictChecking,
            slaveId, false);
    }

    public void readDiscreteInputs (int address, int nbits)
        throws ModbusException, InterruptedException, IOException,
            ValueException
    {
        cantBroadcast ();
        AddressMap.Map space = model.getAddressMap ().getDiscreteInputMap ();
        int valueSize = model.getDiscreteInputValueSize ();
        int nbytes = (nbits + 7) / 8;
        if (!allowLongMessages && nbytes > 255)
        {
            throw new ValueException ("M102",
                "Invalid read count (" + nbits +
                ") - response byte count (" + nbytes +
                ") would not fit in a byte");
        }
        if (nbytes + 2 > maxPdu)
        {
            throw new ValueException ("M103",
                "Invalid read count (" + nbits +
                ") - response would exceed max PDU size (" +
                maxPdu + " bytes)");
        }
        if (enforceCountLimits && nbits > 2000)
        {
            throw new ValueException ("M108",
                "Count (" + nbits + ") exceeds count limit (2000)");
        }
        if (nbits > 65535)
        {
            throw new ValueException ("M114",
                "Invalid read count (" + nbits + ") - will not fit in 16 bits");
        }
        int multiplier = valueSize == 0 ? 1 : valueSize * 8;
        int addr = model.toDiscreteInput (new ModelAddress (address, 0));
        int nvalues = (nbits + multiplier - 1) / multiplier;
        int size = valueSize == 0 ? (nvalues + 7) / 8 : nvalues * valueSize;
        MessageBuilder body = new MessageBuilder ();
        body.addInt (addr);
        body.addInt (nbits);
        ModbusRequest request = new ModbusRequest (slaveId,
            Modbus.FUNC_READ_DISCRETE_INPUTS, client.genTransId (),
            body.getData ());
        ModbusResponse reply = client.handleRequest (request);
        checkSize (reply, 1, nbytes);
        if (strictChecking && reply.getByte (0) != nbytes)
        {
            Modbus.dataError ("M101",
                "Wrong byte count in response: " +
                reply.getByte (0) + " when expecting " + nbytes);
        }
        byte [] data = reply.getData (1, nbytes);
        if (model.bitReverseDiscreteInputs ())
            Bytes.reverseBits (data);
        if (model.byteSwapDiscreteInputs ())
            Bytes.byteSwap (data);
        model.unpack (data, address, nvalues, space, strictChecking,
            slaveId, false);
    }

    public void writeCoils (int address, int nbits)
        throws ModbusException, InterruptedException, IOException,
            ValueException
    {
        AddressMap.Map space = model.getAddressMap ().getCoilMap ();
        int valueSize = model.getCoilValueSize ();
        int nbytes = (nbits + 7) / 8;
        int multiplier = valueSize == 0 ? 1 : valueSize * 8;
        int addr = model.toCoil (new ModelAddress (address, 0));
        int nvalues = (nbits + multiplier - 1) / multiplier;
        int size = valueSize == 0 ? (nvalues + 7) / 8 : nvalues * valueSize;
        byte [] data = model.pack (address, nvalues, space, true,
            slaveId, true);
        assert data.length == nbytes;
        assert data.length == size;
        if (!allowLongMessages && nbytes > 255)
        {
            throw new ValueException ("M105",
                "Invalid write count (" + nbits +
                ") - byte count (" + data.length +
                ") would not fit in a byte");
        }
        if (nbytes + 6 > maxPdu)
        {
            throw new ValueException ("M106",
                "Invalid write count (" + nbits +
                ") - request would exceed max PDU size (" +
            maxPdu + " bytes)");
        }
        if (enforceCountLimits && nbits > 1968)
        {
            throw new ValueException ("M108",
                "Count (" + nbits + ") exceeds count limit (1968)");
        }
        if (nbits > 65535)
        {
            throw new ValueException ("M115",
                "Invalid write count (" + nbits +
                ") - will not fit in 16 bits");
        }
        if (model.bitReverseCoils ())
            Bytes.reverseBits (data);
        if (model.byteSwapCoils ())
            Bytes.byteSwap (data);
        MessageBuilder body = new MessageBuilder ();
        body.addInt (addr);
        body.addInt (nbits);
        body.addByte (nbytes > 255 ? 0 : nbytes);
        body.addData (data);
        ModbusRequest request = new ModbusRequest (slaveId,
            Modbus.FUNC_WRITE_MULTIPLE_COILS, client.genTransId (),
            body.getData ());
        ModbusResponse reply = client.handleRequest (request);
        if (strictChecking)
        {
            checkSize (reply, 4, 0);
            if (reply.getInt (0) != addr)
            {
                Modbus.dataError ("M109",
                    "Wrong address in response: " + reply.getInt (0) +
                    " instead of " + addr);
            }
            if (reply.getInt (2) != nbits)
            {
                Modbus.dataError ("M110",
                    "Wrong count in response: " + reply.getInt (2) +
                    " instead of " + nbits);
            }
        }
    }

    public void writeCoil (int address)
        throws ModbusException, InterruptedException, IOException
    {
        AddressMap.Map space = model.getAddressMap ().getCoilMap ();
        int valueSize = model.getCoilValueSize ();
        int multiplier = valueSize == 0 ? 1 : valueSize * 8;
        int size = valueSize == 0 ? 1 : valueSize;
        int addr = model.toCoil (new ModelAddress (address, 0));
        byte [] data = model.pack (address, 1, space, true, slaveId, true);
        assert data.length == size;
        if (model.bitReverseCoils ())
            Bytes.reverseBits (data);
        if (model.byteSwapCoils ())
            Bytes.byteSwap (data);
        int value = (data [0] & 1) != 0 ? 0xff00 : 0;
        MessageBuilder body = new MessageBuilder ();
        body.addInt (addr);
        body.addInt (value);
        ModbusRequest request = new ModbusRequest (slaveId,
            Modbus.FUNC_WRITE_SINGLE_COIL, client.genTransId (),
            body.getData ());
        ModbusResponse reply = client.handleRequest (request);
        if (strictChecking)
        {
            checkSize (reply, 4, 0);
            if (reply.getInt (0) != addr)
            {
                Modbus.dataError ("M109",
                    "Wrong address in response: " + reply.getInt (0) +
                    " instead of " + addr);
            }
            if (reply.getInt (2) != value)
            {
                Modbus.dataError ("M111",
                    "Wrong value in response: " + reply.getInt (2) +
                    " instead of " + value);
            }
        }
    }

    public void readWriteRegisters (int readAddress, int readNValues,
            int writeAddress, int writeNValues)
        throws ModbusException, InterruptedException, IOException,
            ValueException
    {
        cantBroadcast ();
        AddressMap.Map space = model.getAddressMap ().getHoldingRegisterMap ();
        int readCount = model.getCount (readAddress, readNValues, space);
        int readAddr = model.toHoldingRegister (readAddress);
        int writeCount = model.getCount (writeAddress, writeNValues, space);
        int writeAddr = model.toHoldingRegister (writeAddress);
        byte [] data = model.pack (writeAddress, writeNValues, space, true,
            slaveId, true);
        if (!allowLongMessages && data.length > 255)
        {
            throw new ValueException ("M105",
                "Invalid write count (" + writeNValues +
                ") - request byte count (" + data.length +
                ") would not fit in a byte");
        }
        if (data.length + 10 > maxPdu)
        {
            throw new ValueException ("M106",
                "Invalid write count (" + writeNValues +
                ") - request would exceed max PDU size (" +
                maxPdu + "bytes)");
        }
        if (enforceCountLimits && data.length > 121 * 2)
        {
            throw new ValueException ("M107",
                "Invalid write count (" + writeNValues +
                ") - request would exceed count limit (121 * 2 bytes)");
        }
        if (writeCount > 65535)
        {
            throw new ValueException ("M115",
                "Invalid write count (" + writeCount +
                ") - will not fit in 16 bits");
        }
        int readBytes = model.getTotalBytes (readAddress, readNValues, space);
        if (!allowLongMessages && readBytes > 255)
        {
            throw new ValueException ("M102",
                "Invalid read count (" + readNValues +
                ") - response byte count (" + readBytes +
                ") would not fit in a byte");
        }
        if (readBytes + 2 > maxPdu)
        {
            throw new ValueException ("M103",
                "Invalid read count (" + readNValues +
                ") - response would exceed max PDU size (" +
                maxPdu + " bytes)");
        }
        if (enforceCountLimits && readBytes > 125 * 2)
        {
            throw new ValueException ("M104",
                "Invalid read count (" + readNValues +
                ") - request would exceed count limit (125 * 2 bytes)");
        }
        if (readCount > 65535)
        {
            throw new ValueException ("M114",
                "Invalid read count (" + readCount +
                ") - will not fit in 16 bits");
        }
        MessageBuilder body = new MessageBuilder ();
        body.addInt (readAddr);
        body.addInt (readCount);
        body.addInt (writeAddr);
        body.addInt (writeCount);
        body.addByte (data.length > 255 ? 0 : data.length);
        body.addData (data);
        ModbusRequest request = new ModbusRequest (slaveId,
            Modbus.FUNC_READ_WRITE_MULTIPLE_REGISTERS, client.genTransId (),
            body.getData ());
        ModbusResponse reply = client.handleRequest (request);
        reply.checkMinSize (1 + readBytes);
        int byteCount = checkSize (reply, 1, reply.getByte (0));
        if (strictChecking && byteCount != readBytes)
        {
            Modbus.dataError ("M101",
                "Wrong byte count in response: " +
                byteCount + " when expecting " + readBytes);
        }
        data = reply.getData (1, readBytes);
        model.unpack (data, readAddress, readNValues, space, strictChecking,
            slaveId, false);
    }
}



