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

// $Id: SpeechAnalyzer.java,v 1.3 2004/04/06 19:41:12 cadlerun Exp $
// SpeechAnalyzer.java -- 
// Author: Aaron Adler <cadlerun@fracas.ai.mit.edu> 
// Copyright:  Copyright (C) 2003 by MIT.  All rights reserved.
// Created: <Fri May 16 12:12:32 2003>

/***
 * This class will analyze the speech coming into the system
 * it will look for keywords which will trigger a later analysis.
 * The analysis can cause the widgets to be manipulated.
 ****/

package rec.multi;

import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Set;
import java.util.HashSet;
import java.util.Collection;
import java.util.List;

import java.awt.Rectangle;

import edu.mit.tools.watch.timeline.TimelineDataElement;
import edu.mit.tools.watch.display.GroupList;

import rec.sys.Widget;
import rec.simulate.widgets.wm.WMSimCircle;
import rec.simulate.widgets.wm.WMSimRod;
import rec.simulate.widgets.wm.WMSimPinJoint;
import rec.simulate.SimulateSurface;
import rec.simulate.widgets.WSimObject;
import rec.simulate.widgets.WSimBody;
import rec.simulate.widgets.WSimPolygon;

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

public class SpeechAnalyzer {
  private static Logger LOG = Logging.SPEECH;

  private ArrayList m_lookingFor; // looking for these utterances - SpeechEvents
  private ArrayList m_oldSpeech; // not looking for these, but keep them around -- SpeechEvents

  private Hashtable m_widgets; // keep track of the widgets themselves

  private DynamicWatchController m_dwc;
  
  public SpeechAnalyzer(DynamicWatchController dwc) {
    // initialize variables
    m_dwc = dwc;
    m_lookingFor = new ArrayList();
    m_oldSpeech = new ArrayList();
    
    //set up the hashtable
    m_widgets = new Hashtable();
  }

  public void addSpeechEvent(int startTime, int endTime, String speech) {
    // add a new speech event
    
    LOG.info("received a new speech event.");
    SpeechEvent se = new SpeechEvent(startTime, endTime, speech);

    if (hasTriggerWord(speech)) {
      LOG.debug("speech has a trigger word... adding to looking for list");
      m_lookingFor.add(se);
    }
    else {
      LOG.debug("speech does not have a trigger word... adding to old list");
      m_oldSpeech.add(se);
    }
  }

  public void addWidget(int start, int end, Widget w) {
    // TO DO: can't really use start+end time here, but for now its ok.
    String key = start + " " + end;
    m_widgets.put(key, w);
  }

  public Widget getWidget(String key) {
    return (Widget)m_widgets.get(key);
  }

  public Widget getWidget(int start, int end) {
    return getWidget(start + " " + end);
  }

  public boolean hasSpeechToAnalyze() {
    return (m_lookingFor.size() > 0);
  }

