package jp.agentec.abook.abv.bl.websocket;

import org.java_websocket.WebSocket;
import org.java_websocket.drafts.Draft;
import org.java_websocket.drafts.Draft_17;
import org.java_websocket.handshake.ServerHandshake;
import org.json.adf.JSONArray;
import org.json.adf.JSONException;
import org.json.adf.JSONObject;

import java.net.URI;
import java.net.URLDecoder;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Observable;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import jp.agentec.abook.abv.bl.common.ABVEnvironment;
import jp.agentec.abook.abv.bl.common.exception.ABVRuntimeException;
import jp.agentec.abook.abv.bl.common.exception.HttpUnauthorizedException;
import jp.agentec.abook.abv.bl.common.exception.NetworkDisconnectedException;
import jp.agentec.abook.abv.bl.common.exception.ServerException;
import jp.agentec.abook.abv.bl.common.log.Logger;
import jp.agentec.abook.abv.bl.common.util.JsonUtil;
import jp.agentec.abook.abv.bl.data.ABVDataCache;
import jp.agentec.abook.abv.bl.dto.MeetingDto;
import jp.agentec.abook.abv.bl.observer.RemoteObserver;
import jp.agentec.adf.net.http.HttpParameterObject;
import jp.agentec.adf.net.http.HttpRequestSender;
import jp.agentec.adf.net.http.HttpResponse;
import jp.agentec.adf.util.StringUtil;

public class MeetingManager extends Observable {
	public static final String MEETING_VERSION = "1.5.2";
	private static final String TAG = "MeetingManager";
	public static final int NO_MEETING_ID = -1;

	// ws接続ステータス定数
	public static final int STATUS_READY = 0;
	public static final int STATUS_STARTED = 1;
	public static final int STATUS_ENDED = 2;
	public static final int STATUS_DELETED = 9;
	public static final int STATUS_PEND = 5;

	// コマンド定数
	public static final String CMD_NOTICE = "notice";
	public static final String CMD_MEETINGINFO = "meetinginfo";
	public static final String CMD_OPEN = "open";
	public static final String CMD_MOVEPAGE = "movePage";
	public static final String CMD_TOBOOKSHELF = "toBookShelf";
	public static final String CMD_ACTION = "action";
	public static final String CMD_LINKURLACTION = "linkUrlAction";
	public static final String CMD_IMAGEPREVIEWACTION = "imagePreViewAction";
	public static final String CMD_3DVIEWACTION = "3DViewAction";
	public static final String CMD_MOVIEACTION = "movieAction";
	public static final String CMD_AUDIOACTION = "audioAction";
	public static final String CMD_QUESTIONNAIREACTION = "questionnaireAction";
	public static final String CMD_CLOSEPOPUP = "closePopup";
	public static final String CMD_CURSORMODE = "cursorMode";
	public static final String CMD_CURSOR = "cursor";
	public static final String CMD_APPROVALSTATUS = "approvalStatus";
	public static final String CMD_PRESENTERREQUEST = "presenterRequest";
	public static final String CMD_ZOOM = "zoom";
	public static final String CMD_MARKING = "marking";
	public static final String CMD_CLOSECALL = "closeCall";
	public static final String CMD_OPENCALL = "openCall";
	private static String[] CMD_ARRAY = { CMD_NOTICE, CMD_MEETINGINFO, CMD_OPEN, CMD_MOVEPAGE, CMD_TOBOOKSHELF, CMD_ACTION,
			CMD_LINKURLACTION, CMD_IMAGEPREVIEWACTION, CMD_3DVIEWACTION, CMD_MOVIEACTION, CMD_AUDIOACTION, CMD_QUESTIONNAIREACTION,
			CMD_CLOSEPOPUP, CMD_CURSORMODE, CMD_CURSOR, CMD_APPROVALSTATUS, CMD_PRESENTERREQUEST, CMD_ZOOM, CMD_MARKING ,CMD_OPENCALL, CMD_CLOSECALL};

