/*
 * 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 static org.eclipse.jgit.lib.Constants.OBJECT_ID_LENGTH;

import java.io.DataInput;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.security.DigestInputStream;
import java.text.MessageFormat;
import java.util.Arrays;

import org.eclipse.jgit.errors.CorruptObjectException;
import org.eclipse.jgit.errors.PackMismatchException;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.internal.storage.pack.PackExt;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.util.Hex;
import org.eclipse.jgit.util.IO;

/**
 * Reverse index for forward pack index which is parsed from a version 1 file.
 * <p>
 * The file format is specified at
 * https://git-scm.com/docs/pack-format#_pack_rev_files_have_the_format.
 */
final class PackReverseIndexV1 implements PackReverseIndex {
	static final int OID_VERSION_SHA1 = 1;

	static final int OID_VERSION_SHA256 = 2;

	private static final int SHA1_BYTES = OBJECT_ID_LENGTH;

	private final DigestInputStream inputStream;

	private final DataInput dataIn;

	/**
	 * A lazy supplier for the corresponding PackIndex. The PackIndex is not
	 * needed during instantiation and parsing, only later when querying the
	 * reverse index. Allow lazy loading so that the parsing of the forward and
	 * reverse indices could happen in parallel.
	 */
	private final PackBitmapIndex.SupplierWithIOException<PackIndex> packIndexSupplier;

	private int objectCount;

	private byte[] packChecksum;

	private int[] indexPositionsSortedByOffset;

	private PackIndex packIndex;

	PackReverseIndexV1(DigestInputStream inputStream, long objectCount,
			PackBitmapIndex.SupplierWithIOException<PackIndex> packIndexSupplier)
			throws IOException {
		try {
			this.objectCount = Math.toIntExact(objectCount);
		} catch (ArithmeticException e) {
			throw new IllegalArgumentException(
					JGitText.get().hugeIndexesAreNotSupportedByJgitYet);
		}

		this.inputStream = inputStream;
		dataIn = new SimpleDataInput(inputStream);

		int oid_version = dataIn.readInt();
		switch (oid_version) {
		case OID_VERSION_SHA1:
			// JGit Pack only supports AnyObjectId, which represents SHA1.
			break;
		case OID_VERSION_SHA256:
			throw new IOException(MessageFormat.format(
					JGitText.get().unsupportedObjectIdVersion, "SHA256")); //$NON-NLS-1$
		default:
			throw new IOException(MessageFormat.format(
					JGitText.get().unsupportedObjectIdVersion,
					String.valueOf(oid_version)));
		}

		indexPositionsSortedByOffset = new int[this.objectCount];
		this.packIndexSupplier = packIndexSupplier;

		parseBody();
		parseChecksums();
	}

	@Override
	public void verifyPackChecksum(String packFilePath)
			throws PackMismatchException {
		if (!Arrays.equals(packChecksum, getPackIndex().getChecksum())) {
			throw new PackMismatchException(
					MessageFormat.format(JGitText.get().packChecksumMismatch,
							packFilePath, PackExt.INDEX.getExtension(),
							Hex.toHexString(getPackIndex().getChecksum()),
							PackExt.REVERSE_INDEX.getExtension(),
							Hex.toHexString(packChecksum)));
		}
	}

		private void parseBody() throws IOException {
		for (int i = 0; i < objectCount; i++) {
			indexPositionsSortedByOffset[i] = dataIn.readInt();
		}
	}

	private void parseChecksums() throws IOException {
		packChecksum = new byte[SHA1_BYTES];
		IO.readFully(inputStream, packChecksum);

		// Take digest before reading the self checksum changes it.
		byte[] observedSelfChecksum = inputStream.getMessageDigest().digest();

		byte[] readSelfChecksum = new byte[SHA1_BYTES];
		IO.readFully(inputStream, readSelfChecksum);

		if (!Arrays.equals(readSelfChecksum, observedSelfChecksum)) {
			throw new CorruptObjectException(MessageFormat.format(
					JGitText.get().corruptReverseIndexChecksumIncorrect,
					Hex.toHexString(readSelfChecksum),
					Hex.toHexString(observedSelfChecksum)));
		}
	}

	@Override
	public ObjectId findObject(long offset) {
		int reversePosition = findPosition(offset);
		if (reversePosition < 0) {
			return null;
		}
		int forwardPosition = findForwardPositionByReversePosition(
				reversePosition);
		return getPackIndex().getObjectId(forwardPosition);
	}

	@Override
	public long findNextOffset(long offset, long maxOffset)
			throws CorruptObjectException {
		int position = findPosition(offset);
		if (position < 0) {
			throw new CorruptObjectException(MessageFormat.format(JGitText
					.get().cantFindObjectInReversePackIndexForTheSpecifiedOffset,
					Long.valueOf(offset)));
		}
		if (position + 1 == objectCount) {
			return maxOffset;
		}
		return findOffsetByReversePosition(position + 1);
	}

	@Override
	public int findPosition(long offset) {
		return binarySearchByOffset(offset);
	}

	@Override
	public ObjectId findObjectByPosition(int position) {
		return getPackIndex()
				.getObjectId(findForwardPositionByReversePosition(position));
	}

	private long findOffsetByReversePosition(int position) {
		return getPackIndex()
				.getOffset(findForwardPositionByReversePosition(position));
	}

	private int findForwardPositionByReversePosition(int reversePosition) {
		assert (reversePosition >= 0);
		assert (reversePosition < indexPositionsSortedByOffset.length);
		return indexPositionsSortedByOffset[reversePosition];
	}

	private int binarySearchByOffset(long wantedOffset) {
		int low = 0;
		int high = objectCount - 1;
		while (low <= high) {
			int mid = (low + high) >>> 1;
			long offsetAtMid = findOffsetByReversePosition(mid);
			if (offsetAtMid == wantedOffset) {
				return mid;
			} else if (offsetAtMid > wantedOffset) {
				high = mid - 1;
			} else {
				low = mid + 1;
			}
		}
		return -1;
	}

	private PackIndex getPackIndex() {
		if (packIndex == null) {
			try {
				packIndex = packIndexSupplier.get();
			} catch (IOException e) {
				throw new UncheckedIOException(e);
			}
		}
		return packIndex;
	}
}
