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

// -*-Mode: Java-*- 
// $Id: RecGrammar.java,v 1.10 2003/03/06 00:40:40 moltmans Exp $
// RecGrammar.java -- 
// Author: Michael Oltmans <moltmans@ai.mit.edu> 
// Created: <Tue Feb 29 13:38:02 2000> 
// Time-stamp: <2001-03-05 20:44:45 moltmans> 
package rec.speech;

import java.io.*;
import java.util.*;
import java.net.*;
import java.text.DateFormat;
import javax.speech.*;
import javax.speech.recognition.*;
import javax.speech.recognition.Recognizer.*;
import rec.util.*;

/**
 * This is a simplified interface to the rule and dictation grammars
 * in the Java Speech API (JSAPI beta v0.7).
 *
 * This interface provides all the basic functionality of the JSAPI
 * RuleGrammar class but omits for the time being anything esoteric.
 * It also adds several very useful things, such as a save() method,
 * TagsListeners and synchronous getDictation() and saySync() methods.
 * It is also thread-safe.
 *
 * For more information on the Java Speech Grammar Format (JSGF)
 * specification, in which you actually write your grammars, see:
 *
 * http://java.sun.com/products/java-media/speech/forDevelopers/JSGF
 *
 * If you want to use speech recognition from Metaglue, don't use this
 * class directly.  Use the AppGrammarAgent instead, which is an
 * agentified interface to this class.
 *
 * Notes:
 *
 * In the RecGrammar model, every grammar has its own thread,
 * so that it can't monopolize control over the recognizer's (& AWT's)
 * event thread.
 *
 * People using this class don't need to be concerned with this,
 * except to note that until you are done processing a given result in
 * your grammar, you can't receive new ones.  But, the all-important
 * point is that other RecGrammars can receive their speech
 * events while you are still processing yours.
 *
 * Simply put, don't worry, everything will work the way you (should)
 * expect it to!
 *
 * Sample calls:
 *
 * RecGrammar g = new RecGrammar ("package.grammarName");
 * g.setEnabled(boolean value);
 * g.addResultListener(Listener myListener);
 * g.addTagsListener(TagsListener myTagsListener);
 * g.removeGrammarListener(Listener myListener);
 * g.addToGrammarGroup(String name);
 * g.activate();
 * g.deactive();
 * g.activate(String ruleName);
 * g.deactive(String ruleName);
 * g.addAlternative(String ruleName, String spokenText);
 * g.save();
 * String spokenText = g.getDictation();
 * 
 * // The following should not be used if you want to be RMI-friendly
 * // (see note below)
 * g.addTagsListener(String tag, TagsListener myTagsListener);
 *
 * @author mhcoen@ai.mit.edu
 * @date July 7, 1998
 * Copyright (c) 1998 Michael H. Coen.  All Rights Reserved.
 *
 * Adapted for Rec by Mike Oltmans Feb 29, 2000
 **/
public class RecGrammar {

  private static RecListen m_listen = null;
  private static Hashtable m_grammars = new Hashtable();

  private RecGrammar m_me = this;
  private RuleGrammar m_myGrammar;
  private Vector m_listenersV = new Vector();
  private Vector m_tagsListenersV = new Vector();
  private Hashtable m_tagsHash = new Hashtable ();
  //  private RecListener m_grammarListener;
  private ResultEvent m_resultEvent = null;
  private RuleParse m_parseEvent = null;
  private String m_myGrammarFile = "";
  private String m_grammarName = "";
  private long m_eventStartTime;
  private int m_tokenCounter = 0;

  static
  {
    if (m_listen == null) {
      m_listen = RecListen.getListen();
    }
  }

  public static RecGrammar makeRecGrammar( String packageList ) 
  {
    RecGrammar grammar = (RecGrammar)m_grammars.get( packageList );
    if( grammar == null )
      grammar = new RecGrammar( packageList );
    return grammar;
  }

  public static RecGrammar getRecGrammar( String packageList ) 
  {
    return (RecGrammar)m_grammars.get( packageList );
  }
  

