/* ***** BEGIN LICENSE BLOCK *****
	Copyright (c) 2018-2020 Famibee (famibee.blog38.fc2.com)

	This software is released under the MIT License.
	http://opensource.org/licenses/mit-license.php
** ***** END LICENSE BLOCK ***** */

import {CmnLib, IEvtMng} from './CmnLib';
import {CmnTween} from './CmnTween';
import {IHTag, IVariable, IMain, HArg} from './CmnInterface';
import {Config} from './Config';
import {SysBase} from './SysBase';

const PSnd = require('pixi-sound').default;
import {Loader, LoaderResource} from 'pixi.js';
const Tween = require('@tweenjs/tween.js').default;

interface ISndBuf {
	snd		: any;
	loop	: boolean;
	ret_ms	: number;
	end_ms	: number;
	resume	: boolean;
	playing	: ()=> boolean;
	onend	: ()=> void;

	twFade?			: TWEEN.Tween;
	resumeFade?		: boolean;
	onCompleteFade?	: ()=> {};
};

export class SoundMng {
	private hSndBuf	: {[name: string]: ISndBuf} = {};

	constructor(private readonly cfg: Config, hTag: IHTag, private readonly val: IVariable, private readonly main: IMain, private readonly sys: SysBase) {
		hTag.volume		= o=> this.volume(o);		// 音量設定（独自拡張）
		hTag.fadebgm	= o=> this.fadebgm(o);		// BGMのフェード
		hTag.fadeoutbgm	= o=> this.fadeoutbgm(o);	// BGMのフェードアウト
		hTag.fadeoutse	= o=> this.fadeoutse(o);	// 効果音のフェードアウト
		hTag.fadese		= o=> this.fadese(o);		// 効果音のフェード
		hTag.playbgm	= o=> this.playbgm(o);		// BGM の演奏
		hTag.playse		= o=> this.playse(o);		// 効果音の再生
		hTag.stop_allse	= ()=> this.stop_allse();	// 全効果音再生の停止
		hTag.stopbgm	= o=> this.stopbgm(o);		// BGM 演奏の停止
		hTag.stopse		= o=> this.stopse(o);		// 効果音再生の停止
		hTag.wb			= o=> this.wb(o);			// BGM フェードの終了待ち
		hTag.wf			= o=> this.wf(o);			// 効果音フェードの終了待ち
		hTag.stopfadese	= o=> this.stopfadese(o);	// 音声フェードの停止
		hTag.wl			= o=> this.wl(o);			// BGM 再生の終了待ち
		hTag.ws			= o=> this.ws(o);			// 効果音再生の終了待ち
		hTag.xchgbuf	= o=> this.xchgbuf(o);		// 再生トラックの交換

		val.defValTrg('sys:sn.sound.global_volume', (_name: string, val: any)=> PSnd.sound.volumeAll = Number(val));
		this.val.setVal_Nochk('save', 'const.sn.loopPlaying', '{}');

		val.setVal_Nochk('tmp', 'const.sn.sound.codecs', JSON.stringify(PSnd.utils.supported));
	}

	private evtMng	: IEvtMng;
	setEvtMng(evtMng: IEvtMng) {this.evtMng = evtMng;}

	// 音量設定（独自拡張）
	private volume(hArg: HArg) {
		const buf = hArg.buf ?? 'SE';
		const bvn = 'const.sn.sound.'+ buf +'.volume';
		const arg_vol = this.getVol(hArg, 1);
		if (Number(this.val.getVal('sys:'+ bvn)) == arg_vol) return false;

		this.val.setVal_Nochk('sys', bvn, arg_vol)	// 基準音量（sys:）
		this.val.flush();	// fadese()内で必ずしも呼ばれないので

		// 再生中音声の一時的音量も変更
		hArg.time = 0;
		hArg.volume = Number(this.val.getVal('save:'+ bvn));	// 目標音量（save:）
		return this.fadese(hArg);
	}
	private getVol(hArg: HArg, def: number) {
		const vol = CmnLib.argChk_Num(hArg, 'volume', def);
		if (vol < 0) return 0;
		if (vol > 1) return 1;
		return vol;
	}

