/*
 * Copyright (c) 2013, 2016 QNX Software Systems and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Andrew Eidsness - Initial implementation
 */

package org.aspectj.org.eclipse.jdt.internal.core.nd.db;

import java.util.HashMap;
import java.util.Map;

import org.eclipse.core.runtime.CoreException;

/**
 * A container for storing a set of strings in the Database. The container allows only one instance of each
 * string to be stored.
 * <p>
 * This implementation should only be used when the set is expected to be small. It uses a singly linked list
 * for storing strings in Database. Which means that a linear lookup is needed to find strings in the list. An
 * in-memory, lazily-loaded, cache is provided so the list will only be fully retrieved once in the lifetime
 * of this instance. A BTree will be more efficient for larger sets.
 */
public class NdStringSet {
	private final Database db;

	private long ptr;
	private long head;
	private long loaded;

	// A lazily initialized, in-memory cache that maps a persisted string to its storage record.
	private Map<String, Long> lazyCache;

	public NdStringSet(Database db, long ptr) throws CoreException {
		this.db = db;
		this.ptr = ptr;

		this.head = 0;
		this.loaded = 0;
	}

	public void clearCaches() {
		this.head = 0;
		this.loaded = 0;

		if (this.lazyCache != null)
			this.lazyCache = null;
	}

	private long getHead() throws CoreException {
		if (this.head == 0)
			this.head = this.db.getRecPtr(this.ptr);
		return this.head;
	}

	// A simple enum describing the type of the information that is stored in the Database. Each
	// enumerator represents a single field in the persistent structure and is able to answer its
	// offset in that structure.
	private static enum NodeType {
		Next, Item, _last;

		// NOTE: All fields are pointers, if that changes then these initializations will need
		// to be updated.
		public final long offset = ordinal() * Database.PTR_SIZE;
		public static final int sizeof = (int) _last.offset;

		/** Return the value of the pointer stored in this field in the given instance. */
		public long get(Database db, long instance) throws CoreException {
			return db.getRecPtr(instance + this.offset);
		}

		/** Store the given pointer into this field in the given instance. */
		public void put(Database db, long instance, long value) throws CoreException {
			db.putRecPtr(instance + this.offset, value);
		}
	}

	/**
	 * Adds the given string to the receiving set. May cause the entire list to be loaded from the Database
	 * while testing for uniqueness. Returns the record of the string that was inserted into the list.
	 */
	public long add(String str) throws CoreException {
		long record = find(str);
		if (record != 0)
			return record;

		IString string = this.db.newString(str);
		record = string.getRecord();

		long new_node = this.db.malloc(NodeType.sizeof, Database.POOL_STRING_SET);
		NodeType.Next.put(this.db, new_node, getHead());
		NodeType.Item.put(this.db, new_node, record);

		if (this.lazyCache == null)
			this.lazyCache = new HashMap<String, Long>();
		this.lazyCache.put(str, record);

		// If the Database has already been partially searched, then the loaded pointer will be after the
		// head. Since we've already put this new record into the lazy cache, there is no reason to try to
		// load it again. We put the new node at the start of the list so that it will be before the loaded
		// pointer.
		this.head = new_node;
		if (this.loaded == 0)
			this.loaded = new_node;
		this.db.putRecPtr(this.ptr, new_node);
		return record;
	}

	/**
	 * Search for the given string in the receiver. This could cause the entire list to be loaded from the
	 * Database. The results are cached, so the list will only be loaded one time during the lifetime of this
	 * instance. Returns the record of the String.
	 */
	public long find(String str) throws CoreException {
		if (this.lazyCache != null) {
			Long l = this.lazyCache.get(str);
			if (l != null)
				return l.longValue();
		}

		// if there is nothing in the Database, then there is nothing to load
		if (getHead() == 0)
			return 0;

		// otherwise prepare the cache for the data that is about to be loaded
		if (this.lazyCache == null)
			this.lazyCache = new HashMap<String, Long>();

		// if nothing has been loaded, then start loading with the head node, otherwise continue
		// loading from whatever is after the last loaded node
		long curr = this.loaded == 0 ? getHead() : NodeType.Next.get(this.db, this.loaded);
		while (curr != 0) {
			long next = NodeType.Next.get(this.db, curr);
			long item = NodeType.Item.get(this.db, curr);

			IString string = this.db.getString(item);

			// put the value into the cache
			this.lazyCache.put(string.getString(), Long.valueOf(item));

			// return immediately if this is the target
			if (string.compare(str, true) == 0)
				return item;

			// otherwise keep looking
			this.loaded = curr;
			curr = next;
		}

		return 0;
	}

	/**
	 * Return a pointer to the record of the String that was removed.
	 */
	public long remove(String str) throws CoreException {
		if (this.lazyCache != null)
			this.lazyCache.remove(str);

		long prev = 0;
		long curr = getHead();
		while (curr != 0) {
			long next = NodeType.Next.get(this.db, curr);
			long item = NodeType.Item.get(this.db, curr);

			IString string = this.db.getString(item);

			if (string.compare(str, true) == 0) {
				if (this.head != curr)
					NodeType.Next.put(this.db, prev, next);
				else {
					this.db.putRecPtr(this.ptr, next);
					this.head = next;
				}

				this.db.free(curr, Database.POOL_STRING_SET);
				return item;
			}

			prev = curr;
			curr = next;
		}

		return 0;
	}
}
