// $Id: MultiModal.java,v 1.4 2004/03/04 18:37:15 cadlerun Exp $
// MultiModal.java -- 
// Author: Aaron Adler <cadlerun@fracas.ai.mit.edu> 
// Copyright: Copyright (C) MIT 2003.  All rights reserved.
// Created: <Fri Dec  6 22:41:16 2002> 

package rec.multi;

import java.util.Iterator;
import java.util.Set;
import java.util.HashSet;
import java.util.Collections;

import rec.util.Console;
import rec.speech.RecListen;
import rec.sys.Surface;
import rec.sys.Widget;
import rec.ui.RecSystem;
import rec.ui.RecFrame;

import org.apache.log4j.Logger;
import edu.mit.tools.watch.Logging;

import java.rmi.Naming;
import java.rmi.RMISecurityManager;

import java.io.IOException;

/**
 * This class initializes the connection to one of several speech
 * systems (ViaVoice, SLS).  It also interfaces with the dynamic WATCH
 * controller which allows the system to add speech and sketching
 * events to the timeline The timeline/WATCH can then use speech and
 * sketching events
 **/
public class MultiModal
{
  private static Logger LOG = Logging.MULTIMODAL;  
  private Set m_widgetSet = new HashSet();
  private DynamicWatchController m_dynamicWatchController;
  private int m_objId;
  private boolean m_ready = false; // ready to execute?

  private SpeechInput m_speechIn = null;
  private RecSystem m_recSys;
  private Surface m_surface;
  private RecFrame m_frame;
  private String m_speechInputType = "";
  private static Process m_vvProcess = null;
  
  private ViaVoice m_vv;
  private ViaVoiceImpl m_vvi;

  private ProcessOutput m_pOut;
  
  public MultiModal(RecSystem recSys)
  {
    m_recSys = recSys;
    m_surface = recSys.getSurface();
    m_frame = recSys.getRecFrame();
    

    Set m_widgetSet = new HashSet();
    
    // Initialize object counter
    m_objId = 1;
    
    //initialize the watch controller
    m_dynamicWatchController = new DynamicWatchController(Console.isOutputEnabled());
    LOG.debug("Starting DynamicWatchController");

    
    // then initialize a speech input. keep handle to the general speech interface
    boolean connected = makeSpeechConnection();

    // check to see if we have a connection to a speech system
    if (!connected) {
      LOG.warn("Startup failed; aborting Multimodal interaction");
      abort();
    }
    else {
      LOG.debug("Startup succeeded");
      m_frame.activateSpeech();
      m_frame.addSpeechIcon();
    }
  }

  public synchronized boolean ready() {
    return m_ready;
  }

  public synchronized void setReady(boolean ready) {
    LOG.debug("setting multimodal ready state to "+ready);
    m_ready = ready;
  }

  public synchronized void updateSurfaceWidgets(Surface s)
  {
    // update out knowledge about the widgets.
    // add and delete widgets as necessary
    LOG.debug("updating surface widgets");
    
    if (m_widgetSet == null) return;
    
    Set unseenSet = new HashSet(m_widgetSet);
    Set seenSet = new HashSet();
    Set addSet = new HashSet();
    Set removeSet;

    Iterator surfaceWidgets = s.getSurfaceWidgets().iterator();

    while (surfaceWidgets.hasNext())
    {
      
      Widget w = (Widget)surfaceWidgets.next();

      if (unseenSet.contains(w))
      {
	// now we've seen it, move it to the seen set
	unseenSet.remove(w);
	seenSet.add(w);
      }
      else
      {
	// it must be a new widget -- don't add the button
	if (!w.isItA("rec.simulate.widgets.WSimulateButton")) {
	  addSet.add(w);
	}
      }
    }

    // any widgets we didn't see must not exist any longer and should be removed
    removeSet = unseenSet;
    remove(removeSet);
    
    // now do the actual additions
    add(addSet);

    // update our base set
    m_widgetSet.removeAll(removeSet);
    m_widgetSet.addAll(addSet);
  }


  //// add and remove things from the timeline /////

  public synchronized void addSpeech(String speech, long startTime, long endTime)
  {
    LOG.debug("Adding speech: "+speech);

    System.out.println("got speech "+speech);
    
    m_frame.newSpeech(speech);
    m_dynamicWatchController.addSpeechEvent(startTime, endTime, speech);
  }    

  private void add(Set widgets)
  {
    // add widgets to the timeline
    
    Iterator widgetsIt = widgets.iterator();
    
    while (widgetsIt.hasNext()) {
      Widget w = (Widget)widgetsIt.next();
      addToTimeline(w, m_objId);
      LOG.debug("adding widget to set: "+w+" "+w.getStartTime()+"-"+w.getEndTime());
    }
    m_objId++;
  }

  private void addToTimeline(Widget w, int objID)
  {
    // when adding to the timeline we actually want to add the root widgets of this widget
    Iterator rootWidgets = w.getRootAncestors().iterator();
    String objType = w.getEnglishName(); // stays the same for all the root widgets

    while (rootWidgets.hasNext()) {
      Widget rw = (Widget)rootWidgets.next();

      long startT = rw.getStartTime();
      long endT = rw.getEndTime();
      String actionType = "draw";
      String text = rw.toString();
    
      m_dynamicWatchController.addSketchEvent(startT, endT, objType, actionType, text, objID, w);
    }
  }

  private void remove(Set widgets)
  {
    // remove widgets from the timeline

    Iterator widgetsIt = widgets.iterator();
    
    while (widgetsIt.hasNext()) {
      Widget w = (Widget)widgetsIt.next();
      removeFromTimeline(w);
      LOG.debug("removing widget from set: "+w+" "+w.getStartTime()+"-"+w.getEndTime());
    }
  }

