/*
 * Paraselene
 * Copyright (c) 2009, 2010  Akira Terasaki
 * このファイルは同梱されているLicense.txtに定めた条件に同意できる場合にのみ
 * 利用可能です。
 */
package paraselene.tag.table;


import java.util.*;
import paraselene.*;
import paraselene.tag.*;

/**
 * TABLEタグ。
 */
public class Table extends Tag {
	private static final long serialVersionUID = 2L;
	static final String	COLSPAN = "colspan";
	static final String	TR = "tr";
	private boolean tabe_dirty_f = true;
	private int map_x, map_y;
	private Column[][]	map = null;

	/**
	 * テーブル整合性エラー。
	 */
	public class TableException extends Exception {
		private Table table;
		TableException( String mes, Table t ) {
			super( mes );
			table = t;
		}
		/**
		 * 例外が発生したテーブルを返します。
		 * @return テーブル。
		 */
		public Table getTable() {
			return table;
		}
	}

	private int seekFree( Column[] dim, int start, int y ) throws TableException {
		for ( ; ; start++ )	{
			if ( start >= dim.length ) {
				throw new TableException( String.format( "%d行が不正", y ), this );
			}
			if ( map[y][start] == null )	return start;
		}
	}

	private void makeMap() throws TableException {
		if ( !tabe_dirty_f )	return;
		tabe_dirty_f = false;
		Line[]	line = getLineArray();
		int	xsize = 0;
		for ( int i =  0; i < line.length; i++ ) {
			int	cnt = line[i].getColumnRowCount();
			if ( cnt > xsize )	xsize = cnt;
		}
		map = new Column[line.length][];
		for ( int i = 0; i < line.length; i++ ) {
			map[i] = new Column[xsize];
		}
		for ( int i = 0; i < line.length; i++ ) {
			Column[]	clm = line[i].getColumnArray();
			int	x = 0;
			for ( int j = 0; j < clm.length; j++ ) {
				int col = clm[j].getColspan();
				int	row = clm[j].getRowspan();
				for ( int x_cnt = 0; x_cnt < col; x_cnt++ ) {
					x = seekFree( map[i], x, i );
					map[i][x] = clm[j];
					x++;
				}
				for ( int y_cnt = 1; y_cnt < row; y_cnt++ ) {
					int	sub_x = 0;
					for ( int x_cnt = 0; x_cnt < col; x_cnt++ ) {
						sub_x = seekFree( map[i + y_cnt], sub_x, i + y_cnt );
						map[i + y_cnt][sub_x] = clm[j];
						sub_x++;
					}
				}
			}
		}
		map_x = xsize;
		map_y = line.length;
		for ( int i = 0; i < map_y; i++ ) {
			for ( int j = 0; j < map_x; j++ ) {
				if ( map[i][j] == null ) {
					throw new TableException( String.format( "不正な行あり[%d,%d]=null", j, i ), this );
				}
			}
		}
	}

	/**
	 * テーブルの列数を返します。
	 * @return 列数。
	 * @exception TableException colspan rowspan の整合性が損なわれている。
	 */
	public int getXsize() throws TableException {
		makeMap();
		return map_x;
	}

	/**
	 * テーブルの行数を返します。
	 * @return 行数。
	 * @exception TableException colspan rowspan の整合性が損なわれている。
	 */
	public int getYsize() throws TableException {
		makeMap();
		return map_y;
	}

	/**
	 * 連結状態の取得。<br>
	 * 指定位置のカラムが隣接するカラムから連結されている場合、true を返します。<br>
	 * 指定位置が連結の起点となっているカラムであれば false を返します。<br>
	 * 指定位置が連結状態に無い場合、false を返します。
	 * @param x 列番号。
	 * @param y 行番号。
	 * @return 連結状態。
	 * @exception TableException colspan rowspan の整合性が損なわれている。
	 * x y の指定が範囲を超えている。
	 */
	public boolean isJoinedColumn( int x, int y ) throws TableException {
		Column	clm = getColumn( x, y );
		if ( x > 0 ) {
			if ( getColumn( x - 1, y ) == clm )	return true;
		}
		if ( y > 0 ) {
			if ( getColumn( x, y - 1 ) == clm )	return true;
		}
		return false;
	}

	/**
	 * カラムの取得。
	 * 列番号、行番号はカラムの連結状態に関係なく、連結されていない場合の番号を
	 * 指定して下さい。内部で自動判断して、然るべきインスタンスを返します。
	 * @param x 列番号。
	 * @param y 行番号。
	 * @return カラム。
	 * @exception TableException colspan rowspan の整合性が損なわれている。
	 * x y の指定が範囲を超えている。
	 */
	public Column getColumn( int x, int y ) throws TableException {
		makeMap();
		if ( x < 0 || y < 0 || x >= map_x || y >= map_y ) {
			throw new TableException(
				String.format( "x=%d, y=%d 番号が範囲を超えています。", x, y ),
				this
			);
		}
		return map[y][x];
	}

