/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *                                                                       *
 *   JavaWorld Library, Copyright 2011 Bryan Chadwick                    *
 *                                                                       *
 *   FILE: ./world/sound/SoundWorld.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.sound;

import java.util.HashMap;

import world.sound.tunes.*;
import world.BigBang;
import image.Scene;

/**
 * <style type='text/css'><!--
 *    .def{ color: #000000; }
 *    .com{ font-style: italic; color: #880000; }
 *    .keyw{ font-weight: bold; color: #000088; }
 *    .num{ color: #00AA00; }
 *    .str{ color: #CC00AB; }
 *    .prim{ color: #0000FF; }
 *    .func{ color: #BB7733; }
 *    img.example{ padding-left: 50px; padding-bottom: 30px; }
 *    table.events td{ padding-bottom: 20px; }
 *    table.events{ padding-left: 30px; padding-bottom: 20px; }
 *  --></style>
 *  
 *  A Class representing an imperative World with sound/music and the related methods for drawing the 
 *    world and handling various events.  In order to implement a functioning World with sound you
 *    must <i>extend</i> this class, and implement an {@link world.sound.SoundWorld#onDraw onDraw}
 *    method.  Other handler methods ({@link world.sound.SoundWorld#tickRate tickRate}, {@link world.sound.SoundWorld#onTick onTick}, 
 *    {@link world.sound.SoundWorld#onMouse onMouse}, {@link world.sound.SoundWorld#onKey onKey}, {@link world.sound.SoundWorld#onRelease onRelease}, 
 *    {@link world.sound.SoundWorld#stopWhen stopWhen}, and {@link world.sound.SoundWorld#lastScene lastScene}) are optional, and
 *    can be overridden to add new functionality.
 * <p>
 *  Each of the interaction methods can add {@link world.sound.tunes.Note Note}s (sounds) to be played
 *  (e.g., <code>onTick</code>) for a length of time after the event.  There are two <i>tune-collections</i>
 *  for adding sounds, the first {@link world.sound.SoundWorld#tickTunes tickTunes}, should be added
 *  to for notes/sounds that should be played for a specified length of time corresponding to a World
 *  event. The second, {@link world.sound.SoundWorld#keyTunes keyTunes}, should be added to for
 *  notes/sounds that should be played for as long as the key is pressed;  when the key is released
 *  the sound will be removed and playing of the note will stop.
 * </p>
 * <p>
 *  See the <tt>world.sound.tunes</tt> package for more details (<tt>Notes</tt>,
 *  <tt>Chords</tt>, etc.).
 * </p>
 * 
 * <p>
 * <h3>Extending VoidWorld</h3>
 * <blockquote>
 * Below is a simple example of a <code>VoidWorld</code> that adds a new point at each mouse
 * click.  The world contains a {@link image.Scene Scene} and a new {@link image.Circle Circle}
 * is placed for each <code class='str'>"button-down"</code> event received, and a
 * {@link world.sound.tunes.Note Note} is added to the {@link world.sound.SoundWorld#tickTunes tickTunes}
 * to be played at the current <code>pitch</code>, which is incremented.
 * 
 * <pre>   
 *        <span class="keyw">import</span> image.*;
 *        <span class="keyw">import</span> world.sound.SoundWorld;
 *        <span class="keyw">import</span> world.sound.tunes.Note;
 *        
 *        <span class="keyw">public</span> <span class="keyw">class</span> MousePointsSoundWorld <span class="keyw">extends</span> SoundWorld{
 *            <span class="com">// Simple Main Program</span>
 *            <span class="keyw">public</span> <span class="keyw">static</span> <span class="keyw">void</span> <span class="func">main</span>(<span class="prim">String</span>[] args)
 *            { <span class="keyw">new</span> <span class="func">MousePointsSoundWorld</span>().<span class="func">bigBang</span>(); }
 *            
 *            <span class="com">// The inner Scene</span>
 *            Scene scene = <span class="keyw">new</span> <span class="func">EmptyScene</span>(<span class="num">200</span>, <span class="num">200</span>);
 *            <span class="com">// The current pitch to be played</span>
 *            <span class="keyw">int</span> pitch = noteDownC;
 *        
 *            <span class="com">// Create a new World</span>
 *            <span class="func">MousePointsSoundWorld</span>(){}
 *            
 *            <span class="com">// Draw by returning the inner Scene</span>
 *            <span class="keyw">public</span> Scene <span class="func">onDraw</span>(){ <span class="keyw">return</span> <span class="keyw">this</span>.scene; }
 *        
 *            <span class="com">// On a mouse click add a circle to the inner Scene, increment the</span>
 *            <span class="com">//    current pitch and play a short note</span>
 *            <span class="keyw">public</span> <span class="keyw">void</span> <span class="func">onMouse</span>(<span class="keyw">int</span> x, <span class="keyw">int</span> y, <span class="prim">String</span> me){
 *                <span class="keyw">if</span>(me.<span class="func">equals</span>(<span class="str">"button-down"</span>)){
 *                    <span class="keyw">this</span>.pitch++;
 *                    <span class="keyw">this</span>.tickTunes.<span class="func">addNote</span>(WOOD_BLOCK, <span class="keyw">new</span> <span class="func">Note</span>(<span class="keyw">this</span>.pitch, <span class="num">1</span>));
 *                    <span class="keyw">this</span>.scene = <span class="keyw">this</span>.scene.<span class="func">placeImage</span>(
 *                                     <span class="keyw">new</span> <span class="func">Circle</span>(<span class="num">20</span>, <span class="str">"solid"</span>, <span class="str">"red"</span>)
 *                                           .<span class="func">overlay</span>(<span class="keyw">new</span> <span class="func">Circle</span>(<span class="num">20</span>, <span class="str">"outline"</span>, <span class="str">"black"</span>)), x, y);
 *                }
 *            }
 *        }
 * </pre>
 * 
 * After a few mouse clicks, the window will look something like this, though every
 * mouse click will have a corresponding sound at an increasing pitch:<br/><br/>
 * 
 *   <img class="example" src="test/sound-mouse-points.png" />
 * </blockquote>
 * </p>    
 */
