/* ###
 * IP: GHIDRA
 *
 * 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 ghidra.app.util.bin;

import java.io.*;

import ghidra.util.Msg;

/**
 * Instances of this class support both reading and writing to a
 * random access file. A random access file behaves like a large
 * array of bytes stored in the file system. There is a kind of cursor,
 * or index into the implied array, called the <em>file pointer</em>.
 * This implementation relies on java.net.RandomAccessFile,
 * but adds buffering to limit the amount.
 */
public class GhidraRandomAccessFile implements AutoCloseable {
	private static final byte[] EMPTY = new byte[0];
	private static final int BUFFER_SIZE = 0x100000;

	private File file;
	private RandomAccessFile randomAccessFile;
	private byte[] buffer = EMPTY;
	private long bufferOffset = 0;
	private long bufferFileStartIndex = 0;
	private byte[] lastbuffer = EMPTY;
	private long lastbufferOffset = 0;
	private long lastbufferFileStartIndex = 0;
	private boolean open = false;

	private void checkOpen() throws IOException {
		if (!open) {
			throw new IOException("GhidraRandomAccessFile is closed");
		}
	}

	/**
	 * Creates a random access file stream to read from, and optionally to
	 * write to, the file specified by the {@link File} argument.  A new {@link
	 * FileDescriptor} object is created to represent this file connection.
	 *
	 * <p>
	 * This implementation relies on java.net.RandomAccessFile,
	 * but adds buffering to limit the amount.
	 *
	 * <a id="mode"></a><p> The <code>mode</code> argument specifies the access mode
	 * in which the file is to be opened.  The permitted values and their
	 * meanings are:
	 *
	 * <blockquote><table><caption style="visibility:hidden;font-size:0px">Access mode permitted values and meanings</caption>
	 * <tr><th><p style="text-align:left">Value</p></th><th><p style="text-align:left">Meaning</p></th></tr>
	 * <tr><td style="vertical-align:top"><code>"r"</code></td>
	 *     <td> Open for reading only.  Invoking any of the <code>write</code>
	 *     methods of the resulting object will cause an {@link
	 *     java.io.IOException} to be thrown. </td></tr>
	 * <tr><td style="vertical-align:top"><code>"rw"</code></td>
	 *     <td> Open for reading and writing.  If the file does not already
	 *     exist then an attempt will be made to create it. </td></tr>
	 * <tr><td style="vertical-align:top"><code>"rws"</code></td>
	 *     <td> Open for reading and writing, as with <code>"rw"</code>, and also
	 *     require that every update to the file's content or metadata be
	 *     written synchronously to the underlying storage device.  </td></tr>
	 * <tr><td style="vertical-align:top"><code>"rwd"&nbsp;&nbsp;</code></td>
	 *     <td> Open for reading and writing, as with <code>"rw"</code>, and also
	 *     require that every update to the file's content be written
	 *     synchronously to the underlying storage device. </td></tr>
	 * </table></blockquote>
	 *
	 * @param      file   the file object
	 * @param      mode   the access mode, as described
	 *                    <a href="#mode">above</a>
	 * @exception  IllegalArgumentException  if the mode argument is not equal
	 *               to one of <code>"r"</code>, <code>"rw"</code>, <code>"rws"</code>, or
	 *               <code>"rwd"</code>
	 * @exception FileNotFoundException
	 *					that name cannot be created, or if some other error occurs
	 *            while opening or creating the file
	 */
	public GhidraRandomAccessFile(File file, String mode) throws FileNotFoundException {
		this.file = file;
		randomAccessFile = new RandomAccessFile(file, mode);
		this.open = true;
	}

	@Override
	protected void finalize() {
		if (open) {
			Msg.warn(this, "FAIL TO CLOSE " + file);
		}
	}

	/**
	 * Closes this random access file stream and releases any system
	 * resources associated with the stream. A closed random access
	 * file cannot perform input or output operations and cannot be
	 * reopened.
	 * <p>
	 * If this file has an associated channel then the channel is closed as well.
	 * @exception  IOException  if an I/O error occurs.
	 */
	@Override
	public void close() throws IOException {
		checkOpen();
		open = false;
		randomAccessFile.close();
		buffer = EMPTY;
		lastbuffer = EMPTY;
	}

	/**
	 * Returns the length of this file.
	 * @return     the length of this file, measured in bytes.
	 * @exception  IOException  if an I/O error occurs.
	 */
	public long length() throws IOException {
		checkOpen();
		return randomAccessFile.length();
	}

	/**
	 * Sets the file-pointer offset, measured from the beginning of this
	 * file, at which the next read or write occurs.  The offset may be
	 * set beyond the end of the file. Setting the offset beyond the end
	 * of the file does not change the file length.  The file length will
	 * change only by writing after the offset has been set beyond the end
	 * of the file.
	 * @param      pos   the offset position, measured in bytes from the
	 *                   beginning of the file, at which to set the file
	 *                   pointer.
	 * @throws  IOException  if <code>pos</code> is less than
	 *                          <code>0</code> or if an I/O error occurs.
	 */
	public void seek(long pos) throws IOException {
		checkOpen();

		if (pos < 0) {
			throw new IOException("pos cannot be less than zero");
		}

		if (pos < bufferFileStartIndex || pos >= bufferFileStartIndex + BUFFER_SIZE) {
			// check if the last buffer contained it, and swap in if necessary
			swapInLast();
			if (pos < bufferFileStartIndex || pos >= bufferFileStartIndex + BUFFER_SIZE) {
				// not in either, gotta get a new one
				buffer = EMPTY;
				bufferOffset = 0;
				bufferFileStartIndex = pos;
			}
		}
		bufferOffset = pos - bufferFileStartIndex;
	}

