// Copyright 2006, 2007, 2009 The Apache Software Foundation
//
// 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.apache.tapestry5.ioc.internal.util;

import org.apache.tapestry5.commons.util.CollectionFactory;
import org.apache.tapestry5.ioc.IdMatcher;
import org.apache.tapestry5.ioc.Orderable;
import org.apache.tapestry5.ioc.internal.IdMatcherImpl;
import org.apache.tapestry5.ioc.internal.OrIdMatcher;
import org.slf4j.Logger;

import static org.apache.tapestry5.commons.util.CollectionFactory.newList;

import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
 * Used to order objects into an "execution" order. Each object must have a unique id. It may specify a list of
 * constraints which identify the ordering of the objects.
 */
public class Orderer<T>
{
    private final OneShotLock lock = new OneShotLock();

    private final Logger logger;
    
    private final boolean exceptionWhenDuplicateId;

    private final List<Orderable> orderables = CollectionFactory.newList();

    private final Map<String, Orderable<T>> idToOrderable = CollectionFactory.newCaseInsensitiveMap();

    private final Map<String, DependencyNode<T>> dependencyNodesById = CollectionFactory.newCaseInsensitiveMap();

    // Special node that is always dead last: all other nodes are a dependency
    // of the trailer.

    private DependencyNode<T> trailer;

    interface DependencyLinker<T>
    {
        void link(DependencyNode<T> source, DependencyNode<T> target);
    }

    // before: source is added as a dependency of target, so source will
    // appear before target.

    final DependencyLinker<T> _before = new DependencyLinker<T>()
    {
        @Override
        public void link(DependencyNode<T> source, DependencyNode<T> target)
        {
            target.addDependency(source);
        }
    };

    // after: target is added as a dependency of source, so source will appear
    // after target.

    final DependencyLinker<T> _after = new DependencyLinker<T>()
    {
        @Override
        public void link(DependencyNode<T> source, DependencyNode<T> target)
        {
            source.addDependency(target);
        }
    };

    public Orderer(Logger logger)
    {
        this(logger, false);
    }
    
    public Orderer(Logger logger, boolean exceptionWhenDuplicateId)
    {
        this.logger = logger;
        this.exceptionWhenDuplicateId = exceptionWhenDuplicateId;
    }

    /**
     * Adds an object to be ordered.
     *
     * @param orderable
     */
    public void add(Orderable<T> orderable)
    {
        lock.check();

        String id = orderable.getId();

        if (idToOrderable.containsKey(id))
        {
            final String message = UtilMessages.duplicateOrderer(id);
            if (exceptionWhenDuplicateId) {
                throw new IllegalArgumentException(message);
            }
            else {
                logger.warn(message);
                return;
            }
        }

        orderables.add(orderable);

        idToOrderable.put(id, orderable);
    }

    public void override(Orderable<T> orderable)
    {
        lock.check();

        String id = orderable.getId();

        Orderable<T> existing = idToOrderable.get(id);

        if (existing == null)
            throw new IllegalArgumentException(
                    String.format("Override for object '%s' is invalid as it does not match an existing object.", id));

        orderables.remove(existing);
        orderables.add(orderable);

        idToOrderable.put(id, orderable);
    }

    /**
     * Adds an object to be ordered.
     *
     * @param id          unique, qualified id for the target
     * @param target      the object to be ordered (or null as a placeholder)
     * @param constraints optional, variable constraints
     * @see #add(Orderable)
     */

    public void add(String id, T target, String... constraints)
    {
        lock.check();

        add(new Orderable<T>(id, target, constraints));
    }

    public void override(String id, T target, String... constraints)
    {
        lock.check();

        override(new Orderable<T>(id, target, constraints));
    }

    public List<T> getOrdered()
    {
        lock.lock();

        initializeGraph();

        List<T> result = newList();

        for (Orderable<T> orderable : trailer.getOrdered())
        {
            T target = orderable.getTarget();

            // Nulls are placeholders that are skipped.

            if (target != null) result.add(target);
        }

        return result;
    }

    private void initializeGraph()
    {
        trailer = new DependencyNode<T>(logger, new Orderable<T>("*-trailer-*", null));

        addNodes();

        addDependencies();
    }

    private void addNodes()
    {
        for (Orderable<T> orderable : orderables)
        {
            DependencyNode<T> node = new DependencyNode<T>(logger, orderable);

            dependencyNodesById.put(orderable.getId(), node);

            trailer.addDependency(node);
        }
    }

    private void addDependencies()
    {
        for (Orderable<T> orderable : orderables)
        {
            addDependencies(orderable);
        }
    }

    private void addDependencies(Orderable<T> orderable)
    {
        String sourceId = orderable.getId();

        for (String constraint : orderable.getConstraints())
        {
            addDependencies(sourceId, constraint);
        }
    }

    private void addDependencies(String sourceId, String constraint)
    {
        int colonx = constraint.indexOf(':');

        String type = colonx > 0 ? constraint.substring(0, colonx) : null;

        DependencyLinker<T> linker = null;

        if ("after".equals(type))
            linker = _after;
        else if ("before".equals(type)) linker = _before;

        if (linker == null)
        {
            logger.warn(UtilMessages.constraintFormat(constraint, sourceId));
            return;
        }

        String patternList = constraint.substring(colonx + 1);

        linkNodes(sourceId, patternList, linker);
    }

    private void linkNodes(String sourceId, String patternList, DependencyLinker<T> linker)
    {
        Collection<DependencyNode<T>> nodes = findDependencies(sourceId, patternList);

        DependencyNode<T> source = dependencyNodesById.get(sourceId);

        for (DependencyNode<T> target : nodes)
        {
            linker.link(source, target);
        }
    }

    private Collection<DependencyNode<T>> findDependencies(String sourceId, String patternList)
    {
        IdMatcher matcher = buildMatcherForPattern(patternList);

        Collection<DependencyNode<T>> result = newList();

        for (String id : dependencyNodesById.keySet())
        {
            if (sourceId.equals(id)) continue;

            if (matcher.matches(id)) result.add(dependencyNodesById.get(id));
        }

        return result;
    }

    private IdMatcher buildMatcherForPattern(String patternList)
    {
        List<IdMatcher> matchers = newList();

        for (String pattern : patternList.split(","))
        {
            IdMatcher matcher = new IdMatcherImpl(pattern.trim());

            matchers.add(matcher);
        }

        return matchers.size() == 1 ? matchers.get(0) : new OrIdMatcher(matchers);
    }
}
