/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.opengion.fukurou.business;

import org.opengion.fukurou.model.DataModel;
import org.opengion.fukurou.model.NativeType;
import static org.opengion.fukurou.system.HybsConst.CR;				// 6.1.0.0 (2014/12/26) refactoring
import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;	// 6.1.0.0 (2014/12/26) refactoring

// import java.util.Map;
// import java.util.HashMap;
import java.util.concurrent.ConcurrentMap;							// 6.4.3.3 (2016/03/04)
import java.util.concurrent.ConcurrentHashMap;						// 6.4.3.1 (2016/02/12) refactoring
import java.util.Arrays;

/**
 * 業務ロジックを処理するためのテーブルモデルです。
 *
 * このテーブルモデルでは、オブジェクト生成時に、カラム配列、値配列を元に、内部データを生成し、
 * その後は、行の追加や値の変更はできません。
 *
 * @og.rev 5.1.1.0 (2009/12/01) 新規作成
 * @og.group 業務ロジック
 *
 * @version 5.0
 * @author Hiroki Nakamura
 * @since JDK1.6,
 */
public class ArrayTableModel implements DataModel<String> {

	private final String[] names;
	private final String[][] vals;
	private final String[] modTypes;

	/** 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。  */
//	private ConcurrentMap<Integer,String[]> rtnMap ;	// 5.6.0.3 (2012/01/24) 変更された値を、書き戻すためのMap<インデックス,値配列> 
	private final ConcurrentMap<Integer,String[]> rtnMap = new ConcurrentHashMap<>() ;	// 6.4.3.3 (2016/03/04) final化で、初期から作成しておきます。

	/**
	 * 引数に名前配列、値配列を指定したコンストラクター
	 *
	 * @param	nms	名前配列
	 * @param	vs	値２重配列
	 * @throws  IllegalArgumentException 引数の配列が不正な場合
	 */
	public ArrayTableModel( final String[] nms, final String[][] vs ) {
		this( nms, vs, null );
	}

	/**
	 * 引数に名前配列、値配列、変更区分配列を指定したコンストラクター
	 *
	 * @og.rev 5.6.7.0 (2013/07/27) エラーメッセージを判りやすくする。
	 * @og.rev 5.7.2.3 (2014/01/31) vsのチェック条件を戻す
	 * @og.rev 5.7.3.1 (2014/02/14) nmsのチェック条件も戻す
	 *
	 * @param	nms	名前配列
	 * @param	vs	値２重配列
	 * @param	ms	変更区分の配列
	 * @throws  IllegalArgumentException 引数の配列が不正な場合
	 */
	public ArrayTableModel( final String[] nms, final String[][] vs, final String[] ms ) {
		if( nms == null || nms.length == 0 ) {
			final String errMsg = "引数の名前配列に、null は設定できません。";
			throw new IllegalArgumentException( errMsg );
		}
		// 5.6.7.0 (2013/07/27) エラーメッセージを判りやすくする。
		// 5.7.2.3 (2014/01/31) 結果０行でlength=0で通るようなのでvsのエラーチェック条件を戻す。
		if( vs == null ) {
			final String errMsg = "引数の値配列に、null は設定できません。";
			throw new IllegalArgumentException( errMsg );
		}
		// 5.7.3.1 (2014/02/14) 5.7.2.3での戻しでは不十分だったのでこちらも戻す
		// 6.0.0.1 (2014/04/25) These nested if statements could be combined
		if( vs.length > 0 && ( vs[0] == null || vs[0].length == 0 || nms.length != vs[0].length ) ) {
			final String errMsg = "名前配列と値配列のカラム数が異なります。"	+ CR
							+ "   nms   =" + Arrays.toString( nms   )			+ CR
							+ "   vs[0] =" + Arrays.toString( vs[0] ) ;
			throw new IllegalArgumentException( errMsg );
		}

		final int cols = nms.length;
		names = new String[cols];
		System.arraycopy( nms, 0, names, 0, cols );

		final int rows = vs.length;
		vals = new String[rows][cols];
		for( int i=0; i<rows; i++ ) {
			System.arraycopy( vs[i], 0, vals[i], 0, cols );
		}

		if( ms != null && ms.length > 0 ) {
			if( vs.length == ms.length ) {
				modTypes = new String[rows];
				System.arraycopy( ms, 0, modTypes, 0, rows );
			}
			else {
				// 5.6.7.0 (2013/07/27) エラーメッセージを判りやすくする。
				final String errMsg = "変更区分を指定する場合、値配列の行数と一致する必要があります。"	+ CR
									+ "   変更区分 行数 =" + ms.length		+ CR
									+ "   値配列   行数 =" + vs.length ;
				throw new IllegalArgumentException( errMsg );
			}
		}
		else {
			modTypes = null;
		}
	}

