/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *                                                                         *
 *   AndroidWorld Library, Copyright 2011 Bryan Chadwick                   *
 *                                                                         *
 *   FILE: ./android/world/BigBang.java                                    *
 *                                                                         *
 *   This file is part of AndroidWorld.                                    *
 *                                                                         *
 *   AndroidWorld 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          *
 *   3 of the License, or (at your option) any later version.              *
 *                                                                         *
 *   AndroidWorld 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 AndroidWorld.  If not, see <http://www.gnu.org/licenses/>. *
 *                                                                         *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

package android.world;

import java.io.File;
import java.lang.reflect.Method;
import java.util.Timer;
import java.util.TimerTask;

import android.graphics.*;
import android.hardware.*;
import android.image.Scene;
import android.image.Image;
import android.os.Environment;
import android.text.InputType;
import android.util.Log;
import android.util.Util;
import android.view.*;
import android.widget.*;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.pm.ActivityInfo;

/** A Class representing a World of some type, and the related methods and Function
 *    Objects (call-backs) for drawing the world and handling various events.  As
 *    handlers are installed, each is checked for a corresponding <tt>apply</tt>
 *    method with the appropriate signature.
 *    
 *  <p>The initial value of the World assigns a (minimum) <i>type</i>, which is
 *    used to search/check all of the handlers.  Functions that produce a world
 *    deserve special attention, since they may return a super-type of the
 *    initial World (e.g., initial <tt>EmptyScene</tt>, with an tick handler that
 *    returns a <tt>Scene</tt>).  The name and types of handlers are given in the
 *    table below:<br/><br/>
 *    
 *    <style>
 *       table.mine{ margin-left: 20px; border: 1px solid blue; }
 *       table.mine td, table.mine th{ padding-left:20px; padding-right:5px;  border: 1px solid blue; }
 *       td.event, th.event{ text-align: center; }
 *       .com { color: #AA240F; font-style: italic; }
 *       .keyw { color: #262680; font-weight: bold; }
 *       .useful { color: #1111C0; }
 *       .num { color: #00AA00; }
 *       .str { color: #00AA00; }
 *       .fun { color: #AA5500; }
 *    </style>
 *    <table class="mine">
 *      <tr><th class="event">Event Name</th><th>BigBang Method</th><th>Handler Signature</th><th>Required?</th><tr/>
 *      <tr><td class="event">OnDraw</td><td><tt><span class='fun'>onDraw</span>(<i>handler</i>)</tt></td><td><tt>Scene <span class='fun'>apply</span>(World w)</tt></td><td><b><i>yes</i></b></td><tr/>
 *      <tr><td class="event">OnTick</td><td><tt><span class='fun'>onTick</span>(<i>handler</i>)</tt> or<br/> <tt><span class='fun'>ontick</span>(<i>handler</i>, <span class="keyw">double</span>)</tt></td><td><tt>World <span class='fun'>apply</span>(World w)</tt></td><td>no</td><tr/>
 *      <tr><td class="event">OnMouse</td><td><tt><span class='fun'>onMouse</span>(<i>handler</i>)</tt></td><td><tt>World <span class='fun'>apply</span>(World w, <span class="keyw">int</span> x, <span class="keyw">int</span> y, String what)</tt></td><td>no</td><tr/>
 *      <tr><td class="event">OnKey</td><td><tt><span class='fun'>onKey</span>(<i>handler</i>)</tt></td><td><tt>World <span class='fun'>apply</span>(World w, String key)</tt></td><td>no</td><tr/>
 *      <tr><td class="event">OnRelease</td><td><tt><span class='fun'>onRelease</span>(<i>handler</i>)</tt></td><td><tt>World <span class='fun'>apply</span>(World w, String key)</tt></td><td>no</td><tr/>
 *      <tr><td class="event">StopWhen</td><td><tt><span class='fun'>stopWhen</span>(<i>handler</i>)</tt></td><td><tt><span class="keyw">boolean</span> <span class='fun'>apply</span>(World w)</tt></td><td>no</td><tr/>
 *      <tr><td class="event">LastScene</td><td><tt><span class='fun'>lastScene</span>(<i>handler</i>)</tt></td><td><tt>Scene <span class='fun'>apply</span>(World w)</tt></td><td>no</td><tr/>
 *    </table><br/>
 *    
 *    For Android phones we have a few more options like orientation sensor(s) that we can tap
 *    into.  Orientation changes of a device can be received by installing an orientation handler:<br/><br/>
 *    
 *    <table class="mine">
 *      <tr><th class="event">Event Name</th><th>BigBang Method</th><th>Handler Signature</th><th>Required?</th><tr/>
 *      <tr><td class="event">Orientation</td><td><tt><span class='fun'>orientation</span>(<i>handler</i>)</tt></td><td><tt>World <span class='fun'>apply</span>(World w, <span class="keyw">float</span> x, <span class="keyw">float</span> y, <span class="keyw">float</span> z)</tt></td><td>no</td><tr/>
 *    </table><br/><br/>
 *    
 *    Key and Mouse events are also handled slightly different, as most devices do not have hardware keyboards, and
 *    generate Touch events without the typical "move" events (only Down, Drag and Up events).
 *    </p>
 *    
 */
