/*
 * ナビゲーション要求クラス
 * 
 * 作成日: 2006/05/11
 */
package jp.co.ntt.lms.imode.lo.scorm.engine;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.xml.transform.stream.StreamResult;

import jp.co.ntt.lms.lo.util.UserIDFolder;
import jp.co.ntt.lms.lo.scorm.engine.Engine;
import jp.co.ntt.lms.lo.scorm.engine.ExecuteParameters;
import jp.co.ntt.lms.lo.scorm.engine.SCORMAPIAdapter;
import jp.co.ntt.lms.lo.scorm.kernel.Activity;
import jp.co.ntt.lms.lo.scorm.kernel.exception.SequencingException;
import jp.co.ntt.lms.lo.scorm.kernel.exception.SystemException;
import jp.co.ntt.lms.lo.scorm.util.Debug;
import jp.co.ntt.lms.lo.scorm.util.SG;
import jp.co.ntt.lms.lo.scorm.util.XMLUtil;
import jp.co.ntt.lms.xmo.util.DebugLog;
import jp.co.ntt.lms.xms.framework.XmsException;
import jp.co.ntt.lms.xms.framework.XmsSystemException;
import jp.co.ntt.lms.xms.framework.XmsWarningException;

import org.w3c.dom.Document;

/**
 * ナビゲーション要求クラス<br>
 * SCORMエンジンにナビゲーションコマンドをリクエストします。<br>
 * 
 * <pre>
 * コマンド種別 
 * ・学習開始
 * ・前頁
 * ・次頁
 * ・中断
 * ・終了
 * </pre>
 * 
 * @author ntc-sakai
 * 
 */
public class NavigateEngine {

	private String scoUrl;
	private String aiccSid;
	private boolean endLearn;

