// (c) MIT 2003.  All rights reserved.

// -*-Mode: Java-*- 
// $Id: RecListen.java,v 1.15 2003/12/04 20:01:10 cadlerun Exp $
// RecListen.java -- 
// Author: Michael Oltmans <moltmans@ai.mit.edu> 
// Created: <Tue Feb 29 13:38:02 2000> 
// Time-stamp: <2001-04-24 10:59:17 moltmans> 

/**
 * This is the Intelligent Room's interface to our speech recognizer
 * (currently it is IBM's ViaVoice Gold).
 * 
 * This code conforms to the Java Speech API (JSAPI) v1.0.  Note
 * that IBM has not yet implemented the full interface, so some lines
 * below are commented out until IBM comes up to spec.
 * 
 * This is at the moment fairly undocumented, but you shouldn't access
 * this directly.  Use the RecGrammar class instead.  (And
 * from Metaglue, use the AppGrammarAgent.)
 * 
 * In case you need to modify this code, the getDictation() method
 * below should _always_ be called from another thread -- not from the
 * recognizer's event thread (which calls listeners via AWT's event
 * queue) -- or speech and your GUI will simply hang.  The
 * RecGrammar class takes care of this for you, so you should
 * use that to access your JSGF grammars rather than do so yourself.
 * 
 * Also, there's some debugging stuff in here that should probably
 * remain even though it's not being used right now.
 * 
 * Michael (mhcoen@ai.mit.edu) 
 */
package rec.speech;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.net.*;
import javax.speech.*;
import javax.speech.recognition.*;
import javax.speech.recognition.Recognizer.*;
import javax.speech.recognition.RecognizerProperties.*;
import rec.util.*;

/* The startup interaction between viaVoice's speech recognition and
   synthesis is buggy.  Don't change things involving these below in
   the class's constructor.  
 */

public class RecListen {

  static RecListen m_listener;
  
  Hashtable m_activeGrammars = new Hashtable();
  Hashtable m_manualCallbacks = new Hashtable();

  DictationGrammar dictationGrammar;    

  boolean m_iAmAsleep = false; // Global flag for current status

  private final static String SOUND_COMPUTER_ATTENTION = "room_sound2.wav";
  private final static String SOUND_COMPUTER_ACKNOWLEDGMENT = "room_ack.wav";

  // spokenString holds dictation result so it can be shared among threads
  private String m_spokenString = "";
  private boolean m_recognizing = false;
  Recognizer m_recognizer = null;       
  RecListen m_me = this;

  // keep track of the start and end times of the speech too 
  private long m_startTime = -1; 
  private long m_endTime = -1;
  
  public void setManualCallback (String name, RecGrammar ag) {
    m_manualCallbacks.put(name, ag);
  }
    
  public static synchronized RecListen getListen()
  {
    if ( m_listener == null )
      m_listener = new RecListen();

    return m_listener;
  }
  
  /**
   * THE CONSTRUCTOR--SET UP EVERYTHING
   */  
  private RecListen () 
  {       
//      TextInputFrame f = new TextInputFrame();
//      f.setSize( 300, 100 );
//      WindowListener wl = new WindowAdapter() {
//        public void windowClosing( WindowEvent e ) {
//  	e.getWindow().dispose();
//        }
//      };

//      f.addWindowListener( wl );
//      f.show();

    loadRecognizer();
  }
  

  public void pauseRecognizer () {
    try {
      m_recognizer.pause();
    }
    catch (Exception ex) {
      ex.printStackTrace();
      Say.say("Fatal error in pausing the recognizer");
    }
  }

  public void resumeRecognizer () {
    try {
      m_recognizer.resume();
    }
    catch (Exception ex) {
      ex.printStackTrace();
      Say.say("Fatal error in resuming the recognizer");
    }
  }

