
package uk.co.wingpath.modbusgui;

import java.util.*;
import java.io.*;
import java.util.concurrent.*;
import java.awt.EventQueue;
import uk.co.wingpath.modbus.*;
import uk.co.wingpath.io.*;
import uk.co.wingpath.util.*;
import uk.co.wingpath.gui.*;
import uk.co.wingpath.event.*;

public class Backend
{
    public enum Mode
    {
        master,
        slave,
        monitor
    }

    private boolean isRunning = false;
    private boolean isStopped = true;

    private BackendState.Source listeners = new BackendState.Source ();

    public synchronized void addStateListener (BackendState.Listener l)
    {
        listeners.addListener (l);
        l.stateChanged (new BackendState (isRunning, isStopped,
            mode.isMaster (), mode.isSlave ()));
    }

    public synchronized void removeStateListener (BackendState.Listener l)
    {
        listeners.removeListener (l);
    }

    private void reportState ()
    {
        listeners.reportState (new BackendState (isRunning, isStopped,
            mode.isMaster (), mode.isSlave ()));
    }

    private interface BackendMode
    {
        void start ()
            throws IOException;
        void stop ();
        boolean isMaster ();
        boolean isSlave ();
    }

    private final Frontend frontend;
    private final Reporter reporter;
    private final Reporter reporterNSB;
    private final Settings settings;
    private final InterfaceSettings masterInterfaceSettings;
    private final InterfaceSettings slaveInterfaceSettings;
    private final Variable<Boolean> tracing;
    private final Variable<Boolean> intTraceSetting;
    private final Variable<Boolean> rawTraceSetting;
    private final Variable<Boolean> readTraceSetting;
    private final Variable<Boolean> writeTraceSetting;
    private Thread.UncaughtExceptionHandler exceptionHandler;

    private EnumMap<Mode,BackendMode> modeMap;
    private BackendMode mode;
    private final ExecutorService controlExecutor;
    private volatile ExecutorService poolExecutor;
    private volatile ModbusClient client;
    private volatile ModbusSlave slave;
    private volatile ModbusServiceFactory serviceFactory;
    private volatile Service service;
    private SocketServer server;
    private volatile PacketType masterPacketType;
    private volatile PacketType slavePacketType;
    private volatile String lastResponseTime = "";

