/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.openejb.util;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Stack;

/**
 * @version $Rev$ $Date$
 */
public class References {

    public interface Visitor<T> {
        String getName(T t);

        Set<String> getReferences(T t);
    }

    public static <T> List<T> sort(final List<T> objects, final Visitor<T> visitor) {

        if (objects.size() <= 1) {
            return objects;
        }

        final Map<String, Node> nodes = new LinkedHashMap<String, Node>();

        // Create nodes
        for (final T obj : objects) {
            final String name = visitor.getName(obj);
            final Node node = new Node(name, obj);
            nodes.put(name, node);
        }

        // Link nodes
        for (final Node node : nodes.values()) {
            for (final String name : visitor.getReferences((T) node.object)) {
                final Node ref = nodes.get(name);
                if (ref == null) {
                    throw new IllegalArgumentException("No such object in list: " + name);
                }
                node.references.add(ref);
                node.initialReferences.add(ref);
            }
        }
        boolean circuitFounded = false;
        for (final Node node : nodes.values()) {
            final Set<Node> visitedNodes = new HashSet<>();
            if (!normalizeNodeReferences(node, node, visitedNodes)) {
                circuitFounded = true;
                break;
            }
            node.references.addAll(visitedNodes);
        }

        //detect circus
        if (circuitFounded) {
            final Set<Circuit> circuits = new LinkedHashSet<Circuit>();

            for (final Node node : nodes.values()) {
                findCircuits(circuits, node, new Stack<Node>());
            }

            final ArrayList<Circuit> list = new ArrayList<Circuit>(circuits);
            Collections.sort(list);

            final List<List> all = new ArrayList<List>();
            for (final Circuit circuit : list) {
                all.add(unwrap(circuit.nodes));
            }

            throw new CircularReferencesException(all);
        }

        //Build Double Link Node List
        final Node rootNode = new Node(null, null);
        rootNode.previous = rootNode;
        rootNode.next = nodes.values().iterator().next();

        for (final Node node : nodes.values()) {
            node.previous = rootNode.previous;
            rootNode.previous.next = node;
            node.next = rootNode;
            rootNode.previous = node;
        }

        for (final Node node : nodes.values()) {
            for (final Node reference : node.references) {
                swap(node, reference, rootNode);
            }
        }

        final List sortedList = new ArrayList(nodes.size());
        Node currentNode = rootNode.next;
        while (currentNode != rootNode) {
            sortedList.add(currentNode.object);
            currentNode = currentNode.next;
        }
        return sortedList;
    }

    private static boolean normalizeNodeReferences(final Node rootNode, final Node node, final Set<Node> referenceNodes) {
        if (node.references.contains(rootNode)) {
            return false;
        }
        for (final Node reference : node.references) {
            if (!referenceNodes.add(reference)) {
                //this reference node has been visited in the past
                continue;
            }
            if (!normalizeNodeReferences(rootNode, reference, referenceNodes)) {
                return false;
            }
        }
        return true;
    }

    private static void swap(final Node shouldAfterNode, final Node shouldBeforeNode, final Node rootNode) {
        Node currentNode = shouldBeforeNode;
        while (currentNode.next != rootNode) {
            if (currentNode.next == shouldAfterNode) {
                return;
            }
            currentNode = currentNode.next;
        }
        //Remove the shouldAfterNode from list
        shouldAfterNode.previous.next = shouldAfterNode.next;
        shouldAfterNode.next.previous = shouldAfterNode.previous;
        //Insert the node immediately after the shouldBeforeNode
        shouldAfterNode.previous = shouldBeforeNode;
        shouldAfterNode.next = shouldBeforeNode.next;
        shouldBeforeNode.next = shouldAfterNode;
        shouldAfterNode.next.previous = shouldAfterNode;
    }

    private static <T> List<T> unwrap(final List<Node> nodes) {
        final ArrayList<T> referees = new ArrayList<T>(nodes.size());
        for (final Node node : nodes) {
            referees.add((T) node.object);
        }
        return referees;
    }

    private static void findCircuits(final Set<Circuit> circuits, final Node node, final Stack<Node> stack) {
        if (stack.contains(node)) {
            final int fromIndex = stack.indexOf(node);
            final int toIndex = stack.size();
            final ArrayList<Node> circularity = new ArrayList<Node>(stack.subList(fromIndex, toIndex));

            // add ending node to list so a full circuit is shown
            circularity.add(node);

            final Circuit circuit = new Circuit(circularity);

            circuits.add(circuit);

            return;
        }

        stack.push(node);

        for (final Node reference : node.initialReferences) {
            findCircuits(circuits, reference, stack);
        }

        stack.pop();
    }

    private static class Node implements Comparable<Node> {
        private final String name;
        private final Object object;
        private final List<Node> initialReferences = new ArrayList<Node>();
        private final Set<Node> references = new HashSet<Node>();
        private Node next;
        private Node previous;

        public Node(final String name, final Object object) {
            this.name = name;
            this.object = object;
        }

        public boolean equals(final Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            final Node node = (Node) o;

            return name.equals(node.name);
        }

        public int hashCode() {
            return name.hashCode();
        }

        public int compareTo(final Node o) {
            return this.name.compareTo(o.name);
        }

        public String toString() {
            return name;
            //return "Node("+ name +" : "+ Join.join(",", unwrap(initialReferences))+")";
        }
    }

    private static class Circuit implements Comparable<Circuit> {
        private final List<Node> nodes;
        private final List<Node> atomic;

        public Circuit(final List<Node> nodes) {
            this.nodes = nodes;
            atomic = new ArrayList<Node>(nodes);
            atomic.remove(atomic.size() - 1);
            Collections.sort(atomic);
        }

        public List<Node> getNodes() {
            return nodes;
        }

        public boolean equals(final Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()) {
                return false;
            }

            final Circuit circuit = (Circuit) o;

            if (!atomic.equals(circuit.atomic)) {
                return false;
            }

            return true;
        }

        public int hashCode() {
            return atomic.hashCode();
        }

        public int compareTo(final Circuit o) {
            int i = atomic.size() - o.atomic.size();
            if (i != 0) {
                return i;
            }

            final Iterator<Node> iterA = atomic.listIterator();
            final Iterator<Node> iterB = o.atomic.listIterator();
            while (iterA.hasNext() && iterB.hasNext()) {
                final Node a = iterA.next();
                final Node b = iterB.next();
                i = a.compareTo(b);
                if (i != 0) {
                    return i;
                }
            }

            return 0;
        }

        public String toString() {
            return "Circuit(" + Join.join(",", unwrap(nodes)) + ")";
        }
    }

}
