package thb.jeanluc.adventure.io;

import lombok.extern.slf4j.Slf4j;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.FloatControl;
import javax.sound.sampled.SourceDataLine;
import java.io.BufferedInputStream;
import java.io.InputStream;

/**
 * Streams OGG Vorbis tracks bundled on the classpath under {@code /music/}
 * through a {@link SourceDataLine}, looping, with ~400 ms gain fades. Decoding
 * uses the vorbisspi/jorbis {@code javax.sound} SPI. All playback runs on a
 * daemon thread; missing/undecodable files are logged and stay silent. GUI-only
 * — verified manually.
 */
@Slf4j
public final class OggMusicBackend implements MusicBackend {

    private static final String MUSIC_DIR = "/music/";
    private static final int FADE_MS = 400;
    private static final int FADE_STEPS = 20;
    private static final int BUFFER = 4096;
    private static final float FLOOR_DB = -40f; // near-silence for fades

    private volatile Thread playThread;
    private volatile boolean stopping;
    private volatile SourceDataLine line;
    private volatile float targetGainDb = MusicLevel.MEDIUM.gainDb();

    @Override
    public synchronized void fadeTo(String track, float gainDb) {
        stopCurrent();
        targetGainDb = gainDb;
        String resource = MUSIC_DIR + track;
        if (getClass().getResource(resource) == null) {
            log.warn("Music resource not found, staying silent: {}", resource);
            return;
        }
        stopping = false;
        playThread = new Thread(() -> playLoop(resource, gainDb), "music");
        playThread.setDaemon(true);
        playThread.start();
    }

    @Override
    public synchronized void fadeOut() {
        stopCurrent();
    }

    @Override
    public synchronized void setGainDb(float gainDb) {
        targetGainDb = gainDb;
        SourceDataLine l = line;
        if (l != null) {
            applyGain(l, gainDb);
        }
    }

    @Override
    public synchronized void shutdown() {
        stopCurrent();
    }

    /** Signals the play thread to stop and waits for its fade-out + cleanup. */
    private void stopCurrent() {
        Thread t = playThread;
        if (t == null) {
            return;
        }
        stopping = true;
        try {
            t.join(FADE_MS + 600L);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        playThread = null;
    }

    private void playLoop(String resource, float gainDb) {
        SourceDataLine l = null;
        try {
            while (!stopping) {
                // Reopen the bundled resource each loop; BufferedInputStream gives
                // the SPI the mark/reset support it needs to read the OGG header.
                try (InputStream res = getClass().getResourceAsStream(resource);
                     AudioInputStream in = AudioSystem.getAudioInputStream(new BufferedInputStream(res))) {
                    AudioFormat base = in.getFormat();
                    AudioFormat pcm = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED,
                            base.getSampleRate(), 16, base.getChannels(),
                            base.getChannels() * 2, base.getSampleRate(), false);
                    try (AudioInputStream din = AudioSystem.getAudioInputStream(pcm, in)) {
                        if (l == null) {
                            l = AudioSystem.getSourceDataLine(pcm);
                            l.open(pcm);
                            l.start();
                            line = l;
                            ramp(l, FLOOR_DB, gainDb); // fade in once
                        }
                        byte[] buf = new byte[BUFFER];
                        int n;
                        while (!stopping && (n = din.read(buf, 0, buf.length)) != -1) {
                            l.write(buf, 0, n);
                        }
                    }
                }
            }
        } catch (Exception e) {
            log.warn("Music playback failed for {}: {}", resource, e.getMessage());
        } finally {
            if (l != null) {
                try {
                    ramp(l, currentGain(l), FLOOR_DB); // fade out
                } catch (RuntimeException ignored) {
                    // best-effort fade
                }
                l.stop();
                l.close();
            }
            line = null;
        }
    }

    private void ramp(SourceDataLine l, float fromDb, float toDb) {
        for (int i = 0; i <= FADE_STEPS; i++) {
            applyGain(l, fromDb + (toDb - fromDb) * i / FADE_STEPS);
            sleep(FADE_MS / FADE_STEPS);
        }
    }

    private float currentGain(SourceDataLine l) {
        try {
            return ((FloatControl) l.getControl(FloatControl.Type.MASTER_GAIN)).getValue();
        } catch (IllegalArgumentException e) {
            return targetGainDb;
        }
    }

    private void applyGain(SourceDataLine l, float db) {
        try {
            FloatControl gain = (FloatControl) l.getControl(FloatControl.Type.MASTER_GAIN);
            gain.setValue(Math.max(gain.getMinimum(), Math.min(gain.getMaximum(), db)));
        } catch (IllegalArgumentException ignored) {
            // line has no master-gain control on this platform
        }
    }

    private void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