    public Backend (Frontend frontend,
        Settings settings,
        boolean masterModesOnly,
        Thread.UncaughtExceptionHandler exceptionHandler,
        Reporter reporter, Reporter reporterNSB)
    {
        // Note that this constructor is called before the frontend has
        // built its GUI, so we can't, for example, get the status bar yet.
        if (reporter == null)
            throw new NullPointerException ("reporter must not be null");
        if (reporterNSB == null)
            throw new NullPointerException ("reporterNSB must not be null");
        this.frontend = frontend;
        this.settings = settings;
        this.exceptionHandler = exceptionHandler;
        this.reporter = reporter;
        this.reporterNSB = reporterNSB;
        tracing = settings.getGeneral ().getTracingSetting ();
        intTraceSetting = settings.getGeneral ().getIntTraceSetting ();
        rawTraceSetting = settings.getGeneral ().getRawTraceSetting ();
        readTraceSetting = settings.getGeneral ().getReadTraceSetting ();
        writeTraceSetting = settings.getGeneral ().getWriteTraceSetting ();

        masterInterfaceSettings = settings.getMasterInterface ();
        slaveInterfaceSettings = settings.getSlaveInterface ();

        // Add a shutdown-hook to close I/O connections.
        // Although the backend thread-pool has its own shutdown-hook, this
        // can't stop threads that are blocked in I/O.
        // Since this shutdown-hook closes I/O connections
        // using by the backend, it should unblock any threads waiting on
        // those connections.
        try
        {
            Runtime.getRuntime ().addShutdownHook (
                new Thread (new Runnable ()
                {
                    public void run ()
                    {
                        closeClient ();
                        closeSlave ();
                    }
                },
                "Backend-Shutdown"));
        }
        catch (IllegalStateException e)
        {
            // Already shutting down
        }
        catch (SecurityException e)
        {
            // May happen if called from applet.
        }

        // Create control thread for running backend tasks atomically.
        controlExecutor = ThreadPool.createSingle ("Backend-Control",
            exceptionHandler);

        // Create thread pool for running backend tasks.
        poolExecutor = null;

        client = null;
        slave = null;
        serviceFactory = null;
        service = null;
        server = null;
        masterPacketType = null;
        slavePacketType = null;
        modeMap = new EnumMap<Mode,BackendMode> (Mode.class);
        modeMap.put (Mode.master, new MasterMode ());

        if (!masterModesOnly)
        {
            modeMap.put (Mode.slave, new SlaveMode ());
            modeMap.put (Mode.monitor, new MonitorMode ());
        }
        mode = modeMap.get (Mode.master);
        reportState ();

        final Variable<Integer> slaveId = settings.getGeneral ().getSlaveId ();
        slaveId.addValueListener (
            new ValueListener ()
            {
                public void valueChanged (ValueEvent e)
                {
                    controlExecutor.execute (
                        new Runnable ()
                        {
                            public void run ()
                            {
                                if (slave != null)
                                    slave.setSlaveId (slaveId.getValue ());
                            }
                        });
                }
            });

        final Variable<Integer> maxPdu = settings.getGeneral ().getMaxPdu ();
        maxPdu.addValueListener (
            new ValueListener ()
            {
                public void valueChanged (ValueEvent e)
                {
                    controlExecutor.execute (
                        new Runnable ()
                        {
                            public void run ()
                            {
                                int mp = maxPdu.getValue ();
                                if (slave != null)
                                    slave.setMaxPdu (mp);
                                if (masterPacketType != null)
                                    masterPacketType.setMaxPdu (mp);
                                if (slavePacketType != null)
                                    slavePacketType.setMaxPdu (mp);
                            }
                        });
                }
            });

        final Variable<Boolean> checkCountLimits =
            settings.getGeneral ().getCheckCountLimits ();
        checkCountLimits.addValueListener (
            new ValueListener ()
            {
                public void valueChanged (ValueEvent e)
                {
                    controlExecutor.execute (
                        new Runnable ()
                        {
                            public void run ()
                            {
                                if (slave != null)
                                {
                                    slave.setCheckCountLimits (
                                        checkCountLimits.getValue ());
                                }
                            }
                        });
                }
            });

        final Variable<Boolean> strictChecking =
            settings.getGeneral ().getStrictChecking ();
        strictChecking.addValueListener (
            new ValueListener ()
            {
                public void valueChanged (ValueEvent e)
                {
                    controlExecutor.execute (
                        new Runnable ()
                        {
                            public void run ()
                            {
                                if (slave != null)
                                {
                                    slave.setStrictChecking (
                                        strictChecking.getValue ());
                                }
                            }
                        });
                }
            });

        final Variable<Boolean> allowLongMessages =
            settings.getGeneral ().getAllowLongMessages ();
        allowLongMessages.addValueListener (
            new ValueListener ()
            {
                public void valueChanged (ValueEvent e)
                {
                    controlExecutor.execute (
                        new Runnable ()
                        {
                            public void run ()
                            {
                                boolean alm = allowLongMessages.getValue ();
                                if (slave != null)
                                    slave.setAllowLongMessages (alm);
                                if (masterPacketType != null)
                                    masterPacketType.setAllowLongMessages (alm);
                                if (slavePacketType != null)
                                    slavePacketType.setAllowLongMessages (alm);
                            }
                        });
                }
            });

        final Variable<Integer> retries = settings.getGeneral ().getRetries ();
        retries.addValueListener (
            new ValueListener ()
            {
                public void valueChanged (ValueEvent e)
                {
                    controlExecutor.execute (
                        new Runnable ()
                        {
                            public void run ()
                            {
                                if (client != null)
                                    client.setRetries (retries.getValue ());
                            }
                        });
                }
            });

        final Variable<Integer> replyTimeout =
            settings.getGeneral ().getReplyTimeout ();
        replyTimeout.addValueListener (
            new ValueListener ()
            {
                public void valueChanged (ValueEvent e)
                {
                    controlExecutor.execute (
                        new Runnable ()
                        {
                            public void run ()
                            {
                                if (client != null)
                                {
                                    client.setResponseTimeout (
                                        replyTimeout.getValue () * 1000000L);
                                }
                            }
                        });
                }
            });

        final Variable<Integer> responseDelay =
            settings.getGeneral ().getResponseDelay ();
        if (responseDelay != null)
        {
            responseDelay.addValueListener (
                new ValueListener ()
                {
                    public void valueChanged (ValueEvent e)
                    {
                        controlExecutor.execute (
                            new Runnable ()
                            {
                                public void run ()
                                {
                                    if (serviceFactory != null)
                                    {
                                        serviceFactory.setResponseDelay (
                                            responseDelay.getValue ());
                                    }
                                }
                            });
                    }
                });
        }
    }

