
package uk.co.wingpath.modbus;

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

/**
* <p>This class is used to represent Modbus transactions.</p>
* <p>A transaction may be one of three states:</p>
* <ul>
* <li>NEW - Not yet submitted for processing.</li>
* <li>READY - Waiting for request to be sent for the first time.</li>
* <li>TO_BE_RETRIED - Waiting for request to be retried.</li>
* <li>ACTIVE - Request sent, waiting for a response.</li>
* <li>COMPLETED - Transaction completed OK.</li>
* <li>FAILED - Transaction failed.</li>
* <li>TIMED_OUT - No response received and not to be retried.</li>
* </ul>
* <p>COMPLETED, FAILED and TIMED_OUT are final states - i.e. processing of
* the transaction has finished.
* <p>When a transaction has finished, it will contain a response
* (see {@link #getResponse}) and  possibly an exception
* (see {@link #getException}).
* If an error response has been received, the transaction will contain
* exception constructed from the response.
* If an exception has been set for some other error, the transaction will
* contain an error response contructed from the exception.
*/
public class ModbusTransaction
{
    public enum State
    {
        NEW,            // Not yet submitted for processing.
        READY,          // Waiting for request to be sent for the first time.
        TO_BE_RETRIED,  // Waiting for request to be retried.
        ACTIVE,         // Request sent, waiting for a response.
        COMPLETED,      // Transaction completed OK.
        FAILED,         // Transaction failed.
        TIMED_OUT       // No response received and not to be retried.
    }

    private final int id;
    private final ModbusMessage request;
    private final ModbusResponseHandler responseHandler;
    private final Reporter reporter;
    private ResponseTimeMonitor responseTimeMonitor;
    private ModbusMessage response;
    private ModbusException exception;

    // Time that processing of the transaction started.
    private long timeStart;

    // Time that the most recent request was sent to the slave.
    private long timeRequest;

    // Flag to indicate that a request has been sent to the slave and we are
    // waiting for a response.
    private boolean requestSent;

    // Time that a response was received from the slave.
    // 0 if no response has yet been received to the latest request.
    private long timeResponse;

    private int maxAttempts;
    private int attemptsMade;
    private long responseTimeout;
    private volatile State state;

    /**
    * Constructs a ModbusTransaction using the specified Modbus request message.
    * @param request the request message.
    * @param reporter used for reporting errors etc.
    * @param responseHandler handler to be called when the transaction is
    * finished. The handler may be null, in which case {@link ModbusClient
    * #handleTransaction} will not return until the transaction is finished.
    */
    public ModbusTransaction (ModbusMessage request,
        Reporter reporter, ModbusResponseHandler responseHandler)
    {
        if (reporter == null)
            throw new NullPointerException ("reporter must not be null");
        id = request.getTransId ();
        this.request = request;
        this.reporter = reporter;
        this.responseHandler = responseHandler;
        responseTimeMonitor = null;
        response = null;
        exception = null;
        timeStart = System.nanoTime ();
        timeRequest = timeStart;
        requestSent = false;
        timeResponse = 0;
        maxAttempts = 1;
        attemptsMade = 0;
        responseTimeout = 1000 * 1000 * 1000L;      // 1 second
        state = State.NEW;
    }

    /**
    * Constructs a ModbusTransaction using the specified Modbus request message.
    * No response handler is used, so the handleTransaction will not return
    * until the transaction is finished.
    * @param request the request message.
    */
    public ModbusTransaction (ModbusMessage request, Reporter reporter)
    {
        this (request, reporter, null);
    }

    /**
    * Sets the {@link ResponseTimeMonitor} that will be informed of the response
    * time when a response has been received.
    * <p>The monitor may be informed of the response time before or after the
    * {@link ModbusResponseHandler} has been called.
    * The monitor is informed when a response is actually received,
    * even if this is after the transaction has timed out.
    */
    public void setResponseTimeMonitor (ResponseTimeMonitor monitor)
    {
        responseTimeMonitor = monitor;
    }

    /**
    * Informs the response-time monitor of the response time.
    */
    private void informResponseTimeMonitor ()
    {
        if (responseTimeMonitor != null)
        {
            long responseTime = timeResponse - timeRequest;
            if (responseTime != 0)
            {
                assert reporter.debug ("informResponseTimeMonitor %d %s",
                    id, Metric.formatNanoTime (responseTime));
                responseTimeMonitor.inform (responseTime);
            }
        }
    }

    /** Tests whether the transaction has a response handler.
    * @return true if the transaction has a response handler.
    */
    public boolean hasResponseHandler ()
    {
        return responseHandler != null;
    }