  public void analyze(EventGroup eventGrp) {
    // analyze the EventGroup trying to match speech utterance with
    // widgets... 
    
    String speech;
    int startT, endT;
    String trigger;
    SpeechEvent se;
    
    LOG.debug("analyzing speech.");
    
    // loop through the speech phrases we have to analyze
    Iterator sents = m_lookingFor.iterator();
    while (sents.hasNext()) {
      se = (SpeechEvent)sents.next();
      speech = se.getSpeech();
      startT = se.getStartTime();
      endT = se.getEndTime();
      
      trigger = getTriggerWord(speech);

      if (trigger.equals("pendulum")) {
	// trying to find pendulums
	boolean equal = containsWord("equal", speech);
	boolean identical = containsWord("identical",speech);
	boolean touch = containsWord("touch",speech);
	int number = containsNum(speech);

	LOG.info("pendulum trigger has the following words: equal "+equal+" identical "+identical+" touch " + touch +" number "+number);

	Set pendulums = findPendulums(eventGrp, startT);

	int numPendulums = pendulums.size();
	if (numPendulums > number) {
	  // too many -- for now ignore the speech
	  LOG.info("too many pendulums found for speech. tossing speech.");
	  m_lookingFor.remove(se);
	  sents = m_lookingFor.iterator(); // reset iterator
	  m_oldSpeech.add(se);
	}
	else if (numPendulums == number){
	  LOG.info("we have a match, move the pendulums.");
	  movePendulums(pendulums, equal, identical, touch);
	  
	  m_lookingFor.remove(se);
	  sents = m_lookingFor.iterator(); // reset iterator
	  m_oldSpeech.add(se);
	}
	// if there aren't enough pendulums yet, just keep waiting
      }
      
      if (trigger.equals("wheel")) {
	// trying to find wheels
	boolean identical = containsWord("identical",speech);
	int number = containsNum(speech);

	LOG.info("wheel trigger has the following words: identical "+identical+" number "+number);

	Set wheels = findWheels(eventGrp, startT);

	int numWheels = wheels.size();
	if (numWheels > number) {
	  // too many -- for now ignore the speech
	  LOG.info("too many wheels found for speech. tossing speech.");
	  m_lookingFor.remove(se);
	  sents = m_lookingFor.iterator(); // reset iterator
	  m_oldSpeech.add(se);
	}
	else if (numWheels == number){
	  LOG.info("we have a match, move the wheels.");
	  moveWheels(wheels, identical);
	  
	  m_lookingFor.remove(se);
	  sents = m_lookingFor.iterator(); // reset iterator
	  m_oldSpeech.add(se);
	}
	// if there aren't enough wheels yet, just keep waiting
      }
      
    }
    
  }

  private void movePendulums(Set pendulums, boolean equal, boolean identical, boolean touch) {
    // move the pendulums according to the values of equal, identical and touch
    
    LOG.debug("moving the pendulums....");

    ManipulatePendulum mp = new ManipulatePendulum(pendulums);
    
    if (identical) {
      mp.makeIdentical();
    }
    if (touch) {
      mp.touching();
    }
    else if (equal) {
      mp.equallySpace();
    }

  }

  private void moveWheels(Set wheels,boolean identical) {
    // move the wheels according to the values of identical
    
    LOG.debug("moving the wheels....");

    ManipulateWheel mw = new ManipulateWheel(wheels);
    
    if (identical) {
      mw.makeIdentical();
    }
    //if (touch) {
    //mw.touching();
    //}
    //else if (equal) {
    //  mw.equallySpace();
    //}

  }

  ////////////// pendulum finding functions //////////////
  
  private Set findPendulums(EventGroup eventGrp, int time) {
    // this function looks for pendulums in the event group around
    // time time.
    Set grp;
    HashSet relevantWidgets = new HashSet();
    ArrayList widgetList;
    boolean loop = true;
    
    grp = eventGrp.getGroupByTime(time);
    widgetList = getCorrespondingWidgets(grp);
    relevantWidgets.addAll(widgetList); // add everything in the set in which the speech occurs regardless of anything else

    // search forward
    while (loop && eventGrp.hasNextGroup()) {
      grp = eventGrp.getNextGroup();
      widgetList = getCorrespondingWidgets(grp);
      if (checkPendulumGroup(widgetList)) {
	// these widgets are important
	LOG.debug("found more widgets in forward search");
	relevantWidgets.addAll(widgetList);
      }
      else {
	// found irrelevant widgets, stop search
	LOG.debug("found irrelevant widgets, stopping forward search");
	loop = false;
      }
    }

    // search backwards
    grp = eventGrp.getGroupByTime(time);
    while (loop && eventGrp.hasPreviousGroup()) {
      grp = eventGrp.getPreviousGroup();
      widgetList = getCorrespondingWidgets(grp);
      if (checkPendulumGroup(widgetList)) {
	// these widgets are important
	LOG.debug("found more widgets in backwards search");
	relevantWidgets.addAll(widgetList);
      }
      else {
	// found irrelevant widgets, stop search
	LOG.debug("found irrelevant widgets, stopping backwards search");
	loop = false;
      }
    }

    return getPendulumsFromSet(relevantWidgets);
  }


