module yamalib.dict.dictdata;

private import std.stream;
private import std.string;

private import y4d_aux.filesys;
private import y4d_aux.lineparser;
private import y4d_aux.widestring;
private import y4d_draw.texture;
private import y4d_draw.drawbase;
private import y4d_draw.fontrepository;

private import ytl.exception;
private import ytl.y4d_result;

private import yamalib.draw.textdraw;

private import yamalib.log.log;


/**
	画像データ
*/
class PicData {
	
	/// 文字列表現
	override char[] toString() {
		char[] buf = "PicData";
		buf ~= ":{";
		buf ~= "src=" ~ filename;
		buf ~= ",ox=" ~ std.string.toString(ox);
		buf ~= ",oy=" ~ std.string.toString(oy);
		buf ~= "}";
		
		return buf;
	}
	
	/// ファイル名
	void setFilename(char[] filename_) {
		this.filename = filename_;
	}
	
	/// オフセット位置
	void setOffsetPos(int x_, int y_) {
		this.ox = x_;
		this.oy = y_;
	}
	
	/// 表示サイズの設定
	void setDispSize(int sx_, int sy_) {
		sx = sx_;
		sy = sy_;
	}
	
private :
	char[] filename;
	int sx;
	int sy;
	
	int ox;
	int oy;
}

/**
	インデックスデータ
	
	データ形式は
	
	タイトル,タイトルかな,data/data1.dat
	
	のCSV形式
	
*/
class IndexData {
	
	/// 文字列表現を返却する
	override char[] toString() {
		char[] buf = "IndexData";
		
		buf ~= ":{";
		for ( int i = 0; i < m_indexes.length; ++i ) {
			buf ~= "[";
			buf ~= "title=" ~ m_indexes[i].title;
			buf ~= ",title_kana=" ~ m_indexes[i].title_kana;
			buf ~= ",filename=" ~ m_indexes[i].filename;
			buf ~= "]";
		}
		buf ~= "}";
		
		return buf;
	}

	/// ファイルからインデックスデータを読み込む
	void load(char[] flnm) {
		 ubyte[] mem = cast(ubyte[]) ( cast(char[]) FileSys.read(flnm) );
		if (!mem) return; // 読み込みエラー
		
		m_indexes = null;
		
		std.stream.MemoryStream m = new std.stream.MemoryStream(mem);
		LineParser lp = new LineParser();
		while (!m.eof) {
			char[] linebuf = m.readLine();
			lp.setLine(linebuf);
			
			Index index = new Index();

			char[] title = lp.getStr();
			if (!title) continue; // ダメやん

			char[] title_kana = lp.getStr();
			if (!title_kana) continue; // ダメやん

			char[] filename = lp.getStr();
			if (!filename) continue; // ダメやん
			
			index.title = title;
			index.title_kana = title_kana;
			index.filename = filename;

			m_indexes ~= index;
		}
		
		m.close();
		m = null;
	}
	
	/// インデックスの取得
	Index getIndex(int i) {
		if ( i < 0 || i >= m_indexes.length ) {
			Index index;
			return index;
		}
		
		return m_indexes[i];
	}
	
	/// 全インデックス取得
	Index[] getIndexes() {
		return this.m_indexes;
	}

	/**
		１項目のデータ
	*/
	public static class Index {
		char[] title;
		char[] title_kana;
		char[] filename;
	}
	
private:
	Index[] m_indexes;
}


/**
	メインビューデータ
	
	Scenarioデータ形式の簡易版形式データ
	
	HTML構文を踏襲して
	FONT、IMG,引用タグ <A>を実装
	HRまでを１ページとし、複数ページを記述することができる
*/
class DictData {
	
