/**************************************\
 * Буферизованный ввод/вывод          *
 **                                  **
 * Буферизованный поток ввода         *
 * (C) 2005+, Vitali Filippov [VMX]   *
 * (C) 2010,  Kulikov Dmitriy         *
 *                                    *
 *            BufDataInputStream.java *
 *      Created on 24 Oct 2005, 13:25 *
\**************************************/

package com.vmx;

import java.io.*;
import com.one.RandomAccessInputStream;
import java.util.Vector;

/**
 * Класс для хитрого буферизованного ввода.
 * По мере необходимости размещает в памяти блоки данных указанного размера;
 * новые, если получится, или повторно использует существующие.
 * Таким образом значительно повышается эффективность работы
 * с медленными входными потоками, где свободный поиск затруднен.
 */

/*
 * Class for buffered data input.
 * For documentation, see java.io.DataInput.
 */
public class BufDataInputStream extends RandomAccessInputStream implements DataInput
{
	protected static class DataBlock
	{
		public byte[] data;
		public int start, end, len;

		public DataBlock(int size)
		{
			data = new byte[size];
		}

		public void read(RandomAccessInputStream is) throws IOException
		{
			start = is.getPosition();
			len = is.read(data);
			end = start + len - 1;
		}

		public boolean contains(int pos)
		{
			return pos >= start && pos <= end;
		}

		public String toString()
		{
			return "[" + start + ".." + end + "]";
		}
	}

	protected byte[] buffer;
	protected int bmax, blen, bpos;
	protected int currpos, markedpos;
	protected int capacity;
	protected RandomAccessInputStream is;

	protected Vector datablocks;
	
	/**
	 * Конструктор.
	 * Используется буфер стандартного размера (4 КБ).
	 */
	public BufDataInputStream(RandomAccessInputStream iis) throws IOException
	{
		this(iis, -1);
	}
	
	/**
	 * Конструктор.
	 * Используется буфер заданного размера.
	 */
	public BufDataInputStream(RandomAccessInputStream iis, int bufsize) throws IOException
	{
		if(bufsize <= 0)
		{
			bufsize = 4096;
		}
		
		bmax = bufsize;
		currpos = 0;
		markedpos = 0;

		is = iis;

		capacity = is.getCapacity();
		is.mark(capacity + 0x100);

		datablocks = new Vector();

		bufferize();
	}
	
	/**
	 * Установить новый базовый поток для этого буферизованного потока.
	 * При этом буфер автоматически обновляется.
	 */
	public void setInputStream(RandomAccessInputStream iis) throws IOException
	{
		is = iis;
		updateBuffer();
	}
	
	/**
	 * Закрытие буферизованного потока.
	 * Базовый поток НЕ остается открытым для дальнейшего использования.
	 */
	public void close() throws IOException
	{
		is.close();
	}
	
	/**
	 * Возвращает количество байт, которые ещё возможно прочесть из
	 * этого буферизованного потока.
	 */
	public int available()
	{
		return capacity - currpos;
	}
	
	/**
	 * Получить объём потока
	 */
	public int getCapacity()
	{
		return capacity;
	}
	
	/**
	 * Перейти к положению pos
	 */
	public void setPosition(int pos) throws IOException
	{
		if(pos < 0)
		{
			currpos = 0;
		}
		else if(pos > capacity)
		{
			currpos = capacity;
		}
		else
		{
			currpos = pos;
		}

		blen = -1;
		bpos = -1;
	}
	
	/**
	 * Возвращает текущую позицию в буферизованном потоке.
	 */
	public int getPosition()
	{
		return currpos;
	}
	
	/**
	 * Ставит метку, на которую возвращаться потом можно по reset.
	 */
	public void mark(int readlimit)
	{
		markedpos = currpos;
	}
	
	/**
     * Перейти на последнюю заданную mark'ом позицию.
     */
	public void reset() throws IOException
	{
		currpos = markedpos;
		blen = -1;
		bpos = -1;
	}
	
	/**
     * Пропустить n байт
     */
	public long skip(long n) throws IOException
	{
		if(n > available())
		{
			n = available();
		}

		bpos += n;
		currpos += n;

		return n;
	}
	
