package de.duehl.mp3.player;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

import javax.sound.sampled.FloatControl;
import javax.sound.sampled.SourceDataLine;

import de.duehl.basics.system.SystemTools;
import de.duehl.mp3.player.data.MusicEndReachedReactor;
import de.duehl.mp3.player.javazoom.MoreAdvancedPlayer;

import java.io.IOException;
import javazoom.jl.decoder.JavaLayerException;
import javazoom.jl.player.AudioDevice;
import javazoom.jl.player.FactoryRegistry;
import javazoom.jl.player.JavaSoundAudioDevice;

/**
 * Ein Player für MP3-Dateien.
 *
 * Nach Code von David J. Barnes und Michael Kölling (31.07.2011) und anderen, überarbeitet von
 * Christian Dühl.
 *
 * Siehe auch:
 * https://falconbyte.net/blog-java-mp3.php, hieß ursprünglich "MusikPlayer".
 *
 * https://github.com/Gikkman/JavaZoom-Volume-Controll/blob/
 *     master/src/main/java/com/gikk/javazoom/JLayerTest.java
 *
 * @version 1.01     2025-08-06
 * @author Christian Dühl
 */

public class MP3Player {

    private static final boolean DEBUG = false;


    /** Der Dateiname der MP3-Datei, die abgespielt wird. */
    private String filename;

    /** Der JavaZoom-Player. */
    private MoreAdvancedPlayer player;

    /** Das AudioDevice, das die Frames abspielt. */
    private AudioDevice device;

    /** Zur Steuerung der Lautstärke. */
    private FloatControl volumeControl;

    /**
     * Die hier hinterlegten Klassen werden informiert, wenn das aktuell gespielte Stück
     * vollständig abgespielt ist.
     */
    private List<MusicEndReachedReactor> musicEndReachedReactors;

    /** Konstruktor. */
    public MP3Player() {
        player = null;
        musicEndReachedReactors = new ArrayList<>();
    }

    /**
     * Fügt eine Klasse hinzu, die informiert wird, wenn das aktuell gespielte Stück vollständig
     * abgespielt ist.
     */
    public void addMusicEndReachedReactor(MusicEndReachedReactor musicEndReachedReactor) {
        musicEndReachedReactors.add(musicEndReachedReactor);
    }

    /**
     * Entfernt eine Klasse, die informiert wird, wenn das aktuell gespielte Stück vollständig
     * abgespielt ist.
     */
    public void removeMusicEndReachedReactor(MusicEndReachedReactor musicEndReachedReactor) {
        musicEndReachedReactors.remove(musicEndReachedReactor);
    }

    /** Spielt das Musikstück in einem eigenen Tread ab. */
    public void playMP3InOwnThread(String filename) {
        playMP3InOwnThread(filename, 0);
    }

    /**
     * Spielt das Musikstück in einem eigenen Tread ab.
     *
     * @param start
     *            Der erste Frame, der abgespielt werden soll.
     */
    public void playMP3InOwnThread(String filename, int start) {
        this.filename = filename;
        startPlayInOwnThread(start);
    }

    /**
     * Spielt das Stück ab dem gegebenen Frame ab.
     *
     * @param start
     *            Der erste Frame, der abgespielt werden soll.
     */
    private void startPlayInOwnThread(int start) {
        try {
            tryToStartPlayInOwnThread(start);
        }
        catch (Exception exception) {
            throw new RuntimeException("Es gab ein Problem bei startPlayInOwnThread().", exception);
        }
    }

    /**
     * Spielt das Stück ab dem gegebenen Frame ab.
     *
     * @param start
     *            Der erste Frame, der abgespielt werden soll.
     */
    private void tryToStartPlayInOwnThread(int start) {
        preparePlayer();
        new Thread(() -> startPlay(start)).start();
    }

    private void preparePlayer() {
        try {
            InputStream stream = createInputStream();
            device = createAudioDevice();
            player = new MoreAdvancedPlayer(stream, device);
        }
        catch (Exception exception) {
            killPlayer();
            throw new RuntimeException("Es gab ein Problem bei preparePlayer().", exception);
        }
    }

    private InputStream createInputStream() throws IOException {
        return new BufferedInputStream(new FileInputStream(filename));
    }

    private AudioDevice createAudioDevice() throws JavaLayerException {
        return FactoryRegistry.systemRegistry().createAudioDevice();
    }

    /**
     * Spielt das Stück ab dem gegebenen Frame ab.
     *
     * @param start
     *            Der erste Frame, der abgespielt werden soll.
     */
    private void startPlay(int start) {
        try {
            tryToStartPlay(start);
        }
        catch (Exception exception) {
            throw new RuntimeException("Es gab ein Problem bei startPlay().", exception);
        }
        finally {
            killPlayer();
        }
    }

