
package uk.co.wingpath.modsnmp;

import java.awt.*;
import java.io.*;
import java.util.*;
import java.util.concurrent.*;
import org.snmp4j.*;
import org.snmp4j.log.*;
import org.snmp4j.security.*;
import uk.co.wingpath.snmp.*;
import uk.co.wingpath.util.*;
import uk.co.wingpath.event.*;
import uk.co.wingpath.modbus.*;
import uk.co.wingpath.modbusgui.*;
import uk.co.wingpath.io.*;

class Backend
{
    private final Settings settings;
    private final Reporter reporter;
    private final ModSnmp.ModSnmpProduct product;
    private final boolean debugging;
    private final boolean autoStart;
    private ConfigurationFile configFile;
    private final ExecutorService poolExecutor;
    private ArrayList<SnmpServer> snmpServers;
    private SnmpResponder responder;
    private SocketServer modbusServer;
    private ModbusServiceFactory modbusServiceFactory;
    private Variable<Boolean> snmpTraceSetting;
    private volatile BackendState backendState =
        new BackendState (false, true, true, false);
    private BackendState.Source backendListeners = new BackendState.Source ();
    private Thread pollThread;

    Backend (Settings settings, final Reporter reporter,
        ModSnmp.ModSnmpProduct product, boolean autoStart, boolean debugging,
        ConfigurationFile configFile)
    {
        if (reporter == null)
            throw new NullPointerException ("reporter must not be null");
        this.settings = settings;
        this.reporter = reporter;
        this.product = product;
        this.autoStart = autoStart;
        this.debugging = debugging;
        this.configFile = configFile;
        snmpServers = new ArrayList<SnmpServer> ();
        responder = null;
        modbusServer = null;
        pollThread = null;
        poolExecutor = new ThreadPool (5, 10,
            60L, TimeUnit.SECONDS,
            new LinkedBlockingQueue<Runnable> (10),
            "Backend-Pool",
            new Thread.UncaughtExceptionHandler ()
            {
                public void uncaughtException (Thread t, Throwable e)
                {
                    if (backendState.isRunning)
                    {
                        reporter.fatal (e, "Uncaught exception in thread '%s'",
                            t.getName ());
                        System.exit (1);
                    }
                }
            });
        SnmpThreadPool threadPool = new SnmpThreadPool (poolExecutor);
        SNMP4JSettings.setThreadFactory (threadPool);
        SNMP4JSettings.setForwardRuntimeExceptions (true);
        setupTracing ();
    }

    private void setupTracing ()
    {
        final Variable<Boolean> modbusTraceSetting =
            settings.getModbus ().getTraceSetting ();

        ArrayList<ModbusInterface> interfaces =
            settings.getModbus ().getInterfaces ();

        snmpTraceSetting = settings.getSnmp ().getTraceSetting ();
        snmpTraceSetting.addValueListener (new ValueListener () {
            public void valueChanged (ValueEvent e)
            {
                if (responder != null)
                    responder.setTraceEnabled (snmpTraceSetting.getValue ());
            }
        });
    }

    private void reportState ()
    {
        backendListeners.reportState (backendState);
    }

    private void reportState (boolean isRunning, boolean isStopped)
    {
        backendState = new BackendState (isRunning, isStopped, true, false);
        reportState ();
    }

    private void startModbusInterfaces ()
        throws IOException
    {
        reporter.trace (null, "Setting up Modbus interfaces");
        Devices devices = settings.getDevices ();
        ArrayList<ModbusInterface> interfaces =
            settings.getModbus ().getInterfaces ();
        boolean traceEnabled =
            settings.getModbus ().getTraceSetting ().getValue ();

        for (ModbusInterface intf : interfaces)
        {
            if (!devices.isInterfaceInUse (intf))
                continue;
            intf.start ();
        }
    }

    private void stopModbusInterfaces ()
    {
        ArrayList<ModbusInterface> interfaces =
            settings.getModbus ().getInterfaces ();

        for (ModbusInterface intf : interfaces)
            intf.stop ();
    }

    private void startDevices ()
        throws ValueException
    {
        Devices devices = settings.getDevices ();

        for (int i = 0 ; i < devices.getSize () ; ++i)
        {
            Device device = devices.getDevice (i);
            device.start (reporter);
        }
    }

