module yamalib.log.logwriter;

private import SDL;

private import std.thread;
private import std.stream;
private import std.path;
private import std.string;
private import std.conv;

private import y4d_aux.filesys;
private import ytl.y4d_result;

private import yamalib.date.date;

/// <summary>
/// Log出力ヘルパクラス
/**
1.
logを1行ずつ書き出すと、disk上でfragmentationが生じる。これが気持ち悪い。
そこで、1MBほど事前に確保('\0'を書き込んだファイルを作成)して、そこに
書いていくことを考える。

2.
ファイルは日付別になっていて欲しい。
20060401_1.logのように。
↑日付

ただし、1日のファイルサイズの上限は、1MBとして、それを超えると
のように自動的に次のファイルが生成されるものとする。
 例)
	20060401_2.log
	20060401_4.log
		・
		・

3.
書き込みは1行ずつ。エラーログ等で用いるのでバッファリングは
なしで、即座に書き込む。

4.
最初に書き込むときに、今日の日付が20060401だとすれば
	20060401_1.log
		・
		・
	20060401_6.log
というように存在チェックを行ない、一番最後のファイルに追記していく。
(このとき1MBを超えれば、また新しいファイルに書き出す)

5.
ファイルはぴったり1MBに収まるように設計して欲しい。1MBを超えてはならない。
余ったぶんは、'\0'で埋まっている状態にする。

6.
↑で1MBと書いているのは、実際には、propertyで変更できるように
してある。
*/
/// </summary>
public class LogWriter
{
	// #region publicメソッド
	/// <summary>
	///		コンストラクタ
	/// </summary>
	public this()
	{
		textbuf = new char[][0];
		// 書き込み用のスレッドを開始する。
		streamThread = new Thread(&writeThread);
		streamThread.start();
	}

	/// <summary>書き出すフォルダ位置を指定する</summary>
	/// <example>
	///		SetPath("log");とやれば、log/20060401_1.log に書き出される
	///		(default : "")
	/// </example>
	/// <param name="path">書き出すフォルダ位置</param>
	public y4d_result setPath(char[] path) 
	{
		try
		{
			if ( path !is null && path != "" )	// 空文字の場合は特にディレクトリ作成の必要はない。
			{
				path ~= std.path.sep; // ファイル名と連結させるので/を足しておく
			}
		}
		catch
		{
			path = cast(char[]) ""; // ディレクトリが作成できない場合は、デフォルトに戻す。
			return y4d_result.happen_some_error;
		}
		finally
		{
			outPath = path;
		}

		return y4d_result.no_error;
	}

	/// <summary>
	///	[async]	1行テキストを出力する
	/// </summary>
	/// <remarks>
	///		実際には即座に書き込みは行われない。
	///		いったんtextbufにためられ、別スレッドからファイルに書き込まれる。
	/// </remarks>
	/// <param name="logtext">出力するテキスト</param>
	public y4d_result write(char[] logtext)
	{
		try
		{
			synchronized (this) {
				// バッファに投げる。
				textbuf ~= logtext;
//				textbuf.Enqueue(logtext);	
			}
		}
		catch 
		{
			return y4d_result.happen_some_error;
		}

		return y4d_result.no_error;
	}

	/// <summary>
	///		終了処理。
	/// </summary>
	public void shutdown()
	{
		if (streamThread !is null) {
			// スレッドの終了
			threadExitRequest = true;
			streamThread.wait();
		}
	}
	
	public ~this() {
	}

	//#endregion

	//#region プロパティ
	/// <summary>
	/// 1ファイルの最大サイズ。default = 1MB = 1024*1024
	/// </summary>
	public long fileMaxSize()
	{
		return m_fileMaxSize;
	}
	public long fileMaxSize(long value)
	{
		return m_fileMaxSize = value;
	}
	private long m_fileMaxSize = 1024 * 1024; // デフォルトは1MB

	//#endregion

	//#region privateメソッド

	/// <summary>
	///		同じ日付で最後に開いたファイルがあればそれを開く。
	/// </summary>
	/// <remarks>
	///		このメソッドが成功したときstreamは過去のファイルを開き
	///		書き込み位置がファイルの終端まで移動している。
	/// </remarks>
	/// <returns></returns>
	private bool openLastFile()
	{
		DateTime lastDate = DateUtil.getDateTime();

		// 日付内での番号表記。最後の番号を探す。20060101_1←この番号
		int count = 1;
		while ( std.file.exists(getFileName(lastDate , count)) ) {
			++count;
		}

		if (count == 1) {
			return false; // 過去に書き込まれたファイルはない。
		}
		--count;

		try
		{
			// 最後に書き込んだファイルを開く。
			stream = new File(getFileName(lastDate , count), FileMode.Out | FileMode.In);
			stream.seek(0, SeekPos.End ); 
/*
			// ゼロはtruncateされているので、streamの最後の位置へ。
			long streamLastPos = stream.position;

			byte[] temp = new byte[fileMaxSize - stream.position];
			stream.write(temp); // 末尾までゼロを埋める。
			stream.position(streamLastPos); // zerofill前の位置に戻す。
*/
			this.lastDate = lastDate;

			return true;
		}
		catch
		{
			close();
			return false;
		}
	}