  public void loadRecognizer()
  {
    if (m_recognizer != null)
      return;

    //    System.out.println("Starting Listen's Recognizer Setup");

    try {
      // create a recognizer matching default locale, add audio listener
      m_recognizer = Central.createRecognizer(null);
      if (m_recognizer == null ) {
	System.err.println("In RecListen.loadRecognizer()\n" +
		       "  Central.createRecognizer returned null." );
	throw new RuntimeException( "RecListen.loadRecognizer(): createRecognizer returned null" );
      } // end if

      m_recognizer.allocate();
      m_recognizer.getAudioManager().addAudioListener(m_audioListener);
      
      // create dictation grammar
      dictationGrammar = m_recognizer.getDictationGrammar(null);
      dictationGrammar.addResultListener(dictationListener);
      
      // commit new grammars, start recognizer
      m_recognizer.commitChanges();
      m_recognizer.resume();
    }
    catch (Exception e) {
      e.printStackTrace();
      System.out.println("recs: " + Central.availableRecognizers(null));
      System.err.println( "KILLING ERROR: Failed to do constructor." );
      Assert.error("Error doing constructor in RecListen");
    }
      
    //    goToSleep();
  } // end loadRecognizer()


  public HashSet voiceNames()
  {
    HashSet st = new HashSet();
    SpeakerManager spk = m_recognizer.getSpeakerManager();
    if ( spk == null )
      {
	System.err.println( "Got null SpeakerManager--bailing voiceNames" );
	return st;
      }

    try
      {
	System.err.println( "LISTING ALL SPEAKERS" );
	SpeakerProfile[] profs = spk.listKnownSpeakers();
	
	if ( profs != null )
	  {
	    for ( int i = 0; i < profs.length; i++ )
	      {
		System.err.println( "Prof : " + profs[i] );
		System.err.println( "   name = " + profs[i].getName() );
		st.add( profs[i].getName() );
		System.err.println( "   id = " + profs[i].getId() );
		System.err.println( "   var = " + profs[i].getVariant() );
	      }
	  }
	else
	  System.err.println( "NO SPEAKER PROFILES" );
      }
    catch ( Throwable th )
      {
	th.printStackTrace();
	System.err.println( "....continue...." );
      }
    return st;
  } // end voiceNames()


  public void setVoice( String name )
  {
    System.err.println( "setVoice( " + name + " )" );

    SpeakerManager spk = m_recognizer.getSpeakerManager();

    try {
      SpeakerProfile[] profs = spk.listKnownSpeakers();
      if ( profs != null )
	{
	  for ( int i = 0; i < profs.length; i++ )
	    {
	      System.err.println( "Prof : " + profs[i] );
	      System.err.println( "   name = " + profs[i].getName() );
	      
	      if ( profs[i].getName().equals( name ) )
		{
		  // set the voice
		  System.err.println( "Setting the speaker to " 
                                      + profs[i].getName() );
		  spk.setCurrentSpeaker( profs[i] );
		  System.err.println( "Set complete." );
		}
	      System.err.println( "   id = " + profs[i].getId() );
	      System.err.println( "   var = " + profs[i].getVariant() );
	    }
	}
      else
	System.err.println( "NO SPEAKER PROFILES" );
    }
    catch ( Throwable th )
      {
	th.printStackTrace();
	System.err.println( "....continue...." );
      }
  }

  // Play the "I heard you" sound
  public  void playAcknowledgment () 
  {
    //DebugWindow.addLine( "Speech", "Heard you!" );
    Say.playWav(SOUND_COMPUTER_ACKNOWLEDGMENT);
  }

  // Play the "You have my attention" sound
  public  void playAttention () 
  {
    //DebugWindow.addLine( "Speech", "Now Attending" );    
    Say.playWav( SOUND_COMPUTER_ATTENTION);
  }

  // Get the recognizers current active grammars
  // (an active grammar is one that contains at least one active rule)
  private Vector getActiveGrammars () {
    Vector activeV = new Vector();
    RuleGrammar[] activeG = m_recognizer.listRuleGrammars();
    if (activeG != null) {
      for (int i=0; i < activeG.length; i++) {
	if (activeG[i].isEnabled()) {   
	  activeV.addElement(activeG[i]);
	}
      }
    }
    return activeV;
  }
  
  // Get the public (i.e., speakable) rules for a given grammar
  public  Vector getPublicRules (RuleGrammar ruleG) {
    Vector publicV = new Vector();
    String[] rules = ruleG.listRuleNames();
    if (rules != null) {
      for (int i = 0; i < rules.length; i++) {
	if (ruleG.isRulePublic(rules[i])) {
	  publicV.addElement(rules[i]);
	}
      }
    }
    return publicV;
  }

