/*
 * Copyright © 2012 Nokia Corporation. All rights reserved.
 * Nokia and Nokia Connecting People are registered trademarks of Nokia Corporation.
 * Oracle and Java are trademarks or registered trademarks of Oracle and/or its
 * affiliates. Other product and company names mentioned herein may be trademarks
 * or trade names of their respective owners.
 * See LICENSE.TXT for license information.
 */

package com.nokia.example.drumkit;

import com.nokia.example.drumkit.helpers.Sample;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;
import javax.microedition.media.Manager;
import javax.microedition.media.Player;

public class SoundManager {
    // Constants
    public static final int SAMPLE_KICK = 0;
    public static final int SAMPLE_TOM1 = 1;
    public static final int SAMPLE_TOM2 = 2;
    public static final int SAMPLE_TOM3 = 3;
    public static final int SAMPLE_SNARE = 4;
    public static final int SAMPLE_COWBELL = 5;
    public static final int SAMPLE_HIHAT_OPEN = 6;
    public static final int SAMPLE_HIHAT_CLOSED = 7;
    public static final int SAMPLE_CRASH = 8;
    public static final int SAMPLE_CHINA = 9;
    public static final int SAMPLE_SPLASH = 10;
    public static final int SAMPLE_RIDE_BELL = 11;
    public static final int SAMPLE_RIDE = 12;
    
    // Members
    private int maxPrefetchedPlayers = 8;
    private int maxStartedPlayers = supportsMixing() ? 3 : 1;
    private final Vector prefetchedPlayers = new Vector();
    private final Vector startedPlayers = new Vector();
    
    private final String[] resources = {
        "/kick.wav", // 0
        "/tom1.wav", // 1
        "/tom2.wav", // 2
        "/tom3.wav", // 3
        "/snare.wav", // 4
        "/cowbell.wav", // 5
        "/hihat2.wav", // 6
        "/hihat1.wav", // 7
        "/crash.wav", // 8
        "/china.wav", // 9
        "/splash.wav", // 10
        "/ride2.wav", // 11
        "/ride1.wav", // 12
    };
    
    private Vector tape = new Vector();
    private Timer tapePlayer = new Timer();
    private boolean recording = false;
    private boolean playing = false;
    private Vector listeners = new Vector();

    /**
     * Constructor.
     */
    public SoundManager() {
        prefetchSounds();
    }

    public void addListener(Listener listener) {
        listeners.addElement(listener);
    }

    public void removeListener(Listener listener) {
        listeners.removeElement(listener);
    }

    /**
     * @return true if mixing is supported
     */
    public boolean supportsMixing() {
        String s = System.getProperty("supports.mixing");
        return s != null && s.equalsIgnoreCase("true") && !Main.isS60Phone();
    }
    
    public void prefetchSounds() {
        for (int i = 0; i < maxPrefetchedPlayers; i++) {
            InputStream stream = SoundManager.class.getResourceAsStream(resources[i]);
            
            try {
                limitPrefetchedPlayers();
                Player player = Manager.createPlayer(stream, "audio/wav");
                player.realize();
                player.prefetch();
                prefetchedPlayers.addElement(new SoundPlayer(i, player));
            }
            catch (Exception e) {
            }
        }
    }

    /**
     * Play a sound
     *
     * @param sound
     */
    public void playSound(int sound) {
        if (recording) {
            Sample sample = new Sample();
            sample.sound = sound;
            sample.time = System.currentTimeMillis();
            tape.addElement(sample);
        }

        if (!restart(sound)) {
            Player player = null;
            InputStream stream = SoundManager.class.getResourceAsStream(resources[sound]);
            
            try {
                limitPrefetchedPlayers();
                player = Manager.createPlayer(stream, "audio/wav");
                player.realize();
                player.prefetch();
                start(sound, player);
            }
            catch (Exception e) {
                // Either the device is in silent mode...
                if (prefetchedPlayers.isEmpty()) {
                    Main.getInstance().sampleAlert();
                } // ...or does not support having all 8 players prefetched
                else if (maxPrefetchedPlayers > 1) {
                    // Reduce amount of players and try again
                    maxPrefetchedPlayers--;
                    playSound(sound);
                }
            }
        }

        Enumeration e = listeners.elements();
        
        while (e.hasMoreElements()) {
            Listener l = (Listener) e.nextElement();
            l.soundPlayed(sound);
        }
    }

    /**
     * Clean up all loaded resources
     */
    public void cleanUp() {
        synchronized (prefetchedPlayers) {
            while (!prefetchedPlayers.isEmpty()) {
                clean((SoundPlayer) prefetchedPlayers.firstElement());
            }
        }
    }

    /**
     * Deallocate player
     *
     * @param sp
     */
    private void clean(SoundPlayer sp) {
        synchronized (prefetchedPlayers) {
            prefetchedPlayers.removeElement(sp);
            stop(sp.player);
            
            try {
                sp.player.deallocate();
                sp.player.close();
            }
            catch (Exception e) {
            }
        }
    }

