/* Copyright (c) 2008 Charles Rich and Worcester Polytechnic Institute.
 * All Rights Reserved.  Use is subject to license terms.  See the file
 * "license.terms" for information on usage and redistribution of this
 * file and for a DISCLAIMER OF ALL WARRANTIES.
 */
package edu.wpi.cetask.guide;

import java.beans.Statement;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.text.DateFormat;
import java.util.*;

import javax.script.*;

import edu.wpi.cetask.*;

// TODO add stack to handle interruptions

public class Guide {
   
	public static String VERSION = "0.8.4w beta";
	private static final int INPUT_POLLING_INTERVAL = 200;

   public static void main (String[] args) {
      // for convenience to start both Guide and SpeechGuide from same jar file
      if ( args.length > 0 && args[0].equals("-speech") )
         SpeechGuide.main(args.length > 1 ? new String[]{args[1]} : new String[0]);
      else if ( args.length > 0 && args[0].equals("-gui") )
    	  if (args.length > 2)CustomGuide.main(new String[]{args[1], args[2]});
    	  else CustomGuide.main(args.length > 1 ? new String[]{args[1]} : new String[0]);
      else new Guide(args.length > 0 ? args[0] : null).loop();
   }
   
   public static Guide guide;
   protected final TaskEngine engine = new TaskEngine();

   private final PrintStream out = System.out, err = System.err; // to restore                             
   private final File log;
   private PrintStream logStream;
   
   protected ArrayList<String> inputQueue;
   protected ArrayList<ITaskUI> uiList;

   private TextUI textUI;
   protected boolean doProposal = true;
   
   protected Guide (String filename) {
	   Guide.guide = this;
	   
	   log = new File( 
			   (filename == null ? System.getProperty("java.io.tmpdir") + "/console" : filename) 
			   + ".guide");

	  inputQueue = new ArrayList<String>();
      uiList = new ArrayList<ITaskUI>();
      
      textUI = new TextUI(filename);
      uiList.add(textUI);
      
   }

   /**
    * Command loop. Uses reflection to invoke commands; see methods with command
    * names.
    */
   protected void loop () {
      String line;
      try {
    	 logStream = new PrintStream(new BufferedOutputStream(
                new FileOutputStream(log)), true);
         println("# Writing console log to " +log+ "...");
         //} else { System.setOut(logStream); System.setErr(logStream); }
         long now = System.currentTimeMillis();
         engine.setStart(now);
         println("# "
               +DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
                  .format(new Date(now))
               +" - Version "+VERSION);
         respond("@welcome");

         // tell all UIs they can prompt
    	 java.util.Iterator<ITaskUI> iter = uiList.iterator();
    	 for (ITaskUI ui; iter.hasNext();) {
    		 ui = iter.next();
    		 ui.prompt();
			}
         while (true) { 
         	line = null;
            // read from input queue
            synchronized (this) {
            	if (inputQueue.size() > 0) {
            		line = inputQueue.remove(0);
            		System.out.println("   " + line);
            	}
            }
            // if line != null
            if (line != null) {
            	processCommand(line);
           	 // tell all UIs they can prompt
            	if (inputQueue.size() == 0) {
            		iter = uiList.iterator();
            		for (ITaskUI ui; iter.hasNext();) {
            			ui = iter.next();
            			ui.prompt();
            		}
            	}
            } else {
            	synchronized (this) {
            		wait(INPUT_POLLING_INTERVAL);
            	}
            }
         }
      } 
      catch (Throwable e) { 
         if (! ( e instanceof Quit ) ) exception(e); 
      } finally { 
    	 System.setOut(out); System.setErr(err);
         if ( logStream != null) logStream.close(); 
         
         java.util.Iterator<ITaskUI> iter = uiList.iterator();
  	   	for (ITaskUI ui; iter.hasNext();) {
  	   		ui = iter.next();
  	   		ui.deallocate();
  	   	}
      }
   }
   
   protected static class Quit extends Throwable {};
   
