// $Id: LogicalAddress.java,v 1.9 2005/07/17 11:34:20 chrislott Exp $

package org.jgroups.stack;

import org.jgroups.Address;
import org.jgroups.util.Util;

import java.io.*;
import java.net.InetAddress;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;



/**
 * Logical address that spans the lifetime of a member. Assigned at member (JVM) startup, and
 * retained until member is shutdown. Note that the address does <em>not</em> change on
 * disconnect-connect sequences. For example, when a member is shunned and subsequently
 * readmitted to the group, the member's address (LogicalAddress) remains the same.<br/>
 * An instance of LogicalAddress is generated by the transport protocol. Currently, only
 * UDP_NIO generates LogicalAddresses.<br/>
 * Note that host, timestamp and id are supposed to make LogicalAddress as unique as possible.
 * However, there is a remote chance that 2 instances started on the same machine create their
 * address at exactly the same time, resulting in identical addresses (leading to problems).
 * In the future, I will try to make this totally unique, by for example using the PID of the current
 * process (once available though the JDK, or by locking on a common resource (e.g. /dev/random)
 * to serialize creation. However, as for now, chances are you will never experience this problem.
 * @author Bela Ban, Dec 23 2003
 */
public class LogicalAddress implements Address {
    protected static   int count=1;
    protected String   host=null;
    protected long     timestamp=0;
    protected int      id=0;
    protected boolean  multicast_addr=false;

    /** Address of the primary physical address. This is set to the sender when a message is received.
     * If this field is set, we will send unicast messages only to this address, not to all addresses listed
     * in physical_addrs; this reduces the number of msgs we have to send.<br/>
     * Note that this field is not shipped across the wire.
     */
    transient SocketAddress primary_physical_addr=null;

    /** List<SocketAddress> of physical addresses */
    protected ArrayList physical_addrs=null;

    /** To tack on some additional data */
    byte[] additional_data=null;



    // Used only by Externalization
    public LogicalAddress() {
    }


    /** Use this constructor to create an instance, not the null-constructor */
    public LogicalAddress(String host_name, List physical_addrs) {
        init(host_name, physical_addrs);
    }



    protected void init(String host_name, List physical_addrs) {
        if(host_name != null) {
            this.host=host_name;
        }
        else {
            try {
                host=InetAddress.getLocalHost().getHostName();
            }
            catch(Exception e) {
                host="localhost";
            }
        }

        timestamp=System.currentTimeMillis();

        synchronized(LogicalAddress.class) {
            id=count++;
        }

        if(physical_addrs != null) {
            this.physical_addrs=new ArrayList(physical_addrs);
        }
    }


    public String getHost() {
        return host;
    }

    public long getTimestamp() {
        return timestamp;
    }

    public long getId() {
        return id;
    }

    public SocketAddress getPrimaryPhysicalAddress() {
        return primary_physical_addr;
    }

    public void setPrimaryPhysicalAddress(SocketAddress primary_physical_addr) {
        this.primary_physical_addr=primary_physical_addr;
    }

    /**
     * Returns a <em>copy</em> of the list of physical addresses. Reason for the copy is that the list is not supposed
     * to be modified (should be immutable).
     * @return List of physical addresses (return value maybe null)
     */
    public ArrayList getPhysicalAddresses() {
        return physical_addrs != null? (ArrayList)physical_addrs.clone() : null;
    }

    /**
     * For internal use only. Don't use this method!
     * @param addr
     */
    public void addPhysicalAddress(SocketAddress addr) {
        if(addr != null) {
            if(physical_addrs == null)
                physical_addrs=new ArrayList();
            if(!physical_addrs.contains(addr))
                physical_addrs.add(addr);
        }
    }

    /**
     * For internal use only. Don't use this method !
     * @param addr
     */
    public void removePhysicalAddress(SocketAddress addr) {
        if(addr != null && physical_addrs != null)
            physical_addrs.remove(addr);
    }

    /**
     * For internal use only. Don't use this method !
     */
    public void removeAllPhysicalAddresses() {
        if(physical_addrs != null)
            physical_addrs.clear();
    }

    public boolean isMulticastAddress() {
        return false; // LogicalAddresses can never be multicast
    }

    public int size() {
        return 22;
    }

    /**
     * Returns the additional_data.
     * @return byte[]
     */
    public byte[] getAdditionalData() {
        return additional_data;
    }

