/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.javascript.commonjs.module.provider;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import org.mozilla.javascript.ScriptRuntime;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;

/**
 * A base implementation for all module script providers that actually load module scripts. Performs
 * validation of identifiers, allows loading from preferred locations (attempted before
 * require.paths), from require.paths itself, and from fallback locations (attempted after
 * require.paths). Note that while this base class strives to be as generic as possible, it does
 * have loading from an URI built into its design, for the simple reason that the require.paths is
 * defined in terms of URIs.
 *
 * @author Attila Szegedi
 * @version $Id: ModuleSourceProviderBase.java,v 1.3 2011/04/07 20:26:12 hannes%helma.at Exp $
 */
public abstract class ModuleSourceProviderBase implements ModuleSourceProvider, Serializable {
    private static final long serialVersionUID = 1L;

    @Override
    public ModuleSource loadSource(String moduleId, Scriptable paths, Object validator)
            throws IOException, URISyntaxException {
        if (!entityNeedsRevalidation(validator)) {
            return NOT_MODIFIED;
        }

        ModuleSource moduleSource = loadFromPrivilegedLocations(moduleId, validator);
        if (moduleSource != null) {
            return moduleSource;
        }
        if (paths != null) {
            moduleSource = loadFromPathArray(moduleId, paths, validator);
            if (moduleSource != null) {
                return moduleSource;
            }
        }
        return loadFromFallbackLocations(moduleId, validator);
    }

    @Override
    public ModuleSource loadSource(URI uri, URI base, Object validator)
            throws IOException, URISyntaxException {
        return loadFromUri(uri, base, validator);
    }

    private ModuleSource loadFromPathArray(String moduleId, Scriptable paths, Object validator)
            throws IOException {
        final long llength = ScriptRuntime.toUint32(ScriptableObject.getProperty(paths, "length"));
        // Yeah, I'll ignore entries beyond Integer.MAX_VALUE; so sue me.
        int ilength = llength > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) llength;

        for (int i = 0; i < ilength; ++i) {
            final String path =
                    ensureTrailingSlash(ScriptableObject.getTypedProperty(paths, i, String.class));
            try {
                URI uri = new URI(path);
                if (!uri.isAbsolute()) {
                    uri = new File(path).toURI().resolve("");
                }
                final ModuleSource moduleSource =
                        loadFromUri(uri.resolve(moduleId), uri, validator);
                if (moduleSource != null) {
                    return moduleSource;
                }
            } catch (URISyntaxException e) {
                throw new MalformedURLException(e.getMessage());
            }
        }
        return null;
    }

    private static String ensureTrailingSlash(String path) {
        return path.endsWith("/") ? path : path.concat("/");
    }

    /**
     * Override to determine whether according to the validator, the cached module script needs
     * revalidation. A validator can carry expiry information. If the cached representation is not
     * expired, it doesn' t need revalidation, otherwise it does. When no cache revalidation is
     * required, the external resource will not be contacted at all, so some level of expiry
     * (staleness tolerance) can greatly enhance performance. The default implementation always
     * returns true so it will always require revalidation.
     *
     * @param validator the validator
     * @return returns true if the cached module needs revalidation.
     */
    protected boolean entityNeedsRevalidation(Object validator) {
        return true;
    }

    /**
     * Override in a subclass to load a module script from a logical URI. The URI is absolute but
     * does not have a file name extension such as ".js". It is up to the ModuleSourceProvider
     * implementation to add such an extension.
     *
     * @param uri the URI of the script, without file name extension.
     * @param base the base URI the uri was resolved from.
     * @param validator a validator that can be used to revalidate an existing cached source at the
     *     URI. Can be null if there is no cached source available.
     * @return the loaded module script, or null if it can't be found, or {@link
     *     ModuleSourceProvider#NOT_MODIFIED} if it revalidated the existing cached source against
     *     the URI.
     * @throws IOException if the module script was found, but an I/O exception prevented it from
     *     being loaded.
     * @throws URISyntaxException if the final URI could not be constructed
     */
    protected abstract ModuleSource loadFromUri(URI uri, URI base, Object validator)
            throws IOException, URISyntaxException;

    /**
     * Override to obtain a module source from privileged locations. This will be called before
     * source is attempted to be obtained from URIs specified in require.paths.
     *
     * @param moduleId the ID of the module
     * @param validator a validator that can be used to validate an existing cached script. Can be
     *     null if there is no cached script available.
     * @return the loaded module script, or null if it can't be found in the privileged locations,
     *     or {@link ModuleSourceProvider#NOT_MODIFIED} if the existing cached module script is
     *     still valid.
     * @throws IOException if the module script was found, but an I/O exception prevented it from
     *     being loaded.
     * @throws URISyntaxException if the final URI could not be constructed.
     */
    protected ModuleSource loadFromPrivilegedLocations(String moduleId, Object validator)
            throws IOException, URISyntaxException {
        return null;
    }

    /**
     * Override to obtain a module source from fallback locations. This will be called after source
     * is attempted to be obtained from URIs specified in require.paths.
     *
     * @param moduleId the ID of the module
     * @param validator a validator that can be used to validate an existing cached script. Can be
     *     null if there is no cached script available.
     * @return the loaded module script, or null if it can't be found in the privileged locations,
     *     or {@link ModuleSourceProvider#NOT_MODIFIED} if the existing cached module script is
     *     still valid.
     * @throws IOException if the module script was found, but an I/O exception prevented it from
     *     being loaded.
     * @throws URISyntaxException if the final URI could not be constructed.
     */
    protected ModuleSource loadFromFallbackLocations(String moduleId, Object validator)
            throws IOException, URISyntaxException {
        return null;
    }
}
