module y4d_draw.scenariodraw;

private import std.ctype; // tolower
private import std.utf;
private import std.string;

private import y4d_draw.drawbase;
private import y4d_draw.texturevector;
private import y4d_draw.fontrepository;
private import y4d_draw.screen;
private import y4d_aux.lineparser;
private import y4d_aux.widestring;
private import ytl.exception;

private import yamalib.draw.textdraw;
private import yamalib.log.log;

///	シナリオ描画クラス
/**
	このモジュールはでかいので、標準では読み込まず。
	(独立モジュール。)

	private import y4d_draw.scenariodraw;<BR>

	とやって読み込んでください。

	@todo : 作りかけ(ぼちぼち作ってます)
*/
class ScenarioDraw {

}


///	一文字ずつテキスト描画するためのクラス。
/**
	このモジュールはでかいので、標準では読み込まず。
	(独立モジュール。)

	private import y4d_draw.scenariodraw;<BR>

	とやって読み込んでください。

	@todo : 作りかけ(ぼちぼち作ってます)
*/
class ScenarioTextDraw {

	///	フォントリポジトリ
	/**
		このフォントリポジトリを通じて、フォントローダーを
		事前に食わせておくこと。
	*/

	void setTextDrawContext(TextDrawContext v) { textdrawcontext = v; }

	void setTextOffset(int pos) { textdrawcontext.setTextOffset(pos); }
	int getTextOffset() { 
		if ( backLogMode ) {
			return this.textPosOrg;
		}
		return textdrawcontext.getTextOffset(); 
	}
	int getHeadTextOffset() { 
		if ( backLogMode ) {
			return this.headerTextPosOrg;
		}
		return textdrawcontext.getHeadTextOffset(); 
	}

	FontRepository getFontRepository() { return fontrep; }
	FontRepository getRubiFontRepository() { return rubifontrep; }
	FontRepository getBackLogFontRepository() { return backlogFontRep; }

