/*
 * LOGICAL-PARADOX.ORG
 * Copyright (C)2005 satoshi akabane(akabane@logical-paradox.org)
 * $Id: DomainConnectionProxyImpl.java,v 1.2 2005/09/07 13:38:06 rampil Exp $
 */
package org.logical_paradox.vsan.dfs;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

import org.logical_paradox.vsan.dfs.exception.NoSuchAreaException;
import org.logical_paradox.vsan.domain.Domain;

/**
 * ドメイン接続オブジェクトのプロキシの実装
 * @author satoshi akabane@logical-paradox.org
 * @version $Revision: 1.2 $
 */
public class DomainConnectionProxyImpl extends UnicastRemoteObject implements DomainConnectionProxy {
	/** デフォルトの接続タイムアウト秒数(msec) 1時間 */
	public static final long DEFAULT_CONNECTION_TIMEOUT_SEC = 1000 * 60 * 60;

	/** ドメイン接続オブジェクト */
	private final DomainConnection con;
	/** クライアントノードID */
	private String clientId;
	/** 最終通信日時 */
	private long lastUpdate = 0L;
	/** 接続タイムアウト待ち秒数(msec */
	private long connectionTimeOutSec = DEFAULT_CONNECTION_TIMEOUT_SEC;
	/** リソース回収対象フラグ(true:次のGCで回収される / false:タイムアウトするまで回収されない) */
	private boolean garbageTarget = false;
	/** ドメインファイルシステムコンフィグレーション */
	private final DfsConfig config;

	/**
	 * コンストラクタ．
	 * @param con_ ドメイン接続オブジェクト
	 * @param cni クライアントID
	 * @param config_ コンフィグレーション
	 * @throws RemoteException インスタンス化に失敗
	 */
	public DomainConnectionProxyImpl(DomainConnection con_, String cni, DfsConfig config_) throws RemoteException {
		con = con_;
		clientId = cni;
		config = config_;

		connectionTimeOutSec = config.getDomainSessionTimeout();

		touch();
	}
	/**
	 * 通信マークを付与する．
	 */
	public void touch() {
		lastUpdate = System.currentTimeMillis();
	}
	/**
	 * 関連づいているドメイン接続オブジェクトから，このクライアントを除去する．
	 */
	public void unshare() {
		con.unshare(clientId);
	}
	/**
	 * この接続オブジェクトが有効期限切れかどうかを返す．
	 * 有効期限が切れている場合，セッションタイムアウトとし，GCの対象となる．
	 * @return true:有効期限切れ / false:有効期限未到達
	 */
	public boolean isExpired() {
		return lastUpdate + connectionTimeOutSec < System.currentTimeMillis();
	}

