/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.utils.servicemanager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;

import org.dspace.kernel.ServiceManager;
import org.dspace.kernel.mixins.OrderedService;


/**
 * This class stores a list of providers in a specific order
 * (determined by insertion order and the {@link OrderedService}
 * settings).
 * <p>
 * Should be thread safe.
 *
 * @author Aaron Zeckoski (azeckoski @ gmail.com)
 */
public final class ProviderStack<T> {

    protected final List<ProviderHolder<T>> providers;

    /**
     * Default empty constructor.
     * <p>
     * This is mostly only useful if you are planning to add new
     * providers later.  You should probably use the other contructors.
     */
    public ProviderStack() {
        providers = Collections.synchronizedList(new ArrayList<ProviderHolder<T>>());
    }

    /**
     * Construct a provider holder with all currently known providers of
     * the given type.
     *
     * @param serviceManager the system service manager
     * @param providerType   the interface type of the providers we care about
     */
    public ProviderStack(ServiceManager serviceManager, Class<T> providerType) {
        providers = Collections.synchronizedList(new ArrayList<ProviderHolder<T>>());
        List<T> foundProviders = serviceManager.getServicesByType(providerType);
        // filter out the NotProviders first
        for (Iterator<T> iterator = foundProviders.iterator(); iterator.hasNext(); ) {
            T t = iterator.next();
            if (t instanceof NotProvider) {
                iterator.remove();
            }
        }
        Collections.sort(foundProviders, new OrderedServiceComparator());
        for (T t : foundProviders) {
            providers.add(new ProviderHolder<T>(t));
        }
    }

    /**
     * Construct a provider holder with a given set of providers.
     * This will maintain the current order as long as it does not
     * violate the rules for {@link OrderedService}s.
     *
     * @param currentProviders the current set of providers to register in this stack
     */
    public ProviderStack(T[] currentProviders) {
        providers = Collections.synchronizedList(new ArrayList<ProviderHolder<T>>());
        ArrayList<T> tList = new ArrayList<T>();
        // first add in the ordered ones
        for (int i = 0; i < currentProviders.length; i++) {
            T t = currentProviders[i];
            if (t instanceof NotProvider) {
                continue; // skip all the NotProviders
            }
            if (t instanceof OrderedService) {
                tList.add(t);
            }
        }
        // sort the ordered ones
        Collections.sort(tList, new OrderedServiceComparator());
        // now add in the rest in the order given
        for (int i = 0; i < currentProviders.length; i++) {
            T t = currentProviders[i];
            if (!(t instanceof OrderedService)) {
                if (t instanceof NotProvider) {
                    continue; // skip all the NotProviders
                }
                tList.add(t);
            }
        }
        // now put these into holders
        for (T t : tList) {
            providers.add(new ProviderHolder<T>(t));
        }
        tList.clear();
    }

    /**
     * Add a provider to the stack of providers.
     * This will be placed at the bottom if the order is less than or
     * equal to 0 or this provider is not ordered.
     *
     * @param provider the provider to add to the stack
     * @return the position in the stack that this provider was added
     */
    public int addProvider(T provider) {
        if (provider == null) {
            throw new IllegalArgumentException("provider to add cannot be null");
        }
        int position = 0;
        refresh();
        int providerOrder = 0;
        if (provider instanceof NotProvider) {
            throw new IllegalArgumentException(
                "Cannot place anything that implements NotProvider into the provider stack, failure for: " + provider);
        }
        if (provider instanceof OrderedService) {
            providerOrder = ((OrderedService) provider).getOrder();
        }
        // place at the bottom of the stack
        providers.add(new ProviderHolder<T>(provider));
        if (providerOrder > 0) {
            // re-sort the providers
            Collections.sort(this.providers, new ProviderStackComparator());
        }
        return position;
    }

    /**
     * Remove a provider based on the position in the stack (starting at
     * 0 and ending at size-1).
     *
     * @param position the position to remove the provider from
     * @return true if the provider position was found and removed OR false if not found
     */
    public boolean removeProvider(final int position) {
        boolean removed = false;
        try {
            this.providers.remove(position);
            removed = true;
        } catch (IndexOutOfBoundsException e) {
            removed = false;
        }
        refresh();
        return removed;
    }

