/*
 * JBoss, Home of Professional Open Source
 * Copyright 2013, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.richfaces.resource.optimizer.ordering;

import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Collections2;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;

/**
 * <p>
 * Stores partial orderings in order to be able derive complete ordering.
 * </p>
 * 
 * <p>
 * When storing new partial ordering, checks that new partial ordering does not violates partial orderings stored before.
 * </p>
 * 
 * @author <a href="http://community.jboss.org/people/lfryc">Lukas Fryc</a>
 */
public class PartialOrderToCompleteOrder<T> {

    // all items stored in partial orderings for quick access
    private Set<T> allItems = Sets.newLinkedHashSet();

    // partial orderings used to check for ordering violation
    private List<PartialOrdering> partialOrderings = new LinkedList<PartialOrdering>();

    // map from items to their dependencies
    private Map<T, Set<T>> dependencies = Maps.newLinkedHashMap();

    /**
     * <p>
     * Stores collection as partial ordering.
     * </p>
     * 
     * <p>
     * Checks that this collection will not violate another partial orderings stored before.
     * </p>
     * 
     * @param collection as partial order
     */
    public void addPartialOrdering(Collection<T> collection) {
        if (collection == null || collection.isEmpty()) {
            return;
        }

        checkCurrentPartialOrders(collection);

        allItems.addAll(collection);
        partialOrderings.add(new PartialOrdering(collection));
        registerDependencies(Lists.newLinkedList(collection));
    }

    /**
     * Provides all items which was stored in collections as partial orderings
     * 
     * @return all items which was stored in collections as partial orderings
     */
    public Set<T> getAllItems() {
        return allItems;
    }

    /**
     * <p>
     * Provides current complete ordering derived from partial orderings.
     * </p>
     * 
     * @return current complete ordering derived from partial orderings.
     */
    public CompleteOrdering getCompleteOrdering() {
        return new CompleteOrdering();
    }

    /**
     * Get all items completely ordered.
     * 
     * @return all items completely ordered.
     */
    public Collection<T> getCompletelyOrderedItems() {
        return new CompleteOrdering().sortedCopy(allItems);
    }

    /**
     * Class representing result of deriving complete ordering from stored partial orderings.
     */
    public class CompleteOrdering extends Ordering<T> {

        private Set<T> ordered = getCurrentOrder();

        private Predicate<T> IS_ORDERED = new Predicate<T>() {
            public boolean apply(T item) {
                return ordered.contains(item);
            }
        };

        public int compare(T left, T right) {
            if (!ordered.contains(left) || !ordered.contains(right)) {
                throw new IllegalStateException();
            }
            if (left.equals(right)) {
                return 0;
            }
            for (T item : ordered) {
                if (item.equals(left)) {
                    return -1;
                } else if (item.equals(right)) {
                    return +1;
                }
            }
            throw new IllegalStateException();
        }

        /**
         * <p>
         * Returns new iterable sorted according to this complete ordering.
         * </p>
         * 
         * <p>
         * All items which are unknown in this ordering are stored on the end of returned collection in the same order like in
         * iterable.
         * </p>
         */
        @SuppressWarnings("unchecked")
        @Override
        public <E extends T> List<E> sortedCopy(Iterable<E> iterable) {
            List<T> originList = (List<T>) Lists.newLinkedList(iterable);
            Collection<T> onlyOrdered = Collections2.filter(originList, IS_ORDERED);
            Collection<T> onlyNotOrdered = Collections2.filter(originList, Predicates.not(IS_ORDERED));

            List<T> itemsInOrder = super.sortedCopy(onlyOrdered);
            itemsInOrder.addAll(onlyNotOrdered);

            return (List<E>) itemsInOrder;
        }

        private Set<T> getCurrentOrder() {
            Set<T> result = Sets.newLinkedHashSet();
            DependencyResolver resolver = new DependencyResolver();

            for (int i = 0; i < dependencies.size(); i++) {
                List<T> nodesWithoutDependencies = resolver.findNodesWithoutDependencies();
                result.addAll(nodesWithoutDependencies);
                resolver.removeNodes(nodesWithoutDependencies);
            }

            if (resolver.getSize() > 0) {
                throw new IllegalStateException();
            }

            return result;
        }

        private class DependencyResolver {
            private Map<T, Set<T>> deps = deepCopyOfDependencies();

            private List<T> findNodesWithoutDependencies() {
                List<T> list = Lists.newLinkedList();
                for (Entry<T, Set<T>> entry : deps.entrySet()) {
                    if (entry.getValue().isEmpty()) {
                        list.add(entry.getKey());
                    }
                }
                return list;
            }

            private void removeNodes(List<T> nodes) {
                for (Set<T> values : deps.values()) {
                    values.removeAll(nodes);
                }
                for (T node : nodes) {
                    deps.remove(node);
                }
            }

            private Map<T, Set<T>> deepCopyOfDependencies() {
                Map<T, Set<T>> result = Maps.newLinkedHashMap();
                for (Entry<T, Set<T>> entry : dependencies.entrySet()) {
                    result.put(entry.getKey(), Sets.newHashSet(entry.getValue()));
                }
                return result;
            }

            public int getSize() {
                return deps.size();
            }
        }
    }

    private void checkCurrentPartialOrders(Collection<T> collection) {
        for (PartialOrdering p : partialOrderings) {
            List<T> filtered = p.filter(collection);
            if (!p.isStrictlyOrdered(filtered)) {
                throw new IllegalPartialOrderingException("\ncollection: " + collection + "\n" + p);
            }
        }
    }

    private void registerDependencies(Collection<T> collection) {
        List<T> reversedOrder = Lists.reverse(Lists.newLinkedList(collection));
        Set<T> newItemDependencies = Sets.newLinkedHashSet(collection);

        for (T newItem : reversedOrder) {
            newItemDependencies.remove(newItem);
            registerDependenciesForItem(newItem, Sets.newLinkedHashSet(newItemDependencies));
        }
    }

    private void registerDependenciesForItem(T item, Set<T> newItemDependencies) {
        if (!dependencies.containsKey(item)) {
            dependencies.put(item, Sets.<T>newHashSet());
        }
        Set<T> itemDependences = dependencies.get(item);
        itemDependences.addAll(newItemDependencies);
    }

    private class PartialOrdering extends Ordering<T> {
        private LinkedList<T> order = Lists.newLinkedList();
        private HashSet<T> items = Sets.newHashSet();

        public PartialOrdering(Collection<T> collection) {
            order = Lists.newLinkedList(collection);
            items = Sets.newHashSet(collection);
        }

        public int compare(T left, T right) {
            if (!items.contains(left)) {
                throw new IllegalArgumentException("'" + left + "' is not part of this partial ordering");
            }
            if (!items.contains(right)) {
                throw new IllegalArgumentException("'" + right + "' is not part of this partial ordering");
            }
            return order.indexOf(left) - order.indexOf(right);
        }

        public List<T> filter(Collection<T> collection) {
            List<T> list = new LinkedList<T>(collection);
            list.retainAll(items);
            return list;
        }

        @Override
        public String toString() {
            return "PartialOrder" + order;
        }
    }
}
