/*
 * Copyright (c) 2017 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.fileexec;

import java.util.Locale;
import java.util.Date;
import java.text.DateFormat;
import java.text.SimpleDateFormat;

/**
 * StringUtilは、共通的に使用される文字列処理に関する、ユーティリティークラスです。
 *
 * @og.rev 7.0.0.0 (2017/07/07) 新規作成
 *
 * @version  7.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK1.8,
 */
public final class StringUtil {
	/** システム依存の改行記号(String)。	*/
	public static final String CR = System.getProperty("line.separator");

	private static final int	BUFFER_MIDDLE    = 200 ;	// 7.2.9.5 (2020/11/28)

	/**
	 * デフォルトコンストラクターをprivateにして、
	 * オブジェクトの生成をさせないようにする。
	 */
	private StringUtil() {}

	/**
	 * 指定の文字列が nullか、ゼロ文字列 の場合は、２番目以降の引数から、null でない最初の値を返します。
	 * ２番目以降の引数には、ゼロ文字列の指定も可能です。
	 *
	 * @param	val 判定する文字列
	 * @param	def 初期値の可変長引数
	 * @return	指定の値で、最初にnullでない値。最後まで、無ければ、val を返します。
	 */
	public static String nval( final String val , final String... def ) {
		if( val == null || val.trim().isEmpty() ) {
			for( final String str : def ) {
				if( str != null ) { return str.trim(); }			// ゼロ文字列 の場合も、返します。
			}
		}

		return val;		// 最後まで null以外の値が見つからない場合なので、val が null の場合もありうる。
	}

	/**
	 * 指定の数値型文字列が nullか、ゼロ文字列 の場合は、２番目の初期値を返します。
	 *
	 * そうでない場合は、指定の文字列を、Integer.parseInt( String ) した値を返します。
	 *
	 * @param	val 判定する数値型文字列
	 * @param	def 初期値の数値
	 * @return	指定の文字列が nullか、ゼロ文字列でない場合は、数値に変換し、そうでなければ、初期値の数値を返します。
	 * @throws	NumberFormatException	文字列が解析可能な整数型を含まない場合。
	 */
	public static int nval( final String val , final int def ) {
		return val == null || val.trim().isEmpty() ? def : Integer.parseInt( val.trim() );
	}

	/**
	 * LOGファイルのフォーマットに対応した日付、時刻文字列を作成します。
	 *
	 * yyyyMMddHHmmss 形式のフォーマットで、返します。
	 *
	 * @return	現在の日付、時刻文字列
	 */
	public static String getTimeFormat() {
		return getTimeFormat( "yyyyMMddHHmmss" );
	}

	/**
	 * 指定のフォーマットに対応した日付、時刻文字列を作成します。
	 *
	 * 例えば、LOGフォーマットの場合は、yyyyMMdd HH:mm:ss 形式です。
	 *
	 * @param format	日付、時刻文字列のフォーマット
	 * @return	指定のフォーマットに対応した現在の日付、時刻文字列
	 */
	public static String getTimeFormat( final String format ) {
		final DateFormat formatter = new SimpleDateFormat( format , Locale.getDefault() );
		return formatter.format( new Date() );
	}

	/**
	 * 指定のフォーマットに対応した日付、時刻文字列を作成します。
	 *
	 * 例えば、LOGフォーマットの場合は、yyyyMMdd HH:mm:ss 形式です。
	 *
	 * @param date		フォーマットする日付情報
	 * @param format	日付、時刻文字列のフォーマット
	 * @return	指定のフォーマットに対応した指定の日付、時刻文字列
	 */
	public static String getTimeFormat( final long date , final String format ) {
		final DateFormat formatter = new SimpleDateFormat( format , Locale.getDefault() );
		return formatter.format( new Date( date ) );
	}

