// Copyright 2014-2025 Jesper Larsson
//
// This file is part of Klipspringer, <https://klipspringer.eavadeaux.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.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.WritableByteChannel;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.concurrent.Executor;
import javax.sound.sampled.*;

/** Audio stream that sends MP3 encoded data to a channel. Also doubles as file encoder class when
  * created via {@link #fileEncoder(String, PcmFormat, int, int) fileEncoder}.
  */
public class LameStream extends ExternalEncodedStream {
    static { Library.init(); }

    /** Selects format suitable for LAME streaming with the same semantics as {@link
      * PcmFormat.Selector#selectFormat(AudioFormat)}.
      */
    public static PcmFormat selectFormat(AudioFormat fmt) throws UnsupportedFormatException {
        int channels = fmt.getChannels();
        if (fmt.getChannels() > 2) {
            throw new UnsupportedFormatException("More than two channels not supported: "+fmt);
        }
        AudioFormat.Encoding enc = fmt.getEncoding();
        boolean signed = enc == AudioFormat.Encoding.PCM_SIGNED;
        int fs = fmt.getFrameSize();
        int bips = fmt.getSampleSizeInBits();
        int sbips = fmt instanceof PcmFormat ? ((PcmFormat) fmt).significantBips() : fmt.getSampleSizeInBits();
        int rfs = (bips+15)/16*2*channels;
        ByteOrder order = fmt.isBigEndian() ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN;
        ByteOrder rorder = ByteOrder.nativeOrder();
        if (signed && order == rorder && fs == rfs) { return PcmFormat.of(fmt); }
        if (!signed && enc != AudioFormat.Encoding.PCM_UNSIGNED) {
            throw new UnsupportedFormatException("Non-PCM format not supported:"+fmt);
        }
        return new PcmFormat((int) fmt.getSampleRate(), bips, channels, true, rfs, rorder, sbips);
    }

    /** The bit layout for PCM data sent to an MP3 stream. */
    public final static Device.BitLayout BIT_LAYOUT = Device.BitLayout.MSB;

    private final int bitrate;

    // Private constructor with file name parameter. The file name is non-null only when the object
    // is not to be used as a stream, but to encode a file.
    private LameStream(String fileName,
                      PcmFormat format,
                      int bitrate,
                      int quality,
                      int bufferFrames,
                      double writeaheadSecs,
                      double timeoutSecs,
                      WritableByteChannel channel,
                      Executor keepAlivePool,
                      double keepAliveTimeoutSecs)
        throws IOException
    {
        super(channel, format, bufferFrames, writeaheadSecs, timeoutSecs, 0, new EncoderRecordFactory() {
                public long create(Receiver receiver, PcmFormat format, int bufferFrames) throws IOException {
                    if (!Library.lameSupported()) {
                        throw new UnsupportedOperationException("Klipspringer library lacks LAME support");
                    }
                    if (format.channels() > 2) {
                        throw new IOException("At most two channels supported, requested "+format.channels());
                    }
                    byte[] fnamUtf8 = null;
                    if (fileName != null) {
                        fnamUtf8 = fileName.getBytes(StandardCharsets.UTF_8);
                        fnamUtf8 = Arrays.copyOf(fnamUtf8, fnamUtf8.length+1);
                    }
                    return LameStream.create(fnamUtf8, format.rate(), format.bips(), format.channels(), format.ss(), format.signed(), format.bigend(), bitrate, quality, bufferFrames, receiver);
                }
            }, keepAlivePool, keepAliveTimeoutSecs);
        this.bitrate = bitrate;
    }

    /** Creates a FLAC stream with the given specifications and destination channel.
      *
      * <p>If <code>keepAliveExec</code> is non-null, it is used by {@link #close()} to execute a wait
      * loop that terminates when either the recipient has sent callback notification of having
      * played (almost) all the data it has been sent, or no callback is made for
      * <code>keepAliveTimeoutSecs</code> seconds. (See also {@link #drain()}.)
      */
    public LameStream(PcmFormat format,
                      int bitrate,
                      int quality,
                      int bufferFrames,
                      double writeaheadSecs,
                      double timeoutSecs,
                      WritableByteChannel channel,
                      Executor keepAlivePool,
                      double keepAliveTimeoutSecs)
        throws IOException
    {
        this(null, format, bitrate, quality, bufferFrames, writeaheadSecs, timeoutSecs, channel, keepAlivePool, keepAliveTimeoutSecs);
    }

    public Device.BitLayout layout() { return BIT_LAYOUT; }

    /** Gets the bitrate specified for this stream. */
    public String quality() { return bitrate+"\u00a0kbit/s"; }

    /** Gets a player that encodes to a file rather than a stream. */
    public static PcmWriter fileEncoder(String fileName, PcmFormat dataFormat, int bitrate, int quality) throws IOException {
        PcmFormat bufFormat = selectFormat(dataFormat);
        return new LameStream(fileName, bufFormat, bitrate, quality, 2048, 0, 0, null, null, 0)
            .fileEncoder(dataFormat, BIT_LAYOUT);
    }

    // Construction method.
    private native static long create(byte[] fnam,      // null unless called from fileEncoder
                                      int rate,
                                      int bips,
                                      int channels,
                                      int ss,
                                      boolean signed,
                                      boolean bigend,
                                      int brate,
                                      int quality,
                                      int bufferFrames,
                                      Receiver receiver)
        throws IOException;

    // Native methods invoked by superclass.
    native void write(long handle, ByteBuffer data, int pos, int frames);
    native void finish(long handle) throws IOException;
    native void free(long handle);
}