	/**
	 * 内包物の検索。
	 * 引数で指定されたインスタンス群のうち、this のy行目が内包しているもののみを
	 * 抽出して返します。
	 * @param y 行番号。
	 * @param d 検索対象の集合。
	 * @return 存在した要素の集合。無ければ空の配列を返します。
	 * @exception TableException colspan rowspan の整合性が損なわれている。
	 * y の指定が範囲を超えている。
	 */
	public HTMLPart[] getInnerHTMLPart( int y, HTMLPart[] d ) throws TableException {
		makeMap();
		ArrayList<HTMLPart>	list = new ArrayList<HTMLPart>();
		if ( y < 0 || y >= map_y ) {
			throw new TableException(
				String.format( "y=%d 番号が範囲を超えています。", y ),
				this
			);
		}
		for ( int x = 0; x < map_x; x++ ) {
			HTMLPart[]	hit = getColumn( x, y ).getInnerHTMLPart( d );
			for ( int i = 0; i < hit.length; i++ ) {
				if ( list.contains( hit[i] ) )	continue;
				list.add( hit[i] );
			}
		}
		return list.toArray( new HTMLPart[0] );
	}

	/**
	 * 内包物の検索。
	 * 引数で指定されたインスタンス群のうち、this のy行目が内包しているもので
	 * 最初に見つかったものを返します。
	 * @param y 行番号。
	 * @param d 検索対象の集合。
	 * @return 存在した要素。無ければnullを返します。
	 * @exception TableException colspan rowspan の整合性が損なわれている。
	 * y の指定が範囲を超えている。
	 */
	public HTMLPart getFirstInnerHTMLPart( int y, HTMLPart[] d ) throws TableException  {
		makeMap();
		if ( y < 0 || y >= map_y ) {
			throw new TableException(
				String.format( "y=%d 番号が範囲を超えています。", y ),
				this
			);
		}
		for ( int x = 0; x < map_x; x++ ) {
			HTMLPart	hit = getColumn( x, y ).getFirstInnerHTMLPart( d );
			if ( hit != null )	return hit;
		}
		return null;
	}

	/**
	 * カラムの連結/連結解除。
	 * 列番号、行番号はカラムの連結状態に関係なく、連結されていない場合の番号を
	 * 指定して下さい。内部で自動判断して、然るべきインスタンスで処理します。<br>
	 * 連結された先のColumnインスタンスは消滅します。そのタグ内のデータも失われます。
	 * 連結先が既に他のカラムと連結されている場合は、それを解除して連結します。<br>
	 * 連結解除され、空きができた場所には、新規Columnインスタンスが生成されます。
	 * そのインスタンスは属性等は持っていません。
	 * 右寄せ等の必要があれば別途指定する必要があります。
	 * @param x 列番号。現在のテーブル範囲に無い番号であれば何もしません。
	 * @param y 行番号。現在のテーブル範囲に無い番号であれば何もしません。
	 * @param xsize colspan の値。現在のテーブル範囲外であれば調整されます。
	 * 連結を解除する場合は 1 を指定します。
	 * @param ysize rowspan の値。現在のテーブル範囲外であれば調整されます。
	 * 連結を解除する場合は 1 を指定します。
	 * @exception TableException colspan rowspan の整合性が損なわれている。
	 * x y の指定が範囲を超えている。
	 */
	public void joinColumn( int x, int y, int xsize, int ysize ) throws TableException {
		makeMap();
		if ( xsize < 1 )	xsize = 1;
		if ( ysize < 1 )	ysize = 1;
		int	max_xsize = map_x - x;
		int	max_ysize = map_y - y;
		if ( xsize > max_xsize )	xsize = max_xsize;
		if ( ysize > max_ysize )	ysize = max_ysize;
		Column	clm = getColumn( x, y );
		if ( xsize == clm.getColspan() && ysize == clm.getRowspan() )	return;
		boolean	flag = false;
		for ( int chk_y = 0; chk_y < map_y; chk_y++ ) {
			for ( int chk_x = 0; chk_x < map_x; chk_x++ ) {
				if ( map[chk_y][chk_x] == clm ) {
					x = chk_x;
					y = chk_y;
					flag = true;
					break;
				}
			}
			if ( flag )	break;
		}
		clm = getColumn( x, y );
		int	org_x = clm.getColspan();
		clm.setColspan( xsize );
		int	org_y = clm.getRowspan();
		clm.setRowspan( ysize );
		int	all_x = org_x, all_y = org_y;
		if ( all_x < xsize )	all_x = xsize;
		all_x += x;
		if ( all_y < ysize )	all_y = ysize;
		all_y += y;
		int	xx = x + xsize - 1, yy = y + ysize - 1;
		Line[]	l = getLineArray();
		for ( int ycnt = y; ycnt < all_y; ycnt++ ) {
			for ( int xcnt = x; xcnt < all_x; xcnt++ ) {
				if ( ( ycnt > yy && map[ycnt][xcnt] == clm ) ||
					( xcnt > xx && map[ycnt][xcnt] == clm ) ) {
					map[ycnt][xcnt] = new Column( clm.getType() );
					int	idx = 0;
					for ( int tmp = 0; tmp < xcnt; tmp++ ) {
						if ( !isJoinedColumn( tmp, ycnt ) )	idx++;
					}
					l[ycnt].addColumn( idx, map[ycnt][xcnt] );
					tabe_dirty_f = false;
				}
				else if ( ( ycnt <= yy && map[ycnt][xcnt] != clm ) ||
					( xcnt <= xx && map[ycnt][xcnt] != clm ) ) {
					if ( map[ycnt][xcnt].getColspan() > 1 || map[ycnt][xcnt].getRowspan() > 1 ) {
						joinColumn( xcnt, ycnt, 1, 1 );
					}
					l[ycnt].removeColumn( map[ycnt][xcnt] );
					tabe_dirty_f = false;
					map[ycnt][xcnt] = clm;
				}
			}
		}
	}