   // probably not needed anymore
   //protected void deallocate () {} // for SpeechGuide

   protected void processCommand (String line) throws Throwable {
      StringTokenizer tokenizer = new StringTokenizer(line);
      if ( tokenizer.hasMoreTokens() ) {
         command = tokenizer.nextToken().toLowerCase();
         if ( command.equals("quit") ) {
            respond("@goodbye");
            throw new Quit();
         }
         try {
            // before next user turn
            engine.decomposeAll(); 
            // dispatch to command method
            new Statement(this, command, 
                  new Object[] { line.substring(command.length()).trim() }) 
            .execute();
         } catch (NoSuchMethodException m) {
            respond("@invalid");
         } catch (InvocationTargetException t) {
            throw t.getTargetException();
         } catch (Exception e) { exception(e); }
         system(); // system turn
      }
   }
   
   public static String PROMPT = "CE> ";
      
   private void copyOutput (PrintStream logStream) {
      System.setOut(new PrintStream(new CopyOutputStream(out, logStream), true));
      System.setErr(new PrintStream(new CopyOutputStream(err, logStream), true));
   }
   
   public void log(String text) {
	   logStream.println(text);
   }
   
   // see default.properties for system conversational utterances
   // TODO internationalize using resource bundles
   
   private void respondq (String key, TaskClass type, String name) {
      String string;
      if ( engine.DEBUG )
         string = engine.getProperty("@definite")+' '+type+' '+name;
      else {
         String property = type.getProperty("."+name+"@definite");
         string = property != null ? property : 
            engine.getProperty("@definite")+' '
            +Utils.decapitalize(type.getSlotType(name)); 
      }
      println(engine.getProperty(key)+' '+string+'?');
   }
   
   private void respond (String key, Plan plan) {
      respond(key, plan.getGoal());
   }

   private void respond (String key, Task task) {
      respond(key, task, false);
   }

   private void respondq (String key, Task task) {
      respond(key, task, true);
   }
   
   private void respond (String key, Task task, boolean question) {
      respond(key, engine.DEBUG ? task : task.format(), question);
   }
   
   private void respond (String key, Object object, boolean question) {
      println(engine.getProperty(key)+' '+object+(question ? '?' : '.'));
   }
   
   protected void respond (String key) {
      println(engine.getProperty(key));
   }

   protected void println (Object object) {
	   java.util.Iterator<ITaskUI> iter = uiList.iterator();
	   for (ITaskUI ui; iter.hasNext();) {
		   ui = iter.next();
		   ui.println(object);
	   }
   }
   
   protected static void warning (String text) {
      System.err.println("WARNING: "+text);
      guide.log("WARNING: "+text);
   }
   
   protected static void exception (Throwable e) {
      if ( TaskEngine.DEBUG ) throw new RuntimeException(e);
      else System.out.println(e); 
   }
 
   // trivial discourse state
   
   protected Plan proposed, accepted, rejected;
   
   protected String command;

   /**
    * Method for system turn.  Called once each time around command loop.
    * Intended extension point.
    */
   protected void system () {
      if ( !status.contains(command) ) {
         if ( accepted != null && isSystem(accepted) ) {
            accepted.execute();
            // note some outputs may be undefined (especially if task failed)
            if ( accepted.isDone() ) respond("@done", accepted.getGoal());
            accepted = null;
            completed();
            updateFocus();
         } else if (doProposal) { 
        	 if ( proposed == null ) { // unless outstanding proposal
        		 for (Plan next : getLive()) {
        			 if ( next != rejected && isSystem(next) ) {
        				 respondq("@shall", next.getGoal());
        				 propose(next);
        				 break;
        			 }
        		 }
        	 }
         }
      }
   }

   private final List<String> status = Arrays.asList(new String[] { 
      "load", "status", "eval", "source", "verbose", "debug", "help" });