	// jsonキー定数
	public static final String CMD = "cmd";
	public static final String TIME_STAMP = "timeStamp";
	public static final String CONTENTID = "contentId";
	public static final String PAGE_NUMBER = "pageNumber";
	public static final String OBJECTID = "objectId";
	public static final String FILE_NAME = "fileName";
	public static final String URL = "url";
	public static final String BROWSER = "browser";
	public static final String IMAGE_INDEX = "imageIndex";
	public static final String TYPE = "type";
	public static final String PLAY_TIME = "playTime";
	public static final String IS_FULL_SCREEN = "isFullScreen";
	public static final String IS_RIGHT = "isRight";
	public static final String ALPHA = "alpha";
	public static final String IS_BIG_FONT_SIZE = "isBigFontSize";
	public static final String IS_CURSOR_MODE = "isCursorMode";
	public static final String APPROVED = "approved";
	public static final String LOGIN_ID = "loginId";
	public static final String ID = "id";
	public static final String X = "x";
	public static final String Y = "y";
	public static final String SCALE = "scale";

	public static final String MARKING_ACTION = "action";
	public static final String MARKING_DRAW_ACTION = "drawAction";
	public static final String MARKING_DRAW_MODE = "mode";
	public static final String MARKING_DRAW_WIDTH = "width";
	public static final String MARKING_DRAW_ALPHA = "alpha";
	public static final String MARKING_DRAW_RED = "red";
	public static final String MARKING_DRAW_GREEN = "green";
	public static final String MARKING_DRAW_BLUE = "blue";

	public static final String MARKING_SHARE = "share";

	// JSON value
	public static final String START = "START";
	public static final String PAUSE = "PAUSE";

	public static final String MARKING_ACTION_START = "START";
	public static final String MARKING_ACTION_END = "END";
	public static final String MARKING_ACTION_DRAW = "DRAW";
	public static final String MARKING_ACTION_CLEAR = "CLEAR";
	public static final String MARKING_ACTION_SAVE = "SAVE";
	public static final String MARKING_ACTION_VISIBLE = "VISIBLE";
	public static final String MARKING_ACTION_INVISIBLE = "INVISIBLE";

	private static MeetingManager meetingManager;
	private DefaultWebSocketClient webSocketClient;
	private Thread webSocketClientThread;
	private MessageHandlingThread messageHandlingThread;
	private ExecutorService executor;

	private String mSkey; // セッションキー
	private int joinedMeetingId = NO_MEETING_ID; // 参加しているmeetingId
	private int status = STATUS_READY; // ws通信ステータス
	private boolean isOwner; // オーナーか否か
	private boolean paused; // 一時停止状態

	private ArrayList<MeetingDto> meetingList;
	private BlockingQueue<Object> messageQueue;
	private String lastReceiveMessage;
	private String lastSendMessage;
	private long lastSendTime;
	private long lastAppliedTime;
	private boolean debug;
	private String joinedMeetingPassword;

	
	public static synchronized MeetingManager getInstance() {
		if (meetingManager == null) {
			meetingManager = new MeetingManager();
		}
		return meetingManager;
	}

	/* package */ MeetingManager() {
		debug = ABVEnvironment.getInstance().websocketDebug;
		executor = Executors.newFixedThreadPool(2);
		messageQueue = new LinkedBlockingQueue<Object>();
		messageHandlingThread = new MessageHandlingThread(messageQueue);
	}

	/**
	 * セッションキー(mSkey)取得
	 * 
	 * @param createNew
	 * @return
	 * @throws Exception
	 */
	public String getSessionKey(boolean createNew) throws Exception {
		String sid = ABVDataCache.getInstance().getMemberInfo().sid;
		String urlPath = ABVDataCache.getInstance().getUrlPath();
		return getSessionKey(sid, urlPath, createNew);
	}

	/**
	 * セッションキー(skey)取得
	 * 
	 * @param sid
	 * @param urlPath
	 * @param createNew
	 * @return
	 * @throws Exception
	 */
	public String getSessionKey(String sid, String urlPath, boolean createNew)
			throws Exception {
		if (!createNew) {
			if (mSkey != null) {
				return mSkey;
			}
		}

		String json = httpSend(ABVEnvironment.getInstance().websocketServerHttpUrl + "/auth/basic?sid=" + sid + "&urlpath=" + urlPath, null, HttpRequestSender.MethodGet);
		JSONObject root = new JSONObject(json);
		mSkey = root.getString("skey"); // AppDefType.UserPrefKey.SKEY
		return mSkey;
	}

