
package uk.co.wingpath.modbus;

import uk.co.wingpath.util.*;

/**
* This class provides mappings between "message addresses" and
* "model addresses".
* <p>The term "message address" is used to refer
* to the address that is actually sent in a message.
* The term "model address" is used to refer to the address used in register
* definitions.
* <p>Note that message addresses are restricted to the range 0..65535
* since they are transmitted in a 16-bit field in Modbus messages, whereas
* model addesses are only used internally and may be in the range 
* 0..2147483647. 
* <p>The Modbus protocol uses four different address ranges:
* discrete inputs, coils, input registers, and holding registers.
* The function code of a message determines to which address range the
* address in the message belongs.
* <p>For each address range, a "base address" and a "number of addresses"
* have to be specified in the address mapping.
* <p>The number of addresses is simply the number of message addresses in
* the range, i.e.: 
* <blockquote>
* message_address &gt;= 0 and message_address &lt; number_of_addresses
* </blockquote>
* <p>The base address is the model address that corresponds to message
* address 0. For holding registers and input registers this means that:
* <blockquote>
* model_address = base_address + message_address
* </blockquote>
* <p>This also applies to coils and discrete-inputs, provided that you
* use 1-bit registers. If you use 8-bit or larger registers for coils or
* discrete-inputs, the mapping is more complicated
* because message addresses refer to individual bits, whereas model
* addresses refer to registers. In this case, the
* size of the registers in the address range is used to determine
* the number of bits in a register, and then the following mapping is used:
* <blockquote><para>
* model_address = base_address + message_address / bits_in_register
* </blockquote>
* <p>There are two special registers, which have their own Modbus commands and
* are separately mapped. These are the "exception status" register (command
* 17 - Read Exception Status) and the "diagnostic register" (command 8/02 -
* Diagnostics - Get Diagnostic Register).
* <p>An {@code AddressMap} also contains "byte swap" and "bit reverse" flags,
* which affect the overlaying of the coil or discrete-input range onto
* the holding-register or input-register range.
*/
public class AddressMap
    implements Cloneable
{
    /**
    * This class provides address mappings for a single Modbus address range.
    * Four of these are used, with names "Holding Register", "Input Register",
    * "Coil" and "Discrete Input".
    */
    public class Map
        implements Cloneable
    {
        private final String name;
        private int base;
        private int range;
        private int multiplier;

        private Map (String name, int base, int range, int multiplier)
        {
            this.name = name;
            this.base = base;
            this.range = range;
            this.multiplier = multiplier;
        }

        @Override
        public Map clone ()
        {
            return new Map (name, base, range, multiplier);
        }

        /**
        * Gets the base address of the mapping.
        * @return Base address of address range.
        */
        public int getBase ()
        {
            return base;
        }

        /**
        * Gets the number of addresses in the address range.
        * @return Size of address range.
        */
        public int getSize ()
        {
            return range;
        }

        /**
        * Gets the address range "multiplier".
        * <p>The "multiplier" is how many message addresses correspond to
        * each model address. Normally, the multiplier is 1, but if 8-bit
        * or larger registers are used in the coil or discrete-input ranges
        * then the multiplier is the number of bits per register.
        * @return Multiplier.
        */
        int getMultiplier ()
        {
            return multiplier;
        }

        /**
        * Gets the name of the address range.
        * @return name of the address range.
        */
        String getName ()
        {
            return name;
        }

        /**
        * Sets the base address of the address range.
        * @param base the base address.
        * @throws ValueException if the base is negative.
        */
        public void setBase (int base)
        {
            if (base < 0)
                throw new IllegalArgumentException ("Base cannot be negative");
            this.base = base;
        }

        /**
        * Sets the number of addresses in the address range.
        * @param size the number of addresses.
        * @throws ValueException if the size is negative or greater than
        * 65536.
        */
        public void setSize (int size)
        {
            if (size < 0 || size > 65536)
            {
                throw new IllegalArgumentException (
                    "Number of addresses must be in range 0..65536");
            }
            range = size;
        }

        /**
        * Sets the address range "multiplier".
        * <p>The "multiplier" is how many message addresses correspond to
        * each model address. Normally, the multiplier is 1, but if 8-bit
        * or larger registers are used in the coil or discrete-input ranges
        * then the multiplier is the number of bits per register.
        * @param multiplier the multiplier.
        * @throws ValueException if the multiplier is not 1, 8, 16, 32 or 64.
        */
        void setMultiplier (int multiplier)
        {
            if (multiplier != 1 && multiplier != 8 && multiplier != 16 &&
                    multiplier != 32 && multiplier != 64)
            {
                throw new IllegalArgumentException ("Invalid multiplier");
            }
            this.multiplier = multiplier;
        }

        private void check ()
            throws ValueException
        {
            if (range != 0 && base + range - 1 < base)
            {
                throw new ValueException (
                    name + ": Base + number of addresses too big");
            }
        }

        /**
        * Converts a model address to a message address.
        * <p>No check is done on whether the address is in range - an
        * out-of-range model address will return a message address that is
        * negative or larger that the number of addresses in the range.
        * This allows the returned address to be used in out-of-range error
        * messages. The {@link #inRange} method can be used to check
        * whether a model address is in range.
        * @param ma the model address.
        * @return the corresponding message address.
        */
        int fromModel (ModelAddress ma)
        {
            int address = ma.getAddress ();
            int shift = ma.getShift ();
            address -= base;
            address *= multiplier;
            address += shift;
            return address;
        }

        /**
        * Converts a message address to a model address.
        * @param address the message address.
        * @return the corresponding model address.
        * @throws ModbusException if the message address is out of range.
        */
        ModelAddress toModel (int address)
            throws ModbusException
        {
            if (address < 0 || address >= range)
            {
//System.out.println ("toModel: address " + address + ", base " + base + ", range " + range);
                Modbus.addressError ("M001",
                    "Message address " + address +
                    " is not in the " + name + " range");
            }
            int shift = address % multiplier;
            address /= multiplier;
            address += base;
            return new ModelAddress (address, shift);
        }

        /**
        * Tests whether a model address is in range.
        * @param address the model address to be tested.
        * @return {@code true} if the model address is in this range.
        */
        boolean inRange (int address)
        {
            if (address < base)
                return false;
            address -= base;
            address *= multiplier;
            return address >= 0 && address < range;
        }

        /**
        * Checks whether a block of message addresses is in range.
        * @param address first message address.
        * @param count number of message addresses.
        * @throws ModbusException if out of range.
        */
        void checkInRange (int address, int count)
            throws ModbusException
        {
            assert address >= 0;
            assert count >= 0;
            if (address + count > range)
            {
                if (address < range)
                    address = range;
                Modbus.addressError ("M001",
                    "Message address " + address +
                    " is not in the " + name + " range");
            }
        }

        @Override
        public boolean equals (Object obj)
        {
            if (!(obj instanceof Map))
                return false;
            Map m = (Map) obj;
            return base == m.base &&
                range == m.range;
        }

        @Override
        public int hashCode ()
        {
            return base * 65536 + range;
        }

        @Override
        public String toString ()
        {
            return "[" + base + " " + range + "]";
        }
    }

    private final Map coil;
    private final Map discreteInput;
    private final Map inputRegister;
    private final Map holdingRegister;
    private boolean byteSwap;
    private boolean bitReverse;
    private int exceptionStatusRegister;
    private int diagnosticRegister;
    private int exceptionStatusRegisterSize;
    private int diagnosticRegisterSize;

    /**
    * Constructs an {@code AddressMap} with default values.
    * <p>All address ranges have a default base of 0. The default size is
    * 0 for the coil and discrete-input ranges, and 65536 for the input-register
    * and holding-register ranges.
    */
    public AddressMap ()
    {
        coil = new Map ("Coil", 0, 0, 1);
        discreteInput = new Map ("Discrete Input", 0, 0, 1);
        inputRegister = new Map ("Input Register", 0, 65536, 1);
        holdingRegister = new Map ("Holding Register", 0, 65536, 1);
        byteSwap = true;
        bitReverse = false;
        exceptionStatusRegister = -1;
        diagnosticRegister = -1;
        exceptionStatusRegisterSize = 1;
        diagnosticRegisterSize = 2;
    }

    private AddressMap (Map coil, Map discreteInput, Map inputRegister,
        Map holdingRegister, boolean byteSwap, boolean bitReverse)
    {
        this.coil = coil.clone ();
        this.discreteInput = discreteInput.clone ();
        this.inputRegister = inputRegister.clone ();
        this.holdingRegister = holdingRegister.clone ();
        this.byteSwap = byteSwap;
        this.bitReverse = bitReverse;
    }

    @Override
    public AddressMap clone ()
    {
        AddressMap am = new AddressMap (coil, discreteInput, inputRegister,
            holdingRegister, byteSwap, bitReverse);
        am.exceptionStatusRegister = exceptionStatusRegister;
        am.exceptionStatusRegisterSize = exceptionStatusRegisterSize;
        am.diagnosticRegister = diagnosticRegister;
        am.diagnosticRegisterSize = diagnosticRegisterSize;
        return am;
    }

    /**
    * Gets the coil map.
    * @return Coil map.
    */
    public Map getCoilMap ()
    {
        return coil;
    }

    /**
    * Gets the discrete input map.
    * @return Discrete input map.
    */
    public Map getDiscreteInputMap ()
    {
        return discreteInput;
    }

    /**
    * Gets the input register map.
    * @return Input register map.
    */
    public Map getInputRegisterMap ()
    {
        return inputRegister;
    }

    /**
    * Gets the holding register map.
    * @return Holding register map.
    */
    public Map getHoldingRegisterMap ()
    {
        return holdingRegister;
    }

    /**
    * Gets the byte-swap flag.
    * @return Byte-swap flag.
    */
    public boolean getByteSwap ()
    {
        return byteSwap;
    }

    /**
    * Sets the byte-swap flag.
    * @param byteSwap byte-swap flag.
    */
    public void setByteSwap (boolean byteSwap)
    {
        this.byteSwap = byteSwap;
    }

    /**
    * Gets the bit-reverse flag.
    * @return bit-reverse flag.
    */
    public boolean getBitReverse ()
    {
        return bitReverse;
    }

    /**
    * Sets the bit-reverse flag.
    * @param bitReverse bit-reverse flag.
    */
    public void setBitReverse (boolean bitReverse)
    {
        this.bitReverse = bitReverse;
    }

    /**
    * Gets the model address of the exception status register.
    * @return model address of the exception status register, or -1 if
    * not defined.
    */
    public int getExceptionStatusRegister ()
    {
        return exceptionStatusRegister;
    }

    /**
    * Sets the model address of the exception status register.
    * @param address model address of the exception status register, or -1 to
    * undefine it.
    */
    public void setExceptionStatusRegister (int address)
    {
        exceptionStatusRegister = address;
    }

    /**
    * Gets the model address of the diagnostic register.
    * @return model address of the diagnostic register, or -1 if not defined.
    */
    public int getDiagnosticRegister ()
    {
        return diagnosticRegister;
    }

    /**
    * Sets the model address of the diagnostic register.
    * @param address model address of the diagnostic register, or -1 to
    * undefine it.
    */
    public void setDiagnosticRegister (int address)
    {
        diagnosticRegister = address;
    }

    void setExceptionStatusRegisterSize (int size)
    {
        exceptionStatusRegisterSize = size;
    }

    void setDiagnosticRegisterSize (int size)
    {
        diagnosticRegisterSize = size;
    }

    int getExceptionStatusRegisterSize ()
    {
        return exceptionStatusRegisterSize;
    }

    int getDiagnosticRegisterSize ()
    {
        return diagnosticRegisterSize;
    }

    boolean isExceptionStatusRegister (int address)
    {
        if (exceptionStatusRegister < 0)
            return false;
        if (address < exceptionStatusRegister)
            return false;
        address -= exceptionStatusRegister;
        return address < (exceptionStatusRegisterSize == 0 ? 8 : 1);
    }

    boolean isDiagnosticRegister (int address)
    {
        if (diagnosticRegister < 0)
            return false;
        if (address < diagnosticRegister)
            return false;
        address -= diagnosticRegister;
        return address < (diagnosticRegisterSize == 0 ? 16 :
            diagnosticRegisterSize == 1 ? 2 : 1);
    }

    boolean isCoil (int address)
    {
        return coil.inRange (address);
    }

    boolean isDiscreteInput (int address)
    {
        return discreteInput.inRange (address);
    }

    boolean isInputRegister (int address)
    {
        return inputRegister.inRange (address);
    }

    boolean isHoldingRegister (int address)
    {
        return holdingRegister.inRange (address);
    }

    AddressMap.Map getAddressSpace (int address)
    {
        if (isHoldingRegister (address))
            return holdingRegister;
        if (isInputRegister (address))
            return inputRegister;
        if (isCoil (address))
            return coil;
        if (isDiscreteInput (address))
            return discreteInput;
        return null;
    }

    void check ()
        throws ValueException
    {
        holdingRegister.check ();
        inputRegister.check ();
        coil.check ();
        discreteInput.check ();
    }

    @Override
    public boolean equals (Object obj)
    {
        if (!(obj instanceof AddressMap))
            return false;
        AddressMap am = (AddressMap) obj;
        return holdingRegister.equals (am.holdingRegister) &&
            inputRegister.equals (am.inputRegister) &&
            coil.equals (am.coil) &&
            discreteInput.equals (am.discreteInput) &&
            byteSwap == am.byteSwap &&
            bitReverse == am.bitReverse &&
            exceptionStatusRegister == am.exceptionStatusRegister &&
            diagnosticRegister == am.diagnosticRegister;
    }

    @Override
    public int hashCode ()
    {
        return holdingRegister.hashCode () ^
            inputRegister.hashCode () ^
            coil.hashCode () ^
            discreteInput.hashCode () ^
            (byteSwap ? 1 : 0) ^
            (bitReverse ? 1 : 0);
    }

    @Override
    public String toString ()
    {
        return "[AddressMap " +
            holdingRegister.toString () + " " +
            inputRegister.toString () + " " +
            coil.toString () + " " +
            discreteInput.toString () + " " +
            byteSwap + " " +
            bitReverse + " " +
            exceptionStatusRegister + " " +
            diagnosticRegister + "]";
    }
}

