/*
 * Copyright (c) 2007 innoSysTec (R) GmbH, Germany. All rights reserved.
 * Original author: Edmund Wagner
 * Creation date: 22.05.2007
 *
 * Source: $HeadURL$
 * Last changed: $LastChangedDate$
 *
 * the unrar licence applies to all junrar source and binary distributions
 * you are not allowed to use this source to re-create the RAR compression
 * algorithm
 *
 * Here some html entities which can be used for escaping javadoc tags:
 * "&":  "&#038;" or "&amp;"
 * "<":  "&#060;" or "&lt;"
 * ">":  "&#062;" or "&gt;"
 * "@":  "&#064;"
 */
package com.innosystec.unrar;

import com.classpath.io.PipedInputStream;
import com.classpath.io.PipedOutputStream;
import java.io.IOException;
import java.io.OutputStream;

import com.innosystec.unrar.exception.RarException;
import com.innosystec.unrar.exception.RarException.RarExceptionType;
import com.innosystec.unrar.rarfile.AVHeader;
import com.innosystec.unrar.rarfile.BaseBlock;
import com.innosystec.unrar.rarfile.BlockHeader;
import com.innosystec.unrar.rarfile.CommentHeader;
import com.innosystec.unrar.rarfile.EAHeader;
import com.innosystec.unrar.rarfile.EndArcHeader;
import com.innosystec.unrar.rarfile.FileHeader;
import com.innosystec.unrar.rarfile.MacInfoHeader;
import com.innosystec.unrar.rarfile.MainHeader;
import com.innosystec.unrar.rarfile.MarkHeader;
import com.innosystec.unrar.rarfile.ProtectHeader;
import com.innosystec.unrar.rarfile.SignHeader;
import com.innosystec.unrar.rarfile.SubBlockHeader;
import com.innosystec.unrar.rarfile.SubBlockHeaderType;
import com.innosystec.unrar.rarfile.UnixOwnersHeader;
import com.innosystec.unrar.rarfile.UnrarHeadertype;
import com.innosystec.unrar.unpack.ComprDataIO;
import com.innosystec.unrar.unpack.Unpack;
import com.one.ArchiveContainer;
import com.one.ArchiveEntry;
import com.one.RandomAccessInputStream;
import com.one.file.FileConnection;
import com.vmx.ProgressCallback;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

/**
 * DOCUMENT ME
 *
 * @author $LastChangedBy$
 * @version $LastChangedRevision$
 */
public class Archive extends ArchiveContainer
{
	//private IReadOnlyAccess rais;
	private final UnrarCallback unrarCallback;
	private final ComprDataIO dataIO;
	private final Vector headers = new Vector();
	private MarkHeader markHead = null;
	private MainHeader newMhd = null;
	private EndArcHeader endHeader = null;
	private Unpack unpack;
	/** Archive data CRC. */
	private long arcDataCRC = 0xffffffff;
	private int currentHeaderIndex;
	private boolean encrypted = false;
	private int sfxSize = 0;
	/** Size of packed data in current file. */
	private long totalPackedSize = 0L;
	/** Number of bytes of compressed data read from current file. */
	private long totalPackedRead = 0L;

	public Archive()
	{
		unrarCallback = null;
		dataIO = new ComprDataIO(this);
	}

	public Archive(FileConnection file) throws RarException, IOException
	{
		this(file, null);
	}

	/**
	 * create a new archive object using the given file
	 * @param file the file to extract
	 * @throws RarException
	 */
	public Archive(FileConnection file, UnrarCallback unrarCallback) throws RarException, IOException
	{
		setFile(file);
		this.unrarCallback = unrarCallback;
		dataIO = new ComprDataIO(this);
	}

	public FileConnection getFile()
	{
		return file;
	}

	void setFile(FileConnection file) throws IOException
	{
		this.file = file;
		totalPackedSize = 0L;
		totalPackedRead = 0L;
		close();
		
		try
		{
			readEntries();
		}
		catch(Exception e)
		{
			System.out.println("exception in archive constructor maybe file is encrypted or currupt");
			e.printStackTrace();
			//ignore exceptions to allow exraction of working files in
			//corrupt archive
		}

		// Calculate size of packed data

		Enumeration blocks = headers.elements();
		BaseBlock block;

		while(blocks.hasMoreElements())
		{
			block = (BaseBlock) blocks.nextElement();

			if(block.getHeaderType() == UnrarHeadertype.FileHeader)
			{
				totalPackedSize += ((FileHeader) block).getFullPackSize();
			}
		}
		if(unrarCallback != null)
		{
			unrarCallback.volumeProgressChanged(totalPackedRead,
				totalPackedSize);
		}
	}

