/* ***** 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 {uint, argChk_Boolean, getFn} from './CmnLib';
import {IHTag, IMain, IVariable, IMark, HArg, Script, IPropParser} from './CmnInterface';
import {Config} from './Config';
import {CallStack, ICallStackArg} from './CallStack';
import {Grammar} from './Grammar';
import {AnalyzeTagArg} from './AnalyzeTagArg';

import {EventMng} from './EventMng';
import {Loader, LoaderResource} from 'pixi.js';
import {LayerMng} from './LayerMng';
import {DebugMng} from './DebugMng';
import {SoundMng} from './SoundMng';
import {SysBase} from './SysBase';

interface HScript {
	[name: string]: Script;
};

interface ISeek {
	idx		: number;
	lineNum	: number;
};

enum BreakState {running, wait, break, breaking, step, stepping, macro_escaping, macro_esc};

export class ScriptIterator {
	private script		: Script	= {aToken: [''], len: 1, aLNum: [1]};

	private scriptFn_	= '';
	get scriptFn(): string {return this.scriptFn_;};
	private idxToken_	= 0;
	subIdxToken(): void {--this.idxToken_;};
	private lineNum_	= 0;
	get lineNum(): number {return this.lineNum_;}
	readonly addLineNum	= (len: number)=> {this.lineNum_ += len;};


	private aCallStk	: CallStack[]	= [];
	get lenCallStk(): number {return this.aCallStk.length;};
	get lastHArg(): any {return this.aCallStk[this.lenCallStk -1].csArg;};
	readonly getCallStk = (idx: number)=> this.aCallStk[idx].csArg;

	private	grm			= new Grammar;


	constructor(private readonly cfg: Config, private readonly hTag: IHTag, private readonly main: IMain, private readonly val: IVariable, private readonly alzTagArg: AnalyzeTagArg, private readonly runAnalyze: ()=> void, private readonly prpPrs: IPropParser, private readonly sndMng: SoundMng, private readonly sys: SysBase) {
		//	変数操作
		hTag.let_ml		= o=> this.let_ml(o);	// インラインテキスト代入

		// デバッグ・その他
		hTag.dump_stack	= ()=> this.dump_stack();	// スタックのダンプ
		hTag.dump_script= o=> this.dump_script(o);	// スクリプトのダンプ

		// 条件分岐
		hTag['else']	=							// その他ifブロック開始
		hTag.elsif		=							// 別条件のifブロック開始
		hTag.endif		= ()=> this.endif();		// ifブロックの終端
		hTag['if']		= o=> this.if(o);			// ifブロックの開始

		// ラベル・ジャンプ
		//hTag.button	// LayerMng.ts内で定義		// ボタンを表示
		hTag.call		= o=> this.call(o);			// サブルーチンコール
		hTag.jump		= o=> this.jump(o);			// シナリオジャンプ
		hTag.pop_stack	= o=> this.pop_stack(o);	// コールスタック破棄
		hTag.return		= ()=> this.return();		// サブルーチンから戻る

		// マクロ
		hTag.bracket2macro	= o=> this.bracket2macro(o);// 括弧マクロの定義
		hTag.char2macro		= o=> this.char2macro(o);	// 一文字マクロの定義
		hTag.endmacro		= ()=> this.return();		// マクロ定義の終了
		hTag.macro			= o=> this.macro(o);		// マクロ定義の開始

		// しおり
		//hTag.copybookmark		// Variable.ts内で定義	// しおりの複写
		//hTag.erasebookmark	// Variable.ts内で定義	// しおりの消去
		hTag.load			= o=> this.load(o);			// しおりの読込
		hTag.reload_script	= o=> this.reload_script(o);	// スクリプト再読込
		hTag.record_place	= ()=> this.record_place();	// セーブポイント指定
		hTag.save			= o=> this.save(o);			// しおりの保存


		if (cfg.oCfg.debug.token) this.dbgToken = token=> console.log(`🌱 トークン fn:${this.scriptFn_} idxToken:${this.idxToken_} ln:${this.lineNum} token【${token}】`);

		val.defTmp('const.sn.vctCallStk.length', ()=> this.aCallStk.length);

		this.grm.setEscape(cfg.oCfg.init.escape);

		if (sys.isDbg()) {
			sys.addHook((type, o)=> this.hHook[type]?.(type, o));
			this.isBreak = this.isBreak_base;

			const fnc = this.analyzeInit;
			this.analyzeInit = ()=> {
				this.breakState = BreakState.wait;
				main.setLoop(false, 'ステップ実行');
				// ScriptIterator.ts 'launch' の肩代わり
				this.sys.callHook('stopOnStep', {});	// sn全体へ通知
				this.sys.sendDbg('stopOnStep', {});

				this.analyzeInit = fnc;
				this.analyzeInit();
			};
		}
	}

	destroy() {this.isBreak = ()=> false;}

	private	readonly hHook: {[type: string]: (type: string, o: any)=> void}	= {
		'attach': ()=> {
			this.breakState = BreakState.wait;
			this.main.setLoop(false, '一時停止');
			this.sys.sendDbg('stop', {});
		},
		//'launch':	// ここでは冒頭停止に間に合わないのでanalyzeInit()で
		'disconnect': ()=> {
			ScriptIterator.hBrkP = {};
			ScriptIterator.hFuncBP = {};
			this.isBreak = ()=> false;

			this.hHook.continue('', {});
			this.breakState = BreakState.running;
		},
		'restart': ()=> this.isBreak = ()=> false,

		// ブレークポイント登録
		'clear_break': (_, o)=>	ScriptIterator.hBrkP[getFn(o.fn)] = {},
		'add_break': (_, o)=> ScriptIterator.hBrkP[getFn(o.fn)][o.ln] = o,
		'del_break': (_, o)=>delete ScriptIterator.hBrkP[getFn(o.fn)][o.ln],
		'data_break': (_, o)=> {
			if (this.breakState !== BreakState.running) return;

			this.breakState = BreakState.wait;
			this.main.setLoop(false, `変数 ${o.dataId}【${o.old_v}】→【${o.new_v}】データブレーク`);
			this.sys.callHook('stopOnDataBreakpoint', {});	// sn全体へ通知
			this.sys.sendDbg('stopOnDataBreakpoint', {});
		},
		'set_func_break': (_, o)=> {
			ScriptIterator.hFuncBP = {};
			o.a.forEach((v: any)=> ScriptIterator.hFuncBP[v.name] = 1);
			this.sys.sendDbg(o.ri, {});
		},

		// 情報問い合わせ系
		'stack': (_, o)=> this.sys.sendDbg(o.ri, {a: this.aStack()}),
		'eval': (_,o)=> {this.sys.sendDbg(o.ri, {v: this.prpPrs.parse(o.txt)})},

		// デバッガからの操作系
		'continue': ()=> {
			if (this.isIdxOverLast()) return;

			this.idxToken_ -= this.idxDx4Dbg;
			this.breakState = BreakState.breaking;
			this.main.setLoop(true);
			this.main.resume();	// jumpループ後などで停止している場合があるので
		},
		'stepover': (type, o)=> this.go_stepover(type, o),
		'stepin': ()=> {
			if (this.isIdxOverLast()) return;

			const tkn = this.script.aToken[this.idxToken_ -this.idxDx4Dbg];
			this.sys.callHook(`stopOnStep${
				tkn.charAt(0) === '[' && tkn.slice(1,-1)in this.hMacro ?'In' :''
			}`, {});	// sn全体へ通知

			this.idxToken_ -= this.idxDx4Dbg;
			this.breakState = this.breakState === BreakState.wait
				? BreakState.step
				: BreakState.stepping;
			this.main.setLoop(true);
			this.main.resume();	// jumpループ後などで停止している場合があるので
		},
		'stepout': (type, o)=> {
			if (this.isIdxOverLast()) return;

			if (this.lenCallStk > 0) this.go_stepout(true);
			else this.go_stepover(type, o);
		},
	};
	private	go_stepover(type: string, o: any) {
		if (this.isIdxOverLast()) return;

		const tkn = this.script.aToken[this.idxToken_ -this.idxDx4Dbg];
		if (tkn.charAt(0) === '[' && tkn.slice(1, -1) in this.hMacro) this.go_stepout(); else {
			this.sys.callHook('stopOnStep', {});	// sn全体へ通知
			this.hHook.stepin(type, o);
		}
	}
	private	go_stepout(out = false) {
		this.sys.callHook(`stopOnStep${out ?'Out' :''}`, {});	// sn全体へ通知
		this.csDepth_macro_esc = this.lenCallStk -(out ?1 :0);
		this.idxToken_ -= this.idxDx4Dbg;
		this.breakState = out ?BreakState.macro_esc :BreakState.macro_escaping;
		this.main.setLoop(true);
		this.main.resume();	// jumpループ後などで停止している場合があるので
	}
	private	csDepth_macro_esc	= 0;
	private get idxDx4Dbg() {
		return this.breakState === BreakState.break
			|| this.breakState === BreakState.step ?1 :0
	};
	private	isIdxOverLast(): boolean {
		if (this.idxToken_ < this.script.len) return false;
		this.main.setLoop(false, 'スクリプト終端です');
		this.sys.callHook('stopOnEntry', {});	// sn全体へ通知
		return true;
	}

	// reload 再生成 Main に受け渡すため static
	private	static	hBrkP: {[fn: string]: {[ln: number]: any}} = {};
	private	static	hFuncBP: {[tag_name: string]: 1} = {};
	private	breakState	= BreakState.running;
	isBreak = (_token: string)=> false;
	private isBreak_base(token: string): boolean {
		switch (this.breakState) {
			case BreakState.macro_escaping:	this.subHitCondition();
				this.breakState = BreakState.macro_esc;	break;
			case BreakState.macro_esc:
				if (this.lenCallStk !== this.csDepth_macro_esc) break;

				this.breakState = BreakState.step;
				this.main.setLoop(false, 'ステップ実行');
				this.sys.sendDbg('stopOnStep', {});
				return true;	// タグを実行せず、直前停止

			case BreakState.stepping:	this.subHitCondition();
				this.breakState = BreakState.step;	break;
			case BreakState.step:		this.subHitCondition();
				this.main.setLoop(false, 'ステップ実行');
				this.sys.sendDbg('stopOnStep', {});
				return true;	// タグを実行せず、直前停止

			case BreakState.breaking:	this.subHitCondition();
				this.breakState = BreakState.running;	break;

			default:
			{	// 関数ブレークポイント
				const e = Grammar.REG_TAG.exec(token);
				const tag_name = e?.groups?.name ?? '';
				if (tag_name in ScriptIterator.hFuncBP) {
					this.breakState = BreakState.break;
					this.main.setLoop(false, `関数 ${token} ブレーク`);
					this.sys.callHook('stopOnBreakpoint', {});	// sn全体へ通知
					this.sys.sendDbg('stopOnBreakpoint', {});
					return true;	// タグを実行せず、直前停止
				}
			}
			{	// ブレークポイント
				const bp = ScriptIterator.hBrkP[getFn(this.scriptFn_)];
				if (! bp) break;
				const o: any = bp[this.lineNum_];
				if (! o) break;
//console.log(`fn:ScriptIterator.ts line:145 👺 【bs:${this.breakState} idx:${this.idxToken_} ln:${this.lineNum_} tkn:${this.script.aToken[this.idxToken_ -1]}:】 o:%o`, o);
				if (o.condition) {if (! this.prpPrs.parse(o.condition)) break;}
				else if (('hitCondition' in o) && --o.hitCondition > 0) break;
			}
				const isBreak = this.breakState === BreakState.running;
				this.breakState = BreakState.break;
				this.main.setLoop(false, isBreak ?'ブレーク' :'ステップ実行');
				const type = isBreak ?'stopOnBreakpoint' :'stopOnStep';
				this.sys.callHook(type, {});	// sn全体へ通知
				this.sys.sendDbg(type, {});
				return true;	// タグを実行せず、直前停止
		}

		return false;	// no break、タグを実行
	}
	private	subHitCondition() {	// step実行中でbreakしないがヒットカウントだけ減算
		const bpo = ScriptIterator.hBrkP[getFn(this.scriptFn_)]?.[this.lineNum_];
		if (bpo?.hitCondition) --bpo.hitCondition;
	}

	private aStack(): {fn: string, ln: number, col: number, nm: string}[] {
		const tkn0 = this.script.aToken[this.idxToken_ -1];
		const fn0 = this.cfg.searchPath(this.scriptFn_, Config.EXT_SCRIPT);
		if (this.idxToken_ === 0) return [{fn: fn0, ln: 1, col: 1, nm: tkn0,}];

		const lc0 = this.cnvIdx2lineCol(this.script, this.idxToken_);
		const a = [{fn: fn0, ln: lc0.ln, col: lc0.col_s +1, nm: tkn0,}];
		const len = this.aCallStk.length;
		if (len === 0) return a;

		for (let i=len -1; i>=0; --i) {
			const cs = this.aCallStk[i];
			if (! cs.csArg) continue;

			const lc = this.cnvIdx2lineCol(this.hScript[cs.fn], cs.idx);
			a.push({
				fn: this.cfg.searchPath(cs.fn, Config.EXT_SCRIPT),
				ln: lc.ln,
				col: lc.col_s +1,
				nm: cs.csArg.タグ名 ?? '',
			});
		}

		return a;
	}

	// result = true : waitする  resume()で再開
	タグ解析(tagToken: string): boolean {
		const e = Grammar.REG_TAG.exec(tagToken);
		const g = e?.groups;
		if (! g) throw `タグ記述【${tagToken}】異常です(タグ解析)`;

		const tag_name = g.name;
		const tag_fnc = this.hTag[tag_name];
		if (! tag_fnc) throw `未定義のタグ【${tag_name}】です`;

		this.alzTagArg.go(g.args);
		if (this.cfg.oCfg.debug.tag) console.log(`🌲 タグ解析 fn:${this.scriptFn_} lnum:${this.lineNum_} [${tag_name} %o]`, this.alzTagArg.hPrm);

		if (this.alzTagArg.hPrm.cond) {
			const cond = this.alzTagArg.hPrm.cond.val;
			if (! cond || cond.charAt(0) ==='&') throw '属性condは「&」が不要です';
			const p = this.prpPrs.parse(cond);
			const ps = String(p);
			if (ps === 'null' || ps === 'undefined') return false;
			if (! p) return false;
		}

		let hArg: any = {};
		const lenStk = this.aCallStk.length;
		if (this.alzTagArg.isKomeParam) {
			if (lenStk === 0) throw '属性「*」はマクロのみ有効です';
			if (! this.lastHArg) throw '属性「*」はマクロのみ有効です';
			hArg = {...hArg, ...this.lastHArg};
		}
		hArg.タグ名 = tag_name;

		for (const k in this.alzTagArg.hPrm) {
			let v = this.alzTagArg.hPrm[k].val;
			if (v && v.charAt(0) === '%') {
				if (lenStk === 0) throw '属性「%」はマクロ定義内でのみ使用できます（そのマクロの引数を示す簡略文法であるため）';
				const mac = this.lastHArg[v.slice(1)];
				if (mac) {hArg[k] = mac; continue;}

				v = this.alzTagArg.hPrm[k].def;
				if (! v || v === 'null') continue;
					// defのnull指定。%指定が無い場合、タグやマクロに属性を渡さない
			}

			v = this.prpPrs.getValAmpersand(v ?? '');
			if (v !== 'undefined') {hArg[k] = v; continue;}

			const def = this.alzTagArg.hPrm[k].def;
			if (def === undefined) continue;
			v = this.prpPrs.getValAmpersand(def);
			if (v !== 'undefined') hArg[k] = v;	// 存在しない値の場合、属性を渡さない
		}

		return tag_fnc(hArg);
	}


	private	evtMng	: EventMng;
	private	layMng	: LayerMng;
	setOtherObj(evtMng: EventMng, layMng: LayerMng): void {
		this.evtMng = evtMng;
		this.layMng = layMng;
	}


//	//	変数操作
	// インラインテキスト代入
	private let_ml(hArg: HArg) {
		const name = hArg.name;
		if (! name) throw 'nameは必須です';

		let ml = '';
		const len = this.script.len;
		for (; this.idxToken_<len; ++this.idxToken_) {
			ml = this.script.aToken[this.idxToken_];
			if (ml !== '') break;
		}
		hArg.text = ml;
		hArg.cast = 'str';
		this.hTag['let'](hArg);
		this.idxToken_ += 2;
		this.lineNum_ += (ml.match(/\n/g) ?? []).length;

		return false;
	}


//	// デバッグ・その他
	// スタックのダンプ
	private dump_stack() {
		if (this.idxToken_ === 0) {
			console.group(`🥟 [dump_stack] スクリプト現在地 fn:${this.scriptFn_} line:${1} col:${0}`);
			console.groupEnd();
			return false;
		}

		const lc0 = this.cnvIdx2lineCol(this.script, this.idxToken_);
		const now = `スクリプト現在地 fn:${this.scriptFn_} line:${lc0.ln} col:${lc0.col_s +1}`;
		console.group(`🥟 [dump_stack] ${now}`);
		const len = this.aCallStk.length;
		if (len > 0) {
			console.info(now);
			for (let i=len -1; i>=0; --i) {
				const cs = this.aCallStk[i];
				if (! cs.csArg) continue;

				const csa = cs.csArg.hMp;
				const from_macro_nm = csa ?csa['タグ名'] :null;
				const call_nm = cs.csArg.タグ名;
				const lc = this.cnvIdx2lineCol(this.hScript[cs.fn], cs.idx);
				console.info(
					`${len -i}つ前のコール元 fn:${cs.fn} line:${lc.ln
					} col:${lc.col_s +1
					}`+ (from_macro_nm ?'（['+ from_macro_nm +']マクロ内）' :' ')+
					`で [${call_nm} ...]をコール`
				);
			}
		}
		console.groupEnd();

		return false;
	}
	private cnvIdx2lineCol(st: Script, idx: number): {ln: number, col_s: number, col_e: number} {
		const ret = {ln: 0, col_s: 0, col_e: 0};
		if (! st) return ret;

		const lN = ret.ln = st.aLNum[idx -1];
		let col_e = 0;
		let i = idx -1;
		while (st.aLNum[i] === lN) {
			col_e += st.aToken[i].length;
			if (--i < 0) break;
		}
		ret.col_e = col_e;
		ret.col_s = col_e -st.aToken[idx -1].length

		return ret;
	}


	// 外部へスクリプトを表示
	private dump_script(hArg: HArg) {
		const set_fnc = hArg.set_fnc;	// スクリプトを返すコールバック
		if (! set_fnc) throw 'set_fncは必須です';

		this.fncSet = (globalThis as any)[set_fnc];
		if (! this.fncSet) {
			if (argChk_Boolean(hArg, 'need_err', true)) throw `HTML内に関数${set_fnc}が見つかりません`;
			this.fncSet = ()=> {};
			return false;
		}

		this.noticeBreak = (goto: boolean)=> {
			if (this.fnLastBreak !== this.scriptFn_) {
				this.fnLastBreak = this.scriptFn_;
				this.fncSet(
					this.hScrCache4Dump[this.scriptFn_]
					=  this.hScrCache4Dump[this.scriptFn_]
					?? this.script.aToken.join('')
				);
			}
			this.fncBreak(this.lineNum_, goto);
		};
		this.noticeBreak(true);	// 一度目のthis.fncBreak()はスルー（まだ読んでないし）

		const break_fnc = hArg.break_fnc;	// Break通知コールバック
		if (! break_fnc) return false;

		this.fncBreak = (globalThis as any)[break_fnc];
		if (! this.fncBreak) {
			if (argChk_Boolean(hArg, 'need_err', true)) throw `HTML内に関数${break_fnc}が見つかりません`;
			this.fncBreak = ()=> {};
		}

		return false;
	}
	private fncSet: (txt: string)=> void = ()=> {};
	private fncBreak: (ln: number, goto: boolean)=> void = ()=> {};
	private fnLastBreak = '';
	private hScrCache4Dump: {[fn: string]: string;} = {};
	noticeBreak = (_goto: boolean)=> {}


	private dumpErrLine = 5;
	dumpErrForeLine() {
		if (this.idxToken_ === 0) {
			console.group(`🥟 Error line (from 0 rows before) fn:${this.scriptFn_}`);
			console.groupEnd();
			return;
		}

		let s = '';
		for (let i=this.idxToken_ -1; i>=0; --i) {
			s = this.script.aToken[i] + s;
			if ((s.match(/\n/g) ?? []).length >= this.dumpErrLine) break;
		}
		const a = s.split('\n').slice(-this.dumpErrLine);
		const len = a.length;
		console.group(`🥟 Error line (from ${len} rows before) fn:${this.scriptFn_}`);
		const ln_txt_width = String(this.lineNum_).length;
		const lc = this.cnvIdx2lineCol(this.script, this.idxToken_);
		for (let i=0; i<len; ++i) {
			const ln = this.lineNum_ -len +i +1;
			const mes = `${String(ln).padStart(ln_txt_width, ' ')}: %c`;
			const e = a[i];
			const line = (e.length > 75) ?e.slice(0, 75) +'…' :e;	// 長い場合は後略
			if (i === len -1) console.info(
				mes + line.slice(0, lc.col_s) +'%c'+ line.slice(lc.col_s),
				'color: black; background-color: skyblue;', 'color: black; background-color: pink;'
			)
			else console.info(mes + line, 'color: black; background-color: skyblue;');
		}
		console.groupEnd();
		//console.log('Linkの出力   : %o', 'file:///Volumes/MacHD2/_Famibee/SKYNovel/prj/mat/main.sn');
	}



		// 条件分岐
	private aIfStk	: number[]	= [-1];
	private endif() {
		if (this.aIfStk[0] === -1) throw 'ifブロック内ではありません';

		this.idxToken_ = this.aIfStk[0];
		this.lineNum_ =  this.script.aLNum[this.idxToken_ -1];
		this.aIfStk.shift();	// 最初の要素を取り除く

		return false;
	}
	private if(hArg: HArg) {
		//console.log('if idxToken:'+ this.idxToken_);
		const exp = hArg.exp;
		if (! exp) throw 'expは必須です';
		if (exp.charAt(0) === '&') throw '属性expは「&」が不要です';

		let cntDepth = 0;		// if深度カウンター
		let	idxGo = this.prpPrs.parse(exp) ?this.idxToken_ :-1;
		for (; this.idxToken_<this.script.len; ++this.idxToken_) {
			if (! this.script.aLNum[this.idxToken_]) this.script.aLNum[this.idxToken_] = this.lineNum_;
			const t = this.script.aToken[this.idxToken_];
			//console.log(`[if]トークン fn:${this.scriptFn_} lnum:${this.lineNum_} idx:${this.idxToken_} realLn:${this.script.aLNum[this.idxToken_]} idxGo:${idxGo} cntDepth:${cntDepth} token<${t}>`);
			if (! t) continue;

			const uc = t.charCodeAt(0);	// TokenTopUnicode
			if (uc === 10) {this.addLineNum(t.length); continue;}	// \n 改行
			if (uc !== 91) continue;		// [ タグ開始以外

			const a_tag = Grammar.REG_TAG.exec(t);
			const g = a_tag?.groups;
			if (! g) throw 'タグ記述['+ t +']異常です(if文)';
			const tag_name = g.name;
			if (! (tag_name in this.hTag)) throw `未定義のタグ[${tag_name}]です`;
			this.alzTagArg.go(g.args);

			switch (tag_name) {
			case 'if':	++cntDepth; break;

			case 'elsif':
				if (cntDepth > 0) break;
				if (idxGo > -1) break;

				const e = this.alzTagArg.hPrm.exp.val ?? '';
				if (e.charAt(0) === '&') throw '属性expは「&」が不要です';
				if (this.prpPrs.parse(e)) idxGo = this.idxToken_ +1;
				break;

			case 'else':
				if (cntDepth > 0) break;
				if (idxGo === -1) idxGo = this.idxToken_ +1;
				break;

			case 'endif':
				if (cntDepth > 0) {--cntDepth; break;}
				if (idxGo === -1) {
					++this.idxToken_;
					this.script.aLNum[this.idxToken_] = this.lineNum_;
				}
				else {
					this.aIfStk.unshift(this.idxToken_ +1);	// 最初に要素を追加
					this.idxToken_ = idxGo;
					this.lineNum_ =  this.script.aLNum[this.idxToken_];
				}
				return false;
			}
		}
		throw '[endif]がないままスクリプト終端です';
		//return false;
	}


//	// ラベル・ジャンプ
	// サブルーチンコール
	private call(hArg: HArg) {
		if (! argChk_Boolean(hArg, 'count', false)) this.eraseKidoku();

		const fn = hArg.fn;
		if (fn) this.cfg.searchPath(fn, Config.EXT_SCRIPT);	// chk only
		this.callSub({hEvt1Time: this.evtMng.popLocalEvts()});

		if (argChk_Boolean(hArg, 'clear_local_event', false)) this.hTag.clear_event({});
		this.jumpWork(fn, hArg.label);

		return true;
	}
	private callSub(csa: ICallStackArg) {
		this.script.aLNum[this.idxToken_] = this.lineNum_;	// 戻ったときの行番号
		if (! this.resvToken) {csa.resvToken = ''; this.clearResvToken();}
		this.aCallStk.push(new CallStack(this.scriptFn_, this.idxToken_, csa));
		this.aIfStk.unshift(-1);	// 最初に要素を追加
	}

	// シナリオジャンプ
	private jump(hArg: HArg) {
		if (! argChk_Boolean(hArg, 'count', true)) this.eraseKidoku();

		this.aIfStk[0] = -1;
		this.jumpWork(hArg.fn, hArg.label);

		return true;
	}

	// コールスタック破棄
	private pop_stack(hArg: HArg) {
		if (argChk_Boolean(hArg, 'clear', false)) this.aCallStk = [];
		else if (! this.aCallStk.pop()) throw '[pop_stack] スタックが空です';
		this.clearResvToken();
		this.aIfStk = [-1];

		return false;
	}

	// サブルーチンから戻る
	private return() {
		const cs = this.aCallStk.pop();
		if (! cs) throw '[return] スタックが空です';
		const csArg = cs.csArg;
		if (! csArg) return false;
		this.aIfStk.shift();	// 最初の要素を取り除く

		const hMp = csArg.hMp;	// マクロからの復帰の場合にmp:値も復帰
		if (hMp) this.val.setMp(hMp);

		const after_token = csArg.resvToken;
		if (after_token) this.nextToken = ()=> {
			this.clearResvToken();
			return after_token;
		}
		else this.clearResvToken();
		if (csArg.hEvt1Time) this.evtMng.pushLocalEvts(csArg.hEvt1Time);

		if (cs.fn in this.hScript) {this.jump_light(cs); return false;}
		this.jumpWork(cs.fn, '', cs.idx);	// 確実にスクリプトロードなので
		return true;
	}

	private resvToken	= '';
	private clearResvToken() {
		this.resvToken = '';
		this.nextToken = this.nextToken_Proc;
	}


	private skipLabel = '';
	private jumpWork(fn = '', label = '', idx = 0) {
		if (! fn && ! label) this.main.errScript('[jump系] fnまたはlabelは必須です');
		if (label) {
			if (label.charAt(0) !== '*') this.main.errScript('[jump系] labelは*で始まります');
			this.skipLabel = label;
			if (this.skipLabel.slice(0, 2) !== '**') this.idxToken_ = idx;
		}
		else {
			this.skipLabel = '';
			this.idxToken_ = idx;
		}

		if (! fn) {this.analyzeInit(); return;}

		const full_path = this.cfg.searchPath(fn, Config.EXT_SCRIPT);
		if (fn === this.scriptFn_) {this.analyzeInit(); return;}
		this.scriptFn_ = fn;
		const st = this.hScript[this.scriptFn_];
		if (st) {this.script = st; this.analyzeInit(); return;}

		(new Loader()).add(this.scriptFn_, full_path)
		.pre((res: LoaderResource, next: Function)=> res.load(()=> {
			this.sys.pre(res.extension, res.data)
			.then(r=> {res.data = r; next();})
				// TODO: 暗号化スクリプトかは、前方から一定の長さに(\n|\t)有無で分かる
//		if (this.onlyCodeScript()) this.main.errScript('[セキュリティ] 暗号化スクリプト以外許されません');
			.catch(e=> this.main.errScript(`[jump系]snロード失敗です fn:${res.name} ${e}`, false));
		}))
		.load((_ldr: any, hRes: any)=> {
			this.nextToken = this.nextToken_Proc;

			this.resolveScript(hRes[fn].data);
			this.hTag.record_place({});
			this.main.resume(()=> this.analyzeInit());
				// 直接呼んでもいいが、内部コールスタック積んだままになるのがなんかイヤで
		});
		this.main.stop();
	}
	private analyzeInit(): void {
		const o = this.seekScript(this.script, Boolean(this.val.getVal('mp:const.sn.macro_name')), this.lineNum_, this.skipLabel, this.idxToken_);
		this.idxToken_	= o.idx;
		this.lineNum_	= o.lineNum;
		this.runAnalyze();
	}

	// シナリオ解析処理ループ・冒頭処理
	nextToken = ()=> '';	// 初期化前に終了した場合向け
	private nextToken_Proc(): string {
		if (this.errOverScr()) return '';

		this.recordKidoku();

		// トークンの行番号更新
		if (! this.script.aLNum[this.idxToken_]) this.script.aLNum[this.idxToken_] = this.lineNum_;
		const token = this.script.aToken[this.idxToken_];
		this.dbgToken(token);
		++this.idxToken_;

		return token;
	}
	private	dbgToken = (_token: string)=> {};
	private	errOverScr(): boolean {
		if (this.idxToken_ < this.script.len) return false;
		this.main.errScript('スクリプト終端です');
		return true;
	}


	private	readonly REG_NONAME_LABEL		= /(\*{2,})(.*)/;
	private	readonly REG_LABEL_ESC			= /\*/g;
	private	readonly REG_TOKEN_MACRO_BEGIN	= /\[macro\s/;
	private	readonly REG_TOKEN_MACRO_END	= /\[endmacro[\s\]]/;
	private	readonly REG_TAG_LET_ML			= /^\[let_ml\s/g;
	private	readonly REG_TAG_ENDLET_ML		= /^\[endlet_ml\s*]/g;
	private	seekScript(st: Script, inMacro: boolean, ln: number, skipLabel: string, idxToken: number): ISeek {
		//console.log(`seekScript (from)inMacro:${inMacro} (from)lineNum:${ln} (to)skipLabel:${skipLabel}: (to)idxToken:${idxToken}`);
		const len = st.aToken.length;
		if (! skipLabel) {
			if (this.errOverScr()) return {idx: idxToken, lineNum: ln};

			if (! st.aLNum[idxToken]) {	// undefined
				ln = 1;
				for (let j=0; j<idxToken; ++j) {
					// 走査ついでにトークンの行番号も更新
					if (! st.aLNum[j]) st.aLNum[j] = ln;

					const token_j = st.aToken[j];
					if (token_j.charCodeAt(0) === 10) {	// \n 改行
						ln += token_j.length;
					}
				}
				st.aLNum[idxToken] = ln;
			}
			else {
				ln = st.aLNum[idxToken];
			}

			return {
				idx: idxToken,
				lineNum	: ln
			}
		}

		st.aLNum[0] = 1;		// 先頭トークン＝一行目
		const a_skipLabel = skipLabel.match(this.REG_NONAME_LABEL);
		if (a_skipLabel) {
			skipLabel = a_skipLabel[1];
			let i = idxToken;
			switch (a_skipLabel[2]) {
			case 'before':
				while (st.aToken[--i] !== skipLabel) {
					if (i === 0) DebugMng.myTrace('[jump系 無名ラベルbefore] '
						+ ln +'行目以前で'+ (inMacro ?'マクロ内に' :'')
						+ 'ラベル【'+ skipLabel +'】がありません', 'ET');
					if (inMacro && st.aToken[i].search(this.REG_TOKEN_MACRO_BEGIN) > -1) DebugMng.myTrace('[jump系 無名ラベルbefore] マクロ内にラベル【'+ skipLabel +'】がありません', 'ET');
				}
				return {
					idx: i +1,
					lineNum	: st.aLNum[i]
				}	//	break;

			case 'after':
				while (st.aToken[++i] !== skipLabel) {
					if (i === len) DebugMng.myTrace('[jump系 無名ラベルafter] '
						+ ln +'行目以後でマクロ内にラベル【'+ skipLabel +'】がありません', 'ET');
					if (st.aToken[i].search(this.REG_TOKEN_MACRO_END) > -1) DebugMng.myTrace('[jump系 無名ラベルafter] '
						+ ln +'行目以後でマクロ内にラベル【'+ skipLabel +'】がありません', 'ET');
				}
				return {
					idx: i +1,
					lineNum	: st.aLNum[i]
				}	//	break;

			default:
				DebugMng.myTrace('[jump系] 無名ラベル指定【label='+ skipLabel +'】が間違っています', 'ET');
			}
		}

		ln = 1;
		const reLabel = new RegExp(
			'^'+ skipLabel.replace(this.REG_LABEL_ESC, '\\*')
			+'(?:\\s|;|\\[|$)');
		let in_let_ml = false;
		for (let i=0; i<len; ++i) {
			// 走査ついでにトークンの行番号も更新
			if (! st.aLNum[i]) st.aLNum[i] = ln;

			const token = st.aToken[i];
			const uc = token.charCodeAt(0);	// TokenTopUnicode
			if (uc !== 42) {	// 42 = *
				if (in_let_ml) {
					this.REG_TAG_ENDLET_ML.lastIndex = 0;
					if (this.REG_TAG_ENDLET_ML.test(token)) {
						in_let_ml = false;
						continue;
					}
					ln += (token.match(/\n/g) ?? []).length;	// \n 改行
				}
				else {
					this.REG_TAG_LET_ML.lastIndex = 0;
					if (this.REG_TAG_LET_ML.test(token)) {
						in_let_ml = true;
						continue;
					}
					if (uc === 10) ln += token.length;	// \n 改行
				}
				continue;
			}

			if (token.search(reLabel) > -1) return {
				idx: i +1,
				lineNum	: ln
			}	//	break;
		}
		if (in_let_ml) throw '[let_ml]の終端・[endlet_ml]がありません';

		DebugMng.myTrace(`[jump系] ラベル【`+ skipLabel +`】がありません`, 'ET');
		throw 'Dummy';
	}

	private hScript	: HScript	= Object.create(null);	//{} シナリオキャッシュ
	private resolveScript(txt: string) {
		const v = txt
			.replace(/(\r\n|\r)/g, '\n')
			.match(this.grm.REG_TOKEN) ?? [];
		for (let i=v.length -1; i>=0; --i) {
			const e = v[i];
			this.REG_TAG_LET_ML.lastIndex = 0;
			if (this.REG_TAG_LET_ML.test(e)) {
				const idx = e.indexOf(']') +1;
				if (idx === 0) throw '[let_ml]で閉じる【]】がありません';
				const a = e.slice(0, idx);
				const b = e.slice(idx);
				v.splice(i, 1, a, b);
			}
		}
		this.script = {aToken :v, len :v.length, aLNum :[]};

		let mes = '';
		try {
			mes = 'ScriptIterator.replaceScriptChar2macro';
			this.grm.replaceScr_C2M_And_let_ml(this.script);
			mes = 'ScriptIterator.replaceScript_Wildcard';
			this.replaceScript_Wildcard();
		}
		catch (err) {
			if (err instanceof Error) {
				const e = err as Error;
				mes += '例外 mes='+ e.message +'('+ e.name +')';
			}
			else {
				mes = err as string;
			}
			this.main.errScript(mes, false);
		}
		this.hScript[this.scriptFn_] = this.script;

		this.val.loadScrWork(this.scriptFn_);
	}

	private jump_light(cs: CallStack) {
		// jumpでは連続マクロでスタックオーバーフローになるので簡易版を
		// 主に[return]やマクロ終了でジャンプ先がチェック不要な場合用
		// analyzeInit()とかもジャンプ前にやってて不要だし
		this.scriptFn_	= cs.fn;
		this.idxToken_	= cs.idx;
		const st = this.hScript[this.scriptFn_];
		if (st) this.script = st;
		this.lineNum_ = this.script.aLNum[cs.idx];
	}


	private	readonly REG_WILDCARD	= /^\[(call|loadplugin)\s/;
	private	readonly REG_WILDCARD2	= /\bfn\s*=\s*[^\s\]]+/;
	private replaceScript_Wildcard = ()=> {
		for (let i=this.script.len -1; i>=0; --i) {
			const token = this.script.aToken[i];
			this.REG_WILDCARD.lastIndex = 0;
			if (! this.REG_WILDCARD.test(token)) continue;

			const e = Grammar.REG_TAG.exec(token);
			const g = e?.groups;
			if (! g) continue;
			this.alzTagArg.go(g.args);

			const p_fn = this.alzTagArg.hPrm.fn;
			if (! p_fn) continue;
			const fn = p_fn.val;
			if (! fn || fn.slice(-1) !== '*') continue;

			const ext = (g.name === 'loadplugin') ?'css' :'sn';
			const a = this.cfg.matchPath('^'+ fn.slice(0, -1) +'.*', ext);

			this.script.aToken.splice(i, 1, '\t', '; '+ token);
			this.script.aLNum.splice(i, 1, NaN, NaN);

			for (const v of a) {
				const nt = token.replace(
					this.REG_WILDCARD2,
					'fn='+ decodeURIComponent(getFn(v[ext]))
				);
				//console.log('\t='+ nt +'=');
				this.script.aToken.splice(i, 0, nt);
				this.script.aLNum.splice(i, 0, NaN);
			}
		}
		this.script.len = this.script.aToken.length;
	}


	private recordKidoku(): void {
		const areas = this.val.getAreaKidoku(this.scriptFn_);
		if (! areas) throw `recordKidoku fn:'${this.scriptFn_}' (areas === null)`;

		// マクロ内やサブルーチンではisKidokuを変更させない
		if (this.aCallStk.length > 0) {areas.record(this.idxToken_); return;}

		this.isKidoku_ = areas.search(this.idxToken_);
		this.val.setVal_Nochk('tmp', 'const.sn.isKidoku', this.isKidoku_);
		if (this.isKidoku_) return;

		areas.record(this.idxToken_);
		// saveKidoku()
			// 厳密にはここですべきだが、パフォーマンスに問題があるので
			// クリック待ちを期待できるwait、waitclick、s、l、pタグで
			// saveKidoku()をコール。
	}
	private isKidoku_	= false;
	get isKidoku(): boolean {return this.isKidoku_;};
	private eraseKidoku(): void {
		this.val.getAreaKidoku(this.scriptFn_)?.erase(this.idxToken_);
		this.isKidoku_ = false;
	}
	get isNextKidoku(): boolean {
		let fn	= this.scriptFn;
		let idx	= this.idxToken_;
		let len	= this.script.len;
		if (this.aCallStk.length > 0) {
			const cs = this.aCallStk[0];
			fn  = cs.fn;
			idx = cs.idx;
			const st = this.hScript[fn];
			if (st) len = st.len;
		}

		const areas = this.val.getAreaKidoku(fn);
		if (! areas) return false;
		if (idx === len) return false;	// スクリプト終端

		//traceDbg("isNextKidoku fn:"+ fn +" idx:"+ idx +" ret="+ (areas.search(idx)));
		//traceDbg("【"+ vctT[idx-1] +"】【"+ vctT[idx] +"】");

		return areas.search(idx);
	}


	get normalWait(): number {
		return this.isKidoku_
		? (
			this.val.getVal('sys:sn.tagCh.doWait_Kidoku')
			?	uint(this.val.getVal('sys:sn.tagCh.msecWait_Kidoku'))
			:	0
		)
		: (
			this.val.getVal('sys:sn.tagCh.doWait')
			?	uint(this.val.getVal('sys:sn.tagCh.msecWait'))
			:	0
		);
	}


//	// マクロ
	// 括弧マクロの定義
	private bracket2macro(hArg: HArg) {
		this.grm.bracket2macro(hArg, this.script, this.idxToken_);

		return false;
	}

	// 一文字マクロの定義
	private char2macro(hArg: HArg) {
		this.grm.char2macro(hArg, this.hTag, this.script, this.idxToken_);

		return false;
	}

	// マクロ定義の開始
	private macro(hArg: HArg) {
		const name = hArg.name;
		if (! name) throw 'nameは必須です';
		if (name in this.hTag) throw `[${name}]はタグかすでに定義済みのマクロです`;

		const ln = this.lineNum_;
		const cs = new CallStack(this.scriptFn_, this.idxToken_);
		this.hMacro[name] = 1;
		this.hTag[name] = (hArgM: HArg)=> {
			this.callSub({...hArgM, hMp: this.val.cloneMp()} as any);

			// AIRNovelの仕様：親マクロが子マクロコール時、*がないのに値を引き継ぐ
			//for (const k in hArg) this.val.setVal_Nochk('mp', k, hArg[k]);
			this.val.setMp(hArgM);
			this.val.setVal_Nochk('mp', 'const.sn.macro_name', name);
			this.val.setVal_Nochk('mp', 'const.sn.me_call_scriptFn', this.scriptFn_);

			this.lineNum_ = ln;
			this.jump_light(cs);

			return false;
		};

		for (; this.idxToken_ < this.script.len; ++this.idxToken_) {
			// トークンの行番号更新
			if (! this.script.aLNum[this.idxToken_]) this.script.aLNum[this.idxToken_] = this.lineNum_;

			const token = this.script.aToken[this.idxToken_];
			if (token.search(this.REG_TOKEN_MACRO_END) > -1) {
				++this.idxToken_;
				return false;
			}

			const uc = token.charCodeAt(0);	// TokenTopUnicode
			if (uc === 10) this.lineNum_ += token.length;	// \n 改行
			else if (uc === 91) this.lineNum_ += (token.match(/\n/g) ?? []).length;	// [ タグ開始
		}
		throw `マクロ[${name}]定義の終端・[endmacro]がありません`;
	}
	private	readonly	hMacro: {[nm: string]: 1}	= {};


//	// しおり
	// しおりの読込
	private load(hArg: HArg) {
		const place = hArg.place;
		if (! place) throw 'placeは必須です';
		if (('fn' in hArg) !== ('label' in hArg)) throw 'fnとlabelはセットで指定して下さい';

		const mark = this.val.getMark(place);
		if (! mark) throw `place【${place}】は存在しません`;

		return this.loadFromMark(hArg, mark);
	}
	private loadFromMark(hArg: HArg, mark: IMark, reload_sound = true) {
		this.layMng.cover(true);
		this.hTag.clear_event({});
		this.val.mark2save(mark);
		this.val.setMp({});
		this.layMng.recText('', true);

		if (reload_sound) this.sndMng.playLoopFromSaveObj();

		if (argChk_Boolean(hArg, 'do_rec', true)) this.mark = {
			hSave	: this.val.cloneSave(),
			hPages	: {...mark.hPages},
			aIfStk	: [...mark.aIfStk],
		}

		const o: any = {
			enabled: this.val.getVal('save:const.sn.autowc.enabled'),
			text: String(this.val.getVal('save:const.sn.autowc.text')),
			time: String(this.val.getVal('save:const.sn.autowc.time')),
		};
		this.hTag.autowc(o);

		const fn = String(this.val.getVal('save:const.sn.scriptFn'));
		const idx = Number(this.val.getVal('save:const.sn.scriptIdx'));
		delete this.hScript[fn];	// 必ずスクリプトを再読込。吉里吉里に動作を合わせる
		this.aIfStk = [...this.mark.aIfStk];
		this.aCallStk = [];

		// playback中の画像読み込み完了イベントを破棄
		this.layMng.playback(this.mark.hPages, 'label' in hArg
			? ()=> {
				this.layMng.cover(false);
				this.scriptFn_ = fn;
				this.idxToken_ = idx;
				this.hTag.call({fn: hArg.fn, label: hArg.label});
			}
			: ()=> {
				this.layMng.cover(false);
				this.jumpWork(fn, '', idx);
			}
		);

		return true;
	}

	// スクリプト再読込
	private reload_script(hArg: HArg) {	// 最後の[record_place]から再開
		const mark = this.val.getMark(0);
		// 起動から再読込までの間に追加・変更・削除されたファイルがあるかも、に対応
		//	delete this.hScript[this.scriptFn_];	// これだと[reload_script]位置になる
		delete this.hScript[getFn(mark.hSave['const.sn.scriptFn'])];

		hArg.do_rec = false;
		return this.loadFromMark(hArg, mark, false);
	}


	// セーブポイント指定
	private	mark: IMark = {
		hSave	: {},
		hPages	: {},
		aIfStk	: [-1],
	};
	private record_place() {
		if (this.main.isDestroyed()) return false;

		const len = this.aCallStk.length;
		if (len === 0) {
			this.val.setVal_Nochk('save', 'const.sn.scriptFn', this.scriptFn);
			this.val.setVal_Nochk('save', 'const.sn.scriptIdx', this.idxToken_);
		}
		else {
			this.val.setVal_Nochk('save', 'const.sn.scriptFn', this.aCallStk[0].fn);
			this.val.setVal_Nochk('save', 'const.sn.scriptIdx', this.aCallStk[0].idx);
		}
		this.mark = {
			hSave	: this.val.cloneSave(),
			hPages	: this.layMng.record(),
			aIfStk	: this.aIfStk.slice(len),
		};

		return false;
	}

	// しおりの保存
	private save(hArg: HArg) {
		const place = hArg.place;
		if (! place) throw 'placeは必須です';

		delete hArg.タグ名;
		delete hArg.place;
		hArg.text = (hArg.text ?? '').replace(/^(<br\/>)+/, '');
		this.mark.json = hArg;
		this.val.setMark(place, this.mark);

		const now_sp = Number(this.val.getVal('sys:const.sn.save.place'));
		if (place === now_sp) this.val.setVal_Nochk('sys', 'const.sn.save.place', now_sp +1);

		return false;
	}

}
