package de.duehl.mp3.player.javazoom;

/*
 * 11/19/04     1.0 moved to LGPL.
 *-----------------------------------------------------------------------
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU Library General Public License as published
 *   by the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program 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 Library General Public License for more details.
 *
 *   You should have received a copy of the GNU Library General Public
 *   License along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *----------------------------------------------------------------------
 */

import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import de.duehl.mp3.player.javazoom.data.MoreAdvancedPlaybackEvent;
import de.duehl.mp3.player.javazoom.data.MoreAdvancesPlaybackListener;
import javazoom.jl.decoder.Bitstream;
import javazoom.jl.decoder.BitstreamException;
import javazoom.jl.decoder.Decoder;
import javazoom.jl.decoder.DecoderException;
import javazoom.jl.decoder.Header;
import javazoom.jl.decoder.JavaLayerException;
import javazoom.jl.decoder.SampleBuffer;
import javazoom.jl.player.AudioDevice;
import javazoom.jl.player.FactoryRegistry;

/**
 * Ein Javazoom-Player, den man Pausieren und Fortsetzen kann.
 *
 * @version 1.01     2025-07-26
 * @author Christian Dühl
 */

public class MoreAdvancedPlayer {

    /** Der MPEG Audio-Bitstream. */
    private Bitstream bitstream;

    /** Der MPEG Audio-Decoder. */
    private Decoder decoder;

    /**
     * Das AudioDevice, das die Frames abspielt.
     *
     * Beim Zugriff auf dieses AudioDevice wird es oft kopiert, bevor damit interagiert wird. Ich
     * vermute das wird deshalb gemacht, falls in der Zeit von außen das audioDevice neu gesetzt
     * wird. Denn es ist ja nur ein Verweis auf das gleiche Objekt im Spiecher.
     */
    private AudioDevice audioDevide;

    /** Gibt an, ob der Player geschlossen wurde. */
    private boolean closed;

    /** Gibt an, ob alle Frames abgespielt wurden. */
    private boolean complete;

    /**
     * Die Position beim Schließen des Players in Sekunden.
     *
     * In der Klasse AudioDevice steht zwar "Millisekunden", schaut man aber in die
     * Implementierung, werden diese dort durch 1000 geteilt.
     */
    private int lastPosition;

    /** Die Listener für den Abspielprozess. */
    private List<MoreAdvancesPlaybackListener> listeners;

    /** Der zuletzt abgegespielte Frame. */
    private int lastPlayedFrame;

    /** Der zuletzt abgegespielte Frame. */
    private int frameIndexCurrent;

    /** Konstruktor. Es wird ein eigenes AudioDevice erstellt. */
    public MoreAdvancedPlayer(InputStream stream) {
        this(stream, null);
    }

    /**
     * Konstruktor.
     *
     * @param stream
     *            Der Strom mit der abzuspielenden Musik.
     * @param audioDevide
     *            Das AudioDevice, mit dem der Stream abgespielt wird.
     */
    public MoreAdvancedPlayer(InputStream stream, AudioDevice audioDevide) {
        bitstream = new Bitstream(stream);

        closed = false;
        complete = false;
        lastPosition = -1;
        lastPlayedFrame = -1;

        listeners = new ArrayList<>();

        if (audioDevide != null) {
            this.audioDevide = audioDevide;
        }
        else {
            createAudioDevice();
        }
        openInAudioDevice();
    }

    private void createAudioDevice() {
        try {
            tryToCreateAudioDevice();
        }
        catch (JavaLayerException exception) {
            throw new RuntimeException("Fehler beim Erstellen eines eigenen AudioDevice: ",
                    exception);
        }
    }

    private void tryToCreateAudioDevice() throws JavaLayerException {
        this.audioDevide = FactoryRegistry.systemRegistry().createAudioDevice();
    }

    private void openInAudioDevice() {
        try {
            tryToOpenInAudioDevice();
        }
        catch (JavaLayerException exception) {
            throw new RuntimeException("Fehler beim Öffnen des Streams im AudioDevice: ",
                    exception);
        }
    }