	/**
	 * 会議室作成
	 * 
	 * @param skey
	 * @param title
	 * @param password
	 * @return
	 * @throws Exception
	 */
	public int createMeeting(final String skey, final String title, final String password) throws Exception {
		CreateMeetingParam param = new CreateMeetingParam(skey, title, password, MEETING_VERSION);
		String json = httpSend(ABVEnvironment.getInstance().websocketServerHttpUrl + "/meetings", param, HttpRequestSender.MethodPost);
		JSONObject root = new JSONObject(json);
		return root.getInt("meetingid");
	}

	/**
	 * 会議室一覧取得
	 * 
	 * @param skey
	 * @return
	 * @throws Exception
	 */
	public List<MeetingDto> getMeetingList(String skey) throws Exception {
		String json = httpSend(ABVEnvironment.getInstance().websocketServerHttpUrl + "/meetings?skey=" + skey, null, HttpRequestSender.MethodGet);
		Logger.v(TAG, "meetings: %s", json);
		JSONObject root = new JSONObject(json);
		JSONArray meetings = JsonUtil.getJSONArray(root, "meetings");
		meetingList = new ArrayList<MeetingDto>();

		for (int i = 0; i < meetings.length(); i++) {
			JSONObject meeting = meetings.getJSONObject(i);
			MeetingDto meetingDto = new MeetingDto();
			meetingDto.meetingId = meeting.getInt("meetingid");
			meetingDto.type = meeting.getInt("type");
			meetingDto.title = URLDecoder.decode(meeting.getString("title"), "UTF-8");
			meetingDto.ownerLoginId = meeting.getString("owner_logind_id");
			meetingDto.ownerName = meeting.getString("owner_name");
			meetingDto.createdate = meeting.getString("createdate");
			meetingDto.attendees = meeting.getInt("attendees");
			meetingDto.presenterskey = meeting.getString("presenterskey");
			meetingDto.appversion = JsonUtil.getString(meeting, "appversion");
			meetingList.add(meetingDto);
		}

		return meetingList;
	}

	/**
	 * 会議室削除
	 * 
	 * @param meetingId
	 * @param skey
	 * @throws Exception
	 */
	public void deleteMeeting(int meetingId, String skey) throws Exception {
		String json = httpSend(ABVEnvironment.getInstance().websocketServerHttpUrl + "/meetings/" + meetingId + "?skey=" + skey, null, HttpRequestSender.MethodDelete);
		Logger.v(TAG, "deleteMeeting: %s", json);
		if (meetingId == joinedMeetingId) {
			joinedMeetingId = NO_MEETING_ID;
			status = STATUS_DELETED;
		}
	}

	public void deleteMeeting() throws Exception {
		deleteMeeting(joinedMeetingId, mSkey);
	}


	public String refreshSkeyIfInvalid() throws Exception {
        return refreshSkeyIfInvalid(mSkey);
	}

	/**
	 * skeyが無効な場合にrefreshする。
	 * （MeetingListの取得メソッドを使うが、結果は用いない）
     * @param skey String
	 * @throws Exception
	 */
    public String refreshSkeyIfInvalid(String skey) throws Exception {
		try {
			getMeetingList(skey);
			return skey;
		} catch (HttpUnauthorizedException e) { // skeyがセッション切れの場合再取得
			Logger.w(TAG, "skey is invalid. Trying to get a new session key.");
			return getSessionKey(true);
		}
	}