	///	設定されている文字列をフォントリポジトリからTextureをかきあつめる
	/**
		1.事前にgetFontRepositoryでフォントローダーを設定しておくこと。
		2.setTextで文字列を設定しておくこと。
	    3.ルビを使用する場合,ルビ用のフォントを食わせておくこと

		このupdateText後は、
		getDrawCharで更新されたテクスチャ配列が取得できるようになる。

		解析上のエラーがあれば例外がthrowされる
	*/
	bool	updateText(bool back=false) {
		drawchar = null;
		unknownTag = null;
		selectTagInfo = null;
		SelectTagInfo.pos = int.min;

		// <HR> まで探す
		if ( !back ) {
			text = textdrawcontext.getNextText();
		} else {
			text = textdrawcontext.getPreText();
		}
		
		bool vartical = false;
		
		if (!text) return false;
		int pos = 0; 		// 解析ポジション
		int width,height;	//	全体のサイズ
		float l_height=0;	//	このラインの高さ
		Point drawpos;		//	現在の描画位置
		Color4ub color;		//	色。ディフォルトではr,g,b,a=255
		int line_start;		//	行の開始文字のdrawcharのindex
		wchar[][] here_tag;	//	この場所に埋められているタグ

		do {
			wchar c = text[pos++];

			if (c=='<') {
				//	タグの取得
				//	前方へ '>'をサーチ
				int startpos = pos;
				while (true) {
					if (pos >= text.length) {
						throw new y4d_error(this,"タグが閉じられていない");
					}
					wchar wc = text[pos];
					if (wc=='>') break;
					pos ++;
				}
				wchar[] tag = text[startpos..pos];
				pos++;

				//	どのタグに該当するかを調べる
				static const char[][] tags = [
					"br", 		// 改行
					"space", 	// 空白行
					"rubi",		// ルビ
					"select",	// 選択肢
					"font",		// フォント開始
					"/font"		// フォント終了
				];

				lineparser.setLine(toMBS(tag));
				char[] cc = lineparser.getStr();

				// タグは小文字化して探査しよう
				foreach (inout char ch; cc) {
					ch = std.ctype.tolower(ch);
				}

				int found = -1; // not found marker
				for(int i=0;i<tags.length;++i){
					if (cc == tags[i]) { found = i; break; }
				}
				if (found == -1) {
				//	認識できないタグなのでタグリストに追加
					UnknownTagInfo uti;
					uti.tag = tag;
					uti.pos = drawchar.length;
					unknownTag ~= uti; 
					continue;
				}

				//	このタグの処理
				switch(found) {
				case 0: // <BR> : 行送り処理
					
					if (vartical) {
						if (drawpos.y == 0) break;
						drawpos.y = 0;
						drawpos.x -= l_height;
						l_height = 0;
						line_start = pos;
						adjust = false;
					} else {
						if (drawpos.x == 0) break;
						drawpos.x = 0;
						drawpos.y += l_height;
						l_height = 0;
						line_start = pos;
						adjust = false;
					}
					break;

				case 1:	//<space x>
					int space = cast(int) lineparser.getNum(-1);
					drawpos.x = 0;
					drawpos.y += (l_height * space) / 2;
					l_height = 0;
					line_start = pos;
					adjust = false;
					break;

				case 2:	// <rubi,x,str>
					Log.print("RUBI TAG");
					if (!m_useRubi) {
						break;
					}
					
					int backNum = cast(int) lineparser.getNum(-1);
					int search = 0;
					wchar[] moji = toWCS( lineparser.getStr() );

					// 指定チェック
					if ( (backNum == -1) || (moji.length == 0) || (drawchar.length < backNum) ) break;

					// ルビ文字テクスチャの生成
					Texture[] tt;
					int i = 0, j = 0, k = 0;
					for(i = 0; i < moji.length; ++i) {
						tt ~= getRubiFontRepository().getTexture( moji[i] );
					}
					
					// ルビの大きさを取得
					int tWidth = cast(int) tt[0].getWidth();
					int tHeight = cast(int) tt[0].getHeight();

					// １文字あたりのルビ付帯数
					float perChar = moji.length / backNum;
					float rubiRest = perChar;
					int index = drawchar.length - backNum;

					for(i=0; i < tt.length && index < drawchar.length; ++i) {
						for (; rubiRest >= 1.0 && j < tt.length; j++,rubiRest -= 1.0) {
							DrawChar dc = new DrawChar();
							dc.texture = tt[j];
							drawchar[index].rubi ~= dc;
						}
						index++;
						rubiRest += perChar;
					}
					if (j < tt.length) {
						for(i = j; i < tt.length; ++i) {
							DrawChar dc = new DrawChar();
							dc.texture = tt[i];
							drawchar[index-1].rubi ~= dc;
						}
					}

					// 均等割付を行う
					index = drawchar.length - backNum;

					// まず連結したルビ文字列の長さを算出
					int sumWidthRubi = 0;
					int sumWidth = 0;
					for(j = index; j < drawchar.length; ++j) {
						sumWidth += cast(int)drawchar[j].texture.getWidth();
						foreach (DrawChar dc; drawchar[j].rubi) {
							sumWidthRubi += cast(int)dc.texture.getWidth();
						}
					}

					int moji_len = moji.length;
					int dx,ox;
					// 一文字あたりにあける間隔
					int widthPerChar = 0;
					// もし、ルビ文字の数が、親文字に対して２倍でなければ半角分ずらす必要あり
					bool posAdjust = cast(bool) (moji.length != backNum*2);

					if (sumWidth >= sumWidthRubi) {
						// ルビ文字列の長さより、親文字のほうが大きい
						widthPerChar = sumWidth - sumWidthRubi;
						ox = sumWidth / moji_len;
						dx = cast(int) drawchar[index].pos.x;
					} else {
						// ルビ文字列の長さより、親文字のほうが小さい
						widthPerChar = sumWidthRubi - sumWidth;
						dx = widthPerChar / 2;
						//ox = cast(int)(drawchar[index].rubi[0].texture.getWidth()/2);
						ox = 0;
					}
					
					// ルビ親文字が、ルビ中に改行されているかもしれない
					int newLinePos = -1;
					// 振り付け文字の最初のインデックス
					int target = drawchar.length - backNum;
					int baseY = cast(int) drawchar[target].pos.y;
					for ( i = 0; i < backNum; ++i, ++target) {
						if ( baseY != cast(int) drawchar[target].pos.y ) {
							// 描画位置が変更になっていれ改行やで...
							newLinePos = i;
							break;
						}
					} 
					
					// 親文字ごと
					for(i=index; i < drawchar.length; ++i) {
						if (sumWidth < sumWidthRubi && i > index) {
							// ルビ文字のほうが横幅をとるなら、前の文字のルビから、オフセット位置を算出
							DrawChar[] rubi = drawchar[i-1].rubi;
							ox = cast(int)(rubi[length-1].pos.x - drawchar[i].pos.x) + dx + tWidth;
						}
						
						if ( i - index == newLinePos ) {
							// ここで改行はいりやがった！
							dx = 0;
						}
						
						// ルビ一文字ごと
						for(j = 0; j < drawchar[i].rubi.length; ++j) {
	
							if (sumWidth >= sumWidthRubi) {
								// ルビ文字列の長さより、親文字のほうが大きい
								drawchar[i].rubi[j].pos.x = dx;
								if (posAdjust) {
									drawchar[i].rubi[j].pos.x += tWidth / 2;
								}
								drawchar[i].rubi[j].pos.y = drawchar[i].pos.y;
								dx += ox;
								
								debug {
									Log.print("i = %s,j = %s,x = %s,ox = %s\n",i,j,drawchar[i].rubi[j].pos.x,ox);
								}
															
							} else {
								// ルビ文字列の長さより、親文字のほうが小さい
								drawchar[i].rubi[j].pos.x = drawchar[i].pos.x - dx + ox;
								drawchar[i].rubi[j].pos.y = drawchar[i].pos.y;
								int rubiWidth = cast(int)drawchar[i].rubi[j].texture.getWidth();
								ox += rubiWidth;
							}

							if (adjust) {
								drawchar[i].rubi[j].pos.y -= tHeight;
							}
						}
					}
					
					// 改行ルビ付けなら、前の行にすでにルビがあったかどうか
					// 行頭１文字で明示改行したら、これは正しく動作しない
					bool bPreLineRubied = false;
					if ( -1 != newLinePos ) {
						int preLineEnd = drawchar.length - backNum - 1;
						int preY = cast(int) drawchar[preLineEnd].pos.y;
						
						// 前行分についてルビが振られているか調べる
						while (preLineEnd >= 0 && drawchar[preLineEnd].pos.y == preY) {
							if ( drawchar[preLineEnd].rubi.length != 0 ) {
								bPreLineRubied = true;
								break;
							}
							
							preY = cast(int) drawchar[preLineEnd].pos.y;
							--preLineEnd;
						}
					}
					
					Log.print("bPreLineRubied %s", bPreLineRubied);
					
					// 行間調整
					int end = drawchar.length - 1;
					int ly = cast(int)drawchar[end].pos.y;
					// 改行ルビであれば、前の分と今の行、２つ分のスペースを取る必要がある
					int spaceRate = ((-1!=newLinePos) && !bPreLineRubied) ? 2 : 1;
					
					if (drawchar[index].pos.y != 0 && !adjust) {
						
						int loopCnt = 0;
						bool rubiAdust = false;
						while (end >= 0 && (drawchar[end].pos.y == ly || end >= index)) {
							
							// もしルビ中改行であったら、改行の後分だけいい
							if ( -1 != newLinePos ) {
								if ( loopCnt == backNum - newLinePos ) {
									rubiAdust = true;
								}
							} 
							
							if ( rubiAdust ) {

								// ルビ改行対応、前行の処理
								if ( bPreLineRubied ) {
									// 前の行にもルビがあって、すでに行間はあけられている
									foreach ( inout DrawChar dc; drawchar[end].rubi ) {
										dc.pos.y -= tHeight;
									}
								}
								
							} else {
								ly = cast(int)drawchar[end].pos.y;
								drawchar[end].pos.y += (tHeight * spaceRate);
								
								// 改行ルビで前行の行間処理がされていなければ、自分の位置もそれ分、
								// 追加でさげなければならない
								if ( ((-1!=newLinePos) && !bPreLineRubied) ) {
									foreach ( inout DrawChar dc; drawchar[end].rubi ) {
										dc.pos.y += tHeight;
									}
								}
							}
							end--;
							loopCnt++;
						}
						
						// 改行ルビで前の行が未調整なら、前の行の行間をさらに確保
						if ( -1 != newLinePos && !bPreLineRubied ) {
							end = index + (newLinePos-1);
							ly = cast(int)drawchar[end].pos.y;
							
							while (end >= 0 && drawchar[end].pos.y == ly) {
								ly = cast(int)drawchar[end].pos.y;
								drawchar[end].pos.y += tHeight;
								--end;
							}
						}
						
						drawpos.y += (tHeight * spaceRate);
						adjust = true;
						
					} else if(drawchar[index].pos.y == 0){
						// こっちは既に、この行にルビをつけるための行間が設定済み
						for(i = index; i < drawchar.length; ++i) {
							foreach (inout DrawChar dc; drawchar[i].rubi) {
								dc.pos.y -= tHeight;
							}
						}
					}
					break;
				
				case 3:	// <select,str,str>
					SelectTagInfo tagInfo;
					wchar[] text = toWCS( lineparser.getStr() );
					if (!text) continue; // あかんやん...
					tagInfo.nextSceneId = lineparser.getStr();
					if (!tagInfo.nextSceneId) continue;
					
					if (SelectTagInfo.pos < 0) {
						SelectTagInfo.pos = drawchar.length;
					}
					
					tagInfo.texture = getInnerFontRepository().getTexture(text);
					tagInfo.width = cast(int)tagInfo.texture.getWidth();
					tagInfo.height = cast(int)tagInfo.texture.getWidth();
					tagInfo.x = cast(int)drawpos.x;
					tagInfo.y = cast(int)drawpos.y;
					
					drawpos.x = 0;
					drawpos.y += tagInfo.height;
					
					selectTagInfo ~= tagInfo;
					break;
					
				case 4: // <font>
					printf("----------------- find font tag ------------------\n");
					float fSize = 1.0f;
					wchar[] strColor = "FFFFFF";

					while ( !lineparser.isEnd() ) {
						
						// 属性を走査する
						if ( lineparser.isMatch( "size=", false ) ) {
							// サイズ指定
							fSize = lineparser.getDecimal(1.0f);
						} else if ( lineparser.isMatch( "color=", false ) ) {
							// 色指定
							strColor = toWCS( lineparser.getStr() );
						} else {
							// 未知の属性
//							Log.printWarn("Invaild Font tag option %s", lineparser.getStr() );
							char[] str = lineparser.getStr();
							if ( find(str, "color=") >= 0 ) {
								 strColor = toWCS( str[length-7..length-1] );
							}
							
							break;
						}
					}

					// FontInfo配列に現在のフォントをスタック					
					FontInfo fi;
					fi.size = fSize;
					fi.color = strColor;
					fontInfos ~= fi;
					
					break;
					
				case 5: // </font>
					// pop
					
					// 明示的なスタックがなければ、無視する
					if (fontInfos.length >= 2) {
						fontInfos.length = fontInfos.length - 1; 
					} else {
						Log.printWarn("</font> appeared before <font>");
					}
				
					break;
				default:
					assert(false);
				}

				continue;
			}

			if (c == '\t') continue;
			if (c == '\n') continue;
			if (c == '\r') continue;
			
			wchar[1] test;
			test[0] = c;
		
			Texture t = getInnerFontRepository().getTexture( c );
			if (!t) continue; // なんだこの文字は
			
			// フォントサイズで拡縮した後のサイズ
			float charRate = fontInfos[length-1].size;
			float t_width  = t.getWidth() * charRate;
			float t_height = t.getHeight() * charRate;
			DrawChar dc = new DrawChar();
			dc.c = c;
			{
				if (vartical) {
					// 縦書き
					if ( (drawpos.y > textdrawcontext.width) && !isProhibition(c) ) {
						drawpos.x -= l_height;
						drawpos.y = 0;
						l_height = 0;
						line_start = pos;
						adjust = false;
					}
					
					dc.texture = t;
					dc.color = fontInfos[length-1].getColor4ub();	// フォントタグで指定されている色
					dc.size = charRate;	// フォントタグで指定されているサイズ
					dc.pos	 = drawpos;
	
					if (!here_tag){
						dc.tags  = here_tag;
						here_tag = null;
					}
	
					drawpos.y += (t_height * fontInfos[length-1].size);
					if (l_height < t_width){
					//	このラインのベース位置が変わるので、この差の分、
					//	加算する必要がある。
						float d = t_width - l_height;
						for(int i = line_start; i < drawchar.length; ++i){
							drawchar[i].pos.x -= d;
						}
						l_height = t_width;
					}
					
				} else {
					// 禁則文字ではなく、描画位置が指定範囲を超えていれば改行する
					if ( (drawpos.x > textdrawcontext.width) && !isProhibition(c) ) {
						drawpos.x = 0;
						drawpos.y += l_height;
						l_height = 0;
						line_start = pos;
						adjust = false;
					}
	
					dc.texture = t;
					dc.color = fontInfos[length-1].getColor4ub();	// フォントタグで指定されている色
					dc.size = charRate;	// フォントタグで指定されているサイズ
					dc.pos	 = drawpos;
	
					if (!here_tag){
						dc.tags  = here_tag;
						here_tag = null;
					}
	
					drawpos.x += (t_width * fontInfos[length-1].size);
					if (l_height < t_height){
					//	このラインのベース位置が変わるので、この差の分、
					//	加算する必要がある。
						float d = t_height - l_height;
						for(int i = line_start; i < drawchar.length; ++i){
							drawchar[i].pos.y += d;
						}
						l_height = t_height;
					}
				}

			}
			drawchar ~= dc;
		} while (pos < text.length);

		return true;
	}
	
