/*
 * Copyright (C) 2022, Google LLC 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.internal.storage.file;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;

import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.util.NB;

/**
 * Memory representation of the object-size index
 *
 * The object size index is a map from position in the primary idx (i.e.
 * position of the object-id in lexicographical order) to size.
 *
 * Most of the positions fit in unsigned 3 bytes (up to 16 million)
 */
class PackObjectSizeIndexV1 implements PackObjectSizeIndex {

	private static final byte BITS_24 = 0x18;

	private static final byte BITS_32 = 0x20;

	private final int threshold;

	private final UInt24Array positions24;

	private final int[] positions32;

	/**
	 * Parallel array to concat(positions24, positions32) with the size of the
	 * objects.
	 *
	 * A value >= 0 is the size of the object. A negative value means the size
	 * doesn't fit in an int and |value|-1 is the position for the size in the
	 * size64 array e.g. a value of -1 is sizes64[0], -2 = sizes64[1], ...
	 */
	private final int[] sizes32;

	private final long[] sizes64;

	static PackObjectSizeIndex parse(InputStream in) throws IOException {
		/** Header and version already out of the input */
		IndexInputStreamReader stream = new IndexInputStreamReader(in);
		int threshold = stream.readInt(); // minSize
		int objCount = stream.readInt();
		if (objCount == 0) {
			return new EmptyPackObjectSizeIndex(threshold);
		}
		return new PackObjectSizeIndexV1(stream, threshold, objCount);
	}

	private PackObjectSizeIndexV1(IndexInputStreamReader stream, int threshold,
			int objCount) throws IOException {
		this.threshold = threshold;
		UInt24Array pos24 = null;
		int[] pos32 = null;

		byte positionEncoding;
		while ((positionEncoding = stream.readByte()) != 0) {
			if (Byte.compareUnsigned(positionEncoding, BITS_24) == 0) {
				int sz = stream.readInt();
				pos24 = new UInt24Array(stream.readNBytes(sz * 3));
			} else if (Byte.compareUnsigned(positionEncoding, BITS_32) == 0) {
				int sz = stream.readInt();
				pos32 = stream.readIntArray(sz);
			} else {
				throw new UnsupportedEncodingException(
						String.format(JGitText.get().unknownPositionEncoding,
								Integer.toHexString(positionEncoding)));
			}
		}
		positions24 = pos24 != null ? pos24 : UInt24Array.EMPTY;
		positions32 = pos32 != null ? pos32 : new int[0];

		sizes32 = stream.readIntArray(objCount);
		int c64sizes = stream.readInt();
		if (c64sizes == 0) {
			sizes64 = new long[0];
			return;
		}
		sizes64 = stream.readLongArray(c64sizes);
		int c128sizes = stream.readInt();
		if (c128sizes != 0) {
			// this MUST be 0 (we don't support 128 bits sizes yet)
			throw new IOException(JGitText.get().unsupportedSizesObjSizeIndex);
		}
	}

	@Override
	public long getSize(int idxOffset) {
		int pos = -1;
		if (!positions24.isEmpty() && idxOffset <= positions24.getLastValue()) {
			pos = positions24.binarySearch(idxOffset);
		} else if (positions32.length > 0 && idxOffset >= positions32[0]) {
			int pos32 = Arrays.binarySearch(positions32, idxOffset);
			if (pos32 >= 0) {
				pos = pos32 + positions24.size();
			}
		}
		if (pos < 0) {
			return -1;
		}

		int objSize = sizes32[pos];
		if (objSize < 0) {
			int secondPos = Math.abs(objSize) - 1;
			return sizes64[secondPos];
		}
		return objSize;
	}

	@Override
	public long getObjectCount() {
		return positions24.size() + positions32.length;
	}

	@Override
	public int getThreshold() {
		return threshold;
	}

	/**
	 * Wrapper to read parsed content from the byte stream
	 */
	private static class IndexInputStreamReader {

		private final byte[] buffer = new byte[8];

		private final InputStream in;

		IndexInputStreamReader(InputStream in) {
			this.in = in;
		}

		int readInt() throws IOException {
			int n = in.readNBytes(buffer, 0, 4);
			if (n < 4) {
				throw new IOException(JGitText.get().unableToReadFullInt);
			}
			return NB.decodeInt32(buffer, 0);
		}

		int[] readIntArray(int intsCount) throws IOException {
			if (intsCount == 0) {
				return new int[0];
			}

			int[] dest = new int[intsCount];
			for (int i = 0; i < intsCount; i++) {
				dest[i] = readInt();
			}
			return dest;
		}

		long readLong() throws IOException {
			int n = in.readNBytes(buffer, 0, 8);
			if (n < 8) {
				throw new IOException(JGitText.get().unableToReadFullInt);
			}
			return NB.decodeInt64(buffer, 0);
		}

		long[] readLongArray(int longsCount) throws IOException {
			if (longsCount == 0) {
				return new long[0];
			}

			long[] dest = new long[longsCount];
			for (int i = 0; i < longsCount; i++) {
				dest[i] = readLong();
			}
			return dest;
		}

		byte readByte() throws IOException {
			int n = in.readNBytes(buffer, 0, 1);
			if (n != 1) {
				throw new IOException(JGitText.get().cannotReadByte);
			}
			return buffer[0];
		}

		byte[] readNBytes(int sz) throws IOException {
			return in.readNBytes(sz);
		}
	}

	private static class EmptyPackObjectSizeIndex
			implements PackObjectSizeIndex {

		private final int threshold;

		EmptyPackObjectSizeIndex(int threshold) {
			this.threshold = threshold;
		}

		@Override
		public long getSize(int idxOffset) {
			return -1;
		}

		@Override
		public long getObjectCount() {
			return 0;
		}

		@Override
		public int getThreshold() {
			return threshold;
		}
	}
}