    private class StartTask
        implements Runnable
    {
        public void run ()
        {
            assert !(isRunning && isStopped);
            if (!isStopped)
                return;
            try
            {
                isStopped = false;
                reportState ();
                poolExecutor = ThreadPool.createCached ("Backend-Pool",
                    exceptionHandler);
                mode.start ();
                isRunning = true;
                reportState ();
            }
            catch (IOException e)
            {
                isStopped = true;
                reportState ();
                controlExecutor.execute (new StopTask ());
                reporter.error (Exceptions.getHelpId (e),
                    Exceptions.getMessage (e));
            }
            assert isRunning || isStopped;
        }
    }

    private class StopTask
        implements Runnable
    {
        public void run ()
        {
            try
            {
                assert !(isRunning && isStopped);
                if (!isRunning)
                    return;
                isRunning = false;
                reportState ();
                mode.stop ();
                poolExecutor.shutdownNow ();
                poolExecutor.awaitTermination (10, TimeUnit.SECONDS);
            }
            catch (SecurityException e)
            {
                // May happen if called from applet.
            }
            catch (InterruptedException e)
            {
            }
            finally
            {
                poolExecutor = null;
                isStopped = true;
                reportState ();
                assert isRunning || isStopped;
            }
        }
    }

    public void start ()
    {
        controlExecutor.execute (new StartTask ());
    }

    public void stop ()
    {
        controlExecutor.execute (new StopTask ());
    }

    public void setMode (Mode m)
    {
        final BackendMode newMode = modeMap.get (m);
        controlExecutor.execute (new Runnable ()
            {
                public void run ()
                {
                    if (isStopped)
                    {
                        mode = newMode;
                        reportState ();
                    }
                }
            });
    }

    private PacketType getPacketType (InterfaceSettings interfaceSettings)
    {
        String s = interfaceSettings.getPacketType ();
        PacketType packetType;
        if (s.equals ("tcp"))
            packetType = new TcpPacketType ();
        else if (s.equals ("rtu"))
            packetType = new RtuPacketType ();
        else if (s.equals ("ascii"))
            packetType = new AsciiPacketType ();
        else
            throw new IllegalStateException ("Bad packet type: " + s);
        GeneralSettings gs = settings.getGeneral ();
        packetType.setMaxPdu (gs.getMaxPdu ().getValue ());
        packetType.setAllowLongMessages (
            gs.getAllowLongMessages ().getValue ());
        packetType.setEomTimeout (interfaceSettings.getEomTimeout ());
        return packetType;
    }

    private void setupClient ()
        throws IOException
    {
        GeneralSettings gs = settings.getGeneral ();
        masterPacketType = getPacketType (slaveInterfaceSettings);
        String interfaceTag = slaveInterfaceSettings.getInterfaceTag ();
        Connection connection;
        if (interfaceTag.equals ("serial"))
        {
            SerialSettings serialSettings =
                 slaveInterfaceSettings.getSerialSettings ();
            connection = serialSettings.createConnection (reporter);
        }
        else if (interfaceTag.equals ("tcp"))
        {
            TcpSettings tcpSettings = slaveInterfaceSettings.getTcpSettings ();
            connection = tcpSettings.createConnection (reporter);
        }
        else if (interfaceTag.equals ("udp"))
        {
            UdpSettings udpSettings = slaveInterfaceSettings.getUdpSettings ();
            connection = udpSettings.createConnection (reporter);
        }
        else
        {
            throw new IllegalStateException ("Invalid interface tag: " +
                interfaceTag);
        }
        connection.open ();
        Tracer tracer = gs.createTracer (reporter);
        // tracer.setConnection (connection);
        client = new ModbusClient (connection, masterPacketType,
            slaveInterfaceSettings.getAlwaysRespond (),
            tracer, reporter);
        client.setRetries (gs.getRetries ().getValue ());
        client.setResponseTimeout (
            gs.getReplyTimeout ().getValue () * 1000000L);
        client.setIdleTimeout (slaveInterfaceSettings.getIdleTimeout ());
    }

    private void closeClient ()
    {
        ModbusClient cl = client;
        if (cl != null)
        {
            client = null;
            cl.shutdown ();
            settings.getGeneral ().deleteTracer (cl.getTracer ());
        }
        masterPacketType = null;
    }

