/**
 * This file is part of the Meeds project (https://meeds.io/).
 *
 * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io
 *
 * This program 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 3 of the License, or (at your option) any later version.
 * This program 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 program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
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.gatein.portal.controller.resource.ResourceId;
import org.gatein.portal.controller.resource.ResourceScope;

import org.exoplatform.web.application.javascript.JavascriptConfigParser;
import org.exoplatform.web.application.javascript.JavascriptConfigService;

public class ScriptGraph {

    protected static final Map<String, ScriptGroup>           LOAD_GROUPS = new HashMap<>();

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

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

    /**
     * <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<>();
        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 = LOAD_GROUPS.computeIfAbsent(groupName,
                                                    grpName -> new ScriptGroup(this,
                                                                               new ResourceId(ResourceScope.GROUP, grpName),
                                                                               contextPath));
                group.addDependency(id);
            }
            resource = new ScriptResource(this, id, fetchMode, alias, group);
            map.put(name, resource);
        } 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 LOAD_GROUPS.get(groupName);
    }
}