  // Get the active rules for a given grammar
  public  Vector getActiveRules (RuleGrammar ruleG) {
    Vector activeV = new Vector();
    String[] rules = ruleG.listRuleNames();
    if (rules != null) {
      for (int i = 0; i < rules.length; i++) {
	if (ruleG.isEnabled(rules[i])) {
	  activeV.addElement(rules[i]);
	}
      }
    }
    return activeV;
  }
  
  /**
   * Disable speech input when appropriate, e.g., user says, "go to
   * sleep" or after a command
   *
   * Process: Find out what the active grammars are, call them Set_i
   * Save Set_i and deactive all the grammars in it This is so they
   * can be reactivated when the user says, "computer"
   *
   * Active the wakeUp grammar (which only contains "computer")
   * This goes to sleep.
   **/
  public synchronized void goToSleep () {
    realGoToSleep();
  }

  private  void realGoToSleep () 
  {
    m_iAmAsleep = true; // Set global flag

    Vector activeV = getActiveGrammars();
    Enumeration activeE = activeV.elements();
    while (activeE.hasMoreElements()) {           
      RuleGrammar ruleG = (RuleGrammar) activeE.nextElement();
      Vector activeR = getActiveRules(ruleG);
      m_activeGrammars.put(ruleG, activeR);
      //      System.out.println("Turning off " + ruleG.getName());
      ruleG.setEnabled(false);
    }
    //    wakeUpGrammar.setEnabled(true);
    commitChanges();
  }
  

  /**
   * Public wakeUp method to programatically wake up the system.
   **/
  public synchronized void requestWakeUp () {
    if (m_iAmAsleep) {
      playAttention();
      wakeUp();
    }
  }
      
  private synchronized  void wakeUp () {
    m_iAmAsleep = false; // Unset global flag
    Enumeration grammarsE = m_activeGrammars.keys();
    // Go through grammars with previously active rules
    while (grammarsE.hasMoreElements()) {
      RuleGrammar ruleG = (RuleGrammar) grammarsE.nextElement();
      Vector activeR = (Vector) m_activeGrammars.get(ruleG);
      Enumeration rulesE = activeR.elements();
      // Activate the previously active rules for each grammar
      while (rulesE.hasMoreElements()) {
	String rule = (String) rulesE.nextElement();
	//	System.out.println("Activating " + ruleG.getName() + "." + rule);
	ruleG.setEnabled(rule, true);
      }
    }
    //    wakeUpGrammar.setEnabled(true);                  
    commitChanges();
  }

  /*
   * These are the methods for activating and deactivating individual
   * grammars and their rules.  I put all of the functionality here
   * rather than in RecGrammar, but perhaps it belongs there
   * instead.
   *
   * Active grammars are ones the room is listening for when it is
   * awake.  (E.g., the user has just said, "Computer.")  If the room
   * is awake, they are immediately activated.  Otherwise, they will
   * become available the next time the room wakes up.
   */
  public synchronized void activate(RuleGrammar g) {
    //    System.out.println("Activating " + g.getName());
    Vector publicV = getPublicRules(g);
    m_activeGrammars.put(g, publicV);
    if (! m_iAmAsleep) {
      g.setEnabled(true);
      commitChanges();
    }
  }

  public synchronized void activate(RuleGrammar g, String ruleName) {
    Vector publicV = getActiveRules(g);
    Rule r = g.getRule(ruleName);
    if (r == null) {
      DebugWindow.addLine( "Speech",
			   "I can't find rule " + ruleName +
			   " in your grammar" );
      return;
    }
    if (! publicV.contains(r)) {
      publicV.addElement(ruleName);
    }
    m_activeGrammars.put(g, publicV);
    if (! m_iAmAsleep) {
      g.setEnabled(ruleName, true);
      commitChanges();
    }
  }

  public synchronized void deactivate(RuleGrammar g) {
    //    System.out.println("Deactivating " + g.getName());
    m_activeGrammars.remove(g);
    if (! m_iAmAsleep) {
      g.setEnabled(false);
      commitChanges();
    }
  }