	/**
     * Прочитать массив из потока: прочитать и записать максимум len байт,
     * записать их в b[], начиная со смещения off, и вернуть количество
     * считанных байт.
     */
	public int read(byte[] b, int off, int len) throws IOException
	{
		int available = available();

		if(available <= 0)
		{
			return -1;
		}

		if(len > available)
		{
			len = available;
		}
		
		int rest = len;
		int count;
		
		while(rest > 0)
		{
			if(bpos >= blen && !bufferize())
			{
				break;
			}
			
			count = Math.min(rest, blen - bpos);
			System.arraycopy(buffer, bpos, b, off, count);

			bpos += count;
			currpos += count;
			off += count;
			rest -= count;
		}

		return len - rest;
	}
	
	/**
     * Прочитать массив b[] полностью - эквивалентно read(b, 0, b.length);
     */
	public int read(byte[] b) throws IOException
	{
		return read(b, 0, b.length);
	}
	
	/**
     * Прочитать 1 байт из потока, вернуть его, если успешно, и -1,
     * если достигнут конец потока.
     */
	public int read() throws IOException
	{
		if(currpos >= capacity)
		{
			return -1;
		}

		if(bpos >= blen)
		{
			bufferize();
		}

		currpos++;
		
		return ((int)buffer[bpos++]) & 0xFF;
	}
	
	/**
     * Прочитать 1 байт из потока назад, если начало файла - вернуть -1.
     * Для работы требуется поддержка mark() и reset()
     */
	public int readBack() throws IOException
	{
		if(currpos <= 0)
		{
			return -1;
		}

		currpos--;
		bpos--;

		if(bpos < 0)
		{
			bufferize();
		}

		return ((int)buffer[bpos]) & 0xFF;
	}
	
	/**
     * Получить буфер, содержащий текущую позицию в потоке.
	 * Это может быть как новый буфер, так и сохраненный старый.
     */
	protected boolean bufferize() throws IOException
	{
		DataBlock currblock = null;

		for(int i = 0; i < datablocks.size(); i++)
		{
			currblock = (DataBlock)datablocks.elementAt(i);

			if(currblock.contains(currpos))
			{
				break;
			}
			else
			{
				currblock = null;
			}
		}

		if(currblock == null)
		{
			boolean set = false;

			for(int i = 0; i < datablocks.size(); i++)
			{
				currblock = (DataBlock)datablocks.elementAt(i);

				if(currblock.start > currpos)
				{
					if(currblock.start - bmax <= currpos)
					{
						is.setPosition(currblock.start - bmax);
						set = true;
					}

					break;
				}
			}

			if(!set)
			{
				if(currpos > capacity - bmax)
				{
					is.setPosition(capacity - bmax);
				}
				else
				{
					is.setPosition(currpos);
				}
			}
			
			try
			{
				currblock = new DataBlock(bmax);
				currblock.read(is);
			}
			catch(OutOfMemoryError oome)
			{
				if(datablocks.size() > 0)
				{
					int index;

					if(currpos < ((DataBlock)datablocks.elementAt(0)).start)
					{
						index = datablocks.size() - 1;
					}
					else
					{
						index = 0;
					}

					currblock = (DataBlock)datablocks.elementAt(index);
					currblock.read(is);

					datablocks.removeElementAt(index);
				}
				else
				{
					throw new RuntimeException("Consider this as \"OutOfMemoryError\"");
				}
			}

			boolean added = false;

			for(int i = 0; i < datablocks.size(); i++)
			{
				if(((DataBlock)datablocks.elementAt(i)).start > currblock.start)
				{
					datablocks.insertElementAt(currblock, i);
					added = true;

					break;
				}
			}

			if(!added)
			{
				datablocks.addElement(currblock);
			}
		}

		buffer = currblock.data;
		blen = currblock.len;
		bpos = currpos - currblock.start;

		return blen > 0;
	}
	
	/**
     * Обновить содержимое буфера в соответствии с потоком
     */
	protected void updateBuffer() throws IOException
	{
		capacity = is.getCapacity();

		if(currpos > capacity)
		{
			currpos = capacity;
		}

		datablocks.removeAllElements();
		bufferize();
	}
	
	/**
	 * Общее обновление потока.
	 */
	public void update() throws IOException
	{
		is.update();
		updateBuffer();
	}

	public void pause() throws IOException
	{
		is.pause();
	}

