/**
 *
 * Copyright 2018 Florian Schmaus
 *
 * Licensed 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.jivesoftware.smack.fsm;

import java.io.PrintWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Logger;

import org.jivesoftware.smack.fsm.AbstractXmppStateMachineConnection.DisconnectedStateDescriptor;
import org.jivesoftware.smack.util.MultiMap;

/**
 * Smack's utility API for Finite State Machines (FSM).
 *
 * <p>
 * Thanks to Andreas Fried for the fun and successful bug hunting session.
 * </p>
 *
 * @author Florian Schmaus
 *
 */
public class StateDescriptorGraph {

    private static final Logger LOGGER = Logger.getLogger(StateDescriptorGraph.class.getName());

    private static GraphVertex<StateDescriptor> addNewStateDescriptorGraphVertex(
                    Class<? extends StateDescriptor> stateDescriptorClass,
                    Map<Class<? extends StateDescriptor>, GraphVertex<StateDescriptor>> graphVertexes)
                    throws InstantiationException, IllegalAccessException, IllegalArgumentException,
                    InvocationTargetException, NoSuchMethodException, SecurityException {
        Constructor<? extends StateDescriptor> stateDescriptorConstructor = stateDescriptorClass.getDeclaredConstructor();
        stateDescriptorConstructor.setAccessible(true);
        StateDescriptor stateDescriptor = stateDescriptorConstructor.newInstance();
        GraphVertex<StateDescriptor> graphVertexStateDescriptor = new GraphVertex<>(stateDescriptor);

        GraphVertex<StateDescriptor> previous = graphVertexes.put(stateDescriptorClass, graphVertexStateDescriptor);
        assert previous == null;

        return graphVertexStateDescriptor;
    }

    private static final class HandleStateDescriptorGraphVertexContext {
        private final Set<Class<? extends StateDescriptor>> handledStateDescriptors = new HashSet<>();
        Map<Class<? extends StateDescriptor>, GraphVertex<StateDescriptor>> graphVertexes;
        MultiMap<Class<? extends StateDescriptor>, Class<? extends StateDescriptor>> inferredForwardEdges;

        private HandleStateDescriptorGraphVertexContext(
                        Map<Class<? extends StateDescriptor>, GraphVertex<StateDescriptor>> graphVertexes,
                        MultiMap<Class<? extends StateDescriptor>, Class<? extends StateDescriptor>> inferredForwardEdges) {
            this.graphVertexes = graphVertexes;
            this.inferredForwardEdges = inferredForwardEdges;
        }

        private boolean recurseInto(Class<? extends StateDescriptor> stateDescriptorClass) {
            boolean wasAdded = handledStateDescriptors.add(stateDescriptorClass);
            boolean alreadyHandled = !wasAdded;
            return alreadyHandled;
        }

        private GraphVertex<StateDescriptor> getOrConstruct(Class<? extends StateDescriptor> stateDescriptorClass)
                        throws InstantiationException, IllegalAccessException, IllegalArgumentException,
                        InvocationTargetException, NoSuchMethodException, SecurityException {
            GraphVertex<StateDescriptor> graphVertexStateDescriptor = graphVertexes.get(stateDescriptorClass);

            if (graphVertexStateDescriptor == null) {
                graphVertexStateDescriptor = addNewStateDescriptorGraphVertex(stateDescriptorClass, graphVertexes);

                for (Class<? extends StateDescriptor> inferredSuccessor : inferredForwardEdges.getAll(
                                stateDescriptorClass)) {
                    graphVertexStateDescriptor.getElement().addSuccessor(inferredSuccessor);
                }
            }

            return graphVertexStateDescriptor;
        }
    }