	public void bytesReadRead(int count)
	{
		if(count > 0)
		{
			totalPackedRead += count;
			if(unrarCallback != null)
			{
				unrarCallback.volumeProgressChanged(totalPackedRead,
					totalPackedSize);
			}
		}
	}

	public RandomAccessInputStream getRof()
	{
		return rais;
	}

	/**
	 * @return returns all file headers of the archive
	 */
	public Vector getFileHeaders()
	{
		Vector list = new Vector();

		Enumeration blocks = headers.elements();
		BaseBlock block;

		while(blocks.hasMoreElements())
		{
			block = (BaseBlock) blocks.nextElement();

			if(block.getHeaderType() == (UnrarHeadertype.FileHeader))
			{
				list.addElement((FileHeader) block);
			}
		}

		return list;
	}

	public FileHeader nextFileHeader()
	{
		int n = headers.size();

		while(currentHeaderIndex < n)
		{
			BaseBlock block = (BaseBlock) headers.elementAt(currentHeaderIndex++);

			if(block.getHeaderType() == UnrarHeadertype.FileHeader)
			{
				return (FileHeader) block;
			}
		}

		return null;
	}

	public UnrarCallback getUnrarCallback()
	{
		return unrarCallback;
	}

	/**
	 *
	 * @return whether the archive is encrypted
	 */
	public boolean isEncrypted()
	{
		if(newMhd != null)
		{
			return newMhd.isEncrypted();
		}
		else
		{
			throw new NullPointerException("mainheader is null");
		}
	}

	public boolean isReadOnly()
	{
		return true;
	}

	protected ArchiveEntry createActualEntry(RandomAccessInputStream source, String name, long time, ProgressCallback callback) throws IOException
	{
		return null;
	}

	public void deleteActualEntry(ArchiveEntry entry) throws IOException
	{
	}

	public void renameActualEntry(ArchiveEntry entry, String newName) throws IOException
	{
	}

	public void finish() throws IOException
	{
	}

	public InputStream getInputStream(ArchiveEntry entry) throws IOException
	{
		RarEntry rarentry = (RarEntry)entry;

		PipedOutputStream os = new PipedOutputStream();
		PipedInputStream is = new PipedInputStream();

		is.connect(os);

		(new Thread(new ExtractThread(this, rarentry.getFileHeader(), os))).start();

		return is;
	}

