/*
 * Copyright (C) 2011, 2013 Google Inc., and others. 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.dfs;

import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
import static org.eclipse.jgit.internal.storage.pack.PackExt.REFTABLE;

import java.util.Arrays;
import java.util.Comparator;

import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.internal.storage.commitgraph.CommitGraphWriter;
import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource;
import org.eclipse.jgit.internal.storage.pack.PackExt;
import org.eclipse.jgit.internal.storage.reftable.ReftableWriter;
import org.eclipse.jgit.storage.pack.PackStatistics;

/**
 * Description of a DFS stored pack/index file.
 * <p>
 * Implementors may extend this class and add additional data members.
 * <p>
 * Instances of this class are cached with the DfsPackFile, and should not be
 * modified once initialized and presented to the JGit DFS library.
 */
public class DfsPackDescription {
	/**
	 * Comparator for packs when looking up objects in indexes.
	 * <p>
	 * This comparator tries to position packs in the order readers should examine
	 * them when looking for objects by SHA-1. The default tries to sort packs
	 * with more recent modification dates before older packs, and packs with
	 * fewer objects before packs with more objects.
	 * <p>
	 * Uses {@link PackSource#DEFAULT_COMPARATOR} for the portion of comparison
	 * where packs are sorted by source.
	 *
	 * @return comparator.
	 */
	public static Comparator<DfsPackDescription> objectLookupComparator() {
		return objectLookupComparator(PackSource.DEFAULT_COMPARATOR);
	}

	/**
	 * Comparator for packs when looking up objects in indexes.
	 * <p>
	 * This comparator tries to position packs in the order readers should examine
	 * them when looking for objects by SHA-1. The default tries to sort packs
	 * with more recent modification dates before older packs, and packs with
	 * fewer objects before packs with more objects.
	 *
	 * @param packSourceComparator
	 *            comparator for the {@link PackSource}, used as the first step in
	 *            comparison.
	 * @return comparator.
	 */
	public static Comparator<DfsPackDescription> objectLookupComparator(
			Comparator<PackSource> packSourceComparator) {
		return Comparator.comparing(
					DfsPackDescription::getPackSource, packSourceComparator)
			.thenComparing((a, b) -> {
				PackSource as = a.getPackSource();
				PackSource bs = b.getPackSource();

				// Tie break GC type packs by smallest first. There should be at most
				// one of each source, but when multiple exist concurrent GCs may have
				// run. Preferring the smaller file selects higher quality delta
				// compression, placing less demand on the DfsBlockCache.
				if (as == bs && isGC(as)) {
					int cmp = Long.signum(a.getFileSize(PACK) - b.getFileSize(PACK));
					if (cmp != 0) {
						return cmp;
					}
				}

				// Newer packs should sort first.
				int cmp = Long.signum(b.getLastModified() - a.getLastModified());
				if (cmp != 0) {
					return cmp;
				}

				// Break ties on smaller index. Readers may get lucky and find
				// the object they care about in the smaller index. This also pushes
				// big historical packs to the end of the list, due to more objects.
				return Long.signum(a.getObjectCount() - b.getObjectCount());
			});
	}

	static Comparator<DfsPackDescription> reftableComparator() {
		return (a, b) -> {
				// GC, COMPACT reftables first by reversing default order.
				int c = PackSource.DEFAULT_COMPARATOR.reversed()
						.compare(a.getPackSource(), b.getPackSource());
				if (c != 0) {
					return c;
				}

				// Lower maxUpdateIndex first.
				c = Long.signum(a.getMaxUpdateIndex() - b.getMaxUpdateIndex());
				if (c != 0) {
					return c;
				}

				// Older reftable first.
				return Long.signum(a.getLastModified() - b.getLastModified());
			};
	}

	static Comparator<DfsPackDescription> reuseComparator() {
		return (a, b) -> {
			PackSource as = a.getPackSource();
			PackSource bs = b.getPackSource();

			if (as == bs && DfsPackDescription.isGC(as)) {
				// Push smaller GC files last; these likely have higher quality
				// delta compression and the contained representation should be
				// favored over other files.
				return Long.signum(b.getFileSize(PACK) - a.getFileSize(PACK));
			}

			// DfsPackDescription.compareTo already did a reasonable sort.
			// Rely on Arrays.sort being stable, leaving equal elements.
			return 0;
		};
	}

	private final DfsRepositoryDescription repoDesc;
	private final String packName;
	private PackSource packSource;
	private long lastModified;
	private long[] sizeMap;
	private int[] blockSizeMap;
	private long objectCount;
	private long deltaCount;
	private long minUpdateIndex;
	private long maxUpdateIndex;

	private PackStatistics packStats;
	private ReftableWriter.Stats refStats;
	private CommitGraphWriter.Stats commitGraphStats;
	private int extensions;
	private int indexVersion;
	private long estimatedPackSize;