    private static void handleStateDescriptorGraphVertex(GraphVertex<StateDescriptor> node,
                    HandleStateDescriptorGraphVertexContext context)
                    throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException, NoSuchMethodException, SecurityException {
        Class<? extends StateDescriptor> stateDescriptorClass = node.element.getClass();
        boolean alreadyHandled = context.recurseInto(stateDescriptorClass);
        if (alreadyHandled) {
            return;
        }

        Set<Class<? extends StateDescriptor>> successorClasses = node.element.getSuccessors();
        int numSuccessors = successorClasses.size();

        Map<Class<? extends StateDescriptor>, GraphVertex<StateDescriptor>> successorStateDescriptors = new HashMap<>(
                        numSuccessors);
        for (Class<? extends StateDescriptor> successorClass : successorClasses) {
            GraphVertex<StateDescriptor> successorGraphNode = context.getOrConstruct(successorClass);
            successorStateDescriptors.put(successorClass, successorGraphNode);
        }

        switch (numSuccessors) {
        case 0:
            throw new IllegalStateException("State " + stateDescriptorClass + " has no successor");
        case 1:
            GraphVertex<StateDescriptor> soleSuccessorNode = successorStateDescriptors.values().iterator().next();
            node.addOutgoingEdge(soleSuccessorNode);
            handleStateDescriptorGraphVertex(soleSuccessorNode, context);
            return;
        }

        // We hit a state with multiple successors, perform a topological sort on the successors first.
        // Process the information regarding subordinates and superiors states.

        // The preference graph is the graph where the precedence information of all successors is stored, which we will
        // topologically sort to find out which successor we should try first. It is a further new graph we use solely in
        // this step for every node. The graph is representent as map. There is no special marker for the initial node
        // as it is not required for the topological sort performed later.
        Map<Class<? extends StateDescriptor>, GraphVertex<Class<? extends StateDescriptor>>> preferenceGraph = new HashMap<>(numSuccessors);

        // Iterate over all successor states of the current state.
        for (GraphVertex<StateDescriptor> successorStateDescriptorGraphNode : successorStateDescriptors.values()) {
            StateDescriptor successorStateDescriptor = successorStateDescriptorGraphNode.element;
            Class<? extends StateDescriptor> successorStateDescriptorClass = successorStateDescriptor.getClass();
            for (Class<? extends StateDescriptor> subordinateClass : successorStateDescriptor.getSubordinates()) {
                if (!successorClasses.contains(subordinateClass)) {
                    LOGGER.severe(successorStateDescriptor + " points to a subordinate '" + subordinateClass + "' which is not part of the successor set");
                    continue;
                }

                GraphVertex<Class<? extends StateDescriptor>> superiorClassNode = lookupAndCreateIfRequired(
                                preferenceGraph, successorStateDescriptorClass);
                GraphVertex<Class<? extends StateDescriptor>> subordinateClassNode = lookupAndCreateIfRequired(
                                preferenceGraph, subordinateClass);

                superiorClassNode.addOutgoingEdge(subordinateClassNode);
            }
            for (Class<? extends StateDescriptor> superiorClass : successorStateDescriptor.getSuperiors()) {
                if (!successorClasses.contains(superiorClass)) {
                    LOGGER.severe(successorStateDescriptor + " points to a superior '" + superiorClass
                                    + "' which is not part of the successor set");
                    continue;
                }

                GraphVertex<Class<? extends StateDescriptor>> subordinateClassNode = lookupAndCreateIfRequired(
                                preferenceGraph, successorStateDescriptorClass);
                GraphVertex<Class<? extends StateDescriptor>> superiorClassNode = lookupAndCreateIfRequired(
                                preferenceGraph, superiorClass);

                superiorClassNode.addOutgoingEdge(subordinateClassNode);
            }
        }

        // Perform a topological sort which returns the state descriptor classes in their priority.
        List<GraphVertex<Class<? extends StateDescriptor>>> sortedSuccessors = topologicalSort(preferenceGraph.values());

        // Handle the successor nodes which have not preference information available. Simply append them to the end of
        // the sorted successor list.
        outerloop: for (Class<? extends StateDescriptor> successorStateDescriptor : successorClasses) {
            for (GraphVertex<Class<? extends StateDescriptor>> sortedSuccessor : sortedSuccessors) {
                if (sortedSuccessor.getElement() == successorStateDescriptor) {
                    continue outerloop;
                }
            }

            sortedSuccessors.add(new GraphVertex<>(successorStateDescriptor));
        }

        for (GraphVertex<Class<? extends StateDescriptor>> successor : sortedSuccessors) {
            GraphVertex<StateDescriptor> successorVertex = successorStateDescriptors.get(successor.element);
            node.addOutgoingEdge(successorVertex);

            // Recurse further.
            handleStateDescriptorGraphVertex(successorVertex, context);
        }
    }