    private void tryToOpenInAudioDevice() throws JavaLayerException {
        audioDevide.open(decoder = new Decoder());
    }

    /** Fügt einen Listener für den Abspielprozess hinzu. */
    public void addPlayBackListener(MoreAdvancesPlaybackListener listener) {
        listeners.add(listener);
    }

    /**
     * Spielt das Stück ab, solange es läuft.
     *
     * @return Gibt wahr zurück, falls es noch weitere abzuspielende Frames gibt, anderenfalls,
     *         wenn der letzte Frame abgespielt wurde, wird false zurückgegeben.
     */
    public boolean play() {
        return playFromFrame(0);
    }

    /**
     * Spielt das Stück ab dem gegebenen Frame ab.
     *
     * @param start
     *            Der erste Frame, der abgespielt werden soll.
     * @return Gibt wahr zurück, falls es noch weitere abzuspielende Frames gibt, anderenfalls,
     *         wenn der letzte Frame abgespielt wurde, wird false zurückgegeben.
     */
    public boolean playFromFrame(int start) {
        return play(start, Integer.MAX_VALUE);
    }

    /**
     * Spielt einen Ausschnitt aus dem Stück ab.
     *
     * @param start
     *            Der erste Frame, der abgespielt werden soll.
     * @param end
     *            Der letzte Frame, der abgespielt werden soll.
     * @return Gibt wahr zurück, falls es noch weitere abzuspielende Frames gibt, anderenfalls,
     *         wenn der letzte Frame abgespielt wurde, wird false zurückgegeben.
     */
    public boolean play(int start, int end) {
        boolean ret = true;
        int offset = start; // -52 ?
        while (offset-- > 0 && ret) {
            ret = skipFrame();
        }
        return playNumberOfFrames(end - start);
    }

    /**
     * Spielt die übergebene Anzahl von Audio-Frames ab.
     *
     * @param numberOfFramesFrames
     *            Die Anzahl der Frames, die abgespielt werden sollen.
     * @return Gibt wahr zurück, falls es noch weitere abzuspielende Frames gibt, anderenfalls,
     *         wenn der letzte Frame abgespielt wurde, wird false zurückgegeben.
     */
    private boolean playNumberOfFrames(int numberOfFramesFrames) {
        try {
            return tryToPlay(numberOfFramesFrames);
        }
        catch (JavaLayerException exception) {
            throw new RuntimeException("Fehler beim Abspielen: ", exception);
        }
    }

    private boolean tryToPlay(int numberOfFramesFrames) throws JavaLayerException {
        boolean continueReadingFrames = true;

        reportStartToListener();

        frameIndexCurrent = 0;

        while (frameIndexCurrent < numberOfFramesFrames && continueReadingFrames) {
            lastPlayedFrame = frameIndexCurrent;
            continueReadingFrames = decodeFrame();
            this.frameIndexCurrent++;
        }

        AudioDevice out = audioDevide;
        if (out != null) {
            out.flush();
            synchronized (this) {
                complete = (!closed);
                close();
            }

            reportEndToListenerWithOwnPosition(out);
        }

        return continueReadingFrames;
    }

    /** Berichtet dem Listener, dass das Abspielen gestartet wurde. */
    private void reportStartToListener() {
        for (MoreAdvancesPlaybackListener listener : listeners) {
            listener.playbackStarted(createEvent(MoreAdvancedPlaybackEvent.STARTED));
        }
    }

    /** Berichtet dem Listener, dass das Abspielen beendet wurde. */
    private void reportEndToListenerWithOwnPosition(AudioDevice out) {
        for (MoreAdvancesPlaybackListener listener : listeners) {
            listener.playbackFinished(
                    createEvent(MoreAdvancedPlaybackEvent.STOPPED, out.getPosition()));
        }
    }

    /** Berichtet dem Listener, dass das Abspielen beendet wurde. */
    private void reportEndToListener() {
        for (MoreAdvancesPlaybackListener listener : listeners) {
            listener.playbackFinished(createEvent(MoreAdvancedPlaybackEvent.STOPPED));
        }
    }

