/*
 * Copyright (C) 2011 eXo Platform SAS.
 *
 * 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.gatein.portal.controller.resource.script;

import java.util.Collection;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.Map;

import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;
import org.exoplatform.web.application.javascript.JavascriptConfigParser;
import org.exoplatform.web.application.javascript.JavascriptConfigService;
import org.gatein.portal.controller.resource.ResourceId;
import org.gatein.portal.controller.resource.ResourceScope;

/**
 * @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a>
 */
public class ScriptGraph {

    /** . */
    final EnumMap<ResourceScope, Map<String, ScriptResource>> resources;

    /** . */
    final Map<String, ScriptGroup> loadGroups;

    /** . */
    private static final Log log = ExoLogger.getExoLogger(ScriptGraph.class);

    public ScriptGraph() {
        EnumMap<ResourceScope, Map<String, ScriptResource>> resources = new EnumMap<ResourceScope, Map<String, ScriptResource>>(
                ResourceScope.class);
        for (ResourceScope scope : ResourceScope.values()) {
            resources.put(scope, new HashMap<String, ScriptResource>());
        }

        //
        this.resources = resources;
        this.loadGroups = new HashMap<String, ScriptGroup>();
    }

    /**
     * <p>
     * Resolve a collection of pair of resource id and fetch mode, each entry of the map will be processed in the order
     * specified by the iteration of the {@link java.util.Map#entrySet()}. For a given pair the fetch mode may be null or not.
     * When the fetch mode is null, the default fetch mode of the resource is used. When the fetch mode is not null, this fetch
     * mode may override the resource fetch mode if it implies this particular fetch mode. This algorithm tolerates the absence
     * of resources, for instance if a resource is specified (among the pairs or by a transitive dependency) and does not exist,
     * the resource will be skipped.
     * </p>
     *
     * @param pairs the pairs to resolve
     * @return the resources sorted
     */
    public Map<ScriptResource, FetchMode> resolve(Map<ResourceId, FetchMode> pairs) {
        // Build a fetch graph
        Map<ResourceId, ScriptFetch> determined = new HashMap<ResourceId, ScriptFetch>();
        for (Map.Entry<ResourceId, FetchMode> pair : pairs.entrySet()) {
            traverse(determined, pair.getKey(), pair.getValue());
        }

        // We remove one by one the nodes of the fetch graph having no dependencies
        // each node will build the dependency list
        LinkedHashMap<ScriptResource, FetchMode> result = new LinkedHashMap<ScriptResource, FetchMode>();
        LinkedList<ScriptFetch> all = new LinkedList<ScriptFetch>(determined.values());
        while (all.size() > 0) {
            ScriptFetch next = null;
            for (Iterator<ScriptFetch> i = all.iterator(); i.hasNext();) {
                ScriptFetch fetch = i.next();
                if (fetch.dependencies.size() == 0) {
                    i.remove();
                    next = fetch;
                    for (ScriptFetch dependent : fetch.dependsOnMe) {
                        dependent.dependencies.remove(fetch);
                    }
                    break;
                }
            }
            if (next == null) {
                // This should not happen:
                // we have an DAG, on each iteration we must have at least one node that has no dependencies
                // we remove it from the graph and update its dependencies
                // (unless the graph is not correctly constructed above)
                throw new AssertionError("This is a bug");
            } else {
                result.put(next.resource, next.mode);
            }
        }

        //
        return result;
    }

    private ScriptFetch traverse(Map<ResourceId, ScriptFetch> map, ResourceId id, FetchMode mode) {
        ScriptResource resource = getResource(id);
        if (resource != null) {
            if (mode != null && !resource.fetchMode.equals(mode)) {
                return null;
            } else {
                mode = resource.fetchMode;
                ScriptFetch fetch = map.get(id);
                if (fetch == null) {
                    fetch = new ScriptFetch(resource, mode);
                    if (!resource.isEmpty() || ResourceScope.SHARED.equals(resource.getId().getScope())) {
                        map.put(id, fetch);
                    }

                    // Recursively add the dependencies
                    if (FetchMode.IMMEDIATE.equals(mode) || resource.isEmpty()) {
                        for (ResourceId dependencyId : resource.dependencies.keySet()) {
                            ScriptFetch dependencyFetch = traverse(map, dependencyId, mode);
                            if (dependencyFetch != null) {
                                dependencyFetch.dependsOnMe.add(fetch);
                                fetch.dependencies.add(dependencyFetch);
                            }
                        }
                    }
                }
                return fetch;
            }
        } else {
            return null;
        }
    }