    private void stopDevices ()
    {
        Devices devices = settings.getDevices ();

        for (int i = 0 ; i < devices.getSize () ; ++i)
        {
            Device device = devices.getDevice (i);
            device.stop ();
        }
    }

    private void startPolling ()
    {
        reporter.trace (null, "Starting polling");
        pollThread = WThread.create ("Poller", reporter,
            new Runnable ()
            {
                public void run ()
                {
                    Devices devices = settings.getDevices ();

                    while (!Thread.interrupted ())
                    {
                        try
                        {
                            int sleepTime = 5000;

                            for (int i = 0 ; i < devices.getSize () ; ++i)
                            {
                                Device device = devices.getDevice (i);
                                int t = device.poll ();
                                if (t < sleepTime)
                                    sleepTime = t;
                            }

                            if (sleepTime > 0)
                                Thread.sleep (sleepTime);
                        }
                        catch (InterruptedException e)
                        {
                            break;
                        }
                    }
                    assert reporter.debug ("Poll thread stopping");
                }
            });
        pollThread.start ();
    }

    private void stopPolling ()
    {
        if (pollThread != null)
        {
            reporter.trace (null, "Stopping polling");
            pollThread.interrupt ();
            pollThread = null;
        }
    }

    private void startModbusServer ()
        throws IOException
    {
        ModbusServerSettings serverSettings = settings.getModbusServer ();
        if (!serverSettings.isEnabled ())
            return;

        reporter.trace (null, "Starting Modbus server");

        // Create a Modbus slave for each device, and a ModbusMultiSlave that
        // delegates to them.
        ModbusMultiSlave multiSlave = new ModbusMultiSlave ();
        Devices devices = settings.getDevices ();
        DeviceId deviceId = new DeviceId ();
        deviceId.put (Modbus.DEVICE_VENDOR_NAME, "Wingpath Limited"); 
        deviceId.put (Modbus.DEVICE_PRODUCT_CODE, product.getName ());
        deviceId.put (Modbus.DEVICE_MAJOR_MINOR_REVISION,
            product.getVersion ());

        for (int i = 0 ; i < devices.getSize () && i < 254 ; ++i)
        {
            Device device = devices.getDevice (i);
            int slaveId = i + 1;
            ModbusTransactionHandler slave = device.createModbusSlave (slaveId);
            // slave.setCmd17Data (product.getName ());
            // slave.setDeviceId (deviceId);
            multiSlave.add (slaveId, slave);
        }

        // Create a TCP server using the multi-slave.
        TcpPacketType packetType = new TcpPacketType ();
        packetType.setEomTimeout (serverSettings.getEomTimeout ());
        modbusServiceFactory = new ModbusServiceFactory (multiSlave,
            packetType, true, reporter, null, settings.getModbus ());
        modbusServiceFactory.setIdleTimeout (serverSettings.getIdleTimeout ());
        modbusServiceFactory.setTracePrefix ("M");
        Variable<Boolean> modbusTraceSetting =
            settings.getModbus ().getTraceSetting ();
        modbusServer = new SocketServer (
            serverSettings.getHost (), serverSettings.getPort (),
            modbusServiceFactory, reporter);
        poolExecutor.execute (modbusServer);
        reporter.trace (null, "Started Modbus server");
    }

    private void stopModbusServer ()
    {
        if (modbusServer != null)
        {
            reporter.trace (null, "Stopping Modbus server");
            modbusServer.shutdown ();
            modbusServer = null;
        }
    }

    private void startSnmpServer ()
        throws IOException
    {
        if (settings.getSnmp ().getSize () == 0)
            return;
        reporter.trace (null, "Starting SNMP servers");
        SnmpSettings ss = settings.getSnmp ();
        ss.incEngineBoots ();
        USM usm = ss.getUsm ();
        SnmpSystemModel sysModel = new SnmpSystemModel (product, usm);
        sysModel.setSysObjectID (settings.getOidPrefix ());
        SnmpModel model = new ModbusSnmpModel (settings, reporter);
        responder = new SnmpResponder (usm, reporter, ss.getAccessManager (),
            model,
            new ConstantOidModel (settings.getConstantOids ()),
            sysModel);
        responder.setTraceEnabled (snmpTraceSetting.getValue ());

        ArrayList<SnmpInterface> interfaces = ss.getInterfaces ();
        for (SnmpInterface iface : interfaces)
        {
            SnmpServer snmpServer = new SnmpServer (
                iface.getType ().equals ("tcp"),
                iface.getHost (), iface.getPort (), responder, usm);
            snmpServer.listen ();
            snmpServers.add (snmpServer);
        }

        reporter.trace (null, "Started SNMP servers");
    }

