/***** BEGIN LICENSE BLOCK *****
 * Version: EPL 1.0/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Eclipse Public
 * License Version 1.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a copy of
 * the License at http://www.eclipse.org/legal/epl-v10.html
 *
 * Software distributed under the License is distributed on an "AS
 * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * rights and limitations under the License.
 *
 * Copyright (C) 2007 Charles Nutter <charles.o.nutter@sun.com>
 * Copyright (C) 2008 MenTaLguY <mental@rydia.net>
 * 
 * Alternatively, the contents of this file may be used under the terms of
 * either of the GNU General Public License Version 2 or later (the "GPL"),
 * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the EPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the EPL, the GPL or the LGPL.
 ***** END LICENSE BLOCK *****/
package org.jruby;

import java.applet.Applet;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Insets;
import java.awt.Graphics;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.image.VolatileImage;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.URL;
import java.util.Arrays;

import java.lang.reflect.InvocationTargetException;

import org.jruby.anno.JRubyMethod;
import org.jruby.demo.TextAreaReadline;
import org.jruby.javasupport.JavaUtil;
import org.jruby.runtime.Block;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;

import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import org.jruby.javasupport.JavaEmbedUtils;

/**
 * @author <a href="mailto:mental@rydia.net">MenTaLguY</a>
 *
 * The JRubyApplet class provides a simple way to write Java applets using
 * JRuby without needing to create a custom Java applet class.  At applet
 * initialization time, JRubyApplet starts up a JRuby runtime, then evaluates
 * the scriptlet given as the "eval" applet parameter.
 *
 * The Java applet instance is available to the Ruby script as
 * JRUBY_APPLET; the script can define callbacks for applet start, stop,
 * and destroy by passing blocks to JRUBY_APPLET.on_start,
 * JRUBY_APPLET.on_stop, and JRUBY_APPLET.on_destroy, respectively.
 *
 * Ruby code can install a custom paint callback using JRUBY_APPLET.on_paint
 * (the Graphics2D object is passed as an argument to the callback).  By
 * default, JRubyApplet painting is double-buffered, but you can select
 * single-buffered painting via JRUBY_APPLET.double_buffered = false.
 *
 * The applet's background color can be set via JRUBY_APPLET.background_color=.
 * You may want to set it to nil if you're not using double-buffering, so that
 * no background color will be drawn (your own paint code is then responsible
 * for filling the area).
 *
 * Beyond these things, you should be able to use JRuby's Java integration
 * to do whatever you would do in Java with the applet instance.
 *
 */
public class JRubyApplet extends Applet {
    private Ruby runtime;
    private boolean doubleBuffered = true;
    private Color backgroundColor = Color.WHITE;
    private RubyProc startProc;
    private RubyProc stopProc;
    private RubyProc destroyProc;
    private RubyProc paintProc;
    private Graphics priorGraphics;
    private IRubyObject wrappedGraphics;
    private VolatileImage backBuffer;
    private Graphics backBufferGraphics;
    private Facade facade;

    private interface Facade {
        public InputStream getInputStream();
        public PrintStream getOutputStream();
        public PrintStream getErrorStream();
        public void attach(Ruby runtime, Applet applet);
        public void destroy();
    }

    private static RubyProc blockToProc(Ruby runtime, Block block) {
        if (block.isGiven()) {
            RubyProc proc = block.getProcObject();
            if (proc == null) {
                proc = RubyProc.newProc(runtime, block, block.type);
            }
            return proc;
        } else {
            return null;
        }
    }

    private boolean getBooleanParameter(String name, boolean defaultValue) {
        String value = getParameter(name);
        if ( value != null ) {
            return value.equals("true");
        } else {
            return defaultValue;
        }
    }
    
    /**
     * Return Ruby language version from the applet parameter
     * @param compat - name of the parameter handed to the applet
     * @param defaultVersion
     * @return Ruby language version; CompatVersion.RUBY1_9 if parameter is exactly "1.9".
     * Otherwise, return defaultVersion
     */
    private CompatVersion getCompatVersionParameter(String compat, CompatVersion defaultVersion) {
        CompatVersion compatVersion = defaultVersion;
        if (getParameter(compat).equals("1.9")) compatVersion = CompatVersion.RUBY1_9;
        return compatVersion;
    }

    private InputStream getCodeResourceAsStream(String name) {
        if (name == null) {
            return null;
        }
        try {
            final URL directURL = new URL(getCodeBase(), name);
            return directURL.openStream();
        } catch (IOException e) {
        }
        return JRubyApplet.class.getClassLoader().getResourceAsStream(name);
    }