	/**
	 * Initialize a description by pack name and repository.
	 * <p>
	 * The corresponding index file is assumed to exist. If this is not true
	 * implementors must extend the class and override
	 * {@link #getFileName(PackExt)}.
	 * <p>
	 * Callers should also try to fill in other fields if they are reasonably
	 * free to access at the time this instance is being initialized.
	 *
	 * @param name
	 *            name of the pack file. Must end with ".pack".
	 * @param repoDesc
	 *            description of the repo containing the pack file.
	 * @param packSource
	 *            the source of the pack.
	 */
	public DfsPackDescription(DfsRepositoryDescription repoDesc, String name,
			@NonNull PackSource packSource) {
		this.repoDesc = repoDesc;
		int dot = name.lastIndexOf('.');
		this.packName = (dot < 0) ? name : name.substring(0, dot);
		this.packSource = packSource;

		int extCnt = PackExt.values().length;
		sizeMap = new long[extCnt];
		blockSizeMap = new int[extCnt];
	}

	/**
	 * Get description of the repository.
	 *
	 * @return description of the repository.
	 */
	public DfsRepositoryDescription getRepositoryDescription() {
		return repoDesc;
	}

	/**
	 * Adds the pack file extension to the known list.
	 *
	 * @param ext
	 *            the file extension
	 */
	public void addFileExt(PackExt ext) {
		extensions |= ext.getBit();
	}

	/**
	 * Whether the pack file extension is known to exist.
	 *
	 * @param ext
	 *            the file extension
	 * @return whether the pack file extension is known to exist.
	 */
	public boolean hasFileExt(PackExt ext) {
		return (extensions & ext.getBit()) != 0;
	}

	/**
	 * Get file name
	 *
	 * @param ext
	 *            the file extension
	 * @return name of the file.
	 */
	public String getFileName(PackExt ext) {
		return packName + '.' + ext.getExtension();
	}

	/**
	 * Get cache key for use by the block cache.
	 *
	 * @param ext
	 *            the file extension.
	 * @return cache key for use by the block cache.
	 */
	public DfsStreamKey getStreamKey(PackExt ext) {
		return DfsStreamKey.of(getRepositoryDescription(), getFileName(ext),
				ext);
	}

	/**
	 * Get the source of the pack.
	 *
	 * @return the source of the pack.
	 */
	@NonNull
	public PackSource getPackSource() {
		return packSource;
	}

	/**
	 * Set the source of the pack.
	 *
	 * @param source
	 *            the source of the pack.
	 * @return {@code this}
	 */
	public DfsPackDescription setPackSource(@NonNull PackSource source) {
		packSource = source;
		return this;
	}

	/**
	 * Get time the pack was created, in milliseconds.
	 *
	 * @return time the pack was created, in milliseconds.
	 */
	public long getLastModified() {
		return lastModified;
	}

	/**
	 * Set time the pack was created, in milliseconds.
	 *
	 * @param timeMillis
	 *            time the pack was created, in milliseconds. 0 if not known.
	 * @return {@code this}
	 */
	public DfsPackDescription setLastModified(long timeMillis) {
		lastModified = timeMillis;
		return this;
	}

	/**
	 * Get minUpdateIndex for the reftable, if present.
	 *
	 * @return minUpdateIndex for the reftable, if present.
	 */
	public long getMinUpdateIndex() {
		return minUpdateIndex;
	}

	/**
	 * Set minUpdateIndex for the reftable.
	 *
	 * @param min
	 *            minUpdateIndex for the reftable.
	 * @return {@code this}
	 */
	public DfsPackDescription setMinUpdateIndex(long min) {
		minUpdateIndex = min;
		return this;
	}

	/**
	 * Get maxUpdateIndex for the reftable, if present.
	 *
	 * @return maxUpdateIndex for the reftable, if present.
	 */
	public long getMaxUpdateIndex() {
		return maxUpdateIndex;
	}

	/**
	 * Set maxUpdateIndex for the reftable.
	 *
	 * @param max
	 *            maxUpdateIndex for the reftable.
	 * @return {@code this}
	 */
	public DfsPackDescription setMaxUpdateIndex(long max) {
		maxUpdateIndex = max;
		return this;
	}

	/**
	 * Set size of the file in bytes.
	 *
	 * @param ext
	 *            the file extension.
	 * @param bytes
	 *            size of the file in bytes. If 0 the file is not known and will
	 *            be determined on first read.
	 * @return {@code this}
	 */
	public DfsPackDescription setFileSize(PackExt ext, long bytes) {
		int i = ext.getPosition();
		if (i >= sizeMap.length) {
			sizeMap = Arrays.copyOf(sizeMap, i + 1);
		}
		sizeMap[i] = Math.max(0, bytes);
		return this;
	}

	/**
	 * Get size of the file, in bytes.
	 *
	 * @param ext
	 *            the file extension.
	 * @return size of the file, in bytes. If 0 the file size is not yet known.
	 */
	public long getFileSize(PackExt ext) {
		int i = ext.getPosition();
		return i < sizeMap.length ? sizeMap[i] : 0;
	}

	/**
	 * Get blockSize of the file, in bytes.
	 *
	 * @param ext
	 *            the file extension.
	 * @return blockSize of the file, in bytes. If 0 the blockSize size is not
	 *         yet known and may be discovered when opening the file.
	 */
	public int getBlockSize(PackExt ext) {
		int i = ext.getPosition();
		return i < blockSizeMap.length ? blockSizeMap[i] : 0;
	}

