/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *                                                                       *
 *   JavaWorld Library, Copyright 2011 Bryan Chadwick                    *
 *                                                                       *
 *   FILE: ./world/BigBang.java                                          *
 *                                                                       *
 *   This file is part of JavaWorld.                                     *
 *                                                                       *
 *   JavaWorld 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.            *
 *                                                                       *
 *   JavaWorld 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 JavaWorld.  If not, see <http://www.gnu.org/licenses/>.  *
 *                                                                       *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

package world;

import javax.swing.*;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;

import util.Util;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.RenderingHints;
import java.awt.event.*;
import java.lang.reflect.Method;
import java.util.Timer;
import java.util.TimerTask;

import image.*;

/** A Class representing the creation of a World/System 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/>
 *    
 *    If a matching method is not found when installing handlers, a RuntimeException is
 *    thrown, describing the offense.
 *    </p>
 */
public class BigBang{

    /** Default Tick rate for the world: ~33 frames per second */
    public static double DEFAULT_TICK_RATE = 0.03;

    /** Mouse down (button-down) event String */
    public static String MOUSE_DOWN = "button-down";
    /** Mouse up (button-up) event String */
    public static String MOUSE_UP = "button-up";
    /** Mouse window enter (enter) event String */
    public static String MOUSE_ENTER = "enter";
    /** Mouse window leave (leave) event String */
    public static String MOUSE_LEAVE = "leave";
    /** 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";   
    /** Key escape event String */
    public static String KEY_ESCAPE = "escape";   

    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;
    
    
    /** Create a new BigBang with a value of the initial World */
    public BigBang(Object initial){
        this(initial, initial.getClass(), 0.02,
                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[]{this.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);
    }
    /** 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[]{this.worldType}, this.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);
    }
    /** 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[]{this.worldType, int.class, int.class, String.class}, this.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);
    }
    /** 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[]{this.worldType, String.class}, this.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);
    }
    /** 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[]{this.worldType, String.class}, this.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);
    }
    /** 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[]{this.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);
    }
    /** 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[]{this.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);
    }
    
    // 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){
        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;
    }    
    
    /** Gap left around the border of the Window */
    private static int SPACE = 5;
    /** 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(this.ontick, this.ontickM, new Object[]{w});
    }
    /** Wrapper for the Mouse Handler */
    private Object doOnMouseEvent(Object w, int x, int y, String me) {
        return Util.applyFunc(this.onmouse, this.onmouseM, new Object[]{w,x-SPACE,y-SPACE,me});
    }
    /** Wrapper for the Key Handler */
    private Object doOnKeyEvent(Object w, String ke){
        if(ke.length() == 0)return w;
        return Util.applyFunc(this.onkey, this.onkeyM, new Object[]{w,ke});
    }
    /** Wrapper for the Key Release Handler */
    private Object doOnReleaseEvent(Object w, String ke){
        if(ke.length() == 0)return w;
        return Util.applyFunc(this.onrelease, this.onreleaseM, new Object[]{w,ke});
    }
    /** Wrapper for the Draw Handler */
    private Scene doOnDraw(Object w) {
        return (Scene)Util.applyFunc(this.ondraw, this.ondrawM, new Object[]{w});
    }
    /** Wrapper for the StopWhen Handler */
    private boolean doStopWhen(Object w) {
        if(this.stopwhen == null)return false;
        return (Boolean)Util.applyFunc(this.stopwhen, this.stopwhenM, new Object[]{w});
    }
    /** Wrapper for the LastScene Handler */
    private Scene doLastScene(Object w) {
        if(this.lastscene == null)return doOnDraw(w);
        return (Scene)Util.applyFunc(this.lastscene, this.lastsceneM, new Object[]{w});
    }
    
    /** Construct and run the animation/interaction system.  For the
     *    Swing version the method returns the final value of the
     *    World after the animation has completed.  The Window is
     *    opened as a Modal dialog, so control does not return to the
     *    bigband caller until the window is closed. */
    public Object bigBang(){
        return this.bigBang("BigBang");
    }
    
    /** Open a window and run the animation with the given title */
    public Object bigBang(String title){
        try{
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        }catch(Exception e){}

        if(this.ondraw == null)
            throw new RuntimeException("No World Draw Handler");
        JDialog f = new JDialog((JFrame)null, title, true);
        Scene scn = doOnDraw(this.initial);
        f.setSize((int)(SPACE*2+Math.max(20, 14+scn.width())),
                (int)(Math.max(20, SPACE*2+31+scn.height())));
        
        f.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
        f.setResizable(false);
        final Handler handler = new Handler(this,this.initial, scn,
                new BufferedImage((int)(scn.width()+2*SPACE), (int)(scn.height()+2*SPACE), BufferedImage.TYPE_INT_RGB), f);
        f.getContentPane().add(handler);
        f.setVisible(true);
        handler.run.cancel();
        return handler.w;        
    }
    /** Handles the nitty-gritty of world updates and interfacing with Swing */
    static class Handler extends javax.swing.JComponent
                         implements MouseListener,KeyListener,MouseMotionListener{
        private static final long serialVersionUID = 1L;
        BigBang world;
        Object w;
        Scene scnBuffer;
        BufferedImage buffer;
        Graphics2D graph;
        Timer run;
        TimerTask ticker;
        boolean isRunning = false;
        boolean isDone = false;
        
        /** Create a new Handler for all the World's events */
        Handler(BigBang world, Object ww, Scene scn, BufferedImage buff, JDialog dia){
            this.world = world;
            this.w = ww;

            this.scnBuffer = null;
            this.buffer = buff;
            this.graph = buff.createGraphics();
            this.graph.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            this.graph.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
            this.run = new Timer();
            addMouseListener(this);
            if(world.onmouse != null){
                addMouseMotionListener(this);
            }
            if(world.onkey != null)
                dia.addKeyListener(this);
            this.isRunning = true;
            if(world.ontick != null){
                this.run.scheduleAtFixedRate(this.ticker = new TimerTask(){
                    public void run(){ tickAction(); }
                }, 200, (int)(world.time*1000));
            }
        }
        /** Swing uses a <tt>paint(Graphics)</tt> method to draw the
         *    component (Handler) into the window. */
        public void paint(java.awt.Graphics g){
            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;
                this.graph.setColor(Color.white);
                this.graph.fillRect(0,0, this.getWidth(), this.getHeight());
                this.graph.clipRect(SPACE, SPACE, this.buffer.getWidth()-SPACE*2, this.buffer.getHeight()-SPACE*2);
                this.scnBuffer.paint(this.graph,SPACE,SPACE);
            }
            g.drawImage(this.buffer, 0, 0, null);
        }
        /** Rather than Swing timers, we use to java.util.Timer to
         *    provide compatibility with Android (i.e., so the code
         *    for both versions looks the same). */
        public void tickAction(){
            if(!this.isRunning || this.isDone)return;
            replace(this.world.doOnTick(this.w));
        }
        /** Support saving screenshots... */
        JPopupMenu popup = new JPopupMenu("World Options");
        {
            addItem(this.popup, "Save Image...", new ActionListener(){
                public void actionPerformed(ActionEvent e){
                    doSaveAs(Handler.this.scnBuffer);
                    synchronized(Handler.this.popup){ Handler.this.popup.notify(); }
                }
            });
            addItem(this.popup, "Continue",  new ActionListener(){
                public void actionPerformed(ActionEvent e){
                    synchronized(Handler.this.popup){ Handler.this.popup.notify(); }
                }
            });
            this.popup.addPopupMenuListener(new PopupMenuListener(){
                public void popupMenuCanceled(PopupMenuEvent arg0){
                    synchronized(Handler.this.popup){ Handler.this.popup.notify(); }
                }
                public void popupMenuWillBecomeVisible(PopupMenuEvent arg0){}
                public void popupMenuWillBecomeInvisible(PopupMenuEvent arg0){}
            });
        }
        private static void addItem(JPopupMenu m, String s, ActionListener l){
            JMenuItem item = new JMenuItem(s);
            if(l != null)
                item.addActionListener(l);
            m.add(item);
        }
        boolean doSaveAs(Scene scn){
            try{
                JFileChooser fc = new JFileChooser();
                int result = fc.showDialog(this, "SaveAs");
                // Check for existence... 
                if(result == JFileChooser.APPROVE_OPTION){
                    this.scnBuffer.toFile(fc.getSelectedFile().getAbsolutePath());
                    return true;
                }
            }catch(Exception e){
                System.err.println("Exception: "+e);
            }
            return false;
        }
        public void mousePressed(final MouseEvent e){
            if(e.isPopupTrigger()){
                // Pause the simulation...
                final Handler that = this;
                final boolean temp = this.isRunning;
                this.isRunning = false;

                // Show the context Menu
                new Thread(){
                    public void run(){
                        Handler.this.popup.show(that, e.getX(), e.getY());
                        try{
                            synchronized(Handler.this.popup){
                                Thread.yield();
                                Handler.this.popup.wait();
                            }
                        }catch(Exception ee){}
                        // Restart the simulation if running...
                        that.isRunning = temp;
                    }
                }.start();
            }else{
                if(this.isRunning && !this.isDone)
                    replace(this.world.doOnMouseEvent(this.w, e.getX(), e.getY(), MOUSE_DOWN));
            }
        }
        /** Mouse click/move/event Methods */
        public void mouseClicked(MouseEvent e){}
        public void mouseEntered(MouseEvent e){ if(this.isRunning && !this.isDone)replace(this.world.doOnMouseEvent(this.w, e.getX(), e.getY(), MOUSE_ENTER)); }
        public void mouseExited(MouseEvent e){ if(this.isRunning && !this.isDone)replace(this.world.doOnMouseEvent(this.w, e.getX(), e.getY(), MOUSE_LEAVE)); }
        public void mouseReleased(MouseEvent e){ if(this.isRunning && !this.isDone)replace(this.world.doOnMouseEvent(this.w, e.getX(), e.getY(), MOUSE_UP)); }
        public void mouseDragged(MouseEvent e){ if(this.isRunning && !this.isDone)replace(this.world.doOnMouseEvent(this.w, e.getX(), e.getY(), MOUSE_DRAG)); }
        public void mouseMoved(MouseEvent e){ if(this.isRunning && !this.isDone)replace(this.world.doOnMouseEvent(this.w, e.getX(), e.getY(), MOUSE_MOVE)); }
        /** Keys are converted to strings to simplify handling */
        public void keyPressed(KeyEvent e){
            if(this.isRunning && !this.isDone)
                replace(this.world.doOnKeyEvent(this.w, convert(e.getKeyCode(), ""+e.getKeyChar())));    
        }
        public void keyReleased(KeyEvent e){
            if(this.isRunning && !this.isDone)
                replace(this.world.doOnReleaseEvent(this.w, convert(e.getKeyCode(), ""+e.getKeyChar())));
        }
        public void keyTyped(KeyEvent e){
            //if(isRunning && !isDone)replace(world.doOnKeyEvent(w, ""+e.getKeyChar()));    
        }
        
        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)repaint();
        }
        private String convert(int code, String ch){
            switch(code){
            case KeyEvent.VK_UP: return KEY_ARROW_UP;
            case KeyEvent.VK_DOWN: return KEY_ARROW_DOWN;
            case KeyEvent.VK_LEFT: return KEY_ARROW_LEFT;
            case KeyEvent.VK_RIGHT: return KEY_ARROW_RIGHT;
            case KeyEvent.VK_ESCAPE: return KEY_ESCAPE;
            default: return ch;
            }
        }

    }
}