/*
 * Galatea Dialog Manager:
 * (c)2003 Takuya NISHIMOTO (nishi@hil.t.u-tokyo.ac.jp)
 * $Id: IC.java,v 1.53 2008/03/11 08:17:50 nishi Exp $
 */

package galatea.dialog;

import galatea.command.Command;
import galatea.dialog.window.IDMWindowActionListener;
import galatea.document.ContentState;
import galatea.document.DocError;
import galatea.document.StateMap;
import galatea.httpserver.HttpAsyncResponse;
import galatea.httpserver.HttpRequestTask;
import galatea.httpserver.IHttpAsyncResponseListener;
import galatea.httpserver.IHttpResponseHtmlMaker;
import galatea.io.DeviceEvent;
import galatea.io.DeviceManager;
import galatea.io.IOutputEventListener;
import galatea.io.OutItemQueueManager;
import galatea.io.OutputEventQueueManager;
import galatea.logger.Logger;
import galatea.outitem.OutItem;
import galatea.outitem.VoiceOutItem;
import galatea.relaxer.event.EventEv;
import galatea.relaxer.event.EventInterpreted;
import galatea.relaxer.event.IEventEvChoice;
import galatea.scripting.ECMAScript;
import galatea.util.StringHashArray;
import galatea.util.Util;

import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.ArrayList;