    /**
    * Gets the transaction identifier.
    */
    public int getId ()
    {
        return id;
    }

    /**
    * Gets the transaction reporter.
    * @return the reporter.
    */
    public Reporter getReporter ()
    {
        return reporter;
    }

    /**
    * Gets the state of the transaction.
    * @return the transaction state.
    */
    public State getState ()
    {
        return state;
    }

    /**
    * Is the transaction finished?
    * @return true if the transaction is finished.
    */
    public boolean isFinished ()
    {
        return
            state == State.COMPLETED ||
            state == State.FAILED ||
            state == State.TIMED_OUT;
    }

    /**
    * Is the transaction new?
    * @return true if the transaction is new.
    */
    public boolean isNew ()
    {
        return state == State.NEW;
    }

    /**
    * Is the transaction ready?
    * @return true if the transaction is ready.
    */
    public boolean isReady ()
    {
        return state == State.READY;
    }

    /**
    * Is the transaction to be retried?
    * @return true if the transaction is to be retried.
    */
    public boolean isToBeRetried ()
    {
        return state == State.TO_BE_RETRIED;
    }

    /**
    * Is the transaction timed out?
    * @return true if the transaction is timed out.
    */
    public boolean isTimedOut ()
    {
        return state == State.TIMED_OUT;
    }

    /**
    * Is the transaction active?
    * @return true if the transaction is active.
    */
    public boolean isActive ()
    {
        return state == State.ACTIVE;
    }

    /**
    * Is the transaction completed?
    * @return true if the transaction is completed.
    */
    public boolean isCompleted ()
    {
        return state == State.COMPLETED;
    }

    /**
    * Is the transaction failed?
    * @return true if the transaction is failed.
    */
    public boolean isFailed ()
    {
        return state == State.FAILED;
    }

    /**
    * Marks the transaction as ready.
    */
    public void setReady ()
    {
        if (state != State.NEW)
            throw new IllegalStateException (state.toString ());
        state = State.READY;
    }

    /**
    * Informs any waiter that the transaction has finished.
    */
    private void informWaiter ()
    {
        if (responseHandler != null)
        {
            responseHandler.handleResponse (this);
        }
        else
        {
            synchronized (this)
            {
                this.notifyAll ();
            }
        }
    }

    /**
    * Marks the transaction as completed, and informs any waiter.
    */
    private void setCompleted ()
    {
        assert state == State.ACTIVE ||
            state == State.TO_BE_RETRIED : state;
            
        state = State.COMPLETED;
        exception = null;
        informWaiter ();
    }


    /**
    * Marks the transaction as active.
    */
    public void setActive ()
    {
        if (state != State.NEW &&
            state != State.READY &&
            state != State.TO_BE_RETRIED)
        {
            throw new IllegalStateException (state.toString ());
        }
        if (attemptsMade >= maxAttempts)
        {
            throw new IllegalStateException (
                "attemptsMade " + attemptsMade +
                " >= maxAttempts " + maxAttempts);
        }
        state = State.ACTIVE;
        ++attemptsMade;
        assert reporter.debug ("setActive %d %d", id, attemptsMade);
    }

    /**
    * Marks the transaction as having timed out, or as to be retried if
    * the maximum number of attempts has not been reached.
    */
    public void setTimedOut (boolean allowRetry)
    {
        assert reporter.debug ("setTimedOut %d %s", id, state);

        if (state != State.ACTIVE &&
            state != State.READY &&
            state != State.TO_BE_RETRIED)
        {
            throw new IllegalStateException (state.toString ());
        }

        // See if we can retry.
        if (allowRetry && attemptsMade < maxAttempts)
        {
            state = State.TO_BE_RETRIED;
        }
        else
        {
            if (exception == null)
            {
                exception = new ModbusException (Modbus.ERROR_TIMED_OUT,
                    "I120", "Timed out");
            }
            if (response == null)
            {
                response = new ModbusMessage (false, request.getSlaveId (),
                        request.getFunctionCode () | 0x80,
                        request.getTransId (),
                        new byte [] { Modbus.ERROR_TIMED_OUT },
                        request.getTracer ());
                response.setException (exception);
            }
            state = State.TIMED_OUT;
            informWaiter ();
        }
    }

    /**
    * Waits until the transaction has finished.
    */
    public void waitUntilFinished ()
        throws InterruptedException
    {
        if (responseHandler != null)
        {
            throw new IllegalStateException (
                "waitUntilFinished called when response handler supplied");
        }
        synchronized (this)
        {
            assert reporter.debug ("waitUntilFinished %d %s", id, state);
            while (!isFinished ())
                this.wait ();
            assert reporter.debug ("waitUntilFinished %d done", id);
        }
    }