public class BigBang{
    
    /** Mouse down (button-down) event String */
    public static String MOUSE_DOWN = "button-down";
    /** Mouse down (button-down) event String */
    public static String LONG_MOUSE_DOWN = "long-button-down";
    /** Mouse up (button-up) event String */
    public static String MOUSE_UP = "button-up";
    /** Mouse motion (move) event String */
    public static String MOUSE_MOVE = "move";
    /** Mouse down & move (drag) event String */
    public static String MOUSE_DRAG = "drag";   
    /** Key arrow-up event String */
    public static String KEY_ARROW_UP = "up";   
    /** Key arrow-down event String */
    public static String KEY_ARROW_DOWN = "down";   
    /** Key arrow-left event String */
    public static String KEY_ARROW_LEFT = "left";   
    /** Key arrow-right event String */
    public static String KEY_ARROW_RIGHT = "right";   
    /** Menu Key event String.  The menu key will usually be intercepted to open a save dialog
     *    to enable the capture of application/game screen-shots. */
    public static String KEY_MENU = "menu";   
    /** Search Key event String */
    public static String KEY_SEARCH = "search";   

    private Object initial;
    private Class<?> worldType;
    private double time;
    
    // Handlers and their corresponding selected Method
    protected Object ondraw;
    protected Method ondrawM;
    protected Object ontick;
    protected Method ontickM;
    protected Object onmouse;
    protected Method onmouseM;
    protected Object onkey;
    protected Method onkeyM;
    protected Object onrelease;
    protected Method onreleaseM;
    protected Object stopwhen;
    protected Method stopwhenM;
    protected Object lastscene;
    protected Method lastsceneM;
    protected Object orientation;
    protected Method orientationM;
    private Handler handler = null;
    