    public static GraphVertex<StateDescriptor> constructStateDescriptorGraph(Set<Class<? extends StateDescriptor>> backwardEdgeStateDescriptors)
                    throws InstantiationException, IllegalAccessException, IllegalArgumentException,
                    InvocationTargetException, NoSuchMethodException, SecurityException {
        Map<Class<? extends StateDescriptor>, GraphVertex<StateDescriptor>> graphVertexes = new HashMap<>();

        final Class<? extends StateDescriptor> initialStatedescriptorClass = DisconnectedStateDescriptor.class;
        GraphVertex<StateDescriptor> initialNode = addNewStateDescriptorGraphVertex(initialStatedescriptorClass, graphVertexes);

        MultiMap<Class<? extends StateDescriptor>, Class<? extends StateDescriptor>> inferredForwardEdges = new MultiMap<>();
        for (Class<? extends StateDescriptor> backwardsEdge : backwardEdgeStateDescriptors) {
            GraphVertex<StateDescriptor> graphVertexStateDescriptor = addNewStateDescriptorGraphVertex(backwardsEdge, graphVertexes);

            for (Class<? extends StateDescriptor> predecessor : graphVertexStateDescriptor.getElement().getPredeccessors()) {
                inferredForwardEdges.put(predecessor, backwardsEdge);
            }
        }
        // Ensure that the intial node has their successors inferred.
        for (Class<? extends StateDescriptor> inferredSuccessorOfInitialStateDescriptor : inferredForwardEdges.getAll(initialStatedescriptorClass)) {
            initialNode.getElement().addSuccessor(inferredSuccessorOfInitialStateDescriptor);
        }

        HandleStateDescriptorGraphVertexContext context = new HandleStateDescriptorGraphVertexContext(graphVertexes, inferredForwardEdges);
        handleStateDescriptorGraphVertex(initialNode, context);

        return initialNode;
    }

    private static GraphVertex<AbstractXmppStateMachineConnection.State> convertToStateGraph(GraphVertex<StateDescriptor> stateDescriptorVertex,
                    AbstractXmppStateMachineConnection connection, Map<StateDescriptor, GraphVertex<AbstractXmppStateMachineConnection.State>> handledStateDescriptors) {
        StateDescriptor stateDescriptor = stateDescriptorVertex.getElement();
        GraphVertex<AbstractXmppStateMachineConnection.State> stateVertex = handledStateDescriptors.get(stateDescriptor);
        if (stateVertex != null) {
            return stateVertex;
        }

        AbstractXmppStateMachineConnection.State state = stateDescriptor.constructState(connection);
        stateVertex = new GraphVertex<>(state);
        handledStateDescriptors.put(stateDescriptor, stateVertex);
        for (GraphVertex<StateDescriptor> successorStateDescriptorVertex : stateDescriptorVertex.getOutgoingEdges()) {
            GraphVertex<AbstractXmppStateMachineConnection.State> successorStateVertex = convertToStateGraph(successorStateDescriptorVertex, connection, handledStateDescriptors);
            // It is important that we keep the order of the edges. This should do it.
            stateVertex.addOutgoingEdge(successorStateVertex);
        }

        return stateVertex;
    }

    static GraphVertex<AbstractXmppStateMachineConnection.State> convertToStateGraph(GraphVertex<StateDescriptor> initialStateDescriptor,
                    AbstractXmppStateMachineConnection connection) {
        Map<StateDescriptor, GraphVertex<AbstractXmppStateMachineConnection.State>> handledStateDescriptors = new HashMap<>();
        GraphVertex<AbstractXmppStateMachineConnection.State> initialState = convertToStateGraph(initialStateDescriptor, connection,
                        handledStateDescriptors);
        return initialState;
    }

    // Graph API after here.
    // This API could possibly factored out into an extra package/class, but then we will probably need a builder for
    // the graph vertex in order to keep it immutable.
    public static final class GraphVertex<E> {
        private final E element;
        private final List<GraphVertex<E>> outgoingEdges = new ArrayList<>();

        private VertexColor color = VertexColor.white;

        private GraphVertex(E element) {
            this.element = element;
        }

        private void addOutgoingEdge(GraphVertex<E> vertex) {
            assert vertex != null;
            if (outgoingEdges.contains(vertex)) {
                throw new IllegalArgumentException("This " + this + " already has an outgoing edge to " + vertex);
            }
            outgoingEdges.add(vertex);
        }

        public E getElement() {
            return element;
        }

        public List<GraphVertex<E>> getOutgoingEdges() {
            return Collections.unmodifiableList(outgoingEdges);
        }

        private enum VertexColor {
            white,
            grey,
            black,
        }

        @Override
        public String toString() {
            return toString(true);
        }

        public String toString(boolean includeOutgoingEdges) {
            StringBuilder sb = new StringBuilder();
            sb.append("GraphVertex " + element + " [color=" + color
                            + ", identityHashCode=" + System.identityHashCode(this)
                            + ", outgoingEdgeCount=" + outgoingEdges.size());

            if (includeOutgoingEdges) {
                sb.append(", outgoingEdges={");

                for (Iterator<GraphVertex<E>> it = outgoingEdges.iterator(); it.hasNext();) {
                    GraphVertex<E> outgoingEdgeVertex = it.next();
                    sb.append(outgoingEdgeVertex.toString(false));
                    if (it.hasNext()) {
                        sb.append(", ");
                    }
                }
                sb.append('}');
            }

            sb.append(']');
            return sb.toString();
        }
    }

