
package org.jgroups.stack;

import org.jgroups.Address;
import org.jgroups.Constructable;
import org.jgroups.Global;
import org.jgroups.PhysicalAddress;

import java.io.*;
import java.net.*;
import java.util.function.Supplier;


/**
 * Network-dependent address (Internet). Generated by the bottommost layer of the protocol
 * stack (UDP). Contains an InetAddress and port.
 * @author Bela Ban
 */
public class IpAddress implements PhysicalAddress, Constructable<IpAddress> {
    protected InetAddress  ip_addr;
    protected int          port;


    // Used only by marshalling
    public IpAddress() {
    }

    /** e.g. 192.168.1.5:7800 */
    public IpAddress(String addr_port) throws Exception {
        int index=addr_port.lastIndexOf(':');
        if(index == -1)
            ip_addr=InetAddress.getByName(addr_port);
        else {
            ip_addr=InetAddress.getByName(addr_port.substring(0, index));
            port=Integer.valueOf(addr_port.substring(index+1));
        }
    }

    public IpAddress(String i, int p) throws UnknownHostException {
        port=p;
        ip_addr=InetAddress.getByName(i);
    }



    public IpAddress(InetAddress i, int p) {
        ip_addr=i; port=p;
        if(this.ip_addr == null)
            setAddressToLocalHost();
    }

    public Supplier<? extends IpAddress> create() {
        return IpAddress::new;
    }

    protected void setAddressToLocalHost() {
        try {
            ip_addr=InetAddress.getLocalHost();  // get first NIC found (on multi-homed systems)
        }
        catch(Exception e) {
            ip_addr=null;
        }
        if(ip_addr == null) {
            try {
                ip_addr=InetAddress.getByName(null);
            }
            catch(UnknownHostException e) {
            }
        }
    }

    public IpAddress(int port) {
        this(port, true);
    }

    public IpAddress(int port, boolean set_default_host) {
        this.port=port;
        if(set_default_host)
            setAddressToLocalHost();
    }


    public IpAddress(InetSocketAddress sock_addr) {
        port=sock_addr.getPort();
        ip_addr=sock_addr.getAddress();
    }



    public InetAddress  getIpAddress() {return ip_addr;}
    public  int         getPort()      {return port;}


    /**
     * implements the java.lang.Comparable interface
     * @see java.lang.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 java.lang.ClassCastException - if the specified object's type prevents it
     *            from being compared to this Object.
     */
    public int compareTo(Address o) {
        int h1, h2, rc; // added Nov 7 2005, makes sense with canonical addresses

        if(this == o) return 0;
        if(!(o instanceof IpAddress))
            throw new ClassCastException("comparison between different classes: the other object is " +
                    (o != null? o.getClass() : o));
        IpAddress other = (IpAddress) o;
        if(ip_addr == null)
            if (other.ip_addr == null) return port < other.port ? -1 : (port > other.port ? 1 : 0);
            else return -1;

        h1=ip_addr.hashCode();
        h2=other.ip_addr.hashCode();
        rc=h1 < h2? -1 : h1 > h2? 1 : 0;
        return rc != 0 ? rc : port < other.port ? -1 : (port > other.port ? 1 : 0);
    }


    public boolean equals(Object obj) {
        if(this == obj) return true; // added Nov 7 2005, makes sense with canonical addresses

        if(!(obj instanceof IpAddress))
            return false;
        IpAddress other=(IpAddress)obj;
        boolean sameIP;
        if(this.ip_addr != null)
            sameIP=this.ip_addr.equals(other.ip_addr);
        else
            sameIP=(other.ip_addr == null);
        return sameIP && (this.port == other.port);
    }



    public int hashCode() {
        return ip_addr != null ? ip_addr.hashCode() + port : port;
    }




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

    public String printIpAddress() {
        return String.format("%s:%d", ip_addr != null? ip_addr.getHostAddress() : "<null>", port);
    }


    public void writeTo(DataOutput out) throws Exception {
        if(ip_addr != null) {
            byte[] address=ip_addr.getAddress();  // 4 bytes (IPv4) or 16 bytes (IPv6)
            out.writeByte(address.length); // 1 byte
            out.write(address, 0, address.length);
            if(ip_addr instanceof Inet6Address)
                out.writeInt(((Inet6Address)ip_addr).getScopeId());
        }
        else {
            out.writeByte(0);
        }
        out.writeShort(port);
    }

    public void readFrom(DataInput in) throws Exception {
        int len=in.readByte();
        if(len > 0 && (len != Global.IPV4_SIZE && len != Global.IPV6_SIZE))
            throw new IOException("length has to be " + Global.IPV4_SIZE + " or " + Global.IPV6_SIZE + " bytes (was " +
                    len + " bytes)");
        byte[] a = new byte[len]; // 4 bytes (IPv4) or 16 bytes (IPv6)
        in.readFully(a);
        if(len == Global.IPV6_SIZE) {
            int scope_id=in.readInt();
            this.ip_addr=Inet6Address.getByAddress(null, a, scope_id);
        }
        else {
            this.ip_addr=InetAddress.getByAddress(a);
        }

        // changed from readShort(): we need the full 65535, with a short we'd only get up to 32K !
        port=in.readUnsignedShort();
    }

    public int serializedSize() {
        // length (1 bytes) + 4 bytes for port
        int tmp_size=Global.BYTE_SIZE+ Global.SHORT_SIZE;
        if(ip_addr != null) {
            // 4 bytes for IPv4, 20 for IPv6 (16 + 4 for scope-id)
            tmp_size+=(ip_addr instanceof Inet4Address)? 4 : 20;
        }
        return tmp_size;
    }

    public IpAddress copy() {
        return new IpAddress(ip_addr, port);
    }



}