	/**
	 * 会議室参加 （ここでWebSocketに接続。ただし接続完了はonOpen()へコールバックされる）
	 * 
	 * @param meetingId
	 * @param skey
	 * @param password
	 * @param isOwner
	 */
	public synchronized void join(int meetingId, String skey, String password, boolean isOwner) {
		if (joinedMeetingId == meetingId && status == STATUS_PEND) {
			return;
		}
		
		this.isOwner = isOwner;
		joinedMeetingPassword = password;
		Draft draft = new Draft_17();
		URI serverUri = URI.create(ABVEnvironment.getInstance().websocketServerWsUrl + "/meetings/" + meetingId + "/join?skey=" + skey + (password == null ? "" : "&password=" + password));
		WebSocket.DEBUG = debug;
		try {
			close();
			status = STATUS_PEND;
			joinedMeetingId = meetingId;
			this.mSkey = skey;
			webSocketClient = new DefaultWebSocketClient(serverUri, draft, this);
			webSocketClientThread = new Thread(webSocketClient);
			webSocketClientThread.start();
			Logger.d(TAG, "webSocketClientThread id=(%s) mSkey=%s", webSocketClientThread.getId(), skey);
		} catch (KeyManagementException e) {
			throw new ABVRuntimeException(e);
		} catch (NoSuchAlgorithmException e) {
			throw new ABVRuntimeException(e);
		}
	}

	/**
	 * WebSocketを閉じる。
	 * 
	 */
	public void close() {
		if (webSocketClient != null) {
			webSocketClient.close();
		}
		if (webSocketClientThread != null) {
			webSocketClientThread.interrupt();
		}
		joinedMeetingId = NO_MEETING_ID;
		status = STATUS_READY;
		paused = false;

		if (messageHandlingThread != null) {
			messageHandlingThread.terminate();
			messageHandlingThread = null;
		}
	}

	@Override
	protected void finalize() throws Throwable {
		super.finalize();
		close();
	}

	/**
	 * メッセージを送信する
	 * 
	 * @param cmd
	 * @param contentId
	 * @param pageNumber
	 * @param objectId
	 * @param other
	 */
	public void sendWs(String cmd, Long contentId, Integer pageNumber, Long objectId, JSONObject other) {
		if (!isConnected()) {
			Logger.e(TAG, "Not started. Ignore message. status=" + status + " cmd=" + cmd);
			return;
		}
		if (!isOwner && !cmd.equals(CMD_PRESENTERREQUEST)) {
			Logger.e(TAG, "Not owner. Ignore message. isOwner= false cmd=" + cmd);
			return;
		}

		JSONObject json = new JSONObject();
		try {
			json.put(CMD, cmd);
			if (contentId != null) {
				json.put(CONTENTID, contentId);
			}
			if (pageNumber != null) {
				json.put(PAGE_NUMBER, pageNumber + 1);
			}
			if (objectId != null) {
				json.put(OBJECTID, objectId);
			}

			if (other != null) {
				for (@SuppressWarnings("unchecked")
				Iterator<String> iterator = other.keys(); iterator.hasNext();) {
					String key = iterator.next();
					json.put(key, other.get(key));
				}
			}

		} catch (JSONException e) {
			Logger.e(TAG, "sendWs json error.", e);
			return;
		}

		sendWs(json.toString());
	}

	/**
	 * メッセージを送信する
	 * 
	 * @param message
	 */
	public synchronized void sendWs(String message) {
		long thisTime = System.currentTimeMillis();
		if (message.equals(lastSendMessage) && thisTime - lastSendTime < 1000) { // 前回と１秒以内に同じメッセージの場合は送らない
			return;
		}
		lastSendMessage = message;
		lastSendTime = thisTime;
		Logger.v(TAG, "Send message. %s", message);
		webSocketClient.send(message);
	}