	public void resume() throws IOException
	{
		is.resume();
	}
	
	/**
     * Прочитать булево значение из потока (см. DataInput)
     */
	public boolean readBoolean() throws IOException
	{
		int r = read();
		
		if(r == -1)
		{
			throw new IOException("EOF");
		}
		
		return r != 0;
	}
	
	/**
     * Прочитать байт из потока; если достигнут конец потока,
     * генерируется исключение IOException с сообщением "EOF" (см. DataInput)
     */
	public byte readByte() throws IOException
	{
		int r = read();
		
		if(r == -1)
		{
			throw new IOException("EOF");
		}
		
		return (byte)r;
	}
	
	/**
     * Прочитать символ (Unicode Big Endian) из потока (см. DataInput)
     */
	public char readChar() throws IOException
	{
		//return (char)((readUnsignedByte() << 8) | readUnsignedByte());
		return (char)((read() << 8) | read());
	}
	
	/**
     * Прочитать символ (Unicode Big Endian) из потока НАЗАД (см. DataInput)
     */
	public char readCharBack() throws IOException
	{
		return (char)(readBack() | (readBack() << 8));
	}
	
	/**
     * Прочитать число с плавающей точкой двойной точности (см. DataInput)
     */
	public double readDouble() throws IOException
	{
		return Double.longBitsToDouble(readLong());
	}
	
	/**
     * Прочитать число с плавающей точкой одинарной точности (см. DataInput)
     */
	public float readFloat() throws IOException
	{
		return Float.intBitsToFloat(readInt());
	}
	
	/**
     * Прочитать массив b[] из потока целиком, если целиком не получится, 
     * сгенерировать исключение IOException с сообщением "EOF"
     */
	public void readFully(byte[] b) throws IOException
	{
		if(read(b) < b.length)
		{
			throw new IOException("EOF");
		}
	}
	
	/**
     * Прочитать в точности len байт и записать их в массив b[], начиная
     * со смещения off. Если достигнут конец файла - сгенерировать
     * исключение IOException с сообщением "EOF"
     */
	public void readFully(byte[] b, int off, int len) throws IOException
	{
		if(read(b, off, len) < len)
		{
			throw new IOException("EOF");
		}
	}
	
	/**
     * Прочитать из потока целое число (см. DataInput)
     */
	public int readInt() throws IOException
	{
		return (readUnsignedByte() << 24) |
			   (readUnsignedByte() << 16) |
			   (readUnsignedByte() << 8) |
			   (readUnsignedByte());
	}
	
	/**
     * Прочитать из потока целое число (Little Endian)
     */
	public int readLeInt() throws IOException
	{
		return (readUnsignedByte()) |
			   (readUnsignedByte() << 8) |
			   (readUnsignedByte() << 16) |
			   (readUnsignedByte() << 24);
	}
	
	/**
     * Прочитать из потока длинное целое число (см. DataInput)
     */
	public long readLong() throws IOException
	{
		return ((long)readUnsignedByte() << 56) |
			   ((long)readUnsignedByte() << 48) |
			   ((long)readUnsignedByte() << 40) |
			   ((long)readUnsignedByte() << 32) |
			   ((long)readUnsignedByte() << 24) |
			   ((long)readUnsignedByte() << 16) |
			   ((long)readUnsignedByte() << 8) |
			   ((long)readUnsignedByte());
	}
	
	/**
     * Прочитать из потока короткое целое число (см. DataInput)
     */
	public short readShort() throws IOException
	{
		return (short)((readUnsignedByte() << 8) | readUnsignedByte());
	}
	
	public short readLeShort() throws IOException
	{
		return (short)(readUnsignedByte() | (readUnsignedByte() << 8));
	}
	
	/**
     * Прочитать из потока беззнаковый байт (см. DataInput)
     */
	public int readUnsignedByte() throws IOException
	{
		return ((int)readByte()) & 0xFF;
	}
	
	/**
     * Прочитать из потока беззнаковое короткое целое (см. DataInput)
     */
	public int readUnsignedShort() throws IOException
	{
		return ((int)readShort()) & 0xFFFF;
	}
	
	public int readUnsignedLeShort() throws IOException
	{
		return ((int)readLeShort()) & 0xFFFF;
	}
	
