/*
 * LOGICAL-PARADOX.ORG b-tree library
 * Copyright (C)2004,2005 satoshi akabane(akabane@logical-paradox.org)
 * $Id: BtreeFile.java,v 1.3 2005/05/16 15:36:45 rampil Exp $
 */
package org.logical_paradox.common.btree;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Properties;

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


/**
 * このライブラリで使用するデータファイルを管理するクラス．<br>
 * B-treeの構造に関するデータは，このクラスに格納され，このクラスで出力される．
 * @author satoshi akabane@logical-paradox.org
 * @version $Revision: 1.3 $
 */
public class BtreeFile {
	/*
	 * プロパティで指定するパラメータのキーの値(必須項目)
	 */
	public static final String FILENAME = "btreefile.filename";				// ファイル名
	public static final String MAGIC_NUMBER = "btreefile.magicnumber";		// マジックナンバー(4バイト)
	public static final String MAJOR_VERSION = "btreefile.majorversion";		// メジャーバージョン(整数)
	public static final String MINOR_VERSION = "btreefile.minorversion";		// マイナーバージョン(整数)
	public static final String HEADER_SIZE = "btreefile.headersize";			// ヘッダブロックのサイズ(整数)
	public static final String ORDER = "btreefile.order";					// 次数(整数)
	public static final String KEYSIZE = "btreefile.keysize";				// キーの大きさ(バイト数,整数)

	/*
	 * プロパティで指定するパラメータのキーの値(任意項目)
	 */
	public static final String ROOT_PAGE = "btreefile.rootpage";				// ルートのページ番号(整数)
	public static final String MAX_ASSIGNED_PAGE = "btreefile.maxassignedpage";	// 現在割り当てられている最大のページ番号(整数)

	protected byte[] _magicNumber = new byte[4];				// マジックナンバー(4バイト固定)
	protected byte _majorVersion;								// ファイルフォーマットのメジャーバージョン
	protected byte _minorVersion;								// ファイルフォーマットのマイナーバージョン
	protected int _sizeOfHeader;								// ヘッダのサイズ
	protected long _rootPageNo;								// ルートのページ番号(-1: ルートなし)
	protected int _order;										// 次数
	protected int _keysize;									// キーのバイトサイズ
	protected long _maxAssignedPageNo;						// 現在割り当てられている最大のページ番号
	protected BtreeFactory _factory = null;					// ファクトリ
	protected RandomAccessFileDelegator _btfile;				// 対象ファイル
	protected BtreeAvailableList _fragment;					// B-tree断片化レコードファイル