    private static void safeInvokeAndWait(Runnable runnable) throws InvocationTargetException, InterruptedException {
        if (EventQueue.isDispatchThread()) {
            try {
                runnable.run();
            } catch (Exception e) {
                throw new InvocationTargetException(e);
            }
        } else {
            EventQueue.invokeAndWait(runnable);
        }
    }

    public static class RubyMethods {
        @JRubyMethod
        public static IRubyObject on_start(IRubyObject recv, Block block) {
            JRubyApplet applet = (JRubyApplet) JavaEmbedUtils.rubyToJava(recv);
            synchronized (applet) {
                applet.startProc = blockToProc(applet.runtime, block);
            }
            return recv;
        }

        @JRubyMethod
        public static IRubyObject on_stop(IRubyObject recv, Block block) {
            JRubyApplet applet = (JRubyApplet) JavaEmbedUtils.rubyToJava(recv);
            synchronized (applet) {
                applet.stopProc = blockToProc(applet.runtime, block);
            }
            return recv;
        }

        @JRubyMethod
        public static IRubyObject on_destroy(IRubyObject recv, Block block) {
            JRubyApplet applet = (JRubyApplet) JavaEmbedUtils.rubyToJava(recv);
            synchronized (applet) {
                applet.destroyProc = blockToProc(applet.runtime, block);
            }
            return recv;
        }

        @JRubyMethod
        public static IRubyObject on_paint(IRubyObject recv, Block block) {
            JRubyApplet applet = (JRubyApplet) JavaEmbedUtils.rubyToJava(recv);
            synchronized (applet) {
                applet.paintProc = blockToProc(applet.runtime, block);
                applet.repaint();
            }
            return recv;
        }
    }

    @Override
    public void init() {
        super.init();

        if (getBooleanParameter("jruby.console", false)) {
            facade = new ConsoleFacade(getParameter("jruby.banner"));
        } else {
            facade = new TrivialFacade();
        }

        synchronized (this) {
            if (runtime != null) {
                return;
            }

            final RubyInstanceConfig config = new RubyInstanceConfig() {{
                setInput(facade.getInputStream());
                setOutput(facade.getOutputStream());
                setError(facade.getErrorStream());
                setObjectSpaceEnabled(getBooleanParameter("jruby.objectspace", false));
                setCompatVersion(getCompatVersionParameter("jruby.compat", CompatVersion.RUBY1_8));
            }};
            Ruby.setSecurityRestricted(true);
            runtime = Ruby.newInstance(config);
        }

        final String scriptName = getParameter("jruby.script");
        final InputStream scriptStream = getCodeResourceAsStream(scriptName);
        final String evalString = getParameter("jruby.eval");

        try {
            final JRubyApplet applet = this;
            safeInvokeAndWait(new Runnable() {
                public void run() {
                    applet.setLayout(new BorderLayout());
                    applet.facade.attach(applet.runtime, applet);
                    if (scriptStream != null) {
                        applet.runtime.runFromMain(scriptStream, scriptName);
                    }
                    if (evalString != null) {
                        applet.runtime.evalScriptlet(evalString);
                    }
                }
            });
        } catch (InterruptedException e) {
        } catch (InvocationTargetException e) {
            throw new RuntimeException("Error running script", e.getCause());
        }
    }

    private void invokeCallback(final RubyProc proc, final IRubyObject[] args) {
        if (proc == null) {
            return;
        }
        final Ruby ruby = this.runtime;
        try {
            safeInvokeAndWait(new Runnable() {
                public void run() {
                    proc.call(ruby.getCurrentContext(), args);
                }
            });
        } catch (InterruptedException e) {
        } catch (InvocationTargetException e) {
            throw new RuntimeException("Ruby callback failed", e.getCause());
        }
    }

    public synchronized void setBackgroundColor(Color color) {
        backgroundColor = color;
        repaint();
    }

    public synchronized Color getBackgroundColor() {
        return backgroundColor;
    }

    @Override
    public synchronized boolean isDoubleBuffered() {
        return doubleBuffered;
    }

    public synchronized void setDoubleBuffered(boolean shouldBuffer) {
        doubleBuffered = shouldBuffer;
        repaint();
    }

    @Override
    public synchronized void start() {
        super.start();
        invokeCallback(startProc, new IRubyObject[] {});
    }

    @Override
    public synchronized void stop() {
        invokeCallback(stopProc, new IRubyObject[] {});
        super.stop();
    }

    @Override
    public synchronized void destroy() {
        try {
            invokeCallback(destroyProc, new IRubyObject[] {});
        } finally {
            facade.destroy();
            final Ruby ruby = this.runtime;
            this.runtime = null;
            startProc = null;
            stopProc = null;
            destroyProc = null;
            paintProc = null;
            priorGraphics = null;
            wrappedGraphics = null;
            ruby.tearDown();
            super.destroy();
        }
    }

