/*
 *  Copyright (c) 2024 - 2025 by Paul Hertz <ignotus@gmail.com>
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU Library General Public License as published
 *   by the Free Software Foundation; either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program 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 Library General Public License for more details.
 *
 *   You should have received a copy of the GNU Library General Public
 *   License along with this program; if not, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

package net.paulhertz.pixelaudio.granular;

import ddf.minim.AudioOutput;
import ddf.minim.analysis.WindowFunction;

import net.paulhertz.pixelaudio.sampler.ADSRParams;
import net.paulhertz.pixelaudio.schedule.AudioUtility;

/**
 * PAGranularInstrument
 *
 * High-level granular instrument wrapper.
 *
 * Mirrors PASamplerInstrument but uses:
 *   PAGranularSampler → PAGranularVoice → PASource.
 *
 * Provides:
 *   - Global pan
 *   - Global gain scaling
 *   - Default ADSR envelope
 *   - Looping option for granular paths
 *   - Thread-safe play() methods
 *
 * Does NOT manage MultiChannelBuffer. PASource encapsulates its data.
 * 
 * Now also exposes helpers to schedule playback at sample-accurate times
 * via the underlying PAGranularSampler.
 */
public class PAGranularInstrument {

    // ------------------------------------------------------------------------
    // Core components
    // ------------------------------------------------------------------------
    private final PAGranularSampler sampler;
    private final AudioOutput out;

    private ADSRParams defaultEnv;

    // Global modifiers
    private float globalPan  = 0f;   // -1..+1
    private float globalGain = 1f;   // overall amplitude multiplier (linear)

    private boolean isClosed = false;

    // ------------------------------------------------------------------------
    // Constructors
    // ------------------------------------------------------------------------
    public PAGranularInstrument(AudioOutput out,
                                ADSRParams defaultEnv,
                                int maxVoices) {
        this.out = out;
        this.sampler = new PAGranularSampler(out, maxVoices);
        this.defaultEnv = (defaultEnv != null)
                ? defaultEnv
                : new ADSRParams(1f, 0.01f, 0.2f, 0.8f, 0.3f);
    }

    public PAGranularInstrument(AudioOutput out) {
        this(out, new ADSRParams(1f, 0.01f, 0.2f, 0.8f, 0.3f), 16);
    }

    // ------------------------------------------------------------------------
    // Immediate Playback API
    // ------------------------------------------------------------------------

    /**
     * Main granular play() method. The PASource in the current Granular synthesis engine is 
     * expected to be a PABurstGranularSource passed in by PAGranularInstrumentDirector, the 
     * standard entry point for granular synthesis in the PixelAudio library. Earlier PASource
     * classes such as BasicIndexGranularSource, PathGranularSource, etc., are deprecated. 
     *
     * @param src       PASource (PABurstGranularSource)
     * @param amp       amplitude
     * @param pan       stereo pan (-1..+1)
     * @param env       envelope (or null → default)
     * @param looping   loop the grain path
     * @return voiceId or -1
     */
    public synchronized long play(PASource src,
                                  float amp,
                                  float pan,
                                  ADSRParams env,
                                  boolean looping)
    {
        if (src == null || sampler == null || isClosed) return -1;

        float finalGain = amp * globalGain;
        float finalPan = clampPan(globalPan + pan);

        ADSRParams useEnv = (env != null) ? env : defaultEnv;

        return sampler.play(src, useEnv, finalGain, finalPan, looping);
    }

    /** Convenience: uses default envelope, no looping. */
    public synchronized long play(PASource src, float amp, float pan) {
        return play(src, amp, pan, defaultEnv, false);
    }

    /** Convenience: default pan, default envelope. */
    public synchronized long play(PASource src, float amp) {
        return play(src, amp, 0f, defaultEnv, false);
    }

    /** Convenience: default env and global pan. */
    public synchronized long play(PASource src) {
        return play(src, 1f, globalPan, defaultEnv, false);
    }

    /** Convenience: looping version. */
    public synchronized long playLooping(PASource src, float amp, float pan) {
        return play(src, amp, pan, defaultEnv, true);
    }

    // ------------------------------------------------------------------------
    // Scheduled Playback API
    // ------------------------------------------------------------------------

    /**
     * Schedule playback of a PASource at an absolute sample time.
     *
     * @param src         PASource
     * @param amp         amplitude
     * @param pan         stereo pan
     * @param env         envelope (or null → default)
     * @param looping     loop flag
     * @param startSample absolute sample index at which to start
     */
    public synchronized void startAtSampleTime(PASource src,
    		float amp,
    		float pan,
    		ADSRParams env,
    		boolean looping,
    		long startSample) {
    	if (src == null || sampler == null || isClosed) return;

    	float finalGain = amp * globalGain;
    	float finalPan = clampPan(globalPan + pan);
    	ADSRParams useEnv = (env != null) ? env : defaultEnv;

    	sampler.startAtSampleTime(src, useEnv, finalGain, finalPan, looping, startSample);
    }
    
