/*
 * Copyright (C) 2008-2009, Google Inc.
 * Copyright (C) 2008, Jonas Fonseca <fonseca@diku.dk>
 * Copyright (C) 2008, Marek Zawirski <marek.zawirski@gmail.com>
 * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com>
 * Copyright (C) 2006-2008, Shawn O. Pearce <spearce@spearce.org> and others
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0 which is available at
 * https://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

package org.eclipse.jgit.lib;

import java.io.EOFException;
import java.io.IOException;
import java.io.OutputStream;

import org.eclipse.jgit.errors.LargeObjectException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.util.IO;

/**
 * Base class for a set of loaders for different representations of Git objects.
 * New loaders are constructed for every object.
 */
public abstract class ObjectLoader {
	/**
	 * Get Git in pack object type
	 *
	 * @return Git in pack object type, see
	 *         {@link org.eclipse.jgit.lib.Constants}.
	 */
	public abstract int getType();

	/**
	 * Get size of object in bytes
	 *
	 * @return size of object in bytes
	 */
	public abstract long getSize();

	/**
	 * Whether this object is too large to obtain as a byte array.
	 *
	 * @return true if this object is too large to obtain as a byte array.
	 *         Objects over a certain threshold should be accessed only by their
	 *         {@link #openStream()} to prevent overflowing the JVM heap.
	 */
	public boolean isLarge() {
		try {
			getCachedBytes();
			return false;
		} catch (LargeObjectException tooBig) {
			return true;
		}
	}

	/**
	 * Obtain a copy of the bytes of this object.
	 * <p>
	 * Unlike {@link #getCachedBytes()} this method returns an array that might
	 * be modified by the caller.
	 *
	 * @return the bytes of this object.
	 * @throws org.eclipse.jgit.errors.LargeObjectException
	 *             if the object won't fit into a byte array, because
	 *             {@link #isLarge()} returns true. Callers should use
	 *             {@link #openStream()} instead to access the contents.
	 */
	public final byte[] getBytes() throws LargeObjectException {
		return cloneArray(getCachedBytes());
	}

	/**
	 * Obtain a copy of the bytes of this object.
	 *
	 * If the object size is less than or equal to {@code sizeLimit} this method
	 * will provide it as a byte array, even if {@link #isLarge()} is true. This
	 * utility is useful for application code that absolutely must have the
	 * object as a single contiguous byte array in memory.
	 *
	 * Unlike {@link #getCachedBytes(int)} this method returns an array that
	 * might be modified by the caller.
	 *
	 * @param sizeLimit
	 *            maximum number of bytes to return. If the object is larger
	 *            than this limit,
	 *            {@link org.eclipse.jgit.errors.LargeObjectException} will be
	 *            thrown.
	 * @return the bytes of this object.
	 * @throws org.eclipse.jgit.errors.LargeObjectException
	 *             if the object is bigger than {@code sizeLimit}, or if
	 *             {@link java.lang.OutOfMemoryError} occurs during allocation
	 *             of the result array. Callers should use {@link #openStream()}
	 *             instead to access the contents.
	 * @throws org.eclipse.jgit.errors.MissingObjectException
	 *             the object is large, and it no longer exists.
	 * @throws java.io.IOException
	 *             the object store cannot be accessed.
	 */
	public final byte[] getBytes(int sizeLimit) throws LargeObjectException,
			MissingObjectException, IOException {
		byte[] cached = getCachedBytes(sizeLimit);
		try {
			return cloneArray(cached);
		} catch (OutOfMemoryError tooBig) {
			throw new LargeObjectException.OutOfMemory(tooBig);
		}
	}

	/**
	 * Obtain a reference to the (possibly cached) bytes of this object.
	 * <p>
	 * This method offers direct access to the internal caches, potentially
	 * saving on data copies between the internal cache and higher level code.
	 * Callers who receive this reference <b>must not</b> modify its contents.
	 * Changes (if made) will affect the cache but not the repository itself.
	 *
	 * @return the cached bytes of this object. Do not modify it.
	 * @throws org.eclipse.jgit.errors.LargeObjectException
	 *             if the object won't fit into a byte array, because
	 *             {@link #isLarge()} returns true. Callers should use
	 *             {@link #openStream()} instead to access the contents.
	 */
	public abstract byte[] getCachedBytes() throws LargeObjectException;