public abstract class SoundWorld implements SoundConstants{
    
    /** 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 = BigBang.MOUSE_DOWN;
    /** Mouse up (button-up) event String */
    public static String MOUSE_UP = BigBang.MOUSE_UP;
    /** Mouse window enter (enter) event String */
    public static String MOUSE_ENTER = BigBang.MOUSE_ENTER;
    /** Mouse window leave (leave) event String */
    public static String MOUSE_LEAVE = BigBang.MOUSE_LEAVE;
    /** Mouse motion (move) event String */
    public static String MOUSE_MOVE = BigBang.MOUSE_MOVE;
    /** Mouse down & move (drag) event String */
    public static String MOUSE_DRAG = BigBang.MOUSE_DRAG;   

    /** Key arrow-up event String */
    public static String KEY_ARROW_UP = BigBang.KEY_ARROW_UP;
    /** Key arrow-down event String */
    public static String KEY_ARROW_DOWN = BigBang.KEY_ARROW_RIGHT;
    /** Key arrow-left event String */
    public static String KEY_ARROW_LEFT = BigBang.KEY_ARROW_LEFT;
    /** Key arrow-right event String */
    public static String KEY_ARROW_RIGHT = BigBang.KEY_ARROW_RIGHT;
    
    /** A representation of the current state of the MIDI synthesizer. */
    public MusicBox musicBox = new MusicBox();    
    
    /** The collection of tunes to play on tick.  Any tunes added after an event will begin
     *  playing as soon as the event is completely processed and will finish playing when
     *  the sound/Note's duration has elapsed. */
    public TuneCollection tickTunes;
    /** The collection of tunes to start playing when a key is
     *    pressed, which will automatically removed when the same
     *    key is released. */
    public TuneCollection keyTunes;  
    
    /** The collection of tunes currently playing on tick */
    private TuneCollection currentTickTunes;
    /** The number of ticks per Tune Tick (so clients can adjust the game speed without
     *  changing the length of Notes/sounds. */
    private int ticksPerTuneTick;
    /** The number of ticks left until the next Tune Tick */
    private int tuneTickJiffies;
    
    /** the collection of tunes currently playing on key event */
    protected HashMap<String,TuneCollection> keyReleasedTunes;
    
    
    /** Default constructor.  Simply initializes the tune/music classes. */
    public SoundWorld(){ 
        this.initMusic();
        // Each tune-tick is about a quarter of a second
        this.ticksPerTuneTick = (int)Math.max(1.0, 0.125/this.tickRate());
    }