  private Set getPendulumsFromSet(Set widgets) {
    // returns a set of pendulums from widgets
    
    HashSet pend = new HashSet();
    boolean attach = false;

    LOG.debug("hunting for the pendulums...");
    LOG.debug("widgets: "+widgets);

    Iterator wIt = widgets.iterator();

    while (wIt.hasNext()) {
      // look for a rod, and then see if it is
      // connected to a widget that is also in
      // the set
      Widget w = (Widget) wIt.next();
      if (!attach) {
	// we need to make sure the attachments are up-to-date
	LOG.debug("finding attachments on surface");
	((SimulateSurface)w.getSurface()).findAttachments();
	attach = true;
      }
      
      LOG.debug("looking at next widget");
      
      if (w instanceof WMSimRod) {
	WMSimRod rod = (WMSimRod) w;
	LOG.debug("found a rod - attached to :"+rod.numAttached());

	// check to see how many things it is attached to
	// NOTE for now we only consider pendulums to be
	// a rod with one end attached to a circle.
	if (rod.numAttached() == 1) {
	  LOG.debug("attached at one point");
	  Widget body = (Widget)(rod.getBodies().get(0));
	  if (widgets.contains(body) && (body instanceof WMSimCircle)) {
	    LOG.debug("found a pendulum!!!");

	    HashSet p = new HashSet();
	    p.add(body);
	    p.add(rod);
	    pend.add(p); // add to the set of pendulums

	    // remove the rod and body so that they can't be in more than one pendulum
	    widgets.remove(body);
	    widgets.remove(rod);

	    wIt = widgets.iterator(); // get a new iterator
	    
	  }
	}
      }
    }

    return pend;
  }

  private boolean checkPendulumGroup(ArrayList allWidgets) {
    // returns true if allWidgets contains only Rods and circles
    boolean contains = true;

    Iterator wIt = allWidgets.iterator();

    while (wIt.hasNext() && contains) {
      Widget w = (Widget) wIt.next();

      if ((!(w instanceof WMSimRod)) &&
	  (!(w instanceof WMSimCircle))) {
	LOG.debug("widget "+w+" doesn't match");
	contains = false;
      }
    }

    return contains;
  }

  ////////////// wheel finding functions //////////////
  
  private Set findWheels(EventGroup eventGrp, int time) {
    // this function looks for wheels in the event group around
    // time time.
    Set grp;
    HashSet relevantWidgets = new HashSet();
    ArrayList widgetList;
    boolean loop = true;
    
    grp = eventGrp.getGroupByTime(time);
    widgetList = getCorrespondingWidgets(grp);
    relevantWidgets.addAll(widgetList); // add everything in the set in which the speech occurs regardless of anything else

    // search forward
    while (loop && eventGrp.hasNextGroup()) {
      grp = eventGrp.getNextGroup();
      widgetList = getCorrespondingWidgets(grp);
      if (checkWheelGroup(widgetList)) {
	// these widgets are important
	LOG.debug("found more widgets in forward search");
	relevantWidgets.addAll(widgetList);
      }
      else {
	// found irrelevant widgets, stop search
	LOG.debug("found irrelevant widgets, stopping forward search");
	loop = false;
      }
    }

    // search backwards
    grp = eventGrp.getGroupByTime(time);
    while (loop && eventGrp.hasPreviousGroup()) {
      grp = eventGrp.getPreviousGroup();
      widgetList = getCorrespondingWidgets(grp);
      if (checkWheelGroup(widgetList)) {
	// these widgets are important
	LOG.debug("found more widgets in backwards search");
	relevantWidgets.addAll(widgetList);
      }
      else {
	// found irrelevant widgets, stop search
	LOG.debug("found irrelevant widgets, stopping backwards search");
	loop = false;
      }
    }

    return getWheelsFromSet(relevantWidgets);
  }