	/// ルビを使用する
	void enableRubiText() {
		m_useRubi = true;
	}
	
	/// ルビを使用しない
	void disenableRubiText() {
		m_useRubi = false;
	}

	///	テキストの設定
	/**
		html風のタグも使える。設定したあとはupdateTextを呼び出すこと。
<PRE>
		BR : 改行
</PRE>

	*/
	void	setText(wchar[] text_) { text = text_; }
	
	/* バックログ位置を設定する */
	void	setBackLogMode(bool backMode) {
		if ( !backLogMode && backMode) {
			// on 要請
			textPosOrg = textdrawcontext.getTextOffset();
			headerTextPosOrg = textdrawcontext.getHeadTextOffset();
			textOrg = getText();
			backLogMode = true;
		} else if ( backLogMode && !backMode ) {
			// off 要請
			textdrawcontext.setTextOffset(textPosOrg);
			textdrawcontext.setHeadTextOffset(headerTextPosOrg);
			textdrawcontext.resetBackLog();
			backLogMode = false;
		}
	}

	///	テキストの取得
	/**
		設定してあったテキストの取得。ここで取得したものに追加して
		再度setTextを呼び出すなどしても良い。
	*/
	wchar[] getText() { 
		if ( backLogMode ) {
			return textOrg;
		}
		return text; 
	}