    /**
     * Schedule playback of a PASource at an absolute sample time.
     * Called by PAGranularInstrumentDirector
     *
     * @param src            PASource
     * @param amp            amplitude
     * @param pan            stereo pan
     * @param env            envelope (or null → default)
     * @param looping        loop flag
     * @param startSample    absolute sample index at which to start
     * @param grainWindow    a WindowFunction
     * @param grainLenSamples    number of samples in one grain
     */
    public synchronized void startAtSampleTime(PASource src,
            float amp,
            float pan,
            ADSRParams env,
            boolean looping,
            long startSample,
            WindowFunction grainWindow,
            int grainLenSamples) {

        if (src == null || sampler == null || isClosed) return;

        float finalGain = amp * globalGain;
        float finalPan  = clampPan(globalPan + pan);
        ADSRParams useEnv = (env != null) ? env : defaultEnv;

        // Delegate — sampler must carry these into the scheduled play / voice params.
        sampler.startAtSampleTime(
                src,
                useEnv,
                finalGain,
                finalPan,
                looping,
                startSample,
                grainWindow,
                Math.max(1, grainLenSamples)
        );
    }    

    /**
     * Schedule playback after a delay in samples relative to "now".
     *
     * @param src          PASource
     * @param amp          amplitude
     * @param pan          stereo pan
     * @param env          envelope (or null → default)
     * @param looping      loop flag
     * @param delaySamples how many samples from "now"
     */
    public synchronized void startAfterDelaySamples(PASource src,
    		float amp,
    		float pan,
    		ADSRParams env,
    		boolean looping,
    		long delaySamples) {
    	if (src == null || sampler == null || isClosed) return;

    	float finalGain = amp * globalGain;
    	float finalPan = clampPan(globalPan + pan);
    	ADSRParams useEnv = (env != null) ? env : defaultEnv;

    	sampler.startAfterDelaySamples(src, useEnv, finalGain, finalPan, looping, delaySamples);
    }

    /**
     * Convenience: schedule using current instrument cursor as "now".
     *
     * @param src         PASource
     * @param amp         amplitude
     * @param pan         pan
     * @param env         envelope (or null → default)
     * @param looping     loop flag
     */
    public synchronized void startNow(PASource src,
    		float amp,
    		float pan,
    		ADSRParams env,
    		boolean looping) {
    	startAfterDelaySamples(src, amp, pan, env, looping, 0L);
    }

    // ------------------------------------------------------------------------
    // Controls
    // ------------------------------------------------------------------------

    public void stopAll() {
        if (sampler != null) sampler.stopAll();
    }

    public void setDefaultEnvelope(ADSRParams env) {
        if (env != null) this.defaultEnv = env;
    }

    public ADSRParams getDefaultEnvelope() { return defaultEnv; }

    public void setGlobalPan(float pan) { this.globalPan = clampPan(pan); }

    public float getGlobalPan() { return globalPan; }

    public void setGlobalGain(float gLinear) { this.globalGain = gLinear; }

    public float getGlobalGain() { return globalGain; }
    
    public void setGlobalGainDb(float gDb) { this.globalGain = AudioUtility.dbToLinear(gDb); }

    public float getGlobalGainDb() { return AudioUtility.linearToDb(globalGain); }
    
    public float getSampleRate() { return this.out.sampleRate(); }

    public PAGranularSampler getSampler() { return sampler; }
    
    public long getSampleCursor() {
        return sampler.getCurrentSampleTime();
    }

    // ------------------------------------------------------------------------
    // Lifecycle
    // ------------------------------------------------------------------------

    public synchronized void close() {
        if (isClosed) return;
        isClosed = true;
        stopAll();
        // sampler is a UGen patched to out — optionally leave patch on
    }
    
    // fade or stop
    
    public void clearScheduled() {
        if (sampler != null) sampler.clearScheduled();
    }

    public void releaseAll() {
        if (sampler != null) sampler.releaseAll();
    }

    public void cancelAndStopAll() {
        if (sampler != null) sampler.cancelAndStopAll();
    }

    public void cancelAndReleaseAll() {
        if (sampler != null) sampler.cancelAndReleaseAll();
    }

    
    // ------------------------------------------------------------------------
    // Helpers
    // ------------------------------------------------------------------------
    private static float clampPan(float p) {
        if (p < -1f) return -1f;
        if (p > 1f) return 1f;
        return p;
    }
    
}