  private Set getWheelsFromSet(Set widgets) {
    // returns a set of wheels from widgets
    
    HashSet wheels = new HashSet();
    boolean attach = false;

    LOG.debug("hunting for the wheels...");
    LOG.debug("widgets: "+widgets);

    Iterator wIt = widgets.iterator();

    while (wIt.hasNext()) {
      // look for a pin joint, and then see if it connected
      // to a circle that is also in the set
      
      Widget w = (Widget) wIt.next();
      if (!attach) {
	// we need to make sure the attachments are up-to-date
	LOG.debug("finding attachments on surface");
	((SimulateSurface)w.getSurface()).findAttachments();
	attach = true;
      }
      
      LOG.debug("looking at next widget");
      
      if (w instanceof WMSimPinJoint) {
	WMSimPinJoint pin = (WMSimPinJoint) w;
	LOG.debug("found a pin joint - attached to :"+pin.numAttached());

	// now see if it is a wheel where a wheel is a circle
	// with a pin joint. if it is attached to 2 circles, then we
	// assume that the wheel is the smaller of the two circles
	if (pin.numAttached() == 1) {
	  LOG.debug("attached at one point");
	  Widget body = (Widget)(pin.getBodies().get(0));
	  if (widgets.contains(body) && (body instanceof WMSimCircle)) {
	    LOG.debug("found a wheel!!!");

	    HashSet p = new HashSet();
	    p.add(body);
	    p.add(pin);
	    wheels.add(p); // add to the set of pendulums

	    // remove the pin joint and body so that they can't be in more than one wheel
	    widgets.remove(body);
	    widgets.remove(pin);

	    wIt = widgets.iterator(); // get a new iterator
	    
	  }
	}
	else if (pin.numAttached() == 2) {
	  // if it is attached to 2 bodies, see if either one is a circle
	  List bodies = pin.getBodies();
	  Widget body1 = (Widget)(bodies.get(0));
	  Widget body2 = (Widget)(bodies.get(1));
	  boolean isBody1Wheel = (widgets.contains(body1) && (body1 instanceof WMSimCircle));
	  boolean isBody2Wheel = (widgets.contains(body2) && (body2 instanceof WMSimCircle));
	  
	  if (isBody1Wheel || isBody2Wheel) {
	    LOG.debug("found a wheel!!!");

	    // if both bodies are possible wheels, we'll take the smaller of the circles
	    if (isBody1Wheel && isBody2Wheel) {
	      double size1 = ((WMSimCircle)body1).size();
	      double size2 = ((WMSimCircle)body2).size();
	      
	      LOG.debug("both attached bodies are circles, resolving ambiguity");
	      if (size1 < size2) {
		LOG.debug("body 1 is the wheel");
		isBody2Wheel = false;
	      }
	      else {
		LOG.debug("body 1 is the wheel");
		isBody1Wheel = false;
	      }
	    }

	    // add to the set of wheels and remove the used components
	    HashSet p = new HashSet();
	    if (isBody1Wheel) {
	      widgets.remove(body1);
	      p.add(body1);
	    }
	    else {
	      widgets.remove(body2);
	      p.add(body2);
	    }
	    p.add(pin);
	    wheels.add(p); // add to the set of pendulums

	    // remove the pin joint so that it can't be in more than one wheel
	    widgets.remove(pin);

	    wIt = widgets.iterator(); // get a new iterator
	    
	  }

	}
      }
    }

    return wheels;
  }