    private static GraphVertex<Class<? extends StateDescriptor>> lookupAndCreateIfRequired(
                    Map<Class<? extends StateDescriptor>, GraphVertex<Class<? extends StateDescriptor>>> map,
                    Class<? extends StateDescriptor> clazz) {
        GraphVertex<Class<? extends StateDescriptor>> vertex = map.get(clazz);
        if (vertex == null) {
            vertex = new GraphVertex<>(clazz);
            map.put(clazz, vertex);
        }
        return vertex;
    }

    private static <E> List<GraphVertex<E>> topologicalSort(Collection<GraphVertex<E>> vertexes) {
        List<GraphVertex<E>> res = new ArrayList<>();
        dfs(vertexes, (vertex) -> res.add(0, vertex), null);
        return res;
    }

    private static <E> void dfsVisit(GraphVertex<E> vertex, DfsFinishedVertex<E> dfsFinishedVertex, DfsEdgeFound<E> dfsEdgeFound) {
        vertex.color = GraphVertex.VertexColor.grey;

        final int totalEdgeCount = vertex.getOutgoingEdges().size();

        int edgeCount = 0;

        for (GraphVertex<E> successorVertex : vertex.getOutgoingEdges()) {
            edgeCount++;
            if (dfsEdgeFound != null) {
                dfsEdgeFound.onEdgeFound(vertex, successorVertex, edgeCount, totalEdgeCount);
            }
            if (successorVertex.color == GraphVertex.VertexColor.white) {
                dfsVisit(successorVertex, dfsFinishedVertex, dfsEdgeFound);
            }
        }

        vertex.color = GraphVertex.VertexColor.black;
        if (dfsFinishedVertex != null) {
            dfsFinishedVertex.onVertexFinished(vertex);
        }
    }

    private static <E> void dfs(Collection<GraphVertex<E>> vertexes, DfsFinishedVertex<E> dfsFinishedVertex, DfsEdgeFound<E> dfsEdgeFound) {
        for (GraphVertex<E> vertex : vertexes) {
            if (vertex.color == GraphVertex.VertexColor.white) {
                dfsVisit(vertex, dfsFinishedVertex, dfsEdgeFound);
            }
        }
    }

    public static <E> void stateDescriptorGraphToDot(Collection<GraphVertex<StateDescriptor>> vertexes,
                    PrintWriter dotOut, boolean breakStateName) {
        dotOut.append("digraph {\n");
        dfs(vertexes,
               (finishedVertex) -> {
                   boolean isMultiVisitState = finishedVertex.element.isMultiVisitState();
                   boolean isFinalState = finishedVertex.element.isFinalState();
                   boolean isNotImplemented = finishedVertex.element.isNotImplemented();

                   String style = null;
                   if (isMultiVisitState) {
                       style = "bold";
                   } else if (isFinalState) {
                       style = "filled";
                   } else if (isNotImplemented) {
                       style = "dashed";
                   }

                   if (style == null) {
                       return;
                   }

                   dotOut.append('"')
                       .append(finishedVertex.element.getFullStateName(breakStateName))
                       .append("\" [ ")
                       .append("style=")
                       .append(style)
                       .append(" ]\n");
               },
               (from, to, edgeId, totalEdgeCount) -> {
                   dotOut.append("  \"")
                       .append(from.element.getFullStateName(breakStateName))
                       .append("\" -> \"")
                       .append(to.element.getFullStateName(breakStateName))
                       .append('"');
                   if (totalEdgeCount > 1) {
                       // Note that 'dot' requires *double* quotes to enclose the value.
                       dotOut.append(" [xlabel=\"")
                       .append(Integer.toString(edgeId))
                       .append("\"]");
                   }
                   dotOut.append(";\n");
               });
        dotOut.append("}\n");
    }

    // TODO: Replace with java.util.function.Consumer<GraphVertex<E>> once Smack's minimum Android SDK level is 24 or higher.
    private interface DfsFinishedVertex<E> {
        void onVertexFinished(GraphVertex<E> vertex);
    }

    // TODO: Replace with java.util.function.Consumer<GraphVertex<E>> once Smack's minimum Android SDK level is 24 or higher.
    private interface DfsEdgeFound<E> {
        void onEdgeFound(GraphVertex<E> from, GraphVertex<E> to, int edgeId, int totalEdgeCount);
    }
}
