/*
 * Copyright (c) 1998 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * The contents of this file are subject to the Sun Community Source License,
 * Jini Software Kit, v. 1.0 DC (the "License"); you may not use this
 * file except in compliance with the License.  You may obtain a copy of
 * the License at http://java.sun.com/products/jini. Software distributed
 * under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY
 * OF ANY KIND, either express or implied.  See the License for the
 * specific language governing rights and limitations under the License.
 * 
 * CopyrightVersion 1.0_JiniDC_Public
 */
package mde.jini.service;

import java.io.*;
import java.net.*;
import java.rmi.*;
import java.rmi.server.*;
import java.util.*;
import net.jini.discovery.*;
import net.jini.core.entry.*;
import net.jini.core.event.*;
import net.jini.core.lease.*;
import net.jini.core.lookup.*;
import net.jini.lookup.entry.*;

/**
 * The PalmProxy class is a Jini proxy for the PalmPilot.  A PalmProxy object is instantiated by the PalmDaemon
 * when a socket connection is established. At that time, the PalmProxy participates in the discovery and join
 * protocol to become part of the Jini network. The PalmProxy can then perform lookups and receive service events
 * from Lego Mindstorm tank proxies.
 */  
public class PalmProxy extends Thread {
   
   private Map registrations = Collections.synchronizedMap(new HashMap());
   private ServiceRegistrar[] registrars;
   
   private BufferedReader in;
   private PrintWriter out;
   private Socket sock;
 
   private TankListener listener;
   private TankProxy tank;
   private ServiceID tankServiceID;
   
   private byte tankStatus = PalmProtocol.STATUS_BASE;
   private Byte tankStatusSync = new Byte((byte)-1);

   private static int SOCKET_TIMEOUT = 120000;  // 2 minutes

   /** Inner class for receiving and handling Jini service events */
   private class TankListener extends UnicastRemoteObject implements Serializable, RemoteEventListener, TankEventListener {

      /** Create a tank event listener */
      public TankListener() throws RemoteException {
         super();
      }

      /** RemoteEventListener interface method called by the Jini lookup service to deliver events */
      public void notify(RemoteEvent event) throws UnknownEventException {
         ServiceEvent se = (ServiceEvent)event;
         if (se.getTransition() == ServiceRegistrar.TRANSITION_MATCH_NOMATCH) {
            if (tankServiceID != null && tankServiceID.equals(se.getServiceID())) {
               clearTankStatus();
               updateTankStatus(PalmProtocol.STATUS_ERROR);
               tank = null;
            }
            cancelEventLease((ServiceRegistrar)se.getSource());
         }
      }

      /** TankEventListener interface method called by a TankProxy for notification of tank events */
      public void eventOccurred(TankEvent event) throws RemoteException {
         switch (event.getCommand()) {
            case TankCommands.HIT:
               updateTankStatus(PalmProtocol.STATUS_HIT);
               break;
            case TankCommands.OBSTRUCTED:
               updateTankStatus(PalmProtocol.STATUS_OBSTRUCTED);
               break;
         }
      }

   }
   
   /** Create a palm proxy */
   public PalmProxy(Socket sock, ServiceRegistrar[] registrars) throws SocketException, IOException {
      System.out.println("PalmProxy: accepting connection from " + sock.getInetAddress());
      this.sock = sock;
      this.registrars = registrars;
      sock.setSoTimeout(SOCKET_TIMEOUT);
      in = new BufferedReader(new InputStreamReader(sock.getInputStream()));
      out = new PrintWriter(sock.getOutputStream());
   }

   /** Start execution of the palm proxy */
   public void run() {
      try {
         while (true) {
            String request = receivePalm();
            
            if (request.equals(PalmProtocol.REGISTER)) {
               if (!palmIsRegistered()) {
                  clearTankStatus();
                  tank = findAvailableTank();
                  
                  //    This is really ugly but is done so that status can be sent back to the
                  //    PalmPilot as quickly as possible. Since registerForEvents() makes a
                  //    remote method call to a Jini service registrar, which takes time, we
                  //    send status to the Palm and assume the event registration succeeded.

                  if (tank == null) {
                     sendTankStatus();
                     continue;
                  }
                  updateTankStatus(PalmProtocol.STATUS_REGISTERED);
                  sendTankStatus();
                  
                  registerForEvents(tank);
                  continue;
               }
               sendTankStatus();
               continue;
            }
            if (request.equals(PalmProtocol.FORWARD)) {
               sendTankStatus();
               if (palmIsRegistered()) {
                  tank.sendCommand(TankCommands.FORWARD);
               }
               continue;
            }
            if (request.equals(PalmProtocol.BACKWARD)) {
               sendTankStatus();
               if (palmIsRegistered()) {
                  tank.sendCommand(TankCommands.BACKWARD);
               }
               continue;
            }
            if (request.equals(PalmProtocol.LEFT)) {
               sendTankStatus();
               if (palmIsRegistered()) {
                  tank.sendCommand(TankCommands.LEFT);
               }
               continue;
            }
            if (request.equals(PalmProtocol.RIGHT)) {
               sendTankStatus();
               if (palmIsRegistered()) {
                  tank.sendCommand(TankCommands.RIGHT);
               }
               continue;
            }
            if (request.equals(PalmProtocol.FIRE)) {
               sendTankStatus();
               if (palmIsRegistered()) {
                  tank.sendCommand(TankCommands.FIRE);
               }
               continue;
            }
            if (request.equals(PalmProtocol.STOP)) {
               sendTankStatus();
               if (palmIsRegistered()) {
                  tank.sendCommand(TankCommands.STOP);
               }
               continue;
            }
            if (request.equals(PalmProtocol.STATUS)) {
               sendTankStatus();
               continue;
            }
            if (request.equals(PalmProtocol.QUIT)) {
               sendTankStatus();
               break;
            }
            System.out.println("PalmProxy.run(): received invalid command: '" + request + "'");
            sendTankStatus();
         }

      } catch (RemoteException e) {
         System.err.println("PalmProxy.run(): RemoteException caught during tank command execution: " + e);
         updateTankStatus(PalmProtocol.STATUS_ERROR);
      } catch (IOException e) {
         //    InterruptedIOException thrown if connection lost during read operation
         System.err.println("PalmProxy.run(): connection lost to PalmPilot: " + e);
      } finally {
         System.out.println("PalmProxy.run(): shutting down connection from " + sock.getInetAddress());
         cancelEventLeases();
         try {
            sock.close();
            if (tank != null) {
               tank.removeTankEventListener(listener);
               tank.unregister();
            }
         } catch (Exception e) {}
      }
   }