	/**
	 * デフォルトコンストラクタ
	 *
	 */
	protected BtreeFile() {
	}
	/**
	 * 各種プロパティを指定することのできるコンストラクタ
	 * @param factory ファクトリ
	 * @param prop プロパティ
	 * @exception BtreeException 指定されたハンドルが無効だった場合
	 */
	public BtreeFile(BtreeFactory factory, Properties prop) throws BtreeException {
		_factory = factory;

		setProperty(prop);
		try {
			_btfile = openPhysicalFile(prop.getProperty(BtreeFile.FILENAME));
			// 断片化レコードファイルを作成する
			_fragment = _factory.getBtreeAvailableList(prop.getProperty(BtreeFile.FILENAME)+".fgm");
		} catch(Exception e) {
			throw new BtreeException(e.getMessage());
		}
	}
	/**
	 * 物理ファイルをオープンする
	 * @param filename ファイル名
	 * @return 物理ファイルへの参照
	 * @throws Exception 何らかの例外が発生した場合
	 */
	protected RandomAccessFileDelegator openPhysicalFile(String filename) throws Exception {
		return new CommonRandomAccessFileDelegator(filename, "rw");
	}
	/**
	 * プロパティを設定する
	 * @param prop プロパティ
	 * @throws BtreeException プロパティに間違いがあった場合
	 */
	protected void setProperty(Properties prop) throws BtreeException {
		try {
			// わざと例外が発生しやすいようなコーディングにしている(checkの一環)
			_btfile = openPhysicalFile(prop.getProperty(BtreeFile.FILENAME));
			byte[] magicNumber = prop.getProperty(BtreeFile.MAGIC_NUMBER).getBytes();
			_magicNumber[0] = magicNumber[0];
			_magicNumber[1] = magicNumber[1];
			_magicNumber[2] = magicNumber[2];
			_magicNumber[3] = magicNumber[3];

			setVersion(Integer.parseInt(prop.getProperty(BtreeFile.MAJOR_VERSION)),
					Integer.parseInt(prop.getProperty(BtreeFile.MINOR_VERSION)));

			_sizeOfHeader = Integer.parseInt(prop.getProperty(BtreeFile.HEADER_SIZE));
			_order = Integer.parseInt(prop.getProperty(BtreeFile.ORDER));
			_keysize = Integer.parseInt(prop.getProperty(BtreeFile.KEYSIZE));

			// これ以降はオプションの項目
			String rootpage = prop.getProperty(BtreeFile.ROOT_PAGE);
			if(rootpage != null) {
				_rootPageNo = Long.parseLong(rootpage);
			}
			String maxAssignedPage = prop.getProperty(BtreeFile.MAX_ASSIGNED_PAGE);
			if(maxAssignedPage != null) {
				_maxAssignedPageNo = Long.parseLong(maxAssignedPage);
			}
		} catch(Exception e) {
			throw new BtreeException("プロパティに間違いがあります");
		}
	}
	/**
	 * 現在割り当てられている最大のページ番号を返す
	 * @return ページ番号
	 */
	public long getMaxAssignedPageNo() {
		return _maxAssignedPageNo;
	}
	/**
	 * このファイルをクローズする
	 * @exception IOException クローズに失敗
	 */
	public void close() throws IOException {
		try {
			// 断片化レコードをファイルに出力する処理
			_fragment.flush();
			flush();
		} catch(BtreeException be) {
			throw new IOException(be.getMessage());
		}
		_btfile.close();
		_btfile = null;
	}
	/**
	 * このファイルが閉じているかどうかを返す
	 * @return true:閉じている / false:開いている
	 */
	public boolean isClosed() {
		return _btfile == null;
	}
	/**
	 * 現時点でのルートページ番号を返す
	 * @return long ルートのページ番号
	 */
	public long getRootPageNo() {
		return _rootPageNo;
	}

	/**
	 * マジックヘッダを文字列で返す
	 * @return String マジックナンバー(4文字)
	 */
	public String getMagicNumber() {
		try {
			return new String(_magicNumber, "US-ASCII");
		} catch(UnsupportedEncodingException uee) {
			return null;
		}
	}

	/**
	 * マジックナンバーを設定する
	 * @param magicNumber マジックナンバー(バイト配列4要素)
	 * @exception IllegalArgumentException
	 */
	public void setMagicNumber(byte[] magicNumber) throws IllegalArgumentException {
		if(magicNumber == null || magicNumber.length != 4) {
			throw new IllegalArgumentException("マジックナンバーは要素数4のバイト配列で指定してください");
		}
		_magicNumber = new byte[ magicNumber.length ];
		System.arraycopy(magicNumber, 0, _magicNumber, 0, _magicNumber.length);
	}

	/**
	 * ファイルフォーマットのメジャーバージョンを返す
	 * @return int メジャーバージョン
	 */
	public int getMajorVersion() {
		return (int)_majorVersion;
	}

	/**
	 * ファイルフォーマットのマイナーバージョンを返す
	 * @return int マイナーバージョン
	 */
	public int getMinorVersion() {
		return _minorVersion;
	}

	/**
	 * ファイルのバージョンを設定する
	 */
	public void setVersion(int major, int minor) {
		_majorVersion = (byte)major;
		_minorVersion = (byte)minor;
	}

	/**
	 * ヘッダのサイズを返す
	 *
	 * @return int ヘッダブロックのサイズ
	 */
	public int getHeaderSize() {
		return _sizeOfHeader;
	}

	/**
	 * ファイルサイズを返す
	 * @return long ファイルサイズ
	 */
	public long length() {
		try {
			return _btfile.length();
		} catch(IOException e) {
			return 0;
		}
	}

