package mde.jini.service;

import java.awt.Frame;
import java.awt.Point;
import java.awt.event.*;
import java.applet.Applet;
import java.io.IOException;
import java.rmi.*;
import java.rmi.activation.*;
import java.rmi.server.*;
import java.security.*;
import java.util.*;
import net.jini.*;
import net.jini.discovery.*;
import net.jini.core.entry.*;
import net.jini.core.lookup.*;
import net.jini.lookup.entry.*;
import mde.jini.visual.*;
 
/**
 * This class acts as the Jini proxy for a Lego Mindstorms tank.  It is instantiated by TankDaemonImpl
 * when a tank registers during the sign-up state. Once intstantiated, this class discovers and joins
 * the Jini network, making itself available to be commanded by either a PalmPilot or workstation client. 
 */
public class TankProxyImpl implements TankProxy {

   private List commandQueue = Collections.synchronizedList(new ArrayList());
   private Set pendingEvents = Collections.synchronizedSet(new HashSet());
   
   private EventNotifier notifyThread = null;
   private Vector listeners = new Vector();

   private boolean registered = false;
	private byte tankID;

   private long proxyMaxSilence = 1000 * 5;      // 30 seconds
   private long leaseDuration = 1000 * 30;        // 30 seconds

   private Long proxyExpirationSync = new Long(-1);
   private long proxyExpiration;

   private TankExpirer expirer = null;

   private RandomDriver driver;
   private boolean driverAlive;

   private String[] groups = LookupDiscovery.NO_GROUPS;
   private ServiceAgent agent;
   
   private Applet applet;
   private Remote remote;

   private Point[] locations = {
      new Point(50,50), new Point(575,50), new Point(50,275), new Point(575,275)
   };
   private Frame frame;
   private long lastUpdate;

   private class RandomDriver extends Thread {
	public void run() {
		byte[] cmds = {TankCommands.FORWARD, TankCommands.BACKWARD, 
			TankCommands.LEFT, TankCommands.RIGHT, 
			TankCommands.STOP, TankCommands.FIRE};

		while (driverAlive) {
			try {
				sendCommand(cmds[(int)(Math.random() * 6)]);
				sleep((int)(Math.random() * 3000));
			} catch (Exception e) {e.printStackTrace();}	
		}
	} 
   }

   private class TankExpirer extends Thread {

      public TankExpirer() {
         super("tank proxy expirer");
         setDaemon(false);
         extendExpiration();
      }

      public void run() {
         while (!isInterrupted()) {
            synchronized (proxyExpirationSync) {
               if (proxyExpiration <= System.currentTimeMillis()) {
                  System.out.println("TankProxyImpl(): tank proxy expired, tankID = " + (tankID & 0x0f));
                  //    Inform the service agent to cancel its service lease w/ all of its discovered
                  //    service registrars.  They will, in turn, send "service delete" notifications
                  //    to any matching event listeners (ie, TankDaemonImpl)
                  stopApplet();
                  agent.unregister();
                  return;
               }
            }
            try { sleep(1000); }
            catch (InterruptedException e) {}
         }
      }

      public void extendExpiration() {
         synchronized (proxyExpirationSync) {
            proxyExpiration = System.currentTimeMillis() + proxyMaxSilence;
         }
      }

   }
   
   private class EventNotifier extends Thread {

      public EventNotifier() {
         super("tank event notifier");
         setDaemon(false);
      }

      public void run() {
         while (!isInterrupted()) {
            TankEvent event = null;
            synchronized (pendingEvents) {
               if (pendingEvents.isEmpty()) {
                  notifyThread = null;
                  return;
               }
               event = (TankEvent)pendingEvents.iterator().next();
            }
            notifyListeners(event);
            synchronized (pendingEvents) {
               pendingEvents.remove(event);
            }
         }
      }

      public void notifyListeners(TankEvent event) {
         Vector l;
         synchronized (listeners) {
            l = (Vector)listeners.clone();
         }
         for (int i = l.size(); --i >= 0; ) {
            TankEventListener listener = (TankEventListener)l.elementAt(i);
            try {
               listener.eventOccurred(event);
            } catch (ConnectException e) {
               synchronized (listeners) {
                  listeners.removeElement(listener);
               }
            } catch (RemoteException e) {
               System.out.println("TankProxyImpl(): exception during event notification: " + e);
            }
         }
      }

   }
   
   /** Create a new tank proxy instance */

	public TankProxyImpl(ActivationID id, MarshalledObject data) throws IOException {
		Activatable.exportObject(this, id, 0);
		System.out.println("TankProxyImpl(): constructor called...");
	}

   /** Startup a tank proxy, causing it to discover and join the Jini network */

   public void startup(byte tankID, String[] groups) throws RemoteException {
	   this.tankID = tankID;
      this.groups = groups;

	System.out.println("TankProxyImpl.startup() called for tankID = " + (tankID & 0x0f));

   	//    Discover/join a Jini lookup service 
   	//       
   	try { remote = RemoteObject.toStub(this); }
      catch (NoSuchObjectException e) { e.printStackTrace(); }
      
      //    Start a service agent to manage our Jini registration and lease renewals
      try { agent = new ServiceAgent(groups); }
      catch (IOException e) { e.printStackTrace(); }

      String filename = "/tmp/TankProxy-" + toHex(tankID);
      ServiceID serviceID = ServiceAgent.readServiceID(filename);
      
      applet = new TankApplet((TankProxy)remote);

      Entry[] attribs = { new Name(describeTank(tankID)), (Entry)applet };
      ServiceItem item = new ServiceItem(serviceID, remote, attribs);

      agent.register(item, leaseDuration, filename);
      startApplet();

   }

