/*
 * Copyright (c) 2021
 * NDE Netzdesign und -entwicklung AG, Hamburg, Germany
 * All rights reserved.
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Library General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this program (see the file LICENSE.txt for more
 * details); if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
 */

package org.acplt.oncrpc.apps.jrpcgen;

import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * An item table collects items sharing the same scope. In detail, each item within
 * an item table comes with an unique identifier. An item table is backed by both a
 * hash map and a linked list. The former ensures items with unique identifiers,
 * the latter stores them in the sequence of their appearance.
 * 
 * <p>An item list is prepared to hold a collection of any item as well as items
 * of a defined type. The global definition list of an RPC/XDR specification will
 * contain realisations of class {@link JrpcgenXdrDefinition}, for example. 
 * 
 * @author Harald Wirths {@literal <hwirths@nde.ag>}
 *
 * @param <T> An interface extending the interface {@code JrpcgenItem} or a class implementing
 *            the interface {@code JrpcgenItem}.
 */
public class JrpcgenItemTable<T extends JrpcgenItem> implements Iterable<T> {

	/**
	 * Returns the list of items stored in this table. The returned list may be empty,
	 * if no item was put to this table so far.
	 * 
	 * @return The list of items stored in this table.
	 */
	public List<T> getItemList() {
		return list;
	}
	
	/**
	 * Clears this table. The table will not contain any item afterwards.
	 */
	public void clear() {
		list.clear();
		table.clear();
	}
	
	/**
	 * Returns whether the table is empty.
	 * 
	 * @return {@code true} if the table does not contain any element,
	 *         {@code false} otherwise.
	 */
	public boolean isEmpty() {
		return list.isEmpty();
	}
	
	/**
	 * Returns the size of this table reflecting the number of items
	 * kept in this table.
	 * 
	 * @return The number of items stored in this table.
	 */
	public int size() {
		return list.size();
	}
	
	/**
	 * Returns whether the passed idnetifier is unknown to the table. In detaiĺ,
	 * this method checks the table for the existence of an item whose identifier
	 * equals the passed one and returns {@code true}, if no item was found, or
	 * {@code false}, if an item was found having set the passed identifier.
	 * 
	 * @param identifier The identifier to check for.
	 * @return {@code true} if no item was found whose identifier equals the passed one,
	 *         {@code false} otherwise.
	 */
	public boolean isUnknownIdentifier(String identifier) {
		return (! table.containsKey(identifier));
	}
	
	/**
	 * Adds the passed item to this table, if its identifier is unknown to
	 * this table.
	 * 
	 * @param item The item to be added to this table.
	 * @return {@code true} if the item was added due to the fact, that its
	 *         identifier was unknown to the table before, {@code false}
	 *         otherwise.
	 */
	public boolean addItem(T item) {
		boolean added = false;
		
		if (isUnknownIdentifier(item.getIdentifier())) {
			putItem(item);
			added = true;
		}
		
		return added;
	}
	
	/**
	 * Puts the passed item to this table, if no item with the same
	 * identifier is already in this table.
	 * 
	 * @param item The item to be put into this table.
	 * @throws IllegalArgumentException Passing an item with an already
	 *         known identifier to this table will cause this exception
	 *         to be thrown.
	 */
	public void putItem(T item) {
		T previousItem = table.put(item.getIdentifier(), item);
		
		if (previousItem == null) {
			list.add(item);
		} else {
			table.put(item.getIdentifier(), previousItem);
			throw new IllegalArgumentException("Identifier '" + item.getIdentifier() + "' is already known to this table.");
		}
	}

	/**
	 * Returns the item mapped to the passed identifier. The item
	 * is looked up in the hash map of this table.
	 * 
	 * @param identifier The identifier of the requested item.
	 * @return The requested item will be returned, if the table
	 * contains an item with the passed identifier, {@code null}
	 * otherwise.
	 */
	public T getItem(String identifier) {
		return table.get(identifier);
	}
	
