// Copyright 2014-2025 Jesper Larsson
//
// This file is part of Klipspringer, <https://klipspringer.avadeaux.net/>
//
// Klipspringer is free software: you can redistribute it and/or modify it under the terms of the
// GNU General Public License as published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// Klipspringer 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 for more details.
//
// You should have received a copy of the GNU General Public License along with Klipspringer. If
// not, see <https://www.gnu.org/licenses/>.

package net.avadeaux.klipspringer.codec;

import java.nio.ByteBuffer;
import java.io.IOException;
import javax.sound.sampled.*;

/** Audio decoder that reads from an AudioInputStream. This allows obtaining an {@link AudioDecoder}
  * for any format supported by the standard Java Sound system by wrapping a stream obtained for
  * instance by {@link AudioSystem#getAudioInputStream(File)}.
  */
public class AudioInputStreamDecoder implements AudioDecoder {
    private enum State { NEW, INITIALIZED, METADATA_DECODED, AUDIO_DECODED, DESTROYED }

    private final Target target;
    private final AudioInputStream stream;
    private State state = State.NEW;

    // Set in decodeMetadata, to postpone actually reading input.
    private PcmFormat format;

    /** Sets up for sending data from the given AudioInputStream to the given target. */
    public AudioInputStreamDecoder(AudioInputStream stream, Target target) {
        this.target = target;
        this.stream = stream;
        state = State.INITIALIZED;
    }

    /** Returns false, indicating that decodeAll can be called at most once. */
    public boolean reusable() { return false; }

    public synchronized void close() throws IOException {
        state = State.DESTROYED;
        stream.close();
    }

    // Rethrows exception after closing.
    private void fail(RuntimeException ex) throws IOException {
        try { close(); } catch (Throwable th) {
            if (System.getProperty("klipspringer.debug") != null) { th.printStackTrace(); }
        }
        throw ex;
    }

    // Rethrows exception after closing.
    private void fail(IOException ex) throws IOException {
        try { close(); } catch (Throwable th) {
            if (System.getProperty("klipspringer.debug") != null) { th.printStackTrace(); }
        }
        throw ex;
    }

    public synchronized void decodeMetadata() throws IOException {
        try {
            if (state != State.INITIALIZED) {
                throw new IllegalStateException("Attempted decodeMetadata in state "+state);
            }
            format = PcmFormat.of(stream.getFormat());
            target.metadata(format, stream.getFrameLength());
            state = State.METADATA_DECODED;
        } catch (IOException ex) { fail(ex); } catch (RuntimeException ex) { fail(ex); }
    }

    public synchronized boolean decodeAll(long fromSampleNo) throws IOException {
        try (this) {
            if (state == State.INITIALIZED) {
                decodeMetadata();
            }
            if (state != State.METADATA_DECODED) {
                throw new IllegalStateException("Attempted decodeAll in state "+state);
            }
            if (fromSampleNo != 0) { stream.skip(fromSampleNo * format.getFrameSize()); }
            byte[] b = new byte[1024 * format.fs()];
            ByteBuffer bb = ByteBuffer.wrap(b);
            bb.order(format.order());
            while (true) {
                int n = stream.read(b);
                if (n < 0) { state = State.AUDIO_DECODED; return true; }
                bb.position(0);
                bb.limit(n);
                if (!target.write(bb)) { state = State.AUDIO_DECODED; return false; }
            }
        } catch (IOException ex) { fail(ex); } catch (RuntimeException ex) { fail(ex); }
        return false;           // unreachable
    }
}
