/*
 * LOGICAL-PARADOX.ORG
 * Copyright (C)2005 satoshi akabane(akabane@logical-paradox.org)
 * $Id: Database.java,v 1.4 2005/10/11 16:13:34 rampil Exp $
 */
package org.logical_paradox.vsan.node;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.logical_paradox.common.io.CommonRandomAccessFileDelegator;
import org.logical_paradox.common.io.RandomAccessFileDelegator;
import org.logical_paradox.common.util.StringUtils;
import org.logical_paradox.vsan.dac.DacConfig;
import org.logical_paradox.vsan.dac.DacConstant;
import org.logical_paradox.vsan.dac.DacProperty;
import org.logical_paradox.vsan.dac.DomainSynchronizer;
import org.logical_paradox.vsan.dac.NodeContext;
import org.logical_paradox.vsan.dac.ResponseCode;
import org.logical_paradox.vsan.node.api.DataStreamIoApi;
import org.logical_paradox.vsan.node.dbr.DbFile;
import org.logical_paradox.vsan.node.dbr.DbRecord;
import org.logical_paradox.vsan.node.dbr.RecordSet;

/**
 * ドメインデータベースファイル
 * @author satoshi akabane@logical-paradox.org
 * @version $Revision: 1.4 $
 */
public class Database {
	/** ロガー */
	private static final Log log = LogFactory.getLog(Database.class);

	/** マジックナンバー */
	public static final String MAGIC_NUMBER = "VSDD";

	/** 参加しているドメイン名 */
	private String domain;
	/** ファイルフォーマットのメジャーバージョン */
	private int majorVersion;
	/** ファイルフォーマットのマイナーバージョン */
	private int minorVersion;
	/** レコードのサイズ */
	private int recordSize = 0;
	/** 最終最適化日時 */
	private Timestamp lastOptimizeTime;
	/** データがストアされていないレコード番号群の開始位置(=現在利用中の最大のレコード番号+1) */
	private long recordBeginOfEmpties = 1;

	/** データファイル */
	private List datafiles = new ArrayList();
	/** ノードコンテキスト */
	private final NodeContext context;
	/** ランダムアクセスファイル */
	private RandomAccessFileDelegator file;


	/*==========================================================================
	 * API
	 *========================================================================**/
	/** データ入出力API */
	private DataStreamIoApi apiDataStreamIo;