   private boolean palmIsRegistered() {
      return ((tankStatus & PalmProtocol.MASK_REGISTERED) == 0) ? (false) : (true);
   }
   
   private void sendTankStatus() {
      System.out.println("PalmProxy.sendTankStatus(): sending " + Integer.toBinaryString((int)tankStatus));
      try {
         sendPalm(tankStatus);
      } catch (IOException e) {
         System.err.println("PalmProxy.sendTankStatus(): exception caught sending to Palm: " + e);
      }
      resetTankStatus();
   }

   private void updateTankStatus(byte value) {
      synchronized (tankStatusSync) {
         tankStatus |= value;
      }
   }

   private void resetTankStatus() {
      synchronized (tankStatusSync) {
         tankStatus = (palmIsRegistered()) ? (PalmProtocol.STATUS_REGISTERED) : (PalmProtocol.STATUS_BASE);
      }
   }

   private void clearTankStatus() {
      synchronized (tankStatusSync) {
         tankStatus = PalmProtocol.STATUS_BASE;
      }
   }

   private TankProxy findAvailableTank() throws RemoteException {
      
      System.out.println("PalmProxy.findAvailableTank(): " + registrars.length + " service registrars available to search");
      ServiceTemplate tmpl = tankTemplate();

      for (int i = 0; i < registrars.length; i++) {
         ServiceRegistrar registrar = registrars[i];
         ServiceMatches matches = null;
         try {
            matches = registrar.lookup(tmpl, 50);
         } catch (RemoteException e) {
            System.err.println("PalmProxy.findAvailableTank(): registrar " + registrar.getServiceID() + " threw: " + e);
            continue;
         }
         System.out.println("PalmProxy.findAvailableTank(): found " + matches.totalMatches + " matching tank(s)");
         for (int j = 0; j < matches.totalMatches; j++) {
            TankProxy tank = (TankProxy)matches.items[j].service;
            if (tank.register()) {
               System.out.println("PalmProxy.findAvailableTank(): tank " + (tank.getTankID() & 0xf) + " is ready for action!");
               tankServiceID = matches.items[j].serviceID;
               return tank;
            } else {
               System.out.println("PalmProxy.findAvailableTank(): tank " + (tank.getTankID() & 0xf) + " is already taken");
            }
         }
      }
      return null;
         
   }

   private ServiceTemplate tankTemplate() {
      Class[] classes = { null };
      try {
         classes[0] = Class.forName("mde.jini.service.TankProxy");
      } catch (ClassNotFoundException e) {
         e.printStackTrace();
      }
      return new ServiceTemplate(null, classes, null);
   }

   private boolean registerForEvents(TankProxy tank) {
      try {
         //    First, register for events from the tank proxy
         listener = new TankListener();
         tank.addTankEventListener(listener);
         
         //    Next, register for Jini service transition events
         int transitions = ServiceRegistrar.TRANSITION_MATCH_NOMATCH;
         ServiceTemplate tmpl = tankTemplate();
         
         for (int i = 0; i < registrars.length; i++) {
            ServiceRegistrar registrar = registrars[i];
            EventRegistration registration = registrar.notify(tmpl, transitions, listener, null, Long.MAX_VALUE);
            synchronized (registrations) {
               registrations.put(registrar.getServiceID(), registration);
            }
         }
         
      } catch (RemoteException e) {
         System.err.println("RemoteException caught during event registration: " + e);
         return false;
      }
      return true;
   }
   
   /** Send a response to the PalmPilot */
   public void sendPalm(byte value) throws IOException {
      synchronized (out) {
         out.write(value);
         out.write((byte)'\n');
         out.flush();
      }
   }

   private String receivePalm() throws IOException {
      synchronized (in) {
         String line = in.readLine();
         if (line == null) {
            throw new InterruptedIOException();
         }
         return line;
      }
   }

   private void cancelEventLease(ServiceRegistrar registrar) {
      ServiceID serviceID = registrar.getServiceID();
      EventRegistration registration;

      synchronized (registrations) {
         registration = (EventRegistration)registrations.remove(serviceID);
      }
      if (registration != null) {
         Lease lease = registration.getLease();
         try {
            lease.cancel();
         } catch (UnknownLeaseException e) {
         } catch (RemoteException e) {}
      }
   }

   private void cancelEventLeases() {
      for (int i = 0; i < registrars.length; i++) {
         cancelEventLease(registrars[i]);
      }
   }
   
}