package org.limewire.collection;

import java.util.Locale;
import java.util.Arrays;



/** 
 * Allows storage & retrieval of numbers based on the index of an
 * on or off bit in a byte[] or a hexadecimal String representation
 * of that byte[].
 */
public class BitNumbers {
    
    private static final byte[] EMPTY = new byte[0];

    /** A set of masks for finding if the bit is set at the right index. */
    private static final byte[] MASKS = new byte[] {
        (byte)0x80, 0x40, 0x20, 0x10, 0x8, 0x4, 0x2, 0x1
    };
    
    /** The bits that are stored. */
    private byte[] data;
    
    /** The total size of this bitNumbers. */
    private final int size;
    
    /** A convenient shared immutable empty BitNumbers. */
    public static final BitNumbers EMPTY_BN = new ImmutableBitNumbers();
    
    /** Constructs a BitNumbers backed by the given byte[]. */
    public BitNumbers(byte[] data) {
        this.data = data;
        this.size = data.length * 8;
    }
    
    /** Constructs a BitNumbers large enough to store numbers up to size. */
    public BitNumbers(int size) {
        this.size = size;
    }
    
    /**
     * Constructs a BitNumbers based on the given hex string.
     * This accepts a nibble for the last element,
     * thus: 
     *    FF   corresponds to elements 0 through 8 being on
     *    FFF  corresponds to elements 0 through 12 being on (implies below)
     *    FFF0 corresponds to elements 0 through 12 being on also
     */
    public BitNumbers(String hexString) throws IllegalArgumentException {
        this.data = new byte[(int)Math.ceil(hexString.length() / 2d)];
        this.size  = data.length * 8;
        
        // Now fill up data, decoding the string...
        for(int i = 0; i < hexString.length(); i+=2) {
            // Will throw NFE (which extends IAE) if data is invalid
            boolean nibble = i == hexString.length() - 1;
            int j = Integer.parseInt(hexString.substring(i, nibble ? i+1 : i+2), 16);
            if(nibble) // the last element may just be a nibble
                j <<= 4;
            assert j <= 0xFF;
            data[i/2] = (byte)j;
        }
    }
    
    /** Returns true if the correct bit is set. */
    public boolean isSet(int idx) {
        if(idx >= size)
            return false;
        
        int index = (int)Math.floor(idx / 8d);
        int offset = idx % 8;
        return data != null && index < data.length && (data[index] & MASKS[offset]) != 0;
    }
    
    /** Returns the maximum number that can be stored in this BitNumbers. */
    public int getMax() {
        return size;
    }
    
    /** Sets the bit corresponding to the index. */
    public void set(int idx) {
        if(idx >= size)
            throw new IndexOutOfBoundsException("idx: " + idx + ", max: " + size);
        
        int index = (int)Math.floor(idx / 8d);
        int offset = idx % 8;
        if(data == null)
            data = new byte[(int)Math.ceil(size / 8d)];
        data[index] |= MASKS[offset];
    }
    
    /** Returns the byte array that BitNumbers is backed off of. */
    public byte[] toByteArray() {
        if(data == null) {
            return EMPTY;
        } else {
            int lastNonZero = getLastNonZeroIndex();
            if(lastNonZero == -1) { // completely empty
                return EMPTY;
            } else if(lastNonZero == data.length - 1) { // uses full width
                return data;
            } else { // must strip out the extra bytes.
                byte[] shortened = new byte[lastNonZero+1];
                System.arraycopy(data, 0, shortened, 0, lastNonZero+1);
                return shortened;
            }
        }
    }
    
    /** Returns true if no set bits exist. */
    public boolean isEmpty() {
        if(data == null)
            return true;
        
        for(int i = 0; i < data.length; i++)
            if(data[i] != 0)
                return false;
        return true;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof BitNumbers) {
            BitNumbers other = (BitNumbers)obj;
            return Arrays.equals(toByteArray(), other.toByteArray());
        }
        return false;
    }
    
    @Override
    public int hashCode() {
        return Arrays.hashCode(toByteArray());
    }
    
    /** A hexadecimal representation of the byte[] */
    public String toHexString() {
        if(isEmpty()) {
            return "";
        } else {
            StringBuilder sb = new StringBuilder(data.length * 2);
            int lastNonZero = 0;
            for(int i = 0; i < data.length; i++) {
                if(data[i] != 0)
                    lastNonZero = i;
                String hex = Integer.toHexString(data[i] & 0x00FF);
                if(hex.length() == 1)
                    sb.append("0");
                sb.append(hex.toUpperCase(Locale.US));
            }
            sb.setLength(lastNonZero * 2 + 2); // erase empty fields.
            if(sb.length() > 1 && sb.charAt(sb.length()-1) == '0')
                sb.setLength(sb.length()-1);
            return sb.toString();
        }
    }
    
    /** Returns the last non-empty index. */
    private int getLastNonZeroIndex() {
        for(int i = data.length - 1; i >= 0; i--) {
            if(data[i] != 0)
                return i;
        }
        return -1;
    }
    
    @Override
    public String toString() {
        return toHexString();
    }
    
    /** A BitNumbers that is empty and non-settable. */
    private static class ImmutableBitNumbers extends BitNumbers {
        @Override
        public void set(int idx) {
            throw new UnsupportedOperationException("immutable!");
        }

        ImmutableBitNumbers() {
            super(0);
        }
        
    }
    
    public static BitNumbers synchronizedBitNumbers(BitNumbers delegate) {
        return new SynchronizedBitNumbers(delegate);
    }

    private static class SynchronizedBitNumbers extends BitNumbers {

        private final BitNumbers delegate;
        private SynchronizedBitNumbers(BitNumbers delegate) {
            super(0);
            this.delegate = delegate;
        }

        @Override
        public synchronized boolean isEmpty() {
            return delegate.isEmpty();
        }

        @Override
        public synchronized int getMax() {
            return delegate.getMax();
        }

        @Override
        public synchronized boolean isSet(int idx) {
            return delegate.isSet(idx);
        }

        @Override
        public synchronized void set(int idx) {
            delegate.set(idx);
        }

        @Override
        public synchronized byte[] toByteArray() {
            return delegate.toByteArray();
        }

        @Override
        public synchronized String toHexString() {
            return delegate.toHexString();
        }

        @Override
        public synchronized String toString() {
            return delegate.toString();
        }

    }

}