	/**
	 * Read the headers of the archive
	 * @throws RarException
	 */
	protected void readEntries() throws IOException
	{
		entries = new Hashtable();
		
		markHead = null;
		newMhd = null;
		endHeader = null;
		headers.removeAllElements();
		currentHeaderIndex = 0;
		int toRead = 0;

		long fileLength = rais.getCapacity();

		while(true)
		{
			int size = 0;
			int newpos = 0;
			byte[] baseBlockBuffer = new byte[BaseBlock.BaseBlockSize];

			int position = rais.getPosition();

			// Weird, but is trying to read beyond the end of the file
			if(position >= fileLength)
			{
				break;
			}

//            System.out.println("\n--------reading header--------");
			size = rais.read(baseBlockBuffer, 0, BaseBlock.BaseBlockSize);
			if(size == 0)
			{
				break;
			}
			BaseBlock block = new BaseBlock(baseBlockBuffer);

			block.setPositionInFile(position);


			switch(block.getHeaderType())
			{

				case UnrarHeadertype.MarkHeader:
					markHead = new MarkHeader(block);
					if(!markHead.isSignature())
					{
						throw new RarException(
							RarException.RarExceptionType.badRarArchive);
					}
					headers.addElement(markHead);
//                    markHead.print();
					break;

				case UnrarHeadertype.MainHeader:
					int mainHeaderSize = 0;
					toRead = block.hasEncryptVersion()
						? MainHeader.mainHeaderSizeWithEnc
						: MainHeader.mainHeaderSize;
					byte[] mainbuff = new byte[toRead];
					mainHeaderSize = rais.read(mainbuff, 0, toRead);
					MainHeader mainhead = new MainHeader(block, mainbuff);
					headers.addElement(mainhead);
					this.newMhd = mainhead;
					if(newMhd.isEncrypted())
					{
						throw new RarException(
							RarException.RarExceptionType.rarEncryptedException);
					}
//                    mainhead.print();
					break;

				case UnrarHeadertype.SignHeader:
					int signHeaderSize = 0;
					toRead = SignHeader.signHeaderSize;
					byte[] signBuff = new byte[toRead];
					signHeaderSize = rais.read(signBuff, 0, toRead);
					SignHeader signHead = new SignHeader(block, signBuff);
					headers.addElement(signHead);
//                    System.out.println("HeaderType: SignHeader");

					break;

				case UnrarHeadertype.AvHeader:
					int avHeaderSize = 0;
					toRead = AVHeader.avHeaderSize;
					byte[] avBuff = new byte[toRead];
					avHeaderSize = rais.read(avBuff, 0, toRead);
					AVHeader avHead = new AVHeader(block, avBuff);
					headers.addElement(avHead);
//                    System.out.println("headertype: AVHeader");
					break;

				case UnrarHeadertype.CommHeader:
					int commHeaderSize = 0;
					toRead = CommentHeader.commentHeaderSize;
					byte[] commBuff = new byte[toRead];
					commHeaderSize = rais.read(commBuff, 0, toRead);
					CommentHeader commHead = new CommentHeader(block, commBuff);
					headers.addElement(commHead);
//                    System.out.println("method: "+commHead.getUnpMethod()+"; 0x"+
//                            Integer.toHexString(commHead.getUnpMethod()));
					newpos = commHead.getPositionInFile() + commHead.getHeaderSize();
					rais.setPosition(newpos);

					break;
				case UnrarHeadertype.EndArcHeader:

					toRead = 0;
					if(block.hasArchiveDataCRC())
					{
						toRead += EndArcHeader.endArcArchiveDataCrcSize;
					}
					if(block.hasVolumeNumber())
					{
						toRead += EndArcHeader.endArcVolumeNumberSize;
					}
					EndArcHeader endArcHead;
					if(toRead > 0)
					{
						int endArcHeaderSize = 0;
						byte[] endArchBuff = new byte[toRead];
						endArcHeaderSize = rais.read(endArchBuff, 0, toRead);
						endArcHead = new EndArcHeader(block, endArchBuff);
//                        System.out.println("HeaderType: endarch\ndatacrc:"+
//                                endArcHead.getArchiveDataCRC());
					}
					else
					{
//                        System.out.println("HeaderType: endarch - no Data");
						endArcHead = new EndArcHeader(block, null);
					}
					headers.addElement(endArcHead);
					this.endHeader = endArcHead;
//                    System.out.println("\n--------end header--------");
					return;

				default:
					byte[] blockHeaderBuffer =
						new byte[BlockHeader.blockHeaderSize];
					int bhsize = rais.read(blockHeaderBuffer, 0, BlockHeader.blockHeaderSize);
					BlockHeader blockHead = new BlockHeader(block,
						blockHeaderBuffer);

					switch(blockHead.getHeaderType())
					{
						case UnrarHeadertype.NewSubHeader:
						case UnrarHeadertype.FileHeader:
							toRead = blockHead.getHeaderSize()
								- BlockHeader.BaseBlockSize
								- BlockHeader.blockHeaderSize;
							byte[] fileHeaderBuffer = new byte[toRead];
							int fhsize = rais.read(fileHeaderBuffer, 0, toRead);

							FileHeader fh = new FileHeader(blockHead, fileHeaderBuffer);
//                            if (DEBUG) {
//                                fh.print();
//                            }
							headers.addElement(fh);

							if(fh.getFileNameString() != null &&
							   fh.getFileNameString().length() > 0)
							{
								RarEntry entry = new RarEntry(fh);

								entries.put(entry.getName(), entry);
								addFile(entry.getName());
							}

							newpos = fh.getPositionInFile() + fh.getHeaderSize() + fh.getFullPackSize();
							rais.setPosition(newpos);
							break;

						case UnrarHeadertype.ProtectHeader:
							toRead = blockHead.getHeaderSize()
								- BlockHeader.BaseBlockSize
								- BlockHeader.blockHeaderSize;
							byte[] protectHeaderBuffer = new byte[toRead];
							int phsize = rais.read(protectHeaderBuffer, 0, toRead);
							ProtectHeader ph = new ProtectHeader(blockHead,
								protectHeaderBuffer);

//                            System.out.println("totalblocks"+ph.getTotalBlocks());
							newpos = ph.getPositionInFile()
								+ ph.getHeaderSize();
							rais.setPosition(newpos);
							break;

						case UnrarHeadertype.SubHeader:
						{
							byte[] subHeadbuffer = new byte[SubBlockHeader.SubBlockHeaderSize];
							int subheadersize = rais.read(subHeadbuffer, 0, SubBlockHeader.SubBlockHeaderSize);
							SubBlockHeader subHead = new SubBlockHeader(blockHead, subHeadbuffer);
							subHead.print();
							switch(subHead.getSubType())
							{
								case SubBlockHeaderType.MAC_HEAD:
								{
									byte[] macHeaderbuffer = new byte[MacInfoHeader.MacInfoHeaderSize];
									int macheadersize = rais.read(macHeaderbuffer, 0, MacInfoHeader.MacInfoHeaderSize);
									MacInfoHeader macHeader = new MacInfoHeader(subHead, macHeaderbuffer);
									macHeader.print();
									headers.addElement(macHeader);

									break;
								}
								//TODO implement other subheaders
								case SubBlockHeaderType.BEEA_HEAD:
									break;
								case SubBlockHeaderType.EA_HEAD:
								{
									byte[] eaHeaderBuffer = new byte[EAHeader.EAHeaderSize];
									int eaheadersize = rais.read(eaHeaderBuffer, 0, EAHeader.EAHeaderSize);
									EAHeader eaHeader = new EAHeader(subHead, eaHeaderBuffer);
									eaHeader.print();
									headers.addElement(eaHeader);

									break;
								}
								case SubBlockHeaderType.NTACL_HEAD:
									break;
								case SubBlockHeaderType.STREAM_HEAD:
									break;
								case SubBlockHeaderType.UO_HEAD:
									toRead = subHead.getHeaderSize();
									toRead -= BaseBlock.BaseBlockSize;
									toRead -= BlockHeader.blockHeaderSize;
									toRead -= SubBlockHeader.SubBlockHeaderSize;
									byte[] uoHeaderBuffer = new byte[toRead];
									int uoHeaderSize = rais.read(uoHeaderBuffer, 0, toRead);
									UnixOwnersHeader uoHeader = new UnixOwnersHeader(subHead, uoHeaderBuffer);
									uoHeader.print();
									headers.addElement(uoHeader);
									break;
								default:
									break;
							}

							break;
						}
						default:
							System.out.println("Unknown Header");
							throw new RarException(
								RarExceptionType.notRarArchive);

					}
			}
//            System.out.println("\n--------end header--------");
		}
	}

