/*
 * Snow.java
 *
 * Copyright (C) 2008-2009 Alessio Stalla
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * As a special exception, the copyright holders of this library give you
 * permission to link this library with independent modules to produce an
 * executable, regardless of the license terms of these independent
 * modules, and to copy and distribute the resulting executable under
 * terms of your choice, provided that you also meet, for each linked
 * independent module, the terms and conditions of the license of that
 * module.  An independent module is a module which is not derived from
 * or based on this library.  If you modify this library, you may extend
 * this exception to your version of the library, but you are not
 * obligated to do so.  If you do not wish to do so, delete this
 * exception statement from your version.
 */

package snow;

import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.script.*;

import java.security.*;

import org.armedbear.lisp.Interpreter;

import com.jgoodies.binding.PresentationModel;

import java.io.*;

public abstract class Snow {

    private static boolean init = false;
    private static ScriptEngine lispEngine;
    private static final String fileSeparator = System.getProperty("file.separator");
        
    /**
     * This method is public only because it needs to be called from Lisp.
     * Do not call it.
     */
    public static synchronized void initAux() throws ScriptException {
	if(!init) {
	    try {
		lispEngine = new ScriptEngineManager(Snow.class.getClassLoader()).getEngineByExtension("lisp");
	    } catch(final Throwable t) {
	    }
	    URL url = Snow.class.getResource("/snow/snow.asd");
	    if(url == null) {
		throw new RuntimeException("snow.asd not found in classpath: have you installed Snow correctly?");
	    }
	    String baseDir;
	    String libDir;
	    if(!"file".equals(url.getProtocol())) {
		if("jar".equals(url.getProtocol())) {
		    ZipInputStream extractor = null;
		    try {
			String tmpDir = System.getProperty("java.io.tmpdir");
			if(tmpDir != null && fileSeparator != null) {
			    tmpDir = fixDirPath(tmpDir);
			    String jarUrlStr = url.getPath();
			    int bangPos = jarUrlStr.indexOf('!');
			    if(bangPos >= 0) {
				jarUrlStr = jarUrlStr.substring(0, bangPos);
			    }
			    URL jarUrl = new URL(jarUrlStr);
			    extractor = new ZipInputStream(jarUrl.openStream());
			    int targetDirIndex = 0;
			    File targetDir;
			    do {
				targetDir = new File(tmpDir + "snow" + (targetDirIndex++));
			    } while(targetDir.exists());
			    targetDir.mkdir();
			    targetDir.deleteOnExit();
			    baseDir = targetDir.getAbsolutePath();
			    baseDir = fixDirPath(baseDir);
			    libDir = baseDir;
			    for(ZipEntry entry = extractor.getNextEntry(); entry != null; entry = extractor.getNextEntry()) {
				File extracted = new File(baseDir + entry.getName());
				extracted.deleteOnExit();
				if(entry.isDirectory()) {
				    extracted.mkdirs();
				} else {
				    extracted.getParentFile().mkdirs();
				    FileOutputStream fos = new FileOutputStream(extracted);
				    byte[] buf = new byte[4096];
				    while(true) {
					int read = extractor.read(buf);
					if(read != -1) {
					    fos.write(buf, 0, read);
					} else {
					    fos.flush();
					    fos.close();
					    break;
					}
				    }
				}
				extracted.setLastModified(entry.getTime());
				System.out.println("Extracted " + extracted.getAbsolutePath());
			    }
			} else {
			    throw new RuntimeException("Cannot extract jar " + url + " - no temp dir or file separator defined");
			}
		    } catch(Exception e) {
			throw new RuntimeException("Cannot extract jar " + url, e);
		    } finally {
			if(extractor != null) {
			    try {
				extractor.close();
			    } catch (IOException e) {
				System.err.println("Couldn't close jar extractor: " + e.getMessage());
				e.printStackTrace();
			    }
			}
		    }
		} else {
		    throw new RuntimeException("Unsupported URL for snow.asd: " + url +
					       " make sure it is a regular file or is in a jar.");
		}
	    } else {
		URI uri;
		try {
		    uri = url.toURI();
		} catch (URISyntaxException e) {
		    throw new RuntimeException(e);
		}
		File f = new File(uri);
		baseDir = fixDirPath(f.getParentFile().getParent());
		libDir = baseDir; 
	    }
	    addToAsdfCentralRegistry(lispEngine, baseDir, "snow");
	    addToAsdfCentralRegistry(lispEngine, baseDir, "snow", "swing");
	    addToAsdfCentralRegistry(lispEngine, libDir, "cl-utilities");
	    addToAsdfCentralRegistry(lispEngine, libDir, "named-readtables");
	    addToAsdfCentralRegistry(lispEngine, libDir, "cells");
	    addToAsdfCentralRegistry(lispEngine, libDir, "cells", "utils-kt");
	}
    }

    private static Object addToAsdfCentralRegistry(ScriptEngine lispEngine, String base, String... path) throws ScriptException {
	return lispEngine.eval("(pushnew #P\"" + makePath(base, path) + "\" asdf:*central-registry* :test #'equal)");
    }
    
    private static String makePath(String base, String... path) {
	for(String s : path) {
	    base = fixDirPath(base) + s;
	}
	return escapePath(fixDirPath(base));
    }

    private static String escapePath(String str) {
	//Replace single \ with double \ for Windows paths
	return str.replace("\\", "\\\\"); 
    }

