/*
 * Copyright (C) 2010, Google Inc. 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.notes;

import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH;
import static org.eclipse.jgit.lib.FileMode.REGULAR_FILE;

import java.io.IOException;
import java.util.Iterator;
import java.util.NoSuchElementException;

import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectInserter.Formatter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.TreeFormatter;

/**
 * A note tree holding only notes, with no subtrees.
 *
 * The leaf bucket contains on average less than 256 notes, all of whom share
 * the same leading prefix. If a notes branch has less than 256 notes, the top
 * level tree of the branch should be a LeafBucket. Once a notes branch has more
 * than 256 notes, the root should be a {@link FanoutBucket} and the LeafBucket
 * will appear only as a cell of a FanoutBucket.
 *
 * Entries within the LeafBucket are stored sorted by ObjectId, and lookup is
 * performed using binary search. As the entry list should contain fewer than
 * 256 elements, the average number of compares to find an element should be
 * less than 8 due to the O(log N) lookup behavior.
 *
 * A LeafBucket must be parsed from a tree object by {@link NoteParser}.
 */
class LeafBucket extends InMemoryNoteBucket {
	static final int MAX_SIZE = 256;

	/** All note blobs in this bucket, sorted sequentially. */
	private Note[] notes;

	/** Number of items in {@link #notes}. */
	private int cnt;

	LeafBucket(int prefixLen) {
		super(prefixLen);
		notes = new Note[4];
	}

	private int search(AnyObjectId objId) {
		int low = 0;
		int high = cnt;
		while (low < high) {
			int mid = (low + high) >>> 1;
			int cmp = objId.compareTo(notes[mid]);
			if (cmp < 0)
				high = mid;
			else if (cmp == 0)
				return mid;
			else
				low = mid + 1;
		}
		return -(low + 1);
	}

	@Override
	Note getNote(AnyObjectId objId, ObjectReader or) {
		int idx = search(objId);
		return 0 <= idx ? notes[idx] : null;
	}

	Note get(int index) {
		return notes[index];
	}

	int size() {
		return cnt;
	}

	@Override
	Iterator<Note> iterator(AnyObjectId objId, ObjectReader reader) {
		return new Iterator<Note>() {
			private int idx;

			@Override
			public boolean hasNext() {
				return idx < cnt;
			}

			@Override
			public Note next() {
				if (hasNext()) {
					return notes[idx++];
				}
				throw new NoSuchElementException();
			}

			@Override
			public void remove() {
				throw new UnsupportedOperationException();
			}
		};
	}

	@Override
	int estimateSize(AnyObjectId noteOn, ObjectReader or) throws IOException {
		return cnt;
	}

	@Override
	InMemoryNoteBucket set(AnyObjectId noteOn, AnyObjectId noteData,
			ObjectReader or) throws IOException {
		int p = search(noteOn);
		if (0 <= p) {
			if (noteData != null) {
				notes[p].setData(noteData.copy());
				return this;

			}
			System.arraycopy(notes, p + 1, notes, p, cnt - p - 1);
			cnt--;
			return 0 < cnt ? this : null;

		} else if (noteData != null) {
			if (shouldSplit()) {
				return split().set(noteOn, noteData, or);
			}
			growIfFull();
			p = -(p + 1);
			if (p < cnt) {
				System.arraycopy(notes, p, notes, p + 1, cnt - p);
			}
			notes[p] = new Note(noteOn, noteData.copy());
			cnt++;
			return this;

		} else {
			return this;
		}
	}

	@Override
	ObjectId writeTree(ObjectInserter inserter) throws IOException {
		return inserter.insert(build());
	}

	@Override
	ObjectId getTreeId() {
		try (Formatter f = new ObjectInserter.Formatter()) {
			return f.idFor(build());
		}
	}

	private TreeFormatter build() {
		byte[] nameBuf = new byte[OBJECT_ID_STRING_LENGTH];
		int nameLen = OBJECT_ID_STRING_LENGTH - prefixLen;
		TreeFormatter fmt = new TreeFormatter(treeSize(nameLen));
		NonNoteEntry e = nonNotes;

		for (int i = 0; i < cnt; i++) {
			Note n = notes[i];

			n.copyTo(nameBuf, 0);

			while (e != null
					&& e.pathCompare(nameBuf, prefixLen, nameLen, REGULAR_FILE) < 0) {
				e.format(fmt);
				e = e.next;
			}

			fmt.append(nameBuf, prefixLen, nameLen, REGULAR_FILE, n.getData());
		}

		for (; e != null; e = e.next)
			e.format(fmt);
		return fmt;
	}

	private int treeSize(int nameLen) {
		int sz = cnt * TreeFormatter.entrySize(REGULAR_FILE, nameLen);
		for (NonNoteEntry e = nonNotes; e != null; e = e.next)
			sz += e.treeEntrySize();
		return sz;
	}

	void parseOneEntry(AnyObjectId noteOn, AnyObjectId noteData) {
		growIfFull();
		notes[cnt++] = new Note(noteOn, noteData.copy());
	}

	@Override
	InMemoryNoteBucket append(Note note) {
		if (shouldSplit()) {
			return split().append(note);
		}
		growIfFull();
		notes[cnt++] = note;
		return this;
	}

	private void growIfFull() {
		if (notes.length == cnt) {
			Note[] n = new Note[notes.length * 2];
			System.arraycopy(notes, 0, n, 0, cnt);
			notes = n;
		}
	}

	private boolean shouldSplit() {
		return MAX_SIZE <= cnt && prefixLen + 2 < OBJECT_ID_STRING_LENGTH;
	}

	FanoutBucket split() {
		FanoutBucket n = new FanoutBucket(prefixLen);
		for (int i = 0; i < cnt; i++)
			n.append(notes[i]);
		n.nonNotes = nonNotes;
		return n;
	}
}