   private boolean completed () {
      Plan focus = engine.getFocus();
      Plan top;
      if ( focus != null ) {
         if ( focus.isDone() && !focus.occurred() ) {
            respond("@completed", focus);
            return true;
         } else if ( focus.isFailed() ) {
            respond("@failed", focus);
            return true;
         } else if ( (top = engine.getTop(focus)).isDone() ) {
            // special case for completed toplevel plan
            respond("@completed", top);
            return true;
         }
      }
      return false;
   }
   
   private void updateFocus () {
      Plan focus = engine.getFocus();
      if ( focus != null && !focus.isLive() ) 
         setFocus(engine.newFocus(focus));
   }

   private List<Plan> getLive () {
      Plan focus = engine.getFocus();
      if ( focus == null ) return Collections.emptyList();
      List<Plan> live = focus.getLiveDescendants();
      if ( focus.isLive() ) live.add(focus);
      if ( live.isEmpty() ) live = engine.getTop(focus).getLive();
      return live;
   }
  
   private static boolean isSystem (Plan plan) {
      Task task = plan.getGoal();
      // TODO preference for external false (over null)?
      return plan.isLive() && !Utils.isTrue(task.getExternal())
         // note deciding here that decompositions take precedence
         && task.getType().isPrimitive() && task.getScript() != null 
         // TODO ask for input values if needed
         // user can provide inputs using 'task' command
         && task.isDefinedInputs(); 
   }
   
   protected void propose (Plan plan) {
      proposed = plan;
      if ( plan != null ) setFocus(plan);
      accepted = null;
      rejected = null;
   }
           
   // commands 
   
   public void help (String ignore) {
      help();
      System.out.println("    help                - show this message");
      System.out.println("    (See edu.wpi.cetask.guide.Guide javadoc for more information)");
   }
   
   protected void help () {
      System.out.println("    load <uri>          - load xml for task model");
      System.out.println("    task <id> [<namespace>] [ / <value> ]*");
      System.out.println("                        - set current task");
      System.out.println("                          (namespace defaults to last loaded)");
      System.out.println("                          (slot values optional)");
      System.out.println("    next                - tell me what to do next");
      System.out.println("    by <id>             - select decomposition class for current task");
      System.out.println("    done [<id> [<namespace>]] [ / <value> ]*");
      System.out.println("                        - I have performed this task");
      System.out.println("                          (defaults to suggested next task)");
      System.out.println("                          (all slot values required)");
      System.out.println("    execute [<id> [<namespace>]] [ / <value> ]*");
      System.out.println("                        - Like 'done', except runs script if any");
      System.out.println("    no                  - I don't want to do suggested next task");
      System.out.println("    yes                 - answer to yes/no question");
      System.out.println("    status              - print out current task tree");
      System.out.println("    clear               - delete all current tasks");
      System.out.println("    eval <javascript>   - evaluate inline JavaScript");
      System.out.println("    source <filename>   - read command input from file");
      System.out.println("    quit                - quit program");
      System.out.println("    verbose [<boolean>] - turn verbose output on/off (default true)");
      System.out.println("    debug [<boolean>]   - turn debugging output on/off (default true)");
   }
   
   /**
    * Load, parse and validate specified task model.
    * 
    * @param uri for xml file containing task model description
    */
   public void load (String uri) {
      // verify model against schema
	   try {
		   uri = Utils.toURL(uri).toString();
		   } catch (Exception e) {
			   System.out.println("Exception on URI: " + e.toString());
		   }

      if ( Utils.validate(uri) ) {
         TaskModel model = engine.parse(uri);
         if ( TaskEngine.VERBOSE ) 
            println("# Parsed and validated "+model);
      }
   }
   