	///	描画するキャラクター実体
	static class DrawChar {
		Texture		texture;	//!< 	文字はテクスチャー
		float		size=1.0f;	//!<	基準値に比する倍率
		Color4ub	color;		//!<	描画する色
		Point		pos;		//!<	描画する文字の描画ポジション
		wchar[][]	tags;		//!<	その位置に書かれていたタグ(色指定・改行指定等は取り除かれる)
		DrawChar[]  rubi;		//!<    その文字に付帯するルビ文字
		wchar		c;			//!<	テクスチャにした文字
	}

	/// 未知のタグを格納する
	struct UnknownTagInfo {
		wchar[] tag;
		int pos;
	}
	
	struct SelectTagInfo {
		static int pos;		//!< 通常選択肢は連続して配置される。従って表示位置はクラス変数とする
		TextureVector texture;	//!< 選択肢文字列テクスチャ
		int x;
		int y;
		int width;		//!< 文字列の幅
		int height;		//!< 文字列の高さ
		char[] nextSceneId;	//!< 選択後の遷移先
		Color4ub	color;		//!<	描画する色
	}
	
	struct FontInfo {
		float size=1.0f;	//!< フォントのサイズ
		wchar[] color; 		//!< 文字色
		
		/// 属性文字列からColor4ubを構築して返却する
		Color4ub getColor4ub() {
			Color4ub color4ub;
			
			if ( !(color is null) && color.length >= 6 ) {
				// ごにょごにょする
				ubyte r,g,b;
				
				// 0..Fの文字から数値を逆引きして、それを８ビットにまとめて求める
				// そんな処理でいいかいな...
				with (std.string) {
					b = cast(ubyte) find( hexdigits, color[length-1] );
					b |= (cast(ubyte) find( hexdigits, color[length-2] )) << 4;
					
					g = cast(ubyte) find( hexdigits, color[length-3] );
					g |= (cast(ubyte) find( hexdigits, color[length-4] )) << 4;
					
					r = cast(ubyte) find( hexdigits, color[length-5] );
					r |= (cast(ubyte) find( hexdigits, color[length-6] )) << 4;
				}

				color4ub.setColor(r,g,b);
			}
			
			return color4ub;
		}
	}