    private void stopSnmpServers ()
    {
        if (responder == null)
            return;
        reporter.trace (null, "Stopping SNMP servers");

        for (SnmpServer snmpServer : snmpServers)
        {
            reporter.debug ("Closing SNMP server " + snmpServer.getName ());
            snmpServer.close ();
        }
        snmpServers.clear ();
        responder.shutdown ();
        responder = null;
    }

    synchronized void start ()
    {
        if (EventQueue.isDispatchThread ())
        {
            poolExecutor.execute (
                new Runnable ()
                {
                    public void run ()
                    {
                        start ();
                    }
                });
            return;
        }
        if (!backendState.isStopped)
        {
            reportState ();
            return;
        }
        if (!product.lock ())
        {
            reporter.error (null,
                "Another ModSnmp instance is already running");
            reportState ();
            return;
        }
        int variableLimit = product.getVariableLimit ();
        if (variableLimit != 0)
        {
            int variableCount = settings.getDevices ().getVariableCount ();
            if (variableCount > variableLimit)
            {
                reporter.error (null,
                    "Too many variables defined: %d. Maximum is %d." +
                    variableCount, variableLimit);
                reportState ();
                product.unlock ();
                return;
            }
        }
        if (settings.getSnmp ().getSize () == 0 &&
            !settings.getModbusServer ().isEnabled ())
        {
            reporter.error (null, "No servers configured");
            reportState ();
            product.unlock ();
            return;
        }
        reportState (false, false);
        reporter.info (null, "Starting");
        try
        {
            startModbusInterfaces ();
            startDevices ();
            startSnmpServer ();
            startModbusServer ();
            startPolling ();
            reportState (true, false);
        }
        catch (ValueException e)
        {
            reporter.error (e.getHelpId (), Exceptions.getMessage (e));
            stop ();
            if (autoStart)
            {
                reporter.fatal ("Can't run without valid settings");
                System.exit (6);
            }
            return;
        }
        catch (IOException e)
        {
            String helpId = e instanceof HIOException ?
                ((HIOException) e).getHelpId () : null;
            reporter.error (helpId, Exceptions.getMessage (e));
            stop ();
            if (autoStart)
            {
                reporter.fatal ("Can't run without valid settings");
                System.exit (6);
            }
            return;
        }

        if (autoStart)
        {
            // Save incremented SNMP engineBoots
            reporter.info (null, "Saving settings");
            configFile.save ();
        }
    }

    void stop ()
    {
        // This method may be called from a shutdown hook, so we may not have
        // set up the thread pool or started the backend yet.
        if (poolExecutor == null || !backendState.isRunning)
        {
            product.unlock ();
            reportState (false, true);
            return;
        }
        if (EventQueue.isDispatchThread ())
        {
            poolExecutor.execute (
                new Runnable ()
                {
                    public void run ()
                    {
                        stop ();
                    }
                });
            return;
        }
        synchronized (this)
        {
            if (!backendState.isRunning)
            {
                product.unlock ();
                reportState (false, true);
                return;
            }
            reporter.info (null, "Stopping");
            reportState (false, false);
            stopPolling ();
            stopModbusServer ();
            stopSnmpServers ();
            reporter.trace (null, "Closing Modbus connections");
            stopDevices ();
            stopModbusInterfaces ();
            reporter.trace (null, "Closed Modbus connections");
            product.unlock ();
            reportState (false, true);
        }
    }

    public BackendState getState ()
    {
        return backendState;
    }

    public void addStateListener (BackendState.Listener listener)
    {
        backendListeners.addListener (listener);
        listener.stateChanged (backendState);
    }

    public void removeStateListener (BackendState.Listener listener)
    {
        backendListeners.removeListener (listener);
    }
}

