/* ***** 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 {Layer} from './Layer';

import {uint, CmnLib, IEvtMng} from './CmnLib';
import {IVariable, IHTag, HArg, IPutCh, IMain} from './CmnInterface';
import {TxtStage, IInfTxLay} from './TxtStage';
import {Config} from './Config';
import {RubySpliter} from './RubySpliter';
import {GrpLayer} from './GrpLayer';
import {Button} from './Button';
import {Sprite, DisplayObject, Graphics, Container, Renderer} from 'pixi.js';
import {LayerMng} from './LayerMng';

export class TxtLayer extends Layer {
	private	static	cfg		: Config;
	private	static	val		: IVariable;
	private	static	recText	: (txt: string, pagebreak?: boolean)=> void;
	static	init(cfg: Config, hTag: IHTag, val: IVariable, recText: (txt: string)=> void): void {
		TxtLayer.cfg = cfg;
		TxtStage.init(cfg);
		TxtLayer.val = val;
		TxtLayer.recText = recText;

		val.setDoRecProc(TxtLayer.chgDoRec);

		hTag.autowc			= o=> TxtLayer.autowc(o);	// 文字を追加する
		const o: any = {enabled: 'false', text: '', time: ''};
		hTag.autowc(o);
		hTag.ch_in_style	= o=> TxtLayer.ch_in_style(o);	// 文字出現演出
		hTag.ch_out_style	= o=> TxtLayer.ch_out_style(o);	// 文字消去演出

		// ギャラリーリロード用初期化
		TxtStage.initChStyle();
		const he = document.getElementsByTagName('head')[0];
		const len = he.children.length;
		for (let i=len -1; i>=0; --i) {
			const v = he.children[i];
			if (! (v instanceof HTMLStyleElement)) continue;
			if (v.innerText.slice(0, 14) != TxtLayer.css_key4del) continue;
			he.removeChild(v);
		}

		let font = '';
		for (const o of cfg.matchPath('.+', Config.EXT_FONT)) {
			for (const key in o) font += `
@font-face {
	font-family: '${CmnLib.getFn(o[key])}';
	src: url('${o[key]}');
}
`;
		}

		// 文字出現演出関係
		font += `
.sn_tx {
	pointer-events: none;
	user-select: none;
	-webkit-touch-callout: none;
	box-sizing: border-box;
}
.sn_ch {
	position: relative;
	display: inline-block;
}
`;	// 「sn_ch」と「sn_ch_in_〜」の中身が重複しているが、これは必須
		TxtLayer.addStyle(font);

		TxtLayer.ch_in_style({
			name	: 'default',
			wait	: 500,
			alpha	: 0,
			x		: '=0.3',
			y		: '=0',
			scale_x	: 1,
			scale_y	: 1,
			rotate	: 0,
			join	: true,
			ease	: 'ease-out',
		});
		TxtLayer.ch_out_style({
			name	: 'default',
			wait	: 0,
			alpha	: 0,
			x		: '=0',
			y		: '=0',
			scale_x	: 1,
			scale_y	: 1,
			rotate	: 0,
			join	: false,
			ease	: 'ease-out',
		});
	}
	private	static	css_key4del	= '/* SKYNovel */';
	static addStyle(style: string) {
		const gs = document.createElement('style');
		gs.type = 'text/css';
		gs.innerHTML = TxtLayer.css_key4del + style;
		document.getElementsByTagName('head')[0].appendChild(gs);
	}

	// 文字出現演出
	private	static	ch_in_style(hArg: HArg) {
		const o = TxtStage.ch_in_style(hArg);
		const x = (o.x.charAt(0) == '=') ?`${o.nx *100}%` :`${o.nx}px`;
		const y = (o.y.charAt(0) == '=') ?`${o.ny *100}%` :`${o.ny}px`;
		const name = hArg.name;
		TxtLayer.addStyle(`
.sn_ch_in_${name} {
	position: relative;
	display: inline-block;
}
.go_ch_in_${name} {
	opacity: ${o.alpha};
	position: relative;
	display: inline-block;
	animation: sn_ch_in_${name} ${o.wait}ms ${o.ease} 0s both;
}
@keyframes sn_ch_in_${name} {
	from {transform: rotate(${o.rotate}deg) scale(${o.scale_x}, ${o.scale_y}) translate(${x}, ${y});}
	to {opacity: 1; transform: none;}
}
`		);

		return false;
	}
	// 文字消去演出
	private	static	ch_out_style(hArg: HArg) {
		const o = TxtStage.ch_out_style(hArg);
		const x = (o.x.charAt(0) == '=') ?`${o.nx *100}%` :`${o.nx}px`;
		const y = (o.y.charAt(0) == '=') ?`${o.ny *100}%` :`${o.ny}px`;
		const name = hArg.name;
		TxtLayer.addStyle(`
.go_ch_out_${name} {
	position: relative;
	display: inline-block;
	animation: go_ch_out_${name} ${o.wait}ms ${o.ease} 0s both;
}
@keyframes go_ch_out_${name} {
	to {
		opacity: ${o.alpha};
		transform: rotate(${o.rotate}deg) scale(${o.scale_x}, ${o.scale_y}) translate(${x}, ${y});
	}
`);

		return false;
	}


	private	static	main	: IMain;
	private	static	evtMng	: IEvtMng;
	static setEvtMng(main: IMain, evtMng: IEvtMng) {
		TxtLayer.main = main;
		TxtLayer.evtMng = evtMng;
		TxtStage.setEvtMng(evtMng);
	}

	// 文字ごとのウェイト
	private	static doAutoWc	= false;
	private	static hAutoWc	: {[ch: string]: number} = Object.create(null);//{}
	private static autowc(hArg: HArg) {
		TxtLayer.doAutoWc = CmnLib.argChk_Boolean(hArg, 'enabled', TxtLayer.doAutoWc);
		TxtLayer.val.setVal_Nochk('save', 'const.sn.autowc.enabled', TxtLayer.doAutoWc);

		const ch = hArg.text;
		if (('text' in hArg) != ('time' in hArg)) throw '[autowc] textとtimeは同時指定必須です';
		TxtLayer.val.setVal_Nochk('save', 'const.sn.autowc.text', ch);
		if (! ch) {
			TxtLayer.val.setVal_Nochk('save', 'const.sn.autowc.time', '');
			return false;
		}

		const len = ch.length;
		if (TxtLayer.doAutoWc && len == 0) throw '[autowc] enabled == false かつ text == "" は許されません';

		const a = String(hArg.time).split(',');
		if (a.length != len) throw '[autowc] text文字数とtimeに記述された待ち時間（コンマ区切り）は同数にして下さい';
		TxtLayer.hAutoWc = Object.create(null);	//{}	// 毎回クリアを仕様とする
		a.forEach((v, i)=> TxtLayer.hAutoWc[ch[i]] = uint(v));
		TxtLayer.val.setVal_Nochk('save', 'const.sn.autowc.time', hArg.time);

		return false;
	}


	private infTL :IInfTxLay = {
		fontsize	: 24,
		$width		: 0,	// レイヤサイズであり、背景色（画像）サイズ
		$height		: 0,
		pad_left	: 0,	// paddingLeft（レイヤサイズの内側のスペーサー）
		pad_right	: 0,	// paddingRight
		pad_top		: 0,	// paddingTop
		pad_bottom	: 0,	// paddingBottom
	}

	// バック
	private b_color			= 0x000000;
	private b_alpha			= 0;
	private b_alpha_isfixed	= false;
	private b_do			: DisplayObject | null	= null;
	private b_pic			= '';	// 背景画像無し（＝単色塗り）

	// 文字表示
	private cntInsidePadding= new Container;
	private	txs				= new TxtStage(this.infTL, this.cntInsidePadding, this.cnt);

	private	rbSpl			= new RubySpliter;

	private cntBtn			= new Container;

	constructor() {
		super();

		const padding = 16 *CmnLib.retinaRate;	// 初期padding
		this.cnt.addChild(this.cntInsidePadding);
		this.cntInsidePadding.name = 'cntInsidePadding';
		this.cntInsidePadding.position.set(padding, padding);

		this.rbSpl.init(this.putCh);

		this.cnt.addChild(this.cntBtn);	// ボタンはpaddingの影響を受けない
		this.cntBtn.name = 'cntBtn';

		this.lay({style: `width: ${CmnLib.stageW}px; height: ${CmnLib.stageH}px; font-family: 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', '游ゴシック Medium', meiryo, sans-serif; color: white; font-size: 24px; line-height: 1.5; padding: ${padding}px;`, in_style: 'default', out_style: 'default', back_clear: 'true'});
	}
	destroy() {
		if (this.b_do) {this.cnt.removeChild(this.b_do).destroy(); this.b_do = null}

		this.clearText();
		this.txs.destroy();
	}
	set name(nm: string) {if (this.txs) this.txs.name = nm;}
	get name() {return this.txs ?this.txs.name :''}


	cvsResize() {this.txs.cvsResize();}

	lay(hArg: HArg) {
		super.lay(hArg);
		Layer.setXY(this.cnt, hArg, this.cnt);

		this.rbSpl.setting(hArg);
		this.setFfs(hArg);
		this.txs.lay(hArg);

		if ('r_align' in hArg) this.r_align = hArg.r_align ?? '';
		this.ruby_pd = CmnLib.isSafari
			? this.txs.tategaki		// Safariでは親文字幅 l は疑似値
				? (v, l)=> `text-align: start; height: ${l}em; padding-top: ${v}; padding-bottom: ${v};`
				: (v, l)=> `text-align: start; width: ${l}em; padding-left: ${v}; padding-right: ${v};`
			: this.txs.tategaki
				? v=> `text-align: justify; text-align-last: justify; padding-top: ${v}; padding-bottom: ${v};`
				: v=> `text-align: justify; text-align-last: justify; padding-left: ${v}; padding-right: ${v};`;
		if (CmnLib.isFirefox) this.mkStyle_r_align = this.mkStyle_r_align4ff;

		if ('alpha' in hArg) this.cntBtn.children.forEach(e=> e.alpha = this.cnt.alpha);

		this.set_ch_in(hArg);
		this.set_ch_out(hArg);

		return this.drawBack(hArg);
	}
	private set_ch_in(hArg: HArg) {
		const ins = hArg.in_style;
		if (! ins) return;
		const cis = TxtStage.getChInStyle(ins);
		if (! cis) throw `存在しないin_style【${ins}】です`;
		this.ch_in_style = ins;
		this.ch_in_join = cis.join;
	}
	private	ch_in_style		= '';
	private	ch_in_join		= true;

	private set_ch_out(hArg: HArg) {
		const outs = hArg.out_style;
		if (! outs) return;
		const cos = TxtStage.getChOutStyle(outs);
		if (! cos) throw `存在しないout_style【${outs}】です`;
		this.ch_out_style = outs;
	}
	private	ch_out_style	= '';

	private drawBack(hArg: HArg): boolean {
		if ('back_clear' in hArg) {
			if (CmnLib.argChk_Boolean(hArg, 'back_clear', false)) {
				this.b_color = 0x000000;
				this.b_alpha = 0;
				this.b_alpha_isfixed = false;
				this.b_pic = '';
			}
			return false;
		}

		this.b_alpha = CmnLib.argChk_Num(hArg, 'b_alpha', this.b_alpha);
		this.b_alpha_isfixed = CmnLib.argChk_Boolean(hArg, 'b_alpha_isfixed', this.b_alpha_isfixed);
		const alpha = (this.b_alpha_isfixed
			? 1
			: Number(TxtLayer.val.getVal('sys:TextLayer.Back.Alpha'))
		) *this.b_alpha;
		if (hArg.b_pic) {
			if (this.b_pic != hArg.b_pic) {
				this.b_pic = hArg.b_pic;
				if (this.b_do) {
					this.cnt.removeChild(this.b_do);
					this.b_do.destroy();
				}
				return GrpLayer.csv2Sprites(this.b_pic, this.cnt, sp=> {
					this.b_do = sp;
					sp.name = 'back(pic)';
					sp.visible = (alpha > 0);
					sp.alpha = alpha;
					//CmnLib.adjustRetinaSize(this.b_pic, sp);
					this.txs.setSize(sp.width, sp.height);
						// ちなみに左上表示位置は本レイヤと同じ
					this.cnt.setChildIndex(sp, 0);
					TxtLayer.main.resume();
				});
			}
		}
		else if ('b_color' in hArg) {
			this.b_color = parseInt(hArg.b_color || '0');
			if (this.b_do) {
				this.cnt.removeChild(this.b_do);
				this.b_do.destroy();
			}
			this.b_pic = '';	// 忘れずクリア
			const grp = this.b_do = new Graphics;
			grp.name = 'back(color)';
			grp.beginFill(this.b_color);
			grp.lineStyle(undefined);
			grp.drawRect(0, 0, this.infTL.$width, this.infTL.$height);
			grp.endFill();
			this.cnt.addChildAt(grp, 0);
			//cacheAsBitmap = true;	// これを有効にするとスナップショットが撮れない？？
		}

		if (this.b_do) {
			this.b_do.visible = (alpha > 0);
				// 透明の時は表示しない。こうしないと透明テキストレイヤ下のボタンが
				// 押せなくなる（透明だが塗りがあるという扱いなので）
			this.b_do.alpha = alpha;
		}

		return false;
	}
	chgBackAlpha(g_alpha: number): void {
		const alpha = this.b_alpha_isfixed
			? this.b_alpha
			: g_alpha * this.b_alpha;
		if (this.b_do instanceof Graphics) {
			if (this.b_do) {
				this.cnt.removeChild(this.b_do);
				this.b_do.destroy();
			}
			const grp = this.b_do = new Graphics;
			grp.name = 'back(color)';
			grp.beginFill(this.b_color);
			grp.lineStyle(undefined);
			grp.drawRect(0, 0, this.infTL.$width, this.infTL.$height);
			grp.endFill();
			this.cnt.addChildAt(grp, 0);
			//cacheAsBitmap = true;	// これを有効にするとスナップショットが撮れない？？
		}
		if (this.b_do) {
			this.b_do.visible = (alpha > 0);
				// 透明の時は表示しない。こうしないと透明テキストレイヤ下のボタンが
				// 押せなくなる（透明だが塗りがあるという扱いなので）
			this.b_do.alpha = alpha;
		}
	}

	private	setFfs(hArg: HArg) {
		if ('noffs' in hArg) {
			this.strNoFFS = hArg.noffs ?? '';
			this.regNoFFS = new RegExp(`[　${this.strNoFFS}]`);
		}
		if (! ('ffs' in hArg)) return;

		this.ffs = hArg.ffs ?? '';
		if (this.ffs == '') {
			this.fncFFSStyle = ()=> '';
			this.fncFFSSpan = ch=> ch;
		}
		else {
			this.fncFFSStyle = ch=> this.regNoFFS.test(ch)
				? ''
				: ` font-feature-settings: ${this.ffs};`;
			this.fncFFSSpan = ch=> this.regNoFFS.test(ch)
				? ch
				: `<span style='font-feature-settings: ${this.ffs};'>${ch}</span>`;
		}
	}
	private	ffs	= '';
	private	fncFFSStyle	= (_ch: string)=> '';
	private	fncFFSSpan	= (ch: string)=> ch;
	private	strNoFFS	= '';
	private	regNoFFS	= new RegExp('[　]');
	// Safariが全体に「font-feature-settings」した後、特定文字の「font-feature-settings: initial;」を受け付けてくれないのでわざわざ一つずつ指定


	static	chgDoRec(doRec: boolean) {
		TxtLayer.rec = doRec
			? (tx: string)=> tx
			: (tx: string)=> `<span class='offrec'>${tx}</span>`;
				// 囲んだ領域は履歴で非表示
	}
	static	rec = (tx: string)=> tx;

	isCur	= false;
	private ruby_pd: (v: string, l: number)=> string = ()=> '';
	private mkStyle_r_align(ch: string, rb: string, r_align: string): string {
		if (! r_align) return '';

		const len = ch.length *2;
		if (len -rb.length < 0) return ` style='text-align: ${r_align};'`;

		let st = '';
		switch (r_align) {
		case 'justify':
			st = this.ruby_pd('0', len);	break;
		case '121':
			st = this.ruby_pd(`calc(${(len -rb.length) /(rb.length *2)}em)`, len);	break;
		case 'even':
			st = this.ruby_pd(`calc(${(len -rb.length) /(rb.length +1)}em)`, len);	break;
		case '1ruby':
			st = this.ruby_pd('1em', len);	break;
		default:
			st = `text-align: ${r_align};`;
		}
		return ` style='${st}'`;
	};
	private	r_align	= '';
	private mkStyle_r_align4ff(ch: string, rb: string, r_align: string): string {
		if (! r_align) return '';

		const len = ch.length *2;
		if (len -rb.length < 0) return ` style='text-align: ${r_align};'`;

		let st = '';
		switch (r_align) {
			case 'left':	st = `ruby-align: start;`;	break;
			case 'center':	st = `ruby-align: center;`;	break;
			case 'right':	// エレガントにサポートできていない
				st = `ruby-align: start;`;	break;
			case 'justify':	st = `ruby-align: space-between;`;	break;
			case '121':		st = `ruby-align: space-around;`;	break;
			case 'even':
				const ev = (len -rb.length) /(rb.length +1);
				st = `ruby-align: space-between; `+
				(this.txs.tategaki
					? `padding-top: ${ev}em; padding-bottom: ${ev}em;`
					: `padding-left: ${ev}em; padding-right: ${ev}em;`);
				break;
			case '1ruby':	st = `ruby-align: space-between; `+
				(this.txs.tategaki
					? `padding-top: 1em; padding-bottom: 1em;`
					: `padding-left: 1em; padding-right: 1em;`);
				break;
			default:		st = `text-align: ${r_align};`;
		}
		return ` style='${st}'`;
	}

	tagCh(text: string): void {this.rbSpl.putTxt(text);}
	private	needGoTxt = false;
	private	putCh	: IPutCh = (ch, ruby)=> {
		if (TxtLayer.cfg.oCfg.debug.putCh) console.log(`🖊 文字表示 text:\`${ch}\` ruby:\`${ruby}\` name:\`${this.name}\``);

		const a_ruby = ruby.split('｜');
		let add_htm = '';
		const isSkip = TxtLayer.evtMng.isSkipKeyDown();
		switch (a_ruby.length) {
		case 1:		// 字or春《はる》
			this.needGoTxt = true;
			if (ch == '\n') {
				if (this.aSpan_bk) {
					add_htm = this.aSpan_bk.slice(-1)[0];
					this.autoCloseSpan();
					this.aSpan.push(TxtLayer.rec('<br/>'));
					this.aSpan.push(add_htm);	// ここでaSpan末尾に追加しないと続かない
					this.aSpan_bk = this.aSpan;
					this.aSpan = [];
					return;	// breakではない
				}
				if (this.firstCh) {	// １文字目にルビが無い場合は不可視ルビで、行揃え
					this.firstCh = false;
					add_htm = '<ruby>　<rt>　</rt></ruby><br/>';
				}
				else {
					add_htm = '<br/>';
				}
				break;
			}
			if (this.firstCh) {	// １文字目にルビが無い場合は見えないルビで、行揃え
				this.firstCh = false;
				if (ruby == '') ruby = '　';
			}
			add_htm = this.tagCh_sub(ch, ruby, isSkip, this.r_align);
			break;

		case 2:		// 《grp｜{"id":"break","pic":"breakline"}》
			switch (a_ruby[0]) {
			// ルビ揃え指定と同時シリーズ
			case 'start':	// 初期値
			case 'left':	//（肩付き）先頭親文字から、ルビ間は密着
			case 'center':	//（中付き）センター合わせ、〃
			case 'right':	//（右／下揃え）末尾親文字から、〃
			case 'justify':	//（両端揃え）先頭から末尾親文字間に、ルビ間は均等にあける
			case '121':		//（1-2-1(JIS)）ルビの前後を比率1、ルビ間を比率2であける
			case 'even':	//（均等アキ）ルビの前後、ルビ間も均等にあける
			case '1ruby':	//（1ルビ文字アキ）ルビの前後をルビ一文字空け、ルビ間は均等にあける
				this.firstCh = false;
				this.needGoTxt = true;
				add_htm = this.tagCh_sub(ch, a_ruby[1], isSkip, a_ruby[0]);
				break;

			case 'gotxt':
			{
				this.autoCloseSpan();
				if (this.isCur) TxtLayer.recText(
					this.aSpan.join('')
					.replace(/^<ruby>　<rt>　<\/rt><\/ruby>(<br\/>)+/, '')
						// 前方の空行をtrim
					.replace(/style='(anim\S+ \S+?;\s*)+/g, `style='`)
					.replace(/( style=''| data-(add|arg|cmd)='.+?'|\n+|\t+)/g, '')
					.replace(/class='sn_ch .+?'/g, `class='sn_ch'`)
						// 不要情報削除
					.replace(/class='offrec'/g, `style='display: none;'`)
						// 囲んだ領域は履歴で非表示
					.replace(/`/g, '\\`')
						// JSON対策
				);

				if (! this.needGoTxt) return;	// breakではない
				this.txs.goTxt(	// クリック待ち用ダミー空白を追加
					[...this.aSpan, this.tagCh_sub('　', '', false, '')]
				);
				this.needGoTxt = false;
				this.cumDelay = 0;
				return;	// breakではない
			}

			case 'add':	// 文字幅を持たない汎用的な命令（必ずadd_closeすること）
			{
				const o = JSON.parse(a_ruby[1]);
				o.style = o.style ?? '';
				this.beginSpan(o);
				if (this.aSpan_bk) this.autoCloseSpan();
				else {
					if (isSkip) this.cumDelay = 0;
					const wait = o.wait ?? -1;
					const sn_ch = (wait == 0)
						? ''
						: ` sn_ch_in_${this.ch_in_style}`;
					const ad = (wait < 0)
						? ''
						: ` animation-duration: ${wait}ms;`
					this.aSpan.push(`<span class='sn_ch${sn_ch}' style='animation-delay: ${this.cumDelay}ms;${ad} ${o.style}' data-add='${JSON.stringify(o)}'>`);	// "を"にしてはいけない
				}
				this.aSpan_bk = this.aSpan;
				this.aSpan = [];
			}
				return;	// breakではない
			case 'add_close':
				this.autoCloseSpan();
				return;	// breakではない

			case 'grp':	//	画像など 《grp｜{"id":"break","pic":"breakline"}》
				this.needGoTxt = true;
			{
				if (isSkip) this.cumDelay = 0;
				const arg = (a_ruby[1] ?a_ruby[1].slice(0, -1) +',' :`{`) +`"delay": ${this.cumDelay}}`;
				if (this.ch_in_join) this.cumDelay += (TxtLayer.doAutoWc) ?0 :LayerMng.msecChWait;

				const o = JSON.parse(arg);
				o.style = o.style ?? '';
				if (! ('id' in o)) o.id = this.aSpan.length;
				if (o.id == 'break') {this.txs.dispBreak(o.pic); return;}
					// breakではない
				add_htm = `<span data-cmd='grp' data-id='${o.id}' data-arg='${arg}'`;
				const wait = o.wait ?? -1;
				const sn_ch = (wait == 0)
					? ''
					: ` sn_ch_in_${this.ch_in_style}`;
				const ad = (wait < 0)
					? ''
					: ` animation-duration: ${wait}ms;`
				add_htm += ` class='sn_ch${sn_ch}' style='animation-delay: ${this.cumDelay}ms;${ad} ${o.style}' data-add='{"ch_in_style":"${this.ch_in_style}", "ch_out_style":"${this.ch_out_style}"}'>　</span>`;
				if (this.firstCh) {	// １文字目にルビが無い場合は不可視ルビで、行揃え
					this.firstCh = false;
					add_htm = `<ruby>${add_htm}<rt>　</rt></ruby>`;
				}
				if (this.aSpan.slice(-1)[0] == add_htm) return;	// breakではない
			}
				break;

			case 'del':
				const id_del = a_ruby[1];
				if (id_del != 'break') throw '文字レイヤdelコマンドは、現在id=breakのみサポートします';

				TxtStage.delBreak();
				return;	// breakではない

			case 'span':
				this.autoCloseSpan();
				this.needGoTxt = true;
			{
				// style, in_style
				const o = JSON.parse(a_ruby[1]);
			//	o.style = o.style ?? '';
				this.beginSpan(o);
				if (! o.style) return;	// breakではない

				if (isSkip) this.cumDelay = 0;
				const wait = o.wait ?? -1;
				const sn_ch = (wait == 0)
					? ''
					: ` sn_ch_in_${this.ch_in_style}`;
				const ad = (wait < 0)
					? ''
					: ` animation-duration: ${wait}ms;`
				this.aSpan.push(`<span class='sn_ch${sn_ch}' style='animation-delay: ${this.cumDelay}ms;${ad} ${o.style}' data-add='{"ch_in_style":"${this.ch_in_style}", "ch_out_style":"${this.ch_out_style}"}'>`);
				this.aSpan_bk = this.aSpan;
				this.aSpan = [];
			}
				return;	// breakではない

			case 'link':
				this.autoCloseSpan();
				this.needGoTxt = true;
			{
				// b_color, b_alpha, fn, label
				const o = JSON.parse(a_ruby[1]);
				o.style = o.style ?? '';
				this.beginSpan(o);
				if (isSkip) this.cumDelay = 0;
				const wait = o.wait ?? -1;
				const sn_ch = (wait == 0)
					? ''
					: ` sn_ch_in_${this.ch_in_style}`;
				const ad = (wait < 0)
					? ''
					: ` animation-duration: ${wait}ms;`
				this.aSpan_link = ` data-cmd='link' data-arg='${a_ruby[1]}'`;
				this.aSpan.push(`<span${this.aSpan_link} class='sn_ch${sn_ch}' style='animation-delay: ${this.cumDelay}ms;${ad} ${o.style}' data-add='{"ch_in_style":"${this.ch_in_style}", "ch_out_style":"${this.ch_out_style}"}'>`);
				this.aSpan_bk = this.aSpan;
				this.aSpan = [];
			}
				return;	// breakではない

			case 'endlink':
				this.needGoTxt = true;
				if (this.aSpan_bk) {
					const len = this.aSpan.length;
					for (let i=0; i<len; ++i) this.aSpan[i] = this.aSpan[i].replace(/ data-cmd='linkrsv'/, this.aSpan_link);
				}
				this.autoCloseSpan();	return;	// breakではない

			default:	// ルビあり文字列
				this.needGoTxt = true;
				add_htm = this.tagCh_sub(ch, ruby, isSkip, this.r_align);
			}
			break;

		case 3:		// 《tcy｜451｜かし》
			this.firstCh = false;
			this.needGoTxt = true;
			switch (a_ruby[0]) {
			case 'tcy':	// ルビ付き縦中横
			{
				if (TxtLayer.val.doRecLog()) this.page_text += ch
				+(ruby ?`《${ruby}》` :'');

				// text-combine-upright: all;			縦中横
				// -webkit-text-combine: horizontal;	縦中横(Safari)
				const tx = a_ruby[1];
				const id_tcy = (tx.length > 1)
					? (this.aSpan.length +1)	// 0にならないよう +1
					: '';
				const rb = CmnLib.isSafari
					? a_ruby[2].replace(/[A-Za-z0-9]/g, s=> String.fromCharCode(s.charCodeAt(0) + 65248))
						// 英数字を全角に(Safariで縦中横ルビが半角文字だと、
						// 選択矩形が横倒しになる不具合対策)
					: a_ruby[2];
				if (isSkip) this.cumDelay = 0;
				const rs = this.mkStyle_r_align(tx, rb, this.r_align);
				add_htm = rb
					? (this.aSpan_bk
						? (`<ruby><span data-tcy='${id_tcy}' style='
								text-combine-upright: all;
								-webkit-text-combine: horizontal;
								${this.fncFFSStyle(tx)}
							' data-add='{"ch_in_style":"${this.ch_in_style}", "ch_out_style":"${this.ch_out_style}"}' data-cmd='linkrsv'>${tx}</span>`
							+`<rt${rs}>${rb}</rt></ruby>`)
						: (`<span class='sn_ch sn_ch_in_${this.ch_in_style}' style='animation-delay: ${this.cumDelay}ms;'>`
							+`<ruby><span data-tcy='${id_tcy}' style='
									text-combine-upright: all;
									-webkit-text-combine: horizontal;
									${this.fncFFSStyle(tx)}
								' data-add='{"ch_in_style":"${this.ch_in_style}", "ch_out_style":"${this.ch_out_style}"}'>${tx}</span>`
								+`<rt${rs}>${rb}</rt></ruby>`
						+`</span>`))
					: (this.aSpan_bk
						? (`<span data-tcy='${id_tcy}' style='
							text-combine-upright: all;
							-webkit-text-combine: horizontal;
							${this.fncFFSStyle(tx)}
						' data-add='{"ch_in_style":"${this.ch_in_style}", "ch_out_style":"${this.ch_out_style}"}' data-cmd='linkrsv'>${tx}</span>`)
						: `<span data-tcy='${id_tcy}' style='
							text-combine-upright: all;
							-webkit-text-combine: horizontal;
							animation-delay: ${this.cumDelay}ms;
							height: 1em;
							${this.fncFFSStyle(tx)}
						' class='sn_ch sn_ch_in_${this.ch_in_style}' data-add='{"ch_in_style":"${this.ch_in_style}", "ch_out_style":"${this.ch_out_style}"}'>${tx}</span>`);
				if (this.ch_in_join) this.cumDelay += (TxtLayer.doAutoWc)
					? TxtLayer.hAutoWc[ch.charAt(0)] ?? 0
					: LayerMng.msecChWait;
			}
				break;

			default:
				throw `異常な値です putCh(text: ${ch}, ruby: ${ruby})`;
			}
			break;
		}
		this.aSpan.push(TxtLayer.rec(add_htm));
	}
	private tagCh_sub(ch: string, ruby: string, isSkip: boolean, r_align: string): string {
		if (ch == ' ') ch = '&nbsp;';
		if (TxtLayer.val.doRecLog()) this.page_text += ch
		+(ruby ?`《${ruby}》` :'');

		let add_htm = '';
		const rs = this.mkStyle_r_align(ch, ruby, r_align);
		if (isSkip) this.cumDelay = 0;
		add_htm = ruby
			? (this.aSpan_bk
				? `<ruby style='${this.fncFFSStyle(ch)}' data-add='{"ch_in_style":"${this.ch_in_style}", "ch_out_style":"${this.ch_out_style}"}' data-cmd='linkrsv'>${ch}<rt${rs}>${ruby}</rt></ruby>`
				: (`<span class='sn_ch sn_ch_in_${this.ch_in_style}' style='animation-delay: ${this.cumDelay}ms;${this.fncFFSStyle(ch)}'>`
					+`<ruby data-add='{"ch_in_style":"${this.ch_in_style}", "ch_out_style":"${this.ch_out_style}"}'>${ch}<rt${rs}>${ruby}</rt></ruby>`
				+`</span>`))
			: (this.aSpan_bk
				? this.fncFFSSpan(ch)
				: `<span class='sn_ch sn_ch_in_${this.ch_in_style}' style='animation-delay: ${this.cumDelay}ms;${this.fncFFSStyle(ch)}' data-add='{"ch_in_style":"${this.ch_in_style}", "ch_out_style":"${this.ch_out_style}"}'>${ch}</span>`);
		if (this.ch_in_join) this.cumDelay += TxtLayer.doAutoWc
			? TxtLayer.hAutoWc[ch.charAt(0)] ?? 0
			: LayerMng.msecChWait;

		return add_htm;
	}
	private cumDelay	= 0;
	private firstCh		= true;
	private aSpan		: string[]		= [];
	private aSpan_bk	: any[] | null	= null;
	private aSpan_link	= '';

	private	hSpanBk = {
		ch_in_style	: '',
		ch_out_style: '',
		r_align		: '',
	};
	private beginSpan(o :any) {
		this.hSpanBk.ch_in_style = this.ch_in_style;
		this.set_ch_in(o);
		this.hSpanBk.ch_out_style = this.ch_out_style;
		this.set_ch_out(o);
		this.hSpanBk.r_align = this.r_align;
		if ('r_align' in o) this.r_align = o.r_align;
	}
	private autoCloseSpan() {
		if (! this.aSpan_bk) return;

		this.aSpan_bk.push(this.aSpan, '</span>')
		this.aSpan = Array.prototype.concat.apply([], this.aSpan_bk);
		this.aSpan_bk = null;

		this.set_ch_in({in_style: this.hSpanBk.ch_in_style});
		this.set_ch_out({out_style: this.hSpanBk.ch_out_style});
		this.r_align = this.hSpanBk.r_align;
	}

	readonly click = ()=> {
		if (! this.cnt.interactiveChildren || ! this.cnt.visible) return true;
		return this.txs.skipChIn();	// true is stay
	}

	clearText(): void {
		const txs = this.txs;
		this.txs = this.txs.passBaton();
		txs.destroy();

		this.cumDelay = 0;
		this.firstCh = true;
		this.aSpan = [];
		this.aSpan_bk = null;
		this.page_text = '';
		TxtLayer.recText('', true);
	}
	private	page_text	= '';
	get pageText() {return this.page_text}

	get enabled() {return this.cnt.interactiveChildren}
	set enabled(e) {this.cnt.interactiveChildren = e}

	addButton(hArg: HArg): boolean {
		hArg.key = `btn=[${this.cntBtn.children.length}] `+ this.name;
		const btn = new Button(TxtLayer.main, TxtLayer.evtMng, hArg);
		btn.name = JSON.stringify(hArg);
		this.cntBtn.addChild(btn);
		return btn.isStop;
	}


	clearLay(hArg: HArg): void {
		super.clearLay(hArg);

		this.clearText();
		for (const c of this.cntBtn.removeChildren()) c.removeAllListeners().destroy();	// removeAllListeners()はマウスオーバーイベントなど。クリックは別
	}
	readonly record = ()=> Object.assign(super.record(), {
		enabled	: this.enabled,
		r_align	: this.r_align,

		// バック
		b_do	: (this.b_do == null)
					? null
					: (this.b_do instanceof Sprite ?'Sprite' :'Graphics'),
		b_pic	: this.b_pic,
		b_color	: this.b_color,
		b_alpha	: this.b_alpha,
		b_alpha_isfixed	: this.b_alpha_isfixed,

		txs		: this.txs.record(),
		ffs		: this.ffs,
		strNoFFS: this.strNoFFS,

		btns	: this.cntBtn.children.map(btn=> btn.name),
	});
	playback(hLay: any, fncComp: undefined | {(): void} = undefined): boolean {
		super.playback(hLay);
		this.enabled	= hLay.enabled;
		this.r_align	= hLay.r_align;
		this.cvsResize();

		// バック
		this.b_alpha			= hLay.b_alpha;
		this.b_alpha_isfixed	= hLay.b_alpha_isfixed;
		let ret = this.drawBack(
			(hLay.b_do)
			? (hLay.b_do == 'Sprite' ?{b_pic: hLay.b_pic} :{b_color: hLay.b_color})
			: {b_pic: ''}
		);

		this.setFfs(hLay);
		this.txs.playback(hLay.txs);

		// addButton(hArg: HArg): boolean
		const aBtn: string[] = hLay.btns;
		aBtn.forEach(v=> ret = ret || this.addButton(JSON.parse(v)));

		if (fncComp != undefined) fncComp();

		return ret;
	}

	snapshot(rnd: Renderer, re: ()=> void) {
		rnd.render(this.cnt, undefined, false);
		this.txs.snapshot(rnd, re);
	}
	snapshot_end() {this.txs.snapshot_end();}

	dump(): string {
		let aPixiObj: string[] = [];
		const len = this.cnt.children.length;
		for (let i=0; i<len; ++i) {
			const e = this.cnt.children[i];
			const cls = (e instanceof Sprite) ?"Sprite" :(
				(e instanceof Graphics) ?"Graphics" :(
					(e instanceof Container) ?"Container" :"?"
				)
			);
			aPixiObj.push(`{"class":"${cls}", "name":"${e.name}", "alpha":${e.alpha || 1}, "x":${e.x}, "y":${e.y}, "visible":"${
				e.visible}"}`);
		}
		return super.dump() +`, "enabled":"${this.enabled}", ${this.txs.dump()
		}, "b_pic":"${this.b_pic}", "b_color":"${this.b_color
		}", "b_alpha":${this.b_alpha}, "b_alpha_isfixed":"${this.b_alpha_isfixed
		}", "b_width":${this.infTL.$width}, "b_height":${this.infTL.$height
		}, "pixi_obj":[${aPixiObj.join(',')}]`;
	}

}