  private boolean checkWheelGroup(ArrayList allWidgets) {
    // returns true if allWidgets contains only pin joints and circles
    boolean contains = true;

    Iterator wIt = allWidgets.iterator();

    while (wIt.hasNext() && contains) {
      Widget w = (Widget) wIt.next();

      if ((!(w instanceof WMSimPinJoint)) &&
	  (!(w instanceof WMSimCircle))) {
	LOG.debug("widget "+w+" doesn't match");
	contains = false;
      }
    }

    return contains;
  }
  
  private ArrayList getCorrespondingWidgets(Set tdeSet) {
    // this function takes a set of timeline data elements
    // and returns the corresponding set of widgets

    Iterator tdeIt = tdeSet.iterator();
    ArrayList widgetList = new ArrayList();

    while(tdeIt.hasNext()) {
      TimelineDataElement tde = (TimelineDataElement)tdeIt.next();
      Widget w = getWidget(tde.getStartTime(), tde.getEndTime());

      // don't add null widget!
      if (w != null) widgetList.add(w);
    }

    return widgetList;
  }

  /////////// speech related functions /////////////

  private String getTriggerWord(String speech) {
    // this function looks for important key words in the speech string
    // and returns the word it finds first -- if it doesn't find
    // a word, it just returns a null string.
    
    // right now we're just looking for the word "pendulum" but later
    // this will also look for words like "wheel"

    String[] words = new String[2];
    int length;
    
    words[0] = "pendulum";
    words[1] = "wheel";
    length = words.length;

    for (int i=0; i< length; i++) {
      if (containsWord(words[i], speech)) {
	LOG.debug("found word: "+words[i]);
	return words[i];
      }
    }
    
    return null;

  }
  
  private boolean hasTriggerWord(String speech) {
    // this function looks for important key words in the speech string
    // right now we're just looking for the word "pendulum" but later
    // this will also look for words like "wheel"

    if (getTriggerWord(speech) != null)
      return true;
    else
      return false;
  }

  private boolean containsWord(String word, String sent) {
    // returns true if sent contains word, false otherwise
							  
    int loc = sent.indexOf(word);
    if (loc != -1)
      return true;
    else
      return false;
  }

  private int containsNum(String sent) {
    // returns an int 0-5 if sent contains the numbers 0-5
    // returns -1 otherwise
    String[] nums = new String[6];
    int loc;
    int length;
    
    nums[0] = "zero";
    nums[1] = "one";
    nums[2] = "two";
    nums[3] = "three";
    nums[4] = "four";
    nums[5] = "five";

    length = nums.length;

    for (int i=0; i< length; i++) {
      loc = sent.indexOf(nums[i]);
      if (loc != -1) {
	// found a number
	return i;
      }
    }

    // didn't find any number
    return -1;
  }


  /////////////////////////////////////////////////////

  // Widget Manipulation functions

  public void makeWidgetsIdentical(GroupList gl) {

    ArrayList widgets = getCorrespondingWidgets((Set) gl);

    Iterator wIt = widgets.iterator();
    boolean poly = false; // poly flag -- this is a hack. need a better way
    
    while (wIt.hasNext()) {
      Widget w = (Widget)wIt.next();
      if (w instanceof WSimPolygon) poly = true;
      LOG.debug("widget is "+w.getEnglishName());
    }

    if (poly) {
      PolyGroup.makeIdentical(widgets);
    }
    else {
    
      wIt = widgets.iterator();
      double numWidgets = widgets.size();
      double totalW = 0.0;
      double totalH = 0.0;
      LOG.debug(numWidgets+" widgets in set");
      
      while (wIt.hasNext()) {
	Widget w1 = (Widget) wIt.next();
	Collection types = w1.isa();
	LOG.debug("types: "+types);
	Class c = null;
	try {
	  c = Class.forName( "rec.simulate.widgets.WSimObject" );
	}
	catch( ClassNotFoundException e ) {
	  LOG.error("bad unrecognized name");
	}
	if (types.contains(c)) {
	  LOG.debug("is a WSimObject");
	  WSimObject w2 = (WSimObject) w1;
	  Rectangle bounds = w2.getBounds();
	  totalW = totalW + bounds.getWidth();
	  totalH = totalH +bounds.getHeight();
	}
      }
      double wScale = totalW / numWidgets;
      double hScale = totalH / numWidgets;
      
      // now scale the shapes
      wIt = widgets.iterator();
      while (wIt.hasNext()) {
	Widget w1 = (Widget)wIt.next();
	Collection types = w1.isa();
	LOG.debug("types: "+types);
	Class c = null;
	try {
	  c = Class.forName( "rec.simulate.widgets.WSimObject" );
	}
	catch( ClassNotFoundException e ) {
	  LOG.error("bad unrecognized name");
	}
	if (types.contains(c)) {
	  LOG.debug("is a WSimObject");
	  WSimObject w2 = (WSimObject) w1;
	  Rectangle bounds = w2.getBounds();
	  // we can only scale in one direction at the moment --
	  // not sure what to do about that
	  double scaleFactor = wScale / bounds.getWidth();
	  w2.resize(scaleFactor);
	  
	  // update the surface to see the results
	  w2.getSurface().getScreen().repaint();

	}
      }
    }
  }