    private void setupSlave (boolean isMonitor)
    {
        slavePacketType = getPacketType (masterInterfaceSettings);
        slave = new ModbusSlave (settings.getRegisters (), 1, isMonitor);
        ModbusFilePacker filePacker =
            new ModbusFilePacker (settings.getFileRegisters ());
        slave.setModbusFilePacker (filePacker);
        GeneralSettings gs = settings.getGeneral ();
        slave.setSlaveId (gs.getSlaveId ().getValue ());
        slave.setMaxPdu (gs.getMaxPdu ().getValue ());
        slave.setCheckCountLimits (
            gs.getCheckCountLimits ().getValue ());
        slave.setStrictChecking (gs.getStrictChecking ().getValue ());
        slave.setAllowLongMessages (gs.getAllowLongMessages ().getValue ());
    }

    private void startSlave (ModbusTransactionHandler slave, String prefix,
            Reporter reporter, ModbusCounters counters)
        throws IOException
    {
        GeneralSettings gs = settings.getGeneral ();
        serviceFactory = new ModbusServiceFactory (slave, slavePacketType,
            masterInterfaceSettings.getAlwaysRespond (), reporter, counters,
            gs);
        serviceFactory.setTracePrefix (prefix);
        serviceFactory.setResponseDelay (gs.getResponseDelay ().getValue ());
        String interfaceTag = masterInterfaceSettings.getInterfaceTag ();
        if (interfaceTag.equals ("serial"))
        {
            SerialSettings serialSettings =
                 masterInterfaceSettings.getSerialSettings ();
            SerialConnection connection =
                serialSettings.createConnection (reporter);
            connection.open ();
            service = serviceFactory.createService (connection);
        }
        else if (interfaceTag.equals ("tcp"))
        {
            TcpSettings tcpSettings = masterInterfaceSettings.getTcpSettings ();
            serviceFactory.setIdleTimeout (
                masterInterfaceSettings.getIdleTimeout ());
            server = new SocketServer (
                tcpSettings.getLocalHost (), tcpSettings.getLocalPort (),
                serviceFactory, reporter);
        }
        else if (interfaceTag.equals ("udp"))
        {
            UdpSettings udpSettings = masterInterfaceSettings.getUdpSettings ();
            UdpConnection connection = new UdpConnection (
                udpSettings.getLocalHost (), udpSettings.getLocalPort (),
                reporter);
            service = serviceFactory.createService (connection);
        }
        else
        {
            throw new IllegalStateException ("Invalid interface tag: " +
                interfaceTag);
        }
    }

    private void closeSlave ()
    {
        slave = null;
        if (service != null)
        {
            service.terminate ();
            service = null;
        }
        if (server != null)
        {
            server.shutdown ();
            server = null;
        }
        serviceFactory = null;
        slavePacketType = null;
    }

    public ModbusClient getClient ()
    {
        return client;
    }

    private class MasterMode
        implements BackendMode
    {
        public boolean isMaster ()
        {
            return true;
        }

        public boolean isSlave ()
        {
            return false;
        }

        public void start ()
            throws IOException
        {
            setupClient ();
            client.getTracer ().setPrefix ("");

            if (settings.getGeneral ().getPoll () != null)
            {
                // Start automatic polling thread.

                poolExecutor.execute (
                    new Runnable ()
                    {
                        public void run ()
                        {
                            GeneralSettings gs = settings.getGeneral ();
                            Variable<Boolean> pollFlag = gs.getPoll ();
                            try
                            {
                                for (;;)
                                {
                                    if (Thread.interrupted ())
                                        break;
                                    if (pollFlag.getValue ())
                                    {
                                        if (!poll (frontend.getStatusBar ()))
                                            break;
                                        int passDelay =
                                            gs.getPassDelay ().getValue ();
                                        if (passDelay != 0)
                                            Thread.sleep (passDelay);
                                    }
                                    else
                                    {
                                        Thread.sleep (200);
                                    }
                                }
                            }
                            catch (IOException e)
                            {
                                reporter.error (Exceptions.getHelpId (e),
                                    Exceptions.getMessage (e));
                            }
                            catch (InterruptedException e)
                            {
                            }

                            // Stop backend running
                            Backend.this.stop ();
                        }
                    });
            }
        }

        public void stop ()
        {
            closeClient ();
        }
    }

    private class SlaveMode
        implements BackendMode
    {
        public boolean isMaster ()
        {
            return false;
        }

        public boolean isSlave ()
        {
            return true;
        }

        public void start ()
            throws IOException
        {
            setupSlave (false);
            slave.setDeviceId (settings.getDeviceId ().getDeviceId ());
            slave.setCmd17Data (settings.getDeviceId ().getCmd17Data ());
            startSlave (slave, "", reporter, slave.getCounters ());
        }

        public void stop ()
        {
            closeSlave ();
        }
    }

