/*
 * Copyright (C) 2021, Matthias Sohn <matthias.sohn@sap.com> 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.memory;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.eclipse.jgit.annotations.Nullable;
import org.eclipse.jgit.internal.JGitText;
import org.eclipse.jgit.util.StringUtils;

/**
 * A ternary search tree with String keys and generic values.
 *
 * TernarySearchTree is a type of trie (sometimes called a prefix tree) where
 * nodes are arranged in a manner similar to a binary search tree, but with up
 * to three children rather than the binary tree's limit of two. Like other
 * prefix trees, a ternary search tree can be used as an associative map
 * structure with the ability for incremental string search. However, ternary
 * search trees are more space efficient compared to standard prefix trees, at
 * the cost of speed.
 *
 * Keys must not be null or empty. Values cannot be null.
 *
 * This class is thread safe.
 *
 * @param <Value>
 *            type of values in this tree
 * @since 6.5
 */
public final class TernarySearchTree<Value> {

	private static final char WILDCARD = '?';

	private static class Node<Value> {
		final char c;

		Node<Value> lo, eq, hi;

		Value val;

		Node(char c) {
			this.c = c;
		}

		boolean hasValue() {
			return val != null;
		}
	}

	/**
	 * Loader to load key-value pairs to be cached in the tree
	 *
	 * @param <Value>
	 *            type of values
	 */
	public static interface Loader<Value> {
		/**
		 * Load map of all key value pairs
		 *
		 * @return map of all key value pairs to cache in the tree
		 */
		Map<String, Value> loadAll();
	}

	private static void validateKey(String key) {
		if (StringUtils.isEmptyOrNull(key)) {
			throw new IllegalArgumentException(
					JGitText.get().illegalTernarySearchTreeKey);
		}
	}

	private static <V> void validateValue(V value) {
		if (value == null) {
			throw new IllegalArgumentException(
					JGitText.get().illegalTernarySearchTreeValue);
		}
	}

	private final ReadWriteLock lock;

	private final AtomicInteger size = new AtomicInteger(0);

	private Node<Value> root;

	/**
	 * Construct a new ternary search tree
	 */
	public TernarySearchTree() {
		lock = new ReentrantReadWriteLock();
	}

	/**
	 * Get the lock guarding read and write access to the cache.
	 *
	 * @return lock guarding read and write access to the cache
	 */
	public ReadWriteLock getLock() {
		return lock;
	}

	/**
	 * Replace the tree with a new tree containing all entries provided by an
	 * iterable
	 *
	 * @param loader
	 *            iterable providing key-value pairs to load
	 * @return number of key-value pairs after replacing finished
	 */
	public int replace(Iterable<Entry<String, Value>> loader) {
		lock.writeLock().lock();
		try {
			clear();
			for (Entry<String, Value> e : loader) {
				insertImpl(e.getKey(), e.getValue());
			}
		} finally {
			lock.writeLock().unlock();
		}
		return size.get();
	}

	/**
	 * Reload the tree entries provided by loader
	 *
	 * @param loader
	 *            iterable providing key-value pairs to load
	 * @return number of key-value pairs
	 */
	public int reload(Iterable<Entry<String, Value>> loader) {
		lock.writeLock().lock();
		try {
			for (Entry<String, Value> e : loader) {
				insertImpl(e.getKey(), e.getValue());
			}
		} finally {
			lock.writeLock().unlock();
		}
		return size.get();
	}

	/**
	 * Delete entries
	 *
	 * @param delete
	 *            iterable providing keys of entries to be deleted
	 * @return number of key-value pairs
	 */
	public int delete(Iterable<String> delete) {
		lock.writeLock().lock();
		try {
			for (String s : delete) {
				delete(s);
			}
		} finally {
			lock.writeLock().unlock();
		}
		return size.get();
	}

	/**
	 * Get the number of key value pairs in this trie
	 *
	 * @return number of key value pairs in this trie
	 */
	public int size() {
		return size.get();
	}

	/**
	 * Get the value associated to a key or {@code null}.
	 *
	 * @param key
	 *            the key
	 * @return the value associated to this key
	 */
	@Nullable
	public Value get(String key) {
		validateKey(key);
		lock.readLock().lock();
		try {
			Node<Value> node = get(root, key, 0);
			if (node == null) {
				return null;
			}
			return node.val;
		} finally {
			lock.readLock().unlock();
		}
	}

	/**
	 * Check whether this tree contains the given key.
	 *
	 * @param key
	 *            a key
	 * @return whether this tree contains this key
	 */
	public boolean contains(String key) {
		return get(key) != null;
	}

	/**
	 * Insert a key-value pair. If the key already exists the old value is
	 * overwritten.
	 *
	 * @param key
	 *            the key
	 * @param value
	 *            the value
	 * @return number of key-value pairs after adding the entry
	 */
	public int insert(String key, Value value) {
		lock.writeLock().lock();
		try {
			insertImpl(key, value);
			return size.get();
		} finally {
			lock.writeLock().unlock();
		}
	}