    /**
     * Spielt das Stück ab dem gegebenen Frame ab.
     *
     * @param start
     *            Der erste Frame, der abgespielt werden soll.
     */
    private void tryToStartPlay(int start) {
        boolean moreToPlay = player.playFromFrame(start);
        if (!moreToPlay) {
            informReactors();
        }
    }

    private void informReactors() {
        for (MusicEndReachedReactor musicEndReachedReactor : musicEndReachedReactors) {
            musicEndReachedReactor.musicEndReached();
        }
    }

    /**
     * Setzt die Lautstärke. Werte sind nur von 0 bis 100 gültig und sind in der Einheit.
     * Funktioniert nur, wenn die Musik bereits abgespielt wird.
     *
     * @param percent
     *            Die Lautstärke zwischen 0 und 100 Prozent.
     */
    public void setVolumeInPercent(int percent) {
        float volume = VolumeCalculations.volumePercentToVolumeDb(percent);
        say("Setze auf: " + VolumeCalculations.toCorrectVolumePercent(percent) + "%, also " + volume
                + " dB");
        setVolumeInDb(volume);
    }

    /**
     * Setzt die Lautstärke. Werte sind nur von -80.0 bis 6.0 gültig und sind in der Einheit dB.
     *
     * Funktioniert nur, wenn die Musik bereits abgespielt wird.
     *
     * @param volume
     *            Die Lautstärke in dB von -80.0 bis 6.0.
     */
    public void setVolumeInDb(float volume) {
        say("setVolume() gain = " + volume);
        if (volumeControl == null) {
            say("War null");
            initVolumeControl();
        }

        if (volumeControl == null) {
            say("Ist immer noch null");
        }
        else {
            float minimum = volumeControl.getMinimum();
            if (Math.abs(minimum - VolumeCalculations.MIN_VOLUME_DB) > 0.01f) {
                System.out.println("Abweichung vom erwarteten Lautstärke Minimum: " + minimum
                        + " statt " + VolumeCalculations.MIN_VOLUME_DB);
            }
            //say("Lautstärke Minimum: " + minimum);
            float maximum = volumeControl.getMaximum();
            if (maximum > VolumeCalculations.MAX_VOLUME_DB) {
                maximum = VolumeCalculations.MAX_VOLUME_DB;
            }
            /*
            if (Math.abs(maximum - VolumeCalculations.MAX_VOLUME_DB) > 0.01f) {
                System.out.println("Abweichung vom erwarteten Lautstärke Maximum: " + maximum
                        + " statt " + VolumeCalculations.MAX_VOLUME_DB);
            }
            */
            //say("Lautstärke Maximum: " + maximum);
            float volumeToSet = Math.min(Math.max(volume, minimum), maximum);
            say("War: " + volumeControl.getValue() + ", wird nun gesetzt auf: " + volumeToSet);
            volumeControl.setValue(volumeToSet);
        }
    }

    private void initVolumeControl() {
        try {
            tryToInitVolumeControl();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void tryToInitVolumeControl() throws IllegalArgumentException, IllegalAccessException {
        Class<JavaSoundAudioDevice> clazz = JavaSoundAudioDevice.class;
        Field[] fields = clazz.getDeclaredFields();

        SourceDataLine source = null;
        for (Field field : fields) {
            if ("source".equals(field.getName())) {
                field.setAccessible(true);
                while (source == null) {
                    SystemTools.sleep(5);
                    source = (SourceDataLine) field.get(device);
                    if (!sourceHasMasterGain(source)) {
                        source = null;
                    }
                }
                field.setAccessible(false);
                volumeControl = (FloatControl) source.getControl(FloatControl.Type.MASTER_GAIN);
                break;
            }
        }
    }

    private boolean sourceHasMasterGain(SourceDataLine source) {
        try {
            source.getControl(FloatControl.Type.MASTER_GAIN);
            return true;
        }
        catch (Exception exception) {
            return false;
        }
    }

    /** Beendet das Abspielen des aktuell gespielten Musikstücks. */
    public int stop() {
        return killPlayer();
    }

    private int killPlayer() {
        synchronized (this) {
            if (player == null) {
                return -1;
            }
            else {
                player.stop();
                player.flush();
                int lastPlayedFrame = player.getLastPlayedFrame();
                player = null;
                return lastPlayedFrame;
            }
        }
    }

    private void say(String message) {
        if (DEBUG) {
            System.out.println(message);
        }
    }

}