    /**
     * Sets the additional_data.
     * @param additional_data The additional_data to set
     */
    public void setAdditionalData(byte[] additional_data) {
        this.additional_data = additional_data;
    }


    /**
     * Establishes an order between 2 addresses. Assumes other contains non-null IpAddress.
     * Excludes channel_name from comparison.
     * @return 0 for equality, value less than 0 if smaller, greater than 0 if greater.
     */
    public int compare(LogicalAddress other) {
        return compareTo(other);
    }


    /**
     * implements the java.lang.Comparable interface
     * @see Comparable
     * @param o - the Object to be compared
     * @return a negative integer, zero, or a positive integer as this object is less than,
     *         equal to, or greater than the specified object.
     * @exception ClassCastException - if the specified object's type prevents it
     *            from being compared to this Object.
     */
    public int compareTo(Object o) {
        int   rc;

        if ((o == null) || !(o instanceof LogicalAddress))
            throw new ClassCastException("LogicalAddress.compareTo(): comparison between different classes");
        LogicalAddress other = (LogicalAddress) o;

        rc=this.host.compareTo(other.host);
        if(rc != 0) return rc;
        if(this.timestamp != other.timestamp)
            return this.timestamp < other.timestamp? -1 : 1;
        if(this.id != other.id)
            return this.id < other.id? -1 : 1;
        return 0;
    }



    public boolean equals(Object obj) {
        if(obj == null) return false;
        return compareTo(obj) == 0 ? true : false;
    }




    public int hashCode() {
        int retval=(int)(host.hashCode() + timestamp + id);
        return retval;
    }




    public String toString() {
        return toString(false);
    }


    public String toString(boolean print_details) {
        StringBuffer sb=new StringBuffer();

        sb.append(host);
        sb.append(':').append(id);
        if(print_details) {
            sb.append(" (created ").append(new Date(timestamp)).append(')');
            if(physical_addrs != null)
                sb.append("\nphysical addrs: ").append(physical_addrs);
        }
        if(additional_data != null)
            sb.append(" (additional data: ").append(additional_data.length).append(" bytes)");
        return sb.toString();
    }



    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(host);
        out.writeLong(timestamp);
        out.writeInt(id);

        if(physical_addrs != null) {
            out.writeInt(physical_addrs.size());
            out.writeObject(physical_addrs);
        }
        else
            out.writeInt(0);

        if(additional_data != null) {
            out.writeInt(additional_data.length);
            out.write(additional_data, 0, additional_data.length);
        }
        else
            out.writeInt(0);
    }




    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        int len;

        host=(String)in.readObject();
        timestamp=in.readLong();
        id=in.readInt();

        len=in.readInt();
        if(len > 0) {
            physical_addrs=(ArrayList)in.readObject();
        }

        len=in.readInt();
        if(len > 0) {
            additional_data=new byte[len];
            in.readFully(additional_data, 0, additional_data.length);        
        }
    }



    public void writeTo(DataOutputStream out) throws IOException {
        Util.writeString(host, out);
        out.writeLong(timestamp);
        out.writeInt(id);
        out.writeBoolean(multicast_addr);
        ObjectOutputStream oos=new ObjectOutputStream(out);
        oos.writeObject(physical_addrs);
        oos.close();
        Util.writeByteBuffer(additional_data, out);
    }

    public void readFrom(DataInputStream in) throws IOException, IllegalAccessException, InstantiationException {
        host=Util.readString(in);
        timestamp=in.readLong();
        id=in.readInt();
        multicast_addr=in.readBoolean();
        ObjectInputStream ois=new ObjectInputStream(in);
        try {
            physical_addrs=(ArrayList)ois.readObject();
        }
        catch(ClassNotFoundException e) {
        }
        additional_data=Util.readByteBuffer(in);
    }

    public Object clone() throws CloneNotSupportedException {
        LogicalAddress ret=new LogicalAddress();
        ret.host=host;
        ret.timestamp=timestamp;
        ret.id=id;
        ret.multicast_addr=multicast_addr;
        ret.additional_data=additional_data;
        ret.primary_physical_addr=primary_physical_addr;
        if(physical_addrs != null)
            ret.physical_addrs=(ArrayList)physical_addrs.clone();
        return ret;
    }

    public LogicalAddress copy() {
        try {
            return (LogicalAddress)clone();
        }
        catch(CloneNotSupportedException e) {
            return null;
        }
    }


}