	///	文字列の描画
	/**
		Color4ubを指定しておけば、その色に変移させたものが描画される
	*/
	static void	s_onDraw(Screen dst,DrawChar[] src,int x,int y,Color4ub color)
	{
		foreach(DrawChar d;src){
			dst.setColor(color * d.color);
			dst.blt(d.texture,cast(int)(d.pos.x + x),cast(int)(d.pos.y + y));
		}
	}

	///	文字列の描画
	/**
		Color4ubを指定しておけば、その色に変移させたものが描画される
	*/
	void	onDraw(Screen dst,int x,int y,Color4ub color)
	{
		s_onDraw(dst,getDrawChar,x,y,color);
	}

	///	描画する文字集合の取得
	DrawChar[]	getDrawChar() { return drawchar; }

	/// 未知タグを返す
	UnknownTagInfo[] getUnknownTagInfo() { return unknownTag; }
	SelectTagInfo[] getSelectTagInfo() { return selectTagInfo; }

	/// コンストラクタ
	this() {
		fontrep = new FontRepository;
		rubifontrep = new FontRepository;
		backlogFontRep = new FontRepository;
		lineparser = new LineParser;
		FontInfo fi;
		fontInfos ~= fi; 
	}
	
private:

	FontRepository getInnerFontRepository() {
		return backLogMode ? backlogFontRep : fontrep;
	}