	// BGM/効果音のフェードアウト（loadから使うのでマクロ化禁止）
	private fadeoutbgm(hArg: HArg) {hArg.volume = 0; return this.fadebgm(hArg);}
	// 効果音のフェードアウト（loadから使うのでマクロ化禁止）
	private fadeoutse(hArg: HArg) {hArg.volume = 0; return this.fadese(hArg);}
	// BGMのフェード（loadから使うのでマクロ化禁止）
	private fadebgm(hArg: HArg) {hArg.buf = 'BGM'; return this.fadese(hArg);}
	// 効果音のフェード
	private fadese(hArg: HArg) {
		this.stopfadese(hArg);

		const buf = hArg.buf ?? 'SE';
		const oSb = this.hSndBuf[buf];
		if (! oSb || ! oSb.playing()) return false;

		const bvn = 'const.sn.sound.'+ buf +'.volume';
		const savevol = this.getVol(hArg, NaN);
		this.val.setVal_Nochk('save', bvn, savevol);	// 目標音量（save:）
		const vol = savevol * Number(this.val.getVal('sys:'+ bvn, 1))
		const stop = CmnLib.argChk_Boolean(hArg, 'stop', (hArg.volume == 0));
		if (stop) {
			this.delLoopPlay(buf);
			this.val.setVal_Nochk('save', 'const.sn.sound.'+ buf +'.fn', '');
				// 先行して
		}
		this.val.flush();

		if (CmnLib.argChk_Num(hArg, 'time', NaN) == 0) {
			oSb.snd.volume = vol;
			if (stop) {
				if (buf == 'BGM') this.stopbgm(hArg); else this.stopse(hArg);
			}
			return false;
		}

		const ease = CmnTween.ease(hArg.ease);
		const repeat = CmnLib.argChk_Num(hArg, 'repeat', 1);
		//console.log('fadese start from:%f to:%f', oSb.snd.volume, vol);
		oSb.twFade = new Tween.Tween({v: oSb.snd.volume})
			.to({v: vol}, CmnLib.argChk_Num(hArg, 'time', NaN))
			.delay(CmnLib.argChk_Num(hArg, 'delay', 0))
			.easing(ease)
			.repeat(repeat == 0 ?Infinity :(repeat -1))	// 一度リピート→計二回なので
			.yoyo(CmnLib.argChk_Boolean(hArg, 'yoyo', false))
			.onUpdate((o: any)=> {if (oSb.playing()) oSb.snd.volume = o.v;})
			.onComplete(()=> {	//console.log('fadese: onComplete');
				// [xchgbuf]をされるかもしれないので、外のoSb使用不可
				const oSb = this.hSndBuf[buf];
				if (! oSb || ! oSb.twFade) return;
				delete oSb.twFade;
				if (stop) {
					if (buf == 'BGM') this.stopbgm(hArg); else this.stopse(hArg);
				}
				if (oSb.resumeFade) {
					this.evtMng.popLocalEvts();	// [wf]したのにキャンセルされなかった時用
					this.main.resume();
				}
				if (oSb.onCompleteFade) oSb.onCompleteFade();
			});
		oSb.twFade!.start();

		return false;
	}

	// BGM の演奏
	private playbgm(hArg: HArg) {
		hArg.buf = 'BGM';
		hArg.canskip = false;
		CmnLib.argChk_Boolean(hArg, 'loop', true);
		return this.playse(hArg);
	}