    @Override
    public void update(Graphics g) {
        paint(g);
    }

    @Override
    public synchronized void paint(Graphics g) {
        if (doubleBuffered) {
            paintBuffered(g);
        } else {
            paintUnbuffered(g);
        }
    }

    private synchronized void paintBuffered(Graphics g) {
        do {
            GraphicsConfiguration config = getGraphicsConfiguration();
            int width = getWidth();
            int height = getHeight();
            if (backBuffer == null || width != backBuffer.getWidth() || height != backBuffer.getHeight() || backBuffer.validate(config) == VolatileImage.IMAGE_INCOMPATIBLE) {
                if (backBuffer != null) {
                    backBufferGraphics.dispose();
                    backBufferGraphics = null;
                    backBuffer.flush();
                    backBuffer = null;
                }
                backBuffer = config.createCompatibleVolatileImage(width, height);
                backBufferGraphics = backBuffer.createGraphics();
            }
            backBufferGraphics.setClip(g.getClip());
            paintUnbuffered(backBufferGraphics);
            g.drawImage(backBuffer, 0, 0, this);
        } while (backBuffer.contentsLost());
    }

    private synchronized void paintUnbuffered(Graphics g) {
        if (backgroundColor != null) {
            g.setColor(backgroundColor);
            g.fillRect(0, 0, getWidth(), getHeight());
        }
        if (paintProc != null) {
            if (priorGraphics != g) {
                wrappedGraphics = JavaUtil.convertJavaToUsableRubyObject(runtime, g);
                priorGraphics = g;
            }
            ThreadContext context = runtime.getCurrentContext();
            paintProc.call(context, new IRubyObject[] {wrappedGraphics});
        }
        super.paint(g);
    }

    private static class TrivialFacade implements Facade {
        public TrivialFacade() {}
        public InputStream getInputStream() { return System.in; }
        public PrintStream getOutputStream() { return System.out; }
        public PrintStream getErrorStream() { return System.err; }
        public void attach(Ruby runtime, Applet applet) {
            final IRubyObject wrappedApplet = JavaEmbedUtils.javaToRuby(runtime, applet);
            runtime.defineGlobalConstant("JRUBY_APPLET", wrappedApplet);
            wrappedApplet.getMetaClass().defineAnnotatedMethods(RubyMethods.class);
        }
        public void destroy() {}
    }

    private static class ConsoleFacade implements Facade {
        private JTextPane textPane;
        private JScrollPane scrollPane;
        private TextAreaReadline adaptor;
        private InputStream inputStream;
        private PrintStream outputStream;
        private PrintStream errorStream;
        
        public ConsoleFacade(String bannerText) {
            textPane = new JTextPane();
	    textPane.setMargin(new Insets(4, 4, 0, 4));
            textPane.setCaretColor(new Color(0xa4, 0x00, 0x00));
            textPane.setBackground(new Color(0xf2, 0xf2, 0xf2));
            textPane.setForeground(new Color(0xa4, 0x00, 0x00));

            Font font = findFont("Monospaced", Font.PLAIN, 14,
                                 new String[] {"Monaco", "Andale Mono"});

            textPane.setFont(font);

            scrollPane = new JScrollPane(textPane);
            scrollPane.setDoubleBuffered(true);
            if ( bannerText != null ) {
                bannerText = "  " + bannerText + "  \n\n";
            }
            adaptor = new TextAreaReadline(textPane, bannerText);
            inputStream = adaptor.getInputStream();
            outputStream = new PrintStream(adaptor.getOutputStream());
            errorStream = new PrintStream(adaptor.getOutputStream());
        }

        public InputStream getInputStream() { return inputStream; }
        public PrintStream getOutputStream() { return outputStream; }
        public PrintStream getErrorStream() { return errorStream; }

        public void attach(Ruby runtime, Applet applet) {
            adaptor.hookIntoRuntime(runtime);
            applet.add(scrollPane);
            applet.validate();
        }

        public void destroy() {
            Container parent = scrollPane.getParent();
            adaptor.shutdown();
            if (parent != null) {
                parent.remove(scrollPane);
            }
        }

        private Font findFont(String otherwise, int style, int size, String[] families) {
            String[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
            Arrays.sort(fonts);
            for (int i = 0; i < families.length; i++) {
                if (Arrays.binarySearch(fonts, families[i]) >= 0) {
                    return new Font(families[i], style, size);
                }
            }
            return new Font(otherwise, style, size);
        }
    }
}
