/*******************************************************************************
 * Copyright (c) 2007, 2018 compeople AG and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 * 	compeople AG (Stefan Liebig) - initial API and implementation
 *  IBM Corporation - bug fixes and enhancements
 *******************************************************************************/
package org.eclipse.equinox.internal.p2.sar;

import java.io.*;

/**
 * The SarOutputStream writes a stream archive as an OutputStream. Methods are
 * provided to put entries, and then write their contents by writing to this
 * stream using write().
 */
public class SarOutputStream extends OutputStream {

	private boolean finished;
	private final DataOutputStream dataOutputStream;
	private final DirectByteArrayOutputStream entryContent;

	/**
	 * @param outputStream
	 * @throws IOException
	 */
	public SarOutputStream(OutputStream outputStream) throws IOException {
		dataOutputStream = new DataOutputStream(outputStream);
		entryContent = new DirectByteArrayOutputStream(16 * 1024);
		writeString(SarConstants.SARFILE_MARKER);
		dataOutputStream.writeInt(SarConstants.SARFILE_VERSION);
		finished = false;
	}

	/**
	 * Ends the SAR archive and closes the underlying OutputStream.
	 *
	 * @see java.io.OutputStream#close()
	 */
	@Override
	public void close() throws IOException {
		finish();
		super.close();
	}

	/**
	 * Finish this SAR archive but does not close the underlying output stream.
	 *
	 * @throws IOException
	 */
	public void finish() throws IOException {
		if (finished)
			return;

		writeEOFRecord();
		finished = true;
	}

	/**
	 * Put an entry on the output stream. This writes the entry's header record and
	 * positions the output stream for writing the contents of the entry. Once this
	 * method is called, the stream is ready for calls to write() to write the
	 * entry's contents. Once the contents are written, closeEntry() <B>MUST </B> be
	 * called to ensure that all buffered data is completely written to the output
	 * stream.
	 *
	 * @param entry the SarEntry to be written to the archive.
	 * @throws IOException
	 */
	public void putNextEntry(SarEntry entry) throws IOException {
		entry.writeTo(this);
	}

	/**
	 * Close an entry. This method MUST be called for all file entries that contain
	 * data. The reason is that we must buffer data written to the stream in order
	 * to satisfy the buffer's record based writes. Thus, there may be data
	 * fragments still being assembled that must be written to the output stream
	 * before this entry is closed and the next entry written.
	 *
	 * @throws IOException
	 */
	public void closeEntry() throws IOException {
		writeBytes(entryContent.getBuffer(), entryContent.getBufferLength());
		entryContent.reset();
	}

	/**
	 * @param s
	 * @throws IOException
	 */
	void writeString(String s) throws IOException {
		byte[] bytes = null;
		if (s != null)
			bytes = s.getBytes(SarConstants.DEFAULT_ENCODING);

		writeBytes(bytes);
	}

	/**
	 * @param bytes
	 * @throws IOException
	 */
	void writeBytes(byte[] bytes) throws IOException {
		writeBytes(bytes, bytes != null ? bytes.length : -1);
	}

	/**
	 * @param bytes
	 * @throws IOException
	 */
	void writeBytes(byte[] bytes, int length) throws IOException {
		if (bytes != null) {
			dataOutputStream.writeInt(length);
			dataOutputStream.write(bytes, 0, length);
		} else {
			dataOutputStream.writeInt(-1);
		}
	}

	/**
	 * @param v
	 * @throws IOException
	 */
	void writeInt(int v) throws IOException {
		dataOutputStream.writeInt(v);
	}

	/**
	 * @param bool
	 * @throws IOException
	 */
	public void writeBool(boolean bool) throws IOException {
		dataOutputStream.writeBoolean(bool);
	}

	/**
	 * @param v
	 * @throws IOException
	 */
	void writeLong(long v) throws IOException {
		dataOutputStream.writeLong(v);
	}

	/**
	 * Writes a byte to the current org.eclipse.equinox.p2.sar archive entry.
	 *
	 * @param b the byte written.
	 * @throws IOException
	 *
	 * @see java.io.OutputStream#write(int)
	 */
	@Override
	public void write(int b) throws IOException {
		byte[] bytes = new byte[1];
		bytes[0] = (byte) b;
		entryContent.write(bytes);
	}

	/**
	 * Writes bytes to the current org.eclipse.equinox.p2.sar archive entry.
	 *
	 * @param bytes The buffer to write to the archive.
	 * @throws IOException
	 *
	 * @see java.io.OutputStream#write(byte[])
	 */
	@Override
	public void write(byte[] bytes) throws IOException {
		entryContent.write(bytes, 0, bytes.length);
	}

	/**
	 * Writes bytes to the current org.eclipse.equinox.p2.sar archive entry.
	 *
	 * @param bytes      The buffer to write to the archive.
	 * @param offset     The offset in the buffer from which to get bytes.
	 * @param numToWrite The number of bytes to write.
	 *
	 * @throws IOException
	 *
	 * @see java.io.OutputStream#write(byte[], int, int)
	 */
	@Override
	public void write(byte[] bytes, int offset, int numToWrite) throws IOException {
		entryContent.write(bytes, offset, numToWrite);
	}

	/**
	 * Write an EOF (end of archive) entry to the org.eclipse.equinox.p2.sar
	 * archive.
	 *
	 * @throws IOException
	 */
	private void writeEOFRecord() throws IOException {
		SarEntry eofEntry = new SarEntry();
		eofEntry.writeTo(this);
	}

}
