
package uk.co.wingpath.modsnmp;

import java.util.*;
import java.io.*;
import java.net.*;
import org.snmp4j.*;
import org.snmp4j.smi.*;
import org.snmp4j.smi.Variable;
import org.snmp4j.mp.*;
import uk.co.wingpath.modbus.*;
import uk.co.wingpath.snmp.*;
import uk.co.wingpath.util.*;
import uk.co.wingpath.io.*;

class ModbusSnmpModel
    implements SnmpModel
{
    private final Settings settings;
    private final Reporter reporter;
    private final Devices devices;

    ModbusSnmpModel (Settings settings, Reporter reporter)
    {
        this.settings = settings;
        this.reporter = reporter;
        devices = settings.getDevices ();
    }

    public String getName ()
    {
        return "Modbus";
    }

    private class OidRef
    {
        Device device;
        ObjType objType;
        int element;
        int instance;
        boolean valid;

        OidRef (OID oid, String userName)
        {
            device = null;
            objType = null;
            element = 0;
            instance = 0;
            valid = false;

            OID oidPrefix = settings.getOidPrefix ();
            if (!oid.startsWith (oidPrefix))
                return;
            int offset = oidPrefix.size ();
            int size = oid.size () - offset;
            if (size < 3 || size > 5)
                return;
            device = devices.getDevice (oid.get (offset) - 1);
            if (device == null)
                return;
            element = oid.get (offset + 1);
            if (element == 0)
                return;
            assert reporter.debug ("OidRef %s: size %d, device %d, element %d",
                oid, size, oid.get (offset) - 1, element);
            if (element == 1)
            {
                // Device ID or description.
                if (size == 3)
                {
                    // Device description (legacy).
                    if (size != 3)
                        return;
                    if (oid.get (offset + 2) != 0)
                        return;
                    instance = 1;
                }
                else
                {
                    // Device ID - collection of scalars.
                    if (size != 4)
                        return;
                    if (oid.get (offset + 3) != 0)
                        return;
                    instance = oid.get (offset + 2);
                    if (instance == 0)
                        return;
                }
            }
            else
            {
                // Registers.
                assert element > 1;
                objType = device.getObjType (element - 2, userName);
                if (objType == null)
                    return;
                if (objType.getNumber () == 1)
                {
                    // Scalar - valid OID should have size 3.
                    if (size != 3)
                        return;
                    instance = oid.get (offset + 2);
                }
                else
                {
                    // Tabular - valid OID should have size 5
                    // with components .1.2 preceding the instance.
                    if (size != 5)
                        return;
                    if (oid.get (offset + 2) != 1 || oid.get (offset + 3) != 2)
                        return;
                    instance = oid.get (offset + 4);
                }
            }
            valid = true;
        }
    }

    public OID getNextOID (OID oid, String userName)
    {
        int devNum = 0;
        int element = 0;
        int instance = 0;
        OID oidPrefix = settings.getOidPrefix ();
        if (oid.compareTo (oidPrefix) <= 0)
        {
            // Return first valid OID.
        }
        else if (oid.startsWith (oidPrefix))
        {
            int offset = oidPrefix.size ();
            int size = oid.size () - offset;
            assert size >= 1;       // since oid > oidPrefix.
            devNum = oid.get (offset) - 1;
            if (devNum == -1)
            {
                // Return OID of first device ID.
                devNum = 0;
                instance = 1;
            }
            else
            {
                Device device = devices.getDevice (devNum);
                if (device == null)
                    return null;
                if (size >= 2)
                {
                    element = oid.get (offset + 1) - 1;
                    if (element == -1)
                    {
                        // Return OID of device ID.
                        element = 0;
                        instance = 1;
                    }
                    else if (element == 0)
                    {
                        // Step through device ID fields.
                        if (size >= 3)
                        {
                            instance = oid.get (offset + 2);
                            if (instance < 1)
                                instance = 1;
                            else
                                ++instance;
                        }
                        else
                        {
                            instance = 1;
                        }
                    }
                    else
                    {
                        ObjType objType =
                            device.getObjType (element - 1, userName);
                        if (objType != null)
                        {
                            if (objType.getNumber () == 1)
                            {
                                // Scalar - valid OID should have size 3 and
                                // instance 0.
                                if (size >= 3)
                                {
                                    // Move on to next element, ignoring the
                                    // instance.
                                    ++element;
                                    instance = 0;
                                }
                            }
                            else
                            {
                                // Tabular - valid OID should have size 5
                                // with components .1.2 preceding the instance.
                                int n0 = size < 3 ? 0 : oid.get (offset + 2);
                                int n1 = size < 4 ? 0 : oid.get (offset + 3);
                                int n2 = size < 5 ? 0 : oid.get (offset + 4);
                                if (n0 < 1 ||
                                    n0 == 1 && n1 < 2 ||
                                    n0 == 1 && n1 == 2 && n2 == 0)
                                {
                                    // Invalid OID that precedes the first
                                    // instance.
                                }
                                else if (n0 == 1 && n1 == 2)
                                {
                                    // Return next instance (or move to next
                                    // OID).
                                    instance = n2 + 1;
                                }
                                else
                                {
                                    assert n0 > 1 || n0 == 1 && n1 > 2;
                                    // Invalid OID that follows the last
                                    // instance.
                                    ++element;
                                }
                            }
                        }
                    }
                }
            }
        }
        else
        {
            // oid > oidPrefix and oid doesn't start with oidPrefix.
            return null;
        }

        // If the OID we have generated isn't valid, keep incrementing it
        // until we find a valid one or go off the end.
        for (;;)
        {
            Device device = devices.getDevice (devNum);
            if (device == null)
                return null;
            if (element == 0)
            {
                // Device ID.
                if (instance == 0)
                    instance = 1;
                if (instance > 1)
                {
                    DeviceId deviceId = device.getDeviceId ();
                    ArrayList<Integer> ids = deviceId.getIds ();
                    boolean found = false;
                    for (Integer id : ids)
                    {
                        if (id + 2 >= instance)
                        {
                            instance = id + 2;
                            found = true;
                            break;
                        }
                    }
                    if (!found)
                    {
                        ++element;
                        instance = 0;
                        continue;
                    }
                }

                OID nextOid = new OID (settings.getOidPrefix ());
                nextOid.append (devNum + 1);
                nextOid.append (element + 1);
                nextOid.append (instance);
                nextOid.append (0);
                return nextOid;
            }
            ObjType objType = device.getObjType (element - 1, userName);
            if (objType == null)
            {
                ++devNum;
                element = 0;
                instance = 0;
                continue;
            }
            int number = objType.getNumber ();
            if (number == 1)
            {
                if (instance != 0)
                {
                    ++element;
                    instance = 0;
                    continue;
                }
            }
            else
            {
                if (instance == 0)
                    instance = 1;
                if (instance > number)
                {
                    ++element;
                    instance = 0;
                    continue;
                }
            }

            // Return OID of Modbus object.
            OID nextOid = new OID (settings.getOidPrefix ());
            nextOid.append (devNum + 1);
            nextOid.append (element + 1);
            if (objType.getNumber () > 1)
            {
                nextOid.append (1);
                nextOid.append (2);
            }
            nextOid.append (instance);
            return nextOid;
        }
    }

    public boolean manages (OID oid, String userName)
    {
        OidRef oidRef = new OidRef (oid, userName);
        if (!oidRef.valid)
            return false;
        if (oidRef.element == 1)
        {
            // Device ID.
            assert oidRef.instance > 0 : oidRef.instance;
            if (oidRef.instance == 1)
                return true;
            DeviceId deviceId = oidRef.device.getDeviceId ();
            return (oidRef.instance - 2) <= 255 &&
                deviceId.get (oidRef.instance - 2) != null;
        }
        int offset = oidRef.objType.getAddressOffset (oidRef.instance);
        if (offset < 0)
            return false;
        return true;
    }

    public Variable getValue (OID oid, String userName,
            long requestTime, Waiter waiter)
        throws SnmpException, InterruptedException
    {
        OidRef oidRef = new OidRef (oid, userName);
        if (!oidRef.valid)
            return Null.noSuchObject;
        Device device = oidRef.device;
        if (oidRef.element == 1)
        {
            // Device ID.
            String value;
            assert oidRef.instance > 0 : oidRef.instance;
            if (oidRef.instance == 1)
            {
                value = device.getDescription ();
            }
            else
            {
                DeviceId deviceId = device.getDeviceId ();
                if (oidRef.instance - 2 > 255)
                    return Null.noSuchInstance;
                value = deviceId.get (oidRef.instance - 2);
                if (value == null)
                    return Null.noSuchInstance;
            }
            return SnmpUtil.toOctetString (value);
        }
        ObjType objType = oidRef.objType;
        int offset = objType.getAddressOffset (oidRef.instance);
        if (offset < 0)
            return Null.noSuchInstance;
        Variable value = device.getSnmpValue (objType, offset,
            requestTime, waiter);
        assert value != null;
        return value;
    }

    public boolean setValue (OID oid, String userName, Variable value,
            long requestTime, Waiter waiter)
        throws SnmpException, InterruptedException
    {
        OidRef oidRef = new OidRef (oid, userName);
        if (!oidRef.valid)
            throw new SnmpException (PDU.notWritable);
        Device device = oidRef.device;
        // The device ID is not writable.
        if (oidRef.element == 1)
            throw new SnmpException (PDU.notWritable);
        ObjType objType = oidRef.objType;
        int offset = objType.getAddressOffset (oidRef.instance);
        if (offset < 0)
            throw new SnmpException (PDU.noCreation);
        // If value is null, this is for a trap/notification.
        if (value == null)
            value = new Integer32 (objType.getTrapValue ());
        return
            device.setSnmpValue (objType, offset, value, requestTime, waiter);
    }

    public void checkWritable (OID oid, String userName, Variable value)
        throws SnmpException
    {
        // Quotations are from RFC3416 section 4.2.5.

        // If value is null, this is for a trap/notification so the value
        // will be an integer.
        if (value == null)
            value = new Integer32 (0);

        // Check whether "(2) ... there are no variables which share the same
        // OBJECT IDENTIFIER prefix as the variable binding's name".
        OidRef oidRef = new OidRef (oid, userName);
        if (!oidRef.valid)
            throw new SnmpException (PDU.notWritable);
        Device device = oidRef.device;
        // The device ID is not writable.
        // This check should be under (9), but would have to be treated
        // as a special case in all the intervening code if we did the check
        // there.
        if (oidRef.element == 1)
            throw new SnmpException (PDU.notWritable);

        // Check whether the "(3) ... value field specifies ... a type which is
        // inconsistent with that required for all variables which share the
        // same OBJECT IDENTIFIER prefix as the variable binding's name"
        ObjType objType = oidRef.objType;
        boolean ok = false;
        int type = value.getSyntax ();
        switch (objType.getSnmpType ())
        {
        case Integer32:
        case Unsigned32:
        case Gauge32:
        case TimeTicks:
            switch (type)
            {
            case SMIConstants.SYNTAX_INTEGER32:
            case SMIConstants.SYNTAX_UNSIGNED_INTEGER32:
            case SMIConstants.SYNTAX_OCTET_STRING:
                ok = true;
                break;
            case SMIConstants.SYNTAX_TIMETICKS:
                switch (objType.getModbusType ())
                {
                case Coil:
                case Discrete:
                    ok = false;
                    break;
                default:
                    ok = true;
                    break;
                }
                break;
            default:
                ok = false;
                break;
            }
            break;
        case Counter32:
        case Counter64:
            // Counters are read-only (see RFC 2578 sections 7.1.6 and 7.1.10).
            ok = false;
            break;
        case OctetString:
        case BITS:
            ok = type == SMIConstants.SYNTAX_OCTET_STRING;
            break;
        case Float:
            ok = type == SMIConstants.SYNTAX_OCTET_STRING;
            break;
        case Float32:
        case Float64:
            ok = type == SMIConstants.SYNTAX_OCTET_STRING;
            break;
        }
        if (!ok)
            throw new SnmpException (PDU.wrongType);

        // Check whether the "(4) ... value field specifies ... a length
        // which is inconsistent ..."
        int size = objType.getModbusSize ();
        switch (value.getSyntax ())
        {
        case SMIConstants.SYNTAX_OCTET_STRING:
            OctetString os = (OctetString) value;
            if (objType.getSnmpType () != SnmpType.Float)
            {
                if (os.length () != (size + 7) / 8)
                    throw new SnmpException (PDU.wrongLength);
            }
            break;
        }

        // Check whether the "(6) ... value field specifies a value which
        // could under no circumstances be assigned to the variable"
        if (objType.getSnmpType () == SnmpType.Float)
        {
            // Float value transmitted as an ASCII string - check that
            // the value is parsable and within range for the register size.
            if (value.getSyntax () != SMIConstants.SYNTAX_OCTET_STRING)
                throw new SnmpException (PDU.wrongValue);
            String str = SnmpUtil.fromOctetString ((OctetString) value);
            try
            {
                if (objType.getModbusSize () == 32)
                    Float.parseFloat (str);
                else
                    Double.parseDouble (str);
            }
            catch (NumberFormatException e)
            {
                throw new SnmpException (PDU.wrongValue);
            }
        }
        switch (value.getSyntax ())
        {
        case SMIConstants.SYNTAX_OCTET_STRING:
            // Unless it's a Float, the size must match exactly (already
            // checked).
            break;

        default:
            // Check that the value is in range for the register size.
            ok = true;
            try
            {
                long l = value.toLong ();
                switch (objType.getModbusSize ())
                {
                case 1:
                    ok = l == 0 || l == 1;
                    break;
                case 16:
                    ok = l >= Short.MIN_VALUE && l <= 0xffff;
                    break;
                case 32:
                    ok = l >= Integer.MIN_VALUE && l <= 0xffffffffL;
                    break;
                }
            }
            catch (UnsupportedOperationException e)
            {
                // May be thrown by the toLong method.
                ok = false;
            }
            if (!ok)
                throw new SnmpException (PDU.wrongValue);
            break;
        }

        // Check whether the "(7) ... name specifies a variable which does
        // not exist and could not ever be created"
        int offset = objType.getAddressOffset (oidRef.instance);
        if (offset < 0)
            throw new SnmpException (PDU.noCreation);

        // Check whether the "(9) ... name specifies a variable which exists
        // but can not be modified no matter what new value is specified"
        if (!objType.getWritable ())
            throw new SnmpException (PDU.notWritable);
        switch (objType.getModbusType ())
        {
        case Input:
        case Discrete:
            throw new SnmpException (PDU.notWritable);
        }
    }
}