	/// <summary>
	///		書き込みスレッド
	/// </summary>
	private int writeThread()
	{
		
		while ( !threadExitRequest || textbuf.length != 0 )
		{
			while ( textbuf !is null && textbuf.length != 0 )
			{
				if (textbuf is null) {
					break;
				}
				char[] text = null;
				synchronized (this) {
					text = textbuf[0].dup;
					textbuf = textbuf[1..textbuf.length];
				}
				
				if (isFirst) // 起動時の1回だけ、最後に開いたファイルを読みにいってみる。
				{
					isFirst = false;
					if (!openLastFile()) // 現在の日付で、最後に開いたファイルがあればそれを開く
					{
						
						if (!createNewFile())// 新しいファイルを作成する						
						{
							// うーん…新しいファイルが作れないとなると、どうしようもねーんじゃね？
							// throw null;
						}
					}
				}

				// 日付が変更された、次の書き込みでオーバーする場合
				if (stream is null || isNewDate() || overFileMax(text.length))
				{
					if (!createNewFile()) // 新しいファイルを作成する
					{
						// うーん…新しいファイルが作れないとなると、どうしようもねーんじゃね？
						// throw null;
					}
				}

				try
				{
					if (stream !is null)
					{
						stream.writeLine(text);
						stream.flush();
					}
				}
				catch
				{
					close();
				}
			}

			SDL_Delay(100);
		}

		// ストリームを閉じる
		close();

		return 0;
	}

	/// <summary>
	/// ファイルをcloseする。closeするときにTruncateも行なう。
	/// </summary>
	private void close()
	{
		if ( stream !is null )
		{
			/// <summary>
			///		streamのseek位置からfileMaxSizeまで埋められているゼロを削除する。
			/// </summary>
			/// <remarks>このメソッドに失敗してもstreamはdisposeされたりnullにはならない。</remarks>
//			stream.SetLength(stream.Position);	// ストリームのサイズを変更して、末尾を削除してしまう。

			//	↑TruncateFilledZero();
			
			stream.close();
			stream = null;
		}
	}

	private bool isFirst = true; // 初回起動時フラグ

	/// <summary>
	///		前回の呼び出しから、日付が変更になっているかどうかチェックする
	/// </summary>
	/// <returns>true:日付が変更されている/false:前回と同じ日付</returns>
	private bool isNewDate()
	{
		return DateUtil.getDateTime().day != lastDate.day;
	}

	/// <summary>
	///		次のテキストを書き込むとファイルが規定サイズをオーバーするかどうかチェックする。
	/// </summary>
	/// <param name="logText">書き込むテキスト</param>
	/// <returns>true:オーバーする/false:オーバーしない</returns>
	private bool overFileMax( int length )
	{
		return !( stream.position() + length < fileMaxSize );
	}

	/// <summary>
	///		新しい保存ファイル名を作成する
	/// </summary>
	/// <param name="date">新しいファイル名を作成するときの日付</param>
	/// <returns>YYYYMMDD_Xという形式で新しいファイル名を返す。</returns>
	private char[] getNewFileName( inout DateTime date )
	{
		int count = 1;
		// 日付内での番号表記。次の番号を探す。
		// 20060101_1←この番号
		while ( std.file.exists(getFileName(date, count)) ) {
			++count;
		}

		return getFileName(date, count);
	}

	/// <summary>
	/// ログファイル名を生成する。
	/// </summary>
	/// <param name="fileHead"></param>
	/// <param name="count"></param>
	/// <returns></returns>
	private char[] getFileName(inout DateTime date , int count)
	{
		return cast(char[]) std.string.format("%s%s_%s.txt" ,
				outPath ,
				DateUtil.format(date,cast(char[]) "yyyyMMdd") ,	// 20060101形式で出力
				std.string.toString(count)
			);
	}

	/// <summary>
	///		新しいファイルを作成し、fileMaxSizeの空データを書き込む
	/// </summary>
	/// <remarks>
	///		現在開いているファイルがある場合はcloseされる。
	///		成功するとstreamメンバは新しいファイルに書き込み可能な状態になる。
	///		失敗すると stream は null になる。
	/// </remarks>
	/// <returns>true:成功 , false:失敗</returns>
	private bool createNewFile()
	{
		close();

		try
		{
			DateTime writeFileDate = DateUtil.getDateTime();
			char[] fileName = getNewFileName(writeFileDate);
//			stream = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
			stream = new File(fileName, FileMode.Out | FileMode.In);

/*
			// 0埋めしなくてよい			
			const int tmpSize = 1024;
			byte[] temp = new byte[tmpSize];
			for ( long i = 0 ; i < fileMaxSize / tmpSize ; ++i ) {
				stream.write(temp);
			}
			int modSize = cast( int ) (fileMaxSize % tmpSize);
			if (modSize != 0) {
				stream.Write(temp);
			}

			close();

			// 書き込めるようにファイルを開く
			stream = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite);
*/
			lastDate = writeFileDate; // 日付を更新しておく。
		}
		catch
		{
			close();
			return false;
		}

		return true;
	}
	
	// 書き込みテキストのリスト
	private char[][] textbuf = null;

	// 書き込み用のスレッド
	private Thread streamThread = null;

	// 書き込み用スレッドの終了フラグ
	private bool threadExitRequest = false;

	// 出力先のディレクトリ名
	private char[] outPath;

	//	ファイルを生成するときに使った日付
	private DateTime lastDate;

	//	生成しているログファイル
	private File stream = null;

	//#endregion
}