/* 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.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.Script;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.commonjs.module.ModuleScript;

/**
 * A module script provider that uses a module source provider to load modules
 * and caches the loaded modules. It softly references the loaded modules'
 * Rhino {@link Script} objects, thus a module once loaded can become eligible
 * for garbage collection if it is otherwise unused under memory pressure.
 * Instances of this class are thread safe.
 * @author Attila Szegedi
 * @version $Id: SoftCachingModuleScriptProvider.java,v 1.3 2011/04/07 20:26:12 hannes%helma.at Exp $
 */
public class SoftCachingModuleScriptProvider extends CachingModuleScriptProviderBase {
    private static final long serialVersionUID = 1L;
    private transient ReferenceQueue<Script> scriptRefQueue = new ReferenceQueue<Script>();
    private transient ConcurrentMap<String, ScriptReference> scripts = new ConcurrentHashMap<String, ScriptReference>(16, .75f, getConcurrencyLevel());

    /**
     * Creates a new module provider with the specified module source provider.
     * @param moduleSourceProvider provider for modules' source code
     */
    public SoftCachingModuleScriptProvider(
            ModuleSourceProvider moduleSourceProvider)
    {
        super(moduleSourceProvider);
    }

    @Override
    public ModuleScript getModuleScript(Context cx, String moduleId,
            URI uri, URI base, Scriptable paths)
            throws Exception
    {
        // Overridden to clear the reference queue before retrieving the
        // script.
        for(;;) {
            ScriptReference ref = (ScriptReference)scriptRefQueue.poll();
            if(ref == null) {
                break;
            }
            scripts.remove(ref.getModuleId(), ref);
        }
        return super.getModuleScript(cx, moduleId, uri, base, paths);
    }

    @Override
    protected CachedModuleScript getLoadedModule(String moduleId) {
        final ScriptReference scriptRef = scripts.get(moduleId);
        return scriptRef != null ? scriptRef.getCachedModuleScript() : null;
    }

    @Override
    protected void putLoadedModule(String moduleId, ModuleScript moduleScript,
            Object validator)
    {
        scripts.put(moduleId, new ScriptReference(moduleScript.getScript(),
                moduleId, moduleScript.getUri(), moduleScript.getBase(),
                validator, scriptRefQueue));
    }

    private static class ScriptReference extends SoftReference<Script> {
        private final String moduleId;
        private final URI uri;
        private final URI base;
        private final Object validator;

        ScriptReference(Script script, String moduleId, URI uri, URI base,
                Object validator, ReferenceQueue<Script> refQueue) {
            super(script, refQueue);
            this.moduleId = moduleId;
            this.uri = uri;
            this.base = base;
            this.validator = validator;
        }

        CachedModuleScript getCachedModuleScript() {
            final Script script = get();
            if(script == null) {
                return null;
            }
            return new CachedModuleScript(new ModuleScript(script, uri, base),
                    validator);
        }

        String getModuleId() {
            return moduleId;
        }
    }

    @SuppressWarnings("unchecked")
    private void readObject(ObjectInputStream in) throws IOException,
    ClassNotFoundException
    {
        scriptRefQueue = new ReferenceQueue<Script>();
        scripts = new ConcurrentHashMap<String, ScriptReference>();
        final Map<String, CachedModuleScript> serScripts = (Map)in.readObject();
        for(Map.Entry<String, CachedModuleScript> entry: serScripts.entrySet()) {
            final CachedModuleScript cachedModuleScript = entry.getValue();
            putLoadedModule(entry.getKey(), cachedModuleScript.getModule(),
                    cachedModuleScript.getValidator());
        }
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        final Map<String, CachedModuleScript> serScripts =
            new HashMap<String, CachedModuleScript>();
        for(Map.Entry<String, ScriptReference> entry: scripts.entrySet()) {
            final CachedModuleScript cachedModuleScript =
                entry.getValue().getCachedModuleScript();
            if(cachedModuleScript != null) {
                serScripts.put(entry.getKey(), cachedModuleScript);
            }
        }
        out.writeObject(serScripts);
    }
}