	/**
	 * Insert map of key-value pairs. Values of existing keys are overwritten.
	 * Use this method to insert multiple key-value pairs.
	 *
	 * @param map
	 *            map of key-value pairs to insert
	 * @return number of key-value pairs after adding entries
	 */
	public int insert(Map<String, Value> map) {
		lock.writeLock().lock();
		try {
			for (Entry<String, Value> e : map.entrySet()) {
				insertImpl(e.getKey(), e.getValue());
			}
			return size.get();
		} finally {
			lock.writeLock().unlock();
		}
	}

	private void insertImpl(String key, Value value) {
		validateValue(value);
		if (!contains(key)) {
			size.addAndGet(1);
		}
		root = insert(root, key, value, 0);
	}

	/**
	 * Delete a key-value pair. Does nothing if the key doesn't exist.
	 *
	 * @param key
	 *            the key
	 * @return number of key-value pairs after the deletion
	 */
	public int delete(String key) {
		lock.writeLock().lock();
		try {
			if (contains(key)) {
				size.addAndGet(-1);
				root = insert(root, key, null, 0);
			}
			return size.get();
		} finally {
			lock.writeLock().unlock();
		}
	}

	/**
	 * Remove all key value pairs from this tree
	 */
	public void clear() {
		lock.writeLock().lock();
		try {
			size.set(0);
			root = null;
		} finally {
			lock.writeLock().unlock();
		}
	}

	/**
	 * Find the key which is the longest prefix of the given query string.
	 *
	 * @param query
	 *            the query string
	 * @return the key which is the longest prefix of the given query string or
	 *         {@code null} if none exists.
	 */
	@Nullable
	public String keyLongestPrefixOf(String query) {
		if (StringUtils.isEmptyOrNull(query)) {
			return null;
		}
		lock.readLock().lock();
		try {
			int length = 0;
			Node<Value> node = root;
			int i = 0;
			while (node != null && i < query.length()) {
				char c = query.charAt(i);
				if (node.c > c) {
					node = node.lo;
				} else if (node.c < c) {
					node = node.hi;
				} else {
					i++;
					if (node.hasValue()) {
						length = i;
					}
					node = node.eq;
				}
			}
			return query.substring(0, length);
		} finally {
			lock.readLock().unlock();
		}
	}

	/**
	 * Get all keys.
	 *
	 * @return all keys
	 */
	public Iterable<String> getKeys() {
		Queue<String> queue = new LinkedList<>();
		lock.readLock().lock();
		try {
			findKeysWithPrefix(root, new StringBuilder(), queue);
			return queue;
		} finally {
			lock.readLock().unlock();
		}
	}

	/**
	 * Get keys starting with given prefix
	 *
	 * @param prefix
	 *            key prefix
	 * @return keys starting with given prefix
	 */
	public Iterable<String> getKeysWithPrefix(String prefix) {
		Queue<String> keys = new LinkedList<>();
		if (prefix == null) {
			return keys;
		}
		if (prefix.isEmpty()) {
			return getKeys();
		}
		lock.readLock().lock();
		try {
			validateKey(prefix);
			Node<Value> node = get(root, prefix, 0);
			if (node == null) {
				return keys;
			}
			if (node.hasValue()) {
				keys.add(prefix);
			}
			findKeysWithPrefix(node.eq, new StringBuilder(prefix), keys);
			return keys;
		} finally {
			lock.readLock().unlock();
		}
	}

	/**
	 * Get all entries.
	 *
	 * @return all entries
	 */
	public Map<String, Value> getAll() {
		Map<String, Value> entries = new HashMap<>();
		lock.readLock().lock();
		try {
			findWithPrefix(root, new StringBuilder(), entries);
			return entries;
		} finally {
			lock.readLock().unlock();
		}
	}

	/**
	 * Get all values.
	 *
	 * @return all values
	 */
	public List<Value> getAllValues() {
		List<Value> values = new ArrayList<>();
		lock.readLock().lock();
		try {
			findValuesWithPrefix(root, new StringBuilder(), values);
			return values;
		} finally {
			lock.readLock().unlock();
		}
	}

	/**
	 * Get all entries with given prefix
	 *
	 * @param prefix
	 *            key prefix
	 * @return entries with given prefix
	 */
	public Map<String, Value> getWithPrefix(String prefix) {
		Map<String, Value> entries = new HashMap<>();
		if (prefix == null) {
			return entries;
		}
		if (prefix.isEmpty()) {
			return getAll();
		}
		lock.readLock().lock();
		try {
			validateKey(prefix);
			Node<Value> node = get(root, prefix, 0);
			if (node == null) {
				return entries;
			}
			if (node.hasValue()) {
				entries.put(prefix, node.val);
			}
			findWithPrefix(node.eq, new StringBuilder(prefix), entries);
			return entries;
		} finally {
			lock.readLock().unlock();
		}
	}