   /**
    * Set current task focus. Namespace defaults to last loaded. JavaScript to
    * compute all <em>declared</em> input and output slot values is optional
    * (in order declared). Skipped slots can be specified by '/ /'.<br>
    * <br>
    * Hint: If Javascript contains '/', use 'eval' command to set temporary
    * variable and use variable in 'done'.
    * 
    * @param args &lt;id&gt [&lt;namespace&gt] [ / &lt;value&gt ]*
    */
   public void task (String args) {
      Task task = processTask(args, proposed, false);
      propose(null); // clear proposals, if any
      if ( task != null ) {
         List<Plan> matches = engine.findMatches(task, true);
         Plan focus = null;
         if ( matches.isEmpty() ) {
            engine.addTop(focus = new Plan(task));
            respond("@top", task);
         } else {
            focus = ambiguity(matches);
            focus.recognize(task);
            //respond("@ok");
            respond("@task", task);
         }
         // always change focus
         setFocus(focus);
      }
   } 
   
   private Plan ambiguity (List<Plan> matches) {
      if ( matches.size() > 1 ) {
         String name = null;
         for (Plan plan : matches) {
            Task task = task = plan.getGoal();
            // ignore ambiguity among repeatable steps
            if ( task instanceof Decomposition.Step ) {
               Decomposition.Step step = (Decomposition.Step) task; 
               if ( name == null ) name = step.getName();
               else if ( !step.getName().equals(name) ) {
                  ambiguous(matches);
                  break;
               }
            } else {
               ambiguous(matches);
               break;
            }
         }
      }
      return matches.get(0);
   }
   
   private void ambiguous (List<Plan> matches) {
      warning("Ignoring ambiguity of "+matches.size());
      if ( TaskEngine.DEBUG ) System.err.println(matches);      
   }
   
   private void setFocus (Plan focus) {
      Plan old = engine.getFocus();
      if ( engine.setFocus(focus) && !old.isFailed() ) 
         // unexpected focus shift
         respond("@shift", old);
   }
   
   /**
    * Select decomposition class for current focus task.
    * 
    * @param args &lt;id&gt
    */
   public void by (String args) {
      Plan focus = engine.getFocus();
      if ( focus != null ) {
        Task goal = focus.getGoal();
        DecompositionClass decomp = goal.getType().getDecomposition(args.trim());
        if ( decomp != null && !Utils.isFalse(decomp.isApplicable(goal)) ) {
           focus.apply(decomp);
           return;
        }
      }
      warning("No such decomposition for current focus.");
   }
   
   /**
    * Report user performance of primitive task. Task class defaults to
    * suggested next task. JavaScript to compute all <em>declared</em> input
    * and output slot values is required (in order declared). Success slot value is
    * optional (and last). Skipped slots can be specified by '/ /'.<br>
    * <br>
    * Hint: If Javascript contains '/', use 'eval' command to set temporary
    * variable and use variable in 'done'.
    * 
    * @param args [&lt;id&gt [&lt;namespace&gt]] [ / &lt;value&gt ]*
    */
   public void done (String args) {
      Task occurrence = processTaskIf(args, proposed, true);
      if ( occurrence != null ) done(occurrence); 
   }
   
   private void done (Task occurrence) {
      if ( proposed != null ) {
         Task task = proposed.getGoal();
         if ( task.matches(occurrence) )
            task.copySlotValues(occurrence);
      }
      // leave "success" undefined (see doc for Task.isDefinedOutputs)
      occurrence.setExternal(true); 
      occurrence.setWhen(System.currentTimeMillis());
      if ( occurrence.isDefinedInputs() ) {
         List<Plan> matches = engine.findMatches(occurrence, true);
         if ( matches.isEmpty() ) {
            respond("@unknown");
            Plan plan = new Plan(occurrence),
            focus = engine.getFocus();
            if ( focus == null ) engine.addTop(plan);
            else focus.add(plan);
         } else {
            Plan match = ambiguity(matches);
            match.recognize(occurrence);
            if ( !completed() ) respond("@ok");
            setFocus(match);
         }
         updateFocus();
      } else warning("All input values must be defined--ignored.");
      propose(null); // clear proposals, if any
   }
 