  /**
   * Create an RecGrammar, e.g.
   * new RecGrammar("agentland.device.speechin.VCR");
   *
   * In the code below, you might wonder why we don't just create the
   * recognizer statically once in this class, rather than always
   * check for it.  This has to do with the way the Java class loader
   * initializes static constructors.  Java is not at fault here, but
   * we need to be careful to avoid the unfortunate interactions this
   * can sometimes cause, for example, when you use the reflection
   * API.
   */
  private RecGrammar(String packageList) 
  {
    m_grammarName = packageList;

    fixListen();
    if (packageList.equals("")) {
      System.out.println("Note: RecGrammar started with empty grammar");
      return;
    }

    // get the grammar
    String searchFile = packageList.replace('.', '/');
    searchFile += ".gram";     // Add the required suffix

    // We keep track of the filename in case we want to save a
    // modified version of this grammar, e.g., it gets changed while
    // it is being used and we want to keep its state.  (It might
    // represent, for example, an expandable list of countries in a
    // map system.)

    URL gramUrl = ClassFinder.getResource(searchFile);
    
    m_myGrammarFile = gramUrl.getFile();

    //    m_myGrammarFile = ClassFinder.getRecResource( searchFile ).getFile();

    //    System.out.println("I am loading grammar from file " + myGrammarFile);
    m_myGrammar = m_listen.addGrammar(null, packageList);
      
    if (m_myGrammar == null) {
	System.out.println("WARNING: Grammar " + packageList + " could not be loaded");
	return;
    }

    String grammarText = m_myGrammar.toString();
    
    // This is for triggering manual callbacks when a string is
    // provided as input instead of a spoken utterance
    m_listen.setManualCallback(m_myGrammar.getName(), this);

    //    System.out.println("I am loading grammar " + myGrammar);

    try {
      m_myGrammar.addResultListener(m_ruleListener);
      m_myGrammar.setEnabled(false);
      m_listen.commitChanges();
    }
    catch (Exception ex) {
      System.out.println("I caught an exception adding a grammar.");
      ex.printStackTrace();
    }


    // start the message handler
    Thread life = new Thread(){
      public void run() {
	looper();
      }
    };
    life.start();
    Tag.gatherAllTagsFromAllGrammars();
    m_grammars.put( m_grammarName, this );
  }


  synchronized void fixListen()
  {
    if (m_listen == null) {
      System.err.println( "WARNING: resetting listen!" );

      m_listen = RecListen.getListen(); // Start speech recognizer
    }
  }

  public Enumeration listRuleNames () {
    String[] names = m_myGrammar.listRuleNames();
    Vector namesV = new Vector(names.length);
    for (int i = 0; i < names.length; i++) {
      namesV.addElement(names[i]);
    }
    return namesV.elements();
  }

  public void printRules () {
    Enumeration rules = listRuleNames();
    while (rules.hasMoreElements()) {
      String ruleName = (String) rules.nextElement();
      Rule rule = m_myGrammar.getRule(ruleName);
      System.out.println("Rule: " + rule.toString());
    }
  }

  /**
   * This adds an alternative to some rule.  So if you had a rule
   * mapping to pig | dog, you could add cat so it would map to
   * pig | dog | cat
   */
  public void addAlternative (String ruleName, String spokenText) {
    Vector v = new Vector();
    v.addElement(spokenText);
    addAlternatives(ruleName, v);
  }


  /**
   * This does the same as the single case, but for a vector of
   * alternatives.  More efficient.
   */
  public void addAlternatives (String ruleName, Vector spokenText) 
  {
//      System.out.println("\nEntered addAlternatives: " + ruleName 
//  		       + ", " + spokenText );
    Rule r = m_myGrammar.getRule(ruleName);
    Assert.notNull( r,
		    "I can't find rule " + ruleName +
		    " in grammar " + this );

//      Assert.check( r instanceof javax.speech.recognition.RuleAlternatives,
//  		  "Alternatives may only be added to Alternative rules."
//  		  + "This is a " + r.getClass().getName() );
  
    RuleAlternatives alts = null;

    // if it isn't alternatives make it have alternatives
    if( r instanceof RuleAlternatives ) {
      alts = (RuleAlternatives)r;
    }
    else {
      alts = new RuleAlternatives( r );
    }
    //    System.out.println("rule alternatives: " + alts + "\n");
    Enumeration e = spokenText.elements();
	
    String el = null;
    while(e.hasMoreElements()) 
    {
      el = (String)e.nextElement();
      alts.append(new RuleToken(el));
      //      System.out.println("appending " + el);
    }
    m_myGrammar.setRule(ruleName, alts, m_myGrammar.isRulePublic(ruleName));
    m_listen.commitChanges();
  }