  public synchronized void deactivate(RuleGrammar g, String ruleName) {
    Vector publicV = getActiveRules(g);
    Rule r = g.getRule(ruleName);
    if (r == null) {
      DebugWindow.addLine( "Speech",
			   "I can't find rule " + ruleName +
			   " in your grammar" );
      return;
    }
    publicV.removeElement(ruleName);
    m_activeGrammars.put(g, publicV);
    if (! m_iAmAsleep) {
      g.setEnabled(ruleName, false);
      commitChanges();
    }
  }

  
  /** Process a string against all active grammars **/
  public synchronized String processString (String text) 
  {
    //    System.out.println("At processString");
    // Go through grammars with previously active rules
    Enumeration grammarsE = m_activeGrammars.keys();
    RuleParse rp = null;
    while (grammarsE.hasMoreElements()) {
      RuleGrammar rg = (RuleGrammar) grammarsE.nextElement();
      //      System.out.println("Examining grammar " + rg.getName());
      Vector activeR = (Vector) m_activeGrammars.get(rg);
      Enumeration rulesE = activeR.elements();
      // Activate the previously active rules for each grammar
      while (rulesE.hasMoreElements()) {
	String rule = (String) rulesE.nextElement();
	//	System.out.println("  Examining rule " + rule);
	  try {
	    rp = rg.parse(text, rule);
	  }
	  catch (GrammarException ex) {
	    ex.printStackTrace();
	  }
	  if (rp != null) {
//  	    System.out.println("Parsing matched with text");
//  	    System.out.println("I saw " + rp.toString());
//  	    System.out.println("It is of type :" + rp.getClass().getName());
	    if (m_manualCallbacks.containsKey(rg.getName())) {
	      RecGrammar ag = (RecGrammar) m_manualCallbacks.get(rg.getName());
	      ag.manualTrigger(rp);
	    }
	    else {
	      System.out.println("Warning: Grammar triggered that has no callback defined in processString");
	    }
	    return rg.getName();
	  }
	}
    }
    return null;
  }