	/**
	 * Returns the item based on the passed type and mapped to the passed
	 * identifier. The item is looked up in the hash map of this table.
	 * 
	 * @param <U> The required type of the requested item.
	 * @param clazz The class object representing the expected type of the
	 *        requested item.
	 * @param identifier The identifier of the requested item.
	 * @return The requested item will be returned, if an item is mapped to
	 *         the passed identifier and the item is an instance of the passed
	 *         type. Otherwise {@code null} will be returned.
	 */
	public <U extends JrpcgenItem> U getItem(Class<U> clazz, String identifier) {
		U requestedItem = null;
		JrpcgenItem item = table.get(identifier);
		
		if ((item != null) && clazz.isInstance(item)) {
			requestedItem = clazz.cast(item);
		}
		
		return requestedItem;
	}
	
	/**
	 * Returns the first item of this table. The first item is taken
	 * from the linked list of this table and reflects the first
	 * item that appeared to this table.
	 * 
	 * @return The first item of this table if at least
	 *         one item is in this table, {@code null} otherwise.
	 */
	public T getFirstItem() {
		if (list.isEmpty())
			return null;
		
		return list.getFirst();
	}
	
	/**
	 * Returns the last item of this table. The last item is taken
	 * from the linked list of this table and reflects the last
	 * item that appeared to this table.
	 * 
	 * @return The last item of this table if at least one item
	 *         is in this table, {@code null} otherwise.
	 */
	public T getLastItem() {
		if (list.isEmpty())
			return null;
		
		return list.getLast();
	}

	/**
	 * Returns an iterator to the items of this table. The iterator
	 * is taken from the linked list and therefore iterates over the
	 * items in the sequence of their appearance to this table.
	 * 
	 * @return An iterator to the items of this table taken from the
	 *         internal used linked list.
	 */
	@Override
	public Iterator<T> iterator() {
		return list.iterator();
	}
	
	/**
	 * Returns an iterator to the items of this table, which are instances
	 * of the passed type. The iterator wraps an iterator taken from the
	 * linked list and therefore iterates over the matching items in
	 * the sequence of their appearance to this table.
	 * 
	 * @param <U> The type of the items provided by the returned iterator.
	 * @param clazz The class instance of the requested item type. 
	 * @return An iterator to the items of this table with the requested type
	 *         taken from the internal used linked list.
	 */
	public <U extends JrpcgenItem> Iterator<U> iterator(Class<U> clazz) {
		/*
		 * Which will be the first one?
		 */
		Iterator<T> itemIterator = list.iterator();
		U firstItem = null;
		
		while ((firstItem == null) && itemIterator.hasNext()) {
			JrpcgenItem item = itemIterator.next();
			
			if (clazz.isInstance(item)) {
				firstItem = clazz.cast(item);
			}
		}

		return iterator(clazz, itemIterator, firstItem);
	}
	
	private <U extends JrpcgenItem> Iterator<U> iterator(final Class<U> clazz, final Iterator<T> itemIterator, final U firstItem) {
		return new Iterator<U>() {

			@Override
			public boolean hasNext() {
				return (nextItem != null);
			}

			@Override
			public U next() {
				U currentDefinition = nextItem;
				
				if (currentDefinition != null) {
					nextItem = null;
					
					while ((nextItem == null) && itemIterator.hasNext()) {
						JrpcgenItem item = itemIterator.next();
						
						if (clazz.isInstance(item)) {
							nextItem = clazz.cast(item);
						}
					}
				}
				
				return currentDefinition;
			}
			
			@Override
			public void remove() {
				throw new RuntimeException("Method remove() is not available with this iterator.");
			}
			
			private U nextItem = firstItem;
		};
	}

	private final Map<String, T> table = new HashMap<>();
	private final LinkedList<T> list = new LinkedList<>();

}