    private class MonitorMode
        implements BackendMode
    {
        public boolean isMaster ()
        {
            return true;
        }

        public boolean isSlave ()
        {
            return true;
        }

        public void start ()
            throws IOException
        {
            // Set up an extended slave

            setupSlave (true);

            // Set up a client to talk to the device

            setupClient ();
            Tracer tr = client.getTracer ();
            tr.setPrefix ("S");
            tr.setTransMarkerEnabled (false);

            // Set up a monitor that talks to both the client and the slave

            ModbusMonitor monitor = new ModbusMonitor (client, slave, reporter);

            // Set up and start a slave service using the monitor

            GeneralSettings generalSettings = settings.getGeneral ();
            startSlave (monitor, "M", reporter, null);
        }

        public void stop ()
        {
            closeClient ();
            closeSlave ();
        }
    }

    public interface Task
    {
        void run ()
            throws InterruptedException, IOException, ModbusException,
                ValueException;
        void done ();
        void exceptionOccurred (Exception e);
    }

    public void execute (final Task task)
    {
        if (!isRunning)
            return;
        poolExecutor.execute (new Runnable ()
        {
            public void run ()
            {
                try
                {
                    task.run ();
                    EventQueue.invokeLater (new Runnable ()
                    {
                        public void run ()
                        {
                            task.done ();
                        }
                    });
                }
                catch (InterruptedException e)
                {
                }
                catch (final Exception e)
                {
                    EventQueue.invokeLater (new Runnable ()
                    {
                        public void run ()
                        {
                            task.exceptionOccurred (e);
                        }
                    });
                }
            }
        });
    }

    public void doPoll (final StatusBar statusBar)
    {
        if (!isRunning)
            return;
        poolExecutor.execute (new Runnable ()
            {
                public void run ()
                {
                    try
                    {
                        poll (statusBar);
                    }
                    catch (IOException e)
                    {
                        reporter.error (Exceptions.getHelpId (e),
                            Exceptions.getMessage (e));
                        Backend.this.stop ();
                    }
                    catch (InterruptedException e)
                    {
                    }
                }
            });
    }

    public void read (final Register [] regs, final StatusBar statusBar)
    {
        if (regs.length == 0)
            return;
        final ModbusClient cl = getClient ();
        if (cl == null)
        {
            statusBar.showError ("I100", "Connection closed");
            return;
        }

        statusBar.clear ();
        statusBar.showMessage ("Reading registers ...");

        execute (new Task ()
        {
            ModbusMaster master;

            public void run ()
                throws InterruptedException, IOException, ModbusException,
                    ValueException
            {
                GeneralSettings gs = settings.getGeneral ();
                int slaveId = gs.getSlaveId ().getValue ();
                int maxPdu = gs.getMaxPdu ().getValue ();
                boolean checkCountLimits =
                    gs.getCheckCountLimits ().getValue ();
                boolean strictChecking = gs.getStrictChecking ().getValue ();
                boolean allowLongMessages =
                    gs.getAllowLongMessages ().getValue ();
                ModbusModel model = settings.getRegisters ();
                BigValueFlags bigValueFlags =
                    settings.getBigValue ().getValue ();
                master = new ModbusMaster (model, cl, slaveId,
                    maxPdu, checkCountLimits, strictChecking,
                    allowLongMessages, false);
                int requestDelay = gs.getRequestDelay ().getValue ();

                for (int i = 0 ; i < regs.length ; )
                {
                    if (i != 0 && requestDelay != 0)
                        Thread.sleep (requestDelay);
                    int address = regs [i].getAddress ();
                    int maxValues = getMaxValues (address, false, true);
                    int addr = address;
                    int nvalues = 0;
                    while (nvalues < maxValues &&
                        i + nvalues < regs.length)
                    {
                        Register reg = regs [i + nvalues];
                        if (addr != reg.getAddress ())
                            break;
                        addr = bigValueFlags.nextAddress (addr,
                            reg.getValueSize ());
                        ++nvalues;
                    }
                    assert nvalues > 0;
                    master.read (address, nvalues);
                    i += nvalues;
                }
            }

            public void done ()
            {
                statusBar.showMessage ("OK - response time " +
                    Metric.formatNanoTime (master.getResponseTime ()));
            }

            public void exceptionOccurred (Exception e)
            {
                showException (e, statusBar);
                if (e instanceof IOException)
                    stop ();
            }
        });
    }