    /**
     * Remove a provider by the object equality.
     *
     * @param provider the provider to remove from the stack
     * @return true if the provider was found and removed OR false if not found
     */
    public boolean removeProvider(T provider) {
        if (provider == null) {
            throw new IllegalArgumentException("provider to remove cannot be null");
        }
        boolean removed = false;
        refresh();
        for (Iterator<ProviderHolder<T>> iterator = providers.iterator(); iterator.hasNext(); ) {
            ProviderHolder<T> holder = iterator.next();
            T p = holder.getProvider();
            if (p == null) {
                iterator.remove();
            } else {
                if (p.equals(provider)) {
                    iterator.remove();
                    removed = true;
                }
            }
        }
        return removed;
    }

    /**
     * Get the list of all providers currently in the stack.
     * <p>
     * WARNING: this should not be held onto; it should be only used for
     * iteration and then discarded.
     *
     * @return a read-only list of all providers in the stack in the correct order, highest priority first
     */
    public List<T> getProviders() {
        List<T> l = refresh();
        return Collections.unmodifiableList(l);
    }

    /**
     * This allows access to the provider holders.
     *
     * @return a read only list of the provider holders in the correct order
     */
    public List<ProviderHolder<T>> getProviderHolders() {
        return Collections.unmodifiableList(providers);
    }

    /**
     * Get the iterator of all providers currently in the stack.
     * This will load up lazy so it can be held onto safely but should not be
     * used more than once (i.e. only iterate over this completely once:
     * stopping and continuing later is likely to produce failures).
     * <p>
     * NOTE: This will attempt to iterate over all the valid providers
     * in the stack, but if the provider has been garbage collected
     * during the iteration then a {@link NoSuchElementException} will
     * be thrown if the provider was the last one in the stack and
     * next() is called.  You should probably handle the exception by
     * assuming this indicates all items were iterated over.
     *
     * @return an iterator over all the providers in the stack in order of priority
     */
    public Iterator<T> getIterator() {
        return new Iterator<T>() {
            protected ListIterator<ProviderHolder<T>> it = null;

            public synchronized boolean hasNext() {
                if (it == null) {
                    it = providers.listIterator();
                }
                return it.hasNext();
            }

            public synchronized T next() {
                if (it == null) {
                    it = providers.listIterator();
                }
                /* get the next provider if it is not null, otherwise keep going until we find a non-null one
                 * or just return null if there are none left
                 */
                T t = null;
                while (it.hasNext()) {
                    ProviderHolder<T> holder = it.next();
                    t = holder.getProvider();
                    if (t != null) {
                        break;
                    }
                }
                if (t == null) {
                    throw new NoSuchElementException("No more providers remain with valid weak references");
                }
                return t;
            }

            public void remove() {
                it.remove();
            }
        };
    }

    /**
     * Get a provider based on the position in the stack (starting at 0
     * and ending at size-1).
     *
     * @param position the position to check for the provider
     * @return the provider from the position OR null if there is no provider at that position
     */
    public T getProvider(final int position) {
        T provider = null;
        ProviderHolder<T> holder = getProviderHolder(position);
        if (holder != null) {
            provider = holder.getProvider();
        }
        return provider;
    }

    /**
     * Get the provider holder from the position in the stack if there
     * is one.
     *
     * @param position the position to check for the provider
     * @return the holder from the position OR null if there is no provider at that position
     */
    public ProviderHolder<T> getProviderHolder(final int position) {
        refresh();
        ProviderHolder<T> holder;
        try {
            holder = providers.get(position);
        } catch (IndexOutOfBoundsException e) {
            // no provider at that position
            holder = null;
        }
        return holder;
    }

    /**
     * Check the number of current providers which are available in this
     * stack.
     *
     * @return the total number of viable providers
     */
    public int size() {
        refresh();
        return providers.size();
    }

    /**
     * Delete all providers from the current provider stack.
     */
    public void clear() {
        providers.clear();
    }

    /**
     * Check to make sure all providers are refreshed and any that are
     * no longer valid are flushed out of the list.
     *
     * @return list of valid providers
     */
    protected List<T> refresh() {
        ArrayList<T> l = new ArrayList<T>();
        for (Iterator<ProviderHolder<T>> iterator = providers.iterator(); iterator.hasNext(); ) {
            ProviderHolder<T> holder = iterator.next();
            T provider = holder.getProvider();
            if (provider == null) {
                iterator.remove();
            } else {
                l.add(provider);
            }
        }
        return l;
    }

}