    /** Create a new BigBang with a value of the initial World */
    public BigBang(Object initial){
        this(initial, initial.getClass(), 0.05,
                null, null, null, null, null, null,
                null, null, null, null, null, null,
                null, null, null, null);
    }
    /** Install a Draw Handler into this BigBang.  The Draw handler requires an apply method
     *    [World -> Scene], though the requirement is checked dynamically when this method
     *    is called. */
    public BigBang onDraw(Object ondraw){
        Method ondrawM = checkTypes(ondraw, new Class[]{worldType}, Scene.class, "OnDraw", false, false);
        return new BigBang(this.initial, this.worldType, this.time,
                ondraw, ondrawM, this.ontick, this.ontickM,
                this.onmouse, this.onmouseM, this.onkey, this.onkeyM, this.onrelease, this.onreleaseM,
                this.stopwhen, this.stopwhenM, this.lastscene, this.lastsceneM,
                this.orientation, this.orientationM);
    }
    /** Install a Tick Handler at a tick rate of 1/20th of a second. */
    public BigBang onTick(Object ontick){
        return onTick(ontick, 0.05);
    }
    /** Install a Tick Handler into this BigBang at the given tick rate (per-seconds).  The Tick handler
     *    requires an apply method [World -> World], though the requirement is checked dynamically when
     *    this method is called. */
    public BigBang onTick(Object ontick, double time){
        Method ontickM = checkTypes(ontick, new Class[]{worldType}, worldType, "OnTick", true, true);
        return new BigBang(this.initial, this.worldType, time,
                this.ondraw, this.ondrawM, ontick, ontickM,
                this.onmouse, this.onmouseM, this.onkey, this.onkeyM, this.onrelease, this.onreleaseM,
                this.stopwhen, this.stopwhenM, this.lastscene, this.lastsceneM,
                this.orientation, this.orientationM);
    }
    /** Install a Mouse Handler into this BigBang.  The Mouse handler requires an apply method
     *    [World -> World], though the requirement is checked dynamically when this method is called. */
    public BigBang onMouse(Object onmouse){
        Method onmouseM = checkTypes(onmouse, new Class[]{worldType, int.class, int.class, String.class}, worldType, "OnMouse", true, true);
        return new BigBang(this.initial, this.worldType, this.time,
                this.ondraw, this.ondrawM, this.ontick, this.ontickM,
                onmouse, onmouseM, this.onkey, this.onkeyM, this.onrelease, this.onreleaseM,
                this.stopwhen, this.stopwhenM, this.lastscene, this.lastsceneM,
                this.orientation, this.orientationM);
    }
    /** Install a Key Handler into this BigBang.  The Key handler requires an apply method
     *    [World String -> World], though the requirement is checked dynamically when this
     *    method is called. */
    public BigBang onKey(Object onkey){
        Method onkeyM = checkTypes(onkey, new Class[]{worldType, String.class}, worldType, "OnKey", true, true);
        return new BigBang(this.initial, this.worldType, this.time,
                this.ondraw, this.ondrawM, this.ontick, this.ontickM,
                this.onmouse, this.onmouseM, onkey, onkeyM, this.onrelease, this.onreleaseM,
                this.stopwhen, this.stopwhenM, this.lastscene, this.lastsceneM,
                this.orientation, this.orientationM);
    }
    /** Install a Key Release Handler into this BigBang.  The Key release handler requires an apply method
     *    [World String -> World], though the requirement is checked dynamically when this
     *    method is called. */
    public BigBang onRelease(Object onrelease){
        Method onreleaseM = checkTypes(onrelease, new Class[]{worldType, String.class}, worldType, "OnRelease", true, true);
        return new BigBang(this.initial, this.worldType, this.time,
                this.ondraw, this.ondrawM, this.ontick, this.ontickM,
                this.onmouse, this.onmouseM, this.onkey, this.onkeyM, onrelease, onreleaseM,
                this.stopwhen, this.stopwhenM, this.lastscene, this.lastsceneM,
                this.orientation, this.orientationM);
    }
    /** Install a StopWhen Handler into this BigBang.  The StopWhen handler requires an apply method
     *    [World -> Boolean], though the requirement is checked dynamically when this
     *    method is called.  The StopWhen handler, if installed is call to determine whether or
     *    not the World/animation/events should be stopped.  When/if the handler returns true
     *    then all events stop being received and the LastScene handler is given a chance to
     *    draw the final World. */
    public BigBang stopWhen(Object stopwhen){
        Method stopwhenM = checkTypes(stopwhen, new Class[]{worldType}, Boolean.class, "StopWhen", true, false);
        return new BigBang(this.initial, this.worldType, this.time,
                this.ondraw, this.ondrawM, this.ontick, this.ontickM,
                this.onmouse, this.onmouseM, this.onkey, this.onkeyM, this.onrelease, this.onreleaseM,
                stopwhen, stopwhenM, this.lastscene, this.lastsceneM,
                this.orientation, this.orientationM);
    }
    /** Install a LastScene Handler into this BigBang.  The LastScene handler requires an apply method
     *    [World -> Scene], though the requirement is checked dynamically when this method is called.
     *    After the animation is stopped (StopWhen) the final World is drawn using the LstScene Handler. */
    public BigBang lastScene(Object lastscene){
        Method lastsceneM = checkTypes(lastscene, new Class[]{worldType}, Scene.class, "LastScene", true, false);
        return new BigBang(this.initial, this.worldType, this.time,
                this.ondraw, this.ondrawM, this.ontick, this.ontickM,
                this.onmouse, this.onmouseM, this.onkey, this.onkeyM, this.onrelease, this.onreleaseM,
                this.stopwhen, this.stopwhenM, lastscene, lastsceneM,
                this.orientation, this.orientationM);
    }
    /** Install an Orientation Handler into this BigBang.  The Orientation handler requires an apply method
     *    [World Float Float Float -> World], though the requirement is checked dynamically when this
     *    method is called.  On Android systems, the Orientation handler is called when the device orientation
     *    is updated or changed.  The three floats passed to the handler method represent the device's current
     *    orientation (angles in radians) in the device's three dimensions, X, Y, and Z.
     *    
     *    <p>The three dimensional vector represents the direction of gravitational force (i.e., the ground)
     *      as compared to the device at rest (e.g., flat on a level table) where Z points directly at the
     *      ground.  The X and Y vectors are in the device's screen coordinates, and Z typically points out
     *      the back of the device.</p>     
     */
    public BigBang orientation(Object orientation){
        Method orientationM = checkTypes(orientation, new Class[]{worldType,float.class,float.class,float.class}, worldType, "Orientation", true, true);
        return new BigBang(this.initial, this.worldType, this.time,
                this.ondraw, this.ondrawM, this.ontick, this.ontickM,
                this.onmouse, this.onmouseM, this.onkey, this.onkeyM, this.onrelease, this.onreleaseM,
                this.stopwhen, this.stopwhenM, this.lastscene, this.lastsceneM,
                orientation, orientationM);
    }    
    