	/**
	 * HTTP(S)リクエストを送信
	 * 
	 * @param url
	 * @param param
	 * @param method
	 * @return
	 * @throws NetworkDisconnectedException 
	 * @throws TimeoutException 
	 * @throws ExecutionException 
	 * @throws InterruptedException 
	 * @throws HttpUnauthorizedException 
	 * @throws ServerException 
	 * @throws Exception
	 */
	private String httpSend(final String url, final HttpParameterObject param, final String method) throws NetworkDisconnectedException, InterruptedException, ExecutionException, TimeoutException, HttpUnauthorizedException, ServerException {
		Logger.d(TAG, "call api : ");

		if (!ABVEnvironment.getInstance().networkAdapter.isNetworkConnected()) {
			Logger.w(TAG, "network is disconnected.");
			throw new NetworkDisconnectedException();
		}

		Callable<HttpResponse> task = new Callable<HttpResponse>() {
			@Override
			public HttpResponse call() {
				try {
					if (method.equals(HttpRequestSender.MethodGet)) {
						return HttpRequestSender.get(url, null);
					} else if (method.equals(HttpRequestSender.MethodPost)) {
						return HttpRequestSender.post(url, param);
					} else if (method.equals(HttpRequestSender.MethodDelete)) {
						return HttpRequestSender.delete(url);
					}
					return null;
				} catch (Exception e) {
					Logger.e(TAG, "httpSend error. url=" + url, e);
					return null;
				}
			}
		};

		Future<HttpResponse> f = executor.submit(task);
		HttpResponse response = f.get(5000, TimeUnit.MILLISECONDS);
		if (response == null) {
			throw new NetworkDisconnectedException("httpSend failed. url=" + url);
		}

		if (response.httpResponseCode != 200) {
			Logger.e(TAG, "Nework error. url=" + url + " code=" + response.httpResponseCode + " message=" + response.httpResponseMessage + " body=" + response.httpResponseBody);
			if (response.httpResponseCode == 401) {
				throw new HttpUnauthorizedException();
			} else {
				throw new ServerException("Nework error. " + response.httpResponseCode);
			}
		}

		if (Logger.isDebugEnabled()) {
			Logger.v(TAG, "Response. url=%s code=%s message=%s body=%s", url, response.httpResponseCode, response.httpResponseMessage, response.httpResponseBody);
		}
		return response.httpResponseBody;
	}

	// ////////////////////// WebSocket通信受信イベント処理 ////////////////////////////
	
	/**
	 * WebSocket接続完了時コールバック
	 * 
	 * @param handshake
	 */
	public void onOpen(ServerHandshake handshake) {
		Logger.i(TAG, "onOpen:" + handshake.getHttpStatus());
		setStatus(MeetingManager.STATUS_STARTED);
		if (messageHandlingThread == null) {
			messageHandlingThread = new MessageHandlingThread(messageQueue);
		}
		if (!messageHandlingThread.isAlive() || messageHandlingThread.getState() == Thread.State.NEW) {
			try {
				messageHandlingThread.start();
			} catch (IllegalThreadStateException e) { // 無視:
				// isAlive()ではstarted判定ができない。getStateでは止まっている場合もある
			}
		}

		setChanged();
		super.notifyObservers("onOpen:" + handshake.getHttpStatus());
		clearChanged();
	}

	/**
	 * WebSocketメッセージ受信時コールバック
	 * 
	 * @param message
	 */
	public void onMessage(String message) {
		JSONObject json = validate(message);
		if (json == null) { // 不正な場合、通知だけの場合は無視
			return;
		}

		messageQueue.add(json); // キューに追加
	}

	/**
	 * オブザーバーを追加
	 * 
	 * @param observer
	 */
	public void addObserver(RemoteObserver observer) {
		super.addObserver(observer);
	}

	@Override
	public void notifyObservers(Object obj) {
		setChanged();
		try {
			super.notifyObservers(obj);
		} catch (Exception e) {
			Logger.e(TAG, "[onMessage] notifyObservers error.", e);
		}
		clearChanged();
	}