	/**
	 * rowで指定された行番号(インデックス番号)に行を追加します。
	 *
	 * 値配列をセットする場合は、以下の条件を満たす必要があります。
	 *   １．行番号は、0～(rowCount-1) の範囲
	 *   ２．値配列は、not null、 かつ １件以上
	 *   ３．値配列の個数は、内部カラム数と同じ
	 *
	 * ここで登録した値は、内部の値配列と別管理されますので、セット後に、再びゲットしても
	 * ここでセットした値を取り出すことはできません。
	 * また、同じ行番号でセットした場合は、後でセットした値が有効です。
	 *
	 * ※ ここでの更新時に、modifyType は、設定されません。
	 * 必要であれば、setModifyType メソッドで個別に設定してください。
	 *
	 * ※ インデックス(row)とは、このArrayTableModel に持つ vals 配列の行のインデックスです。
	 * よって、オリジナルのDBTableModelの行番号ではありません。
	 *
	 * @og.rev 5.6.0.3 (2012/01/24) 変更された値を、書き戻す機能を追加します。
	 * @og.rev 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。
	 *
	 * @param   inVals  配列値
	 * @param   rowNo   追加するインデックス
	 * @throws	IllegalArgumentException 引数が１，２，３の条件を満たさない場合。
	 */
	public void setValues( final String[] inVals, final int rowNo ) {
		if( rowNo < 0 || rowNo > getRowCount() ) {
			final String errMsg = "引数のインデックスは、0～" + (getRowCount()-1) + " の間で指定してください。index=[" + rowNo + "]";
			throw new IllegalArgumentException( errMsg );
		}
		else if( inVals == null || inVals.length == 0 ) {
			final String errMsg = "引数の値配列に、null、または 0件配列は指定できません。index=[" + rowNo + "]";
			throw new IllegalArgumentException( errMsg );
		}
		else if( inVals.length != names.length ) {
			final String errMsg = "引数の値配列の個数と、内部カラム数が一致しません。"
							+ " index=[" + rowNo + "] : 引数個数=[" + inVals.length + "] != 内部カラム数=[" + names.length + "]";
			throw new IllegalArgumentException( errMsg );
		}

		// 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。
//		if( rtnMap == null ) { rtnMap = new HashMap<>(); }

		final int cols = names.length;
		final String[] newVals = new String[cols];
		System.arraycopy( inVals, 0, newVals, 0, cols );

		// 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。初期化処理の場所も移動。
//		if( rtnMap == null ) { rtnMap = new ConcurrentHashMap<>(); }			// 6.4.3.3 (2016/03/04) final化で、初期から作成しておきます。
		rtnMap.put( Integer.valueOf( rowNo ) , newVals );
	}

//	/**
//	 * column および row にあるセルのオブジェクト値を設定します。
//	 *
//	 * @og.rev 6.7.9.1 (2017/05/19) インターフェースの見直しにより、追加
//	 *
//	 * @param   value	新しい値。null も可
//	 * @param   rowNo	値が変更される行
//	 * @param   clmNo	値が変更される列
//	 */
//	public void setValueAt( final String value, final int rowNo, final int clmNo ) {
//		if( rowNo < 0 || rowNo > getRowCount() ) {
//			final String errMsg = "引数のインデックスは、0～" + (getRowCount()-1) + " の間で指定してください。index=[" + rowNo + "]";
//			throw new IllegalArgumentException( errMsg );
//		}
//
//		final int cols = names.length;
//		if( clmNo >= 0 && clmNo < cols ) {
//			final String[] newVals = new String[cols];
//			System.arraycopy( vals[rowNo], 0, newVals, 0, cols );
//
//			newVals[clmNo] = value == null ? "" : value;
//
//			rtnMap.put( Integer.valueOf( rowNo ) , newVals );
//		}
//	}

