
package uk.co.wingpath.modbus;

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

/**
* This class is used to represent Modbus messages.
* <p>The "size" of a ModbusMessage is the number of data bytes in the message
* (excluding the header fields and CRC/LRC). It is one less than the
* "PDU size" that is now defined in the Modbus specification (this code
* was originally written long before the current version of the specification).
*/
public class ModbusMessage
{
    private final boolean isRequest;  // Whether request or response message
    private final int slaveId;        // Slave identifier
    private final int function;       // Function code
    private final byte data [];       // Data
    private final int transId;        // Transaction identifier
    private ModbusException exception;
    private final Tracer tracer;
    private Tracer.Tracing tracing;

    /**
    * Constructs a ModbusMessage with the specified slave id,
    * function code, transaction identifier, and data.
    * @param isRequest whether request or response message.
    * @param slaveId slave identifier.
    * @param function function code.
    * @param transId transaction identifier, or -1 if the transaction
    * identifier is not defined (i.e. for RTU and ASCII packets).
    * @param data body of message.
    */
    public ModbusMessage (boolean isRequest, int slaveId, int function,
        int transId, byte [] data, Tracer tracer)
    {
        this.isRequest = isRequest;
        this.slaveId = slaveId;
        this.function = function;
        this.transId = transId;
        this.data = data;
        this.tracer = tracer;
        exception = null;
        tracing = null;
    }

    /**
    * Constructs a copy of a ModbusMessage, with a different slave ID.
    * @param slaveId the new slave ID.
    * @param msg the message to be copied.
    */
    public ModbusMessage (int slaveId, ModbusMessage msg)
    {
        isRequest = msg.isRequest;
        this.slaveId = slaveId;
        function = msg.function;
        transId = msg.transId;
        data = msg.data;
        tracer = msg.tracer;
        exception = msg.exception;
        tracing = null;
    }

    /**
    * Sets the exception associated with this message.
    * This is used to provide additional information when tracing error
    * responses.
    * @param exception the exception associated with this error response.
    */
    public void setException (ModbusException exception)
    {
        this.exception = exception;
    }

    /**
    * Gets the tracer being used to trace this message.
    * @return the tracer.
    */
    public Tracer getTracer ()
    {
        return tracer;
    }

    /**
    * Sets the register tracing for this message.
    * This tracing will be logged when the message is sent.
    * @param tracing the tracing to be logged when the message is sent.
    */
    public void setTracing (Tracer.Tracing tracing)
    {
        this.tracing = tracing;
    }

    /**
    * Tests whether this is a request message.
    * @return true if this is a request message, false if it is a response
    * message.
    */
    public boolean isRequest ()
    {
        return isRequest;
    }

    /**
    * Gets the slave identifier.
    * @return the slave identifier.
    */
    public int getSlaveId ()
    {
        return slaveId;
    }

    /**
    * Gets the transaction identifier.
    * If the transaction identifier is not defined (i.e. for RTU ans ASCII),
    * 0 is returned.
    * @return the transaction identifier.
    */
    public int getTransId ()
    {
        return transId < 0 ? 0 : transId;
    }

    /**
    * Gets the function code.
    * @return the function code.
    */
    public int getFunctionCode ()
    {
        return function;
    }

    /**
    * Gets the message size (number of bytes of data).
    * This size is one less than the PDU size.
    * @return message size.
    */
    public int size ()
    {
        return data.length;
    }

    /**
    * Gets the data.
    * @return the data array.
    */
    public byte [] getData ()
    {
        return data;
    }

    /**
    * Gets a byte of the data.
    * @param offset start of data required.
    * @return the requested data byte.
    */
    public int getByte (int offset)
    {
        return (data [offset] & 0xff);
    }

    /**
    * Gets an unsigned 16-bit integer from the data.
    * @param offset start of data required.
    * @return the requested 16-bit integer.
    */
    public int getInt (int offset)
    {
        return (((data [offset] & 0xff) << 8) | (data [offset + 1] & 0xff));
    }

    /**
    * Gets a sub-array of the data.
    * @param offset start of data required.
    * @param len number of bytes required.
    * @return the requested data.
    */
    public byte [] getData (int offset, int len)
    {
        byte [] a = new byte [len];
        System.arraycopy (data, offset, a, 0, len);
        return a;
    }