	/**
	 * ヘッダブロックをリロードする．
	 * リロードをすると，シークポインタが移動するので注意すること<br>
	 * また，ヘッダブロックのロードが完了した場合，データブロックの終端を示している．
	 * @exception IOException ファイルアクセス中にエラーが発生した場合
	 * @exception BtreeException B-treeのデータファイルとして構造がおかしい場合
	 */
	public void reload() throws IOException, BtreeException {
		// シークポインタを先頭から6バイト目に移動する
		// 6バイト目には，32bitでヘッダブロックの長さが格納されているため
		_btfile.seek(6);

		// ヘッダブロックの長さを読み込む
		try {
			_sizeOfHeader = _btfile.readInt();
			_btfile.seek(0);

			// ヘッダブロック全体を読み込む
			// この段階で，ファイルポインタはデータブロックの先頭を示しているはず
			byte[] headerBlock = new byte[_sizeOfHeader];
			_btfile.readFully(headerBlock);
			DataInputStream din = new DataInputStream(new ByteArrayInputStream(headerBlock));

			// マジックナンバーのロード
			_magicNumber[0] = din.readByte();
			_magicNumber[1] = din.readByte();
			_magicNumber[2] = din.readByte();
			_magicNumber[3] = din.readByte();

			// ファイルフォーマットのバージョンをロード
			_majorVersion = din.readByte();
			_minorVersion = din.readByte();

			// ヘッダサイズのロード
			_sizeOfHeader = din.readInt();

			// ルートページ番号のロード
			_rootPageNo = din.readLong();

			// 現在割り当てられている最大のページ番号
			_maxAssignedPageNo = din.readLong();

			din.close();
		} catch(IOException ioe) {
			ioe.printStackTrace();
			throw new BtreeException(ioe.getMessage());
		}
	}

