
package uk.co.wingpath.modbus;

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

/**
* Implementation of the {@code PacketType} interface for sending and receiving
* Modbus messages using the Modbus/TCP protocol.
*/
public class TcpPacketType
    implements PacketType
{
    private volatile int timeout = 1000;
    private volatile int eomTimeout = 10;
    private int maxPdu = Modbus.MAX_IMP_PDU_SIZE;

    public void setMaxPdu (int maxPdu)
    {
        this.maxPdu = maxPdu;
    }

    public void setAllowLongMessages (boolean allowLongMessages)
    {
    }

    public void send (Connection connection, Tracer tracer,
            ModbusMessage message)
        throws IOException, InterruptedException
    {
        byte [] data = message.getData ();
        byte [] buf = new byte [data.length + 8];
        int transId = message.getTransId ();
        buf [0] = (byte) (transId >> 8);
        buf [1] = (byte) transId;
        buf [2] = buf [3] = 0;      // protocol
        int len = data.length + 2;
        assert len <= 65535 : len;
        buf [4] = (byte) (len >> 8);
        buf [5] = (byte) len;
        buf [6] = (byte) message.getSlaveId ();
        buf [7] = (byte) message.getFunctionCode ();
        System.arraycopy (data, 0, buf, 8, data.length);
        if (tracer != null)
        {
            tracer.traceRaw (">", buf, 0, buf.length);
            message.traceSend (tracer);
        }
        connection.write (buf, 0, buf.length);
    }

    private void read (Connection connection, Tracer tracer,
            byte [] buf, int offset, boolean isHeader,
            Reporter reporter)
        throws IOException, InterruptedException
    {
        int len = buf.length - offset;
        boolean first = isHeader;
        try
        {
            while (len > 0)
            {
                int n = connection.read (buf, offset, len,
                    first ? timeout : eomTimeout, first);
                offset += n;
                len -= n;
                first = false;
            }
        }
        catch (InterruptedIOException e)
        {
            if (first)
                throw e;
            if (tracer != null)
                tracer.traceRaw ("<", buf, 0, offset);
            String msg = isHeader ?
                "Incomplete message: only " + offset + " bytes" :
                "Incomplete message: only " + offset +
                " bytes when expecting " + buf.length;
            String helpId = isHeader ? "S605" : "S601";
            ModbusMessage.traceDiscard (tracer, helpId, msg, buf, 0, offset);
            if (reporter != null)
                reporter.warning (helpId, "Invalid data received: " + msg);
            throw new RecoverableIOException (helpId, msg);
        }
    }

    public ModbusMessage receive (Connection connection,
            Tracer tracer, boolean isRequest, Reporter reporter)
        throws InterruptedIOException, IOException, InterruptedException
    {
        byte [] header = new byte [8];
        read (connection, tracer, header, 0, true, reporter);
        int transId = ((header [0] & 0xff) << 8) | (header [1] & 0xff);
        int protoId = ((header [2] & 0xff) << 8) | (header [3] & 0xff);
        int len = ((header [4] & 0xff) << 8) | (header [5] & 0xff);
        int slaveId = header [6] & 0xff;
        int function = header [7] & 0xff;
        if (protoId != 0)
        {
            if (tracer != null)
                tracer.traceRaw ("<", header, 0, header.length);
            String msg = "Incorrect protocol identifier: " +
                protoId;
            ModbusMessage.traceDiscard (tracer, "S602", msg,
                header, 0, header.length);
            if (reporter != null)
                reporter.warning ("S602", "Invalid data received: " + msg);
            throw new RecoverableIOException ("S602", msg);
        }
        if (len < 2)
        {
            if (tracer != null)
                tracer.traceRaw ("<", header, 0, header.length);
            String msg = "Length (" + len + ") too small";
            ModbusMessage.traceDiscard (tracer, "S602", msg,
                header, 0, header.length);
            if (reporter != null)
                reporter.warning ("S603", "Invalid data received: " + msg);
            throw new RecoverableIOException ("S603", msg);
        }
        len -= 2;
        byte [] buf = new byte [len + header.length];
        System.arraycopy (header, 0, buf, 0, header.length); // for tracing
        if (len > 0)
            read (connection, tracer, buf, header.length, false, reporter);
        if (tracer != null)
            tracer.traceRaw ("<", buf, 0, len + header.length);
        if (len + 1 > maxPdu)
        {
            String msg = "PDU size (" + (len + 1) +
                " bytes) exceeds maximum (" + maxPdu + " bytes)";
            ModbusMessage.traceDiscard (tracer, "S604", msg, buf, 0,
                len + header.length);
            if (reporter != null)
                reporter.warning ("S604", "Invalid data received: " + msg);
            throw new RecoverableIOException ("S604", msg);
        }
        MessageBuilder body = new MessageBuilder ();
        body.addData (buf, header.length, len);
        if (isRequest)
        {
            return new ModbusRequest (slaveId, function, transId,
                body.getData ());
        }
        else if ((function & 0x80) != 0 && len > 0)
        {
            return new ModbusErrorResponse (slaveId, function, transId,
                buf [8] & 0xff);
        }
        else
        {
            return new ModbusResponse (slaveId, function, transId,
                body.getData ());
        }
    }

    public boolean hasTransactionIds ()
    {
        return true;
    }

    public void setTimeout (int timeout)
    {
        this.timeout = timeout;
    }

    public void setEomTimeout (int timeout)
    {
        eomTimeout = timeout;
    }

    public int setDelimiter (int delimiter)
    {
        return delimiter;
    }
}