    /**
    * Tests whether this is an error response.
    * @return true if this is an error response.
    */
    public boolean isError ()
    {
        return (function & 0x80) != 0;
    }

    /**
    * Gets the error code (if this is an error response).
    * @return the error code, if this is an error response, 0 otherwise.
    */
    public int getErrorCode ()
    {
        if (isError ())
            return getByte (0);
        return 0;
    }

    /**
    * Checks that the message size is what is expected, and throws a
    * ModbusException if isn't.
    * @param size expected message size.
    * @throws ModbusException if the response size is not correct.
    */
    public void checkSize (int size)
        throws ModbusException
    {
        if (size () != size)
        {
            if (isRequest)
            {
                Modbus.dataError ("R001",
                    "Request PDU size incorrect: " +
                    (size () + 1) + " instead of " + (size + 1));
            }
            else
            {
                Modbus.dataError ("S201", "Response PDU size incorrect: " +
                    (size () + 1) + " instead of " + (size + 1));
            }
        }
    }

    /**
    * Checks that the message size is not less than the specified minimum,
    * and throws a ModbusException if is.
    * @param minSize expected minimum request size.
    * @throws ModbusException if the message size is too small.
    */
    public void checkMinSize (int minSize)
        throws ModbusException
    {
        if (size () < minSize)
        {
            if (isRequest)
            {
                Modbus.dataError ("R002",
                    "Request PDU too short: " + (size () + 1) +
                    " when it should be at least " + (minSize + 1));
            }
            else
            {
                Modbus.dataError ("S202",
                    "Response PDU too short: " + (size () + 1) +
                    " when it should be at least " + (minSize + 1));
            }
        }
    }

    public void checkSize (boolean allowLongMessages)
        throws ModbusException
    {
        if (isRequest)
        {
            switch (function)
            {
            case Modbus.FUNC_READ_HOLDING_REGISTERS:
            case Modbus.FUNC_READ_INPUT_REGISTERS:
            case Modbus.FUNC_READ_COILS:
            case Modbus.FUNC_READ_DISCRETE_INPUTS:
                checkSize (4);
                break;

            case Modbus.FUNC_READ_WRITE_MULTIPLE_REGISTERS:
                checkMinSize (9);
                if (!allowLongMessages)
                    checkSize (9 + getByte (8));
                break;

            case Modbus.FUNC_REPORT_SLAVE_ID:
                checkSize (0);
                break;

            case Modbus.FUNC_WRITE_MULTIPLE_REGISTERS:
                checkMinSize (5);
                if (!allowLongMessages)
                    checkSize (5 + getByte (4));
                break;

            case Modbus.FUNC_WRITE_SINGLE_REGISTER:
                checkMinSize (3);
                break;

            case Modbus.FUNC_WRITE_MULTIPLE_COILS:
                checkMinSize (5);
                if (!allowLongMessages)
                    checkSize (5 + getByte (4));
                break;

            case Modbus.FUNC_WRITE_SINGLE_COIL:
                checkSize (4);
                break;

            case Modbus.FUNC_MASK_WRITE_REGISTER:
                checkMinSize (4);
                break;

            case Modbus.FUNC_READ_EXCEPTION_STATUS:
                checkSize (0);
                break;

            case Modbus.FUNC_DIAGNOSTICS:
                checkMinSize (2);
                break;

            case Modbus.FUNC_GET_COMM_EVENT_COUNTER:
                checkSize (0);
                break;

            case Modbus.FUNC_ENCAPSULATED_INTERFACE_TRANSPORT:
                checkMinSize (1);
                if (getByte (0) == Modbus.MEI_READ_DEVICE_IDENTIFICATION)
                    checkSize (3);
                break;

            case Modbus.FUNC_READ_FILE_RECORD:
                checkMinSize (1);
                break;

            case Modbus.FUNC_WRITE_FILE_RECORD:
                checkMinSize (1);
                break;
            }
        }
        else
        {
            if (isError ())
            {
                checkMinSize (1);
                return;
            }

            switch (function)
            {
            case Modbus.FUNC_READ_HOLDING_REGISTERS:
            case Modbus.FUNC_READ_INPUT_REGISTERS:
            case Modbus.FUNC_READ_COILS:
            case Modbus.FUNC_READ_DISCRETE_INPUTS:
            case Modbus.FUNC_READ_WRITE_MULTIPLE_REGISTERS:
            case Modbus.FUNC_REPORT_SLAVE_ID:
                checkMinSize (1);
                if (!allowLongMessages)
                    checkSize (getByte (0) + 1);
                break;

            case Modbus.FUNC_WRITE_MULTIPLE_REGISTERS:
                checkSize (4);
                break;

            case Modbus.FUNC_WRITE_SINGLE_REGISTER:
                checkMinSize (3);
                break;

            case Modbus.FUNC_WRITE_MULTIPLE_COILS:
                checkSize (4);
                break;

            case Modbus.FUNC_WRITE_SINGLE_COIL:
                checkSize (4);
                break;

            case Modbus.FUNC_MASK_WRITE_REGISTER:
                checkMinSize (4);
                break;

            case Modbus.FUNC_READ_EXCEPTION_STATUS:
                checkSize (1);
                break;

            case Modbus.FUNC_DIAGNOSTICS:
                checkMinSize (2);
                break;

            case Modbus.FUNC_GET_COMM_EVENT_COUNTER:
                checkSize (4);
                break;

            case Modbus.FUNC_ENCAPSULATED_INTERFACE_TRANSPORT:
                checkMinSize (1);
                if (getByte (0) == Modbus.MEI_READ_DEVICE_IDENTIFICATION)
                    checkMinSize (6);
                break;

            case Modbus.FUNC_READ_FILE_RECORD:
                checkMinSize (1);
                if (!allowLongMessages)
                    checkSize (getByte (0) + 1);
                break;

            case Modbus.FUNC_WRITE_FILE_RECORD:
                checkMinSize (1);
                if (!allowLongMessages)
                    checkSize (getByte (0) + 1);
                break;
            }
        }
    }