  public void pauseRecognizer () {
    m_listen.pauseRecognizer();
  }

  public void resumeRecognizer () {
    m_listen.resumeRecognizer();
  }



  // Save the grammar as a JSGF file.  This allows a grammar's
  // modified state to have persistence.  Grammars can be changed
  // while they are in use, as in for example, an expandable list of
  // countries in a map system.  
  //
  // I recommend that you put modifiable rules in their own grammars,
  // in separate files from static grammars.  This is because comments
  // are lost in the load/save process, so modifiable grammars should
  // be pretty self-explanatory -- so you know what they are doing w/o
  // any commentary.
  public  void save () {
    String grammarText = this.toString();
    System.out.println("Saving grammar to " + m_myGrammarFile);
    try {
      PrintWriter pw = new PrintWriter(new FileWriter(m_myGrammarFile));
      pw.println(grammarText);
      pw.close();
    }
    catch (IOException ex) {
      ex.printStackTrace();
      return;
    }
  }

  // This is here because the JSAPI call of this name isn't working
  // (Bug: does not include imported grammar list.)
  public String toString ()
  {
    String buffer = "grammar " + m_myGrammar.getName() + ";\n";
    buffer += "// Automatically written on "
     + DateFormat.getDateInstance(java.text.DateFormat.FULL).format(new Date())
     + "\n\n";
    Enumeration rules = listRuleNames();
    while (rules.hasMoreElements()) {
      String ruleName = (String) rules.nextElement();
      Rule rule = m_myGrammar.getRule(ruleName);
      if (m_myGrammar.isRulePublic(ruleName))
	buffer += "public <" + ruleName + "> = " + rule.toString() + ";\n\n";
      else
      	buffer += "<" + ruleName + "> = " + rule.toString() + ";\n\n";
    }
    return buffer;
  }


  // Why is this here you ask?  It has to do with a bug in the current
  // IBM JSAPI implementation.  There are some unfortunate
  // interactions between speech recognition and synthesis that this
  // allows us to avoid.  (Detail are in RecListen.java towards the
  // bottom.)  Eventually, we'll move speech out of here.
  public void say (String text) 
  {
    Say.say(text);
  }
//    public void saySync (String text) 
//    {
//      m_listen.saySync(text);
//    }

  public String getDictation () {
    System.out.println("Calling listen.getDictation()");
    return m_listen.getDictation();
  }

  public synchronized void addTagsListener (TagsListener suppliedListener) {
    // AFAIK: there's no good reason why anything would want to get
    // two notifications of the same event
    if( m_tagsListenersV.contains( suppliedListener ) )
      return;
    m_tagsListenersV.addElement(suppliedListener);
  }

  // This is not useful if you want to access this via RMI (which is
  // how Metaglue does it), because there's no easy way to do a
  // generalized callback like this.
  public synchronized void addTagsListener (String tag,
					    TagsListener suppliedListener) {
    Vector specificListeners = (Vector) m_tagsHash.get(tag);
    if (specificListeners == null) {
      specificListeners = new Vector();
      m_tagsHash.put(tag, specificListeners);
    }
    specificListeners.addElement(suppliedListener);
  }

  public synchronized void removeTagsListener (String tag,
					       TagsListener suppliedListener) 
  {
    Vector specificListeners = (Vector) m_tagsHash.get(tag);
    if (specificListeners != null) {
      specificListeners.removeElement(suppliedListener);
    }
  }

  public synchronized void removeTagsListener (TagsListener suppliedListener) {
    m_tagsListenersV.removeElement(suppliedListener);
  }

  public synchronized void addGrammarListener( RecSpeechListener suppliedListener) {
    m_listenersV.addElement(suppliedListener);
  }

  public synchronized void removeGrammarListener (RecSpeechListener suppliedListener) {
    m_listenersV.removeElement(suppliedListener);
  }

  public void activate() {
    m_listen.activate(m_myGrammar);
  }
  public void activate(String ruleName) {
    m_listen.activate(m_myGrammar, ruleName);
  }
  public void deactivate() {
    m_listen.deactivate(m_myGrammar);
  }
  public void deactivate(String ruleName) {
    m_listen.deactivate(m_myGrammar, ruleName);
  }

//    public void setActive(boolean value) {
//      m_myGrammar.setEnabled(value);
//      m_listen.commitChanges();
//    }