    /**
     * Restart player, if the given sound is already set in one of the
     * prefetched players.
     *
     * @param sound The sound to restart.
     * @return True if successful, false otherwise.
     */
    private boolean restart(int sound) {
        synchronized (prefetchedPlayers) {
            for (int i = 0; i < prefetchedPlayers.size() - 1; i++) {
                SoundPlayer sp = (SoundPlayer) prefetchedPlayers.elementAt(i);
                
                if (sp.sound == sound) {
                    prefetchedPlayers.removeElement(sp);
                    prefetchedPlayers.addElement(sp);
                    stop(sp.player);
                    
                    try {
                        sp.player.setMediaTime(0);
                    }
                    catch (Exception e) {
                    }
                    
                    start(sp.player);
                    return true;
                }
            }
            
            return false;
        }
    }

    /**
     * Limit the amount of prefetched players
     */
    private void limitPrefetchedPlayers() {
        synchronized (prefetchedPlayers) {
            try {
                while (prefetchedPlayers.size() >= maxPrefetchedPlayers) {
                    SoundPlayer sp = (SoundPlayer) prefetchedPlayers.firstElement();
                    clean(sp);
                }
            }
            catch (Exception e) {
            }
        }
    }

    /**
     * Start a new player
     *
     * @param sound The sound to play.
     * @param player
     */
    private void start(int sound, Player player) {
        synchronized (prefetchedPlayers) {
            prefetchedPlayers.addElement(new SoundPlayer(sound, player));
            start(player);
        }
    }

    /**
     * Starts the given player.
     *
     * @param player The player to start.
     */
    private void start(Player player) {
        synchronized (startedPlayers) {
            try {
                while (startedPlayers.size() >= maxStartedPlayers) {
                    Player p = (Player) startedPlayers.firstElement();
                    startedPlayers.removeElementAt(0);
                    stop(p);
                }
            }
            catch (Exception e) {
            }
            
            startedPlayers.addElement(player);
            
            try {
                player.start();
            }
            catch (Exception e) {
            }
        }
    }

    /**
     * Stop a player
     *
     * @param player
     */
    private void stop(Player player) {
        synchronized (startedPlayers) {
            startedPlayers.removeElement(player);
            
            try {
                if (player.getState() == Player.STARTED) {
                    player.stop();
                }
            }
            catch (Exception e) {
            }
        }
    }

    public void startRecording() {
        if (recording) {
            return;
        }
        
        synchronized (tape) {
            tape.removeAllElements();
        }
        
        recording = true;
        Enumeration e = listeners.elements();
        
        while (e.hasMoreElements()) {
            Listener l = (Listener) e.nextElement();
            l.recordingStarted();
        }
    }

    public void playRecord() {
        if (tape != null && tape.size() > 0) {
            int length = tape.size();
            int i = 0;
            long startTime = ((Sample) tape.elementAt(0)).time;
            playing = true;

            // Schedule recorded samples
            while (i < length) {
                final Sample sample = (Sample) tape.elementAt(i);
                
                tapePlayer.schedule(new TimerTask() {
                    public void run() {
                        playSound(sample.sound);
                    }
                }, sample.time - startTime);
                i++;
            }

            // Schedule task for stopping the playback
            TimerTask end = new TimerTask() {
                public void run() {
                    stopPlayback();
                }
            };
            
            tapePlayer.schedule(end, ((Sample) tape.lastElement()).time - startTime + 100);
            Enumeration e = listeners.elements();
            
            while (e.hasMoreElements()) {
                Listener l = (Listener) e.nextElement();
                l.playingStarted();
            }
        }
    }

    public void stop() {
        if (playing) {
            stopPlayback();
        }
        
        if (recording) {
            stopRecording();
        }
    }

    public void stopPlayback() {
        playing = false;
        tapePlayer.cancel();
        tapePlayer = new Timer();
        Enumeration e = listeners.elements();
        
        while (e.hasMoreElements()) {
            Listener l = (Listener) e.nextElement();
            l.playingStopped();
        }
    }

    public void stopRecording() {
        recording = false;
        Enumeration e = listeners.elements();
        
        while (e.hasMoreElements()) {
            Listener l = (Listener) e.nextElement();
            l.recordingStopped();
        }
    }

    /**
     * Associate a sound to a player
     */
    private class SoundPlayer {
        public final int sound;
        public final Player player;

        public SoundPlayer(int sound, Player player) {
            this.sound = sound;
            this.player = player;
        }
    }
    
    /**
     * Listener interface for the SoundManager
     */
    public interface Listener {
        public void soundPlayed(int index);
        public void playingStarted();
        public void playingStopped();
        public void recordingStarted();
        public void recordingStopped();
    }
}
