/*
* Copyright 2010 Srikanth Reddy Lingala  
* 
* Licensed under the Apache License, Version 2.0 (the "License"); 
* you may not use this file except in compliance with the License. 
* You may obtain a copy of the License at 
* 
* http://www.apache.org/licenses/LICENSE-2.0 
* 
* Unless required by applicable law or agreed to in writing, 
* software distributed under the License is distributed on an "AS IS" BASIS, 
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
* See the License for the specific language governing permissions and 
* limitations under the License. 
*/

package net.lingala.zip4j.io;

import java.io.IOException;
import java.io.RandomAccessFile;

import net.lingala.zip4j.crypto.AESDecrypter;
import net.lingala.zip4j.crypto.IDecrypter;
import net.lingala.zip4j.exception.ZipException;
import net.lingala.zip4j.unzip.UnzipEngine;
import net.lingala.zip4j.util.InternalZipConstants;
import net.lingala.zip4j.util.Zip4jConstants;

public class PartInputStream extends BaseInputStream
{
	private RandomAccessFile raf;
	private long bytesRead, length;
	private UnzipEngine unzipEngine;
	private IDecrypter decrypter;
	private byte[] oneByteBuff = new byte[1];
	private byte[] aesBlockByte = new byte[16];
	private int aesBytesReturned = 0;
	private boolean isAESEncryptedFile = false;
	private int count = -1;
	
	public PartInputStream(RandomAccessFile raf, long start, long len, UnzipEngine unzipEngine) {
	    this.raf = raf;
	    this.unzipEngine = unzipEngine;
	    this.decrypter = unzipEngine.getDecrypter();
	    this.bytesRead = 0;
	    this.length = len;
	    this.isAESEncryptedFile = unzipEngine.getFileHeader().isEncrypted() && 
			unzipEngine.getFileHeader().getEncryptionMethod() == Zip4jConstants.ENC_METHOD_AES;
	}
  
	public int available() {
		long amount = length - bytesRead;
		if (amount > Integer.MAX_VALUE)
			return Integer.MAX_VALUE;
		return (int) amount;
	}
  
	public int read() throws IOException {
		if (bytesRead >= length)
			return -1;
		
		if (isAESEncryptedFile) {
			if (aesBytesReturned == 0 || aesBytesReturned == 16) {
				if (read(aesBlockByte) == -1) {
					return -1;
				}
				aesBytesReturned = 0;
			}
			return aesBlockByte[aesBytesReturned++] & 0xff;
		} else {
			return read(oneByteBuff, 0, 1) == -1 ? -1 : oneByteBuff[0] & 0xff;
		}
	}
	
	public int read(byte[] b) throws IOException {
		return this.read(b, 0, b.length);
	}
	
	public int read(byte[] b, int off, int len) throws IOException {
		if (len > length - bytesRead) {
			len = (int) (length - bytesRead);
			if (len == 0) {
				checkAndReadAESMacBytes();
				return -1;
			}
		}
		
		if (unzipEngine.getDecrypter() instanceof AESDecrypter) {
			if (bytesRead + len  < length) {
				if (len % 16 != 0) {
					len = len - (len%16);
				}
			}
		}
		
		synchronized (raf) {
			count = raf.read(b, off, len);
			if ((count < len) && unzipEngine.getZipModel().isSplitArchive()) {
				raf.close();
				raf = unzipEngine.startNextSplitFile();
				if (count < 0) count = 0;
				int newlyRead = raf.read(b, count, len-count);
				if (newlyRead > 0)
					count += newlyRead;
			}
		}
		
		if (count > 0) {
			if (decrypter != null) {
				try {
					decrypter.decryptData(b, off, count);
				} catch (ZipException e) {
					throw new IOException(e.getMessage());
				}
			}
			bytesRead += count;
		}
		
		if (bytesRead >= length) {
			checkAndReadAESMacBytes();
		}
		
		return count;
	}
	
	protected void checkAndReadAESMacBytes() throws IOException {
		if (isAESEncryptedFile) {
			if (decrypter != null && decrypter instanceof AESDecrypter) {
				if (((AESDecrypter)decrypter).getStoredMac() != null) {
					//Stored mac already set
					return;
				}
				byte[] macBytes = new byte[InternalZipConstants.AES_AUTH_LENGTH];
				int readLen = -1;
				readLen = raf.read(macBytes);
				if (readLen != InternalZipConstants.AES_AUTH_LENGTH) {
					if (unzipEngine.getZipModel().isSplitArchive()) {
						raf.close();
						raf = unzipEngine.startNextSplitFile();
						int newlyRead = raf.read(macBytes, readLen, InternalZipConstants.AES_AUTH_LENGTH - readLen);
						readLen += newlyRead;
					} else {
						throw new IOException("Error occured while reading stored AES authentication bytes");
					}
				}
				
				((AESDecrypter)unzipEngine.getDecrypter()).setStoredMac(macBytes);
			}
		}
	}

	public long skip(long amount) throws IOException {
		if (amount < 0)
			throw new IllegalArgumentException();
		if (amount > length - bytesRead)
			amount = length - bytesRead;
		bytesRead += amount;
		return amount;
	}
  
	public void close() throws IOException {
		raf.close();
	}
  
	public void seek(long pos) throws IOException {
		raf.seek(pos);
	}
	
	public UnzipEngine getUnzipEngine() {
		return this.unzipEngine;
	} 
}
