/**
 * PlaybackScheduler - Manages AudioBuffer storage and playback scheduling.
 *
 * Single Responsibility: Store decoded buffers and schedule them for playback.
 * Supports pause/resume/seek by retaining all buffers.
 */

import { AudioContextManager } from './AudioContextManager.js';

interface ScheduledSource {
    source: AudioBufferSourceNode;
    bufferIndex: number;
    startTime: number;
    endTime: number;
}

export class PlaybackScheduler {
    private contextManager: AudioContextManager;
    private buffers: AudioBuffer[] = [];
    private scheduledSources: ScheduledSource[] = [];

    // Playback timing
    private playbackAnchorTime: number = 0;      // AudioContext time when playback started/resumed
    private playbackAnchorPosition: number = 0;  // Position in audio when playback started/resumed
    private nextBufferIndex: number = 0;         // Next buffer to schedule during live streaming
    private nextScheduleTime: number = 0;        // AudioContext time for next buffer
    private isActive_: boolean = false;          // Prevents scheduling during pause/stop

    // Offset for seek-beyond-buffer scenarios
    // When seeking to position T beyond buffers, we clear buffers and set playbackOffset = T
    // The new stream starts at T, so buffer positions are relative to T
    private playbackOffset: number = 0;

    // Callbacks
    public onPlaybackEnded: (() => void) | null = null;

    constructor(contextManager: AudioContextManager) {
        this.contextManager = contextManager;
    }

    /**
     * Add a decoded buffer to storage
     */
    addBuffer(buffer: AudioBuffer): void {
        this.buffers.push(buffer);
    }

    /**
     * Get total duration of all stored buffers
     */
    getTotalDuration(): number {
        return this.buffers.reduce((sum, b) => sum + b.duration, 0);
    }

    /**
     * Get number of stored buffers
     */
    getBufferCount(): number {
        return this.buffers.length;
    }

    /**
     * Get current playback position in seconds (includes playbackOffset for seek-beyond-buffer)
     */
    getCurrentPosition(): number {
        // Use isActive_ as the sentinel for "playback is running", not playbackAnchorTime == 0.
        // AudioContext.currentTime can legitimately be 0 at context creation, so comparing
        // against 0 would incorrectly treat an active stream started at t=0 as paused.
        if (!this.isActive_) {
            return this.playbackAnchorPosition + this.playbackOffset;
        }
        const elapsed = this.contextManager.currentTime - this.playbackAnchorTime;
        return Math.min(this.playbackAnchorPosition + this.playbackOffset + elapsed, this.getTotalDuration() + this.playbackOffset);
    }

    /**
     * Set the playback offset for seek-beyond-buffer scenarios
     * This represents the absolute time position where the current buffers start
     */
    setPlaybackOffset(offset: number): void {
        this.playbackOffset = offset;
    }

    /**
     * Get the current playback offset
     */
    getPlaybackOffset(): number {
        return this.playbackOffset;
    }

    /**
     * Start or resume playback from a specific position
     */
    playFromPosition(position: number): void {
        this.stopAllSources();

        // Find which buffer contains this position
        let accumulatedTime = 0;
        let startBufferIndex = 0;
        let offsetInBuffer = 0;

        for (let i = 0; i < this.buffers.length; i++) {
            const bufferDuration = this.buffers[i].duration;
            if (accumulatedTime + bufferDuration > position) {
                startBufferIndex = i;
                offsetInBuffer = position - accumulatedTime;
                break;
            }
            accumulatedTime += bufferDuration;
            startBufferIndex = i + 1;
        }

        if (startBufferIndex >= this.buffers.length) {
            // Position landed at or past the end of all buffers. Previously this
            // returned silently, leaving the player stuck "playing" with no source
            // scheduled — a pause near the end followed by play never recovered.
            // Treat this as end-of-track so listeners (UI / end callback) fire.
            this.isActive_ = false;
            this.playbackAnchorTime = 0;
            this.playbackAnchorPosition = 0;
            this.onPlaybackEnded?.();
            return;
        }

        // Set timing anchors
        this.playbackAnchorPosition = position;
        this.playbackAnchorTime = this.contextManager.currentTime;
        this.nextScheduleTime = this.contextManager.currentTime + 0.01; // Small lookahead
        this.nextBufferIndex = startBufferIndex;
        this.isActive_ = true;  // Enable scheduling

        // Schedule buffers
        this.scheduleBuffersFrom(startBufferIndex, offsetInBuffer);
    }

    /**
     * Schedule newly decoded buffers during live streaming
     */
    scheduleNewBuffers(): void {
        if (this.nextBufferIndex >= this.buffers.length) {
            return; // No new buffers
        }

        // Use isActive_ as the sentinel for "playback is running", not nextScheduleTime === 0.
        // AudioContext.currentTime can legitimately be 0 at context creation, which would cause
        // nextScheduleTime === 0 to incorrectly reset a value already set by playFromPosition.
        if (!this.isActive_) {
            return;
        }

        this.scheduleBuffersFrom(this.nextBufferIndex, 0);
    }