	// 効果音の再生
	private playse(hArg: HArg) {
		const buf = hArg.buf ?? 'SE';
		this.stopse({buf: buf});
		const fn = hArg.fn;
		if (! fn) throw '[playse] fnは必須です(buf='+ buf +')';

		// isSkipKeyDown()は此処のみとする。タイミングによって変わる
		if (CmnLib.argChk_Boolean(hArg, 'canskip', true)
			&& this.evtMng.isSkipKeyDown()) return false;

		const loop = CmnLib.argChk_Boolean(hArg, 'loop', false);
		this.addLoopPlay(buf, loop);

		// この辺で属性を増減したら、loadFromSaveObj()にも反映する
		const nm = 'const.sn.sound.'+ buf +'.';
		this.val.setVal_Nochk('save', nm +'fn', fn);
		const savevol = this.getVol(hArg, 1);
		this.val.setVal_Nochk('save', nm +'volume', savevol);	// 目標音量（save:）
		const vol = savevol * Number(this.val.getVal('sys:'+ nm +'volume', 1));
		const ret_ms = CmnLib.argChk_Num(hArg, 'ret_ms', 0);
		this.val.setVal_Nochk('save', nm +'ret_ms', ret_ms);
		const end_ms = CmnLib.argChk_Num(hArg, 'end_ms', 0);
		this.val.setVal_Nochk('save', nm +'end_ms', end_ms);
		this.val.flush();

		const o: any = {
			autoPlay: true,
		//	autoPlay: false,	// loaded が発生しない
			loop	: loop,			// (apiには載ってないけど、ちゃんと効いた)
			volume	: vol,
			speed	: CmnLib.argChk_Num(hArg, 'speed', 1),
			loaded	: (e: Error, snd: any)=> {
				if (e) {this.main.errScript(`Sound ロード失敗です fn:${fn} ${e}`, false); return;}

				const oSb = this.hSndBuf[buf];
				if (oSb) oSb.snd = snd;
			},
		};
		if (! loop) o.complete = ()=> {
			// [xchgbuf]をされるかもしれないので、外のoSb使用不可
			const oSb = this.hSndBuf[buf];
			if (oSb) {oSb.playing = ()=> false; oSb.onend();}
		};

		const snd = PSnd.find(fn);	// バッファ
		this.hSndBuf[buf] = {
			snd		: snd,
			loop	: loop,
			ret_ms	: ret_ms,		// TODO: ret_ms未作成
			end_ms	: end_ms,		// TODO: end_ms未作成
			resume	: false,
			playing	: ()=> true,	// [ws]的にはここでtrueが欲しい
			onend	: ()=> {
				// [xchgbuf]をされるかもしれないので、外のoSb使用不可
				const oSb = this.hSndBuf[buf];
				if (! oSb) return;
	//			if (CmnLib.isFirefox) oSb.playing = ()=> false;
				//delete this.hSndBuf[buf];
					// [xchgbuf]をされるかもしれないので、delete不可
					// 【2018/06/25】cache=falseならここでunload()？
				this.stopfadese(hArg);	// 止めた方が良いかなと
				if (oSb.resume) {
					this.evtMng.popLocalEvts();	// [ws]中にキャンセルされなかった時用
					this.main.resume();
				}
			},
		};
		if (snd) {snd.volume = vol; snd.play(o); return false;}
			// snd.volume = ...; がないと、音量が戻らない不具合

		const join = CmnLib.argChk_Boolean(hArg, 'join', true);
		if (join) {
			const old = o.loaded;
			o.loaded = (e: Error, snd: any)=> {this.main.resume(); old(e, snd)};
		}
		this.playseSub(fn, o);

		this.initVol();

		return join;
	}
	private playseSub(fn: string, o: any): void {
		const url = this.cfg.searchPath(fn, Config.EXT_SOUND);
	//	const url = 'http://localhost:8080/prj/audio/title.{ogg,mp3}';
		if (url.slice(-4) != '.bin') {o.url = url; PSnd.add(fn, o); return}

		(new Loader()).add(fn, url, {xhrType: 'arraybuffer'})
		.pre((res: LoaderResource, next: Function)=> res.load(()=> {
			this.sys.pre(res.extension, res.data)
			.then(r=> {res.data = r; next();})
			.catch(e=> this.main.errScript(`Sound ロード失敗です fn:${res.name} ${e}`, false));
		}))
		.load((_ldr, hRes)=> {o.source = hRes[fn]?.data; PSnd.add(fn, o);});
	}
	private initVol = ()=> {
		PSnd.sound.volumeAll =Number(this.val.getVal('sys:sn.sound.global_volume',1));
		this.initVol = ()=> {};
	};

	// 全効果音再生の停止
	private stop_allse() {
		for (const buf in this.hSndBuf) this.stopse({buf: buf});
		this.hSndBuf = {};
		return false;
	}
	// BGM 演奏の停止（loadから使うのでマクロ化禁止）
	private stopbgm(hArg: HArg) {hArg.buf = 'BGM'; return this.stopse(hArg);}
	// 効果音再生の停止
	private stopse(hArg: HArg) {
		const buf = hArg.buf ?? 'SE';
		this.stopfadese(hArg);
		this.delLoopPlay(buf);

		const oSb = this.hSndBuf[buf];
		if (oSb) oSb.snd.stop();

		return false;
	}

	// BGM フェードの終了待ち
	private wb(hArg: HArg) {hArg.buf = 'BGM'; return this.wf(hArg);}