	/**
	 * Obtain a reference to the (possibly cached) bytes of this object.
	 *
	 * If the object size is less than or equal to {@code sizeLimit} this method
	 * will provide it as a byte array, even if {@link #isLarge()} is true. This
	 * utility is useful for application code that absolutely must have the
	 * object as a single contiguous byte array in memory.
	 *
	 * This method offers direct access to the internal caches, potentially
	 * saving on data copies between the internal cache and higher level code.
	 * Callers who receive this reference <b>must not</b> modify its contents.
	 * Changes (if made) will affect the cache but not the repository itself.
	 *
	 * @param sizeLimit
	 *            maximum number of bytes to return. If the object size is
	 *            larger than this limit and {@link #isLarge()} is true,
	 *            {@link org.eclipse.jgit.errors.LargeObjectException} will be
	 *            thrown.
	 * @return the cached bytes of this object. Do not modify it.
	 * @throws org.eclipse.jgit.errors.LargeObjectException
	 *             if the object is bigger than {@code sizeLimit}, or if
	 *             {@link java.lang.OutOfMemoryError} occurs during allocation
	 *             of the result array. Callers should use {@link #openStream()}
	 *             instead to access the contents.
	 * @throws org.eclipse.jgit.errors.MissingObjectException
	 *             the object is large, and it no longer exists.
	 * @throws java.io.IOException
	 *             the object store cannot be accessed.
	 */
	public byte[] getCachedBytes(int sizeLimit) throws LargeObjectException,
			MissingObjectException, IOException {
		if (!isLarge())
			return getCachedBytes();

		try (ObjectStream in = openStream()) {
			long sz = in.getSize();
			if (sizeLimit < sz)
				throw new LargeObjectException.ExceedsLimit(sizeLimit, sz);

			if (Integer.MAX_VALUE < sz)
				throw new LargeObjectException.ExceedsByteArrayLimit();

			byte[] buf;
			try {
				buf = new byte[(int) sz];
			} catch (OutOfMemoryError notEnoughHeap) {
				throw new LargeObjectException.OutOfMemory(notEnoughHeap);
			}

			IO.readFully(in, buf, 0, buf.length);
			return buf;
		}
	}

	/**
	 * Obtain an input stream to read this object's data.
	 *
	 * @return a stream of this object's data. Caller must close the stream when
	 *         through with it. The returned stream is buffered with a
	 *         reasonable buffer size.
	 * @throws org.eclipse.jgit.errors.MissingObjectException
	 *             the object no longer exists.
	 * @throws java.io.IOException
	 *             the object store cannot be accessed.
	 */
	public abstract ObjectStream openStream() throws MissingObjectException,
			IOException;

	/**
	 * Copy this object to the output stream.
	 * <p>
	 * For some object store implementations, this method may be more efficient
	 * than reading from {@link #openStream()} into a temporary byte array, then
	 * writing to the destination stream.
	 * <p>
	 * The default implementation of this method is to copy with a temporary
	 * byte array for large objects, or to pass through the cached byte array
	 * for small objects.
	 *
	 * @param out
	 *            stream to receive the complete copy of this object's data.
	 *            Caller is responsible for flushing or closing this stream
	 *            after this method returns.
	 * @throws org.eclipse.jgit.errors.MissingObjectException
	 *             the object no longer exists.
	 * @throws java.io.IOException
	 *             the object store cannot be accessed, or the stream cannot be
	 *             written to.
	 */
	public void copyTo(OutputStream out) throws MissingObjectException,
			IOException {
		if (isLarge()) {
			try (ObjectStream in = openStream()) {
				final long sz = in.getSize();
				byte[] tmp = new byte[8192];
				long copied = 0;
				while (copied < sz) {
					int n = in.read(tmp);
					if (n < 0)
						throw new EOFException();
					out.write(tmp, 0, n);
					copied += n;
				}
				if (0 <= in.read())
					throw new EOFException();
			}
		} else {
			out.write(getCachedBytes());
		}
	}

	private static byte[] cloneArray(byte[] data) {
		final byte[] copy = new byte[data.length];
		System.arraycopy(data, 0, copy, 0, data.length);
		return copy;
	}

	/**
	 * Simple loader around the cached byte array.
	 * <p>
	 * ObjectReader implementations can use this stream type when the object's
	 * content is small enough to be accessed as a single byte array.
	 */
	public static class SmallObject extends ObjectLoader {
		private final int type;

		private final byte[] data;

		/**
		 * Construct a small object loader.
		 *
		 * @param type
		 *            type of the object.
		 * @param data
		 *            the object's data array. This array will be returned as-is
		 *            for the {@link #getCachedBytes()} method.
		 */
		public SmallObject(int type, byte[] data) {
			this.type = type;
			this.data = data;
		}

		@Override
		public int getType() {
			return type;
		}

		@Override
		public long getSize() {
			return getCachedBytes().length;
		}

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

		@Override
		public byte[] getCachedBytes() {
			return data;
		}

		@Override
		public ObjectStream openStream() {
			return new ObjectStream.SmallStream(this);
		}
	}

	/**
	 * Wraps a delegate ObjectLoader.
	 *
	 * @since 4.10
	 */
	public abstract static class Filter extends ObjectLoader {
		/**
		 * @return delegate ObjectLoader to handle all processing.
		 * @since 4.10
		 */
		protected abstract ObjectLoader delegate();

		@Override
		public int getType() {
			return delegate().getType();
		}

		@Override
		public long getSize() {
			return delegate().getSize();
		}

		@Override
		public boolean isLarge() {
			return delegate().isLarge();
		}

		@Override
		public byte[] getCachedBytes() {
			return delegate().getCachedBytes();
		}

		@Override
		public ObjectStream openStream() throws IOException {
			return delegate().openStream();
		}
	}
}
