/*
 * Copyright 2010 Brian Pellin.
 *     
 * This file is part of KeePassDroid.
 *
 *  KeePassDroid is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  KeePassDroid is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with KeePassDroid.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
package com.keepassdroid.stream;

import java.io.IOException;
import java.io.InputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;


public class HashedBlockInputStream extends InputStream {
	
	private final static int HASH_SIZE = 32;

	private LEDataInputStream baseStream;
	private int bufferPos = 0;
	private byte[] buffer = new byte[0];
	private long bufferIndex = 0;
	private boolean atEnd = false;
	
	
	@Override
	public int read(byte[] b) throws IOException {
		return read(b, 0, b.length);
	}

	public HashedBlockInputStream(InputStream is) {
		baseStream = new LEDataInputStream(is);
	}
	
	@Override
	public int read(byte[] b, int offset, int length) throws IOException {
		if ( atEnd ) return -1;
		
		int remaining = length;
		
		while ( remaining > 0 ) {
			if ( bufferPos == buffer.length ) {
				// Get more from the source into the buffer
				if ( ! ReadHashedBlock() ) {
					return length - remaining;
				}
				
			}

			// Copy from buffer out
			int copyLen = Math.min(buffer.length - bufferPos, remaining);
			
			System.arraycopy(buffer, bufferPos, b, offset, copyLen);
			
			offset += copyLen;
			bufferPos += copyLen;
			
			remaining -= copyLen;
		}
		
		return length;
	}

	/**
	 * @return false, when the end of the source stream is reached 
	 * @throws IOException 
	 */
	private boolean ReadHashedBlock() throws IOException {
		if ( atEnd ) return false;
		
		bufferPos = 0;
		
		long index = baseStream.readUInt();
		if ( index != bufferIndex ) {
			throw new IOException("Invalid data format");
		}
		bufferIndex++;
		
		byte[] storedHash = baseStream.readBytes(32);
		if ( storedHash == null || storedHash.length != HASH_SIZE) {
			throw new IOException("Invalid data format");
		}
		
		int bufferSize = LEDataInputStream.readInt(baseStream);
		if ( bufferSize < 0 ) {
			throw new IOException("Invalid data format");
		}
		
		if ( bufferSize == 0 ) {
			for (int hash = 0; hash < HASH_SIZE; hash++) {
				if ( storedHash[hash] != 0 ) {
					throw new IOException("Invalid data format");
				}
			}
				
			atEnd = true;
			buffer = new byte[0];
			return false;
		}
		
		buffer = baseStream.readBytes(bufferSize);
		if ( buffer == null || buffer.length != bufferSize ) {
			throw new IOException("Invalid data format");
		}
		
		MessageDigest md = null;
		try {
			md = MessageDigest.getInstance("SHA-256");
		} catch (NoSuchAlgorithmException e) {
			throw new IOException("SHA-256 not implemented here.");
		}
		
		byte[] computedHash = md.digest(buffer);
		if ( computedHash == null || computedHash.length != HASH_SIZE ) {
			throw new IOException("Hash wrong size");
		}
		
		if ( ! Arrays.equals(storedHash, computedHash) ) {
			throw new IOException("Hashes didn't match.");
		}

		return true;
	}

	@Override
	public long skip(long n) throws IOException {
		return 0;
	}

	@Override
	public int read() throws IOException {
		if ( atEnd ) return -1;
		
		if ( bufferPos == buffer.length ) {
			if ( ! ReadHashedBlock() ) return -1;
		}
		
		int output = buffer[bufferPos];
		bufferPos++;
		
		return output;
	}

	@Override
	public void close() throws IOException {
		baseStream.close();
	}

}