   public void activateRandomDriver(boolean randomDriver) {
	if (randomDriver) {
		driverAlive = true;
		driver = new RandomDriver();
		driver.start();
	} else {
		driverAlive = false;
	}
   }

   /** Called by clients wishing to register with this tank proxy */

   public boolean register() throws RemoteException {
	   if (registered == true) {
         return false;
      }
      return(registered = true);
   }

   /** Called by clients wishing to unregister with this tank proxy */

   public void unregister() throws RemoteException {
      registered = false;
   }

   /** Called by clients to send commands to the tank daemon */

   public void sendCommand(byte command) throws RemoteException {
      System.out.println("TankProxyImpl.sendCommand(): queueing command " + toHex(command));
      try {
         switch (command) {
            case TankCommands.FORWARD:
               appendCommand(TankCommands.FORWARD);
               break;
            case TankCommands.BACKWARD:
               appendCommand(TankCommands.BACKWARD);
               break;
            case TankCommands.LEFT:
               appendCommand(TankCommands.LEFT);
               break;
            case TankCommands.RIGHT:
               appendCommand(TankCommands.RIGHT);
               break;
            case TankCommands.STOP:
               appendCommand(TankCommands.STOP);
               break;
            case TankCommands.FIRE:
               appendCommand(TankCommands.FIRE);
               break;
            default:
               throw new InvalidParameterException("Invalid tank command: " + toHex(command));
         }
      } catch (Exception e) {
         e.printStackTrace();
      }
   }

   /** Called by the tank daemon to fetch the next queued tank command to execute */

   public byte getCommand() throws RemoteException {
      byte command = getNextCommand();
      if (command != TankCommands.STATUS) {
         sendTankEvent(new TankEvent(this, tankID, command));
      }
      return command;
   }

   /** Called by the tank daemon with the latest status message received from a tank */

   public void updateStatus(TankEvent event) throws RemoteException {
      long now = System.currentTimeMillis();
      //System.out.println("TankProxyImpl(): tankID = " + (event.getTankID() & 0x0f) + ", last event " + (now-lastUpdate) + " msecs ago");
      if (expirer == null) {
         expirer = new TankExpirer();
         expirer.start();
      } else {
         expirer.extendExpiration();
      }
      sendTankEvent(event);
      lastUpdate = System.currentTimeMillis();
   }

   /** Called by clients wishing to be notified of tank events */

   public void addTankEventListener(TankEventListener l) throws RemoteException {
      System.out.println("TankProxy.addTankEventListener()...");
      synchronized (listeners) {
         listeners.addElement(l);
      }
   }

   /** Called by clients no longer wishing to receive tank events */

   public void removeTankEventListener(TankEventListener l) throws RemoteException {
      System.out.println("TankProxy.removeTankEventListener()...");
      synchronized (listeners) {
         listeners.removeElement(l);
      }
   }

   /** Returns the id of the tank being proxied */

	public byte getTankID() throws RemoteException {
		return tankID;
	}

   /** Returns the unique Jini service id for this proxy */

   public ServiceID getServiceID() throws RemoteException {
      return agent.getServiceID();
   }

   //    TankProxyImpl methods
	
   private void appendCommand(byte command) throws IOException {
      synchronized (commandQueue) {
         commandQueue.add(new Byte(command));
      }
   }

   private byte getNextCommand() {
      Byte nextCommand = null;
      synchronized (commandQueue) {
         if (commandQueue.size() == 0) {
            return TankCommands.STATUS;
         }
         nextCommand = (Byte)commandQueue.remove(0);
      }
      return nextCommand.byteValue();
   }
  
   private void startApplet() {


      frame = new Frame(describeTank(tankID));
      frame.add(applet);
      
      applet.init();
      applet.start();
      
      int tankNumber = tankID & 0x0f;
      frame.pack();

	System.out.println("TankProxyImpl().startApplet() called for tankID = " + tankNumber);
      
      if (tankNumber >= 0 && tankNumber <= 3) {
         frame.setLocation(locations[tankNumber]);
      }
      frame.setVisible(true);
      
   }

   private void stopApplet() {

	System.out.println("TankProxyImpl().stopApplet() called for tankID = " + (tankID & 0x0f));

      applet.stop();
      applet.destroy();
      frame.dispose();
   }
   
   private void sendTankEvent(TankEvent event) {
      synchronized (pendingEvents) {
         pendingEvents.add(event);
      }
      if (notifyThread == null) {
         notifyThread = new EventNotifier();
         notifyThread.start();
      }
   }
   
   /** Called to shutdown the proxy */

   public void shutdown() throws RemoteException {
      System.out.println("TankProxyImpl.shutdown()");
      driverAlive = false;
   }
   
	private String toHex(byte x) {
		return ((x & 0xff) < 16 ? "0" : "") + Integer.toHexString(x & 0xff).toUpperCase();
	}

   private String describeTank(int tankID) {
      String tankColor = TankColors.getTankColorName((byte)tankID);
      switch (tankID & 0x0f) {
      case 0:
         return "Lego Mindstorms Tank #1 (" + tankColor + ")";
      case 1:
         return "Lego Mindstorms Tank #2 (" + tankColor + ")";
      case 2:
         return "Lego Mindstorms Tank #3 (" + tankColor + ")";
      case 3:
         return "Lego Mindstorms Tank #4 (" + tankColor + ")";
      default:
         return "Lego Mindstorms Tank";
      }
   }

}