	/**
	 * EUROMAPの日付、時間文字列から、openGion系日付時刻文字列を作成します。
	 *
	 * 日付は、yyyyMMdd 形式で、時間は、HH:mm:ss 形式を標準としますが、
	 * 数字以外の文字列を削除して、連結した文字列を作成します。
	 * その場合、１４桁数字文字列 になります。
	 *
	 * その形式に合わない場合は、現在時刻 を返します。
	 * ただし、日付の整合性チェックは、行っていません。
	 *
	 * @og.rev 7.0.5.1 (2019/09/27) 日付指定がない場合に、無条件に１秒待つ仕様を廃止。ロジックミス修正
	 * @og.rev 7.2.5.0 (2020/06/01) 日付と時間を分けて取得します。
	 * @og.rev 7.4.1.0 (2021/04/23) １秒待つ仕様を復活。あくまで現在時刻を設定した場合のみ待ちます。
	 *
	 * @param	ymd EUROMAPの日付(yyyyMMdd形式の８桁数字)
	 * @param	hms EUROMAPの時間(HH:mm:ss形式の８桁数字)
	 * @return	openGion系日付時刻文字列(yyyyMMddHHmmss形式の１４桁数字)
	 */
	public static String getTime( final String ymd , final String hms ) {
		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );

		// yyyyMMdd形式のはずだが、数字以外を削除する。
		// 7.4.1.0 (2021/04/23) 仕様変更。null 出なければ、数字項目を削除してから、桁数を判定する。
//		if( ymd != null && ymd.length() == 8 ) {
		if( ymd != null ) {
			for( int i=0; i<ymd.length(); i++ ) {
				final char ch = ymd.charAt( i );
				if( '0' <= ch && ch <= '9' ) { buf.append( ch ); }	// 数字のみ許可
			}
		}
//		else {
//			buf.append( getTimeFormat( "yyyyMMdd" ) );	// 現在の年月日
//		}

		final int ymdLen = buf.length();
		// 8桁以上の場合は、8桁に切る。
		if( ymdLen > 8 ) {
			buf.setLength( 8 );
		}
		// 8桁未満の場合は、現在の年月日を後ろから埋める。つまり、年月しか指定されていなければ、日だけ現在になる。
		else if( ymdLen < 8 ) {
			buf.append( getTimeFormat( "yyyyMMdd" ).substring( ymdLen ) );
		}
		// 8桁の場合は、日付として不正でも無条件で信頼する。

		// HH:mm:ss形式のはずだが、数字以外を削除する。
//		if( hms != null && hms.length() == 8 ) {
		if( hms != null ) {
			for( int i=0; i<hms.length(); i++ ) {
				// HH:mm:ss形式から、':'を取り除く
				final char ch = hms.charAt( i );
				if( '0' <= ch && ch <= '9' ) { buf.append( ch ); }	// 数字のみ許可
			}
		}
//		else {
//			buf.append( getTimeFormat( "HHmmss" ) );	// 現在の時分秒
//		}

		final int ymdhmsLen = buf.length();
		// 14桁以上の場合は、14桁に切る。
		if( ymdhmsLen > 14 ) {
			buf.setLength( 14 );
		}
		// 14桁未満の場合は、現在の現在時刻を後ろから埋める。日付は埋めているので、日時の不足分を補完する。
		else if( ymdhmsLen < 14 ) {
			buf.append( getTimeFormat().substring( ymdhmsLen ) );

			// 現在時刻を後ろから埋めた場合は、無条件に１秒待ちます。
			// これは、テスト用に日付無しのDATデータを用意して登録する場合に使用します。
//			try{ Thread.sleep( 1000 ); } catch( final InterruptedException ex ){}
			try{ Thread.sleep( 1100 ); } catch( final InterruptedException ex ){}		// 7.4.4.0 (2021/06/30) 何となく重複エラーが出るので
		}

//		// 14桁でなければ、現在時刻を設定します。日付の整合性チェックは行いません。
//		if( buf.length() != 14 ) {
//			buf.append( getTimeFormat() );				// 現在時刻
//		}

//		// １秒待たないので、時刻が重複するとエラーになる可能性があります。
		return buf.toString();
	}