    public void write (final Register [] regs, final StatusBar statusBar)
    {
        if (regs.length == 0)
            return;
        final ModbusClient cl = getClient ();
        if (cl == null)
        {
            statusBar.showError ("I100", "Connection closed");
            return;
        }

        statusBar.clear ();
        statusBar.showMessage ("Writing registers ...");

        execute (new Task ()
        {
            ModbusMaster master;

            public void run ()
                throws InterruptedException, IOException, ModbusException,
                    ValueException
            {
                GeneralSettings gs = settings.getGeneral ();
                int slaveId = gs.getSlaveId ().getValue ();
                int maxPdu = gs.getMaxPdu ().getValue ();
                boolean checkCountLimits =
                    gs.getCheckCountLimits ().getValue ();
                boolean strictChecking = gs.getStrictChecking ().getValue ();
                boolean allowLongMessages =
                    gs.getAllowLongMessages ().getValue ();
                boolean singleWrite = gs.getSingleWrite ().getValue ();
                ModbusModel model = settings.getRegisters ();
                BigValueFlags bigValueFlags =
                    settings.getBigValue ().getValue ();
                master = new ModbusMaster (model, cl, slaveId,
                    maxPdu, checkCountLimits, strictChecking,
                    allowLongMessages, singleWrite);
                int requestDelay = gs.getRequestDelay ().getValue ();

                for (int i = 0 ; i < regs.length ; )
                {
                    if (i != 0 && requestDelay != 0)
                        Thread.sleep (requestDelay);
                    int address = regs [i].getAddress ();
                    int nvalues;
                    if (singleWrite)
                    {
                        nvalues = 1;
                    }
                    else
                    {
                        int maxValues = getMaxValues (address, true, true);
                        int addr = address;
                        nvalues = 0;
                        while (nvalues < maxValues && i + nvalues < regs.length)
                        {
                            Register reg = regs [i + nvalues];
                            if (addr != reg.getAddress ())
                                break;
                            addr = bigValueFlags.nextAddress (addr,
                                reg.getValueSize ());
                            ++nvalues;
                        }
                        assert nvalues > 0;
                    }
                    master.write (address, nvalues);
                    i += nvalues;
                }
            }

            public void done ()
            {
                statusBar.showMessage ("OK - response time " +
                    Metric.formatNanoTime (master.getResponseTime ()));
            }

            public void exceptionOccurred (Exception e)
            {
                showException (e, statusBar);
                if (e instanceof IOException)
                    stop ();
            }
        });
    }

    /**
    * Gets the maximum number of values that may be transferred in a
    * Read Holding Registers response or a Write Multiple Registers request.
    * @param address address of first register to be transferred.
    * @param writing true for a write request, false for a read response.
    * @param allowMixed allow writable and non-writable registers in the
    * same group.
    * @return maximum number of values.
    */
    public int getMaxValues (int address, boolean writing, boolean allowMixed)
    {
        GeneralSettings gs = settings.getGeneral ();
        int maxPdu = gs.getMaxPdu ().getValue ();
        boolean checkCountLimits = gs.getCheckCountLimits ().getValue ();
        boolean allowLongMessages = gs.getAllowLongMessages ().getValue ();
        boolean singleWrite = gs.getSingleWrite ().getValue ();
        ModbusModel model = settings.getRegisters ();

        if (writing && singleWrite)
            return 1;
        int maxDataBytes = maxPdu - (writing ? 6 : 2);
        if (!allowLongMessages && maxDataBytes > 255)
            maxDataBytes = 255;
        if (checkCountLimits)
        {
            int officialMax = writing ? Modbus.MAX_WRITE_BYTES :
                Modbus.MAX_READ_BYTES;
            if (maxDataBytes > officialMax)
                maxDataBytes = officialMax;
        }
        return model.getGroup (address, maxDataBytes, allowMixed);
    }

