/*
 * Copyright [2020] [MaxKey of copyright http://www.maxkey.top]
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 

package org.maxkey.util;
import java.io.Serializable;
import java.net.NetworkInterface;
import java.security.SecureRandom;
import java.util.Enumeration;
import java.util.Random;

public class EthernetAddress   
	implements Serializable, Cloneable, Comparable<EthernetAddress>
{
    private static final long serialVersionUID = 1L;

    private final static char[] HEX_CHARS = "0123456789abcdefABCDEF".toCharArray();

    /**
     * We may need a random number generator, for creating dummy ethernet
     * address if no real interface is found.
     */
    protected static Random _rnd;
    
    /**
     * 48-bit MAC address, stored in 6 lowest-significant bytes (in
     * big endian notation)
     */
    protected final long _address;

    /**
     * Lazily-constructed String serialization
     */
    private volatile String _asString;
    
    /*
    /**********************************************************************
    /* Instance construction
    /**********************************************************************
     */

    /**
     * String constructor; given a 'standard' ethernet MAC address string
     * (like '00:C0:F0:3D:5B:7C'), constructs an EthernetAddress instance.
     *
     * Note that string is case-insensitive, and also that leading zeroes
     * may be omitted. Thus '00:C0:F0:3D:5B:7C' and '0:c0:f0:3d:5b:7c' are
     * equivalent, and a 'null' address could be passed as ':::::' as well
     * as '00:00:00:00:00:00' (or any other intermediate combination).
     *
     * @param addrStr String representation of the ethernet address
     */
    public EthernetAddress(String addrStr)
        throws NumberFormatException
    {
        int len = addrStr.length();
        long addr = 0L;
        
        /* Ugh. Although the most logical format would be the 17-char one
         * (12 hex digits separated by colons), apparently leading zeroes
         * can be omitted. Thus... Can't just check string length. :-/
         */
        for (int i = 0, j = 0; j < 6; ++j) {
            if (i >= len) {
                // Is valid if this would have been the last byte:
                if (j == 5) {
                    addr <<= 8;
                    break;
                }
                throw new NumberFormatException("Incomplete ethernet address (missing one or more digits");
            }
            
            char c = addrStr.charAt(i);
            ++i;
            int value;
            
            // The whole number may be omitted (if it was zero):
            if (c == ':') {
                value = 0;
            } else {
                // No, got at least one digit?
                if (c >= '0' && c <= '9') {
                    value = (c - '0');
                } else if (c >= 'a' && c <= 'f') {
                    value = (c - 'a' + 10);
                } else if (c >= 'A' && c <= 'F') {
                    value = (c - 'A' + 10);
                } else {
                    throw new NumberFormatException("Non-hex character '"+c+"'");
                }
                
                // How about another one?
                if (i < len) {
                    c = addrStr.charAt(i);
                    ++i;
                    if (c != ':') {
                        value = (value << 4);
                        if (c >= '0' && c <= '9') {
                            value |= (c - '0');
                        } else if (c >= 'a' && c <= 'f') {
                            value |= (c - 'a' + 10);
                        } else if (c >= 'A' && c <= 'F') {
                            value |= (c - 'A' + 10);
                        } else {
                            throw new NumberFormatException("Non-hex character '"+c+"'");
                        }
                    }
                }
            }

            addr = (addr << 8) | value;
            
            if (c != ':') {
                if (i < len) {
                    if (addrStr.charAt(i) != ':') {
                        throw new NumberFormatException("Expected ':', got ('"+ addrStr.charAt(i)+"')");
                    }
                    ++i;
                } else if (j < 5) {
                    throw new NumberFormatException("Incomplete ethernet address (missing one or more digits");
                }
            }
        }
        _address = addr;
    }
    
    /**
     * Binary constructor that constructs an instance given the 6 byte
     * (48-bit) address. Useful if an address is saved in binary format
     * (for saving space for example).
     */
    public EthernetAddress(byte [] addr)
        throws NumberFormatException
    {
        if (addr.length != 6) {
            throw new NumberFormatException("Ethernet address has to consist of 6 bytes");
        }
        long l = addr[0] & 0xFF;
        for (int i = 1; i < 6; ++i) {
            l = (l << 8) | (addr[i] & 0xFF); 
        }
        _address = l;
    }
    
    /**
     * Another binary constructor; constructs an instance from the given
     * long argument; the lowest 6 bytes contain the address.
     *
     * @param addr long that contains the MAC address in 6 least significant
     *    bytes.
     */
    public EthernetAddress(long addr) {
        _address = addr;
    }
    
    /**
     * Default cloning behaviour (bitwise copy) is just fine...
     */
    public Object clone()
    {
        return new EthernetAddress(_address);
    }

    /**
     * Constructs a new EthernetAddress given the byte array that contains
     * binary representation of the address.
     *
     * Note that calling this method returns the same result as would
     * using the matching constructor.
     *
     * @param addr Binary representation of the ethernet address
     *
     * @throws NumberFormatException if addr is invalid (less or more than
     *    6 bytes in array)
     */
    public static EthernetAddress valueOf(byte[] addr)
        throws NumberFormatException
    {
        return new EthernetAddress(addr);
    }

    /**
     * Constructs a new EthernetAddress given the byte array that contains
     * binary representation of the address.
     *
     * Note that calling this method returns the same result as would
     * using the matching constructor.
     *
     * @param addr Binary representation of the ethernet address
     *
     * @throws NumberFormatException if addr is invalid (less or more than
     *    6 ints in array)
     */
    public static EthernetAddress valueOf(int[] addr)
        throws NumberFormatException
    {
        byte[] bAddr = new byte[addr.length];

        for (int i = 0; i < addr.length; ++i) {
            bAddr[i] = (byte) addr[i];
        }
        return new EthernetAddress(bAddr);
    }

    /**
     * Constructs a new EthernetAddress given a string representation of
     * the ethernet address.
     *
     * Note that calling this method returns the same result as would
     * using the matching constructor.
     *
     * @param addrStr String representation of the ethernet address
     *
     * @throws NumberFormatException if addr representation is invalid
     */
    public static EthernetAddress valueOf(String addrStr)
        throws NumberFormatException
    {
        return new EthernetAddress(addrStr);
    }

    /**
     * Constructs a new EthernetAddress given the long int value (64-bit)
     * representation of the ethernet address (of which 48 LSB contain
     * the definition)
     *
     * Note that calling this method returns the same result as would
     * using the matching constructor.
     *
     * @param addr Long int representation of the ethernet address
     */
    public static EthernetAddress valueOf(long addr)
    {
        return new EthernetAddress(addr);
    }

    /**
     * Factory method that locates a network interface that has
     * a suitable mac address (ethernet cards, and things that
     * emulate one), and return that address. If there are multiple
     * applicable interfaces, one of them is returned; which one
     * is returned is not specified.
     * Method is meant for accessing an address needed to construct
     * generator for time+location based UUID generation method.
     * 
     * @return Ethernet address of one of interfaces system has;
     *    not including local or loopback addresses; if one exists,
     *    null if no such interfaces are found.
     */
    public static EthernetAddress fromInterface()
    {
        try {
            Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces();
            while (en.hasMoreElements()) {
                NetworkInterface nint = en.nextElement();
                if (!nint.isLoopback()) {
                    byte[] data = nint.getHardwareAddress();
                    if (data != null && data.length == 6) {
                        return new EthernetAddress(data);
                    }
                }
            }
        } catch (java.net.SocketException e) {
            // fine, let's take is as signal of not having any interfaces
        }
        return null;
    }
    
    /**
     * Factory method that can be used to construct a random multicast
     * address; to be used in cases where there is no "real" ethernet
     * address to use. Address to generate should be a multicase address
     * to avoid accidental collision with real manufacturer-assigned
     * MAC addresses.
     *<p>
     * Internally a {@link SecureRandom} instance is used for generating
     * random number to base address on.
     */
    public static EthernetAddress constructMulticastAddress()
    {
        return constructMulticastAddress(_randomNumberGenerator());
    }

    /**
     * Factory method that can be used to construct a random multicast
     * address; to be used in cases where there is no "real" ethernet
     * address to use. Address to generate should be a multicase address
     * to avoid accidental collision with real manufacturer-assigned
     * MAC addresses.
     *<p>
     * Address is created using specified random number generator.
     */
    public static EthernetAddress constructMulticastAddress(Random rnd)
    {
        byte[] dummy = new byte[6];
        synchronized (rnd) {
            rnd.nextBytes(dummy);
        }
        /* Need to set the broadcast bit to indicate it's not a real
         * address.
         */
        /* 20-May-2010, tatu: Actually, we could use both second least-sig-bit
         *   ("locally administered") or the LSB (multicast), as neither is
         *   ever set for 'real' addresses.
         *   Since UUID specs recommends latter, use that.
         */
        dummy[0] |= (byte) 0x01;
        return new EthernetAddress(dummy);
    }
    
    /*
    /**********************************************************************
    /* Conversions to raw types
    /**********************************************************************
     */

    /**
     * Returns 6 byte byte array that contains the binary representation
     * of this ethernet address; byte 0 is the most significant byte
     * (and so forth)
     *
     * @return 6 byte byte array that contains the binary representation
     */
    public byte[] asByteArray()
    {
        byte[] result = new byte[6];
        toByteArray(result);
        return result;
    }

    /**
     * Synonym to 'asByteArray()'
     *
     * @return 6 byte byte array that contains the binary representation
     */
    public byte[] toByteArray() { return asByteArray(); }

    public void toByteArray(byte[] array) {
        if (array.length < 6) {
            throw new IllegalArgumentException("Too small array, need to have space for 6 bytes");
        }
        toByteArray(array, 0);
    }

    public void toByteArray(byte[] array, int pos)
    {
        if (pos < 0 || (pos + 6) > array.length) {
            throw new IllegalArgumentException("Illegal offset ("+pos+"), need room for 6 bytes");
        }
        int i = (int) (_address >> 32);
        array[pos++] = (byte) (i >> 8);
        array[pos++] = (byte) i;
        i = (int) _address;
        array[pos++] = (byte) (i >> 24);
        array[pos++] = (byte) (i >> 16);
        array[pos++] = (byte) (i >> 8);
        array[pos] = (byte) i;
    }

    public long toLong() {
        return _address;
    }

    /*
    /**********************************************************************
    /* Accessors
    /**********************************************************************
     */

    /**
     * Method that can be used to check if this address refers
     * to a multicast address.
     * Such addresses are never assigned to individual network
     * cards.
     */
    public boolean isMulticastAddress() {
        return (((int) (_address >> 40)) & 0x01) != 0;
    }

    /**
     * Method that can be used to check if this address refers
     * to a "locally administered address"
     * (see [http://en.wikipedia.org/wiki/MAC_address] for details).
     * Such addresses are not assigned to individual network
     * cards.
     */
    public boolean isLocallyAdministeredAddress() {
        return (((int) (_address >> 40)) & 0x02) != 0;
    }
    
    /*
    /**********************************************************************
    /* Standard methods
    /**********************************************************************
     */

    @Override
    public boolean equals(Object o)
    {
        if (o == this) return true;
        if (o == null) return false;
        if (o.getClass() != getClass()) return false;
        return ((EthernetAddress) o)._address == _address;
    }

    /**
     * Method that compares this EthernetAddress to one passed in as
     * argument. Comparison is done simply by comparing individual
     * address bytes in the order.
     *
     * @return negative number if this EthernetAddress should be sorted before the
     *   parameter address if they are equal, os positive non-zero number if this address
     *  should be sorted after parameter
     */
    public int compareTo(EthernetAddress other)
    {
        long l = _address - other._address;
        if (l < 0L) return -1;
        return (l == 0L) ? 0 : 1;
    }
    
    /**
     * Returns the canonical string representation of this ethernet address.
     * Canonical means that all characters are lower-case and string length
     * is always 17 characters (ie. leading zeroes are not omitted).
     *
     * @return Canonical string representation of this ethernet address.
     */
    @Override
    public String toString()
    {
        String str = _asString;
        if (str != null) {
            return str;
        }
        
        /* Let's not cache the output here (unlike with UUID), assuming
         * this won't be called as often:
         */
        StringBuilder b = new StringBuilder(17);
        int i1 = (int) (_address >> 32);
        int i2 = (int) _address;

        _appendHex(b, i1 >> 8);
        b.append(':');
        _appendHex(b, i1);
        b.append(':');
        _appendHex(b, i2 >> 24);
        b.append(':');
        _appendHex(b, i2 >> 16);
        b.append(':');
        _appendHex(b, i2 >> 8);
        b.append(':');
        _appendHex(b, i2);
        _asString = str = b.toString();
        return str;
    }

    /*
    /**********************************************************************
    /* Internal methods
    /**********************************************************************
     */
    
    /**
     * Helper method for accessing configured random number generator
     */
    protected synchronized static Random _randomNumberGenerator()
    {
        if (_rnd == null) {
            _rnd = new SecureRandom();
        }
        return _rnd;
    }

    private final void _appendHex(StringBuilder sb, int hex)
    {
        sb.append(HEX_CHARS[(hex >> 4) & 0xF]);
        sb.append(HEX_CHARS[(hex & 0x0f)]);
    }

}
