/*
 * 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.axis2.engine;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * A DeployableChain is a container which manages dependencies between Deployables.  You
 * deploy() them in, then call rebuild() which will set up a chain, correctly ordered according
 * to the constraints in the Deployables.
 */
public class DeployableChain<T> {
    /** The actual things (handlers or phases) */
    List<Object> chain = new ArrayList<Object>();

    Deployable first;
    Deployable last;

    /** A Map of name -> List (of Strings).  Each List contains the key's successors */
    Map<String, Set<String>> activeConstraints = new LinkedHashMap<String, Set<String>>();

    /** A Map of name -> Deployable for all deployed items */
    private Map<String, Deployable> deployed = new LinkedHashMap<String, Deployable>();

    /**
     * Deploy a Deployable into this chain.  Note that this does NOT order yet.  The idea
     * is to deploy everything first, then call rebuild() to generate the fully ordered chain.
     * This method will validate the Deployable, including confirming that it's compatible with
     * any previously deployed item of the same name.
     *
     * @param deployable Deployable to deploy.
     * @throws Exception if there is a deployment error.
     */
    public void deploy(Deployable deployable) throws Exception {
        String name = deployable.getName();
        Set<String> mySuccessors = deployable.getSuccessors();
        Set<String> myPredecessors = deployable.getPredecessors();

        if (deployable.isFirst()) {
            if (first != null) {
                throw new Exception("'" + first.getName() + "' is already first, can't deploy '" +
                    name + "' as first also.");
            }
            if (myPredecessors != null) {
                throw new Exception("Deploying '" + name +
                        "' - can't both be first and have predecessors!");
            }
            first = deployable;
        }

        if (deployable.isLast()) {
            if (last != null) {
                throw new Exception("'" + last.getName() + "' is already last, can't deploy '" +
                        name + "' as last also.");
            }
            if (mySuccessors != null) {
                throw new Exception("Deploying '" + name +
                        "' - can't both be last and have successors!");
            }
            last = deployable;
        }

        Deployable previous = (Deployable)deployed.get(name);
        if (previous == null) {
            deployed.put(name, deployable);
        } else {
            // If something by this name already exists, ensure it's compatible
            if (previous.isFirst() != deployable.isFirst()) {
                throw new Exception("Can't deploy '" + name + "', values for first don't match!");
            }
            if (previous.isLast() != deployable.isLast()) {
                throw new Exception("Can't deploy '" + name + "', values for last don't match!");
            }
            Object target = previous.getTarget();
            if (target != null) {
                if (deployable.getTarget() != null && !target.equals(deployable.getTarget())) {
                    throw new Exception("Can't deploy '" + name +
                            "',  targets must either match or be null.");
                }
            } else {
                previous.setTarget(deployable.getTarget());
            }
        }

        if (mySuccessors != null && !mySuccessors.isEmpty()) {
            Set<String> successors = activeConstraints.get(name);
            if (successors == null) {
                successors = new LinkedHashSet<String>();
                activeConstraints.put(name, successors);
            }
            successors.addAll(mySuccessors);
        }

        if (myPredecessors != null) {
            for (Object myPredecessor : myPredecessors) {
                String predecessor = (String)myPredecessor;
                // define relationship for each one
                addRelationship(predecessor, name);
            }
        }
    }

    /**
     * Find the correct (minimum) index for a given name.  Finds the active constraints
     * for the name, then walks that list, making sure that each one is already placed.
     * If a given successor isn't yet placed, we recursively call this method to place it,
     * passing a set of "what's left to do" and a set of "what we've seen".
     *
     * This results in chain containing the names (Strings).
     *
     * TODO: investigate optimizing this algorithm a bit
     *
     * @param name the target deployable's name
     * @param remaining a Set of the names of things we have yet to deploy
     * @param seen a Set of the names of things we've already seen (to detect loops)
     * @return an index >=0 or -1 if there was a dependency cycle
     * @throws Exception if there's a cyclic dependency
     */
    private int getMinIndex(String name, Set<String> remaining, Set<String> seen) throws Exception {
        if (seen.contains(name)) {
            // We return -1 here instead of throwing the Exception so we can build a better
            // error message below.
            return -1;
        }

        Set<String> successors = activeConstraints.get(name);
        if (successors == null || successors.isEmpty()) {
            // Never put anything after the thing marked "last"...
            int index = (last == null) ? chain.size() : chain.size() - 1;
            chain.add(index, name);
            remaining.remove(name);
            return index;
        }

        int minIndex = -1;
        for (Object successor : successors) {
            String otherName = (String)successor;
            int otherIdx = chain.indexOf(otherName);
            if (otherIdx > -1) {
                if ((minIndex == -1) || (minIndex > otherIdx)) {
                    minIndex = otherIdx;
                }
            } else {
                // This guy isn't in our list yet - put him in IF he's real (exists in
                // the deployed list) and we haven't already placed him (exists in
                // the remaining set).
                if ((deployed.get(otherName) != null) && remaining.contains(otherName)) {
                    seen.add(name);
                    // Not in list yet, go place it
                    int min = getMinIndex(otherName, remaining, seen);
                    if (minIndex == -1 || min < minIndex) {
                        minIndex = min;
                    }
                    if (minIndex == -1) {
                        throw new Exception("Trying to put '" + name + "' before '" +
                                            otherName + "' - incompatible constraints!");
                    }
                }
            }
        }

        if (minIndex == -1) minIndex = 0;

        chain.add(minIndex, name);
        remaining.remove(name);

        return minIndex;
    }

    /**
     * Taking into account all the active constraints, order the list.  This blows
     * away the old order.  Could improve this slightly with a "dirty" flag.
     *
     * @throws Exception if there's an ordering conflict
     */
    public void rebuild() throws Exception {
        chain.clear();

        Set<String> keys = new LinkedHashSet<String>();
        keys.addAll(deployed.keySet());

        // First goes first.
        if (first != null) {
            chain.add(first.getName());
            keys.remove(first.getName());
        }

        // Last goes next, and everything else will get inserted before it if it exists.
        if (last != null) {
            Set<String> afterLast = activeConstraints.get(last.getName());
            if (afterLast != null) {
                throw new Exception("Can't have anything which goes after '" + last.getName() +
                        "', which has been declared last.");
            }
            chain.add(last.getName());
            keys.remove(last.getName());
        }

        while (!keys.isEmpty()) {
            String name = (String)keys.iterator().next();
            getMinIndex(name, keys, new LinkedHashSet<String>());
        }

        // Now we've got a chain of names.  Convert to actual things before we return.
        for (int i = 0; i < chain.size(); i++) {
            String name = (String)chain.get(i);
            chain.set(i, ((Deployable)deployed.get(name)).getTarget());
        }
    }

    /**
     * Adds a before/after relationship to the active constraints.
     *
     * @param before name of the Deployable that must come first
     * @param after name of the Deployable that must come later
     */
    public void addRelationship(String before, String after) {
        Set<String> successors = activeConstraints.get(before);
        if (successors == null) {
            successors = new LinkedHashSet<String>();
            activeConstraints.put(before, successors);
        }
        successors.add(after);
    }

    /**
     * Get the chain - once rebuild() has been called this will be the chain of target objects.
     *
     * @return a List of target objects in the correct order
     */
    public List<T> getChain() {
        // todo - should this call rebuild() automatically (if dirty flag is set)?
        return (List<T>) chain;
    }
}