    /**
    * Gets the Modbus request message.
    * @return the Modbus request message.
    */
    public ModbusMessage getRequest ()
    {
        return request;
    }

    /**
    * Gets the Modbus response message, if any.
    * @return the Modbus response message, or null if there isn't one yet.
    */
    public ModbusMessage getResponse ()
    {
        return response;
    }

    /**
    * Used by a Modbus master to test whether a response message matches this
    * transaction.
    * <p>The transaction ID, function code and slave ID are checked to see
    * if they match those in the request message.
    * @param response the response message.
    * @return true if the transaction ID, function code and slave ID match.
    */
    public boolean doesMatchResponse (ModbusMessage response)
    {
        // This method is called for late responses as well as expected
        // responses. so the state could be any of the states possible after
        // a request has been sent.
        if (state == State.NEW || state == State.READY)
            throw new IllegalStateException (state.toString ());

        // Try matching the slave ID, transaction ID and function code.
        if (response.getSlaveId () != request.getSlaveId ())
            return false;
        int transId = response.getTransId ();
        if (transId != 0 && transId != id)
            return false;
        if ((response.getFunctionCode () & 0x7f) != request.getFunctionCode ())
            return false;
        return true;
    }

    /**
    * Used by a Modbus master to set the response message.
    * <p>If the response indicates a recoverable error and the request can be
    * retried, the transaction state is reset to ready.
    * Otherwise, the transaction is flagged as finished and the response handler
    * (if any) is called.
    * @param response the response message.
    */
    public void setResponse (ModbusMessage response)
    {
        assert doesMatchResponse (response);

        // This method is called for late responses as well as expected
        // responses. so the state could be any of the states possible after
        // a request has been sent.
        if (state == State.NEW || state == State.READY)
            throw new IllegalStateException (state.toString ());

        // Record the time that the response was received.
        setTimeResponse ();

        // Inform any response-time monitor.
        informResponseTimeMonitor ();

        // If this is a late response, don't do anything else with it.
        if (isFinished ())
            return;

        assert state == State.ACTIVE || state == State.TO_BE_RETRIED : state;

        // Construct a new response message to ensure that it contains the
        // correct transaction ID (RTU and ASCII packet types don't support
        // transaction IDs).
        this.response = new ModbusMessage (false,
            response.getSlaveId (),
            response.getFunctionCode (),
            request.getTransId (),
            response.getData (),
            response.getTracer ());

        // If the message is an error response, construct an exception
        // from the response.
        if (response.isError ())
        {
            exception = new ModbusException (response);
            response.setException (exception);

            // Treat timed-out errors specially, since we may be able to retry.
            if (exception.getErrorCode () == Modbus.ERROR_TIMED_OUT)
            {
                setTimedOut (true);
            }
            else
            {
                state = State.FAILED;
                informWaiter ();
            }
        }
        else
        {
            // Response was OK.
            setCompleted ();
        }

        assert
            state == State.TIMED_OUT ||
            state == State.TO_BE_RETRIED ||
            state == State.FAILED ||
            state == State.COMPLETED : state;
    }

    /**
    * Used by a Modbus slave to construct a response with the specified data,
    * and the same slave id, function code and transaction id as this request.
    * <p>The transaction is flagged as finished and the response handler
    * (if any) is called.
    * @param data body of message.
    */
    public void setResponse (byte [] data, Tracer.Tracing tracing)
    {
        if (state != State.ACTIVE)
            throw new IllegalStateException (state.toString ());
        response = new ModbusMessage (false, request.getSlaveId (),
            request.getFunctionCode (), request.getTransId (), data,
            request.getTracer ());
        response.setTracing (tracing);
        setTimeResponse ();
        informResponseTimeMonitor ();
        setCompleted ();
    }