	// 効果音フェードの終了待ち
	private wf(hArg: HArg) {
		const buf = hArg.buf ?? 'SE';
		const oSb = this.hSndBuf[buf];
		if (! oSb || ! oSb.twFade) return false;
		if (! oSb.playing()) return false;

		oSb.resumeFade = true;
		this.evtMng.stdWait(
			()=> {this.stopfadese(hArg)},
			CmnLib.argChk_Boolean(hArg, 'canskip', true)
		);
		return true;
	}

	// 音声フェードの停止
	private stopfadese(hArg: HArg) {
		const buf = hArg.buf ?? 'SE';
		const oSb = this.hSndBuf[buf];
		if (! oSb || ! oSb.twFade) return false;

		oSb.twFade.stop().end();	// stop()とend()は別

		return false;
	}

	// BGM 再生の終了待ち
	private wl(hArg: HArg) {hArg.buf = 'BGM'; return this.ws(hArg);}
	// 効果音再生の終了待ち
	private ws(hArg: HArg) {
		const buf = hArg.buf ?? 'SE';
		const oSb = this.hSndBuf[buf];
		if (! oSb || ! oSb.playing() || oSb.loop) return false;

		oSb.resume = true;
		this.evtMng.stdWait(
			()=> {
				this.stopse(hArg);
				// [xchgbuf]をされるかもしれないので、外のoSb使用不可
				const oSb = this.hSndBuf[buf];
				if (! oSb || ! oSb.playing() || oSb.loop) return;
				oSb.onend();
			},
			CmnLib.argChk_Boolean(hArg, 'canskip', false)
		);

		return true;
	}

	// 再生トラックの交換
	private xchgbuf(hArg: HArg) {	// TODO: xchgbuf()が未テスト
		const buf = hArg.buf ?? 'SE';
		const buf2 = hArg.buf2 ?? 'SE';
		[this.hSndBuf[buf], this.hSndBuf[buf2]] = [this.hSndBuf[buf2], this.hSndBuf[buf]];
		// const oSb = this.hSndBuf[buf];
		// this.hSndBuf[buf] = this.hSndBuf[buf2];
		// this.hSndBuf[buf2] = oSb;

		return false;
	}

	// レスポンス向上のため音声ファイルを先読み
	loadAheadSnd(hArg: HArg): void {
		[hArg.clickse, hArg.enterse, hArg.leavese].forEach(fn=> {
			if (! fn || PSnd.exists(fn)) return;

			this.playseSub(fn, {preload: true, autoPlay: false});
		});
	}

	// しおりの読込（BGM状態復元）
	playLoopFromSaveObj(): void {
		const loopPlaying = String(this.val.getVal('save:const.sn.loopPlaying', '{}'));
		this.val.flush();
		if (loopPlaying == '{}') {this.stop_allse(); return;}

		const aFnc: {(): void}[] = [];
		const hBuf = JSON.parse(loopPlaying);
		for (const buf in hBuf) {
			const nm = 'save:const.sn.sound.'+ buf +'.';
			const hArg = {
				fn		: String(this.val.getVal(nm +'fn')),
				buf		: buf,
				join	: false,
				loop	: true,
				volume	: Number(this.val.getVal(nm +'volume')),
				ret_ms	: Number(this.val.getVal(nm +'ret_ms')),
				end_ms	: Number(this.val.getVal(nm +'end_ms')),
			};
			aFnc.push(()=> {
				if (hArg.buf == 'BGM') this.playbgm(hArg);
				else this.playse(hArg);
			});
		}
		this.stop_allse();
		aFnc.forEach(f=> f());
	}
	private addLoopPlay(buf: string, is_loop: Boolean): void {
		if (! is_loop) {this.delLoopPlay(buf); return;}

		const hBuf = JSON.parse(String(this.val.getVal('save:const.sn.loopPlaying', '{}')));
		hBuf[buf] = 0;
		this.val.setVal_Nochk('save', 'const.sn.loopPlaying', JSON.stringify(hBuf));
		this.val.flush();
	}
	private delLoopPlay(buf: string): void {
		const hBuf = JSON.parse(String(this.val.getVal('save:const.sn.loopPlaying', '{}')));
		delete hBuf[buf];
		this.val.setVal_Nochk('save', 'const.sn.loopPlaying', JSON.stringify(hBuf));
		this.val.flush();
	}

}