    /**
    * Gets the largest group of file registers that may be transferred in a
    * Read File Records response or a Write File Record request.
    * @param filePacker file packer to be used for packing values.
    * @param fileNum file number.
    * @param address address of first register to be transferred.
    * @param writing true for a write request, false for a read response.
    * @return maximum number of values.
    */
    public int getMaxFileValues (ModbusFilePacker filePacker,
        int fileNum, int address, boolean writing)
    {
        GeneralSettings gs = settings.getGeneral ();
        int maxPdu = gs.getMaxPdu ().getValue ();
        boolean checkCountLimits = gs.getCheckCountLimits ().getValue ();
        boolean allowLongMessages = gs.getAllowLongMessages ().getValue ();

        int maxDataBytes = maxPdu - (writing ? 9 : 4);
        if (!allowLongMessages && maxDataBytes > 255)
            maxDataBytes = 255;
        if (checkCountLimits)
        {
            int officialMax = writing ? Modbus.MAX_WRITE_BYTES :
                Modbus.MAX_READ_BYTES;
            if (maxDataBytes > officialMax)
                maxDataBytes = officialMax;
        }
        return filePacker.getGroup (fileNum, address, maxDataBytes);
    }

    private boolean poll (final StatusBar statusBar)
        throws InterruptedException, IOException
    {
        ModbusClient cl = client;
        if (cl == null)
        {
            statusBar.showError ("I100", "Connection closed");
            throw new InterruptedException ();
        }
        GeneralSettings gs = settings.getGeneral ();
        int slaveId = gs.getSlaveId ().getValue ();
        int maxPdu = gs.getMaxPdu ().getValue ();
        boolean checkCountLimits = gs.getCheckCountLimits ().getValue ();
        boolean strictChecking = gs.getStrictChecking ().getValue ();
        boolean allowLongMessages = gs.getAllowLongMessages ().getValue ();
        boolean singleWrite = gs.getSingleWrite ().getValue ();
        ModbusModel model = settings.getRegisters ();
        ModbusMaster master =
            new ModbusMaster (model, cl, slaveId, maxPdu, checkCountLimits,
                strictChecking, allowLongMessages, singleWrite);
        int [] addresses = model.getAddresses ();
        int requestDelay = gs.getRequestDelay ().getValue ();
        boolean result = true;

        for (int i = 0 ; i < addresses.length ; )
        {
            if (Thread.interrupted ())
                throw new InterruptedException ();
            if (i != 0 && requestDelay != 0)
                Thread.sleep (requestDelay);
            int address = addresses [i];
            boolean writing = false;
            try
            {
                writing = model.writable (address);
            }
            catch (ModbusException e)
            {
                // Shouldn't happen - we generated the address ourselves.
                throw new AssertionError (e);
            }
            int nvalues = gs.getSingleValue ().getValue () ? 1 :
                getMaxValues (address, writing, false);
            try
            {
                if (writing)
                    master.write (address, nvalues);
                else
                    master.read (address, nvalues);
            }
            catch (ModbusException e)
            {
                String msg = writing ? "Write" : "Read";
                msg += " register " + address;
                if (nvalues > 1)
                    msg += " (" + nvalues + " values)";
                msg += "\n" + e.getMessage ();
                statusBar.showError (e.getHelpId (), msg,
                    statusBar.getClearAction ());
                reporterNSB.error (e.getHelpId (), e.getMessage ());
                if (!gs.getContinuePolling ().getValue () ||
                    (!e.isResponse () && e.getErrorCode () !=
                        Modbus.ERROR_TIMED_OUT))
                {
                    result = false;
                    break;
                }
            }

            i += nvalues;
        }

        String responseTime = Metric.formatNanoTime (master.getResponseTime ());
        if (!responseTime.equals (lastResponseTime))
        {
            statusBar.showMessage ("Response time: " + responseTime);
            lastResponseTime = responseTime;
        }

        return result;
    }

    private FileGroup [] getFileGroups (ModbusRegister [] registers,
        boolean writing)
    {
        if (registers.length == 0)
            return new FileGroup [0];
        ArrayList<FileGroup> groups = new ArrayList<FileGroup> ();
        FileRegisters fileRegisters = settings.getFileRegisters ();
        ModbusFilePacker filePacker = new ModbusFilePacker (fileRegisters);
        BigValueFlags bigValueFlags = settings.getBigValue ().getValue ();

        for (int i = 0 ; i < registers.length ; )
        {
            ModbusRegister register = registers [i];
            int fileNum = register.getFileNum ();
            int address = register.getAddress ();
            int maxValues = getMaxFileValues (filePacker, fileNum, address,
                writing);
            int nvalues = 0;
            int addr = address;
            while (nvalues < maxValues &&
                i + nvalues < registers.length)
            {
                ModbusRegister reg = registers [i + nvalues];
                if (addr != reg.getAddress ())
                    break;
                addr = bigValueFlags.nextAddress (addr,
                    reg.getValueSize ());
                ++nvalues;
            }
            assert nvalues > 0;
            groups.add (new FileGroup (fileNum, address, nvalues));
            i += nvalues;
        }

        FileGroup [] result = new FileGroup [groups.size ()];
        return groups.toArray (result);
    }

