package aalg.algebra;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class VariableNumberIo {
	
	public static class Range {
		private final long maxNumber;   // Maximal number in the range, inclusive
		private final long base;   // Difference between written number and its value
		private final int maxFirstByte;   // Maximal first byte in the range, inclusive
		
		public Range(final long maxNumber, final long base, final int maxFirstByte) {   
			this.maxNumber = maxNumber;   
			this.base = base;
			this.maxFirstByte = maxFirstByte;
		}

		public long getMax() {
			return maxNumber;
		}
	}
	
	private static Range createRange (final int minFirstByte, final int maxFirstByte, final int byteCount, final long prevMax, final long upperBound) {
		final BigInteger minNumberNotation = BigInteger.valueOf(minFirstByte).shiftLeft(8 * byteCount);
		final BigInteger maxNumberNotation = BigInteger.valueOf(maxFirstByte).shiftLeft(8 * byteCount);
		final long maxNumber = maxNumberNotation.subtract(minNumberNotation).add(BigInteger.valueOf(prevMax)).subtract(BigInteger.ONE).min(BigInteger.valueOf(upperBound)).longValue();
		final long base = minNumberNotation.subtract(BigInteger.valueOf(prevMax)).longValue();
		
		return new Range(maxNumber, base, maxFirstByte - 1);
	}
	
	private static final Range[] initializeRanges (final int[] digits) {
		final Range[] ranges = new Range[digits.length];
		final long upperBound = (1L << (8 * digits.length - 1)) - 1;
		long prevMax = 0;
		int prevDigit = 0;
		
		for (int i = 0; i < digits.length; i++) {
			final int nextDigit = digits[i];
			ranges[i] = createRange(prevDigit, nextDigit, i, prevMax, upperBound);
			
			prevMax = ranges[i].maxNumber + 1;
			prevDigit = nextDigit;
		}
		
		return ranges;
	}

	private static final int[] INT_DIGITS = {64, 96, 128, 256};
	private static final int[] LONG_DIGITS = {64, 96, 112, 120, 124, 126, 128, 256};
	
	private static final Range[] INT_RANGES = initializeRanges (INT_DIGITS);
	private static final Range[] LONG_RANGES = initializeRanges (LONG_DIGITS);

	/**
	 * Writes non-negative int number.
	 * @param num number to write
	 * @param stream stream for data
	 * @throws IOException
	 */
	public static void writeInt(final int num, final OutputStream stream) throws IOException {
		if (num < 0)
			throw new IllegalArgumentException("Cannot write negative number");
		
		writeVariableNumber (num, stream, INT_RANGES);
	}

	/**
	 * Reads int number.
	 * @param stream stream with data
	 * @return read number
	 * @throws IOException
	 */
	public static int readInt(final InputStream stream) throws IOException {
		final int number = (int) readVariableNumber (stream, INT_RANGES);
		
		if (number < 0)
			throw new RuntimeException("Invalid int number");
		
		return number;
	}
	
	/**
	 * Writes non-negative long number.
	 * @param num number to write
	 * @param stream stream for data
	 * @throws IOException
	 */
	public static void writeLong(final long num, final OutputStream stream) throws IOException {
		if (num < 0)
			throw new IllegalArgumentException("Cannot write negative number");

		writeVariableNumber (num, stream, LONG_RANGES);
	}

	/**
	 * Reads long number.
	 * @param stream stream with data
	 * @return read number
	 * @throws IOException
	 */
	public static long readLong(final InputStream stream) throws IOException {
		final long number = readVariableNumber (stream, LONG_RANGES);
		
		if (number < 0)
			throw new RuntimeException("Invalid long number");
		
		return number;
	}
	
	/**
	 * Writes non-negative number with variable length.
	 * @param num number to write
	 * @param stream stream for data
	 * @param ranges precalculated array with value ranges
	 * @throws IOException
	 */
	private static void writeVariableNumber (final long num, final OutputStream stream, final Range[] ranges) throws IOException {
		int byteCount = 0;
		
		while (num > ranges[byteCount].maxNumber)
			byteCount++;
		
		final long numToWrite = num + ranges[byteCount].base;
		Utils.writeLong(numToWrite, stream, byteCount + 1);
	}
	
	/**
	 * Reads number with variable length.
	 * @param stream stream with data
	 * @param ranges precalculated array with value ranges
	 * @return read number
	 * @throws IOException
	 */
	private static long readVariableNumber (final InputStream stream, final Range[] ranges) throws IOException {
		final int firstDigit = Utils.readByte(stream);
		int byteCount = 0;
		
		while (firstDigit > ranges[byteCount].maxFirstByte)
			byteCount++;
		
		long num = firstDigit;
		
		for (int i = 0; i < byteCount; i++) {
			final int digit = Utils.readByte(stream);
			
			num = (num << 8) | digit;
		}
		
		return num - ranges[byteCount].base;
	}

	/**
	 * Returns integer ranges, just for testing.
	 */
	public static List<Range> getIntRanges() {
		return Collections.unmodifiableList(Arrays.asList(INT_RANGES));
	}

	/**
	 * Returns long ranges, just for testing.
	 */
	public static List<Range> getLongRanges() {
		return Collections.unmodifiableList(Arrays.asList(LONG_RANGES));
	}
}