	/**
	 * コンストラクタ
	 * @param ctx ディスクアレイノードコンテキスト
	 */
	public Database(NodeContext ctx) throws DatabaseAccessException {
		context = ctx;
		createApi();
	}
	/**
	 * データベース操作用API群を生成する
	 */
	private void createApi() throws DatabaseAccessException {
		log.info("APIをロードします");
		try {
			// データストリームI/O APIをロードする
			Class clazz = Class.forName(DacProperty.getProperty(DacConstant.PKEY_API_DATASTREAM_IO));
			Constructor constructor = clazz.getConstructor(new Class[]{Database.class});
			apiDataStreamIo = (DataStreamIoApi)constructor.newInstance(new Object[]{this});
			log.info("データストリームI/O APIをロードしました");
		} catch (Exception e) {
			log.error("データストリームI/O APIのロードに失敗しました");
			throw new DatabaseAccessException(ResponseCode.DATABASE_SYSTEM_ERROR);
		}
		log.info("APIのロードが完了しました");
	}
	/*=========================================================================
	 * DiskArrayController側へ提供するAPI
	 *========================================================================*/
	/**
	 * ファイルをクローズする
	 */
	public void close() {
		if(file != null) {
			log.info("データベースを閉じています");
			synchronized(file) {
				// 全てのデータファイルを安全に閉じる
				for(Iterator it = datafiles.iterator(); it.hasNext();) {
					DbFile dbf = (DbFile)it.next();
					try {
						dbf.close();
					} catch (IOException e1) {
						log.warn("データファイル[" + dbf.getFilename() + "]のクローズ処理中にI/O例外が発生しました", e1);
					}
				}
				try {
					file.close();
				} catch (IOException e) {
					// 無視
				} finally {
					file = null;
					log.info("データベースを閉じました");
				}
			}
			try {
				store();
			} catch (IOException e) {
				log.warn("データベース情報ファイルのクローズに失敗しました", e);
			}
		}
	}
	/**
	 * データベースをフォーマットする
	 * データベースファイルの初期化は行わない(ヘッダのみ)
	 * @param dn ドメイン名
	 * @param major メジャーバージョン
	 * @param minor マイナーバージョン
	 * @param rs レコードサイズ
	 */
	public void format(String dn, int major, int minor, int rs) throws IOException {
		log.info("ドメインデータベースファイルをフォーマットしています");
		setDomain(dn);
		setMajorVersion(major);
		setMinorVersion(minor);
		setRecordSize(rs);
		recordBeginOfEmpties = 1;

		// ヘッダ情報を出力する
		store();
		log.info("ドメインデータベースファイルのフォーマットが完了しました");

		// ドメインに参加している場合，ドメインからの離脱を通達する
		if(context.isJoined() == true) {
			DomainSynchronizer.leave(context);
		}
	}
	/**
	 * ドメインデータベースを破棄する
	 * @throws IOException
	 */
	public void dispose() throws IOException {
		log.info("ドメインデータベースを破棄しています");

		// ドメインに参加している場合，先にドメインからの離脱を通達する
		if(context.isJoined() == true) {
			DomainSynchronizer.leave(context);
		}

		synchronized(file) {
			// 最初に全てのデータファイルとドメインデータベース情報ファイルをクローズする
			close();

			for(Iterator it = datafiles.iterator(); it.hasNext();) {
				DbFile dbf = (DbFile)it.next();
				if(new File(context.getConfig().getDatabaseDir() + File.separator + dbf.getFilename()).delete() == false) {
					throw new IOException("[" + dbf.getFilename() + "]の削除に失敗しました。手動で削除してください。");
				}
			}

			// ドメインデータベース情報ファイルの削除
			if(new File(context.getConfig().getDatabaseDir() + File.separator + getFilename()).delete() == false) {
				throw new IOException("[" + getFilename() + "]の削除に失敗しました。手動で削除してください。");
			}

			// データファイルに関するオブジェクトを全て初期化
			file = null;
			domain = null;
			majorVersion = minorVersion = 0;
			lastOptimizeTime = null;
			recordSize = 0;
			recordBeginOfEmpties = 1;
			datafiles.clear();
		}

		log.info("ドメインデータベースを破棄しました");
	}
	/**
	 * 新しいデータベースファイルをこのノードに追加する
	 * @param recordBegin 開始レコード番号
	 * @param recordEnd 終了レコード番号
	 */
	public void expand(long recordBegin, long recordEnd) throws IOException {
		synchronized(file) {
			log.info("データファイルを追加しています");
			DbFile dbf = new DbFile(recordBegin, recordEnd, recordSize);
			// データベースファイルを初期化する
			dbf.init(context.getConfig().getDatabaseDir());
			datafiles.add(dbf);

			// ヘッダ情報を出力する
			store();
			log.info("データファイルの追加が完了しました[" + dbf.getFilename() + "]");
		}

		// ドメインに参加している場合，ドメインに更新内容を通知する
		if(context.isJoined() == true) {
			DomainSynchronizer.sync(context);
		}
	}
	/**
	 * データベースファイルの最適化を実施する
	 * 断片化されたレコードを連続的な位置へ移動する
	 * @throws IOException 最適化処理中にI/Oエラーが発生した
	 */
	public void optimize() throws IOException {
		synchronized(file) {
			// 最終最適化日時を更新する
			setLastOptimizeTime(new Timestamp(System.currentTimeMillis()));

			// ヘッダ情報を出力する
			store();
		}
	}
	/**
	 * 指定されたレコード番号を先頭に、入力ストリームからデータを取得してデータファイルへ書き込む
	 * @param recordBegin 開始レコード番号
	 * @param in データソースたる入力ストリーム
	 * @param size データサイズ
	 * @throws DatabaseAccessException パラメータの不正などが原因で処理を続行できなかった
	 * @throws IOException クライアントとの通信に障害が発生した
	 */
	public void storeStream(long recordBegin, InputStream in, int size) throws DatabaseAccessException, IOException {
		long recordEnd = apiDataStreamIo.storeStream(recordBegin, in, size);
		recordBeginOfEmpties = recordEnd+1;
	}
	/**
	 * 指定された番号のレコードを返す
	 * @param recordno レコード番号の配列
	 * @return 復元されたレコードの配列
	 * @throws DatabaseAccessException パラメータの不正などが原因で処理が続行できなかった
	 * @throws IOException クライアントとの通信に障害が発生した
	 */
	public DbRecord[] restoreStream(long[] recordno) throws DatabaseAccessException, IOException {
		return apiDataStreamIo.restoreStream(recordno);
	}
	/**
	 * ディスクアレイに関する情報を返す．
	 * @return ディスクアレイに関する情報を格納したオブジェクト．
	 */
	public DiskArrayNode getNodeInfo() {
		DiskArrayNode node = new DiskArrayNode();
		DacConfig config = context.getConfig();

		// ノード名
		node.setHost(config.getHost() + ":" + config.getPort());
		// レコードセット
		synchronized(datafiles) {
			for(Iterator it = datafiles.iterator(); it.hasNext();) {
				node.add((RecordSet)it.next());
			}
		}

		return node;
	}
	/*=========================================================================
	 * Databaseクラス内で使用する下位サービスルーチン
	 *========================================================================*/
	/**
	 * データベース情報ファイルを物理ファイルに出力する
	 * @throws IOException 入出力エラー
	 */
	protected void store() throws IOException {
		log.info("ドメインデータベース情報ファイルを更新しています");

		ByteBuffer buffer = ByteBuffer.allocate(4096).order(ByteOrder.BIG_ENDIAN);

		// マジックナンバー
		buffer.put(MAGIC_NUMBER.getBytes());
		// フォーマットバージョン(メジャーバージョン)
		buffer.put((byte)majorVersion);
		// フォーマットバージョン(マイナーバージョン)
		buffer.put((byte)minorVersion);
		// レコードサイズ
		buffer.putInt(recordSize);
		// 未使用領域の先頭
		buffer.putLong(recordBeginOfEmpties);
		// 最終最適化日時
		if(lastOptimizeTime == null) {
			lastOptimizeTime = new Timestamp(System.currentTimeMillis());
		}
		buffer.putLong(lastOptimizeTime.getTime());
		// ドメイン名
		buffer.put((domain + "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0").substring(0, 32).getBytes());

		// データファイル
		buffer.put((byte)datafiles.size());
		for(Iterator it = datafiles.iterator(); it.hasNext();) {
			DbFile dbf = (DbFile)it.next();
			// 開始レコード
			buffer.putLong(dbf.getRecordBegin());
			// 終了レコード
			buffer.putLong(dbf.getRecordEnd());
		}

		if(file == null) {
			file = createPhysicalFileDelegator(context.getConfig().getDatabaseDir() + File.separator + getFilename());
		} else {
			file.seek(0);
		}
		file.write(buffer.array());

		log.info("ドメインデータベース情報ファイルの更新が完了しました");
	}
	/**
	 * データベース情報ファイルの内容を取り込む
	 * @throws IOException 入出力エラー
	 */
	public void restore() throws IOException {
		if(file != null) {
			file.seek(0);
		} else {
			file = createPhysicalFileDelegator(context.getConfig().getDatabaseDir() + File.separator + getFilename());
		}
		if(file.length() == 0L) {
			return;
		}

		log.info("ドメインデータベース情報ファイルを読み込んでいます");

		byte[] buf = new byte[4096];
		file.read(buf);

		ByteBuffer buffer = ByteBuffer.allocate(buf.length).order(ByteOrder.BIG_ENDIAN);
		buffer.put(buf);
		buffer.rewind();

		// マジックナンバー
		String mh = new String(new byte[]{ buffer.get(), buffer.get(), buffer.get(), buffer.get() });
		if(MAGIC_NUMBER.equals(mh) == false) {
			throw new IOException();
		}
		// フォーマットバージョン(メジャー)
		setMajorVersion((int)buffer.get());
		// フォーマットバージョン(マイナー)
		setMinorVersion((int)buffer.get());
		// レコードサイズ
		setRecordSize(buffer.getInt());
		// 未使用領域の先頭
		recordBeginOfEmpties = buffer.getLong();
		// 最終最適化日時
		setLastOptimizeTime(new Timestamp(buffer.getLong()));
		// ドメイン名
		byte[] dn = new byte[32];
		buffer.get(dn);
		setDomain(new String(dn).trim());

		// データファイル
		int filecnt = buffer.get();
		for(int i = 0; i < filecnt; i++) {
			long recordBegin = buffer.getLong();
			long recordEnd = buffer.getLong();

			// データファイルを開いた状態にする
			DbFile dbf = new DbFile(recordBegin, recordEnd, recordSize);
			dbf.open(context.getConfig().getDatabaseDir());
			datafiles.add(dbf);
		}

		log.info("ドメインデータベース情報ファイルの読み込みが完了しました");
	}
	/**
	 * データファイルのリストを返す
	 * 同じパッケージに存在しているクラスからのみアクセス可能
	 * @return データファイルのリスト
	 */
	List getDatafiles() {
		return datafiles;
	}
	/**
	 * データファイルを追加する
	 * @param dbf データファイル
	 */
	protected void addDataFile(DbFile dbf) {
		datafiles.add(dbf);
	}
	/**
	 * データベース情報ファイルの名前を返す
	 * @return ファイル名
	 */
	public String getFilename() {
		return context.getNodeId() + "." + getDomain() + DacConstant.DBI_EXT;
	}
	/**
	 * データファイルのファイル名を返す
	 * @return データファイルの情報
	 */
	public String[] getDatafileSpec() {
		synchronized(datafiles) {
			String[] dbfspecs = new String[datafiles.size()];
			int i = 0;
			for(Iterator it = datafiles.iterator(); it.hasNext(); i++) {
				DbFile dbf = (DbFile)it.next();
				dbfspecs[i] = dbf.toString();
			}
			return dbfspecs;
		}

	}
	/**
	 * ドメイン名を返す
	 * @return ドメイン名
	 */
	public String getDomain() {
		return domain;
	}