    public void readFileRecord (final Register [] registers,
        final StatusBar statusBar)
    {
        final ModbusClient cl = getClient ();
        if (cl == null)
        {
            statusBar.showError ("I100", "Connection closed");
            return;
        }

        statusBar.clear ();
        statusBar.showMessage ("Reading file registers ...");

        execute (new Task ()
        {
            ModbusMaster master;

            public void run ()
                throws InterruptedException, IOException, ModbusException,
                    ValueException
            {
                GeneralSettings gs = settings.getGeneral ();
                int slaveId = gs.getSlaveId ().getValue ();
                int maxPdu = gs.getMaxPdu ().getValue ();
                boolean checkCountLimits =
                    gs.getCheckCountLimits ().getValue ();
                boolean strictChecking = gs.getStrictChecking ().getValue ();
                boolean allowLongMessages =
                    gs.getAllowLongMessages ().getValue ();
                master = new ModbusMaster (null, cl, slaveId,
                    maxPdu, checkCountLimits, strictChecking,
                    allowLongMessages, false);
                FileRegisters fileRegisters = settings.getFileRegisters ();
                ModbusFilePacker filePacker =
                    new ModbusFilePacker (fileRegisters);
                master.setModbusFilePacker (filePacker);
                int requestDelay = gs.getRequestDelay ().getValue ();
                FileGroup [] groups = getFileGroups (registers, false);

                for (int i = 0 ; i < groups.length ; ++i)
                {
                    if (i != 0 && requestDelay != 0)
                        Thread.sleep (requestDelay);
                    master.readFileRecord (groups [i]);
                }
            }

            public void done ()
            {
                statusBar.showMessage ("OK - response time " +
                    Metric.formatNanoTime (master.getResponseTime ()));
            }

            public void exceptionOccurred (Exception e)
            {
                showException (e, statusBar);
                if (e instanceof IOException)
                    stop ();
            }
        });
    }

    public void writeFileRecord (final Register [] registers,
        final StatusBar statusBar)
    {
        final ModbusClient cl = getClient ();
        if (cl == null)
        {
            statusBar.showError ("I100", "Connection closed");
            return;
        }

        statusBar.clear ();
        statusBar.showMessage ("Writing file registers ...");

        execute (new Task ()
        {
            ModbusMaster master;

            public void run ()
                throws InterruptedException, IOException, ModbusException,
                    ValueException
            {
                GeneralSettings gs = settings.getGeneral ();
                int slaveId = gs.getSlaveId ().getValue ();
                int maxPdu = gs.getMaxPdu ().getValue ();
                boolean checkCountLimits =
                    gs.getCheckCountLimits ().getValue ();
                boolean strictChecking = gs.getStrictChecking ().getValue ();
                boolean allowLongMessages =
                    gs.getAllowLongMessages ().getValue ();
                boolean singleWrite = gs.getSingleWrite ().getValue ();
                master = new ModbusMaster (null, cl, slaveId,
                    maxPdu, checkCountLimits, strictChecking,
                    allowLongMessages, singleWrite);
                FileRegisters fileRegisters = settings.getFileRegisters ();
                ModbusFilePacker filePacker =
                    new ModbusFilePacker (fileRegisters);
                master.setModbusFilePacker (filePacker);
                int requestDelay = gs.getRequestDelay ().getValue ();
                FileGroup [] groups = getFileGroups (registers, true);

                for (int i = 0 ; i < groups.length ; ++i)
                {
                    if (i != 0 && requestDelay != 0)
                        Thread.sleep (requestDelay);
                    master.writeFileRecord (groups [i]);
                }
            }

            public void done ()
            {
                statusBar.showMessage ("OK - response time " +
                    Metric.formatNanoTime (master.getResponseTime ()));
            }

            public void exceptionOccurred (Exception e)
            {
                showException (e, statusBar);
                if (e instanceof IOException)
                    stop ();
            }
        });
    }

    private void showException (Throwable e, StatusBar statusBar)
    {
        String helpId = Exceptions.getHelpId (e);
        String msg = Exceptions.getMessage (e);
        reporterNSB.error (helpId, msg);
        statusBar.showError (helpId, msg, statusBar.getClearAction ());
    }
}