	/**
	 * SCORMエンジンにリクエスト要求します。
	 * 
	 * @param request リクエスト
	 * @param pCommand 要求コマンド
	 * @param activityID アクティビティID
	 * @throws XmsException
	 */
	public void execute(HttpServletRequest request, String pCommand, String activityID)
		throws XmsException {
		// ＬＯＧ出力
		DebugLog.write(getClass(), "[request]:HEAD command = " + pCommand, DebugLog.ROW);

		endLearn = false;
		HttpSession session = request.getSession();
		ServletContext application = session.getServletContext();

		try {
			// 起動パラメータをsessionから取得する
			ExecuteParameters executeParameters =
				(ExecuteParameters) session.getAttribute("SCORM.EXECUTE_PARAMETERS");

			// sessionから取得できなかった場合 → Session Timeout 発生時
			if (executeParameters == null) {
				// ＬＯＧ出力
				DebugLog.write(getClass(),
						"[request]SessionTimeout(executeParameters is null)", DebugLog.HIGHT);
				throw new XmsWarningException("SessionTimeout");
			}

			// sessionからapi adapterを取得する
			SCORMAPIAdapter apiAdapter =
				(SCORMAPIAdapter) session.getAttribute("SCORM.API_ADAPTER");

			// sessionから取得できなかった場合 → Session Timeout 発生時
			if (apiAdapter == null) {
				// ＬＯＧ出力
				DebugLog.write(getClass(),
						"[request]SessionTimeout(apiAdapter is null)",
						DebugLog.HIGHT);
				throw new XmsWarningException("SessionTimeout");
			}

			// エンジンをapplicationから取得する
			String engineKeyName = "SCORM." + executeParameters.getUserID()
								 + "."  + executeParameters.getLOID() + ".ENGINE";

			Engine engine = (Engine) application.getAttribute(engineKeyName);

			// applicationから取得できなかった場合 → 既に教材が終了している場合 → Session Timeoutに見せる
			if (engine == null) {
				// ＬＯＧ出力
				DebugLog.write(getClass(),
						"[request]SessionTimeout(Engine is null)", DebugLog.HIGHT);
				throw new XmsWarningException("SessionTimeout");
			}

			// リクエストナビゲーションコマンドを取得する
			String command = pCommand;

			if (apiAdapter.getCommunicationModule() != null) {
				// "cmi.exit"の値を取得する
				String exitStatus =
					apiAdapter.getCommunicationModule().getValue("cmi.exit");

				if (exitStatus != null) {
					// "cmi.exit"の値が"time-out"の場合は、ExitAllを実行する
					if (exitStatus.equals("time-out")) {
						command = "ExitAll";
					}
					// "cmi.exit"の値が"logout"の場合は、SuspendAllを実行する
					else if (exitStatus.equals("logout")) {
						command = "SuspendAll";
					}
				}
			}

			boolean result;
			StringBuffer SCOURL = null;

			try {
				// アクティビティID取得
				if (activityID == null) {
					activityID = "";
				}
				// エンジンにナビゲーション要求を行う
				result = engine.navigate(command, activityID);

				// 正常終了した場合は、シーケンシング例外をクリアする
				engine.setLastSequencingException(null);

				// 「学習継続可能」な場合
				if (result) {
					// カレントアクティビティが存在するかどうかチェックする
					if (engine.getCurrentActivity() != null) {
						int attemptCount = 0;

						if (engine.getCurrentActivity().getActivityProgressStatus()) {
							attemptCount =
								engine.getCurrentActivity().getActivityAttemptCount();
						}

						// AICCログの格納フォルダを作成する
						StringBuffer learningLogFolder = new StringBuffer();

						learningLogFolder.append(userIDPath(SG.get("LearningLogBaseURL"), executeParameters.getUserID()));
						learningLogFolder.append(File.separator);
						learningLogFolder.append(executeParameters.getLOID());
						learningLogFolder.append(File.separator);
						learningLogFolder.append(executeParameters.getStudySessionCount());
						learningLogFolder.append(File.separator);
						learningLogFolder.append(executeParameters.getLaunchCount());
						learningLogFolder.append(File.separator);
						learningLogFolder.append(engine.getCurrentActivity().getIdentifier());
						learningLogFolder.append(File.separator);
						learningLogFolder.append(new DecimalFormat("0000000000").format(attemptCount));
						learningLogFolder.append(File.separator);
						learningLogFolder.append(
								new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()));
						learningLogFolder.append(File.separator);

						// (次)SCOへのURLを取得する
						String baseSCOURL = engine.getURL(learningLogFolder.toString());
						// (次)SCOのURLが取得できた場合
						if (baseSCOURL != null) {
							scoUrl = getNextSco(baseSCOURL);
							aiccSid = getAiccSid(baseSCOURL);
						}

						// api adapterをカレントアクティビティのapi adapterに変更する
						apiAdapter.setCommunicationModule(engine.getCommunicationModule());

					} else {
						// ナビゲーション要求エラー
						// ＬＯＧ出力
						DebugLog.write(getClass(),
										"[request]Navigation reqest error command = "
										+ pCommand	+ " ActivityID = "
										+ request.getParameter("ActivityID"),
										DebugLog.HIGHT);
					}
				}
			} catch (SequencingException e) {
				Debug.write(e);

				// シーケンシング例外が発生した場合
				result = true;
				engine.setLastSequencingException(e);
				throw new XmsSystemException(e);
			}

			Document traceLog = engine.getTraceLog();

			// トレースログが存在する場合
			if (traceLog != null) {
				// トレースログのフルパス名を作成する
				StringBuffer navtraceLogFile = new StringBuffer();

				navtraceLogFile.append(userIDPath(SG.get("ResumeDataBaseURL"), executeParameters.getUserID()));
				navtraceLogFile.append(File.separator);
				navtraceLogFile.append(executeParameters.getLOID());
				navtraceLogFile.append(File.separator);
				navtraceLogFile
						.append(executeParameters.getStudySessionCount());
				navtraceLogFile.append(File.separator);
				navtraceLogFile.append(executeParameters.getLaunchCount());
				navtraceLogFile.append(File.separator);
				navtraceLogFile.append("navtrace.xml");

				// トレースログを「追加モード」でオープンする
				BufferedOutputStream bos = new BufferedOutputStream(
						new FileOutputStream(navtraceLogFile.toString(), true));

				try {
					// navtrace.xmlにトレースログを追加する
					XMLUtil.output(traceLog.getDocumentElement(), true, SG
							.get("XMLEncoding"), new StreamResult(bos));
				} finally {
					bos.close();
				}
			}

			// 「学習中断」もしくは「学習完了」の場合
			if (!result) {
				// api adapterを未設定状態に設定する
				apiAdapter.setCommunicationModule(null);

				// root activityを取得する
				Activity rootActivity = engine.getRootActivity();
				String exitResult;

				// activty is suspended の場合は中断("SuspendAll"が実行された)
				if (rootActivity.isSuspended() && !command.equalsIgnoreCase("ExitAll")) {
					exitResult = "SUSPEND";
				}
				// activty is suspended
				// でない場合は終了(正常に教材の学習が終了した、もしくは"ExitAll"が実行された)
				else {
					exitResult = "EXIT";
				}

				// 中断情報をXMSⅡへ送信する
				Exception error = null;

				synchronized (engine) {
					try {
						// 中断処理を実行し、中断情報をXMSⅡへ送信する
						engine.discontinue(exitResult, false);
					} catch (Exception ex) {
						Debug.write(ex);
						error = ex;
					}

					// エンジンをapplicationから削除する
					application.removeAttribute(engineKeyName);
				}

				// session情報を削除する
				session.removeAttribute("SCORM.TIMEOUT_WATCHER");
				session.removeAttribute("SCORM.EXECUTE_PARAMETERS");
				session.removeAttribute("SCORM.API_ADAPTER");

				// 中断処理中に例外が発生した場合
				if (error != null) {
					// ＬＯＧ出力
					DebugLog.write(getClass(),
							"[request]Suspend Execute error", DebugLog.HIGHT);
					throw new XmsSystemException("Suspend Execute error");
				}
				// 中断・終了処理リターン
				endLearn = true;
				return;
			}

			// ＬＯＧ出力
			DebugLog.write(getClass(), "[request]:Next SCO = " + scoUrl, DebugLog.ROW);

		} catch (Exception e) {
			DebugLog.write(getClass(), "[request]:Exception", DebugLog.HIGHT);
			throw new XmsSystemException(e);
		}

