/**
 * LOGICAL-PARADOX.ORG variable record manager library
 * Copyright (C)2004 satoshi akabane(akabane@logical-paradox.org)
 *
 * Release 1.0
 * ・初期パージョン
 */
package org.logical_paradox.common.vrm;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import org.logical_paradox.common.io.CommonRandomAccessFileDelegator;
import org.logical_paradox.common.io.RandomAccessFileDelegator;
import org.logical_paradox.common.thread.ReadWriteLock;

/**
 * 可変長レコードファイルマネージャー
 * 可変長サイズのレコードを持つファイルの入出力管理を行うクラス
 * 
 * @author satoshi akabane@logical-paradox.org
 * @version 1.0
 */
public class VariableRecordManager {
	protected String filename;
	protected RandomAccessFileDelegator file;
	protected VRMAvailableList availableList;
	protected RecordFactory factory;

	private ReadWriteLock lock = new ReadWriteLock();

	/**
	 * コンストラクタ
	 * @param filename 管理対象のファイル名
	 * @throws IOException ファイルアクセスに失敗した
	 */
	public VariableRecordManager(String filename, RecordFactory factory) throws IOException {
		this.filename = filename;
		this.factory = factory;
		
		open();
	}
	/**
	 * ファイルをオープンする
	 * ※派生クラスで別のタイプのファイルをオープンしたい場合，このメソッドをオーバーライドする
	 * @throws IOException オープンに失敗した
	 */
	public void open() throws IOException {
		// 読み書きモードでRandomAccessFileをオープンする
		file = new CommonRandomAccessFileDelegator(filename, "rw");
		availableList = new VRMAvailableList(filename + ".vra");
	}
	/**
	 * ファイルを閉じる
	 * @throws IOException ファイルアクセスに失敗した
	 */
	public void close() throws IOException {
		// データファイルを閉じる
		if(file != null) {
			file.close();
			file = null;
		}
		// 利用可能リストを出力する
		if(availableList != null) {
			availableList.flush();
			availableList = null;
		}
	}
	/**
	 * ファイルに新しいレコードを追加する
	 * 
	 * @param r 追加したいレコード
	 * @return 新たに作成されたレコードのバイト変位
	 * @throws IOException レコードの追加に失敗した
	 */
	public long insert(Record r) throws IOException {
		long offset = -1;
		try {
			lock.beginWrite();

			// 再利用可能なレコードがあるかどうかを調べる
//			AvailableRecord emptyRecord = (AvailableRecord)availableList.getCache().getOne();
//			if(emptyRecord == null) {
	AvailableRecord emptyRecord = null;
				offset = file.length();
//			} else if(emptyRecord.getRecordSize() < r.size()) {
//				// 取得した利用可能レコードのサイズが十分でない場合
//				// 一度取り出してしまったので，再度追加しておく
//				offset = file.length();
//				availableList.getCache().add(emptyRecord);
//			} else {
//				offset = availableList.getOffset();
//			}
			// シーク
			synchronized(file) {
				file.seek(offset);
				// ファイルの終端にシークして出力する
				file.write(r.byteStream());
			}
			// ファイルの途中に上書きしてレコードを作成した場合，新しい利用可能レコードの生成を試みる
			if(emptyRecord != null) {
				int emptyRecordSize = emptyRecord.getRecordSize() - r.getRecordSize();
				if(emptyRecordSize > 0) {
					availableList.getCache().add(new AvailableRecord(emptyRecordSize, offset+r.size()));
				}
			}
		} catch(InterruptedException e) {
		} finally {
			try {
				lock.release();
			} catch (InterruptedException e1) {
			}
		}
		return offset;
	}
	/**
	 * 指定されたレコードを更新する
	 * 以前と今度で同じレコードサイズの場合，完全に更新される．<br>
	 * それ以外の場合，以前のレコードが削除され，削除された領域は断片化される．
	 * @param rno レコード番号
	 * @param r レコード
	 * @throws IOException 更新に失敗した
	 */
	public void update(long rno, Record r) throws IOException {
		Record pre = get(rno);
		if(pre.size() == r.size()) {
			// サイズが全く同じの場合
		} else {
			delete(rno);
			insert(r);
		}
	}
	/**
	 * 指定されたレコードを取得する
	 * @param rno レコード番号
	 * @return レコード
	 * @throws IOException レコードの取得に失敗
	 */
	public Record get(long rno) throws IOException {
		try {
			lock.beginRead();
			// とりあえず該当位置へシークする
			synchronized(file) {
				file.seek(rno);
				// レコードサイズが格納されている，先頭4バイトをまず取得する
				byte[] recordSizeBuffer = new byte[4];
				file.read(recordSizeBuffer);
		
				// レコードサイズを復元するために，ByteBufferを生成する
				ByteBuffer buffer = ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN);
				buffer.rewind();
				buffer.put(recordSizeBuffer);
				buffer.rewind();

				int recordSize = buffer.getInt();
				byte[] recordBody = new byte[recordSize];

				// 現在ポイントしているシークポインタから，必要なデータを取得する
				file.read(recordBody);
				// ファクトリを利用してバイト列のレコードをオブジェクトにインスタンス化する
				return factory.getRecord(recordBody);
			}
		} catch(InterruptedException e) {
		} finally {
			try {
				lock.release();
			} catch(InterruptedException e) {
			}
		}
		return null;
	}
	/**
	 * 指定されたレコードを削除する
	 * @param rno 削除したいレコード番号
	 * @throws IOException 削除に失敗した
	 */
	public void delete(long rno) throws IOException {
		// ここでは単に指定されたレコードを利用可能リストに追加するだけ
		// それ以上の処理は必要ない
		Record r = get(rno);
		AvailableRecord ar = new AvailableRecord(r.getRecordSize(), rno);
		availableList.getCache().add(ar);
	}
}