	/**
	 * Extract the file specified by the given header and write it
	 * to the supplied output stream
	 *
	 * @param header the header to be extracted
	 * @param os the outputstream
	 * @throws RarException
	 */
	public void extractFile(FileHeader hd, OutputStream os) throws RarException
	{
		if(!headers.contains(hd))
		{
			throw new RarException(RarExceptionType.headerNotInArchive);
		}
		try
		{
			doExtractFile(hd, os);
		}
		catch(Exception e)
		{
			if(e instanceof RarException)
			{
				throw (RarException) e;
			}
			else
			{
				throw new RarException(e);
			}
		}
	}

	private void doExtractFile(FileHeader hd, OutputStream os) throws RarException, IOException
	{
		dataIO.init(os);
		dataIO.init(hd);
		dataIO.setUnpFileCRC(this.isOldFormat() ? 0 : 0xffFFffFF);
		if(unpack == null)
		{
			unpack = new Unpack(dataIO);
		}
		if(!hd.isSolid())
		{
			unpack.init(null);
		}
		unpack.setDestSize(hd.getFullUnpackSize());
		try
		{
			unpack.doUnpack(hd.getUnpVersion(), hd.isSolid());
			// Verify file CRC
			hd = dataIO.getSubHeader();
			long actualCRC = hd.isSplitAfter() ? ~dataIO.getPackedCRC() : ~dataIO.getUnpFileCRC();
			int expectedCRC = hd.getFileCRC();
			if(actualCRC != expectedCRC)
			{
				throw new RarException(RarExceptionType.crcError);
			}
//            if (!hd.isSplitAfter()) {
//                // Verify file CRC
//                if(~dataIO.getUnpFileCRC() != hd.getFileCRC()){
//                    throw new RarException(RarExceptionType.crcError);
//                }
//            }
		}
		catch(Exception e)
		{
			unpack.cleanUp();
			if(e instanceof RarException)
			{
				//throw new RarException((RarException)e);
				throw (RarException) e;
			}
			else
			{
				throw new RarException(e);
			}
		}
	}

	/**
	 * @return returns the main header of this archive
	 */
	public MainHeader getMainHeader()
	{
		return newMhd;
	}

	/**
	 * @return whether the archive is old format
	 */
	public boolean isOldFormat()
	{
		return markHead.isOldFormat();
	}

	/** Close the underlying compressed file. */
	public void close() throws IOException
	{
		if(rais != null)
		{
			rais.close();
			rais = null;
		}
	}
}