   /**
    * Like 'done', but first executes script associated with primitive task, if
    * any. Convenient for running simulations.
    * 
    * @see #done(String)
    */
   public void execute (String args) {
      Task occurrence = processTaskIf(args, proposed, true);
      if ( occurrence != null ) {
         if ( occurrence.isDefinedInputs() ) {
            Script script = occurrence.getScript();
            if ( script != null) script.eval(occurrence);
            done(occurrence);
         } else warning("All input values must be defined--ignored.");
      }
   }
   
   private Task processTaskIf (String args, Plan plan, boolean success) {
      Task occurrence = processTask(args, proposed, true);
      if ( occurrence == null ) 
         warning("Missing task argument (no task proposed).");
      return occurrence;
   }
   
   private Task processTask (String args, Plan plan, boolean success) {
      TaskClass type = plan == null ? null : plan.getGoal().getType(); 
      StringTokenizer tokenizer = new StringTokenizer(args, "/");
      if ( tokenizer.hasMoreTokens() && !args.startsWith("/") ) {
         String namespace = TaskEngine.getNamespace();
         StringTokenizer name = new StringTokenizer(tokenizer.nextToken());
         if ( name.hasMoreTokens() ) {
            String id = name.nextToken();
            if ( name.hasMoreTokens() ) namespace = name.nextToken();
            if ( name.hasMoreTokens() ) 
               warning("Ignoring '"+name.nextToken()+"' (and following)");
            type = engine.getModel(namespace).getTaskClass(id);
         }
      }
      if ( type == null ) return null;
      // TODO: why are we creating a new instance of this instead of using an instance that may already exist
      Task task = type.newInstance();

      // Added by willie: reuse proposed plan, trying to keep slot values
      // TODO: perhaps make this more complete by not only matching against the goal of the plan but any task in the plan
      /* if (plan != null) {
	      if (plan.getGoal().matches(task)) {
	    	  //task = plan.getGoal();
	    	  plan.getGoal().copySlotValues(task);
	      }
      } else {
    	  plan = engine.getFocus();
    	  if (plan != null) {
    	      if (plan.getGoal().matches(task)) {
    	    	  //task = plan.getGoal();
    	    	  plan.getGoal().copySlotValues(task);
    	      }
          }
      } */
      
      // process input, output and success slot values, if any
      for (String name : type.getDeclaredInputNames()) 
         if ( !nextArg(tokenizer, task, name) ) return task;
      for (String name : type.getDeclaredOutputNames()) 
         if ( !nextArg(tokenizer, task, name) ) return task;
      if ( success ) nextArg(tokenizer, task, "success");
      if ( tokenizer.hasMoreTokens() ) 
         warning("Ignoring '"+tokenizer.nextToken()+"' (and following)");
      return task;
   }

   private static boolean nextArg (StringTokenizer tokenizer, 
                                   Task task, String name) {
      if ( tokenizer.hasMoreTokens() ) {
         String next = tokenizer.nextToken().trim();
         if ( next.length() > 0 )
            task.setSlotValueScript(name, next, "Guide");
         return true;
      } else return false;
   }
   
   /**
    * Advise the user what needs to be done next.
    */
   public void next (String ignore) {
      completed();
      List<Plan> live = getLive();
      // remove rejected options
      for (Iterator<Plan> i = live.iterator(); i.hasNext();) 
         if ( i.next() == rejected ) i.remove();
      // first preference is for system to do tasks for which it has all info
      for (Plan next : live) 
         if ( isSystem(next) ) {
            propose(null); // clear proposals, if any
            return;
         }
      // next preference is live user tasks
      for (Plan next : live) {
         Task task = next.getGoal();
         if ( task.getType().isPrimitive() && !Utils.isFalse(task.getExternal()) ) {
            respond((next.isOptional() ? "@optional" : "@please"), task);
            propose(next);
            return;
         }
      }
      // next preference is to ask for unbound input
      for (Plan next : live) {
         Task task = next.getGoal();
         TaskClass type = task.getType();
         for (String name : type.getDeclaredInputNames())
               if ( !task.isDefinedSlot(name) ) {
                  propose(next);
                  respondq("@what", type, name);
                  return;
               }
      }
      // final preference is to choose decomposition
      for (Plan next : live ) {
         if ( !next.isDecomposed() ) {
            Task task = next.getGoal();
            List<DecompositionClass> decomps = task.getDecompositions();
            if ( !decomps.isEmpty() ) {
               propose(next);
               // if only one applicable choice, make it without asking user
               if ( decomps.size() == 1 ) {
                  next.apply(decomps.get(0));
                  next(null); // note recursive call to next method
               } else respondq("@how", task);
               return;
            }
         }
      }
      // otherwise just reiterate current focus, if any
      Plan focus = engine.getFocus();
      if ( focus != null && focus.hasLiveDescendants() ) {
         respond("@lets", focus);
         propose(focus);
      } else {
         respond(focus == null ? "@nofocus" : "@nonext");
         propose(null); // clear proposals, if any
      }
   }

