// 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.nio.ByteOrder;
import java.nio.channels.*;
import java.io.IOException;
import javax.sound.sampled.*;

/** Channel for writing PCM data, with quality-compatible conversion.  */
public class PcmChannelEncoder implements PcmWriter {
    private final WritableByteChannel channel;
    private final ByteBuffer buf;
    private final PcmBuffer fbuf;
    private final PcmFormat inFormat, outFormat;

    /** General constructor. Note that there are factory methods {@link ofWav(PcmFormat,
      * WritableByteChannel, long)} and {@link ofWav(PcmFormat, WritableByteChannel, long)}
      */
    public PcmChannelEncoder(PcmFormat inFormat, WritableByteChannel channel, PcmFormat outFormat, Device.BitLayout outLout) {
        if (!inFormat.matchesQuality(outFormat)) {
            throw new IllegalArgumentException("Format "+inFormat+" not compaticle with "+outFormat);
        }
        this.channel = channel;
        this.inFormat = inFormat;
        this.outFormat = outFormat;
        buf = ByteBuffer.allocateDirect(2048*outFormat.fs());
        fbuf = PcmBuffer.of(buf, outFormat, outLout);
    }

    public boolean write(ByteBuffer data) throws IOException {
        int ifs = inFormat.fs(), ofs = outFormat.fs();
        int p = data.position(), q = data.limit();
        while (p < q) {
            int m = Math.min((q-p)/ifs, buf.capacity()/ofs);    // max samples for this iteration
            data.limit(p + m*ifs);
            buf.clear();
            fbuf.put(data, inFormat);
            buf.flip();
            int w = channel.write(buf)/ofs;                     // samples written
            p += w*ifs;
            if (w < m) {                                        // channel took less than requested
                data.position(p);                               // back to show what was output
                data.limit(q);                                  // reset to original
                return false;
            }
        }
        return true;
    }

    public void close() throws IOException { channel.close(); }

    /** Factory method that gets an encoder for producing raw PCM format output. */
    public static PcmChannelEncoder ofRaw(PcmFormat inFormat, WritableByteChannel channel, PcmFormat outFormat) {
        return new PcmChannelEncoder(inFormat, channel, outFormat, Device.BitLayout.LSB);
    }

    /** Factory method that gets an encoder for producing PCM format output with a WAV header. */
    public static PcmChannelEncoder ofWav(PcmFormat inFormat, WritableByteChannel channel, long totalFrames) throws IOException {
        PcmFormat outFormat =
            new PcmFormat(inFormat.rate(), inFormat.bips(), inFormat.channels(),
                          inFormat.bips() > 8,                          // 8-bit is unsigned
                          (inFormat.bips()+7)/8*inFormat.channels(),    // no byte padding
                          ByteOrder.LITTLE_ENDIAN,
                          inFormat.significantBips());
        ByteBuffer hb = ByteBuffer.allocate(44);
        hb.order(ByteOrder.LITTLE_ENDIAN);
        PcmChannelEncoder pchan;
        if (totalFrames == AudioSystem.NOT_SPECIFIED) {
            // This means that the file size is not known until close, and the channel must be
            // seekable to get back and fill in the size.
            if (!(channel instanceof SeekableByteChannel)) {
                throw new IllegalArgumentException("Size not predetermined and output channel not seekable");
            }
            totalFrames = 0;    // write zero in header for now
            pchan = new PcmChannelEncoder(inFormat, channel, outFormat, Device.BitLayout.MSB) {
                    public void close() throws IOException {
                        if (channel.isOpen()) {
                            SeekableByteChannel seekable = (SeekableByteChannel) channel;
                            long size = seekable.size()-44;
                            if (size+36 > 0xffffffffL) {
                                System.err.println("Output exceedes maximum WAV size, header will indicate shorter than actual");
                                hb.putInt(4, 0xffffffff);
                                hb.putInt(40, 0xffffffff);
                            } else {
                                hb.putInt(4, (int) (size+36));
                                hb.putInt(40, (int) size);
                            }
                            seekable.position(0);
                            hb.position(0);
                            hb.limit(44);
                            while (hb.hasRemaining()) { channel.write(hb); }
                            channel.close();
                        }
                    }
                };
        } else {
            pchan = new PcmChannelEncoder(inFormat, channel, outFormat, Device.BitLayout.MSB);
        }
        long size = totalFrames*outFormat.fs();
        if (size+36 > 0xffffffffL) { throw new IOException("Size too large for WAV: "+size); }
        int bips = outFormat.significantBips();
        if (bips+7 < outFormat.bips() || bips <= 8 && outFormat.bips() > 8) {
            // In these strange cases (not likely to ever arise), we need to lie about the
            // precision, because the encoding is different for the scaled-up bit depth.
            bips = outFormat.bips();
        }
        hb.putInt(0x46464952);                                          // 0:  "RIFF"
        hb.putInt((int) size + 36);                                     // 4
        hb.putLong(0x20746d6645564157L);                                // 8:  "WAVEfmt "
        hb.putInt(16);                                                  // 16: PCM
        hb.putShort((short) 1);                                         // 20: PCM = 1 (lin. quant.)
        hb.putShort((short) outFormat.channels());                      // 22
        hb.putInt(outFormat.rate());                                    // 24
        hb.putInt(outFormat.rate() * outFormat.fs());                   // 28
        hb.putShort((short) outFormat.fs());                            // 32
        hb.putShort((short) bips);                                      // 34
        hb.putInt(0x61746164);                                          // 36: "data"
        hb.putInt((int) size);                                          // 40
        hb.flip();
        while (hb.hasRemaining()) { pchan.channel.write(hb); }
        return pchan;
    }
}