    /**
     * Internal: Schedule buffers starting from a specific index
     */
    private scheduleBuffersFrom(startIndex: number, offsetInFirstBuffer: number): void {
        const lookaheadTarget = 0.5; // Schedule up to 500ms ahead
        const gainNode = this.contextManager.getGainNode();

        for (let i = startIndex; i < this.buffers.length; i++) {
            const buffer = this.buffers[i];
            const isFirstBuffer = (i === startIndex && offsetInFirstBuffer > 0);
            const offset = isFirstBuffer ? offsetInFirstBuffer : 0;
            const duration = buffer.duration - offset;

            // Create and configure source
            const source = this.contextManager.getContext().createBufferSource();
            source.buffer = buffer;
            source.connect(gainNode);

            const scheduleTime = this.nextScheduleTime;
            const endTime = scheduleTime + duration;

            // Track scheduled source
            const scheduled: ScheduledSource = {
                source,
                bufferIndex: i,
                startTime: scheduleTime,
                endTime
            };
            this.scheduledSources.push(scheduled);

            // Set up ended callback
            source.onended = () => this.handleSourceEnded(scheduled);

            // Schedule the source
            source.start(scheduleTime, offset);

            // Update for next buffer
            this.nextScheduleTime = endTime;
            this.nextBufferIndex = i + 1;

            // Check if we have enough lookahead
            const lookahead = this.nextScheduleTime - this.contextManager.currentTime;
            if (lookahead > lookaheadTarget) {
                break;
            }
        }
    }

    /**
     * Handle a source finishing playback
     */
    private handleSourceEnded(scheduled: ScheduledSource): void {
        // Ignore if we're paused/stopped (sources fire onended when stopped)
        if (!this.isActive_) {
            return;
        }

        // Remove from scheduled list
        const index = this.scheduledSources.indexOf(scheduled);
        if (index > -1) {
            this.scheduledSources.splice(index, 1);
        }

        // Schedule more buffers if available
        if (this.nextBufferIndex < this.buffers.length) {
            this.scheduleBuffersFrom(this.nextBufferIndex, 0);
        }

        // Check if all playback has finished
        if (this.scheduledSources.length === 0 && this.nextBufferIndex >= this.buffers.length) {
            this.isActive_ = false;
            this.playbackAnchorTime = 0;
            this.playbackAnchorPosition = 0;
            this.onPlaybackEnded?.();
        }
    }

    /**
     * Pause playback - saves position and stops sources
     */
    pause(): number {
        const position = this.getCurrentPosition();
        this.isActive_ = false;  // Prevent handleSourceEnded from scheduling more
        this.stopAllSources();
        // getCurrentPosition() returns absolute time (anchor + playbackOffset); the anchor
        // is buffer-relative, so strip the offset back out before storing it.
        this.playbackAnchorPosition = position - this.playbackOffset;
        this.playbackAnchorTime = 0;
        this.nextScheduleTime = 0;
        return position;
    }

    /**
     * Stop all scheduled sources
     */
    stopAllSources(): void {
        for (const scheduled of this.scheduledSources) {
            try {
                scheduled.source.stop();
            } catch {
                // Source may already be stopped
            }
        }
        this.scheduledSources = [];
    }

    /**
     * Reset to beginning (for stop)
     */
    resetToStart(): void {
        this.isActive_ = false;
        this.stopAllSources();
        this.playbackAnchorPosition = 0;
        this.playbackAnchorTime = 0;
        this.nextBufferIndex = 0;
        this.nextScheduleTime = 0;
    }

    /**
     * Full reset - clears all buffers and resets offset
     */
    clear(): void {
        this.isActive_ = false;
        this.stopAllSources();
        this.buffers = [];
        this.playbackAnchorPosition = 0;
        this.playbackAnchorTime = 0;
        this.nextBufferIndex = 0;
        this.nextScheduleTime = 0;
        this.playbackOffset = 0;
    }

    /**
     * Clear buffers but keep offset - for seek-beyond-buffer scenarios
     */
    clearForSeek(): void {
        this.isActive_ = false;
        this.stopAllSources();
        this.buffers = [];
        this.playbackAnchorPosition = 0;
        this.playbackAnchorTime = 0;
        this.nextBufferIndex = 0;
        this.nextScheduleTime = 0;
        // Note: playbackOffset is NOT reset - it will be set by the caller
    }

    /**
     * Check if we have buffers
     */
    hasBuffers(): boolean {
        return this.buffers.length > 0;
    }

    /**
     * Check if we have minimum buffers for playback
     */
    hasMinimumBuffers(minCount: number): boolean {
        return this.buffers.length >= minCount;
    }

    /**
     * Check if playback is active
     */
    isActive(): boolean {
        return this.isActive_;
    }
}
