
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;
    private boolean allowLongMessages = false;

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

    @Override
    public void setAllowLongMessages (boolean allowLongMessages)
    {
        this.allowLongMessages = allowLongMessages;
    }

    @Override
    public void send (Connection connection, 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);
        Tracer tracer = message.getTracer ();
        if (tracer != null)
        {
            tracer.traceRaw (">", buf, 0, buf.length);
            message.traceSend ();
        }
        connection.write (buf, 0, buf.length);
    }

    private void read (Connection connection, Tracer tracer,
            byte [] buf, int offset, boolean isHeader)
        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);
            throw new RecoverableIOException (helpId, msg);
        }
    }

    private ModbusMessage receive (Connection connection,
            boolean expectRequest, Tracer tracer, ModbusCounters counters)
        throws InterruptedIOException, IOException, InterruptedException
    {
        byte [] header = new byte [8];
        read (connection, tracer, header, 0, true);
        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);
            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, "S603", msg,
                header, 0, header.length);
            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);
        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, "S308", msg, buf, 0,
                len + header.length);
            throw new RecoverableIOException ("S308", msg);
        }
        MessageBuilder body = new MessageBuilder ();
        body.addData (buf, header.length, len);
        ModbusMessage message = new ModbusMessage (expectRequest, slaveId,
            function, transId, body.getData (), tracer);
        try
        {
            message.checkSize (allowLongMessages);
        }
        catch (ModbusException e)
        {
            if (expectRequest)
            {
                // In a multi-drop setup we may see responses from other
                // slaves, so see if size is valid for a response.
                try
                {
                    ModbusMessage msg = new ModbusMessage (false, slaveId,
                        function, transId, body.getData (), tracer);
                    msg.checkSize (allowLongMessages);
                    message = msg;
                }
                catch (ModbusException e1)
                {
                }
            }
        }
        message.traceReceive ();
        return message;
    }

    @Override
    public ModbusMessage receiveRequest (Connection connection, Tracer tracer,
            ModbusCounters counters)
        throws InterruptedIOException, IOException, InterruptedException
    {
        for (;;)
        {
            ModbusMessage message = receive (connection, true, tracer,
                counters);
            if (message.isRequest ())
                return message;
            // Presumably a response from another slave.
            ModbusMessage.traceDiscard (tracer, "S401", "Unexpected");
        }
    }

    @Override
    public ModbusMessage receiveResponse (Connection connection, Tracer tracer,
            ModbusCounters counters)
        throws InterruptedIOException, IOException, InterruptedException
    {
        return receive (connection, false, tracer, counters);
    }

    @Override
    public ModbusMessage receive (Connection connection, Tracer tracer,
            ModbusCounters counters)
        throws InterruptedIOException, IOException, InterruptedException
    {
        return receive (connection, true, tracer, counters);
    }

    @Override
    public boolean hasTransactionIds ()
    {
        return true;
    }

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

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