	/**
	 * BizLogicで、データが変更された場合は、このMapで値の配列を返します。
	 * Mapのキーは、インデックス(row)のIntegerオブジェクトです。値は、設定された String配列です。
//	 * なにも変更がされていなければ、null を返します。
	 * なにも変更がされていなければ、空のConcurrentMapを返しましす。
	 *
	 * ※ インデックス(row)とは、このArrayTableModel に持つ vals 配列の行のインデックスです。
	 * よって、オリジナルのDBTableModelの行番号ではありません。
	 *
	 * @og.rev 5.6.0.3 (2012/01/24) 変更された値を、書き戻すためのMap&lt;インデックス,値配列&gt; を返します。
	 * @og.rev 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。
	 * @og.rev 6.4.3.3 (2016/03/04) 変更が無い場合は、nulllではなく、空のConcurrentMapを返しましす。
	 *
	 * @return	書き戻すためのMap＜インデックス,値配列＞
	 * @see AbstractBizLogic#isRequireTable()
	 * @og.rtnNotNull
	 */
//	public Map<Integer,String[]> getModifyVals() {
	public ConcurrentMap<Integer,String[]> getModifyVals() {
		return rtnMap;
	}

	/**
	 * カラム名に対応する カラム番号を返します。
	 *
	 * 特殊なカラムが指定された場合は、負の値を返します。
	 * 例えば、[KEY.カラム名]、[I]、[ROW.ID] など、特定の負の値を返します。
	 * また、カラム名が元のデータモデルに存在しない場合も、負の値か、
	 * Exception を返します。負の値なのか、Exception なのかは、
	 * 実装に依存します。
	 *
	 * @param	columnName	値が参照されるカラム名
	 *
	 * @return  指定されたセルのカラム番号。存在しなければ、-1
	 * @throws  IllegalArgumentException 引数のカラム名が null の場合
	 */
	public int getColumnNo( final String columnName ) {
		if( columnName == null ) {
			final String errMsg = "引数のカラム名に、null は設定できません。";
			throw new IllegalArgumentException( errMsg );
		}

		int address = -1;
		for( int i=0; i<names.length; i++ ) {
			if( columnName.equalsIgnoreCase( names[i] ) ) {
				address = i;
				break;
			}
		}

		return address;
	}

	/**
	 * カラム名配列に対応する カラム番号配列を返します。
	 *
	 * これは、#getColumnNo( String ) に対する 複数のカラム名を検索した
	 * 場合と同じです。
	 *
	 * @param	clmNms 	値が参照されるカラム名配列(可変長引数)
	 *
	 * @return  指定されたセルのカラム番号配列。
	 * @og.rtnNotNull
	 */
	public int[] getColumnNos( final String... clmNms ) {
		if( clmNms == null || clmNms.length == 0 ) { return new int[0]; }	// 6.1.1.0 (2015/01/17) 可変長引数でもnullは来る。

		int[] clmNos = new int[clmNms.length];
		for( int j=0; j<clmNms.length; j++ ) {
			int address = -1;
			for( int i=0; i<names.length; i++ ) {
				if( clmNms[j].equalsIgnoreCase( names[i] ) ) {
					address = i;
					break;
				}
			}
			clmNos[j] = address;
		}

		return clmNos;
	}