    /**
     * Schließt den Player.
     *
     * Das abgespielte Musikstück wird augenblicklich angehalten.
     */
    private synchronized void close() {
        AudioDevice out = audioDevide;
        if (out != null) {
            closeWithNonNullAUdioDevice(out);
        }
    }

    private void closeWithNonNullAUdioDevice(AudioDevice out) {
        closed = true;
        audioDevide = null;
        // this may fail, so ensure object state is set up before
        // calling this method.
        out.close();
        lastPosition = out.getPosition();
        closeBitStream();
    }

    private void closeBitStream() {
        try {
            bitstream.close();
        }
        catch (BitstreamException ex) {
            // wird ignoriert
        }
    }

    /**
     * Dekodiert einen einzelnen Frame.
     *
     * @return Gibt wahr zurück, falls der letzte Frame abgespielt wurde, anderenfalls, wenn es
     *         noch weitere abzuspielende Frames gibt, wird false zurückgegeben.
     */
    protected boolean decodeFrame() {
        try {
            return tryToDecodeFrame();
        }
        catch (Exception exception) {
            throw new RuntimeException("Fehler beim Abspielen eines Audio-Frames: ", exception);
        }
    }

    private boolean tryToDecodeFrame()
            throws BitstreamException, DecoderException, JavaLayerException {
        AudioDevice out = audioDevide;
        if (out == null) {
            return false;
        }

        Header header = bitstream.readFrame();
        if (header == null) {
            return false;
        }

        // sample buffer set when decoder constructed
        SampleBuffer output = (SampleBuffer) decoder.decodeFrame(header, bitstream);

        synchronized (this) {
            out = audioDevide;
            if (out != null) {
                out.write(output.getBuffer(), 0, output.getBufferLength());
            }
        }

        bitstream.closeFrame();
        return true;
    }

    /**
     * Überspringt einen einzelnen Frame.
     *
     * @return Gibt wahr zurück, falls der letzte Frame übersprungen wurde, anderenfalls, wenn es
     *         noch weitere abzuspielende Frames gibt, wird false zurückgegeben.
     */
    protected boolean skipFrame() {
        try {
            return tryToSkipFrame();
        }
        catch (Exception exception) {
            throw new RuntimeException("Fehler beim Überspringen eines Audio-Frames: ", exception);
        }
    }

    private boolean tryToSkipFrame() throws BitstreamException {
        Header header = bitstream.readFrame();
        if (header == null) {
            return false;
        }
        bitstream.closeFrame();
        return true;
    }

    /**
     * Erstellt ein Event mit der übergebenen ID und der Position des aktuell verwendeten
     * AudioDevice.
     */
    private MoreAdvancedPlaybackEvent createEvent(int id) {
        return createEvent(id, audioDevide.getPosition());
    }

    /**
     * Erstellt ein Event mit der übergebenen ID und der Position des übergebenen AudioDevice.
     */
    private MoreAdvancedPlaybackEvent createEvent(int id, int frame) {
        return new MoreAdvancedPlaybackEvent(this, id, frame);
    }

    /** Beendet das Abspielen und informiert den PausablePlaybackListener. */
    public void stop() {
        reportEndToListener();
        close();
    }

    /** Spielt alles übertragene noch ab. */
    public void flush() {
        if (audioDevide != null) {
            audioDevide.flush();
        }
    }

    /** Gibt an, ob der Player geschlossen wurde. */
    public boolean isClosed() {
        return closed;
    }

    /** Gibt an, ob alle Frames abgespielt wurden. */
    public boolean isComplete() {
        return complete;
    }

    /** Getter für die Position beim Schließen des Players in Sekunden. */
    public int getLastPosition() {
        return lastPosition;
    }

    /** Getter für den zuletzt abgegespielte Frame. */
    public int getLastPlayedFrame() {
        return lastPlayedFrame;
    }

}