import { connect } from "../../core/context/ToneAudioNode";
import { Param } from "../../core/context/Param";
import { ToneAudioBuffer } from "../../core/context/ToneAudioBuffer";
import { GainFactor, Positive, Seconds, Time } from "../../core/type/Units";
import { defaultArg, optionsFromArguments } from "../../core/util/Defaults";
import { noOp } from "../../core/util/Interface";
import { isDefined } from "../../core/util/TypeCheck";
import { assert } from "../../core/util/Debug";
import { OneShotSource, OneShotSourceCurve, OneShotSourceOptions } from "../OneShotSource";
import { EQ, GTE, LT } from "../../core/util/Math";

export type ToneBufferSourceCurve = OneShotSourceCurve;

export interface ToneBufferSourceOptions extends OneShotSourceOptions {
	url: string | AudioBuffer | ToneAudioBuffer;
	curve: ToneBufferSourceCurve;
	playbackRate: Positive;
	fadeIn: Time;
	fadeOut: Time;
	loopStart: Time;
	loopEnd: Time;
	loop: boolean;
	onload: () => void;
	onerror: (error: Error) => void;
}

/**
 * Wrapper around the native BufferSourceNode.
 * @category Source
 */
export class ToneBufferSource extends OneShotSource<ToneBufferSourceOptions> {

	readonly name: string = "ToneBufferSource";

	/**
	 * The oscillator
	 */
	private _source = this.context.createBufferSource();
	protected _internalChannels = [this._source];

	/**
	 * The frequency of the oscillator
	 */
	readonly playbackRate: Param<"positive">;

	/**
	 * The private instance of the buffer object
	 */
	private _buffer: ToneAudioBuffer;

	/**
	 * indicators if the source has started/stopped
	 */
	private _sourceStarted = false;
	private _sourceStopped = false;

	/**
	 * @param url The buffer to play or url to load
	 * @param onload The callback to invoke when the buffer is done playing.
	 */
	constructor(url?: ToneAudioBuffer | AudioBuffer | string, onload?: () => void);
	constructor(options?: Partial<ToneBufferSourceOptions>);
	constructor() {

		super(optionsFromArguments(ToneBufferSource.getDefaults(), arguments, ["url", "onload"]));
		const options = optionsFromArguments(ToneBufferSource.getDefaults(), arguments, ["url", "onload"]);

		connect(this._source, this._gainNode);
		this._source.onended = () => this._stopSource();

		/**
		 * The playbackRate of the buffer
		 */
		this.playbackRate = new Param({
			context: this.context,
			param: this._source.playbackRate,
			units: "positive",
			value: options.playbackRate,
		});

		// set some values initially
		this.loop = options.loop;
		this.loopStart = options.loopStart;
		this.loopEnd = options.loopEnd;
		this._buffer = new ToneAudioBuffer(options.url, options.onload, options.onerror);

		this._internalChannels.push(this._source);
	}

	static getDefaults(): ToneBufferSourceOptions {
		return Object.assign(OneShotSource.getDefaults(), {
			url: new ToneAudioBuffer(),
			loop: false,
			loopEnd: 0,
			loopStart: 0,
			onload: noOp,
			onerror: noOp,
			playbackRate: 1,
		});
	}

	/**
	 * The fadeIn time of the amplitude envelope.
	 */
	get fadeIn(): Time {
		return this._fadeIn;
	}
	set fadeIn(t: Time) {
		this._fadeIn = t;
	}

	/**
	 * The fadeOut time of the amplitude envelope.
	 */
	get fadeOut(): Time {
		return this._fadeOut;
	}
	set fadeOut(t: Time) {
		this._fadeOut = t;
	}

	/**
	 * The curve applied to the fades, either "linear" or "exponential"
	 */
	get curve(): ToneBufferSourceCurve {
		return this._curve;
	}
	set curve(t) {
		this._curve = t;
	}

	/**
	 * Start the buffer
	 * @param  time When the player should start.
	 * @param  offset The offset from the beginning of the sample to start at.
	 * @param  duration How long the sample should play. If no duration is given, it will default to the full length of the sample (minus any offset)
	 * @param  gain  The gain to play the buffer back at.
	 */
	start(time?: Time, offset?: Time, duration?: Time, gain: GainFactor = 1): this {
		assert(this.buffer.loaded, "buffer is either not set or not loaded");
		const computedTime = this.toSeconds(time);

		// apply the gain envelope
		this._startGain(computedTime, gain);

		// if it's a loop the default offset is the loopstart point
		if (this.loop) {
			offset = defaultArg(offset, this.loopStart);
		} else {
			// otherwise the default offset is 0
			offset = defaultArg(offset, 0);
		}
		// make sure the offset is not less than 0
		let computedOffset = Math.max(this.toSeconds(offset), 0);

		// start the buffer source
		if (this.loop) {
			// modify the offset if it's greater than the loop time
			const loopEnd = this.toSeconds(this.loopEnd) || this.buffer.duration;
			const loopStart = this.toSeconds(this.loopStart);
			const loopDuration = loopEnd - loopStart;
			// move the offset back
			if (GTE(computedOffset, loopEnd)) {
				computedOffset = ((computedOffset - loopStart) % loopDuration) + loopStart;
			}
			// when the offset is very close to the duration, set it to 0
			if (EQ(computedOffset, this.buffer.duration)) {
				computedOffset = 0;
			}
		}
		
		// this.buffer.loaded would have return false if the AudioBuffer was undefined
		this._source.buffer = this.buffer.get() as AudioBuffer;
		this._source.loopEnd = this.toSeconds(this.loopEnd) || this.buffer.duration;
		if (LT(computedOffset, this.buffer.duration)) {
			this._sourceStarted = true;
			this._source.start(computedTime, computedOffset);
		}

		// if a duration is given, schedule a stop
		if (isDefined(duration)) {
			let computedDur = this.toSeconds(duration);
			// make sure it's never negative
			computedDur = Math.max(computedDur, 0);
			this.stop(computedTime + computedDur);
		}

		return this;
	}

	protected _stopSource(time?: Seconds): void {
		if (!this._sourceStopped && this._sourceStarted) {
			this._sourceStopped = true;
			this._source.stop(this.toSeconds(time));
			this._onended();
		}
	}

	/**
	 * If loop is true, the loop will start at this position.
	 */
	get loopStart(): Time {
		return this._source.loopStart;
	}
	set loopStart(loopStart: Time) {
		this._source.loopStart = this.toSeconds(loopStart);
	}

	/**
	 * If loop is true, the loop will end at this position.
	 */
	get loopEnd(): Time {
		return this._source.loopEnd;
	}
	set loopEnd(loopEnd: Time) {
		this._source.loopEnd = this.toSeconds(loopEnd);
	}

	/**
	 * The audio buffer belonging to the player.
	 */
	get buffer(): ToneAudioBuffer {
		return this._buffer;
	}
	set buffer(buffer: ToneAudioBuffer) {
		this._buffer.set(buffer);
	}

	/**
	 * If the buffer should loop once it's over.
	 */
	get loop(): boolean {
		return this._source.loop;
	}
	set loop(loop: boolean) {
		this._source.loop = loop;
		if (this._sourceStarted) {
			this.cancelStop();
		}
	}

	/**
	 * Clean up.
	 */
	dispose(): this {
		super.dispose();
		this._source.onended = null;
		this._source.disconnect();
		this._buffer.dispose();
		this.playbackRate.dispose();
		return this;
	}
}