	/**
	 * 最終最適化日時を返す
	 * @return 最終最適化日時
	 */
	public Timestamp getLastOptimizeTime() {
		return lastOptimizeTime;
	}

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

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

	/**
	 * レコードサイズを返す
	 * @return レコードサイズ
	 */
	public int getRecordSize() {
		return recordSize;
	}

	/**
	 * ドメイン名を設定する
	 * ただし1度しか設定できない
	 * @param string ドメイン名
	 */
	public void setDomain(String string) {
		if(StringUtils.isEmpty(domain) == true) {
			domain = string;
		}
	}

	/**
	 * 最終最適化日時を設定する
	 * @param timestamp 最終最適化日時
	 */
	private void setLastOptimizeTime(Timestamp timestamp) {
		lastOptimizeTime = timestamp;
	}

	/**
	 * ファイルフォーマットのメジャーバージョンを設定する
	 * ただし1度しか設定できない
	 * @param i メジャーバージョン
	 */
	public void setMajorVersion(int i) {
		if(majorVersion <= 0) {
			majorVersion = i;
		}
	}

	/**
	 * ファイルフォーマットのマイナーバージョンを設定する
	 * ただし1度しか設定できない
	 * @param i マイナーバージョン
	 */
	public void setMinorVersion(int i) {
		if(minorVersion <= 0) {
			minorVersion = i;
		}
	}

	/**
	 * レコードサイズを設定する
	 * ただし，既に設定されている場合は変更できない
	 * @param i レコードサイズ
	 */
	public void setRecordSize(int i) {
		if(recordSize <= 0) {
			recordSize = i;
		}
	}

	/**
	 * 未使用レコード番号の先頭位置を返す．
	 * 現在データがストアされているレコード番号の終端位置+1のレコード番号を返却する．
	 * @return 未使用レコード番号の先頭位置
	 */
	public long getRecordBeginOfEmpties() {
		return recordBeginOfEmpties;
	}

	/**
	 * 物理ファイルに対するデリゲーターを生成して返す
	 * @param filename ファイル名
	 * @return 生成されたデリゲーター
	 * @throws FileNotFoundException ファイルが存在しなかった
	 */
	protected RandomAccessFileDelegator createPhysicalFileDelegator(String filename) throws FileNotFoundException {
		return new CommonRandomAccessFileDelegator(filename, "rw");
	}
}