    /**
    * Returns a string describing the function code (and sub-function code,
    * if any).
    * @return function code description.
    */
    public String getFunctionName ()
    {
        String str = Modbus.getFunctionName (function);
        if (str == null)
            return "Function code " + function;
        if (function == Modbus.FUNC_DIAGNOSTICS && data.length >= 2)
        {
            int subfunc = getInt (0);
            String s = Modbus.getDiagFunctionName (subfunc);
            if (s != null)
                str += " - " + s;
            else
                str += " - subfunction " + subfunc;
        }
        else if (function == Modbus.FUNC_ENCAPSULATED_INTERFACE_TRANSPORT &&
            data.length >= 1)
        {
            int type = getByte (0);
            String s = Modbus.getEitTypeName (type);
            if (s != null)
                str += " - " + s;
            else
                str += " - type " + type;
        }

        return str;
    }

    void traceMessage (String prefix, String helpId)
    {
        if (tracer == null || !tracer.isTracing () ||
            !tracer.isIntTraceEnabled ())
        {
            return;
        }
        StringBuilder str = new StringBuilder ();
        if (transId >= 0)
        {
            str.append (" transid ");
            str.append (transId);
        }
        str.append (" slave ");
        str.append (slaveId);
        str.append (" pdulen ");
        str.append (data.length + 1);
        str.append (" func ");
        str.append (function);
        str.append (": ");
        if (data.length <= 10)
        {
            str.append (Bytes.toHexString (data, 0, data.length));
        }
        else
        {
            str.append (Bytes.toHexString (data, 0, 10));
            str.append (" ...");
        }
        tracer.trace (null, prefix, str.toString ());
    }

    static void traceExplanation (Tracer tracer, String helpId, String msg)
    {
        if (tracer == null)
            return;
        tracer.trace (helpId, "      " + msg);
    }

    void traceExplanation (String helpId, String msg)
    {
        traceExplanation (tracer, helpId, msg);
    }

    static void traceError (Tracer tracer, String helpId, String msg)
    {
        if (tracer == null)
            return;
        tracer.trace (helpId, "<", "     " + msg);
    }