    /** Initialize the MIDI synthesizer and the TuneCollections */
    private void initMusic(){
        /** The MIDI synthesizer that plays the notes */
        musicBox = new MusicBox();

        if(musicBox.isReady()){
            this.tickTunes = new TuneCollection(this.musicBox);
            this.currentTickTunes = new TuneCollection(this.musicBox);
            this.keyTunes = new TuneCollection(this.musicBox);  
            this.keyReleasedTunes = new HashMap<String, TuneCollection>();
        }else{
            /** notify the user that music cannot play */
            System.out.println("MIDI synthesizer or the soundbank not available.");
            System.out.println("Tunes will not be played.");
        }
    }
    
    
    /** Return a visualization of this <tt>World</tt> as a {@link image.Scene Scene}.
     *    See {@link image.EmptyScene}, {@link image.Scene#placeImage(Image, int, int)}, and
     *    {@link image.Scene#addLine(int, int, int, int, String)} for documentation on
     *    constructing <tt>Scene</tt>s */
    public abstract Scene onDraw();
    
    /** Return the tick rate for this World in <i>seconds</i>.  For example,
     *  <span class='num'>0.5</span> means two <i>ticks</i> per second.
     *  The rate is only accessed when bigBang() is initially called and the
     *  window is created. */
    public double tickRate(){ return DEFAULT_TICK_RATE; }
    
    /** Change this World based on the Tick of the clock.  This
     *  method is called to get the update the World on each clock tick.
     *  Sounds ({@link world.sound.tunes.Note Note}s) to play starting on the current tick
     *  may be added to the {@link world.sound.SoundWorld#tickTunes tickTunes} tune-collection
     *  to be played for a specified length of time.  Notes will stop playing automatically
     *  when the amount of time corresponding to the note's duration has elapsed.
     */
    public void onTick(){ }
    
    /** Wrapper for sound processing on tick */
    private void processTick(){
        if(musicBox.isReady()){ 
            // advance the tick on current tunes
            // and stop playing those that are done
            this.tuneTickJiffies--;
            if(this.tuneTickJiffies <= 0){
                this.tuneTickJiffies = this.ticksPerTuneTick;
                this.currentTickTunes.nextBeat();
            }
        }

        // process the changes to the world on this tick
        this.onTick();

        if(musicBox.isReady()){
            // play the tunes collected in the tick TuneCollection
            this.tickTunes.playTunes();
            this.currentTickTunes.add(this.tickTunes);
            this.tickTunes.clear();
        }
    }  

    /** Change this World when a mouse event is triggered.
     * <tt>x</tt> and <tt>y</tt> are the location of the event in the window, and 
     * <tt>event</tt> is a <tt>String</tt> that describes what kind of event
     * occurred.
     * 
     * <p>
     *   <b>Possible Mouse Events</b>
     * <table class='events'>
     *    <tr><td style="text-align:right"><tt class='str'>"button-down"</tt> : </td>
     *        <td>The user <i>presses</i> a mouse button in the World window</td></tr>
     *    <tr><td style="text-align:right"><tt class='str'>"button-up"</tt> : </td>
     *        <td>The user <i>releases</i> a mouse button in the World window</td></tr>
     *    <tr><td style="text-align:right"><tt class='str'>"move"</tt> : </td>
     *        <td>The user <i>moves</i> the mouse in the World window</td></tr>
     *    <tr><td style="text-align:right"><tt class='str'>"drag"</tt> : </td>
     *        <td>The user <i>holds</i> a mouse button and <i>moves</i> the mouse in the World window</td></tr>
     *    <tr><td style="text-align:right"><tt class='str'>"enter"</tt> : </td>
     *        <td>The user moves the mouse <i>in-to</i> the World window</td></tr>
     *    <tr><td style="text-align:right"><tt class='str'>"leave"</tt> : </td>
     *        <td>The user moves the mouse <i>out-of</i> the World window</td></tr>
     * </table>
     * </p>
     * 
     *  Sounds ({@link world.sound.tunes.Note Note}s) to play starting when a certain mouse event
     *  occurs may be added to the {@link world.sound.SoundWorld#tickTunes tickTunes} tune-collection
     *  to be played for a specified length of time.  Notes will stop playing automatically
     *  when the amount of time corresponding to the note's duration has elapsed.
     */
    public void onMouse(int x, int y, String event){ }