    /**
    * Fabricates a successful response to this request.
    * <p>This is used where a master requires a response for its own purposes,
    * but no response is available from a slave (e.g. if the request is a
    * broadcast).
    * <p>This only makes sense for write requests.
    * <p>The transaction is flagged as finished and the response handler
    * (if any) is called.
    */
    public void setResponse ()
    {
        if (state != State.ACTIVE)
            throw new IllegalStateException (state.toString ());
        MessageBuilder body = new MessageBuilder ();
        switch (request.getFunctionCode ())
        {
        case Modbus.FUNC_WRITE_MULTIPLE_REGISTERS:
        case Modbus.FUNC_WRITE_MULTIPLE_COILS:
            body.addInt (request.getInt (0));
            body.addInt (request.getInt (2));
            break;

        case Modbus.FUNC_WRITE_SINGLE_REGISTER:
        case Modbus.FUNC_MASK_WRITE_REGISTER:
        case Modbus.FUNC_WRITE_SINGLE_COIL:
        case Modbus.FUNC_DIAGNOSTICS:
            body.addData (request.getData ());
            break;

        default:
            // Presumably a custom command, so we have no idea what the
            // response might have been if the request had not been broadcast.
            // Provide an empty response.
            break;
        }

        response = new ModbusMessage (false, request.getSlaveId (),
            request.getFunctionCode (), request.getTransId (), body.getData (),
            request.getTracer ());
        setTimeResponse ();
        setCompleted ();
    }

    /**
    * <p>Sets the exception for this transaction, and constructs a response
    * message using the error code from the exception and the slave id,
    * function code and transaction id from the request.
    * <p>The transaction is flagged as finished and the response handler (if
    * any) is called.
    * <p>This method is called by a Modbus master for irrecoverable errors
    * when sending a request.
    * <p>This method is called by a Modbus slave to construct an error response.
    * @param e the Modbus exception.
    */
    public void setException (ModbusException e)
    {
        if (state != State.ACTIVE)
            throw new IllegalStateException (state.toString ());

        exception = e;
        response = new ModbusMessage (false, request.getSlaveId (),
                request.getFunctionCode () | 0x80, request.getTransId (),
                new byte [] { (byte) e.getErrorCode () },
                request.getTracer ());
        response.setException (exception);
        setTimeResponse ();
        state = State.FAILED;
        informWaiter ();
    }

    /**
    * Gets the exception for this transaction, if any.
    * @return the exception, or null is there is none.
    */
    public ModbusException getException ()
    {
        return exception;
    }

    /**
    * Sets the maximum number of attempts that will be made to handle this
    * transaction.
    * The default maximum number of attempts is 1.
    * @param maxAttempts the maximum number of attempts.
    */
    public void setMaxAttempts (int maxAttempts)
    {
        attemptsMade = 0;
        this.maxAttempts = maxAttempts;
    }

    /**
    * Gets the maximum number of attempts.
    * @return the maximum number of attempts.
    */
    public int getMaxAttempts ()
    {
        return maxAttempts;
    }

    /**
    * Gets the number of attempts made.
    * @return the number of attempts made.
    */
    public int getAttemptsMade ()
    {
        return attemptsMade;
    }

    /**
    * Sets the request time to 0.
    * <p>This is used by a master to indicate that it has not yet sent the
    * request message.
    */
    public void clearTimeRequest ()
    {
        timeRequest = 0;
        requestSent = false;
        timeResponse = 0;
    }

    /**
    * Sets the request time to now.
    * <p>This is used by a master to record the time that it finished sending
    * the request message.
    */
    public void setTimeRequest ()
    {
        timeRequest = System.nanoTime ();
        requestSent = true;
        timeResponse = 0;
    }

    /**
    * Gets the number of nanoseconds elapsed since the transaction was created.
    * @return age of the transaction in nanoseconds.
    */
    public long getAge ()
    {
        return System.nanoTime () - timeStart;
    }

    /**
    * Gets the number of nanoseconds elapsed since {@link #setTimeRequest}
    * was last called. Returns 0 if {@link #clearTimeRequest} has been called
    * since the last call of setTimeRequest.
    * @return age of the request in nanoseconds.
    */
    public long getAgeRequest ()
    {
        if (!requestSent)
            return 0;
        return System.nanoTime () - timeRequest;
    }

    /**
    * Sets the time of the response to now.
    */
    private void setTimeResponse ()
    {
        timeResponse = System.nanoTime ();
        if (!requestSent)
        {
            timeRequest = timeStart;
            requestSent = true;
        }
    }

    /**
    * Gets the response time, which is simply the difference between
    * the times of the response and the request.
    * @return the response time in nanoseconds, or 0 if the time of the response
    * has not been set.
    */
    public long getResponseTime ()
    {
        if (timeResponse == 0)
            return 0;
        return timeResponse - timeRequest;
    }

    /**
    * Sets the timeout for receiving a response from a slave.
    * @param timeout the timeout period in nanoseconds.
    */
    public void setResponseTimeout (long timeout)
    {
        responseTimeout = timeout;
    }

    /**
    * Gets the timeout for receiving a response from a slave.
    * @return timeout the timeout period in nanoseconds.
    */
    public long getResponseTimeout ()
    {
        return responseTimeout;
    }
}