public class IC 
implements IInteractionController
	,ICommandContext
	,IControllerModeManager
	,IInterpretedEventListener
	,IOutputEventListener
	,IDMWindowActionListener 
	,IHttpResponseHtmlMaker
	{
	
	private Logger dbg = new Logger("IC");

	private String nextDocFile_ = "";
	private String postString_ = "";
	
	private ContentState currState_ = null;
	private OutItemQueueManager outQueueManager_ = null;
	private InterpretedEventQueueManager interpretedEventQueueManager_ = null;
	private OutputEventQueueManager outputEventQueueManager_ = null;
	private ECMAScript ecmascript_ = null;
	private DeviceManager deviceManager_;
	private StateMap stateMap_ = null;
	private ControllerMode imode_ = new ControllerMode();
	private ArrayList<Command> commands_ = null;
	private ArrayList<String> debuggerEvals_ = null;

	private void _updateDialogStateChange(String state) {
		deviceManager_.updateDialogStateChange(state);
	}
	
	private ArrayList<IHttpAsyncResponseListener> 
		httpAsyncResponseListeners_ 
		= new ArrayList<IHttpAsyncResponseListener>();
	public void addHttpAsyncResponseListener(IHttpAsyncResponseListener listener) {
		httpAsyncResponseListeners_.add(listener);
	}
	
	private String nextStateName_ = null;
	public synchronized void setNextState(String state) {
		dbg.print("IC: setNextState: " + state );
		nextStateName_ = state;
	}
	public synchronized void resetNextState() {
		dbg.print("resetNextState");
		nextStateName_ = null;
	}
	
	private String evalAfterInputHandler_ = null;
	
	private synchronized void _resetEvalAfterInputHandler() {
		evalAfterInputHandler_ = null;
	}
	private synchronized void _appendEvalAfterInputHandler(String s) {
		if ( evalAfterInputHandler_ == null ) {
			evalAfterInputHandler_ = s;
		} else {
			evalAfterInputHandler_ += s;
		}
	}
	private synchronized void _doEvalAfterInputHandler() {
		if ( evalAfterInputHandler_  != null) {
			try {
				ecmascript_.evaluate( evalAfterInputHandler_ );
			} catch (RuntimeError e) {
				dbg.print("IC: evaluate " + e.toString());
			}
			dbg.print("IC: afterInputHandler: "+evalAfterInputHandler_);
		}
	}
	
	
	// constructor
	
	public IC() {
		dbg.print("IC: constructor start");
		try {
			ecmascript_ = new ECMAScript();
			// dbg.print(ecmascript_.getVersion());
		} catch (RuntimeError e) {
			dbg.print(e.toString());
		}
		interpretedEventQueueManager_ = new InterpretedEventQueueManager();
		outputEventQueueManager_ = new OutputEventQueueManager();
		deviceManager_ = new DeviceManager();
		
		outQueueManager_ = new OutItemQueueManager(deviceManager_);
		commands_ = new ArrayList<Command>();  
		imode_ = new ControllerMode();
		debuggerEvals_ = new ArrayList<String>();

		dbg.print("IC: constructor end");
	}
	
	public void start() {
		//System.err.println(ecmascript_.getVersionAndCopyright());
		deviceManager_.start(this, this, this, this, this);
	}
	
	public String getScriptVersionAndCopyright() {
		return ecmascript_.getVersionAndCopyright();
	}
	
	public boolean waitDevicesForReady() {
		return deviceManager_.waitDevicesForReady();
	}
	
	public void setStateMap(StateMap stateMap) throws Exception {
		setEndFlag(false);
		setEndImm(false);
		stateMap_ = stateMap;
		currState_ = stateMap_.getFirstState();
		if ( currState_ == null ) {
			throw new RuntimeError("(setStateMap) State not found: " + currState_.getName());
		} else {
			_updateDialogStateChange(currState_.getName());
		}
	}

	
	private void _sleep() {
		try { 
			Thread.sleep(1, 0);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	
	private void _doEvents() throws RuntimeError {
		_resetEvalAfterInputHandler();
		deviceManager_.doTimerTask();
		_sleep();
		_processOutputEventQueueItem();
		_processInterpretedEventQueueItem();
		_doEvalAfterInputHandler();
		_doDebuggerEvals();
	}
	
	
	private void _execCurrentStateCommands() {
		if (currState_ == null) {
			return;
		}
		// execute commands at currState
		commands_.clear();
		commands_.addAll(currState_.getCommands());
		while (!commands_.isEmpty()) {
			Command cmd = (Command)commands_.remove(0);
			cmd.setContext(this, ecmascript_);
			try {
				cmd.execute();
			} catch (RuntimeError e) {
				dbg.print(e.toString());
			}
			dbg.print("IC: command done: " + cmd.toString(), 2);
		}
	}
	
	
	private boolean _processOutQueueIteration() {
		// check stopoutput
		if (!outQueueManager_.getCurrOutQueueIsValid()) {
			// dbg.print("IC: currOutQueue not valid.");
			outQueueManager_.removeDelayedOutputItems();
			outQueueManager_.setCurrOutQueueIsValid(true);
			return false;
		}
		// start or wait output
		if (!imode_.isPauseMode()) {
			if (outQueueManager_.doOutputIteration() == false) {
				return false;
			}
		}
		// check dialog terminate
		if (imode_.isEndImm()) {
			outQueueManager_.discard();
			dbg.print("IC: iteration discard queue");
			currState_ = null;
			return false;
		}
		return true;
	}
	
	private void _processOutQueue() {
		// dbg.print("IC: iteration (output OutQueue items) start.");
		outQueueManager_.setCurrOutQueueIsValid(true);
		while (true) { 
			_handleHttpRequestTasks();
			try {
				_doEvents();
			} catch (RuntimeError e) {
				setPauseMode(true);
				dbg.print(e.toString());
			}
			if (_processOutQueueIteration() == false) {
				break;
			} else {
				// dbg.print("IC: iteration (output OutQueue items) repeating.");
			}
		}
	}
	
	private void _gotoNextState() throws Exception {
		if (imode_.isEnd()) {
			dbg.print("IC: end");
			currState_ = null;
			return;
		}
		if (stateMap_ == null) {
			//throw new RuntimeError("stateMap is null");
			dbg.print("gotoNextState(): stateMap is null");
			imode_.setStatelessMode(true);
			return;
		}
		if (nextStateName_ == null) {
			currState_ = null;
			throw new RuntimeError("nextStateName is null");
		} else {
			dbg.print("IC: from " + currState_.getName() + " to " + nextStateName_);
			currState_ = stateMap_.get(nextStateName_);
			if (currState_ == null) {
				throw new RuntimeError("(gotoNextState) State not found: " + nextStateName_);
			}
			_updateDialogStateChange(currState_.getName());
			resetNextState();
		}
	}
	
	private void _handleHttpRequestTasks() {
		while (deviceManager_.sizeExternalTask() > 0) {
			HttpRequestTask task = deviceManager_.removeExternalTask();
			String arg = task.getArgument();
			String ev = task.getEvent();
			if (ev.equals("talk")) {
				OutItem oi = new VoiceOutItem("'" + arg + "'");
				oi.setECMAScript(ecmascript_);
				try {
					oi.doEvaluate();
					enqueueOutItem(oi);
				} catch (RuntimeError e) {
					dbg.print(e.toString());
				}
			} else if (ev.equals("talk-stop")) {
				// stopOutput();
				dbg.print(ev.toString());
			} else if (ev.equals("start-doc")) {
				setNextDocFile(arg, "");
				dbg.print(ev.toString());
			} else if (ev.equals("quit-doc")) {
				terminateDialogAction();
				dbg.print(ev.toString());
			} else if (ev.equals("go-stateless")) {
				imode_.setStatelessMode(true);
				dbg.print(ev.toString());
			} else if (ev.equals("go-state")) {
				imode_.setStatelessMode(false);
				dbg.print(ev.toString());
			}
		}
	}
	
	// implements AbstractIC
	// this method is used from DialogManager
	public void mainLoop() throws Exception
	{
		dbg.print("IC: mainLoop start");
		while (true) {
			if (imode_.isStatelessMode()) {
				_processOutQueueIteration();
				_handleHttpRequestTasks();
				_doEvents(); // 削ると statelessMode で出力キューが実行されない
				if (nextDocFile_.length() > 0) {
					return;
				}
			} else {
				_execCurrentStateCommands();
				_processOutQueue();
				_gotoNextState();
				// _handleHttpRequestTasks();
				if (currState_ == null) {
					return;
				}
			}
		}
		// dbg.print("IC: mainLoop end");
	}
	
	
//	private void _gotoDefaultNextState() {
//		_gotoNextStateByText("_");
//	}
	
	private void _gotoNextStateByText(String text) {
		if (currState_ == null) {
			return;
		}
		dbg.print("IC: gotoNextStateByText(" + text + ") at state:" + currState_.getName() );
		//String next = (String)currState_.findEventHandler(text);
		String next = (String)currState_.getDefaultEventHandler();
		if (next == null) {
			dbg.print("findEventHandler() not found in " + currState_.getName());
			return;
		} else {
			dbg.print("findEventHandler() returns " + next);
			outQueueManager_.setCurrOutItemIsValid(false);
			outQueueManager_.setCurrOutQueueIsValid(false);
			setNextState(next);
		}
	}
	

	private void _gotoDefaultNextState() {
		if (currState_ == null) {
			return;
		}
		String next = (String)currState_.getDefaultEventHandler();
		if (next == null) {
			dbg.print("findEventHandler() not found in " + currState_.getName());
			return;
		} else {
			dbg.print("findEventHandler() returns " + next);
			outQueueManager_.setCurrOutItemIsValid(false);
			outQueueManager_.setCurrOutQueueIsValid(false);
			setNextState(next);
		}
	}
	
	// TODO: 暫定的な仕様
	private void _sendAsHttpAsyncResponseEvent(InterpretedEvent evt) {
		for (int i = 0; i < httpAsyncResponseListeners_.size(); i++) {
			HttpAsyncResponse h = HttpAsyncResponse.newInstance(evt.toString());
			((IHttpAsyncResponseListener)httpAsyncResponseListeners_.get(i))
				.enqueueHttpAsyncResponse(h);
		}
	}
	
	private void _processInterpretedEvent(InterpretedEvent evt) {
		_sendAsHttpAsyncResponseEvent(evt);
		EventEv ev;
		try {
			ev = evt.getEv();
		} catch (Exception e) {
			dbg.print("IC: getEv() error.");
			return;
		}
		if (ev != null) {
			// src = ev.getSrc();
			String type = ev.getType();
			if ( !type.equals("INPUT") && !type.equals("COMMAND") ) {
				dbg.print("IC: ignored: " + type);
				return;
			}
			IEventEvChoice c = ev.getContent();
			if (c instanceof EventInterpreted) {
				dbg.print("IC: EventInterpreted " + evt.toString());
				EventInterpreted ei = (EventInterpreted)c;
				if (ei.getScript() != null) {
					_appendEvalAfterInputHandler(ei.getScript());
				}
				
				_gotoDefaultNextState();
				
//				if (ei.getText() != null) {
//					String text = ei.getText();
//					_gotoNextStateByText(text);
//				} else {
//					_gotoNextStateByText("_");
//					// _gotoDefaultNextState();
//				}
			}
		}
	}
	
	
	// implements ICEventListener
	// this method is used from DialogManagerWindow
	public void setNextDocumentAction(String uri) {
		if (uri != null && uri.length() > 0 ) {
			dbg.print("IC: setNextDocumentAction " + uri);
			setNextDocFile(uri, "");
		}
	}
	
	
	// implements ICEventListener
	// this method is used from DialogManagerWindow
	public void terminateDialogAction() {
		dbg.print("IC: terminateDialogAction");
		if (outQueueManager_ != null) {
			outQueueManager_.setCurrOutItemIsValid(false);
		} else {
			System.err.println("Warning: no current outitem.");
		}
		outQueueManager_.setCurrOutQueueIsValid(false);
		resetNextState();
		setEndFlag(true);
		setEndImm(true);
		//setNextDocFile("");
	}
	
	
	// implements ICEventListener
	// this method is used from DialogManagerWindow
	public void setPauseModeAction(boolean b) {
		imode_.setPauseMode(b);
	}
	
	public void setPauseMode(boolean b) {
		imode_.setPauseMode(b);
	}
	
	public boolean getPauseMode() 	{
		return imode_.isPauseMode();
	}
	
	// implements ICEventListener
	// this method is used from DialogManagerWindow
	public void quitAction() {
		dbg.print("IC: quitAction");
		outQueueManager_.setCurrOutItemIsValid(false);
		outQueueManager_.setCurrOutQueueIsValid(false);
		resetNextState();
		setEndFlag(true);
		setEndImm(true);
		setNextDocFile("", "");
	}
	
	
	// implements DeviceListener
	// this method is used from AMThread, BreakThread
	public void enqueueOutputEvent(DeviceEvent evt) {
		outputEventQueueManager_.enqueue(evt);
	}
	
	
	// implements DeviceListener
	// this method is used from AMThread
	public void enqueueInterpretedEvent(InterpretedEvent evt) {
		interpretedEventQueueManager_.enqueue(evt);
	}
	

	private void _processOutputEventQueueItem() {
		DeviceEvent evt;
		while ((evt = outputEventQueueManager_.dequeue()) != null) {
			outQueueManager_.processDeviceEvent(evt);
		}
	}
	
	
	private void _processInterpretedEventQueueItem() {
		InterpretedEvent evt;
		while ((evt = interpretedEventQueueManager_.dequeue()) != null) {
			_processInterpretedEvent(evt);
		}
	}
	
	
	// implements AbstractIC for command.*
	// this method is used from AddOutItemCommand
	public void enqueueOutItem(OutItem oi) {
		dbg.print("IC: enqueueOutput " + oi.toString());
		outQueueManager_.enqueue(oi);
	}
	
	// implements AbstractIC for command.*
	// this method is used from CompositeCommand
	public void insertCommandsTop(ArrayList<Command> v) {
		if ( commands_ == null ) return;
		dbg.print("IC: insertCommandsTop " + Util.removeNewLines(v.toString()));
		commands_.addAll(0, v);
	}
	
	// implements AbstractIC for command.*
	// this method is used from EndCommand 
	public void setEndFlag(boolean b) {
		dbg.print("IC: setEndFlag: "+b);
		imode_.setEndFlag(b);
	}
	
	// implements AbstractIC for command.*
	// this method is used from EndCommand 
	public void setNextDocFile(String nextfile, String post) {
		dbg.print("IC: setNextDocFile:"+nextfile+" post:"+post);
		nextDocFile_ = nextfile;
		postString_ = post;
	}
	
	/**
	 * IInteractionController
	 */
	public String getNextDocFile() 	{ 
		return nextDocFile_;
	}
	
	public String getPostString() {
		return postString_;
	}

	/**
	 * IInteractionController
	 */
	public void resetNextDocFile() 	{ 
		nextDocFile_ = "";
		postString_ = "";
	}
	
	// implements AbstractIC for command.*
	// this method is used from GotoCommand 
	public void gotoState(String state) {
		setNextState(state);
		commands_.clear(); //commands_.removeAllElements();
	}
	
	// implements AbstractIC for command.*
	// this method is used from StopOutputCommand 
	public void stopOutput() {
		outQueueManager_.setCurrOutItemIsValid(false);
		outQueueManager_.setCurrOutQueueIsValid(false);
	}
	
	// implements AbstractIC for command.*
	// this method is used from EndCommand 
	public void setEndImm(boolean b) {
		imode_.setEndImm(b);
	}
	
	public boolean isStatelessMode() {
		return imode_.isStatelessMode();
	}

	public void setStatelessMode(boolean statelessMode) {
		if (imode_.isStatelessMode() == false && statelessMode == true) {
			outQueueManager_.removeDelayedOutputItems();
			outQueueManager_.stopCurrentOutput();
			outQueueManager_.setCurrOutQueueIsValid(false);
		}
		imode_.setStatelessMode(statelessMode);
		if (statelessMode) {
			dbg.print("IC: setStatelessMode = true");
		} else {
			dbg.print("IC: setStatelessMode = false");
		}
	}

	public String getResourceAsString(String path) {
		InputStream is = this.getClass().getResourceAsStream(path);
		Charset cs = Charset.forName("UTF-8");
		return Util.readInputStream(is,cs);
	}

	
	public String getDialogMonitorHtml() {
		boolean stateless = isStatelessMode();
		String statelessModeStr = stateless ? "stateless" : "state";
		OutItemQueueManager oqm = outQueueManager_;
		String res = "";
		if (stateless) {
			String html = getResourceAsString("/res/html/gdm.html");
			String script = getResourceAsString("/res/js/gdm.js");
			StringHashArray replaces = new StringHashArray();
			replaces.put("${script}", script);
			replaces.put("${currentStateName}", "N/A");
			replaces.put("${nextDocFile}", "N/A");
			replaces.put("${outputQueue}", Util.encodeXmlChars(oqm.toString()));
			replaces.put("${currentState}", "N/A");
			replaces.put("${statelessModeStr}", statelessModeStr);
			res = Util.getDocumentFromTemplate(html, replaces);
		} else {
			ContentState cs = currState_;
			String docFile = getNextDocFile();
			String html = getResourceAsString("/res/html/gdm.html");
			String script = getResourceAsString("/res/js/gdm.js");
			StringHashArray replaces = new StringHashArray();
			replaces.put("${script}", script);
			replaces.put("${currentStateName}", cs.getName());
			replaces.put("${nextDocFile}", docFile);
			replaces.put("${outputQueue}", Util.encodeXmlChars(oqm.toString()));
			replaces.put("${currentState}", Util.encodeXmlChars(cs.toString()));
			replaces.put("${statelessModeStr}", statelessModeStr);
			res = Util.getDocumentFromTemplate(html, replaces);
		}
		return res;
	}
	
	public String getDemoAppHtml() {
		String html = getResourceAsString("/res/html/demo.html");
		StringHashArray replaces = new StringHashArray();
		replaces.put("${gdm_js}", getResourceAsString("/res/js/gdm.js"));
		replaces.put("${demo_js}", getResourceAsString("/res/js/demo.js"));
		String res = Util.getDocumentFromTemplate(html, replaces);
		return res;
	}
	
	public void dispDocInfo(String file, String state, 
			String src, String trans) {
		deviceManager_.dispDocInfo(file, state, src, trans);
	}
	
	public void setDebuggerEvalAction(String str) {
		debuggerEvals_.add(str);
	}
	
	private void _doDebuggerEvals() {
		for (int i = 0, n = debuggerEvals_.size(); i < n; i++) {
			String s = (String)debuggerEvals_.get(i);
			String result = "";
			try {
				result = ecmascript_.evaluate(s);
			} catch (RuntimeError e) {
				result = "[Error] " + e.toString();
			}
			deviceManager_.dispDebuggerEvalResult(result);
		}
		debuggerEvals_.clear();
	}

}