    /** Wrapper for sound processing on mouse */
    private void processMouse(int x, int y, String event){
        this.onMouse(x, y, event);

        if(musicBox.isReady()){
            this.tickTunes.playTunes();
            this.currentTickTunes.add(this.tickTunes);
            this.tickTunes.clearTunes();
        }
    }
    
    /** Change this World when a key event is
     * triggered. The given <tt>event</tt> is a <tt>String</tt> that
     * describes which key was pressed.
     *
     * <p>
     *   <b>Special Key</b>
     * <table class='events'>
     *    <tr><td style="text-align:right"><tt class='str'>"up"</tt> : </td>
     *        <td>The user presses the <i>up-arrow</i> key</td></tr>
     *    <tr><td style="text-align:right"><tt class='str'>"down"</tt> : </td>
     *        <td>The user presses the <i>down-arrow</i> key</td></tr>
     *    <tr><td style="text-align:right"><tt class='str'>"left"</tt> : </td>
     *        <td>The user presses the <i>left-arrow</i> key</td></tr>
     *    <tr><td style="text-align:right"><tt class='str'>"right"</tt> : </td>
     *        <td>The user presses the <i>right-arrow</i> key</td></tr>
     * </table>
     *
     * Other keys generate a single character <tt>String</tt> that
     * represents the key pressed. For example, Pressing the <i>B</i> key on
     * the keyboard generates <tt class='str'>"b"</tt> as an event.
     * If the shift key is held while pressing <i>B</i> then <tt
     * class='str'>"B"</tt> is generated.
     * </p>
     * 
     *  Sounds ({@link world.sound.tunes.Note Note}s) to play when the given key is pressed
     *  may be added to the {@link world.sound.SoundWorld#keyTunes keyTunes} tune-collection
     *  to be played until the same key is released.  Notes will not stop playing until the
     *  key is released.
     *
     * <p>
     *  Sounds to be played for a specific length of time after a certain key press (i.e., not
     *  until the key is released) may be added to the {@link world.sound.SoundWorld#tickTunes tickTunes}
     *  tune-collection (instead of {@link world.sound.SoundWorld#keyTunes keyTunes}) played until
     *  amount of time corresponding to the note's duration has elapsed.
     * </p>
     */
    public void onKey(String event){ }
    
    /** Wrapper for sound processing on key press */
    protected void processKey(String ke){
        // empty the key TuneCollection
        if(musicBox.isReady()){
            this.keyTunes.clearTunes();
        }
        
        // process the changes to the world on this key event
        this.onKey(ke);

        // play the tunes collected in the key TuneCollection
        // save what is currently playing so it plays until released
        if(musicBox.isReady()){
            if(!this.keyReleasedTunes.containsKey(ke)){
                this.keyReleasedTunes.put(ke, this.keyTunes.copy());
                this.keyTunes.playTunes();
            }
        }
    }

    /** Change this World when a key is released. The given <tt>event</tt>
     * is a <tt>String</tt> that describes which key was released.
     *
     * <p>
     *   <b>Special Keys</b>
     * <table class='events'>
     *    <tr><td style="text-align:right"><tt class='str'>"up"</tt> : </td>
     *        <td>The user presses the <i>up-arrow</i> key</td></tr>
     *    <tr><td style="text-align:right"><tt class='str'>"down"</tt> : </td>
     *        <td>The user presses the <i>down-arrow</i> key</td></tr>
     *    <tr><td style="text-align:right"><tt class='str'>"left"</tt> : </td>
     *        <td>The user presses the <i>left-arrow</i> key</td></tr>
     *    <tr><td style="text-align:right"><tt class='str'>"right"</tt> : </td>
     *        <td>The user presses the <i>right-arrow</i> key</td></tr>
     * </table>
     *
     * Other keys generate a single character <tt>String</tt> that
     * represents the key released. For example, Pressing then releasing the <i>B</i> key on
     * the keyboard generates <tt class='str'>"b"</tt> as an <tt>onKey</tt> event and again
     * as an <tt>onRelease</tt> event.  If the shift key is held while pressing/releasing <i>B</i> then <tt
     * class='str'>"B"</tt> is generated.
     * </p>
     * 
     * Sounds ({@link world.sound.tunes.Note Note}s) that were added to the
     * {@link world.sound.SoundWorld#keyTunes keyTunes} tune-collection on a previous
     * key press will be stopped.
     */
    public void onRelease(String event){ }
    
