
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;
    }
}


