/**
 * 
 */
package io.pkts.buffer;

import java.io.InputStream;
import java.nio.charset.StandardCharsets;

/**
 * @author jonas@jonasborjesson.com
 */
public final class Buffers {

    private final static byte[] DigitTens = {
        '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '1', '1', '1', '1', '1', '1', '1', '1', '1', '1', '2',
        '2', '2', '2', '2', '2', '2', '2', '2', '2', '3', '3', '3', '3', '3', '3', '3', '3', '3', '3', '4', '4',
        '4', '4', '4', '4', '4', '4', '4', '4', '5', '5', '5', '5', '5', '5', '5', '5', '5', '5', '6', '6', '6',
        '6', '6', '6', '6', '6', '6', '6', '7', '7', '7', '7', '7', '7', '7', '7', '7', '7', '8', '8', '8', '8',
        '8', '8', '8', '8', '8', '8', '9', '9', '9', '9', '9', '9', '9', '9', '9', '9', };

    private final static byte[] DigitOnes = {
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0',
        '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1',
        '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2',
        '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '1', '2', '3',
        '4', '5', '6', '7', '8', '9', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', };

    /**
     * All possible chars for representing a number as a String
     */
    private final static byte[] digits = {
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
        'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' };

    private final static int[] sizeTable = {
        9, 99, 999, 9999, 99999, 999999, 9999999, 99999999, 999999999, Integer.MAX_VALUE };

    /**
     * An empty buffer.
     */
    public static Buffer EMPTY_BUFFER = new EmptyBuffer();

    /**
     * 
     */
    private Buffers() {
        // left empty intentionally
    }

    /**
     * Converts the integer value into a string and that is what is being
     * wrapped in a {@link Buffer}
     * 
     * @param value
     * @return
     */
    public static Buffer wrap(final int value) {
        final int size = value < 0 ? stringSize(-value) + 1 : stringSize(value);
        final byte[] bytes = new byte[size];
        getBytes(value, size, bytes);
        return new ByteBuffer(bytes);
    }

    public static Buffer wrap(final long value) {
        final int size = value < 0 ? stringSize(-value) + 1 : stringSize(value);
        final byte[] bytes = new byte[size];
        getBytes(value, size, bytes);
        return new ByteBuffer(bytes);
    }

    public static Buffer wrap(final String s) {
        if (s == null) {
            throw new IllegalArgumentException("String cannot be null");
        }

        if (s.isEmpty()) {
            return Buffers.EMPTY_BUFFER;
        }

        return Buffers.wrap(s.getBytes(StandardCharsets.UTF_8));
    }

    public static Buffer wrap(final InputStream is) {
        if (is == null) {
            throw new IllegalArgumentException("the input stream cannot be null or empty");
        }

        return new BoundedInputStreamBuffer(is);
    }

    /**
     * Create a new Buffer
     * 
     * @param capacity
     * @return
     */
    public static Buffer createBuffer(final int capacity) {
        final byte[] buffer = new byte[capacity];
        return new ByteBuffer(0, 0, buffer.length, 0, buffer);
    }

    /**
     * Wrap the supplied byte array
     * 
     * @param buffer
     * @return
     */
    public static Buffer wrap(final byte[] buffer) {
        if (buffer == null || buffer.length == 0) {
            throw new IllegalArgumentException("the buffer cannot be null or empty");
        }

        return new ByteBuffer(buffer);
    }

    /**
     * Same as {@link #wrap(byte[])} but we will clone the byte array first.
     * 
     * @param buffer
     * @return
     */
    public static Buffer wrapAndClone(final byte[] buffer) {
        if (buffer == null || buffer.length == 0) {
            throw new IllegalArgumentException("the buffer cannot be null or empty");
        }

        final byte[] b = new byte[buffer.length];
        System.arraycopy(buffer, 0, b, 0, buffer.length);
        return new ByteBuffer(b);
    }

    /**
     * Combine two buffers into one. The resulting buffer will share the
     * underlying byte storage so changing the value in one will affect the
     * other. However, the original two buffers will still have their own reader
     * and writer index.
     * 
     * @param one
     * @param two
     * @return
     */
    public static Buffer wrap(final Buffer one, final Buffer two) {
        // TODO: create an actual composite buffer. 
        final int size1 = one != null ? one.getReadableBytes() : 0;
        final int size2 = two != null ? two.getReadableBytes() : 0;
        if (size1 == 0 && size2 > 0) {
            return two.slice();
        } else if (size2 == 0 && size1 > 0) {
            return one.slice();
        } else if (size2 == 0 && size1 == 0) {
            return Buffers.EMPTY_BUFFER;
        }

        final Buffer composite = Buffers.createBuffer(size1 + size2);
        one.getBytes(composite);
        two.getBytes(composite);
        return composite;
    }

    /**
     * Wrap the supplied byte array specifying the allowed range of visible
     * bytes.
     * 
     * @param buffer
     * @param lowerBoundary
     *            the index of the lowest byte that is accessible to this Buffer
     *            (zero based index)
     * @param upperBoundary
     *            the upper boundary (exclusive) of the range of visible bytes.
     * @return
     */
    public static Buffer wrap(final byte[] buffer, final int lowerBoundary, final int upperBoundary) {
        if (buffer == null || buffer.length == 0) {
            throw new IllegalArgumentException("the buffer cannot be null or empty");
        }
        if (upperBoundary > buffer.length) {
            throw new IllegalArgumentException("The upper boundary cannot exceed the length of the buffer");
        }
        if (lowerBoundary >= upperBoundary) {
            throw new IllegalArgumentException("The lower boundary must be lower than the upper boundary");
        }

        if (lowerBoundary < 0) {
            throw new IllegalArgumentException("The lower boundary must be a equal or greater than zero");
        }

        final int readerIndex = 0;
        final int writerIndex = upperBoundary;
        return new ByteBuffer(readerIndex, lowerBoundary, upperBoundary, writerIndex, buffer);
    }

    public static void assertNotEmpty(final Buffer buffer) {
        if (buffer == null || buffer.isEmpty()) {
            throw new IllegalArgumentException("The Buffer is null or empty");
        }
    }

    public static void assertNotEmpty(final Buffer buffer, final String msg) {
        if (buffer == null || buffer.isEmpty()) {
            throw new IllegalArgumentException(msg);
        }
    }

    /**
     * Copied straight from the Integer class but modified to return bytes instead.
     * 
     * Places characters representing the integer i into the character array buf. The characters are
     * placed into the buffer backwards starting with the least significant digit at the specified
     * index (exclusive), and working backwards from there.
     * 
     * Will fail if i == Integer.MIN_VALUE
     */
    public static void getBytes(int i, final int index, final byte[] buf) {
        int q, r;
        int charPos = index;
        byte sign = 0;

        if (i < 0) {
            sign = '-';
            i = -i;
        }

        // Generate two digits per iteration
        while (i >= 65536) {
            q = i / 100;
            // really: r = i - (q * 100);
            r = i - ((q << 6) + (q << 5) + (q << 2));
            i = q;
            buf[--charPos] = DigitOnes[r];
            buf[--charPos] = DigitTens[r];
        }

        // Fall thru to fast mode for smaller numbers
        // assert(i <= 65536, i);
        for (;;) {
            q = i * 52429 >>> 16 + 3;
            r = i - ((q << 3) + (q << 1)); // r = i-(q*10) ...
            buf[--charPos] = digits[r];
            i = q;
            if (i == 0) {
                break;
            }
        }
        if (sign != 0) {
            buf[--charPos] = sign;
        }
    }

    /**
     * Find out how many characters it would take to represent the value as a string.
     * 
     * @param value
     * @return
     */
    public static int stringSizeOf(final int value) {
        return value < 0 ? stringSize(-value) + 1 : stringSize(value);
    }

    // Requires positive x
    public static int stringSize(final int x) {
        for (int i = 0;; i++) {
            if (x <= sizeTable[i]) {
                return i + 1;
            }
        }
    }

    /**
     * Copied straight from the Long class but modified to return bytes instead.
     * 
     * Places characters representing the integer i into the character array buf. The characters are
     * placed into the buffer backwards starting with the least significant digit at the specified
     * index (exclusive), and working backwards from there.
     *
     * Will fail if i == Long.MIN_VALUE
     */
    public static void getBytes(long i, final int index, final byte[] buf) {
        long q;
        int r;
        int charPos = index;
        char sign = 0;

        if (i < 0) {
            sign = '-';
            i = -i;
        }

        // Get 2 digits/iteration using longs until quotient fits into an int
        while (i > Integer.MAX_VALUE) {
            q = i / 100;
            // really: r = i - (q * 100);
            r = (int) (i - ((q << 6) + (q << 5) + (q << 2)));
            i = q;
            buf[--charPos] = DigitOnes[r];
            buf[--charPos] = DigitTens[r];
        }

        // Get 2 digits/iteration using ints
        int q2;
        int i2 = (int) i;
        while (i2 >= 65536) {
            q2 = i2 / 100;
            // really: r = i2 - (q * 100);
            r = i2 - ((q2 << 6) + (q2 << 5) + (q2 << 2));
            i2 = q2;
            buf[--charPos] = DigitOnes[r];
            buf[--charPos] = DigitTens[r];
        }

        // Fall thru to fast mode for smaller numbers
        // assert(i2 <= 65536, i2);
        for (;;) {
            q2 = i2 * 52429 >>> 16 + 3;
            r = i2 - ((q2 << 3) + (q2 << 1)); // r = i2-(q2*10) ...
            buf[--charPos] = digits[r];
            i2 = q2;
            if (i2 == 0) {
                break;
            }
        }
        if (sign != 0) {
            buf[--charPos] = (byte) sign;
        }
    }

    /**
     * Find out how many characters it would take to represent the value as a string.
     * 
     * @param value
     * @return
     */
    public static int stringSizeOf(final long value) {
        return value < 0 ? stringSize(-value) + 1 : stringSize(value);
    }

    // Requires positive x
    public static int stringSize(final long x) {
        long p = 10;
        for (int i = 1; i < 19; i++) {
            if (x < p) {
                return i;
            }
            p = 10 * p;
        }
        return 19;
    }


}