	private JSONObject validate(String message) {
		JSONObject json;
		try {
			json = new JSONObject(message);
			String cmd = json.getString(MeetingManager.CMD);
			
			if (cmd == null || !StringUtil.contains(cmd, CMD_ARRAY)) {// コマンドのチェック
				Logger.w(TAG, "[onMessage] Invalid cmd. " + message);
				return null;
			}

			if (cmd.equals(CMD_OPENCALL) || cmd.equals(CMD_CLOSECALL)) {
				Logger.d(TAG,"json:" + json.toString());
				return json;
			}

			if (cmd.equals(CMD_NOTICE)) { // notice
				Logger.i(TAG, "[onMessage] Notice: " + message);
				return null;
			}

			if (cmd.equals(CMD_MEETINGINFO)) { // meeting info
				Logger.d(TAG, "[onMessage] Meeting info: %s", message);
				String info = JsonUtil.getString(json, "info");// Owner判定を行う
				if (info != null) {
					JSONObject infojson = new JSONObject(info); // なぜかJSONObjectではなくエスケープされた文字列が入っているのでこうせざるを得ない。
					String presenterskey = JsonUtil.getString(infojson, "presenterskey");
					if (presenterskey != null && presenterskey.equals(mSkey)) {
						Logger.i(TAG, "You are an owner.");
						isOwner = true;
					}
				}
				return json; // Toast表示のためActivityHandlingHelperで処理
			}

			lastReceiveMessage = message; // 最新受信メッセージ

			if (isPaused()) { // 休止中
				Logger.v(TAG, "[onMessage] Status Paused. Message ignored.: %s", message);
				return null;
			}

			if (!cmd.equals(CMD_TOBOOKSHELF) && !cmd.equals(CMD_APPROVALSTATUS) && !cmd.equals(CMD_PRESENTERREQUEST)) { // トップに戻ると昇格依頼以外
				if (!json.has(MeetingManager.CONTENTID) || JsonUtil.getInt(json, MeetingManager.CONTENTID) == -1) { // コンテンツIDのチェック
					Logger.w(TAG, "[onMessage] valid contentid is required. " + message);
					return null;
				}
				if (!cmd.equals(CMD_OPEN) && !json.has(MeetingManager.PAGE_NUMBER)) { // ページ番号のチェック
					Logger.w(TAG, "[onMessage] pageNumber is required. " + message);
					return null;
				}
			}
			
			if (json.has(MeetingManager.PAGE_NUMBER)) { // ページ番号（iPadでは1から始まるので1を引く）
				int pageNumber = json.getInt(MeetingManager.PAGE_NUMBER);
				json.put(MeetingManager.PAGE_NUMBER, pageNumber - 1);
			}

		} catch (JSONException e) {
			Logger.e(TAG, "[onMessage] json error. message ignored.", e);
			return null;
		}
		return json;
	}
	
	/**
	 * 最新メッセージを適用する
	 */
	public void applyLastMessage() {
		Logger.v(TAG, "applyLastMessage msg=%s, lastAppliedTime=%s", lastReceiveMessage, new Date(lastAppliedTime));
		if (lastReceiveMessage != null && System.currentTimeMillis() - lastAppliedTime > 10 * 1000L) { // 無限に呼ばれるのを防ぐため前回適用時から10秒経たないと不可
			lastAppliedTime = System.currentTimeMillis();
			onMessage(lastReceiveMessage);
		}
	}

	/**
	 * WebSocket接続断時コールバック
	 * 
	 * @param code
	 *            NORMAL = 1000; 削除 
	 *            GOING_AWAY = 1001; 
	 *            PROTOCOL_ERROR = 1002;
	 *            REFUSE = 1003; 
	 *            NOCODE = 1005; 
	 *            ABNORMAL_CLOSE = 1006; 
	 *            NO_UTF8 = 1007; 
	 *            POLICY_VALIDATION = 1008;
	 *            TOOBIG = 1009;
	 *            EXTENSION = 1010;
	 *            UNEXPECTED_CONDITION = 1011;
	 *            TLS_ERROR = 1015;
	 *            NEVER_CONNECTED = -1; 初期NG
	 *            BUGGYCLOSE = -2;
	 *            FLASHPOLICY = -3;
	 * 
	 * @param reason
	 * @param remote
	 */
	public void onClose(int code, String reason, boolean remote) {
		Logger.i(TAG, "onClose:" + code);
		int beforeStatus = status;
		if (code == -1) {
			setStatus(MeetingManager.STATUS_READY);
		} else if (code == 1000) {
			setStatus(MeetingManager.STATUS_DELETED);
		} else {
			setStatus(MeetingManager.STATUS_ENDED);
		}

		setChanged();
		super.notifyObservers("onClose:" + code + ";" + beforeStatus); // TODO: later Stringの連結ではなくオブジェクトにする
		clearChanged();
	}