  private  void sayActiveGrammars(boolean doSay) {
    RuleGrammar[] activeG = m_recognizer.listRuleGrammars();
    if (activeG != null) {
      if (doSay)
	Say.say("I have the following active grammars:");
      for (int i=0; i < activeG.length; i++) {
	if (activeG[i].isEnabled()) { 
	  String toSpeak = "";
	  System.out.println("I have grammar " + activeG[i].getName());
	  if (activeG[i].getName().indexOf(".") > -1) {
	    StringTokenizer st = new StringTokenizer(activeG[i].getName(),". \t\r\n",true);
	    while (st.hasMoreTokens()) {
	      String tok = st.nextToken();
	      if (tok.equals(".")) 
		toSpeak += "dot ";
	      else
		toSpeak += tok + " ";
	    }
	  }
	  else {
	    toSpeak = activeG[i].getName();
	  }
	  if (doSay)
	    Say.say(toSpeak);
	}
      }
    }
  }
  
  
  //
  // This is the listener for dictation results.  The
  // recognitionUpdate method is called for every recognized token.
  // The resultAccepted method is called when the dictation result
  // completes, which in this application occurs when the user says
  // "stop."
  //
  private ResultListener dictationListener = new ResultAdapter() {
    int n = 0; // number of tokens seen so far
    
    public void resultUpdated(ResultEvent e) {
      // We once set the recognizing flag here to determine whether
      // dictation had a null result.  Since we are no longer using
      // the "stop" command to end dictation, the flag's meaning has
      // changed to simply be whether getDictation is currently
      // active.  
      //recognizing = true;
      //      System.out.println("RecoUpdate");
      Result result = null;
      try {
	result = (Result) e.getSource();
      }
      catch (Exception ex) {
	ex.printStackTrace();
	System.out.println("I caught an exception in recognitionUpdate");
	m_me.doNotify();
	return;
      }
      System.out.println("I have recognitionUpdate inside " +
			 Thread.currentThread());
      for (int i=n; i<result.numTokens(); i++)
	System.out.println("token " + i + " " +
			   result.getBestToken(i).getSpokenText());
      n = result.numTokens();
      // sayActiveGrammars(false);
    }

    public void resultAccepted(ResultEvent e) {
      System.out.println("Dictation accepted result");
      FinalDictationResult result = null;
      try {
	result = (FinalDictationResult) e.getSource();
      }
      catch (Exception ex) {
	ex.printStackTrace();
	System.out.println("I caught an exception in resultAccepted");
	m_me.doNotify();
	return;
      }


      int n = result.numTokens();
      String s = "";
      //      System.out.println("I have n = " + n);
      for (int i=0; i<n; i++) 
	s += result.getBestToken(i).getSpokenText() + " ";

      System.out.println("I heard you say " + s);

      /* The IBM API has added this back so we should try making use of it
      ResultToken firstToken = result.getBestToken(0);
      ResultToken lastToken = result.getBestToken(result.numTokens() - 1 );
      ResultToken[][] altTokens = result.getAlternativeTokens(firstToken, lastToken, 3);

      for (int j = 0; j < altTokens.length; j++) {
	System.out.println("Alternative guess: " + j);
	ResultToken[] secondGuess = altTokens[j];
	for (int i = 0; i < secondGuess.length; i++) {
	  System.out.print(secondGuess[i].getSpokenText() + " ");
	}
	System.out.println();
      }
      */

      // record the start and end times
      m_startTime = result.getBestToken(0).getStartTime();
      m_endTime = result.getBestToken(n-1).getStartTime();	

      m_spokenString = s;
      //dictationGrammar.setEnabled(false);
      //commitChanges();            
      m_me.doNotify();
    }


    public void grammarFinalized (ResultEvent e) {
      System.out.println("I have grammarFinalized result!");
    }
    // This is in case the user says "stop" right away and no
    // resultAccepted for dictation is generated.
    public void resultRejected (ResultEvent e) {
      System.out.println("I have rejected Dictation result!");
      m_me.doNotify();
    }
    public void audioReleased (ResultEvent e) {
      System.out.println("I have audio released Dictation result!");
      m_me.doNotify();
    }
  };
  
  private synchronized void doNotify () {
    notifyAll();
  }
  
  //
  // Audio listener prints out audio levels to help diagnose problems.
  // (but it also slows down the system a bit!)
  private RecognizerAudioListener m_audioListener = new RecognizerAudioAdapter() 
  {
    // Have a little window that is green when user is speaking
    private AudioMonitorFrame monitor;
   
    public void audioLevel(RecognizerAudioEvent e) {
      if ( monitor == null ) 
	monitor = new AudioMonitorFrame(150,45);
      monitor.setLevel( e.getAudioLevel() );
    }
    
    // Turn off dictation when speech stops.
    public void speechStopped (RecognizerAudioEvent e) {
      if (m_recognizing == true) {
	System.out.println("Turning off dictation");
	try {
	  m_recognizing = false;
	  Thread.sleep(750);
	  m_recognizer.pause();
	  m_recognizer.resume();
	  playAcknowledgment();
	  //basicGrammar.setEnabled("stop", false);
	  dictationGrammar.setEnabled(false);

	  // should this be here?
	  wakeUp();
	  
	  System.out.println("Done turning off dictation");
	}
	catch (AudioException ex) {
	  ex.printStackTrace();
	}
	catch (Exception ex) {
	  System.out.println("Couldn't sleep");
	  ex.printStackTrace();
	}
      }
    }
  };
  
  public synchronized  void commitChanges () {
    try {
      m_recognizer.commitChanges();
    }
    catch (GrammarException ex) {
      ex.printStackTrace();
    }
  }

