/*
 * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package com.sun.media.jfxmediaimpl;

import com.sun.media.jfxmedia.MediaManager;
import com.sun.media.jfxmedia.MediaPlayer;
import com.sun.media.jfxmedia.events.MediaErrorListener;
import com.sun.media.jfxmedia.events.PlayerStateEvent;
import com.sun.media.jfxmedia.events.PlayerStateListener;
import com.sun.media.jfxmedia.locator.Locator;
import com.sun.media.jfxmedia.logging.Logger;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * This is a stop-gap solution to eliminate the JavaSound implementation. It's
 * not meant to be perfect, just work well enough to demonstrate functionality.
 */
final class NativeMediaAudioClipPlayer
        implements PlayerStateListener, MediaErrorListener
{
    private MediaPlayer mediaPlayer;
    private int playCount;  // tracks number of times we've played
    private int loopCount;
    private boolean playing;
    private boolean ready; // tracks ready state
    private NativeMediaAudioClip sourceClip;

    private double volume;
    private double balance;
    private double pan;
    private double rate;
    private int priority;

    private final ReentrantLock playerStateLock = new ReentrantLock();

    private static final int MAX_PLAYER_COUNT = 16;

    private static final List<NativeMediaAudioClipPlayer> activePlayers =
                new ArrayList<NativeMediaAudioClipPlayer>(MAX_PLAYER_COUNT);
    private static final ReentrantLock playerListLock = new ReentrantLock();

    public static int getPlayerLimit() {
        return MAX_PLAYER_COUNT;
    }

    public static int getPlayerCount() {
        return activePlayers.size();
    }

    // Singleton scheduler thread
    private static class Enthreaderator {
        private static final Thread schedulerThread;

        static {
            schedulerThread = new Thread(() -> {
                clipScheduler();
            });
            schedulerThread.setDaemon(true);
            schedulerThread.start();
        }

        public static Thread getSchedulerThread() {
            return schedulerThread;
        }
    }

    private static final LinkedBlockingQueue<SchedulerEntry> schedule =
            new LinkedBlockingQueue<SchedulerEntry>();

    private static void clipScheduler() {
        while (true) {
            SchedulerEntry entry = null;
            try {
                entry = schedule.take();
            } catch (InterruptedException ie) {}

            if (null != entry) {
                if (entry.getCommand() == 0) {
                    NativeMediaAudioClipPlayer player = entry.getPlayer();
                    if (null != player) {
                        // play a clip
                        if (addPlayer(player)) {
                            player.play();
                        } else {
                            player.sourceClip.playFinished(); // couldn't schedule
                        }
                    }
                } else if (entry.getCommand() == 1) {
                    // stop all instances of a clip, or all clips
                    // drop from schedule too, synchronize as this is expensive anyways
                    URI sourceURI = entry.getClipURI();

                    playerListLock.lock();
                    try {
                        // Stop all active players
                        NativeMediaAudioClipPlayer[] players = new NativeMediaAudioClipPlayer[MAX_PLAYER_COUNT];
                        players = activePlayers.toArray(players);
                        if (null != players) {
                            for (int index = 0; index < players.length; index++) {
                                if (null != players[index] && (null == sourceURI ||
                                    players[index].source().getURI().equals(sourceURI)))
                                {
                                    players[index].invalidate();
                                }
                            }
                        }
                    } finally {
                        playerListLock.unlock();
                    }

                    // purge the schedule too
                    boolean clearSchedule = (null == sourceURI); // if no source given, kill all instances
                    for (SchedulerEntry killEntry : schedule) {
                        NativeMediaAudioClipPlayer player = killEntry.getPlayer();
                        if (clearSchedule ||
                            (null != player && player.sourceClip.getLocator().getURI().equals(sourceURI)))
                        {
                            // deschedule the entry
                            schedule.remove(killEntry);
                            player.sourceClip.playFinished();
                        }
                    }
                } else if (entry.getCommand() == 2) {
                    entry.getMediaPlayer().dispose();
                }

                // unblock any waiting threads
                entry.signal();
            }
        }
    }

    public static void playClip(NativeMediaAudioClip clip,
            double volume, double balance,
            double rate, double pan,
            int loopCount, int priority)
    {
        // Kickstart the scheduler thread if needed
        Enthreaderator.getSchedulerThread();

        // don't schedule if we're just going to add a duplicate
        // this will prevent the app from overloading the queue
        NativeMediaAudioClipPlayer newPlayer = new NativeMediaAudioClipPlayer(clip, volume, balance, rate, pan, loopCount, priority);
        SchedulerEntry entry = new SchedulerEntry(newPlayer);
        boolean scheduled = schedule.contains(entry);
        if (scheduled || !schedule.offer(entry)) {
            // didn't schedule, make sure we update playCount
                // don't spam the log if it's just a duplicate entry
            if (Logger.canLog(Logger.DEBUG) && !scheduled) {
                Logger.logMsg(Logger.DEBUG, "AudioClip could not be scheduled for playback!");
            }
            clip.playFinished();
        }
    }

    private static boolean addPlayer(NativeMediaAudioClipPlayer newPlayer) {
        // find an available slot, create new player, fill available slot
        // see if we have room first
        playerListLock.lock();
        try {
            int priority = newPlayer.priority();
            while (activePlayers.size() >= MAX_PLAYER_COUNT) {
                // no more room, find a lower priority player to kill
                NativeMediaAudioClipPlayer target = null; // target for removal
                for (NativeMediaAudioClipPlayer player : activePlayers) {
                    if (player.priority() <= priority &&
                            (target != null ? (target.isReady() && (player.priority() < target.priority())) : true))
                    {
                        // DO NOT MODIFY activePlayers here!!!
                        target = player;
                    }
                }
                if (null != target) {
                    // found a target, kill it
                    target.invalidate();
                } else {
                    // this clip has too low priority, punt
                    return false;
                }
            }
            activePlayers.add(newPlayer);
        } finally {
            playerListLock.unlock();
        }
        return true;
    }

    // Pass null to stop all players
    public static void stopPlayers(Locator source) {
        URI sourceURI = (source != null) ? source.getURI() : null;
        // Use the scheduler thread to handle stopping playback
        // that way we avoid inadvertently allowing already scheduled clips to
        // slip through
        if (null != Enthreaderator.getSchedulerThread()) {
            // drop from the schedule too, we post an entry and wait for the
            // scheduler to process it, otherwise we would have to write a lot of
            // ugly code to work around concurrency issues
            CountDownLatch stopSignal = new CountDownLatch(1);
            SchedulerEntry entry = new SchedulerEntry(sourceURI, stopSignal);
            if (schedule.offer(entry)) {
                // block until the command is processed
                try {
                    // if it doesn't happen in five seconds we got problems
                    stopSignal.await(5, TimeUnit.SECONDS);
                } catch (InterruptedException ie) {}
            }
        }
    }

    private NativeMediaAudioClipPlayer(NativeMediaAudioClip clip, double volume,
            double balance, double rate, double pan, int loopCount, int priority)
    {
        sourceClip = clip;
        this.volume = volume;
        this.balance = balance;
        this.pan = pan;
        this.rate = rate;
        this.loopCount = loopCount;
        this.priority = priority;
        ready = false;
    }

    private Locator source() {
        return sourceClip.getLocator();
    }

    public double volume() {
        return volume;
    }

    public void setVolume(double volume) {
        this.volume = volume;
    }

    public double balance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    public double pan() {
        return pan;
    }

    public void setPan(double pan) {
        this.pan = pan;
    }

    public double playbackRate() {
        return rate;
    }

    public void setPlaybackRate(double rate) {
        this.rate = rate;
    }

    public int priority() {
        return priority;
    }

    public void setPriority(int priority) {
        this.priority = priority;
    }

    public int loopCount() {
        return loopCount;
    }

    public void setLoopCount(int loopCount) {
        this.loopCount = loopCount;
    }

    public boolean isPlaying() {
        return playing;
    }

    private boolean isReady() {
        return ready;
    }

    public synchronized void play() {
        playerStateLock.lock();
        try {
            playing = true;
            playCount = 0;

            if (null == mediaPlayer) {
                mediaPlayer = MediaManager.getPlayer(source());
                mediaPlayer.addMediaPlayerListener(this);
                mediaPlayer.addMediaErrorListener(this);
            } else {
                mediaPlayer.play();
            }
        } finally {
            playerStateLock.unlock();
        }
    }

    public void stop() {
        invalidate();
    }

    public synchronized void invalidate() {
        playerStateLock.lock();
        playerListLock.lock();

        try {
            playing = false;
            playCount = 0;
            ready = false;

            activePlayers.remove(this);
            sourceClip.playFinished();

            if (null != mediaPlayer) {
                mediaPlayer.removeMediaPlayerListener(this);
                mediaPlayer.setMute(true);
                SchedulerEntry entry = new SchedulerEntry(mediaPlayer);
                if (!schedule.offer(entry)) {
                    mediaPlayer.dispose();
                }
                mediaPlayer = null;

            }
        } catch (Throwable t) {
//            System.err.println("Caught exception trying to invalidate AudioClip player: "+t);
//            t.printStackTrace(System.err);
        } finally {
            playerListLock.unlock();
            playerStateLock.unlock();
        }
    }

    public void onReady(PlayerStateEvent evt) {
        playerStateLock.lock();
        try {
            ready = true;
            if (playing) {
                mediaPlayer.setVolume((float)volume);
                mediaPlayer.setBalance((float)balance);
                mediaPlayer.setRate((float)rate);
                mediaPlayer.play();
            }
        } finally {
            playerStateLock.unlock();
        }
    }

    public void onPlaying(PlayerStateEvent evt) {
    }

    public void onPause(PlayerStateEvent evt) {
    }

    public void onStop(PlayerStateEvent evt) {
        invalidate();
    }

    public void onStall(PlayerStateEvent evt) {
    }

    public void onFinish(PlayerStateEvent evt) {
        playerStateLock.lock();
        try {
            if (playing) {
                if (loopCount != -1) {
                    playCount++;
                    if (playCount <= loopCount) {
                        mediaPlayer.seek(0); // restart
                    } else {
                        invalidate();
                    }
                } else {
                    mediaPlayer.seek(0); // restart
                }
            }
        } finally {
            playerStateLock.unlock();
        }
    }

    public void onHalt(PlayerStateEvent evt) {
        invalidate();
    }

    public void onWarning(Object source, String message) {
    }

    public void onError(Object source, int errorCode, String message) {
        if (Logger.canLog(Logger.ERROR)) {
            Logger.logMsg(Logger.ERROR, "Error with AudioClip player: code "+errorCode+" : "+message);
        }
        invalidate();
    }

    /*
     * Override equals for using in a List of clips pended for pllayback.
     * Equals is used to avoid repetitions. hashCode is not necessary here.
     */
    @Override
    public boolean equals(Object that) {
        if (that == this) {
            return true;
        }

        if (that instanceof NativeMediaAudioClipPlayer) {
            NativeMediaAudioClipPlayer otherPlayer = (NativeMediaAudioClipPlayer)that;
            URI myURI = sourceClip.getLocator().getURI();
            URI otherURI = otherPlayer.sourceClip.getLocator().getURI();

            return myURI.equals(otherURI) &&
                   priority == otherPlayer.priority &&
                   loopCount == otherPlayer.loopCount &&
                   Double.compare(volume, otherPlayer.volume) == 0 &&
                   Double.compare(balance, otherPlayer.balance) == 0 &&
                   Double.compare(rate, otherPlayer.rate) == 0 &&
                   Double.compare(pan, otherPlayer.pan) == 0;
        } else {
            return false;
        }
    }

    private static class SchedulerEntry {
        private final int command; // 0 = play, 1 = stop, 2 = dispose
        private final NativeMediaAudioClipPlayer player; // MAY BE NULL!
        private final URI clipURI; // MAY BE NULL!
        private final CountDownLatch commandSignal; // MAY BE NULL!
        private final MediaPlayer mediaPlayer; // MAY BE NULL!

        // Play command constructor
        public SchedulerEntry(NativeMediaAudioClipPlayer player) {
            command = 0;
            this.player = player;
            clipURI = null;
            commandSignal  = null;
            mediaPlayer = null;
        }

        // Stop command constructor
        public SchedulerEntry(URI sourceURI, CountDownLatch signal) {
            command = 1;
            player = null;
            clipURI = sourceURI;
            commandSignal = signal;
            mediaPlayer = null;
        }

        // Dispose command constructor
        public SchedulerEntry(MediaPlayer mediaPlayer) {
            command = 2;
            player = null;
            clipURI = null;
            commandSignal = null;
            this.mediaPlayer = mediaPlayer;
        }

        public int getCommand() {
            return command;
        }

        public NativeMediaAudioClipPlayer getPlayer() {
            return player;
        }

        public URI getClipURI() {
            return clipURI;
        }

        public MediaPlayer getMediaPlayer() {
            return mediaPlayer;
        }

        public void signal() {
            if (null != commandSignal) {
                commandSignal.countDown();
            }
        }

        // provided ONLY for play implementation, so we can check for duplicate
        // schedule entries
        @Override public boolean equals(Object other) {
            if (other instanceof SchedulerEntry) {
                if (null != player) {
                    return player.equals(((SchedulerEntry)other).getPlayer());
                }
            }
            return false;
        }
    }
}
