package io;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.util.zip.CRC32;

import util.DiffCompiler;

public class FileHandler {
	public String pathToFile;
	
	private RandomAccessFile inputFile;
	private long crc32;
	private long fileLength;
	
	private DiffCompiler appliedDiffs;
	
	private long nextReadOffset = 0;
	
	public FileHandler(File file) throws IOException {
		super();
		this.pathToFile = file.getAbsolutePath();
		
		inputFile = new RandomAccessFile(file, "r");
		fileLength = inputFile.length();
		
		FileInputStream inputStream = new FileInputStream(pathToFile);
		
		CRC32 checksum = new CRC32();
		long currentOffset = 0;
		int numBytes = Math.min(1024, (int)(fileLength - currentOffset));
		while (numBytes > 0) {
			byte[] batch = new byte[numBytes];
			if (inputStream.read(batch) == -1) { break; }
			checksum.update(batch);
			currentOffset += batch.length;
			numBytes = Math.min(1024, (int)(fileLength - currentOffset));
		}
		
		crc32 = checksum.getValue();
		
		inputStream.close();
	}

	public FileHandler(String pathToFile) throws IOException {
		super();
		this.pathToFile = pathToFile;
		
		inputFile = new RandomAccessFile(pathToFile, "r");
		fileLength = inputFile.length();
		
		FileInputStream inputStream = new FileInputStream(pathToFile);
		
		CRC32 checksum = new CRC32();
		long currentOffset = 0;
		int numBytes = Math.min(1024, (int)(fileLength - currentOffset));
		while (numBytes > 0) {
			byte[] batch = new byte[numBytes];
			if (inputStream.read(batch) == -1) { break; }
			checksum.update(batch);
			currentOffset += batch.length;
			numBytes = Math.min(1024, (int)(fileLength - currentOffset));
		}
		
		crc32 = checksum.getValue();
		
		inputStream.close();
	}
	
	public void close() {
		try {
			if (inputFile != null) { inputFile.close(); }
		} catch (IOException e) {
			
		}
		inputFile = null;
	}
	
	public InputStream getInputStream(long offset) throws IOException {
		InputStream stream = new FileInputStream(pathToFile);
		stream.skip(offset);
		return stream;
	}
	
	public void setAppliedDiffs(DiffCompiler diffs) {
		appliedDiffs = diffs;
	}
	
	public void clearAppliedDiffs() {
		appliedDiffs = null;
	}
	
	public long getNextReadOffset() {
		return nextReadOffset;
	}
	
	public void setNextReadOffset(long newOffset) {
		if (inputFile != null) {
			try {
				inputFile.seek(newOffset);
				nextReadOffset = newOffset;
			} catch (IOException e) {
				System.err.println("Failed to seek to offset.");
			}
		}
	}
	
	public byte continueReadingNextByte() {
		if (inputFile == null) { return 0; }
		byte[] outputBytes = new byte[1];
		try {
			inputFile.readFully(outputBytes);
			nextReadOffset++;
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			System.err.println("Failed to read next byte starting from offset " + Long.toHexString(nextReadOffset) + ".");
			return 0;
		}
		
		return outputBytes[0];
	}
	
	public int continueReadingBytes(byte[] buffer) {
		if (inputFile == null) { return 0; }
		int bytesRead = 0;
		long bytesRemaining;
		try {
			bytesRemaining = fileLength - inputFile.getFilePointer();
		} catch (IOException e1) {
			return 0;
		}
		try {
			inputFile.readFully(buffer);
			bytesRead = (int)Math.min(buffer.length, bytesRemaining);
			nextReadOffset += bytesRead;
		} catch (IOException e) {
		}
		
		return bytesRead;
	}
	
	public byte[] continueReadingBytes(int numBytes) {
		if (inputFile == null) { return new byte[] {}; }
		try {
			long remainingBytes = fileLength - inputFile.getFilePointer();
			if (numBytes > remainingBytes) {
				numBytes = (int)remainingBytes;
			}
		} catch (IOException e) {
			e.printStackTrace();
			System.err.println("Failed to get file pointer.");
			return null;
		}
		
		byte[] outputBytes = new byte[numBytes];
		
		try {
			inputFile.readFully(outputBytes);
			nextReadOffset += numBytes;
			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			System.err.println("Failed to read " + numBytes + " bytes starting from offset " + Long.toHexString(nextReadOffset) + ".");
			return null;
		}
		
		if (appliedDiffs != null) {
			return appliedDiffs.byteArrayWithDiffs(outputBytes, nextReadOffset - numBytes);
		}
		
		return outputBytes;
	}
	
	public byte[] continueReadingBytesUpToNextTerminator(long maxOffset) {
		if (inputFile == null) { return new byte[] {}; }
		
		byte[] result = null;
		int zeroIndex = -1;
		
		do {
			long initialReadOffset = nextReadOffset;
			int numBytes = Math.min(1024, (int)(maxOffset - initialReadOffset - 1));
			if (numBytes <= 0) {
				break;
			}
			
			byte[] batch = continueReadingBytes(numBytes);
			int oldSize = result != null ? result.length : 0;
			int deltaSize = 0;
			for (int i = 0; i < batch.length; i++) {
				deltaSize++;
				if (batch[i] == 0) {
					zeroIndex = i;
					break;
				}
			}
			int newSize = oldSize + deltaSize;
			byte[] newResult = new byte[newSize];
			for (int i = 0; i < oldSize; i++) {
				newResult[i] = result[i];
			}
			for (int i = 0; i < deltaSize; i++) {
				newResult[i + oldSize] = batch[i];
			}
			if (zeroIndex != -1) {
				nextReadOffset = initialReadOffset + deltaSize;
				try {
					inputFile.seek(nextReadOffset);
				} catch (IOException e) {
					System.err.println("Unable to seek to specific offset.");
					e.printStackTrace();
				}
			}
			
			result = newResult;
		} while (zeroIndex == -1);
		
		return result;
	}
	
	public byte[] readBytesAtOffset(long offset, int numBytes) {
		if (inputFile == null) { return new byte[] {}; }
		
		long remainingBytes = fileLength - offset;
		if (numBytes > remainingBytes) {
			numBytes = (int)remainingBytes;
		}
		byte[] outputBytes = new byte[numBytes];
		
		try {
			inputFile.seek(offset);
			inputFile.readFully(outputBytes);
			nextReadOffset = inputFile.getFilePointer();
			
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			System.err.println("Failed to read " + numBytes + " bytes starting from offset " + Long.toHexString(offset) + ".");
			return null;
		}
		
		if (appliedDiffs != null) {
			return appliedDiffs.byteArrayWithDiffs(outputBytes, offset);
		}
		
		return outputBytes;
	}
	
	public long getCRC32() {
		return crc32;
	}
	
	public long getFileLength() {
		return fileLength;
	}
	
	@Override
	protected void finalize() throws Throwable {
		try {
			if (inputFile != null) { inputFile.close(); }
		} finally {
			super.finalize();
		}
	}
}