  public Vector getPublicRules () {
    return m_listen.getPublicRules(m_myGrammar);
  }
  public Vector getActiveRules () {
    return m_listen.getActiveRules(m_myGrammar);
  }

  public String getName () {
    return m_myGrammar.getName();
  }

  public String getFile () {
    return m_myGrammarFile;
  }

  public RuleGrammar getRuleGrammar() 
  {
    return m_myGrammar;
  }
  

  /* Unfold methods for pulling text out of a parse tree.  This allows
     users to actually get the text corresponding to particular tokens
     in a parse.  This is a strange API omission on the part of JSAPI.  */
  private Tag unfoldRule (Rule r, List tokens )
  {
    m_eventStartTime = System.currentTimeMillis();
    m_tokenCounter = 0;
    //    m_tagsValueHash = new Hashtable();
    LinkedList tags = new LinkedList();
    Unfolder result = unfold(r, tags, tokens);
    return new Tag( "__fulltext", result.i_text, tags,
		    result.i_start, result.i_end );
  }

  private Unfolder unfold (Rule r, LinkedList tags, List tokens)
  {
    if (r instanceof javax.speech.recognition.RuleCount) {
      return unfold((RuleCount) r, tags, tokens);
    }
    else if (r instanceof javax.speech.recognition.RuleAlternatives) {
      return unfold((RuleAlternatives) r, tags, tokens);
    }
    else if (r instanceof javax.speech.recognition.RuleSequence) {
      return unfold((RuleSequence) r, tags, tokens);
    }
    else if (r instanceof javax.speech.recognition.RuleTag) {
      return unfold((RuleTag) r, tags, tokens);
    }
    else if (r instanceof javax.speech.recognition.RuleToken) {
      return unfold((RuleToken) r, tags, tokens);
    }
    else if (r instanceof javax.speech.recognition.RuleParse) {
      return unfold((RuleParse) r, tags, tokens);
    }
    else {
      System.out.println("Error!  Unknown rule class");
      System.out.println("I have " + r);
      System.out.println("with class " + r.getClass().getName());
      return null;
    }
  }

  private Unfolder unfold (RuleParse rp, LinkedList tags,
			   List tokens)
  {
    //    DebugWindow.addLine( "Parse", "RuleParse: " + rp );
    return unfold(rp.getRule(), tags, tokens);
  }

  private Unfolder unfold (RuleCount rc, LinkedList tags,
			   List tokens)
  {
    DebugWindow.addLine( "Parse", "RuleCount: " + rc );
    return unfold(rc.getRule(), tags, tokens);
  }

  private Unfolder unfold (RuleAlternatives ra, LinkedList tags,
			   List tokens)
  {
    DebugWindow.addLine( "Parse", "RuleAlternatives: " + ra );
    return unfold((ra.getRules())[0], tags, tokens);
  }

  private Unfolder unfold (RuleSequence rs, LinkedList tags,
			   List tokens)
  {
    //    DebugWindow.addLine( "Parse", "RuleSequence: " + rs );
    Unfolder content = new Unfolder();
    for (int i = 0; i < rs.getRules().length; i++) {
      content.merge( unfold( (rs.getRules())[i], tags, tokens) );
    }
    return content;
  }

  private Unfolder unfold (RuleToken rt, LinkedList tags,
			   List tokens)
  {
    //    DebugWindow.addLine( "Parse", "RuleToken: " + rt );
    if( tokens.isEmpty() ) {
      // This makes it look like the utterance had a
      // duration... albeit a very short one
      long time = m_eventStartTime;
      //      return new Unfolder( rt.getText(), time, time + 1 );
      return new Unfolder( rt.getText(),
			   time + m_tokenCounter, 
			   time + m_tokenCounter++ );
    }    
    
    ResultToken result = (ResultToken)tokens.remove(0);
    return new Unfolder( rt.getText(), result.getStartTime(),
			 result.getEndTime() );
  }