    // Private constructor...
    private BigBang(Object init, Class<?> worldT, double time, 
                    Object ondraw, Method ondrawM, Object ontick, Method ontickM,
                    Object onmouse, Method onmouseM, Object onkey, Method onkeyM,
                    Object onrelease, Method onreleaseM, Object stopwhen, Method stopwhenM,
                    Object lastscene, Method lastsceneM, Object orientation, Method orientationM){
        this.initial = init;
        this.worldType = worldT;
        this.time = time;
        this.ondraw = ondraw;
        this.ondrawM = ondrawM;
        this.ontick = ontick;
        this.ontickM = ontickM;
        this.onmouse = onmouse;
        this.onmouseM = onmouseM;
        this.onkey = onkey;
        this.onkeyM = onkeyM;
        this.onrelease = onrelease;
        this.onreleaseM = onreleaseM;
        this.stopwhen = stopwhen;
        this.stopwhenM = stopwhenM;
        this.lastscene = lastscene;
        this.lastsceneM = lastsceneM;
        this.orientation = orientation;
        this.orientationM = orientationM;
    }    
    /** Check/find a method compatible with the given types in the
     *    given function Object/Handler */
    private Method checkTypes(Object f, Class<?>[] args, Class<?> ret, String what, boolean nullable, boolean wret){
        Class<?> fClass = f.getClass();
        Method[] possibles = fClass.getDeclaredMethods();
        for(Method m : possibles){
            if(m.getName().equals(Util.funcObjMethName) && 
                    Util.subtypes(args, m.getParameterTypes())){
                if(Util.subtype(m.getReturnType(), ret))
                    return m;
                if(Util.subtype(ret, m.getReturnType()) &&
                   !m.getReturnType().equals(Object.class)){
                    this.worldType = m.getReturnType();
                    return m;
                }
            }
        }
        throw Util.exceptionDrop(2, "\n** Function Object ("+fClass.getSimpleName()+") used for "+
                what.toLowerCase()+" does not contain a method sutable for:\n     "+
                ret.getSimpleName()+" "+Util.funcObjMethName+"("+Util.argsString(args,0)+")");
    }
    /** Wrapper for the Tick Handler */
    private Object doOnTick(Object w) {
        return Util.applyFunc(ontick, ontickM, new Object[]{w});
    }
    /** Wrapper for the Mouse Handler */
    private Object doOnMouseEvent(Object w, int x, int y, String me) {
        return Util.applyFunc(onmouse, onmouseM, new Object[]{w,x,y,me});
    }
    /** Wrapper for the Key Handler */
    private Object doOnKeyEvent(Object w, String ke){
        if(ke.length() == 0)return w;
        return Util.applyFunc(onkey, onkeyM, new Object[]{w,ke});
    }
    /** Wrapper for the Key Release Handler */
    private Object doOnKeyRelease(Object w, String ke){
        if(ke.length() == 0)return w;
        return Util.applyFunc(onrelease, onreleaseM, new Object[]{w,ke});
    }
    /** Wrapper for the Draw Handler */
    private Scene doOnDraw(Object w) {
        return (Scene)Util.applyFunc(ondraw, ondrawM, new Object[]{w});
    }
    /** Wrapper for the StopWhen Handler */
    private boolean doStopWhen(Object w) {
        if(stopwhen == null)return false;
        return (Boolean)Util.applyFunc(stopwhen, stopwhenM, new Object[]{w});
    }
    /** Wrapper for the LastScene Handler */
    private Scene doLastScene(Object w) {
        if(lastscene == null)return doOnDraw(w);
        return (Scene)Util.applyFunc(lastscene, lastsceneM, new Object[]{w});
    }
    /** Wrapper for the Orientation Handler */
    private Object doOnOrientationEvent(Object w, float x, float y, float z){
        if(orientation == null)return w;
        return Util.applyFunc(orientation, orientationM, new Object[]{w,x,y,z});
    }
    /** Construct and start the animation/interaction system.  For the
     *    Android version the client must pass the initiating Activity
     *    in order to connect the handlers to the necessary events.
     *    The method returns the initial World (for no good reason)
     *    since the <tt>Activity.onCreate(...)</tt>, must return
     *    before the application may start. */
    public Object bigBang(Activity act){
        if(ondraw == null)
            throw new RuntimeException("No World Draw Handler");
        Scene scn = doOnDraw(initial);
        if(this.handler == null){
            this.handler = new Handler(act, this, initial,
                    Bitmap.createBitmap(scn.width(), scn.height(), Bitmap.Config.ARGB_8888));
            act.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
            act.addContentView(handler, new ViewGroup.MarginLayoutParams(scn.width(),scn.height()));
            if(orientation != null){
                SensorManager s = (SensorManager)act.getSystemService(Context.SENSOR_SERVICE);
                s.registerListener(handler, s.getDefaultSensor(SensorManager.SENSOR_ORIENTATION),
                                   SensorManager.SENSOR_DELAY_GAME);
            }
        }
        handler.requestFocus();
        return handler.w;
    }
    /** Construct and start the animation/interaction system
     *    with the Android device in LANDSCAPE mode. */
    public Object bigBangLandscape(Activity act){
        Object res = this.bigBang(act);
        act.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        return res;
    }
    /** Construct and start the animation/interaction system
     *    with the Android device in FULLSCREEN mode. */
    public Object bigBangFullscreen(Activity act){
        act.requestWindowFeature(Window.FEATURE_NO_TITLE);
        act.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                                 WindowManager.LayoutParams.FLAG_FULLSCREEN);
        Object res = this.bigBang(act);
        return res;
    }
    /** Construct and start the animation/interaction system
     *    with the Android device in LANDSCAPE mode. */
    public Object bigBangLandscapeFullscreen(Activity act){
        act.requestWindowFeature(Window.FEATURE_NO_TITLE);
        act.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                                 WindowManager.LayoutParams.FLAG_FULLSCREEN);
        Object res = this.bigBangLandscape(act);
        return res;
    }
        
    /** Handles the nitty-gritty of world updates and interfacing with Android APIs */
    static class Handler extends EditText
             implements android.hardware.SensorEventListener, View.OnLongClickListener{
        private static final long serialVersionUID = 1L;
        Activity act;
        BigBang world;
        Object w;
        Scene scnBuffer;
        Bitmap buffer;
        Timer run;
        TimerTask ticker;
        boolean isRunning = true;
        boolean isDone = false;
        boolean tap_down = false;
        int lastX = 0, lastY = 0;
    
        static class DoTick extends TimerTask{
            Handler handler;
            DoTick(Handler handler){ this.handler = handler; }
            
            public void run(){ handler.tickAction(); }
        }
        
        /** Create a new Handler for all the World's events */
        Handler(Activity act, BigBang world, Object w, Bitmap buff){
            super(act);
            this.act = act;
            this.world = world;
            this.w = w;
            this.scnBuffer = null;
            this.buffer = buff;
            this.run = new Timer();
            this.resetTimer();
            this.setFocusable(true);
            act.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN |
                                             WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN |
                                             WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
            
            //!! Make sure the keyboard doesn't open when we click
            this.setInputType(InputType.TYPE_NULL);
            
            if(world.onmouse != null){
                this.setOnLongClickListener(this);
            }
        }
        
        /** Unpause this previously paused simulation/animation */
        public void unpause(){
            if(this.isDone)return;
            if(world.ontick != null && !this.isRunning){
                this.resetTimer();
                this.isRunning = true;
            }
        }
        
        /** Pause this simulation/animation */
        public void pause(){
            if(this.isDone)return;
            if(world.ontick != null && this.isRunning){
                this.run.cancel();
                this.isRunning = false;
            }
        }
        
        /** Setup a new Timer task for on-tick */
        public void resetTimer(){
            run.cancel();
            run = new Timer();
            run.schedule(this.ticker = new DoTick(this), 200, (int)(world.time*1000));
        }
        
        /** Android uses <tt>onDraw(Canvas)</tt> instead of
         *    <tt>paint(Graphics)</tt>, though the two systems are
         *    very similar. */
        public void onDraw(Canvas c){
            Scene curr;
            if(!this.isDone)
                curr = this.world.doOnDraw(this.w);
            else
                curr = this.world.doLastScene(this.w);
            
            if(curr != this.scnBuffer){
                this.scnBuffer = curr;
                Canvas c2 = new Canvas(buffer);
                c2.drawRect(0,0, this.getWidth(), this.getHeight(), Image.WHITE);
                this.scnBuffer.paint(c2, 0, 0);
            }
            c.drawBitmap(buffer, 0, 0, Image.WHITE);
        }
        /** Android is not compatible with Swing timers, so we switch
         *    everything over to to java.util.Timer */
        public void tickAction(){
            if(!this.isRunning || this.isDone)return;
            replace(this.world.doOnTick(this.w));
        }
        
        static boolean screenShots = true;
        
        /** Keys in Android systems without a hardware keyboard are handled slightly different.  On
         *    the HTC Incredible holding the Menu button for an extended time will Show/Hid the
         *    software keyboard. */
        public boolean onKeyDown(int keyCode, KeyEvent e){
            if(!this.isRunning || this.isDone)return super.onKeyDown(keyCode, e);
            if(world.onkey == null ||
               keyCode == KeyEvent.KEYCODE_SHIFT_LEFT ||
               keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT ||
               keyCode == KeyEvent.KEYCODE_BACK ||
               keyCode == KeyEvent.KEYCODE_HOME)return super.onKeyDown(keyCode, e);
            if(Handler.screenShots && keyCode == KeyEvent.KEYCODE_MENU){
                final boolean oldRun = this.isRunning;
                this.isRunning = false;
                this.run.cancel();
                
                final String dir = "/screenshots";
                new File(Environment.getExternalStorageDirectory()+dir).mkdirs();
                
                final EditText et = new EditText(this.act);
                et.setText(Environment.getExternalStorageDirectory()+dir+"/screenshot.png");
                
                new AlertDialog.Builder(this.act)
                  .setMessage("Save Image As...")
                  .setView(et)
                  .setPositiveButton("Save", new Dialog.OnClickListener(){
                    public void onClick(DialogInterface d, int which){
                        String file = et.getText().toString();
                        try{
                            Handler.this.scnBuffer.toFile(file);
                        }catch(RuntimeException ee){
                            Throwable e = ee.getCause();
                            new AlertDialog.Builder(Handler.this.act)
                              .setMessage("Unable to save:\n  '"+file+"'\n   ("+
                                      e.getClass().getSimpleName()+")\n   "+e.getMessage())
                              .show();
                        }
                        Handler.this.isRunning = oldRun;
                        resetTimer();
                    }})
                  .setNegativeButton("Cancel", new Dialog.OnClickListener(){
                        public void onClick(DialogInterface d, int which){
                            Handler.this.isRunning = oldRun;
                            resetTimer();
                        }})
                  .show();
            }
            String key = convert(e.getKeyCode(), ""+((char)e.getUnicodeChar()));
            replace(world.doOnKeyEvent(w, key));
            return true;    
        }
        public boolean onKeyUp(int keyCode, KeyEvent e){
            if(!this.isRunning || this.isDone)return super.onKeyDown(keyCode, e);
            if(world.onkey == null ||
               keyCode == KeyEvent.KEYCODE_SHIFT_LEFT ||
               keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT ||
               keyCode == KeyEvent.KEYCODE_BACK ||
               keyCode == KeyEvent.KEYCODE_HOME)return super.onKeyUp(keyCode, e);   
            String key = convert(e.getKeyCode(), ""+((char)e.getUnicodeChar()));
            replace(world.doOnKeyRelease(w, key));
            return true;
        }
        /** Track-ball Events count for a mix of Mouse and Key events */
        public boolean onTrackballEvent(MotionEvent e){
            if(!this.isRunning || this.isDone)return super.onTrackballEvent(e);
            if(world.onkey == null)return super.onTrackballEvent(e);
            
            if(e.getAction() == MotionEvent.ACTION_DOWN)
                replace(world.doOnKeyEvent(w, "\n"));
            else if(e.getAction() == MotionEvent.ACTION_UP)
                replace(world.doOnKeyRelease(w, "\n"));
            
            if(e.getX() > 0.5)replace(world.doOnKeyEvent(w, KEY_ARROW_RIGHT));
            else if(e.getX() < -0.5)replace(world.doOnKeyEvent(w, KEY_ARROW_LEFT));
            if(e.getY() > 0.5)replace(world.doOnKeyEvent(w, KEY_ARROW_DOWN));
            else if(e.getY() < -0.5)replace(world.doOnKeyEvent(w, KEY_ARROW_UP));
            return true;
        }
        /** Android supports a long-press... so we capture that as a
         * separate event */
        public boolean onLongClick(View v){
            if(!this.isRunning || this.isDone)return false;
            if(world.onmouse == null)return false;
            replace(world.doOnMouseEvent(w, this.lastX, this.lastY, LONG_MOUSE_DOWN));
            return true;
        }
        /** Touch events are like Mouse events, but there's no real
         * analog of "move" for touch screens */
        public boolean onTouchEvent(MotionEvent e){
            if(!this.isRunning || this.isDone)return super.onTouchEvent(e);
            if(world.onmouse == null)return super.onTouchEvent(e);
            this.lastX = (int)e.getX();
            this.lastY = (int)e.getY();
            
            String what = "";
            if(e.getAction() == MotionEvent.ACTION_DOWN){
                what = MOUSE_DOWN;
                tap_down = true;
            }else if(e.getAction() == MotionEvent.ACTION_UP){
                what = MOUSE_UP;
                tap_down = false;
            }else if(e.getAction() == MotionEvent.ACTION_MOVE)
                what = tap_down?MOUSE_DRAG:MOUSE_MOVE;
            
            if(what.length() != 0){
                replace(world.doOnMouseEvent(w, (int)e.getX(), (int)e.getY(), what));
            }
            super.onTouchEvent(e);
            return true;
        }

        /** Required for sensor interface implementation */
        public void onAccuracyChanged(Sensor sensor, int accuracy){}
        /** Orientation listener methods */
        public void onSensorChanged(SensorEvent event){
            if(!this.isRunning || this.isDone)return;
            if(event.values.length < 3)return;
            replace(world.doOnOrientationEvent(w, event.values[0], event.values[1],
                                               event.values[2]));
        }

        private void replace(Object w){
            // This isn't enough when mutation is involved...
            if(!this.isRunning || this.isDone)return;
            
            if(this.isRunning && this.world.doStopWhen(w)){
                this.isRunning = false;
                this.isDone = true;
                this.run.cancel();
            }
            
            boolean change = !this.w.equals(w);
            this.w = w;
            if(change)this.postInvalidate();
        }
        private String convert(int code, String ch){
            switch(code){
            case KeyEvent.KEYCODE_DPAD_UP: return KEY_ARROW_UP;
            case KeyEvent.KEYCODE_DPAD_DOWN: return KEY_ARROW_DOWN;
            case KeyEvent.KEYCODE_DPAD_LEFT: return KEY_ARROW_LEFT;
            case KeyEvent.KEYCODE_DPAD_RIGHT: return KEY_ARROW_RIGHT;
            case KeyEvent.KEYCODE_MENU: return KEY_MENU;
            case KeyEvent.KEYCODE_SEARCH: return KEY_SEARCH;
            case KeyEvent.KEYCODE_DEL: return "\b";
            default: return ch;
            }
        }
    }
    
    /** Unpause this previously paused BigBang simulation/animation */
    public void unpause(){
        this.handler.unpause();
    }
    
    /** Pause this BigBang simulation/animation */
    public void pause(){
        this.handler.pause();
    }
    
}