	/**
	 * Set blockSize of the file, in bytes.
	 *
	 * @param ext
	 *            the file extension.
	 * @param blockSize
	 *            blockSize of the file, in bytes. If 0 the blockSize is not
	 *            known and will be determined on first read.
	 * @return {@code this}
	 */
	public DfsPackDescription setBlockSize(PackExt ext, int blockSize) {
		int i = ext.getPosition();
		if (i >= blockSizeMap.length) {
			blockSizeMap = Arrays.copyOf(blockSizeMap, i + 1);
		}
		blockSizeMap[i] = Math.max(0, blockSize);
		return this;
	}

	/**
	 * Set estimated size of the .pack file in bytes.
	 *
	 * @param estimatedPackSize
	 *            estimated size of the .pack file in bytes. If 0 the pack file
	 *            size is unknown.
	 * @return {@code this}
	 */
	public DfsPackDescription setEstimatedPackSize(long estimatedPackSize) {
		this.estimatedPackSize = Math.max(0, estimatedPackSize);
		return this;
	}

	/**
	 * Get estimated size of the .pack file in bytes.
	 *
	 * @return estimated size of the .pack file in bytes. If 0 the pack file
	 *         size is unknown.
	 */
	public long getEstimatedPackSize() {
		return estimatedPackSize;
	}

	/**
	 * Get number of objects in the pack.
	 *
	 * @return number of objects in the pack.
	 */
	public long getObjectCount() {
		return objectCount;
	}

	/**
	 * Set number of objects in the pack.
	 *
	 * @param cnt
	 *            number of objects in the pack.
	 * @return {@code this}
	 */
	public DfsPackDescription setObjectCount(long cnt) {
		objectCount = Math.max(0, cnt);
		return this;
	}

	/**
	 * Get number of delta compressed objects in the pack.
	 *
	 * @return number of delta compressed objects in the pack.
	 */
	public long getDeltaCount() {
		return deltaCount;
	}

	/**
	 * Set number of delta compressed objects in the pack.
	 *
	 * @param cnt
	 *            number of delta compressed objects in the pack.
	 * @return {@code this}
	 */
	public DfsPackDescription setDeltaCount(long cnt) {
		deltaCount = Math.max(0, cnt);
		return this;
	}

	/**
	 * Get statistics from PackWriter, if the pack was built with it.
	 *
	 * @return statistics from PackWriter, if the pack was built with it.
	 *         Generally this is only available for packs created by
	 *         DfsGarbageCollector or DfsPackCompactor, and only when the pack
	 *         is being committed to the repository.
	 */
	public PackStatistics getPackStats() {
		return packStats;
	}

	DfsPackDescription setPackStats(PackStatistics stats) {
		this.packStats = stats;
		setFileSize(PACK, stats.getTotalBytes());
		setObjectCount(stats.getTotalObjects());
		setDeltaCount(stats.getTotalDeltas());
		return this;
	}

	/**
	 * Get stats from the sibling reftable, if created.
	 *
	 * @return stats from the sibling reftable, if created.
	 */
	public ReftableWriter.Stats getReftableStats() {
		return refStats;
	}

	void setReftableStats(ReftableWriter.Stats stats) {
		this.refStats = stats;
		setMinUpdateIndex(stats.minUpdateIndex());
		setMaxUpdateIndex(stats.maxUpdateIndex());
		setFileSize(REFTABLE, stats.totalBytes());
		setBlockSize(REFTABLE, stats.refBlockSize());
	}

	/**
	 * Get stats from the sibling commit graph, if created.
	 *
	 * @return stats from the sibling commit graph, if created.
	 */
	public CommitGraphWriter.Stats getCommitGraphStats() {
		return commitGraphStats;
	}

	void setCommitGraphStats(CommitGraphWriter.Stats stats) {
		this.commitGraphStats = stats;
	}

	/**
	 * Discard the pack statistics, if it was populated.
	 *
	 * @return {@code this}
	 */
	public DfsPackDescription clearPackStats() {
		packStats = null;
		refStats = null;
		return this;
	}

	/**
	 * Get the version of the index file written.
	 *
	 * @return the version of the index file written.
	 */
	public int getIndexVersion() {
		return indexVersion;
	}

	/**
	 * Set the version of the index file written.
	 *
	 * @param version
	 *            the version of the index file written.
	 * @return {@code this}
	 */
	public DfsPackDescription setIndexVersion(int version) {
		indexVersion = version;
		return this;
	}

	@Override
	public int hashCode() {
		return packName.hashCode();
	}

	@Override
	public boolean equals(Object b) {
		if (b instanceof DfsPackDescription) {
			DfsPackDescription desc = (DfsPackDescription) b;
			return packName.equals(desc.packName) &&
					getRepositoryDescription().equals(desc.getRepositoryDescription());
		}
		return false;
	}

	static boolean isGC(PackSource s) {
		switch (s) {
		case GC:
		case GC_REST:
			return true;
		default:
			return false;
		}
	}

	@Override
	public String toString() {
		return getFileName(PackExt.PACK);
	}
}