  public void addResultListener( ResultListener listener )
  {
    m_recognizer.addResultListener( listener );
  }
  
  
  public RuleGrammar addGrammar (URL base, String grammarName) {
    RuleGrammar g = null;
    try {
      g = (RuleGrammar) m_recognizer.loadJSGF(base, grammarName,
					      true, false, null);    
    }
    catch (Exception ex) {
      ex.printStackTrace();
      System.err.println( "ERROR: Failed to load grammar in Listen.java: " + ex );
    } 
    return g;
  }
  
  
  /**
   *  Try to avoid using this filename mechanism to load grammars
   *  because gramars loaded this way don't really support the JSGF
   *  import mechanism.
   **/
  public  RuleGrammar addGrammar (String filename) {
    RuleGrammar g = null;
    try {
      Reader reader = new FileReader(filename);
      g = (RuleGrammar) m_recognizer.loadJSGF(reader);    
      
    }
    catch (Exception ex) {
      ex.printStackTrace();
      System.err.println( "ERROR: Failed to load grammar in Listen.java: " +
			  ex );
    }
    return g;      
  }
  
  public synchronized TimedResult getTimedDictation () {
    //    System.out.println("Inside getDictation");
    goToSleep();
    //      basicGrammar.setEnabled(false);//mhc
    //System.out.println("turning on dictation");
    dictationGrammar.setEnabled(true);
    // basicGrammar.setEnabled("stop", true);
    commitChanges();
    // This flag is used to determine whether to stop dictation when
    // a speechStopped event is heard by the audioListener
    m_recognizing = true;
    // This is to deal with a bug if the user's dictation is empty.
    // The way it works now, users have up to 20 seconds to finish
    // their dictation.
    try {
      wait(10000);
      //System.out.println("Woke up from dictation");
    }
    catch (InterruptedException ex) {
      ex.printStackTrace();            
    }
    TimedResult tr = new TimedResult(m_startTime, m_endTime, m_spokenString);
    m_spokenString = "";
    m_startTime = -1;
    m_endTime = -1;
    //System.out.println("Dictation is done and returning " + tr);
    m_recognizing = false;
    //dictationGrammar.setEnabled(false);
    //basicGrammar.setEnabled("stop", false);
    //commitChanges();
    //goToSleep();        
    return tr;
  }

  public synchronized String getDictation () {
    String returnValue = "";
    //    System.out.println("Inside getDictation");
    goToSleep();
    //      basicGrammar.setEnabled(false);//mhc
    dictationGrammar.setEnabled(true);
    // basicGrammar.setEnabled("stop", true);
    commitChanges();
    // This flag is used to determine whether to stop dictation when
    // a speechStopped event is heard by the audioListener
    m_recognizing = true;
    // This is to deal with a bug if the user's dictation is empty.
    // The way it works now, users have up to 20 seconds to finish
    // their dictation.
    try {
      wait(10000);
      System.out.println("Woke up from dictation");
    }
    catch (InterruptedException ex) {
      ex.printStackTrace();            
    } 
    returnValue = m_spokenString;
    m_spokenString = "";
    m_startTime = -1;
    m_endTime = -1;
    System.out.println("Dictation is done and returning " + returnValue);
    m_recognizing = false;
    //dictationGrammar.setEnabled(false);
    //basicGrammar.setEnabled("stop", false);
    //commitChanges();
    //goToSleep();        
    return returnValue;
  }
  
  // Not yet implemented by IBM but would be really useful
  private RecognizerListener recoListener2 = new RecognizerAdapter () {
    public void committedChanges (RecognizerEvent e) {
      System.out.println("Committed changed");
    }
    
    public void speechEnd (RecognizerEvent e) {
      System.out.println("You are done speaking!");
    }
    
    public void partialResultUpdate(RecognizerEvent e)  {
      System.out.println("The partial result has been updated. ");
    }
    
    public  void partialResultClear(RecognizerEvent e) {
      System.out.println("The partial result has been cleared.");
    }              
  };
  
  
  //
  // main
  //
  public static void main(String args[]) 
  {
    System.err.println( "Main for speech.Listen invoked" );
    //    RecListen listen = new RecListen();
    RecGrammar gram = RecGrammar.makeRecGrammar( args[0] );
    TagsListener tagger = new TagsListener() {
      public void resultAccepted( Tag tags ) {
	DebugWindow.addLine( "Results", tags.toString() );
      }
      
    };

    gram.addTagsListener( tagger );
    System.out.println( "Activate = true:" );
    //    gram.activate();
    gram.activate();
    Vector v = gram.getPublicRules();
    Iterator it = v.iterator();
    while ( it.hasNext() )
      System.out.println( it.next() );
 
  }
}