    static void traceDiscard (Tracer tracer, String helpId, String msg,
        byte [] data, int offset, int len)
    {
        if (tracer == null || !tracer.isTracing () ||
            !tracer.isIntTraceEnabled ())
        {
            return;
        }
        if (!tracer.isRawTraceEnabled ())
        {
            tracer.trace (null, "<",
                " " + Bytes.toHexString (data, offset, len));
        }
        traceExplanation (tracer, helpId,
            "Discarded " + len + " bytes: " + msg);
    }

    static void traceDiscard (Tracer tracer, String helpId, String msg)
    {
        traceExplanation (tracer, helpId, "Discarded message: " + msg);
    }

    void traceInterpretation ()
    {
        if (tracer == null || !tracer.isTracing () ||
            !tracer.isIntTraceEnabled ())
        {
            return;
        }
        StringBuilder msg = new StringBuilder ();
        if (isRequest)
        {
            msg.append ("Request: ");
            msg.append (getFunctionName ());

            try
            {
                switch (function)
                {
                case Modbus.FUNC_READ_HOLDING_REGISTERS:
                case Modbus.FUNC_READ_INPUT_REGISTERS:
                case Modbus.FUNC_READ_COILS:
                case Modbus.FUNC_READ_DISCRETE_INPUTS:
                    msg.append (": address ");
                    msg.append (getInt (0));
                    msg.append (", count ");
                    msg.append (getInt (2));
                    break;

                case Modbus.FUNC_WRITE_MULTIPLE_REGISTERS:
                case Modbus.FUNC_WRITE_MULTIPLE_COILS:
                    msg.append (": address ");
                    msg.append (getInt (0));
                    msg.append (", count ");
                    msg.append (getInt (2));
                    msg.append (", byte count ");
                    msg.append (getByte (4));
                    msg.append (", data bytes ");
                    msg.append (size () - 5);
                    break;

                case Modbus.FUNC_WRITE_SINGLE_REGISTER:
                    msg.append (": address ");
                    msg.append (getInt (0));
                    msg.append (", data bytes ");
                    msg.append (size () - 2);
                    break;

                case Modbus.FUNC_MASK_WRITE_REGISTER:
                    msg.append (": address ");
                    msg.append (getInt (0));
                    msg.append (", data bytes ");
                    msg.append (size () - 2);
                    break;

                case Modbus.FUNC_WRITE_SINGLE_COIL:
                    msg.append (": address ");
                    msg.append (getInt (0));
                    msg.append (", value ");
                    msg.append (Integer.toHexString (getInt (2)));
                    break;

                case Modbus.FUNC_READ_WRITE_MULTIPLE_REGISTERS:
                    msg.append (": read address ");
                    msg.append (getInt (0));
                    msg.append (", read count ");
                    msg.append (getInt (2));
                    msg.append (": write address ");
                    msg.append (getInt (4));
                    msg.append (", write count ");
                    msg.append (getInt (6));
                    msg.append (", byte count ");
                    msg.append (getByte (8));
                    msg.append (", data bytes ");
                    msg.append (size () - 9);
                    break;

                case Modbus.FUNC_READ_EXCEPTION_STATUS:
                    break;

                case Modbus.FUNC_DIAGNOSTICS:
                    msg.append (": data ");
                    msg.append (getInt (2));
                    break;

                case Modbus.FUNC_GET_COMM_EVENT_COUNTER:
                    break;

                case Modbus.FUNC_REPORT_SLAVE_ID:
                    break;

                case Modbus.FUNC_ENCAPSULATED_INTERFACE_TRANSPORT:
                    if (getByte (0) == Modbus.MEI_READ_DEVICE_IDENTIFICATION)
                    {
                        msg.append (": code ");
                        msg.append (getByte (1));
                        msg.append (" objectid ");
                        msg.append (String.format ("%02x", getByte (2)));
                    }
                    break;

                default:
                    break;
                }
            }
            catch (Exception e)
            {
            }

            traceExplanation (null, msg.toString ());
        }
        else
        {
            if (isError ())
            {
                msg.append ("Error response: ");
                int errorCode = getErrorCode ();
                msg.append (Modbus.getErrorMessage (errorCode));
                String helpId;
                if (exception != null)
                {
                    String expl = exception.getExplanation ();
                    if (expl != null)
                    {
                        msg.append (" - ");
                        msg.append (expl);
                    }
                    helpId = exception.getHelpId ();
                }
                else
                {
                    helpId = Modbus.getHelpId (errorCode);
                }
                traceExplanation (helpId, msg.toString ());
            }
            else
            {
                msg.append ("Response: ");
                msg.append (getFunctionName ());

                try
                {
                    switch (function)
                    {
                    case Modbus.FUNC_READ_HOLDING_REGISTERS:
                    case Modbus.FUNC_READ_INPUT_REGISTERS:
                    case Modbus.FUNC_READ_COILS:
                    case Modbus.FUNC_READ_DISCRETE_INPUTS:
                    case Modbus.FUNC_READ_WRITE_MULTIPLE_REGISTERS:
                        msg.append (": byte count ");
                        msg.append (getByte (0));
                        msg.append (", data bytes ");
                        msg.append (size () - 1);
                        break;

                    case Modbus.FUNC_WRITE_MULTIPLE_REGISTERS:
                    case Modbus.FUNC_WRITE_MULTIPLE_COILS:
                        msg.append (": address ");
                        msg.append (getInt (0));
                        msg.append (", count ");
                        msg.append (getInt (2));
                        break;

                    case Modbus.FUNC_WRITE_SINGLE_REGISTER:
                        msg.append (": address ");
                        msg.append (getInt (0));
                        msg.append (", data bytes ");
                        msg.append (size () - 2);
                        break;

                    case Modbus.FUNC_MASK_WRITE_REGISTER:
                        msg.append (": address ");
                        msg.append (getInt (0));
                        msg.append (", data bytes ");
                        msg.append (size () - 2);
                        break;

                    case Modbus.FUNC_WRITE_SINGLE_COIL:
                        msg.append (": address ");
                        msg.append (getInt (0));
                        msg.append (", value ");
                        msg.append (Integer.toHexString (getInt (2)));
                        break;

                    case Modbus.FUNC_READ_EXCEPTION_STATUS:
                        msg.append (": status ");
                        msg.append (Integer.toHexString (getByte (0)));
                        break;

                    case Modbus.FUNC_DIAGNOSTICS:
                        msg.append (": data ");
                        msg.append (getInt (2));
                        break;

                    case Modbus.FUNC_GET_COMM_EVENT_COUNTER:
                        msg.append (": status ");
                        msg.append (getInt (0));
                        msg.append (", count ");
                        msg.append (getInt (2));
                        break;

                    case Modbus.FUNC_REPORT_SLAVE_ID:
                        msg.append (": byte count ");
                        msg.append (getByte (0));
                        break;

                    case Modbus.FUNC_ENCAPSULATED_INTERFACE_TRANSPORT:
                        if (getByte (0) ==
                            Modbus.MEI_READ_DEVICE_IDENTIFICATION)
                        {
                            msg.append (": code ");
                            msg.append (getByte (1));
                        }
                        break;

                    default:
                        break;
                    }
                }
                catch (Exception e)
                {
                }

                traceExplanation (null, msg.toString ());
            }
        }
    }

    void traceSend ()
    {
        traceMessage (">", null);
        traceInterpretation ();
        tracer.trace (tracing);
    }

    void traceReceive ()
    {
        traceMessage ("<", null);
        traceInterpretation ();
    }

    @Override
    public boolean equals (Object obj)
    {
        if (!(obj instanceof ModbusMessage))
            return false;
        ModbusMessage msg = (ModbusMessage) obj;
        return
            msg.isRequest == isRequest &&
            msg.slaveId == slaveId &&
            msg.function == function &&
            Arrays.equals (msg.data, data) &&
            msg.transId == transId;
    }

    @Override
    public int hashCode ()
    {
        int hash = isRequest ? 1 : 0;
        hash = 29 * hash + slaveId;
        hash = 29 * hash + function;
        hash = 29 * hash + Arrays.hashCode (data);
        hash = 29 * hash + transId;
        return hash;
    }
}