	/**
	 * このプロキシを破棄する．
	 * ドメイン接続オブジェクトの共有から、このプロキシを使用しているクライアントノードを解除する．<br>
	 * このメソッドを呼び出すと、回収要求マークがこのオブジェクトに設定される。<br>
	 * 接続管理スレッドが回収要求マーク設定済みのプロキシを発見した際に、初めてプロキシが破棄される。
	 * @throws RemoteException 接続解除処理に失敗
	 */
	public void disconnect() {
		garbageTarget = true;
	}
	/**
	 * リソース回収対象となっているかどうかを返す．
	 * リソース回収対象となっている場合，次のGCで直ちにこのオブジェクトは回収される．
	 * @return true:次のGCで回収される / false:タイムアウトするまで回収されない
	 */
	public boolean isGarbageTarget() {
		return garbageTarget;
	}
	/**
	 * このプロキシを使用しているクライアントノードIDを返す．
	 * @return クライアントID
	 */
	public String getClientId() {
		return clientId;
	}
	/**
	 * このプロキシに関連づいている，ドメイン接続オブジェクトを返す．
	 * @return ドメイン接続オブジェクト
	 */
	public DomainConnection getConnection() {
		touch();
		return con;
	}
	/**
	 * ドメインに割り当てられているファイルサイズを返す．
	 * @return ファイルサイズ
	 * @throws RemoteException ファイルサイズ取得に失敗した場合に発生する例外
	 */
	public long size() throws RemoteException {
		touch();
		return con.size();
	}
	/**
	 * バイトデータを読み込む．
	 * 最大で配列で指定したサイズバイト分読み込んで格納する．
	 * @param seekp シークポインタ
	 * @param length 読み込むデータのサイズ(単位:バイト)
	 * @return 取得できたドメインデータストリーム
	 * @throws RemoteException 読み込みに失敗した
	 */
	public DomainDataStream read(long seekp, int length) throws RemoteException {
		int recordSize = con.getDomain().getRecordSize();
		long recordBegin = calcRecordNum(seekp);
		int beginOffset = 0;
		
		if(config.isFixed() == false) {
			// 可変長参照モードの場合、オフセットを考慮する
			beginOffset = (int)(seekp % recordSize);
		}

		// 読み込みに必要なバッファサイズを計算する
		int records = length / recordSize;
		if(length % recordSize > 0) {
			records++;
		}
		int bytes = records * recordSize;
		byte[] bb = new byte[bytes];

		// DomainConnectionに対して、データ読み込みを要求する
		// 返却値(readcnt)は、実データサイズ
		int readcnt = con.read(recordBegin, bb);

		// 返却されたデータより指定されたオフセットのほうが大きい場合、
		// 該当するデータが存在しないことになるのでEOF扱い
		if(readcnt - beginOffset < 0) {
			throw new NoSuchAreaException();
		}

		byte[] b = new byte[length];

		// 返却されたデータから、必要な部分だけを返す
		int realLength = (readcnt - beginOffset < length) ? readcnt - beginOffset : length;
		System.arraycopy(bb, beginOffset, b, 0, realLength);

		// 結果セットを作成する
		DomainDataStream dstream = new DomainDataStream(realLength, b);
		return dstream;
	}
	/**
	 * 指定レコードからバイトデータを読み込む．
	 * @param recordBegin 開始レコード番号
	 * @param length 読み込むデータのサイズ(単位:バイト)
	 * @return 取得できたドメインデータストリーム
	 * @throws RemoteException 読み込みに失敗した
	 */
	public DomainDataStream readRecords(long recordBegin, int length) throws RemoteException {
		// シークポインタを自動計算する
		long seekp = con.getDomain().getRecordSize() * recordBegin;
		return read(seekp, length);
	}
	/**
	 * バイト列を書き込む．
	 * @param seekp シークポインタ(-1:空いている位置へ適当に書き込む)
	 * @param b バイト列
	 * @return データの書き込みが開始されたレコード番号
	 * @throws RemoteException 書き込みに失敗した
	 */
	public long write(long seekp, byte[] b) throws RemoteException {
		int recordSize = con.getDomain().getRecordSize();
		if(seekp < 0) {
			// 空いている位置を自動的に検索する
			seekp = con.computeBestSeekPointer(b.length);
		}

		long recordBegin = calcRecordNum(seekp);

		if(config.isFixed() == false) {
			// 可変長参照モードの場合，オフセットを計算しておいて，既存のレコードとデータをマージする必要がある
			int beginOffset = (int)(seekp % recordSize);

			// TODO 可変長参照モードは未実装
		}

		// DACと通信して、データを仮想ストレージに書き込む
		con.write(recordBegin, b);

		return seekp;
	}
	/**
	 * バイト列を書き込む．
	 * @param recordBegin 
	 * @param b バイト列
	 * @throws RemoteException 書き込みに失敗した
	 */
	public long writeRecords(long recordBegin, byte[] b) throws RemoteException {
		// シークポインタを自動計算する
		long seekp = con.getDomain().getRecordSize() * recordBegin;
		return write(seekp, b);
	}
	/**
	 * シークポインタからレコードの開始位置を計算して返す．
	 * @param seekp シークポインタ
	 * @return レコード番号
	 */
	protected long calcRecordNum(long seekp) {
		Domain domain = con.getDomain();
		long recordBegin = seekp / domain.getRecordSize();

		if(config.isFixed() == true) {
			// 固定長参照モードの場合、レコード計算で端数が生じたら1つ進める
			recordBegin += seekp % domain.getRecordSize() > 0 ? 1 : 0;
		}
		return 1 + recordBegin;
	}
}