   /**
    * Accept system proposals.
    */
   public void yes (String ignore) {
      if ( proposed != null ) {
         accepted = proposed;
         proposed = null;
         rejected = null;
         respond("@ok");
      }
   }

   /**
    * Reject system proposals.
    */
   public void no (String ignore) {
      if ( proposed != null ) {
         rejected = proposed;
         proposed = null;
         accepted = null;
         respond("@ok");
         setFocus(engine.newFocus(rejected));
      }
   }
   
   /**
    * Evaluate given JavaScript code and report return value.
    */
   public void eval (String script) throws ScriptException {
      println("# Returned '"+engine.evalGlobal(script, "Guide")+"'");
   }
      
   private Plan lastFocus;
   private final String note = "<-focus";
  
   /**
    * Print out current task decomposition tree with status information.
    */
   public void status (String ignore) {
      if ( engine.getFocus() != lastFocus ) {
         // manage focus note
         if ( lastFocus != null ) lastFocus.getNotes().remove(note);
         lastFocus = engine.getFocus();
         if ( lastFocus != null ) lastFocus.getNotes().add(note);
      }
      Iterator<Plan> tops = engine.tops();
      if ( tops.hasNext() ) {
         println("");
         while (tops.hasNext())
            println(tops.next().printToString());
         System.out.println();
      } else respond("@empty");
   }
   
   /**
    * Reinitialize current task state (but keeps loaded models).
    */
   public void clear (String ignore) {
      engine.clear();
   }
   
   /**
    * Start reading user input from given file.
    */
   public void source (String filename) {
	   if (textUI != null) textUI.source(filename);
	   else println("No file reader loaded");
   }
   
   /**
    * Control verbose output.
    * 
    * @param state - "true" or "false"
    */
   public void verbose (String state) {
      TaskEngine.VERBOSE = state.length() > 0 ? Boolean.parseBoolean(state) : true;
   }
   
   /**
    * Control debugging output.
    * 
    * @param state - "true" or "false"
    */
   public void debug (String state) {
      TaskEngine.DEBUG = state.length() > 0 ? Boolean.parseBoolean(state) : true;
   }
 
   public void addInput(String newCommand) {
	   synchronized (this) {
		   inputQueue.add(newCommand);
	   }
   }
   
   // for logging
   private static class CopyOutputStream extends FilterOutputStream {
      private final OutputStream copy;
      public CopyOutputStream (OutputStream stream, OutputStream copy) {
         super(stream); this.copy = copy;
      }
      @Override
      public void write (int oneByte) throws IOException {
         out.write(oneByte); copy.write(oneByte);
      }
      @Override
      public void write (byte[] buffer) throws IOException {
         out.write(buffer); copy.write(buffer);
      }
      @Override
      public void write (byte[] buffer, int offset, int count) throws IOException {
         out.write(buffer, offset, count); copy.write(buffer, offset, count);
      }
      @Override
      public void flush () throws IOException {
         out.flush(); copy.flush();
      }
      @Override
      public void close () throws IOException {
         copy.close(); super.close();
      }
   }
}