	/**
     * Пропустить len байт (см. DataInput)
     */
	public int skipBytes(int len) throws IOException
	{
		return (int)skip(len);
	}
	
	/**
     * Прочитать из потока строку в UTF-8 в соответствии со
     * спецификацией в DataInput (см. DataInput)
     */
	public String readUTF() throws IOException, UTFDataFormatException
	{
		int n = readUnsignedShort();

		byte b[] = new byte[n];
		readFully(b);

		return new String(b, 0, b.length, "UTF-8");
	}
	
	/**
	 * Прочитать из потока символ в кодировке UTF-8.
	 * Поскольку возможна ситуация, когда текст
	 * НЕ в UTF-8 будет читаться как UTF-8,
	 * UTFDataFormatException здесь не используется.
	 * Вместо этого возвращается нулевой символ.
	 */
	public char readCharUTF() throws IOException
	{
		int b, c, d;
		
		b = read();
		
		if(b == -1)
		{
			return (char)-1;
		}
		
		if((b & 0x80) == 0)
		{
			return (char)b;
		}
		else if((b & 0xE0) == 0xC0)
		{
			c = read();
			
			if((c & 0xC0) == 0x80)
			{
				return (char)(((b & 0x1F) << 6) | (c & 0x3F));
			}
		}
		else if((b & 0xF0) == 0xE0)
		{
			c = read();
			d = read();
			
			if((c & 0xC0) == 0x80 && (d & 0xC0) == 0x80)
			{
				return (char)(((b & 0x0F) << 12) | ((c & 0x3F) << 6) | (d & 0x3F));
			}
		}
		
		return 0;
	}
	
	/**
	 * Прочитать из потока символ в кодировке UTF-8 НАЗАД.
	 * Поскольку возможна ситуация, когда текст
	 * НЕ в UTF-8 будет читаться как UTF-8,
	 * UTFDataFormatException здесь не используется.
	 * Вместо этого возвращается нулевой символ.
	 */
	public char readCharBackUTF() throws IOException
	{
		int b, c, d;
		
		d = readBack();
		
		if(d == -1)
		{
			return (char)-1;
		}
		
		if((d & 0x80) == 0)
		{
			return (char)d;
		}
		else if((d & 0xC0) == 0x80)
		{
			c = readBack();
			
			if((c & 0xE0) == 0xC0)
			{
				return (char)(((c & 0x1F) << 6) | (d & 0x3F));
			}
			else if((c & 0xC0) == 0x80)
			{
				b = readBack();
				
				if((b & 0xF0) == 0xE0)
				{
					return (char)(((b & 0x0F) << 12) | ((c & 0x3F) << 6) | (d & 0x3F));
				}
			}
		}
		
		return 0;
	}
	
	/**
	 * Прочитать из потока символ в кодировке UTF-16 (Little Endian)
	 */
	public char readLeChar() throws IOException
	{
		return (char)(read() | (read() << 8));
	}
	
	/**
	 * Прочитать из потока символ в кодировке UTF-16 (Little Endian) НАЗАД
	 */
	public char readLeCharBack() throws IOException
	{
		return (char)((readBack() << 8) | readBack());
	}
	
	/**
     * Прочитать из потока максимум count символов в кодировке UTF-8
     */
	public String readUTF(int count) throws IOException, UTFDataFormatException
	{
		StringBuffer s = new StringBuffer();
		
		for(int i = 0; i < count && available() > 0; i++)
		{
			s.append(readCharUTF());
		}
		
		return s.toString();
	}
	
	/**
     * Пропустить в потоке максимум count символов в кодировке UTF-8.
     * Не очень чётко проверяет соответствие данных кодировке.
     */
	public int skipUTF(int count) throws IOException, UTFDataFormatException
	{
		byte b;
		int i = 0, r = 0;
		
		while(i < count)
		{
			b = readByte();
			
			if((b & 0x80) == 0)
			{
				r++;
			}
			else if((((int)b) & 0xE0) == 0xC0)
			{
				readByte();
				r += 2;
			}
			else if((((int)b) & 0xF0) == 0xE0)
			{
				readShort();
				r += 3;
			}
			else
			{
				throw new UTFDataFormatException();
			}
			
			i++;
		}
		
		return r;
	}
	
//	private static void out(String s)
//	{
//		System.out.println("[BufDataInputStream] " + s);
//	}
}