	/// 文字列表現を返却する
	override char[] toString() {
		char[] buf = "DictData";
		
		buf ~= ":{";
		buf ~= "text=" ~ std.utf.toUTF8(text);
		
		buf ~= "DrawChar[]=";
		foreach ( inout DrawChar c; drawchar ) {
			buf ~= c.toString();
		}
		
		buf ~= "Quotation[]=";
		foreach ( inout Quotation q; quot ) {
			buf ~= q.toString();
		}
		
		buf ~= "UnknownTagInfo[]=";
		foreach ( inout UnknownTagInfo uti; unknownTag ) {
			buf ~= uti.toString();
		}

		buf ~= "LinkInfo[]=";
		foreach ( inout LinkInfo li; links ) {
			buf ~= li.toString();
		}

		buf ~= "PicData[]=";
		foreach ( inout PicData pd; picData ) {
			buf ~= pd.toString();
		}
		
		buf ~= "}";
		
		return buf;
	}

	///	フォントリポジトリ
	/**
		このフォントリポジトリを通じて、フォントローダーを
		事前に食わせておくこと。
	*/
	FontRepository getFontRepository() { return fontrep; }
	FontRepository getRubiFontRepository() { return rubifontrep; }

	// 描画コンテクスト
	void setTextDrawContext(TextDrawContext v) { textdrawcontext = v; }

	// 開始位置の設定
	void setTextOffset(int pos) { textdrawcontext.setTextOffset(pos); }
	// 開始位置の取得
	int getTextOffset() { return textdrawcontext.getTextOffset(); }
	int getHeadTextOffset() { return textdrawcontext.getHeadTextOffset(); }


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

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

		解析上のエラーがあれば例外がthrowされる
	*/
	bool updateText(bool bPrev=false) {
		// 以前のデータは破棄せねば...
		drawchar = null;
		unknownTag = null;
		links = null;
		linkInfo = null;
		linkRc = null;
		picData = null;
		quot = null;
		FontInfo.colorDef = textdrawcontext.rgbColor;

		// <HR> まで探す
		if (bPrev) {
			text = textdrawcontext.getPreText();
		} else {
			text = textdrawcontext.getNextText();
		}
		
		if (text is null) 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;	//	この場所に埋められているタグ
		int t_width;
		int t_height;

		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", 		//  0.改行
					"space", 	//  1.空白行
					"rubi",		//  2.ルビ
					"font",		//  3.フォント開始
					"/font",	//  4.フォント終了
					"a",		//  5.LINK
					"/a",		//  6.LINK END
					"img"		//  7.画像
				];