	/// 指定文字が禁則文字かどうか
	bool isProhibition(wchar c_) {
		foreach ( wchar c; prohibitionChar) {
			if ( c == c_ ) {
				debug {
					printf("prohibition : %c\n" , c_);
				}
				return true;
			}			
		}
		return false;
	}

private:
	// 禁則対象文字セット
	static const wchar[] prohibitionChar = "。、」ァィゥェォャュョッぁぃぅぇぉっゃゅょ！？";

	FontRepository fontrep; 		//	文字フォント
	FontRepository rubifontrep;		// ルビフォント
	FontRepository backlogFontRep;	// バックログ用フォント
	wchar[] text;				//	設定されている文字
	DrawChar[] drawchar;		//	描画するテキスト
	UnknownTagInfo[] unknownTag;	// 未知タグの配列
	SelectTagInfo[] selectTagInfo;	// 選択肢配列
	FontInfo[] fontInfos;	// フォント情報をスタックする
	
	bool m_useRubi = true;	//!< ルビを使用するか
	
	int textPosOrg;			//!< バックログ用待避
	int headerTextPosOrg;	//!< バックログ用待避
	wchar[] textOrg;
	bool backLogMode;

	TextDrawContext textdrawcontext;	// コンテクスト
	LineParser lineparser;	// パーサ
	bool adjust;	// 調整フラグ
}