//	public static String getTime( final String ymd , final String hms ) {
//		if( ymd != null && hms != null ) {
//			if( ymd.length() == 8 && hms.length() == 8 ) {
//				return ymd + hms.substring(0,2) + hms.substring(3,5) + hms.substring(6);
//			}
//			else {
//				final StringBuilder buf = new StringBuilder();
//				for( int i=0; i<ymd.length(); i++ ) {
//					final char ch = ymd.charAt( i );
////					if( '0' <= ch && ch < '9' ) { buf.append( ch ); }	// 数字のみ許可
//					if( '0' <= ch && ch <= '9' ) { buf.append( ch ); }	// 数字のみ許可		7.0.5.1 (2019/09/27)
//				}
//
//				for( int i=0; i<hms.length(); i++ ) {
//					final char ch = hms.charAt( i );
////					if( '0' <= ch && ch < '9' ) { buf.append( ch ); }	// 数字のみ許可
//					if( '0' <= ch && ch <= '9' ) { buf.append( ch ); }	// 数字のみ許可		7.0.5.1 (2019/09/27)
//				}
//
//				if( buf.length() == 14 ) {
//					return buf.toString();
//				}
//			}
//		}
//
//		// 引数が存在しない場合は、無条件に１秒待ちます。
//		// これは、テスト用に日付無しのDATデータを用意して登録する場合に使用します。
////		try{ Thread.sleep( 1000 ); } catch( final InterruptedException ex ){}
//
//		return getTimeFormat() ;		// 現在時刻
//	}

	/**
	 * すべての引数をｽﾍﾟｰｽで連結してtrim()した文字列を返します。
	 *
	 * 各引数は、それぞれ、trim()した後、ｽﾍﾟｰｽで連結します。
	 * ただし、引数が、nullの場合は、ゼロ文字列に変換します。
	 * すべてを連結して、trim() した結果が、ゼロ文字列の場合は、defVal を返します。
	 *
	 * @og.rev 7.2.5.0 (2020/06/01) 日付と時間を分けて取得します。
	 *
	 * @param	defVal 結果が、ゼロ文字列の場合に返す文字列
	 * @param	keys 連結したいキーの可変長文字列配列
	 * @return	合成した文字列。
	 */
	public static String keyAppend( final String defVal , final String... keys ) {
		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
		for( final String key : keys ) {
//			buf.append( nval( key , "" ).trim() ).append( ' ' );
			final String key2 = nval( key , "" ).trim();
			if( !key2.isEmpty() ) { buf.append( key2 ).append( ' ' ); }		// keyがゼロ文字列でなければ連結する。
		}

		return nval( buf.toString().trim() , defVal );		// 最後のスペースを削除
	}

	/**
	 * 指定の文字列を、引数の文字で、前後に分割します。
	 * キー=値 や、キー,値 などの文字列を分割して、キーと値の配列を返します。
	 * １番目の配列[0] は、キー(chの左側)、２番目の配列[1] は、値(chの右側) です。
	 * キーも値も、前後のスペースを削除:trim() します。
	 * キー(１番目の配列[0])は、空文字列を許可しません。
	 *
//	 * 分割文字を含まない場合や、キーが、空文字列の場合は、null を返します。
	 * 分割文字を含まない場合や、キーが、空文字列の場合は、長さゼロの配列 を返します。
	 * nullで無い場合は、配列は、必ず２個存在しそのどちらの値も、null を含みません。
	 *
	 * @og.rev 7.3.0.0 (2021/01/06) SpotBugs:null ではなく長さが0の配列を返すことを検討する
	 *
	 * @param	line	１行分の文字列
	 * @param	ch	分割する文字
	 * @return	キーと値の配列。無ければ、長さゼロの配列
	 * @og.rtnNotNull
	 */
	public static String[] split( final String line , final char ch ) {
		final int ad = line.indexOf( ch );
		if( ad > 0 ) {		// ch が先頭は無視
			final String key = line.substring( 0,ad ).trim();

			if( !key.isEmpty() ) {
				final String val = line.substring( ad+1 ).trim();
				return new String[] { key , val };
			}
		}
//		return null;
		return new String[0];
	}

	/**
	 * 数字型文字列の一致をチェックします。
	 *
	 * 処理としては、数字型文字列以外を判定条件に入れても、同一文字列の場合は、
	 * true になります。
	 * ここでは、どちらかの文字列の末尾が、".0" などの場合に、同じかどうかを
	 * 数値に変換して、判定します。
	 * どちらかが、null の場合は、必ず false になります。両方とも、null でも false です。
	 *
	 * @param	val1 判定用の引数１
	 * @param	val2 判定用の引数２
	 * @return	判定結果(一致する場合は、true)
	 */
	public static boolean numEquals( final String val1 , final String val2 ) {
		if( val1 == null || val2 == null ) { return false; }

		if( val1.equals( val2 ) ) {
			return true;
		}
		else {
			try {
//				return Double.parseDouble( val1 ) == Double.parseDouble( val2 );

				// 6.9.8.0 (2018/05/28) FindBugs:浮動小数点の等価性のためのテスト
				// FindBugs では、等価性のための比較は、if (Math.abs(x - y) < .0000001) が推奨されています。
				// return Math.abs( Double.parseDouble( val1 ) - Double.parseDouble( val2 ) ) < .0000001 ;
				return Double.compare( Double.parseDouble( val1 ) , Double.parseDouble( val2 ) ) == 0 ;
			}
			catch( final NumberFormatException ex ) {
				System.err.println( ex.getMessage() );	// 8.0.0.0 (2021/07/31)
//				;	// 数値変換できなかった場合は、無視します。
			}
		}

		return false;
	}

	/**
	 * 変数の置き換え処理を行います。
	 *
	 * これは、org.opengion.fukurou.util.StringUtil#replaceText( String ,String ,String ,UnaryOperator&lt;String&gt; ) の簡易版です。
	 *
	 * これは、単純な変数ではなく、${env.XXX}または、{&#064;ENV..XXX} の XXX を環境変数に置き換えたり、
	 * {&#064;DATE.XXXX} を、日付文字列に置き換えたりする場合に、使用できます。
	 *
	 * fukurou.util.StringUtil#replaceText 版との違いは、1回のみの変換と、ENV と DATE を決め打ちで処理していることです。
	 * fileexec パッケージに、util  パッケージと同じクラス名のStringUtil を作ったのがそもそもの間違いで、
	 * fileexec パッケージ単独で使用していたものを、統合するにあたり、統合しきれなかったメソッドが
	 * 悪さしています。
	 *
	 * @og.rev 7.2.5.0 (2020/06/01) 新規追加
	 *
	 * @param  orgTxt 変換元の文字列
	 * @return  置換処理したテキスト
	 */
	public static String replaceText( final String orgTxt ) {
		// null か、ゼロ文字列か、'{' を含んでいない場合は、そのまま返します。
		if( orgTxt == null || orgTxt.isEmpty() || orgTxt.indexOf( '{' ) < 0 ) { return orgTxt; }

		final StringBuilder buf = new StringBuilder( orgTxt );

		// 環境変数の処理
		int st1 = buf.indexOf( "${env." );
		if( st1 < 0 ) { st1 = buf.indexOf( "{@ENV." ); }				// どちらか
		if( st1 >= 0 ) {												// 環境変数の処理がある
			final int ed1 = buf.indexOf( "}",st1+6 );
			final String envKey = buf.substring( st1+6,ed1 );
			final String envVal = nval( System.getenv( envKey ), "" );	// 未定義の場合は、ゼロ文字列
			buf.replace( st1,ed1+1,envVal );
		}
		else {															// どちらか
			// 日付文字列の処理
			st1 = buf.indexOf( "{@DATE." );
			if( st1 >= 0 ) {											// 日付文字列の処理がある
				final int ed1 = buf.indexOf( "}",st1+7 );
				final String dateKey = buf.substring( st1+7,ed1 );
				final String dateVal = getTimeFormat( dateKey );		// 未定義の場合は、ゼロ文字列
				buf.replace( st1,ed1+1,dateVal );
			}
		}

		return buf.toString();
	}
}
