// 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.split;

import java.io.IOException;
import java.nio.ByteBuffer;
import javax.sound.sampled.AudioSystem;
import net.avadeaux.klipspringer.*;
import net.avadeaux.klipspringer.codec.*;

public class SplitPlayer implements Device.Player {
    private final TrackSplitter.Segment[] segs;
    private final TrackSplitter.Hop[] hops;
    private final PcmFormat dformat;                            // format of received data
    private final long endFrame;                                // stop playing at this point
    private final double flushSilenceSecs;                      // silence to add after endFrame
    protected final Device.Player out;
    protected final int ofs;                                    // bytes/frame in out
    protected final int dfs;                                    // bytes/frame in received data
    private final ByteBuffer zbuf;                              // buffer of zeros for silent parts
    private final ByteBuffer obuf;                              // intermediate buffer
    private final PcmBuffer fbuf;                               // wraps obuf
    private boolean fading;                                     // fading/silence on
    protected long frame;                                       // current position

    private SplitPlayer(TrackSplitter.Segment[] segs,
                        TrackSplitter.Hop[] hops,
                        PcmFormat dformat,
                        Device.Player out,
                        Device.BitLayout lout,
                        long startFrame,
                        long endFrame,
                        boolean fading,
                        double flushSilenceSecs)
    {
        this.segs = segs;
        this.hops = hops;
        this.dformat = dformat;
        this.endFrame = endFrame;
        this.out = out;
        ofs = out.format().fs();
        dfs = dformat.fs();
        zbuf = ByteBuffer.allocateDirect(1024*ofs);
        zbuf.order(out.format().order());
        obuf = ByteBuffer.allocateDirect(1024*ofs);
        obuf.order(out.format().order());
        fbuf = PcmBuffer.of(obuf, out.format(), lout);
        frame = startFrame;
        this.fading = fading;
        this.flushSilenceSecs = flushSilenceSecs;
    }

    public PcmFormat format() { return dformat; }               // format visible to write caller

    // Copies m samples verbatim from data.
    private boolean copy(ByteBuffer data, int m) throws IOException {
        int q = data.limit();
        boolean ok = true;
        while (ok && m > 0) {
            int p = data.position();
            obuf.clear();
            data.limit(p+Math.min(m*dfs,                        // at most m samples
                                  obuf.capacity()/ofs*dfs));    // do not overfill obuf
            fbuf.put(data, dformat);
            obuf.flip();
            ok = out.write(obuf);
            int wf = obuf.position()/ofs;                       // written frames
            data.position(p + wf*dfs);
            m -= wf;
            frame += wf;
        }
        data.limit(q);
        return ok;
    }