	/**
	 * Get all values with given key prefix
	 *
	 * @param prefix
	 *            key prefix
	 * @return entries with given prefix
	 */
	public List<Value> getValuesWithPrefix(String prefix) {
		List<Value> values = new ArrayList<>();
		if (prefix == null) {
			return values;
		}
		if (prefix.isEmpty()) {
			return getAllValues();
		}
		lock.readLock().lock();
		try {
			validateKey(prefix);
			Node<Value> node = get(root, prefix, 0);
			if (node == null) {
				return values;
			}
			if (node.hasValue()) {
				values.add(node.val);
			}
			findValuesWithPrefix(node.eq, new StringBuilder(prefix), values);
			return values;
		} finally {
			lock.readLock().unlock();
		}
	}

	/**
	 * Get keys matching given pattern using '?' as wildcard character.
	 *
	 * @param pattern
	 *            search pattern
	 * @return keys matching given pattern.
	 */
	public Iterable<String> getKeysMatching(String pattern) {
		Queue<String> keys = new LinkedList<>();
		lock.readLock().lock();
		try {
			findKeysWithPrefix(root, new StringBuilder(), 0, pattern, keys);
			return keys;
		} finally {
			lock.readLock().unlock();
		}
	}

	private Node<Value> get(Node<Value> node, String key, int depth) {
		if (node == null) {
			return null;
		}
		char c = key.charAt(depth);
		if (node.c > c) {
			return get(node.lo, key, depth);
		} else if (node.c < c) {
			return get(node.hi, key, depth);
		} else if (depth < key.length() - 1) {
			return get(node.eq, key, depth + 1);
		} else {
			return node;
		}
	}

	private Node<Value> insert(Node<Value> node, String key, Value val,
			int depth) {
		char c = key.charAt(depth);
		if (node == null) {
			node = new Node<>(c);
		}
		if (node.c > c) {
			node.lo = insert(node.lo, key, val, depth);
		} else if (node.c < c) {
			node.hi = insert(node.hi, key, val, depth);
		} else if (depth < key.length() - 1) {
			node.eq = insert(node.eq, key, val, depth + 1);
		} else {
			node.val = val;
		}
		return node;
	}

	private void findKeysWithPrefix(Node<Value> node, StringBuilder prefix,
			Queue<String> keys) {
		if (node == null) {
			return;
		}
		findKeysWithPrefix(node.lo, prefix, keys);
		if (node.hasValue()) {
			keys.add(prefix.toString() + node.c);
		}
		findKeysWithPrefix(node.eq, prefix.append(node.c), keys);
		prefix.deleteCharAt(prefix.length() - 1);
		findKeysWithPrefix(node.hi, prefix, keys);
	}

	private void findWithPrefix(Node<Value> node, StringBuilder prefix,
			Map<String, Value> entries) {
		if (node == null) {
			return;
		}
		findWithPrefix(node.lo, prefix, entries);
		if (node.hasValue()) {
			entries.put(prefix.toString() + node.c, node.val);
		}
		findWithPrefix(node.eq, prefix.append(node.c), entries);
		prefix.deleteCharAt(prefix.length() - 1);
		findWithPrefix(node.hi, prefix, entries);
	}

	private void findValuesWithPrefix(Node<Value> node, StringBuilder prefix,
			List<Value> values) {
		if (node == null) {
			return;
		}
		findValuesWithPrefix(node.lo, prefix, values);
		if (node.hasValue()) {
			values.add(node.val);
		}
		findValuesWithPrefix(node.eq, prefix.append(node.c), values);
		prefix.deleteCharAt(prefix.length() - 1);
		findValuesWithPrefix(node.hi, prefix, values);
	}

	private void findKeysWithPrefix(Node<Value> node, StringBuilder prefix,
			int i, String pattern, Queue<String> keys) {
		if (node == null || StringUtils.isEmptyOrNull(pattern)) {
			return;
		}
		char c = pattern.charAt(i);
		if (c == WILDCARD || node.c > c) {
			findKeysWithPrefix(node.lo, prefix, i, pattern, keys);
		}
		if (c == WILDCARD || node.c == c) {
			if (i == pattern.length() - 1 && node.hasValue()) {
				keys.add(prefix.toString() + node.c);
			}
			if (i < pattern.length() - 1) {
				findKeysWithPrefix(node.eq, prefix.append(node.c), i + 1,
						pattern, keys);
				prefix.deleteCharAt(prefix.length() - 1);
			}
		}
		if (c == WILDCARD || node.c < c) {
			findKeysWithPrefix(node.hi, prefix, i, pattern, keys);
		}
	}
}