	/**
	 * WebSocketエラー時コールバック 
	 * TODO: later いつ呼び出されるのか不明
	 * 
	 * @param e
	 */
	public void onError(Exception e) {
		// setStatus(WebSocketManager.STATUS_ENDED);
		setChanged();
		super.notifyObservers(e);
		clearChanged();
	}

	// ////////////////////// Status Method ////////////////////////////

	
	/**
	 * 送信可能（オーナーかつ接続時）
	 * 
	 * @return
	 */
	public boolean isSendable() {
		return isOwner && isConnected();
	}

	/**
	 * 受信可能（非オーナーかつ接続時）
	 * 
	 * @return
	 */
	public boolean isSubscribed() {
		return !isOwner && isConnected();
	}

	/**
	 * 接続かどうか
	 * 
	 * @return
	 */
	public boolean isConnected() {
		return status == STATUS_STARTED;
	}

	/**
	 * 会議に参加しているか否か （リストの先頭に対象の会議があること）
	 * 
	 * @param meetingDtoList
	 * @return
	 */
	public boolean isEntered(List<MeetingDto> meetingDtoList) {
		return getJoinedMeetingDto(meetingDtoList) != null;
	}
	
	/**
	 * 参加しているMeeting情報を返す
	 * 
	 * @param meetingDtoList
	 * @return
	 */
	public MeetingDto getJoinedMeetingDto(List<MeetingDto> meetingDtoList) {
		if (meetingDtoList != null && !meetingDtoList.isEmpty()) {
			for (MeetingDto meetingDto : meetingDtoList) {
				if (getJoinedMeetingId() == meetingDto.meetingId) {
					return meetingDto;
				}
			}
		}
		return null;
	}
	
	public MeetingDto getJoinedMeetingDto() {
		List<MeetingDto> meetingDtoList;
		try {
			meetingDtoList = getMeetingList(mSkey);
		} catch (Exception e) {
			Logger.e(TAG, "getJoinedMeetingDto failed.", e);
			return null;
		}
		if (meetingDtoList  != null && !meetingDtoList.isEmpty()) {
			for (MeetingDto meetingDto : meetingDtoList) {
				if (getJoinedMeetingId() == meetingDto.meetingId) {
					return meetingDto;
				}
			}
		}
		return null;
	}
	
	public int getParticipantCount() {
		MeetingDto dto = getJoinedMeetingDto();
		if (dto == null) {
			return 0;
		}
		return dto.attendees;
	}
	

	// ////////////////////// Get/Set Method ////////////////////////////

	public void setStatus(int status) {
		this.status = status;
	}

	public int getJoinedMeetingId() {
		return joinedMeetingId;
	}

	public void setJoinedMeetingId(int joinedMeetingId) {
		this.joinedMeetingId = joinedMeetingId;
	}

	public boolean isOwner() {
		return isOwner;
	}

	public void setOwner(boolean isOwner) {
		this.isOwner = isOwner;
	}

	public int getStatus() {
		return status;
	}

	public String getSkey() {
		return mSkey;
	}

	public void setSkey(String skey) {
		this.mSkey = mSkey;
	}

	public boolean isPaused() {
		return paused;
	}

	public void setPaused(boolean paused) {
		this.paused = paused;
		if (!paused) {
			applyLastMessage();
		}
	}

	public String getJoinedMeetingPassword() {
		return joinedMeetingPassword;
	}

	/**
	 * メッセージ処理スレッド Consumer-Producerパターン
	 * 
	 */
	class MessageHandlingThread extends Thread {
		private BlockingQueue<Object> queue;
		private boolean keepRunning = true;

		MessageHandlingThread(BlockingQueue<Object> queue) {
			super("MessageHandlingThread");
			this.queue = queue;
		}

		public void terminate() {
			keepRunning = false;
			interrupt();
		}

		@Override
		public void run() {
			while (keepRunning) {
				try {
					Object message = queue.take();
					notifyObservers(message);
				} catch (InterruptedException e) {
					if (!keepRunning) {
						break;
					}
					Logger.e(TAG, "[MessageHandlingThread] run()", e);
				} catch (Exception e) {
					Logger.e(TAG, "[MessageHandlingThread] run()", e);
				}

			}
		}
	}

}