    public ScriptResource getResource(ResourceId id) {
        return getResource(id.getScope(), id.getName());
    }

    public ScriptResource getResource(ResourceScope scope, String name) {
        return resources.get(scope).get(name);
    }

    public Collection<ScriptResource> getResources(ResourceScope scope) {
        return resources.get(scope).values();
    }

    public ScriptResource addResource(ResourceId id) {
        return addResource(id, FetchMode.IMMEDIATE);
    }

    public ScriptResource addResource(ResourceId id, FetchMode fetchMode) throws NullPointerException {
        return addResource(id, fetchMode, null, null, null);
    }

    /**
     * Add a resource to the graph if available. return null if ResourceID is duplicated
     *
     * @param id the resource id
     * @param fetchMode the resource fetch mode
     * @param alias default alias
     * @return the resource
     * @throws NullPointerException if id or fetchMode is null
     */
    public ScriptResource addResource(ResourceId id, FetchMode fetchMode, String alias, String groupName, String contextPath)
            throws NullPointerException {
        if (id == null) {
            throw new NullPointerException("No null resource accepted");
        }
        if (fetchMode == null) {
            throw new NullPointerException("No null fetch mode accepted");
        }

        //
        if (ResourceScope.SHARED.equals(id.getScope())) {
            for (Map<String, ScriptResource> mp : resources.values()) {
                for (ScriptResource rs : mp.values()) {
                    if (rs.getDependencies().contains(id) && !rs.getFetchMode().equals(fetchMode)) {
                        throw new IllegalStateException("ScriptResource " + rs.getId() + " can't depend on " + id
                                + ". They have difference fetchMode");
                    }
                }
            }

            if (JavascriptConfigService.RESERVED_MODULE.contains(id.getName())) {
                throw new IllegalStateException("Can't not add " + id + ". The name " + id.getName() + " is reserved name");
            }
        }

        //
        Map<String, ScriptResource> map = resources.get(id.getScope());
        String name = id.getName();
        ScriptResource resource = map.get(name);
        if (resource == null) {
            ScriptGroup group = null;
            if (groupName != null && contextPath != null) {
                group = loadGroups.get(groupName);
                if (group == null) {
                    ResourceId grpId = new ResourceId(ResourceScope.GROUP, groupName);
                    loadGroups.put(groupName, group = new ScriptGroup(this, grpId, contextPath));
                    group.addDependency(id);
                } else if (!contextPath.equals(group.contextPath)) {
                    log.warn("Can't add cross context resource {} to {} group", id, groupName);
                    group = null;
                } else {
                    group.addDependency(id);
                }
            }

            map.put(name, resource = new ScriptResource(this, id, fetchMode, alias, group));
        } else if (!(id.getScope().equals(ResourceScope.SHARED) && JavascriptConfigParser.LEGACY_JAVA_SCRIPT.equals(name))) {
            throw new IllegalStateException("Duplicate ResourceId : " + id + ", later resource definition will be ignored");
        }

        return resource;
    }

    public ScriptResource removeResource(ResourceId id) {
        return removeResource(id, null);
    }

    public ScriptResource removeResource(ResourceId id, String contextPath) {
        ScriptResource toRemove = resources.get(id.getScope()).get(id.getName());

        if (toRemove != null) {
            if (ResourceScope.SHARED.equals(id.getScope()) && JavascriptConfigParser.LEGACY_JAVA_SCRIPT.equals(id.getName())) {
                toRemove.removeModuleByContextPath(contextPath);

                if (toRemove.getModules().size() > 0)
                    return toRemove;
            }

            resources.get(id.getScope()).remove(id.getName());
            toRemove.graph = null;
        }

        return toRemove;
    }

    public ScriptGroup getLoadGroup(String groupName) {
        return loadGroups.get(groupName);
    }
}