		// ＬＯＧ出力
		DebugLog.write(getClass(), "[request]:TAIL", DebugLog.ROW);

	}

	/**
	 * scoUrlからscoファイルパスを抜き出します。
	 * 
	 * @param scoUrl
	 * @return scoファイル名
	 */
	private String getNextSco(String scoUrl) {

		int endIndex;
		String nextSco = "";

		endIndex = scoUrl.indexOf("?&");

		if (endIndex > 0) {
			try {
				nextSco = scoUrl.substring(0, endIndex);
				nextSco = URLDecoder.decode(nextSco, "UTF-8");
			} catch (UnsupportedEncodingException e) {
			}
		}

		return nextSco;
	}

	/**
	 * SCOURLからaicc_sidの値を抜き出します。
	 * 
	 * @param SCOURL
	 * @return aicc_sid
	 */
	private String getAiccSid(String SCOURL) {

		int beginIndex;
		int endIndex;
		String aicc_sid = "";

		beginIndex = SCOURL.indexOf("AICC_SID=");

		if (beginIndex > "AICC_SID=".length()) {
			endIndex = SCOURL.indexOf("&", beginIndex);

			if (endIndex > 0) {
				try {
					aicc_sid = SCOURL.substring(beginIndex
						+ "AICC_SID=".length(), endIndex);
					aicc_sid = URLDecoder.decode(aicc_sid, "UTF-8");
				} catch (UnsupportedEncodingException e) {
				}
			}
		}

		return aicc_sid;
	}

	/**
	 * ＳＣＯＵＲＬを戻します。
	 * 
	 * @return SCOURL
	 */
	public String getScoUrl() {
		return scoUrl;
	}

	/**
	 * aiccsidを戻します。
	 * 
	 * @return aiccSid HACP用aiccsid
	 */
	public String getAiccSid() {
		return aiccSid;
	}

	/**
	 * 学習終了状態を戻します。
	 * 
	 * @return true:学習終了 false:学習中
	 */
	public boolean isEndLean() {
		return endLearn;
	}

	/**
	 * ナビゲーションの有効・無効を戻します。
	 * 
	 * @param request Httpリクエスト
	 * @param command ナビゲーションコマンド
	 * @param url identifier
	 * @return 有効の場合true 無効の場合false アクティビティid不正の場合null
	 */
	public String getRequestValid(HttpServletRequest request, String command, String url) {

		HttpSession session = request.getSession();
		ServletContext application = session.getServletContext();

		// 起動パラメータをsessionから取得する
		ExecuteParameters executeParameters =
			(ExecuteParameters)session.getAttribute("SCORM.EXECUTE_PARAMETERS");
		if (executeParameters == null) {
			// SessionTimeOut
			DebugLog.write(getClass(),
					"[getRequestValid]Session TimeOut ExecuteParameters is null",
					DebugLog.HIGHT);
			return null;
		}

		String engineKeyName =
			"SCORM." + executeParameters.getUserID() + "." + executeParameters.getLOID() + ".ENGINE";

		Engine engine = (Engine)application.getAttribute(engineKeyName);

		if (engine == null) {
			// SessiontimeOut
			DebugLog.write(getClass(),
					"[getRequestValid]Session TimeOut Engine is null",
					DebugLog.HIGHT);
			return null;
		}

		String result = null;
		try {
			if (command.equals("continue")) {
				result = Boolean.toString(engine.canContinue());
			} else if (command.equals("previous")) {
				result = Boolean.toString(engine.canPrevious());
			} else if (command.equals("choice")) {
				Boolean canChoiceResult =
					engine.canChoice((Activity) null, url);
				// 指定されたactivity idが不正な場合
				if (canChoiceResult != null) {
					result = canChoiceResult.toString();
				}
			}
			// 2010.10.05 taku-harada add >>
			else if (command.equals("jump")) {
				Boolean canJumpResult =
					engine.canJump((Activity) null, url);
				// 指定されたactivity idが不正な場合
				if (canJumpResult != null) {
					result = canJumpResult.toString();
				}
			}
			// 2010.10.05 taku-harada add >>
		} catch (SystemException e) {
			DebugLog.write(getClass(), "[getRequestValid] engine execute Error", DebugLog.HIGHT);
		}
		return result;
	}

	private static String userIDPath(String strDirPath, String strUserID)
	{
		return UserIDFolder.getPath(strDirPath, strUserID);
	}
}