	/**
	 * カラム名配列を返します。
	 *
	 * @return	カラム名配列
	 * @og.rtnNotNull
	 */
	public String[] getNames() {
		return names.clone();
	}

	/**
	 * row にあるセルの属性値を配列で返します。
	 *
	 * @param   row     値が参照される行
	 *
	 * @return  指定されたセルの属性値配列
	 * @og.rtnNotNull
	 */
	public String[] getValues( final int row ) {
		return vals[row].clone();
	}

	/**
	 * row および clm にあるセルの属性値をStringに変換して返します。
	 *
	 * @param   row     値が参照される行
	 * @param   clm     値が参照される列
	 *
	 * @return  指定されたセルの値
	 */
	public String getValue( final int row, final int clm ) {
		return vals[row][clm];
	}

	/**
	 * row および clm にあるセルの属性値をStringに変換して返します。
	 *
	 * @param   row     値が参照される行
	 * @param   clm     値が参照される列(キー)
	 *
	 * @return  指定されたセルの値
	 *
	 */
	public String getValue( final int row, final String clm ) {
		return vals[row][getColumnNo( clm )];
	}

	/**
	 * データテーブル内の行の数を返します。
	 *
	 * @return  モデルの行数
	 *
	 */
	public int getRowCount() {
		return vals.length;
	}

	/**
	 * row 単位に変更されたタイプ(追加/変更/削除)を返します。
	 * タイプは始めに一度登録するとそれ以降に変更はかかりません。
	 * つまり、始めに 追加で作成したデータは、その後変更があっても追加のままです。
	 * なにも変更されていない場合は, ""(ゼロストリング)を返します。
	 *
	 * @param   row     値が参照される行
	 *
	 * @return  変更されたタイプの値
	 */
	public String getModifyType( final int row ) {
		return modTypes == null ? "" : modTypes[row];
	}

	/**
	 * row 単位に変更タイプ(追加/変更/削除)をセットします。
	 * このメソッドでは、データのバックアップは取りません。
	 * タイプは始めに一度登録するとそれ以降に変更はかかりません。
	 * なにも変更されていない場合は, ""(ゼロストリング)の状態です。
	 *
	 * @og.rev 6.7.9.1 (2017/05/19) インターフェースの見直しにより、追加
	 *
	 * @param   row      値が参照される行
	 * @param   modType  変更タイプ(追加/変更/削除)
	 *
	 */
	public void setModifyType( final int row , final String modType ) {
		if( modTypes[row].isEmpty() ) { modTypes[row] = modType; }
	}

	/**
	 * clm のNativeタイプを返します。
	 * Nativeタイプはorg.opengion.fukurou.model.NativeTypeで定義されています。
	 *
	 * @og.rev 5.1.8.0 (2010/07/01) NativeType#getType(String) のメソッドを使用するように変更。
	 *
	 * @param  clm      値が参照される列
	 *
	 * @return Nativeタイプ
	 * @see org.opengion.fukurou.model.NativeType
	 */
	public NativeType getNativeType( final int clm ) {
		return NativeType.getType( vals[0][clm] );
	}

	/**
	 * このオブジェクトの文字列表記を返します。
	 * デバッグ用です。
	 *
	 * @og.rev 5.6.7.0 (2013/07/27) 新規追加
	 *
	 * @return 文字列表現
	 * @og.rtnNotNull
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {
		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );

		buf.append( "NAMES=" ).append( Arrays.toString( names ) ).append( CR )
			// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
			.append( " COL_LEN=" ).append( names == null ? -1 : names.length ).append( CR )
			.append( " ROW_LEN=" ).append( vals  == null ? -1 : vals.length  ).append( CR ) ;
//			.append( " COL_LEN=" ).append( (names != null) ? names.length : -1 ).append( CR )
//			.append( " ROW_LEN=" ).append( (vals  != null) ? vals.length  : -1 ).append( CR ) ;

		return buf.toString();
	}
}