  private Unfolder unfold (RuleTag rt, LinkedList tags,
			   List tokens)
  {
    //    DebugWindow.addLine( "Parse", "RuleTag: " + rt );

    LinkedList subTags = new LinkedList();
    String tagName = rt.getTag();
    Unfolder result = unfold( rt.getRule(), subTags, tokens );
    Tag tag = new Tag( tagName, result.i_text, subTags,
		       result.i_start, result.i_end );
    tags.addLast( tag );
    //    m_tagsValueHash.put(tagName, contents);
    //    System.out.println( "   Tag "  + tagName + " = " + text );
    return result;
  }


  class Unfolder
  {
    String i_text;
    long i_start;
    long i_end;
    public Unfolder()
    {
      i_text = "";
      i_start = Long.MAX_VALUE;
      i_end = Long.MIN_VALUE;
    }
    
    public Unfolder( String text, long start, long end )
    {
      i_text = text;
      i_start = start;
      i_end = end;
    }
    public void merge( Unfolder u )
    {
      i_text = ( i_text + " " + u.i_text ).trim();
      i_start = Math.min( u.i_start, i_start );
      i_end = Math.max( u.i_end, i_end );
    }
    
  }
  
  // This is run in a separate thread and waits for the speech
  // recognizer to indicate that it heard something.  It then notifies
  // all the registered listeners.
  private synchronized void looper () {
    while (true) {
      try {
	wait(); // Wait for a recognition event
      } catch (InterruptedException ex) {
	ex.printStackTrace();
      }
      //      System.out.println("Received notify");
      ResultEvent myResultEvent = m_resultEvent;
      RuleParse rp = m_parseEvent;
      String ruleName = "";
      List tokens = null;

      // Was this actually spoken by a person?
      if (rp == null) {
	//	System.out.println("Processing spoken utterance");
	// Now find out what text is associated with each tag
	FinalRuleResult result = (FinalRuleResult) m_resultEvent.getSource();
	Object[] tokenArray = result.getBestTokens();
	if( tokenArray != null ) {
	  tokens = new LinkedList( Arrays.asList( tokenArray ) );
	}

	String[] tags = result.getTags();
	RuleGrammar rg = result.getRuleGrammar(0);
	ruleName = result.getRuleName(0);

	m_listen.playAcknowledgment();

	try {
	  rp = rg.parse(result, 0, ruleName);
	}
	catch (GrammarException ex) {
	  ex.printStackTrace();
	}
	//	System.out.println("I have event for " + result.getRuleName(0));

      }
      else {
	//	System.out.println("Processing textual utterance");
      }

      if( tokens == null ) {
	tokens = new LinkedList();
      }

      //      System.out.println("I saw " + rp.toString());
      //      System.out.println("It is of type :" + rp.getClass().getName());
      RuleSequence rule = (RuleSequence) rp.getRule();
      Tag tags = unfoldRule(rule, tokens);
      Set tagSet = tags.getAllTags();

      //      System.out.println( tags );

      //      m_tagsValueHash.put("rule", ruleName);
      //      m_tagsValueHash.put("grammar", m_grammarName);

      // No tags means no tags listeners get called!
      if ( tagSet.size() == 0) {
	//	System.out.println("There are no tags, so I'm returning");
	continue;
      }
      if (! m_tagsListenersV.isEmpty()) {
	for (int i = 0; i < m_tagsListenersV.size(); i ++) {
	  //	  System.out.println("I am calling parent." );
	  // + m_tagsListenersV.elementAt(i));

	  ((TagsListener) m_tagsListenersV.elementAt(i)).resultAccepted(tags);
	  //	  System.out.println("Call returned");
	}
      }
      m_parseEvent = null;
      m_resultEvent = null;
    }
  }

  private synchronized void doNotify () {
    //    System.out.println("Doing notify in RecGrammar");
    notifyAll();
  }

  public void requestWakeUp() {
    m_listen.requestWakeUp();
  }

  public String processString (String s) {
    return m_listen.processString(s);
  }

  public synchronized void manualTrigger (RuleParse rp) {
    //    System.out.println("I am in manualTrigger");
    m_parseEvent = rp;
    m_me.doNotify();
  }

  private ResultListener m_ruleListener = new ResultAdapter() {
    // Notify the thread running the looper procedure above and then
    // immediately return control to the recognizer.  This allows
    // other speech events to occur while this result is being
    // processed.  This method is called by the recognizer event
    // thread.
    public void resultAccepted(ResultEvent e) {
      //      System.out.println("I am in ruleListener");
      m_resultEvent = e;
      m_me.doNotify();
    }
  };

}