  private void removeFromTimeline(Widget w)
  {
    // when deleting to the timeline we actually want to delete the root widgets of this widget
    Iterator rootWidgets = w.getRootAncestors().iterator();
    String objType = w.getEnglishName(); // stays the same for all the root widgets

    while (rootWidgets.hasNext()) {
      Widget rw = (Widget)rootWidgets.next();
      
      long startT = rw.getStartTime();
      long endT = rw.getEndTime();
      String actionType = "draw";
      String text = rw.toString();
    
      m_dynamicWatchController.removeSketchEvent(startT, endT, objType, actionType, text);
    }
  }


  ////// initialization methods //////////////
  
  /**
   * initialize a speech input. keep handle to the general speech interface
   * returns true if a connection is made, and false if one is not. 
   */
  private boolean makeSpeechConnection() {
    // get the version of speech interaction
    m_speechInputType = m_surface.getSystemProperty("mm_type","");

    if (m_speechInputType.equalsIgnoreCase("galaxy")) {
      // Playbox version of initialization
      boolean started = startPlaybox();
      if (!started) abort();
    }
    else if (m_speechInputType.equalsIgnoreCase("viavoice")) {
      // ViaVoice version of initialization
      boolean started = startViaVoice();
      if (!started) abort();
    }
    else {
      // unrecognized speech input type
      LOG.warn("Unrecognized or missing speech input type");
      return false;
    }
    



    // now wait for the connection if it doesn't work at first
    int waitAmount = 3;
    // wait more for galaxy
    if (m_speechInputType.equalsIgnoreCase("galaxy")) waitAmount = 10;

    // block until MultiModal is ready to go or we have waited long enough
    LOG.debug("Trying to connect to speech system");
    
    int counter = 0;
    while ((!ready()) && (counter < waitAmount)) {
      counter++;
      try {
	Thread.sleep(2000);
      }
      catch (InterruptedException e) {
      }
    }

    // return whether a connection has been made
    return (ready());
  }

  /**
   * Playbox version of initialization
   */
  private boolean startPlaybox() {
    String host = m_surface.getSystemProperty("mm_host",""); // required for galaxy
    String port_s = m_surface.getSystemProperty("mm_port",""); // required for galaxy
    String name = m_surface.getSystemProperty("mm_name",""); // not required
    int port = -1;
    
    if ( (host != "") && (port_s != "") ) {
      // have required parameters
      try {
	port = Integer.parseInt(port_s);
	if (name == "") name = "Multimodal Playbox";
	LOG.info("Initializing Playbox \""+name+"\" on "+host+":"+port);

	// have all the parameters start up the playbox
	Playbox pb = new Playbox(host, port, name, this);
	m_speechIn = (SpeechInput) pb;
	// start the playbox
	new Thread(pb).start();
	
	// everything worked
	return true;
      } catch (NumberFormatException e) {
	// bad port, turn off multimodal
	LOG.info("Bad port \""+port_s+"\"for Multimodal, turning off Multimodal");
	return false;
      }
    }
    else {
      // missing required parameter
      if (host == "")
	LOG.info("Missing Multimodal host name, turning off Multimodal");
      else
	LOG.info("Missing Multimodal port number, turning off Multimodal");
      return false;
    }    
  }

  /**
   * ViaVoice version of initialization
   */
  private boolean startViaVoice() {
    LOG.info("Initializing ViaVoice");

    // Create and install a security manager
    if (System.getSecurityManager() == null) {
      System.setSecurityManager(new RMISecurityManager());
    }
    
    try {
      ViaVoiceImpl m_vvi = new ViaVoiceImpl(this);

      // bind the object instance to the name "ViaVoiceServer"
      Naming.rebind("ViaVoiceServer", m_vvi);
      LOG.info("ViaVoice server bound in registry");
    } catch (Exception e) {
      LOG.error("exception in ViaVoiceImpl: "+e);
    }

    m_speechIn = (SpeechInput) m_vvi;

    try {
      // get the current classpath
      String classpath = System.getProperty("java.class.path");
      
      m_vvProcess = Runtime.getRuntime().exec("java -classpath \""+classpath+"\" rec.multi.ViaVoice");

      m_pOut = new ProcessOutput(m_vvProcess);
      new Thread(m_pOut).start();
      
    }
    catch (IOException e) {
      LOG.error("IO exception trying to start new thread.");
    }
    
    return true;
  }
  
  /**
   * Abort having a multimodal connection. 
   */
  private void abort() {
    if (m_speechIn != null) m_speechIn.abort();
    // make sure ViaVoice is stopped
    stopViaVoice();
    m_recSys.getSurface().setSystemProperty("multimodal","off");
  }

  /**
   * If ViaVoice is the speech engine, then shut it down.
   **/
  public void stopViaVoice() {
    if (m_speechInputType.equalsIgnoreCase("viavoice")) {
      m_pOut.stop();
      if (m_vvProcess != null) {
	m_vvProcess.destroy();
	m_vvProcess = null;
      }
    }
  }
 
  /**
   * If ViaVoice is the speech engine, then restart it. 
   **/
  public void restartViaVoice() {
    if (m_speechInputType.equalsIgnoreCase("viavoice")) {
	startViaVoice();
    }
  }

  /**
   * Get the speech process so that we can shut it down when the user
   * exits the program.
   **/
  public static synchronized Process getSpeechProcess() {
    // get the speech process
    return m_vvProcess;
  }
  
}