    private static final String fixDirPath(String path) {
	if(!path.endsWith(fileSeparator)) {
	    path += fileSeparator;
	}
	return path;
    }

    public static synchronized ScriptEngine init() throws ScriptException {
	if(!init) {
	    initAux();
	    lispEngine.eval("(asdf:oos 'asdf:load-op :snow)");
	    init = true;
	    return lispEngine;
	} else {
	    throw new RuntimeException("Already initialized");
	}
    }
    
    public static synchronized ScriptEngine initIfNecessary() {
	if(!init) {
	    try {
		init();
	    } catch(ScriptException e) {
		throw new RuntimeException("Snow initialization failed", e);
	    }
	}
	return lispEngine;
    }

    /**
     * Compiles and loads a Lisp file from the classpath, relative to aClass.
     */
    @Deprecated
    public static Object evalResource(Class<?> aClass, String resourcePath) throws ScriptException {
	return evalResource(aClass, resourcePath, true);
    }
    
    /**
     * Loads a Lisp file from the classpath, relative to aClass. If compileItFirst is true, the file is compiled before being loaded.
     */
    @Deprecated
    public static Object evalResource(Class<?> aClass, String resourcePath, boolean compileItFirst) throws ScriptException {
	Reader r = new InputStreamReader(aClass.getResourceAsStream(resourcePath));
	return evalResource(r, compileItFirst);
    }

    @Deprecated
    public static Object evalResource(String resourcePath) throws ScriptException {
	return evalResource(Snow.class, resourcePath, true);
    }

    @Deprecated
    public static Object evalResource(String resourcePath, boolean compileItFirst) throws ScriptException {
	return evalResource(Snow.class, resourcePath, compileItFirst);
    }
    
    @Deprecated
    public static Object evalResource(Reader reader) throws ScriptException {
	return evalResource(reader, true);
    }
    
    @Deprecated
    public static Object evalResource(Reader reader, boolean compileItFirst) throws ScriptException {
	initIfNecessary();
	if(compileItFirst) {
	    return getCompilable().compile(reader).eval();
	} else {
	    return lispEngine.eval(reader);
	}
    }

    /**
     * Returns a Snowlet running interpreted code read from the provided Reader.
     * This Snowlet's eval() method may only be called once.
     */
    public static Snowlet getInterpretedSnowlet(final Reader reader) {
	initIfNecessary();
	return new AbstractSnowlet() {
	    private boolean eval = false;
	    public Object eval(Bindings bindings) throws Exception {
		if(!eval) {
		    eval = true;
		} else {
		    throw new IllegalStateException("Already evaluated");
		}
		return lispEngine.eval(reader, bindings);
	    }
	};
    }

    public static Snowlet getInterpretedSnowlet(final URL url) {
	initIfNecessary();
	return new AbstractSnowlet() {
	    public Object eval(Bindings bindings) throws Exception {
		return lispEngine.eval(new InputStreamReader(url.openStream()), bindings);
	    }
	};
    }

    public static Snowlet getCompiledSnowlet(final Reader reader) {
	initIfNecessary();
	final CompiledScript c;
	try {
	    c = getCompilable().compile(reader);
	} catch(ScriptException e) {
	    throw new RuntimeException("Compilation error", e);
	}
	return new AbstractSnowlet() {
	    public Object eval(Bindings bindings) throws Exception {
		return c.eval(bindings);
	    }
	};
    }

    public static Snowlet getCompiledSnowlet(final URL url) {
	try {
	    return getCompiledSnowlet(new InputStreamReader(url.openStream()));
	} catch(IOException e) {
	    throw new RuntimeException("Couldn't open stream for URL: " + url, e);
	}
    }
    
    public static ScriptEngine getScriptEngine() {
	return lispEngine;
    }
    
    public static Compilable getCompilable() {
	return (Compilable) lispEngine;
    }
    
    public static Invocable getInvocable() {
	return (Invocable) lispEngine;
    }
    
    public static void main(final String[] args) {
	try {
	    //Needed for Java WebStart
	    Policy.setPolicy
		(new Policy() {
			public PermissionCollection getPermissions(CodeSource codesource) {
			    Permissions perms = new Permissions();
			    perms.add(new AllPermission());
			    return (perms);
			}
		    });
	    if(args.length == 0) { //Launch GUI REPL
		getCompiledSnowlet(Snow.class.getResource("/snow/start.lisp")).eval();
	    } else if("--showcase".equals(args[0])) {
		getInterpretedSnowlet(Snow.class.getResource("/snow/showcase/showcase.lisp")).eval();
		getInvocable().invokeFunction("snow-showcase::showcase", true);
	    } else {//Launch regular ABCL
		//Copied from org.armedbear.lisp.Main.main()
		Runnable r = new Runnable() {
			public void run() {
			    try {
				Interpreter interpreter = Interpreter.createDefaultInstance(args);
				Snow.init();
				interpreter.eval("(in-package :snow-user)");
				interpreter.run();
			    } catch(Throwable t) {
				System.err.println("Caught error, exiting: " + t);
				t.printStackTrace();
				System.exit(1);
			    }
			}
		    };
		Thread t = new Thread(null, r, "interpreter", 4194304L);
		t.setDaemon(true);
		t.start();
	    }
	} catch (Exception e) {
	    e.printStackTrace();
	}
    }
    
	
}
