
package uk.co.wingpath.modbus;

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

/**
* Provides support for Modbus "device identification" as used in the
* "Read Device Identification" command.
* <p>Quote from the Modbus specification: "The Read Device Identification
* interface is modeled as an address space composed of a set of addressable
* data elements. The data elements are called objects and an object ID
* identifies them."
* <p>This class basically provides a map from "object Ids" (integers) to
* "objects" (strings), with some associated methods.
* <p>Although the Modbus specification specifies ASCII strings, this class
* actually uses UTF-8, as this seems a bit more friendly towards non-US
* countries.
*/
public class DeviceId
    implements Cloneable
{
    private Map<Integer,String> objects;
    private Map<Integer,String> names;
    private int conformityLevel;

    private void checkId (int id)
    {
        if (id < 0 || id > 255)
            throw new IllegalArgumentException ("ID out of range: " + id);
    }

    /**
    * Constructs an empty {@code DeviceId}.
    */
    public DeviceId ()
    {
        objects = new TreeMap<Integer,String> ();
        names = new TreeMap<Integer,String> ();
        names.put (Modbus.DEVICE_VENDOR_NAME, "Vendor Name");
        names.put (Modbus.DEVICE_PRODUCT_CODE, "Product Code");
        names.put (Modbus.DEVICE_MAJOR_MINOR_REVISION, "Major/Minor Revision");
        names.put (Modbus.DEVICE_VENDOR_URL, "Vendor URL");
        names.put (Modbus.DEVICE_PRODUCT_NAME, "Product Name");
        names.put (Modbus.DEVICE_MODEL_NAME, "Model Name");
        names.put (Modbus.DEVICE_USER_APPLICATION_NAME,
            "User Application Name");
        conformityLevel = 0;
    }
    /**
    * Gets the object corresponding to the supplied object id.
    * @param id the object id.
    * @return The object, or {@code null} if the object is not defined.
    */
    public String get (int id)
    {
        checkId (id);
        return objects.get (id);
    }

    /**
    * Gets the object corresponding to the supplied object id as a UTF-8 byte
    * array.
    * @param id the object id.
    * @return The object, or {@code null} if the object is not defined.
    */
    public byte [] getBytes (int id)
    {
        checkId (id);
        String object = objects.get (id);
        if (object == null)
            return null;
        try
        {
            return object.getBytes ("UTF-8");
        }
        catch (UnsupportedEncodingException e)
        {
            // Shouldn't happen - UTF-8 should be supported.
            throw new AssertionError ("Unreachable");
        }
    }

    /**
    * Associates the supplied object with the supplied object id.
    * <p>If the supplied object is {@code null}, the object id is
    * removed from the map.
    * @param id the object id.
    * @param object the object.
    */
    public void put (int id, String object)
    {
        checkId (id);
        if (object == null)
        {
            objects.remove (id);
        }
        else
        {
            try
            {
                byte [] data = object.getBytes ("UTF-8");
                if (data.length > 255)
                {
                    throw new IllegalArgumentException (
                        "Object too long: " + object);
                }
            }
            catch (UnsupportedEncodingException e)
            {
                // Shouldn't happen - UTF-8 should be supported.
                throw new AssertionError ("Unreachable");
            }
            objects.put (id, object);
        }
    }

    /**
    * Gets the name associated with the supplied object id.
    * @param id the object id.
    * @return The associated name, or "" if no associated name.
    */
    public String getName (int id)
    {
        String name = names.get (id);
        return name == null ? "" : name;
    }

    /**
    * Associates the supplied name with the supplied object id.
    * @param id the object id.
    * @param name the name to be associated - may be {@code null}.
    */
    public void setName (int id, String name)
    {
        if (name.equals (""))
            name = null;
        if (name == null)
            names.remove (id);
        else
            names.put (id, name);
    }

    /**
    * Associates the supplied object with the supplied object id.
    * @param id the object id.
    * @param object the object, as a UTF-8 byte array.
    */
    public void putBytes (int id, byte [] object)
    {
        if (object.length > 255)
            throw new IllegalArgumentException ("Object too long: " + object);
        try
        {
            objects.put (id, new String (object, "UTF-8"));
        }
        catch (UnsupportedEncodingException e)
        {
            // Shouldn't happen - UTF-8 should be supported.
            throw new AssertionError ("Unreachable");
        }
    }

    /**
    * Gets all IDs that have associated objects defined.
    * @return The defined IDs, in ascending order.
    */
    public ArrayList<Integer> getIds ()
    {
        ArrayList<Integer> ids = new ArrayList<Integer> ();

        for (Integer id : objects.keySet ())
            ids.add (id);

        return ids;
    }


    /**
    * Gets the basic IDs that have associated objects defined.
    * @param minId only IDs greater than or equal to {@code minId} will be
    * returned.
    * @return The defined basic IDs, in ascending order.
    */
    public List<Integer> getBasicIds (int minId)
    {
        List<Integer> ids = new ArrayList<Integer> ();

        for (Integer id : objects.keySet ())
        {
            if (id >= minId && id <= 0x02)
                ids.add (id);
        }

        return ids;
    }

    /**
    * Gets the regular IDs that have associated objects defined.
    * @param minId only IDs greater than or equal to {@code minId} will be
    * returned.
    * @return The defined regular IDs, in ascending order.
    */
    public List<Integer> getRegularIds (int minId)
    {
        List<Integer> ids = new ArrayList<Integer> ();

        for (Integer id : objects.keySet ())
        {
            if (id >= minId && id >= 0x03 && id <= 0x7f)
                ids.add (id);
        }

        return ids;
    }

    /**
    * Gets the extended IDs that have associated objects defined.
    * @param minId only IDs greater than or equal to {@code minId} will be
    * returned.
    * @return The defined extended IDs, in ascending order.
    */
    public List<Integer> getExtendedIds (int minId)
    {
        List<Integer> ids = new ArrayList<Integer> ();

        for (Integer id : objects.keySet ())
        {
            if (id >= minId && id >= 0x80)
                ids.add (id);
        }

        return ids;
    }

    /**
    * Sets the conformity level.
    * @param level the conformity level.
    */
    public void setConformityLevel (int level)
    {
        conformityLevel = level;
    }

    /**
    * Gets the conformity level.
    * @return the conformity level.
    */
    public int getConformityLevel ()
    {
        return conformityLevel;
    }

    /**
    * Computes the conformity level from the defined IDs.
    * This will be 0x83 if any extended IDs are defined,
    * otherwise 0x82 if any regular IDs are defined,
    * otherwise 0x81.
    * @return the conformity level.
    */
    public int computeConformityLevel ()
    {
        conformityLevel = 0x81;

        for (Integer id : objects.keySet ())
        {
            if (id >= Modbus.DEVICE_MIN_PRIVATE)
            {
                conformityLevel = 0x83;
                break;
            }
            if (id >= Modbus.DEVICE_VENDOR_URL)
                conformityLevel = 0x82;
        }

        return conformityLevel;
    }

    @Override
    public DeviceId clone ()
    {
        DeviceId deviceId = new DeviceId ();
        deviceId.objects.putAll (objects);
        deviceId.names.putAll (names);
        return deviceId;
    }

    @Override
    public boolean equals (Object obj)
    {
        if (!(obj instanceof DeviceId))
            return false;
        DeviceId di = (DeviceId) obj;
        if (di.conformityLevel != conformityLevel)
            return false;
        return di.objects.equals (objects) &&
            di.names.equals (names);
    }

    public boolean matches (DeviceId di)
    {
        if (!di.objects.equals (objects))
            return false;
        if (conformityLevel == 0)
            return true;
        if (di.conformityLevel == 0)
            return true;
        if (di.conformityLevel != conformityLevel)
            return false;
        return true;
    }

    /**
    * Reads the device ID from a device.
    * @param client the interface to the device.
    * @param slaveId the slave ID.
    * @param code the request access code (1, 2, 3 or 4).
    * @param objectId the first object ID to be read.
    * @param strictChecking whether the response should be strictly checked.
    * @throws ModbusException if the ID cannot be read from the device.
    * @throws InterruptedException if the thread is interrupted.
    */
    public void readDeviceId (ModbusClient client, int slaveId,
            int code, int objectId, boolean strictChecking)
        throws ModbusException, InterruptedException
    {
        for (int id = objectId ;;)
        {
            MessageBuilder body = new MessageBuilder ();
            body.addByte (Modbus.MEI_READ_DEVICE_IDENTIFICATION);
            body.addByte (code);
            body.addByte (id);
            ModbusTransaction trans = client.createTransaction (slaveId,
                Modbus.FUNC_ENCAPSULATED_INTERFACE_TRANSPORT,
                body.getData ());
            client.handleTransaction (trans);
            assert trans.isFinished () : trans.getState ();
            Tracer tracer = client.getTracer ();
            if (tracer != null)
                tracer.endTransaction ();
            ModbusException exception = trans.getException ();
            if (exception != null)
                throw exception;
            ModbusMessage response = trans.getResponse ();
            int type = response.getByte (0);
            if (strictChecking &&
                type != Modbus.MEI_READ_DEVICE_IDENTIFICATION)
            {
                Modbus.dataError ("C102",
                    "Wrong 'MEI Type' in response: " +
                    type + " instead of " +
                    Modbus.MEI_READ_DEVICE_IDENTIFICATION);
            }
            int replyCode = response.getByte (1);
            if (strictChecking && replyCode != code)
            {
                Modbus.dataError ("C103",
                    "Wrong 'Read Device ID Code' in response: " +
                    replyCode + " instead of " + code);
            }
            int conformity = response.getByte (2);
            if (strictChecking &&
                conformity != 0x01 &&
                conformity != 0x02 &&
                conformity != 0x03 &&
                conformity != 0x81 &&
                conformity != 0x82 &&
                conformity != 0x83)
            {
                Modbus.dataError ("C104",
                    "Invalid 'Conformity Level' in response: " +
                    String.format ("%02x", conformity));
            }
            setConformityLevel (conformity);
            int moreFollows = response.getByte (3);
            if (strictChecking &&
                (moreFollows != 0 && moreFollows != 0xff ||
                code == 4 && moreFollows != 0))
            {
                Modbus.dataError ("C105",
                    "Invalid 'More Follows' in response: " +
                    String.format ("%02x", moreFollows));
            }
            int nextId = response.getByte (4);
            if (strictChecking && moreFollows == 0 && nextId != 0)
            {
                Modbus.dataError ("C106",
                    "Invalid 'Next Object ID' in response: " +
                    String.format ("%02x", nextId) +
                    " instead of 0");
            }
            int numObjects = response.getByte (5);
            if (code == 4 && numObjects != 1)
            {
                Modbus.dataError ("C107",
                    "Invalid 'Number of Objects' in response: " +
                    numObjects + " instead of 1");
            }

            int offset = 6;

            for (int i = 0 ; i < numObjects ; i++)
            {
                response.checkMinSize (offset + 2);
                id = response.getByte (offset);
                int len = response.getByte (offset + 1);
                response.checkMinSize (offset + 2 + len);
                byte [] data = response.getData (offset + 2, len);
                putBytes (id, data);
                offset += len + 2;
            }

            if (strictChecking)
                response.checkSize (offset);

            if (code == 0x04 || moreFollows == 0)
                break;
            id = nextId;
        }
    }
}