    public synchronized boolean write(ByteBuffer data) throws IOException {
        // Stop at endFrame.
        int q = data.limit();
        if (endFrame != AudioSystem.NOT_SPECIFIED && frame + data.remaining()/dfs >= endFrame) {
            data.limit(data.position() + (int) (endFrame-frame)*dfs);
        }

        int hopi = 0;
        while (hopi < hops.length && hops[hopi].pos()+hops[hopi].length() < frame) { hopi++; }

        boolean ok = true;                                      // tentative return value

        while (ok) {
            int m = data.remaining()/dfs;                       // frames to write in this chunk
            if (hopi < hops.length) {
                TrackSplitter.Hop h = hops[hopi];
                if (h.pos() > frame) {                          // hop is later
                    m = (int) Math.min(m, h.pos() - frame);     // stop at h.pos()
                } else {                                        // hop now
                    int d = (int) Math.min(h.length() - (frame-h.pos()), m);
                    frame += d;
                    m -= d;
                    data.position(data.position() + d*dfs);
                    hopi++;
                }
            }
            if (m == 0) { break; }                              // write done

            if (!fading) {                                      // fading is off, copy verbatim
                ok = copy(data, m);
            } else {
                TrackSplitter.Segment s = TrackSplitter.segment(segs, frame);
                int sub = s.sub(frame);
                if (sub < 4) {                                  // still among segments
                    m = (int) Math.min(m, s.hi(sub)-frame);     // stop at end of subsegment
                }
                switch (sub) {
                case 1:                                         // mid track, play verbatim
                    ok = copy(data, m);
                    break;
                case 0:                                         // fade in
                case 2: {                                       // fade out
                    obuf.clear();
                    m = Math.min(m, obuf.capacity()/ofs);       // do not overfill obuf
                    PcmBuffer fdata = PcmBuffer.of(data, dformat, Device.BitLayout.LSB);
                    int p = data.position();
                    double dt = s.dt(sub);
                    double t = (frame - s.lo(sub))*dt;
                    for (int i = 0; i < m; i++, t += dt) {
                        for (int j = 0; j < dformat.channels(); j++) {
                            fbuf.putSample(s.fade(sub, t, fdata.getSample()));
                        }
                    }
                    obuf.flip();
                    ok = out.write(obuf);
                    int wf = m - obuf.remaining()/ofs;          // written frames
                    frame += wf;
                    data.position(p + wf*dfs);
                    break;
                }
                default: {                                      // silence
                    zbuf.clear();
                    if (m*ofs < zbuf.capacity()) {
                        zbuf.limit(m*ofs);                      // only write m samples
                    } else {
                        m = zbuf.capacity()/ofs;                // connot write more than zbuf
                    }
                    ok = out.write(zbuf);
                    int wf = zbuf.position()/ofs;               // written frames
                    frame += wf;
                    data.position(data.position() + wf*dfs);
                    break;
                } }
            }
        }
        if (frame == endFrame && flushSilenceSecs > 0) {        // add silence to flush out sound
            int z = (int) Math.round(dformat.rate()*ofs*flushSilenceSecs);
            while (z > 0) {
                zbuf.clear();
                zbuf.limit(Math.min(z, zbuf.capacity()));
                out.write(zbuf);
                z -= zbuf.position();
            }
            ok = false;
        }
        data.limit(q);
        return ok;
    }

    synchronized long currentFrame() throws IOException {
        return frame - out.bufferedFrames();
    }

    synchronized void fading(boolean on) { fading = on; }

    public int bufferedFrames() throws IOException { return out.bufferedFrames(); }
    public void drain() throws IOException { out.drain(); }
    public void close() throws IOException { out.close(); }


    // Optimized for equal input and output format.
    private static class VerbatimPlayer extends SplitPlayer {
        VerbatimPlayer(TrackSplitter.Segment[] segs, TrackSplitter.Hop[] hops, Device.Player out, long startFrame, long endFrame, boolean fading, double flushSilenceSecs) {
            super(segs, hops, out.format(), out, Device.BitLayout.LSB, startFrame, endFrame, fading, flushSilenceSecs);
        }

        private boolean copy(ByteBuffer data, int m) throws IOException {
            int p = data.position(), q = data.limit();
            data.limit(p + m*dfs);                              // limit to m samples
            boolean ok = out.write(data);
            frame += (data.position()-p)/dfs;                   // written frames
            data.limit(q);
            return ok;
        }
    }

    public static SplitPlayer create(TrackSplitter.Segment[] segs,
                                     TrackSplitter.Hop[] hops,
                                     PcmFormat dformat,
                                     Device.Player out,
                                     Device.BitLayout lout,
                                     long startFrame,
                                     long endFrame,
                                     boolean fading,
                                     double flushSilenceSecs)
    {
        return dformat.matches(out.format()) && (lout == Device.BitLayout.LSB || dformat.bips() == 8*dformat.ss())
            ? new VerbatimPlayer(segs, hops, out, startFrame, endFrame, fading, flushSilenceSecs)
            : new SplitPlayer(segs, hops, dformat, out, lout, startFrame, endFrame, fading, flushSilenceSecs);
    }
}

