/**
 * Copyright (c) André Bargull
 * Alle Rechte vorbehalten / All Rights Reserved.  Use is subject to license terms.
 *
 * <https://github.com/anba/es6draft>
 */
package com.github.anba.es6draft.runtime.modules.loader;

import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import com.github.anba.es6draft.runtime.Realm;
import com.github.anba.es6draft.runtime.internal.RuntimeContext;
import com.github.anba.es6draft.runtime.modules.MalformedNameException;
import com.github.anba.es6draft.runtime.modules.ModuleLoader;
import com.github.anba.es6draft.runtime.modules.ModuleRecord;
import com.github.anba.es6draft.runtime.modules.ModuleSource;
import com.github.anba.es6draft.runtime.modules.SourceIdentifier;

/**
 * 
 */
public abstract class AbstractModuleLoader<MODULE extends ModuleRecord> implements ModuleLoader {
    private final RuntimeContext context;

    protected AbstractModuleLoader(RuntimeContext context) {
        this.context = context;
    }

    /**
     * Returns the runtime context for this loader.
     * 
     * @return the runtime context
     */
    protected RuntimeContext getContext() {
        return context;
    }

    /**
     * Loads the module source.
     * 
     * @param identifier
     *            the source identifier
     * @return the module source object
     * @throws IOException
     *             if there was any I/O error
     */
    protected abstract ModuleSource loadSource(SourceIdentifier identifier) throws IOException;

    /**
     * Adds a module record to the internal modules list.
     * 
     * @param module
     *            the module record
     */
    protected abstract void defineModule(MODULE module);

    /**
     * Retrieves a module record from the internal modules list.
     * 
     * @param identifier
     *            the source identifier
     * @return the module record or {@code null} if not found
     */
    protected abstract MODULE getModule(SourceIdentifier identifier);

    /**
     * Parses a module record.
     * 
     * @param identifier
     *            the source identifier
     * @param source
     *            the module source
     * @return the parsed module record
     * @throws IOException
     *             if there was any I/O error
     */
    protected abstract MODULE parseModule(SourceIdentifier identifier, ModuleSource source) throws IOException;

    /**
     * Links the module against the realm instance.
     * 
     * @param module
     *            the module record
     * @param realm
     *            the realm instance
     */
    protected abstract void linkModule(MODULE module, Realm realm);

    /**
     * Returns the list of requested modules.
     * 
     * @param module
     *            the module record
     * @return the list of unnormalized module names
     */
    protected abstract Set<String> getRequestedModules(MODULE module);

    /**
     * Returns an unmodifiable collection of the currently loaded modules.
     * <p>
     * Optional operation: Returns an empty collection if not supported.
     * 
     * @return the module collection
     */
    protected Collection<MODULE> getModules() {
        return Collections.emptySet();
    }

    @Override
    public MODULE get(SourceIdentifier identifier, Realm realm) {
        MODULE module = getModule(identifier);
        if (module != null) {
            linkModule(module, realm);
        }
        return module;
    }

    @Override
    public MODULE define(SourceIdentifier identifier, ModuleSource source, Realm realm) throws IOException {
        MODULE module = getModule(identifier);
        if (module == null) {
            module = parseModule(identifier, source);
            defineModule(module);
        }
        linkModule(module, realm);
        return module;
    }

    @Override
    public MODULE resolve(SourceIdentifier identifier, Realm realm) throws IOException {
        MODULE module = loadIfAbsent(identifier);
        linkModule(module, realm);
        return module;
    }

    @Override
    public MODULE load(SourceIdentifier identifier) throws MalformedNameException, IOException {
        MODULE module = loadIfAbsent(identifier);
        loadRequested(module, new HashSet<>());
        return module;
    }

    private MODULE loadIfAbsent(SourceIdentifier identifier) throws IOException {
        MODULE module = getModule(identifier);
        if (module == null) {
            module = parseModule(identifier, loadSource(identifier));
            defineModule(module);
        }
        return module;
    }

    private void loadRequested(MODULE module, HashSet<ModuleRecord> visited)
            throws MalformedNameException, IOException {
        if (visited.add(module)) {
            SourceIdentifier referrerId = module.getSourceCodeId();
            for (String specifier : getRequestedModules(module)) {
                SourceIdentifier identifier = normalizeName(specifier, referrerId);
                loadRequested(loadIfAbsent(identifier), visited);
            }
        }
    }
}