  private Set getWidgets(Set w) {
    Iterator wIterator = w.iterator();
    HashSet setOfWidgets = new HashSet();
    while (wIterator.hasNext()) {
      //Widget widget = (Widget)m_widgets.get(wIterator.next());
      Widget widget = getWidget((String)wIterator.next());

      // all we look at right now is WSimObjects
      Collection types = widget.isa();
      //LOG.debug("types: "+types);
      Class c = null;
      try {
	c = Class.forName( "rec.simulate.widgets.WSimBody" );
      }
      catch( ClassNotFoundException e ) {
	LOG.error("bad unrecognized name");
      }
      if (types.contains(c)) {
	setOfWidgets.add(widget);
	LOG.debug("widget is "+widget.getEnglishName());
      }
    }
    
    return setOfWidgets;
  }


  
  public void makeWidgetsEqSpaced(GroupList gl) {
    LOG.debug("equally spaced");
    
    ArrayList listOfWidgets = getCorrespondingWidgets((Set) gl);

    WidgetAligner widgetGroup = new WidgetAligner(listOfWidgets);

    int alignment = widgetGroup.getBestAlignmentDirection();
    double spacing = widgetGroup.getEqualSpacing(alignment);

    widgetGroup.alignWidgets(alignment, spacing);
  }

  public void makeWidgetsTouch(GroupList gl) {
    LOG.debug("touch");
    
    ArrayList listOfWidgets = getCorrespondingWidgets((Set) gl);

    WidgetAligner widgetGroup = new WidgetAligner(listOfWidgets);

    int alignment = widgetGroup.getBestAlignmentDirection();
    double spacing = 0.0;

    widgetGroup.alignWidgets(alignment, spacing);
  }

  public void insertSpeech() {
    // insert the string "three identical touching pendulums"
    LOG.debug("insert speech");

    String speech = "three identical touching pendulums";
    long curTime = System.currentTimeMillis();
    long startTimeGuess = curTime - (speech.length()*100);
    
    m_dwc.addSpeechEvent(startTimeGuess, curTime, speech);
  }
  
  public void insertSpeechPlus(String addText) {
    // insert "three " + addText + " pendulums" as speech
    LOG.debug("insert speech");

    String speech = "three "+addText+" pendulums";
    long curTime = System.currentTimeMillis();
    long startTimeGuess = curTime - (speech.length()*100);
    
    m_dwc.addSpeechEvent(startTimeGuess, curTime, speech);
  }
  
  public void insertWheelSpeechPlus(String addText) {
    LOG.debug("insert speech");

    long curTime = System.currentTimeMillis();
    long startTimeGuess = curTime - (addText.length()*100);
    
    m_dwc.addSpeechEvent(startTimeGuess, curTime, addText);
  }

  
}
