// 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.nio.ByteBuffer;
import java.util.ArrayList;
import net.avadeaux.klipspringer.codec.*;

public class Detector implements PcmWriter {
    public class Track {
        public final long startFrame;
        public final long endFrame;

        public Track(long startFrame, long endFrame) {
            this.startFrame = startFrame;
            this.endFrame = endFrame;
        }
    }

    private final PcmFormat format;
    private final int atomFrames;
    private final long[] sweep;                 // accumulated atom sums, length is power of 2
    private final long silence;                 // max sum of silent atom
    private final int minTrackAtoms;
    private final int minGapAtoms;
    private final ArrayList<Track> tracks = new ArrayList<Track>();
    private final long startFrame, endFrame;

    private long frame;                         // index of current frame
    private long trackStart;                    // -1 when scanning gap
    private long atomSum = 0;
    private long sweepSum = 0;
    private int xatoms = 0;                     // number of silent in track, or non-silent in gap

    public Detector(PcmFormat format,
                    int atomFrames,
                    double silenceLevel,
                    double minTrackTime,
                    double minGapTime,
                    long startFrame,
                    long endFrame)
    {
        this.format = format;
        this.atomFrames = atomFrames;
        sweep = new long[(int) Math.ceil(format.rate()/atomFrames*0.1)];
        silence = sweep.length * (long) (atomFrames*format.channels()*(((1 << format.bips()-1)-1)*silenceLevel));
        minTrackAtoms = (int) (minTrackTime*format.rate()/atomFrames);
        minGapAtoms = (int) (minGapTime*format.rate()/atomFrames);
        this.startFrame = frame = startFrame;
        this.endFrame = endFrame;
        trackStart = frame == 0
            ? -1                                // start in silence if scanning from start of input
            : frame;                            // otherwise not
    }

    public boolean write(ByteBuffer data) {
        PcmBuffer pb = PcmBuffer.of(data, format, Device.BitLayout.LSB);
        while (true) {                          // loop over atoms
            long endAtom = frame - frame%atomFrames + atomFrames;
            while (frame < endAtom) {
                if (frame == endFrame) { return false; }
                if (!data.hasRemaining()) { return true; }
                for (int i = 0; i < format.channels(); i++) {
                    atomSum += Math.abs((long) pb.getSample());
                }
                frame++;
            }
            int i = ((int) frame/atomFrames) % sweep.length;
            sweepSum -= sweep[i];
            sweepSum += sweep[i] = atomSum;
            if (sweepSum > silence) {           // sweep is non-silent
                if (trackStart < 0) {           // not in track, maybe start one
                    if (++xatoms == minTrackAtoms) {
                        trackStart = Math.max(startFrame, frame - xatoms*atomFrames);
                        xatoms = 0;
                    }
                } else {                        // continue track
                    xatoms = 0;
                }
            } else {                            // atom is silent
                if (trackStart < 0) {           // not in track
                    xatoms = 0;
                } else {                        // maybe end track
                    if (++xatoms == minGapAtoms) {
                        tracks.add(new Track(trackStart, frame - xatoms*atomFrames));
                        trackStart = -1;
                        xatoms = 0;
                    }
                }
            }
            atomSum = 0;
        }
    }

    public void close() {
        if (trackStart >= 0) {
            tracks.add(new Track(trackStart, frame));
            trackStart = -1;
        }
    }

    public Track[] result() {
        close();
        return tracks.toArray(new Track[tracks.size()]);
    }
}