	/**
	 * ヘッダブロックを書き込む．
	 * ヘッダブロックを書き込んだ場合，シークポインタは一度先頭に戻り，書き込んだ後は
	 * データブロックの先頭に移動する．
	 * @exception IOException ファイルアクセス中にエラーが発生した場合
	 * @exception BtreeException B-treeのデータファイルとして構造がおかしい場合
	 */
	public void flush() throws IOException, BtreeException {
		ByteArrayOutputStream bao = new ByteArrayOutputStream(_sizeOfHeader);
		DataOutputStream dos = new DataOutputStream(bao);

		try {
			// マジックナンバー
			dos.write(_magicNumber, 0, _magicNumber.length);

			// ファイルフォーマットのバージョン
			dos.writeByte(_majorVersion);
			dos.writeByte(_minorVersion);

			// ヘッダサイズ
			dos.writeInt(_sizeOfHeader);

			// ルートページ番号
			dos.writeLong(_rootPageNo);

			// 現在割り当てられている最大のページ番号
			dos.writeLong(_maxAssignedPageNo);

			// データブロックの先頭にシークする処理
			byte[] headerBlock = new byte[_sizeOfHeader];
			byte[] storedByteArray = bao.toByteArray();
			System.arraycopy(storedByteArray, 0, headerBlock, 0, storedByteArray.length);
			_btfile.seek(0);
			_btfile.write(headerBlock);
		} catch(ArrayIndexOutOfBoundsException be) {
			be.printStackTrace();
			throw new BtreeException(be.getMessage());
		}
	}
	/**
	 * 新しいページを作成する
	 * 
	 * @return 生成されたページ
	 */
	public BtreePage newPage() throws BtreeException {
		BtreePage btp = null;
		synchronized(this) {
			// ページ領域があるけど使われていないページを探す
			long unusedPageNo = findEmptyPage();
			if(unusedPageNo > -1) {
				// 見つかったので再利用する
				btp = getPage(unusedPageNo);
				btp.init();				// 一応初期化してから返す
				btp.setPageNo(unusedPageNo);
				btp.setEnabled(true);
				return btp;
			}
			// 新しいページを作成する
			_maxAssignedPageNo++;
			btp = new BtreePage(_factory, _maxAssignedPageNo);
			if(_maxAssignedPageNo == 1 && _rootPageNo <= 0) {
				// ルートのページがまだ設定されていない場合，新しく作成されたページをルートとする
				_rootPageNo = _maxAssignedPageNo;
			}
			// ヘッダブロックをストレージに書き込む
			try {
//				flush();
				// ファイルの終端までシークする
				_btfile.seek(_btfile.length());
				// ファイルの終端にページのバイナリストリームを出力する
				_btfile.write(btp.getByteStream());
			} catch(Exception ioe){
				// ヘッダブロックの出力中にエラーになったので，_rootPageNoの整合を保つ
				_maxAssignedPageNo--;
				throw new BtreeException("出力中にエラーが発生");
			}
		}

		return btp;
	}
	/**
	 * 指定されたページをストレージから復元して返す
	 * 
	 * @param pageno ページ番号
	 * @return 復元されたページ
	 * @throws BtreeException 操作中に発生した例外
	 */
	protected BtreePage getPage(long pageno) throws BtreeException {
		synchronized(this) {
			int pageSize = getPageSize();
			if(pageno <= 0) {
				// ページ番号が0以下の場合，ルートのページを取得する
				pageno = getRootPageNo();
				if(pageno <= 0) {
					// ルートページ自体がそもそも存在しない
					return null;
				}
			}
			long seekp = (long)pageSize * (pageno-1) + getHeaderSize();
			try {
				_btfile.seek(seekp);
				byte[] pageStream = new byte[pageSize];
				_btfile.read(pageStream);
				BtreePage btpage = new BtreePage(_factory, pageno, pageStream);

				return btpage;
			} catch(IOException ioe) {
				throw new BtreeException(ioe.getMessage());
			}
		}
	}
	/**
	 * ページのサイズを返す
	 *
	 * @return int ページのバイトサイズ
	 * @see org.logical_paradox.common.btree.BtreeFactory
	 * @see org.logical_paradox.common.btree.BtreePage
	 */
	protected int getPageSize() {
		int keyByteSize = _keysize;

		// ポインタはlongなので8バイトである
		// 最後の4バイトはキーカウント
		return (keyByteSize * _order) + (8 * (_order+1)) + 4;
	}
	/**
	 * 指定されたページ番号に対応するファイル中のシークポインタを返す
	 * @param pageno ページ番号
	 * @return シークポインタ
	 */
	public long calcSeekPointer(long pageno) {
		return (long)getPageSize() * (pageno-1) + getHeaderSize();
	}
	/**
	 * 指定されたページをファイルシステム上で置換する
	 * @param btp 置換対象ページ(存在しない場合は追加)
	 * @throws BtreeException 置換処理時に何らかのI/Oエラーが発生した
	 */
	public void replacePage(BtreePage btp) throws BtreeException {
		byte[] page = btp.getByteStream();
		long seekp = calcSeekPointer(btp.getPageNo());

		try {
			synchronized(this) {
				// ファイルにページを出力する
				_btfile.seek(seekp);
				_btfile.write(btp.getByteStream());

				// 指定されたページが無効状態の場合，ページ番号を-1に設定する
				if(btp.isEnabled() == false) {
					// 断片化レコードとして登録する
					_fragment.getCache().add(new Long(btp.getPageNo()));
					// 下の行は，断片化処理の後に実行する
					btp.setPageNo(-1);
				}
			}
		} catch(IOException ioe) {
			throw new BtreeException(ioe.getMessage());
		}
	}
	/**
	 * ルートページの番号を更新する
	 * 
	 * @param l 新しいページ番号
	 */
	protected synchronized void replaceRootPageNo(long l) throws BtreeException {
		long before = _rootPageNo;
		try {
			_rootPageNo = l;
//			flush();
		} catch(Exception e) {
			// 何が原因か知らんが，ヘッダブロックの更新に失敗したので元に戻す
			_rootPageNo = before;
			e.printStackTrace();
			throw new BtreeException(e.getMessage());
		}
	}
	/**
	 * 未使用領域を探す
	 * 探し方はいろいろあると思うが..
	 * 
	 * @return long スキャンの結果，未使用と判定されたページ番号(-1:発見できなかった)
	 */
	protected synchronized long findEmptyPage() throws BtreeException {
		Long l = (Long)_fragment.getCache().getOne();
		return l == null ? -1 : l.longValue();
	}
}

// end of BtreeFile.java
