/**
 * Copyright 2012 Kamran Zafar 
 * 
 * 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 org.kamranzafar.jtar;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * @author Kamran Zafar
 * 
 */
public class TarInputStream extends FilterInputStream {

	private static final int SKIP_BUFFER_SIZE = 2048;
	private TarEntry currentEntry;
	private long currentFileSize;
	private long bytesRead;
	private boolean defaultSkip = false;

	public TarInputStream(InputStream in) {
		super(in);
		currentFileSize = 0;
		bytesRead = 0;
	}

	@Override
	public boolean markSupported() {
		return false;
	}

	/**
	 * Not supported
	 * 
	 */
	@Override
	public synchronized void mark(int readlimit) {
	}

	/**
	 * Not supported
	 * 
	 */
	@Override
	public synchronized void reset() throws IOException {
		throw new IOException("mark/reset not supported");
	}

	/**
	 * Read a byte
	 * 
	 * @see java.io.FilterInputStream#read()
	 */
	@Override
	public int read() throws IOException {
		byte[] buf = new byte[1];

		int res = this.read(buf, 0, 1);

		if (res != -1) {
			return 0xFF & buf[0];
		}

		return res;
	}

	/**
	 * Checks if the bytes being read exceed the entry size and adjusts the byte
	 * array length. Updates the byte counters
	 * 
	 * 
	 * @see java.io.FilterInputStream#read(byte[], int, int)
	 */
	@Override
	public int read(byte[] b, int off, int len) throws IOException {
		if (currentEntry != null) {
			if (currentFileSize == currentEntry.getSize()) {
				return -1;
			} else if ((currentEntry.getSize() - currentFileSize) < len) {
				len = (int) (currentEntry.getSize() - currentFileSize);
			}
		}

		int br = super.read(b, off, len);

		if (br != -1) {
			if (currentEntry != null) {
				currentFileSize += br;
			}

			bytesRead += br;
		}

		return br;
	}

	/**
	 * Returns the next entry in the tar file
	 * 
	 * @return TarEntry
	 * @throws IOException
	 */
	public TarEntry getNextEntry() throws IOException {
		closeCurrentEntry();

		byte[] header = new byte[TarConstants.HEADER_BLOCK];
		byte[] theader = new byte[TarConstants.HEADER_BLOCK];
		int tr = 0;

		// Read full header
		while (tr < TarConstants.HEADER_BLOCK) {
			int res = read(theader, 0, TarConstants.HEADER_BLOCK - tr);

			if (res < 0) {
				break;
			}

			System.arraycopy(theader, 0, header, tr, res);
			tr += res;
		}

		// Check if record is null
		boolean eof = true;
		for (byte b : header) {
			if (b != 0) {
				eof = false;
				break;
			}
		}

		if (!eof) {
			currentEntry = new TarEntry(header);
		}

		return currentEntry;
	}

	/**
	 * Returns the current offset (in bytes) from the beginning of the stream. 
	 * This can be used to find out at which point in a tar file an entry's content begins, for instance. 
	 */
	public long getCurrentOffset() {
		return bytesRead;
	}
	
	/**
	 * Closes the current tar entry
	 * 
	 * @throws IOException
	 */
	protected void closeCurrentEntry() throws IOException {
		if (currentEntry != null) {
			if (currentEntry.getSize() > currentFileSize) {
				// Not fully read, skip rest of the bytes
				long bs = 0;
				while (bs < currentEntry.getSize() - currentFileSize) {
					long res = skip(currentEntry.getSize() - currentFileSize - bs);

					if (res == 0 && currentEntry.getSize() - currentFileSize > 0) {
						// I suspect file corruption
						throw new IOException("Possible tar file corruption");
					}

					bs += res;
				}
			}

			currentEntry = null;
			currentFileSize = 0L;
			skipPad();
		}
	}

	/**
	 * Skips the pad at the end of each tar entry file content
	 * 
	 * @throws IOException
	 */
	protected void skipPad() throws IOException {
		if (bytesRead > 0) {
			int extra = (int) (bytesRead % TarConstants.DATA_BLOCK);

			if (extra > 0) {
				long bs = 0;
				while (bs < TarConstants.DATA_BLOCK - extra) {
					long res = skip(TarConstants.DATA_BLOCK - extra - bs);
					bs += res;
				}
			}
		}
	}

	/**
	 * Skips 'n' bytes on the InputStream<br>
	 * Overrides default implementation of skip
	 * 
	 */
	@Override
	public long skip(long n) throws IOException {
		if (defaultSkip) {
			// use skip method of parent stream
			// may not work if skip not implemented by parent
			long bs = super.skip(n);
			bytesRead += bs;

			return bs;
		}

		if (n <= 0) {
			return 0;
		}

		long left = n;
		byte[] sBuff = new byte[SKIP_BUFFER_SIZE];

		while (left > 0) {
			int res = read(sBuff, 0, (int) (left < SKIP_BUFFER_SIZE ? left : SKIP_BUFFER_SIZE));
			if (res < 0) {
				break;
			}
			left -= res;
		}

		return n - left;
	}

	public boolean isDefaultSkip() {
		return defaultSkip;
	}

	public void setDefaultSkip(boolean defaultSkip) {
		this.defaultSkip = defaultSkip;
	}
}