	void setDirty() {
		tabe_dirty_f = true;
	}

	/**
	 * コンストラクタ。
	 */
	public Table() {
		this( "table" );
	}

	Table( String s ) {
		super( s, false );
	}

	protected Tag newReplica() {
		return new Table();
	}

	/**
	 * 行の列挙。trタグを検出し配列にして返します。
	 * @return 行。
	 */
	public Line[] getLineArray() {
		Tag[]	tag = getTagArray();
		ArrayList<Line>	line = new ArrayList<Line>();
		for ( int i = 0; i < tag.length; i++ ) {
			if ( tag[i] instanceof Line ) {
				line.add( (Line)tag[i] );
			}
		}
		return line.toArray(new Line[0]);
	}

	/**
	 * 行の取得。
	 * 指定位置のtrタグを返します。
	 * @param no 行番号。
	 * @return 行。
	 */
	public Line getLine( int no ) {
		if ( no < 0 )	return null;
		Line[]	tag = getLineArray();
		if ( no >= tag.length ) return null;
		return tag[no];
	}

	/**
	 * 行の追加。末尾に追加されます。
	 * @param line	追加行。
	 */
	public void addLine( Line line ) {
		addLine( getLineCount(), line );
	}

	/**
	 * 行の追加。指定位置に追加します。
	 * 現在の最大行よりも大きい位置を指定すると末尾に追加します。
	 * @param idx 行番号。
	 * @param line	追加行。
	 */
	public void addLine( int idx, Line line ) {
		Line[]	l = getLineArray();
		setDirty();
		if ( l.length <= idx ) {
			addHTMLPart( line );
			return;
		}
		addHTMLPart( indexOf( l[idx] ), line );
	}

	/**
	 * 行番号を返す。最初に登場した指定インスタンスの位置を返します。
	 * @param line 検索行。
	 * @return 行番号。無ければ-1を返す。
	 */
	public int indexOfLine( Line line ) {
		Line[]	l = getLineArray();
		for ( int i = 0; i < l.length; i++ ) {
			if ( l[i] == line )	return i;
		}
		return -1;
	}

	

	/**
	 * 行の削除。指定行を削除します。
	 * 現在の最大行よりも大きい位置を指定すると何もしません。
	 * @param idx 行番号。
	 */
	public void removeLine( int idx ) {
		if ( idx < 0 )	return;
		Line[]  l = getLineArray();
		if ( l.length <= idx )	return;
		setDirty();
		if ( idx > 0 ) {
			Column[]	clm = l[idx - 1].getColumnArray();
			for ( int i = 0; i < clm.length; i++ ) {
				int row = clm[i].getRowspan();
				row--;
				if ( row > 1 ) {
					clm[i].setRowspan( row );
				}
			}
		}
		removeHTMLPart( l[idx] );
	}

	/**
	 * 行の削除。最初に登場した指定インスタンスを削除します。
	 * @param line 削除行。
	 */
	public void removeLine( Line line ) {
		removeHTMLPart( indexOfLine( line ) );
	}

	/**
	 * 行の削除。指定範囲を削除します。
	 * @param start 開始行。負数を与えると0行目とみなします。
	 * @param end 終了行。負数や最大行を越えると最大行とみなします。
	 */
	public void removeLine( int start, int end ) {
		Line[]	l = getLineArray();
		if ( start < 0 )	start = 0;
		if ( end < 0 )	end = l.length - 1;
		for ( int i = end; i >= start; i-- ) {
			removeLine( i );
		}
	}

	/**
	 * 行の削除。全ての行を削除します。
	 */
	public void removeLine() {
		removeLine( 0, -1 );
	}

	/**
	 * 行の複製作成。指定行の複製インスタンスを返します。<br>
	 * 行番号が0未満または最大行数を超える場合はnullを返します。
	 * @param idx 行番号。
	 * @return 行の複製。
	 */
	public Line getLineReplica( int idx ) {
		Line[]	l = getLineArray();
		if ( l.length == 0 )	return null;
		if ( idx < 0 || idx >= l.length )	return null;
		return (Line)l[idx].getReplica();
	}

	/**
	 * 行数の取得。
	 * @return 行数。
	 */
	public int getLineCount() {
		Line[]	l = getLineArray();
		return l.length;
	}
}

