package pp.s1184725.boppi.symboltable;

import java.util.*;
import java.util.function.Supplier;

import pp.s1184725.boppi.Messages;
import pp.s1184725.boppi.exception.*;
import pp.s1184725.boppi.type.*;

/**
 * This class maps string identifiers to {@link Variable} instances. It takes
 * care of lexical scope, memory offsets and variable lifetime. This
 * implementation has a theoretical lookup time of O(1), scope opening time of
 * O(1) and scope closing time of O(k log k log n) with k the number of
 * identifiers going out of scope. It is agnostic to the type system used.
 *
 * @param <T>
 *            the typing class
 *
 * @author Frank Wibbelink
 */
public class CachingSymbolTable<T extends Type> {
	protected Stack<Map<String, Variable<T>>> symbolMapStack;
	protected Map<String, Variable<T>> symbolCache;
	protected Stack<FunctionScope<T>> functionScope;

	/**
	 * Creates an empty symbol table with no open scope.
	 */
	public CachingSymbolTable() {
		symbolMapStack = new Stack<>();
		symbolCache = new HashMap<>();
		functionScope = new Stack<>();
	}

	/**
	 * Closes a lexical scope for a function declaration, removing all
	 * declarations within this scope, restoring the local offset and decreasing
	 * the lookup depth.
	 *
	 * @return function scope details
	 *
	 * @throws NoProgramException
	 *             if no scope is open
	 */
	public FunctionScope<T> closeFunctionScope() throws NoProgramException {
		closeScope();
		return functionScope.pop();
	}

	/**
	 * Closes a lexical scope, removing all declarations within this scope. Runs
	 * in {@code O(k log k log n)} time, with {@code k} the number of
	 * identifiers going out of scope, when the stream is parallelized.
	 *
	 * @throws NoProgramException
	 *             if no scope is open
	 */
	public void closeScope() throws NoProgramException {
		Map<String, Variable<T>> deletions = symbolMapStack.pop();
		for (String identifier : deletions.keySet()) {
			Optional<Variable<T>> maybeType = symbolMapStack.parallelStream()
					.filter((map) -> map.containsKey(identifier)).map((map) -> map.get(identifier))
					.reduce((__, snd) -> snd);
			maybeType.ifPresent((var) -> symbolCache.replace(identifier, var));
			if (!maybeType.isPresent())
				symbolCache.remove(identifier);
		}
	}

	/**
	 * Returns the type of an identifier, if any.
	 *
	 * @param id
	 *            the name of the identifier
	 * @return the type of the identifier
	 * @throws UndeclaredException
	 *             if the identifier is not declared
	 * @throws NoProgramException
	 *             if no scope is open
	 */
	public Variable<T> get(String id) throws UndeclaredException, NoProgramException {
		if (!this.has(id))
			throw new UndeclaredException(String.format(Messages.getString("SymbolTable.1"), id)); //$NON-NLS-1$

		return symbolCache.get(id);
	}

	/**
	 * Returns the current function scope, if any.
	 *
	 * @return the current function scope
	 * @throws NoProgramException
	 *             if no scope is open
	 */
	public FunctionScope<T> getFunctionScope() throws NoProgramException {
		if (symbolMapStack.isEmpty())
			throw new NoProgramException();

		return functionScope.peek();
	}

	/**
	 * Returns whether the given identifier is declared.
	 *
	 * @param id
	 *            the name of the identifier
	 * @return true if the identifier has a declared type
	 * @throws NoProgramException
	 *             if no scope is open
	 */
	public boolean has(String id) throws NoProgramException {
		if (symbolMapStack.isEmpty())
			throw new NoProgramException();

		return symbolCache.containsKey(id);
	}

	/**
	 * Opens a lexical scope for a function declaration, resetting the local
	 * offset and increasing the lookup depth.
	 */
	public void openFunctionScope() {
		openScope();
		functionScope.push(new FunctionScope<T>(functionScope.size()));
	}

	/**
	 * Opens a lexical scope.
	 */
	public void openScope() {
		symbolMapStack.push(new HashMap<>());
	}

	/**
	 * Associates an identifier with a certain type in the current lexical scope
	 * and returns the newly made variable instance that belongs to this
	 * identifier. Throws an exception if the identifier is declared in this
	 * scope already.
	 *
	 * @param id
	 *            the name of the identifier
	 * @param type
	 *            the type of the identifier
	 * @return the type of the identifier
	 * @throws DeclaredException
	 *             if the identifier is declared in the current scope already
	 * @throws NoProgramException
	 *             if no scope is open
	 */
	public Variable<T> put(String id, T type) throws DeclaredException, NoProgramException {
		if (symbolMapStack.isEmpty())
			throw new NoProgramException();

		if (symbolMapStack.peek().containsKey(id))
			throw new DeclaredException(String.format(Messages.getString("SymbolTable.0"), id)); //$NON-NLS-1$

		Variable<T> var = functionScope.peek().addVariable(type);

		symbolMapStack.peek().put(id, var);
		symbolCache.put(id, var);
		return var;
	}

	/**
	 * Opens a function scope, executes the given code and closes it. This is a
	 * helper method to make sure calls to {@link #openFunctionScope()} and
	 * {@link #closeFunctionScope()} are balanced and returns the function scope
	 * details. {@code code} must take no arguments and must not return a value.
	 *
	 * @param code
	 *            the code to execute within the scope
	 * @return the function scope details
	 */
	public FunctionScope<T> withFunctionScope(Runnable code) {
		openFunctionScope();
		code.run();

		try {
			return closeFunctionScope();
		} catch (NoProgramException e) {
			return null;
		}
	}

	/**
	 * Opens a scope, executes the given code and closes the scope. This is a
	 * helper method to make sure calls to {@link #openScope()} and
	 * {@link #closeScope()} are balanced. {@code code} must take no arguments
	 * and must return a value.
	 *
	 * @param <U>
	 *            the type of the code's return value
	 * @param code
	 *            the code to execute within the scope
	 * @return the return value of the code
	 */
	public <U> U withScope(Supplier<U> code) {
		openScope();
		U result = code.get();
		try {
			closeScope();
		} catch (NoProgramException e) {
		}
		return result;
	}

}