				// 切り出した文字から最初の文字を取得
				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;
				}
				
				Log.print("tag:%s", tags[found]  );

				//	このタグの処理
				switch(found) {
				case 0: // <BR> : 行送り処理
					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");
					int back = cast(int) lineparser.getNum(-1);
					int search = 0;
					wchar[] moji = toWCS( lineparser.getStr() );

					// 指定チェック
					if ( (back == -1) || (moji.length == 0) || (drawchar.length < back) ) 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 / back;
					float rubiRest = perChar;
					int index = drawchar.length - back;

					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 - back;

					// まず連結したルビ文字列の長さを算出
					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 drawWidth = 0;
					bool posAdjust = cast(bool) (moji.length != back*2);
					if (sumWidth >= sumWidthRubi) {
						drawWidth = sumWidth - sumWidthRubi;
						ox = sumWidth / moji_len;
						dx = cast(int)drawchar[index].pos.x;
					} else {
						drawWidth = sumWidthRubi - sumWidth;
						dx = drawWidth / 2;
						//ox = cast(int)(drawchar[index].rubi[0].texture.getWidth()/2);
						ox = 0;
					}
					for(i=index; i < drawchar.length; ++i) {
						if (sumWidth < sumWidthRubi && i > index) {
							DrawChar[] rubi = drawchar[i-1].rubi;
							ox = cast(int)(rubi[rubi.length-1].pos.x - drawchar[i].pos.x) + dx + tWidth;
						}
						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 {
									printf("i = %d,j = %d,x = %f,ox = %d\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;
							}
						}
					}

					// 行間調整
					int end = drawchar.length - 1;
					int ly = cast(int)drawchar[end].pos.y;
					if (drawchar[index].pos.y != 0 && !adjust) {
						while (end >= 0 && (drawchar[end].pos.y == ly || end >= index)) {
							ly = cast(int)drawchar[end].pos.y;
							drawchar[end].pos.y += tHeight;
							end--;
						}
						drawpos.y += tHeight;
						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: // <font>
//					printf("----------------- find font tag ------------------\n");
					float fSize = 1.0f;
					wchar[] strColor;

					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 ( std.string.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 4: // </font>
					// pop
					
					// 明示的なスタックがなければ、無視する
					if (fontInfos.length >= 2) {
						fontInfos.length = fontInfos.length - 1; 
					} else {
						Log.printWarn("</font> appeared before <font>");
					}
					break;
				
				case 5:	// <a href="...">
					Log.print("<a href=\"...\">");
					
					if ( lineparser.isMatch("href=", false) ) {
						linkRc = new RectInt();
						linkInfo = new LinkInfo();
						
						linkInfo.filename = lineparser.getStr();
						linkRc.left = cast(int) drawpos.x;
						linkRc.top = cast(int) drawpos.y;
						
						Log.print("stack <A> TAG");
					}

					break;
				
				case 6:	// </a>
					Log.print("</a>");
					
					if ( linkInfo is null ) {
						Log.print("NO STACK <A> TAG");
						continue;
					}
					
					linkRc.right = cast(int) drawpos.x;
					linkRc.bottom = cast(int) drawpos.y + t_height;
					
					linkInfo.rects ~= *linkRc;
					links ~= *linkInfo;
					
					linkInfo = null;
					linkRc = null;
					break;
					
				case 7:	// <img>
					Log.print("<img,src=\"...\"..>");
					int sw = -1;
					int sh = -1;
					char[] src = null;

					while ( !lineparser.isEnd() ) {
						
						// 属性を走査する
						if ( lineparser.isMatch( "width=", false ) ) {
							// サイズ指定
							sw = cast(int) lineparser.getDecimal(-1.0f);
						} else if ( lineparser.isMatch( "height=", false ) ) {
							sh = cast(int) lineparser.getDecimal(-1.0f);
						} else if ( lineparser.isMatch( "src=", false ) ) {
							src = lineparser.getStr();
						} else {
							// 未知の属性
							Log.printWarn("Invaild Font tag option %s", lineparser.getStr() );
							break;
						}
					}
					
					if ( !(null is src) ) {
						PicData pd = new PicData();
						
						pd.setFilename( src );
						pd.setDispSize( sw, sh );
						
						picData ~= pd;
					}
					
					break;
					
				default:
					assert(false);
				}

				continue;
			}

			if (c == '\t') continue;
			if (c == '\n') continue;
			if (c == '\r') continue;
			
			Texture t = getFontRepository().getTexture(c);
			if (!t) continue; // なんだこの文字は
			
			// フォントサイズで拡縮した後のサイズ
			float charRate = fontInfos[length-1].size;
			t_width  = cast(int) (t.getWidth() * charRate);
			t_height = cast(int) (t.getHeight() * charRate);
			DrawChar dc = new DrawChar();
			{
				// 禁則文字ではなく、描画位置が指定範囲を超えていれば改行する
				if ( (drawpos.x > textdrawcontext.width) && !isProhibition(c) ) {
					drawpos.x = 0;
					drawpos.y += l_height + textdrawcontext.blankHeight;
					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) + textdrawcontext.hInterval;
				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;
	}

	/// 次ページデータの取得	
	bool nextPage() {
		return updateText(false);
	}
	
	/// 前頁データの取得
	bool prevPage() {
		return updateText(true);
	}

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

	*/
	void	setText(wchar[] text_) { text = text_; }

	///	テキストの取得
	/**
		設定してあったテキストの取得。ここで取得したものに追加して
		再度setTextを呼び出すなどしても良い。
	*/
	wchar[] getText() { return text; }
	
	///	描画する文字集合の取得
	DrawChar[]	getDrawChar() { return drawchar; }
	
	/// 引用データの取得
	Quotation[] getQuotation() { return quot; }

	/// 未知タグを返す
	UnknownTagInfo[] getUnknownTagInfo() { return unknownTag; }
	
	/// リンク情報を返す
	LinkInfo[] getLinkInfo() { return this.links; }
	
	/// 画像情報の取得
	PicData[] getPicData() { return this.picData; }

	///	描画するキャラクター実体
	class DrawChar {

		/// 文字列表現		
		override char[] toString() {
			char[] buf = "DrawChar";
			buf ~= ":{";
			buf ~= "size=" ~ std.string.toString(size);
			buf ~= ",pos=" ~ std.string.toString(pos.x) ~ "," ~ std.string.toString(pos.y);
			
			foreach ( inout wchar[] tag; tags ) {
				buf ~= "tag=" ~ std.utf.toUTF8(tag);
			}
			
			buf ~= ",rubi=";
			foreach( inout DrawChar c; rubi ) {
				buf ~= c.toString();
			}
			
			buf ~= "}";
			return buf;
		}

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

			buf ~= ":{";
			buf ~= "x=" ~ std.string.toString(x);
			buf ~= ",y=" ~ std.string.toString(y);
			foreach( inout DrawChar c; text ) {
				buf ~= c.toString();
			}
			buf ~= "}";
			
			return buf;
		}
		
		int x;	// 基本位置
		int y;
		DrawChar[] text;
	}

	/// 未知のタグを格納する
	struct UnknownTagInfo {
		
		/// 文字列表現の返却
		char[] toString() {
			char[] buf = "UnknownTagInfo";
			buf ~= ":{";
			buf ~= "tag=" ~ std.utf.toUTF8(tag);
			buf ~= "pos=" ~ std.string.toString(pos);
			buf ~= "}";
			return buf;
		}
		
		wchar[] tag;
		int pos;
	}
	
	/// 文字ごとのフォント情報
	struct FontInfo {
		static Color4ub colorDef;	// デフォルトの文字色
		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;
			}
			
			return colorDef;
			
		}
	}
	
	/// リンク情報
	struct LinkInfo {
		
		/// クラスの文字列表現を返却する
		char[] toString() {
			char[] buf = "LinkInfo";
			buf ~= ":{";
			buf ~= "filename=" ~ filename;
			buf ~= "RectInt[]=";
			foreach( inout RectInt rc; rects ) {
				buf ~= "left=" ~ std.string.toString(rc.left);
				buf ~= "top=" ~ std.string.toString(rc.top);
				buf ~= "right=" ~ std.string.toString(rc.right);
				buf ~= "bottom=" ~ std.string.toString(rc.bottom);
			}
			
			buf ~= "}";
			return buf;
		}
		
		RectInt[] rects;	// リンク範囲をカバーする矩形配列（複数）
		char[] filename;
	}

	/// コンストラクタ
	this() {
		fontrep = new FontRepository;
		rubifontrep = new FontRepository;
		lineparser = new LineParser;
		FontInfo fi;
//		fontInfos ~= fi; 
	}
	
private:
	/// 禁則対象文字セット
	static const wchar[] prohibitionChar = "。、」ァィゥェォャュョッぁぃぅぇぉっゃゅょ！？";

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

	FontRepository fontrep; 		//	文字フォント
	FontRepository rubifontrep;		// ルビフォント
	wchar[] text;					//	設定されている文字
	DrawChar[] drawchar;			//	描画するテキスト
	Quotation[] quot;				// 引用データ
	UnknownTagInfo[] unknownTag;	// 未知タグの配列
	FontInfo[] fontInfos;			// フォント情報をスタックする
	LinkInfo[] links;				// 現在ビューのリンク情報
	PicData[]  picData;				// 画像データ
	LinkInfo*  linkInfo;
	RectInt*   linkRc;

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