    /** Wrapper for sound processing on key release */
    private void processRelease(String ke){
        if(musicBox.isReady()){
            if(this.keyReleasedTunes.containsKey(ke)){
                this.keyReleasedTunes.remove(ke).clear();
            }
        }
            
        // invoke user-defined onKeyReleased method
        this.onRelease(ke);
    }    
    /** Determine if the World/interaction/animation should be
     * stopped.  Returning a value of <tt class='keyw'>true</tt>
     * discontinues all events (mouse, key, ticks) and causes {@link
     * world.sound.SoundWorld#lastScene} to be used to draw the final
     * <tt>Scene</tt>.
     */
    public boolean stopWhen(){ return false; }
    
    /** Returns the <tt>Scene</tt> that should be displayed when the
     * interaction/animation completes ({@link world.sound.SoundWorld#stopWhen}
     * returns <tt class='keyw'>true</tt>). */
    public Scene lastScene(){ return this.onDraw(); }

    /** Wrapper for the call to LastScene. Allows Tunes to be added to the tick collection. */
    protected Scene processLastScene(){
        Scene ret = this.lastScene();

        if(musicBox.isReady()){
            this.tickTunes.playTunes();
            this.currentTickTunes.add(this.tickTunes);
        }
        return ret;
    }

    
    /** Kick off the interaction/animation.  This method returns the final
     *    state of the world after the user closes the World window. */
    public SoundWorld bigBang(){
        SoundWorld fin = (SoundWorld)new BigBang(this)
            .onDraw(new WorldDraw())
            .onTick(new WorldTick(), tickRate())
            .onMouse(new WorldMouse())
            .onKey(new WorldKey())
            .onRelease(new WorldRelease())
            .stopWhen(new WorldStop())
            .lastScene(new WorldLast())
            .bigBang("SoundWorld");
        
        // Let stuff finish a bit...
        try{ Thread.sleep(500); }catch(Exception e){}
        
        // Kill all the notes
        if(musicBox.isReady()){
            this.currentTickTunes.clear();
            this.currentTickTunes.clearTunes();
            this.tickTunes.clear();
            this.tickTunes.clearTunes();
            this.keyTunes.clear();
            this.keyTunes.clearTunes();
            for(TuneCollection t : this.keyReleasedTunes.values()){
                t.clear();
                t.clearTunes();   
            }
        }
        return fin;
    }
    
    /** Wrapper for OnDraw callback */
    private static class WorldDraw{
        @SuppressWarnings("unused")
        Scene apply(SoundWorld w)
        { return w.onDraw(); }
    }
    /** Wrapper for OnTick callback */
    private static class WorldTick{
        @SuppressWarnings("unused")
        SoundWorld apply(SoundWorld w)
        { w.processTick(); return w; }
    }
    /** Wrapper for OnMouse callback */
    private static class WorldMouse{
        @SuppressWarnings("unused")
        SoundWorld apply(SoundWorld w, int x, int y, String me)
        { w.processMouse(x, y, me); return w; }
    }
    /** Wrapper for OnKey callback */
    private static class WorldKey{
        @SuppressWarnings("unused")
        SoundWorld apply(SoundWorld w, String ke)
        { w.processKey(ke); return w; }
    }
    /** Wrapper for OnRelease callback */
    private static class WorldRelease{
        @SuppressWarnings("unused")
        SoundWorld apply(SoundWorld w, String ke)
        { w.processRelease(ke); return w; }
    }
    /** Wrapper for StopWhen callback */
    private static class WorldStop{
        @SuppressWarnings("unused")
        boolean apply(SoundWorld w)
        { return w.stopWhen(); }
    }
    /** Wrapper for LastScene callback */
    private static class WorldLast{
        @SuppressWarnings("unused")
        Scene apply(SoundWorld w)
        { return w.processLastScene(); }
    }
    /** Overridden equality to method.  Returns <tt>false</tt> to make sure that
     *    changes to the world are redrawn every time. */
    public boolean equals(Object o){ return false; }
}