	/**
	 * This method reads a byte from the file, starting from the current file pointer.
	 * <p>
	 * This method blocks until the byte is read, the end of the stream
	 * is detected, or an exception is thrown.
	 *
	 * @return     the next byte of this file as a signed eight-bit
	 *             <code>byte</code>.
	 * @exception  EOFException  if this file has reached the end.
	 * @exception  IOException   if an I/O error occurs.
	 */
	public byte readByte() throws IOException {
		checkOpen();
		ensure(1);
		return buffer[(int) bufferOffset];
	}

	/**
	 * Reads up to <code>b.length</code> bytes of data from this file
	 * into an array of bytes. This method blocks until at least one byte
	 * of input is available.
	 *
	 * @param      b   the buffer into which the data is read.
	 * @return     the total number of bytes read into the buffer, or
	 *             <code>-1</code> if there is no more data because the end of
	 *             this file has been reached.
	 * @exception  IOException  if an I/O error occurs.
	 */
	public int read(byte[] b) throws IOException {
		checkOpen();
		return read(b, 0, b.length);
	}

	/**
	 * Reads up to <code>length</code> bytes of data from this file into an
	 * array of bytes. This method blocks until at least one byte of input
	 * is available.
	 *
	 * @param      b the buffer into which the data is read.
	 * @param      offset the start offset of the data.
	 * @param      length the maximum number of bytes read.
	 * @return     the total number of bytes read into the buffer, or
	 *             <code>-1</code> if there is no more data because the end of
	 *             the file has been reached.
	 * @exception  IOException  if an I/O error occurs.
	 */
	public int read(byte[] b, int offset, int length) throws IOException {
		checkOpen();
		int readLen = length;
		do {
			int blocklength = readLen;
			if (readLen > (BUFFER_SIZE - bufferOffset)) {
				blocklength = (BUFFER_SIZE - (int) bufferOffset);
				if (blocklength <= 0) {
					blocklength = BUFFER_SIZE;
				}
			}
			ensure(blocklength);
			System.arraycopy(buffer, (int) bufferOffset, b, offset, blocklength);
			readLen -= blocklength;
			offset += blocklength;
			if (readLen > 0) {
				seek(this.bufferFileStartIndex + bufferOffset + blocklength);
			}
		}
		while (readLen > 0);
		return length;
	}

	/**
	 * Writes a byte to this file, starting at the current file pointer.
	 * @param      b   the data.
	 * @exception  IOException  if an I/O error occurs.
	 */
	public void write(byte b) throws IOException {
		checkOpen();
		write(new byte[] { b }, 0, 1);
	}

	/**
	 * Writes <code>b.length</code> bytes from the specified byte array
	 * to this file, starting at the current file pointer.
	 * @param      b   the data.
	 * @exception  IOException  if an I/O error occurs.
	 */
	public void write(byte[] b) throws IOException {
		checkOpen();
		write(b, 0, b.length);
	}

	/**
	 * Writes a sub array as a sequence of bytes.
	 * @param b the data to be written
	 * @param offset the start offset in the data
	 * @param length the number of bytes that are written
	 * @exception IOException If an I/O error has occurred.
	 */
	public void write(byte[] b, int offset, int length) throws IOException {
		checkOpen();
		randomAccessFile.write(b, offset, length);
		buffer = EMPTY;
		bufferOffset = 0;
		lastbuffer = EMPTY;
		lastbufferOffset = 0;
	}

	/**
	 * Ensures that enough bytes are cached to
	 * satisfy the next request to read.
	 */
	private void ensure(int bytesNeeded) throws IOException {
		checkOpen();
		long oldFileStartIndex = bufferFileStartIndex;
		long oldBufferOffset = bufferOffset;
		long oldSeekPos = oldFileStartIndex + oldBufferOffset;

		if (bufferOffset + bytesNeeded > buffer.length) {
			// check if the last buffer contained it, and swap in if necessary
			swapInLast();
			// must ensure that current read pos is in old buffer, and enough bytes
			long newBufferOffset = (oldSeekPos - bufferFileStartIndex);
			if (oldSeekPos < bufferFileStartIndex ||
				oldSeekPos >= bufferFileStartIndex + BUFFER_SIZE ||
				(newBufferOffset + bytesNeeded > buffer.length)) {
				bufferFileStartIndex = oldFileStartIndex + oldBufferOffset;

				buffer = new byte[BUFFER_SIZE];
				randomAccessFile.seek(bufferFileStartIndex);
				int bytesRead = randomAccessFile.read(buffer);
				bufferOffset = 0;
				if (bytesRead <= 0) {
					throw new EOFException();
				}
			}
			else {
				bufferOffset = newBufferOffset;
			}
		}
	}

	private void swapInLast() throws IOException {
		checkOpen();
		if (buffer == EMPTY) {
			return;
		}
		// swap em and return
		byte[] swapbuffer = buffer;
		long swapbufferOffset = bufferOffset;
		long swapbufferFileStartIndex = bufferFileStartIndex;

		buffer = lastbuffer;
		bufferOffset = lastbufferOffset;
		bufferFileStartIndex = lastbufferFileStartIndex;

		lastbuffer = swapbuffer;
		lastbufferOffset = swapbufferOffset;
		lastbufferFileStartIndex = swapbufferFileStartIndex;
	}
}
