/* 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.Reader;
import java.io.Serializable;
import java.net.URI;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.commonjs.module.ModuleScript;
import org.mozilla.javascript.commonjs.module.ModuleScriptProvider;

/**
 * Abstract base class that implements caching of loaded module scripts. It uses a {@link
 * ModuleSourceProvider} to obtain the source text of the scripts. It supports a cache revalidation
 * mechanism based on validator objects returned from the {@link ModuleSourceProvider}. Instances of
 * this class and its subclasses are thread safe (and written to perform decently under concurrent
 * access).
 *
 * @author Attila Szegedi
 * @version $Id: CachingModuleScriptProviderBase.java,v 1.3 2011/04/07 20:26:12 hannes%helma.at Exp
 *     $
 */
public abstract class CachingModuleScriptProviderBase
        implements ModuleScriptProvider, Serializable {
    private static final long serialVersionUID = -1L;
    private static final int loadConcurrencyLevel = Runtime.getRuntime().availableProcessors() * 8;
    private static final int loadLockShift;
    private static final int loadLockMask;
    private static final int loadLockCount;

    static {
        int sshift = 0;
        int ssize = 1;
        while (ssize < loadConcurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        loadLockShift = 32 - sshift;
        loadLockMask = ssize - 1;
        loadLockCount = ssize;
    }

    private final Object[] loadLocks = new Object[loadLockCount];

    {
        for (int i = 0; i < loadLocks.length; ++i) {
            loadLocks[i] = new Object();
        }
    }

    private final ModuleSourceProvider moduleSourceProvider;

    /**
     * Creates a new module script provider with the specified source.
     *
     * @param moduleSourceProvider provider for modules' source code
     */
    protected CachingModuleScriptProviderBase(ModuleSourceProvider moduleSourceProvider) {
        this.moduleSourceProvider = moduleSourceProvider;
    }

    @Override
    public ModuleScript getModuleScript(
            Context cx, String moduleId, URI moduleUri, URI baseUri, Scriptable paths)
            throws Exception {
        final CachedModuleScript cachedModule1 = getLoadedModule(moduleId);
        final Object validator1 = getValidator(cachedModule1);
        final ModuleSource moduleSource =
                (moduleUri == null)
                        ? moduleSourceProvider.loadSource(moduleId, paths, validator1)
                        : moduleSourceProvider.loadSource(moduleUri, baseUri, validator1);
        if (moduleSource == ModuleSourceProvider.NOT_MODIFIED) {
            return cachedModule1.getModule();
        }
        if (moduleSource == null) {
            return null;
        }
        try (Reader reader = moduleSource.getReader()) {
            final int idHash = moduleId.hashCode();
            synchronized (loadLocks[(idHash >>> loadLockShift) & loadLockMask]) {
                final CachedModuleScript cachedModule2 = getLoadedModule(moduleId);
                if (cachedModule2 != null) {
                    if (!equal(validator1, getValidator(cachedModule2))) {
                        return cachedModule2.getModule();
                    }
                }
                final URI sourceUri = moduleSource.getUri();
                final ModuleScript moduleScript =
                        new ModuleScript(
                                cx.compileReader(
                                        reader,
                                        sourceUri.toString(),
                                        1,
                                        moduleSource.getSecurityDomain()),
                                sourceUri,
                                moduleSource.getBase());
                putLoadedModule(moduleId, moduleScript, moduleSource.getValidator());
                return moduleScript;
            }
        }
    }

    /**
     * Store a loaded module script for later retrieval using {@link #getLoadedModule(String)}.
     *
     * @param moduleId the ID of the module
     * @param moduleScript the module script
     * @param validator the validator for the module's source text entity
     */
    protected abstract void putLoadedModule(
            String moduleId, ModuleScript moduleScript, Object validator);

    /**
     * Retrieves an already loaded moduleScript stored using {@link #putLoadedModule(String,
     * ModuleScript, Object)}.
     *
     * @param moduleId the ID of the module
     * @return a cached module script, or null if the module is not loaded.
     */
    protected abstract CachedModuleScript getLoadedModule(String moduleId);

    /**
     * Instances of this class represent a loaded and cached module script.
     *
     * @author Attila Szegedi
     * @version $Id: CachingModuleScriptProviderBase.java,v 1.3 2011/04/07 20:26:12 hannes%helma.at
     *     Exp $
     */
    public static class CachedModuleScript {
        private final ModuleScript moduleScript;
        private final Object validator;

        /**
         * Creates a new cached module script.
         *
         * @param moduleScript the module script itself
         * @param validator a validator for the moduleScript's source text entity.
         */
        public CachedModuleScript(ModuleScript moduleScript, Object validator) {
            this.moduleScript = moduleScript;
            this.validator = validator;
        }

        /**
         * Returns the module script.
         *
         * @return the module script.
         */
        ModuleScript getModule() {
            return moduleScript;
        }

        /**
         * Returns the validator for the module script's source text entity.
         *
         * @return the validator for the module script's source text entity.
         */
        Object getValidator() {
            return validator;
        }
    }

    private static Object getValidator(CachedModuleScript cachedModule) {
        return cachedModule == null ? null : cachedModule.getValidator();
    }

    private static boolean equal(Object o1, Object o2) {
        return o1 == null ? o2 == null : o1.equals(o2);
    }

    /**
     * Returns the internal concurrency level utilized by caches in this JVM.
     *
     * @return the internal concurrency level utilized by caches in this JVM.
     */
    protected static int getConcurrencyLevel() {
        return loadLockCount;
    }
}
