import java.rmi.*;
import java.io.*;
import java.net.*;
import java.util.*;

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

class SncFailure extends Error {
  public SncFailure(String msg) {
    super(msg);
  }
}

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

class Snc {
  private static FileSystem aFS;
  private static FileSystem bFS;

  private File a_cacheFile;
  private File b_cacheFile;

  private static String arootpath, brootpath;
  private static Hashtable FileSysTabA;
  private static Hashtable FileSysTabB;
  private static Hashtable DirtyTabA,DirtyTabB;
  
  private static String aPrefix = "";
  private static String bPrefix = "";


  // should get rid of this!!!
  private static String aName(NetFile a) {
    String name;
    try { name = a.getPrintName(); }
    catch (Exception e) { name = "<getName raised exception>"; }
    return aPrefix + name; 
  }
  private static String bName(NetFile b) {
    String name;
    try { name = b.getPrintName(); }
    catch (Exception e) { name = "<getName raised exception>"; }
    return bPrefix + name; 
  }
  

  private static void markSynchro(Hashtable fs, String path, 
			   FileInfo fileinfo, int dirid, 
			   long timesnc) {
    File f = new File(path);


    try {
      if (f.isDirectory()) {
	Trace.debugmore("Removing cached lastSnc info for children of " + path);
	fileinfo.setdirIds(dirid);
	String[] children = fileinfo.getChildrens();
	for (int i=0; i<children.length; i++) {
	  String child = path + File.separator + children[i];
	  if (fs.containsKey(child)) {
	    Trace.debugmore("Removing cached lastSnc info for " + child);
	    fs.remove(child); }}
      }
      fileinfo.setsncTimes(timesnc);}
    
    catch (RemoteException e) {
      Trace.always("Remote exception:\n" + e.getMessage()); }
  }
   
  private static void markEverythingSynchro(Hashtable fs, String path,
				     FileInfo finfo, int id, long timesnc){
    Trace.debug("markEverythingSynchronized: " + path);
    File f = new File(path);
   try {
    if (f.isDirectory()) {
      String[] children = finfo.getChildrens(); 
      for (int i=0; i<children.length; i++) {
        markEverythingSynchro(fs,path + File.separator + children[i],finfo,
			      id,timesnc); }}
    markSynchro(fs, path, finfo, id, timesnc);}
   catch (RemoteException e) {
     Trace.always("Remote exception:\n" + e.getMessage()); }
  }




  ///////////// 
  // The snc procedure takes as arguments two NetFiles, a and b, to be
  // synchronized and a Action that controls what happens when
  // discrepancies are found between the two.  It returns a boolean,
  // indicating whether it was successful in making a and b coincide.

  private static boolean snc(final String filePath, 
                             final Action action) {

    int setdirID = 0;
    long setTimesnc = 0;

    try {
      if (FileSysTabA.get(aFS.fullName(filePath))==null) 
	{ throw new SncFailure("snc: called with a==null"); }
    } catch (RemoteException re) {}
    if (FileSysTabB==null) 
      { throw new SncFailure("snc: called with b==null"); }

    try {
      boolean success;
      //      boolean aDirty = aFS.dirty(a.getPath());
      DirtyInfo dinfoA = (DirtyInfo) DirtyTabA.get(filePath);
      DirtyInfo dinfoB = (DirtyInfo) DirtyTabB.get(filePath);
      boolean aDirty = dinfoA.dirty;
      boolean aOld =  dinfoA.hasDirtyDescendant;
      
      boolean bDirty = dinfoB.dirty;
      boolean bOld = dinfoB.hasDirtyDescendant;
      
      String afullpath = arootpath + File.separator + filePath;
      String bfullpath = brootpath +  File.separator + filePath;
      FileInfo afinfo = (FileInfo)(FileSysTabA.get(arootpath + 
					 File.separator + filePath));
      FileInfo bfinfo = (FileInfo)(FileSysTabB.get(brootpath + 
					 File.separator + filePath));

      String atobMsg = (!(afinfo==null)) ? 
	("delete " + filePath) : ("copy " + filePath + " from AFS to BFS" ); 
      String btoaMsg = (!(bfinfo==null))
	? ("delete " + filePath) : ("copy " + filePath + " form BFS to AFS"); 

      String aMsg = (aDirty ? ((afinfo!=null) ? "is dirty" : "deleted") : 
                     (aOld ? "is old" : "contains a dirty file"));
      String bMsg = (bDirty ? ((bfinfo!=null) ? "is dirty" : "deleted") : 
                     (bOld ? "is old" : "contains a dirty file"));

      Trace.trace("snc: " + "a: " + filePath + " " + aMsg + ", "
                  + "b: " + filePath + " " + bMsg);
	
      if (aOld && bOld) {
        Trace.debug("Nothing to do");
        success = true;
        }

      else if ((!afinfo.isFile()) && (!bfinfo.isFile())) { 
        Trace.debug("Proceeding recursively");
        Set fileSuccess = new Set();
        success = true;
        Set names = new Set(afinfo.getChildrens());
        names.union(new Set(bfinfo.getChildrens()));
        Enumeration e = names.elements();
        while (e.hasMoreElements()) {
          String name = (String) (e.nextElement());
          boolean s = snc(filePath + File.separator + name, action);
          if (s) { fileSuccess.put(name); }
          success = success && s; 
	}
      }

      else if (aOld) {
	Signature asig = new Signature(new File(afullpath));
	if (asig==null) { 
	  throw new RemoteException("Snc->signatures : null sig"); }
	Signature bsig = new Signature(new File(bfullpath));
	if (bsig==null) { 
	  throw new RemoteException("Snc->signatures : null sig"); }
        if (afinfo.isFile() && bfinfo.isFile() && asig.equals(bsig)) {
          Trace.debug("Files coincide");
          success = true; }
        else {
          success = action.go
            (filePath + " " + bMsg,
             atobMsg,
             new BThunk() {public boolean go() throws RemoteException {
	       // at this point we could fork a thread that will take care 
	       // of copying the file to b;
	       NetFile bnetfile = new NetFileImpl(bFS,filePath);
	       NetFile anetfile = new NetFileImpl(aFS,filePath);
               bnetfile.copyFrom(anetfile); 
	       return true; }},
	     btoaMsg,
             new BThunk() {public boolean go() throws RemoteException {
	       NetFile bnetfile = new NetFileImpl(bFS,filePath);
	       NetFile anetfile = new NetFileImpl(aFS,filePath);
               anetfile.copyFrom(bnetfile); 
	       return true; }},
	     Action.btoa); 

	  if (success) {
	    setdirID = bfinfo.getdirIds();       
	    setTimesnc = bfinfo.getsncTime();
	  }
	  
	}
      }

      else if (bOld) {
	Signature asig = new Signature(new File(afullpath));
	if (asig==null) { 
	  throw new RemoteException("Snc->signatures : null sig"); }
	Signature bsig = new Signature(new File(bfullpath));
	if (bsig==null) { 
	  throw new RemoteException("Snc->signatures : null sig"); }
        if (afinfo.isFile() && bfinfo.isFile() && asig.equals(bsig)) {
          Trace.debug("Files coincide");
          success = true; }
        else {
          success = action.go
            (filePath + " " + aMsg,
             atobMsg,
             new BThunk() {public boolean go() throws RemoteException {
	       NetFile bnetfile = new NetFileImpl(bFS,filePath);
	       NetFile anetfile = new NetFileImpl(aFS,filePath);
               bnetfile.copyFrom(anetfile); return true; }},
             btoaMsg,
             new BThunk() {public boolean go() throws RemoteException {
	       NetFile bnetfile = new NetFileImpl(bFS,filePath);
	       NetFile anetfile = new NetFileImpl(aFS,filePath);
               anetfile.copyFrom(bnetfile); return true; }},
             Action.atob
              ); 
	  
	  if (success) {
	    setdirID = afinfo.getdirIds();       
	    setTimesnc = afinfo.getsncTime();
	  }
	}
      }

      else {
	Signature asig = new Signature(new File(afullpath));
	if (asig==null) { 
	  throw new RemoteException("Snc->signatures : null sig"); }
	Signature bsig = new Signature(new File(bfullpath));
	if (bsig==null) { 
	  throw new RemoteException("Snc->signatures : null sig"); }
        if (afinfo.isFile() && bfinfo.isFile() && asig.equals(bsig)) {
          Trace.debug("Files coincide");
          success = true; }
        else {
          String msg;
          if (!(afinfo==null)) {
            msg = afullpath + " missing; " + bfullpath+ " updated"; }
          else if (!(bfinfo==null)) {
            msg = afullpath + " updated; " + bfullpath + " missing"; }
          else if (afinfo.isFile() && (!bfinfo.isFile())) {
            msg = afullpath + " is a file; " + bfullpath + " is a directory"; }
          else if ((!afinfo.isFile()) && bfinfo.isFile()) {
            msg = afullpath + " is a directory; " + bfullpath + " is a file"; }
          else if (afinfo.isFile() && bfinfo.isFile()) {
            msg = "files " + afullpath + " and " + bfullpath + 
                  " are inconsistent"; }
          else {
            Trace.debug("Should never reach here!!");
            msg = "directories " + afullpath + " and " + bfullpath + 
                  " were both updated"; }

          success = action.go
            (msg,
             atobMsg,
             new BThunk() {public boolean go() throws RemoteException {
	       NetFile bnetfile = new NetFileImpl(bFS,filePath);
	       NetFile anetfile = new NetFileImpl(aFS,filePath);
               bnetfile.copyFrom(anetfile); return true; }},
             btoaMsg,
             new BThunk() {public boolean go() throws RemoteException {
	       NetFile bnetfile = new NetFileImpl(bFS,filePath);
	       NetFile anetfile = new NetFileImpl(aFS,filePath);
               anetfile.copyFrom(bnetfile); return true; }},
             Action.nodefault); 
	  if(success && (action.getName() == "PreferA")) {
	    setdirID = afinfo.getdirIds();       
	    setTimesnc = afinfo.getsncTime();
	  }
	  if(success && (action.getName() == "PreferB")) {
	    setdirID = bfinfo.getdirIds();       
	    setTimesnc = bfinfo.getsncTime();
	  }
	  }
      }

      Trace.debug("snc: " + (success ? "succeeded" : "failed") + " for " +
            afullpath + " and " + bfullpath); 
      if (success) { 
        markSynchro(FileSysTabA,afullpath,afinfo,setdirID,setTimesnc); 
        markSynchro(FileSysTabB,bfullpath,bfinfo,setdirID,setTimesnc);

	/// have to send the cacheFile back to b!!!

      }
      return success; }

    catch (RemoteException re) {
      re.printStackTrace();
      throw new SncFailure("snc: failed with RemoteException:\n " +
                           re.getMessage());
      
    }
  }

  /*
   private static void sncTopLevel(NetFile a, NetFile b, Action action) 
   throws RemoteException
   {
     if (a==null || b==null) {
       throw new RemoteException("sncTopLevel: null argument"); }
     if (a.exists() || b.exists()) {
       boolean success = snc(a, b, action); }
     else {
       throw new RemoteException("Neither " + a.getPrintName() + 
                                 " nor " + b.getPrintName() + " exists!"); }
   }
  */


  private static void showUsage(String reason) {
    if (reason != "") { Trace.always(reason); Trace.always(""); }
    Trace.always
      ("Usage: Snc [<option>...] local path [path...]\n" +
       "    or Snc [<option>...] hostname path [path...]\n" +
       "where each <option> is one of the following:\n" +
       "   -trace N                set verbosity " +
                                   "(0 silent, 1 default, 4 max)\n" +
       "   -log                    trace all RMI messages\n" +
       "   -paranoid               verify all file copy operations\n" +
       "   -init                   mark as already in-sync (use carefully!)\n"+
       "   -clearCache             re-initialize cached information\n"+
       "   -localRoot path         use $HOME/path as root of " + 
                                   "local filesystem\n" +
       "   -remoteRoot path        use $HOME/path as root of " + 
                                   "remote filesystem\n" +
       "   -hardDelete             really delete files " + 
                                   "instead of renaming with ~ suffix\n" +
       "   -port N                 use port N to communicate with " + 
                                   "rmi registry \n" +
       "   -preference <pref>      policy for updated files, " + 
                                   "where <pref> is");
    Enumeration e = Action.prefTable.keys();
    while (e.hasMoreElements()) {
      String k = (String)e.nextElement();
      Trace.always
        ("       " + k + "                       ".substring(0,23-k.length()) +
         ((Action)Action.prefTable.get(k)).doc()); }
  }

  public static void main(String[] args) {
    Action preference;
    String[] rest;
    String aRoot; 
    String bRoot;
    String remoteClassDir;
    boolean beAServer;
    boolean init;
    boolean clearCache;

    // Create and install a security manager
    // System.setSecurityManager(new RMISecurityManager());

    // Parse command line arguments
    try {
      CmdLineParser p = new CmdLineParser();
      p.stringOpt("preference");
      p.intOpt("trace");
      p.nullaryOpt("hardDelete");
      p.nullaryOpt("log");
      p.nullaryOpt("paranoid");
      p.nullaryOpt("server");
      p.nullaryOpt("init");
      p.nullaryOpt("clearCache");
      p.intOpt("port");
      p.stringOpt("localRoot");
      p.stringOpt("remoteRoot");
      p.stringOpt("remoteClassDir");
      p.parse(args);
      preference = 
        (Action)Action.prefTable.get(p.getStringOpt("preference","default"));
      Trace.tracingLevel = p.getIntOpt("trace",1);
      NetFileImpl.hardDelete = p.getNullaryOpt("hardDelete");
      NetFileImpl.paranoid = p.getNullaryOpt("paranoid");
      ServerImpl.log = p.getNullaryOpt("log");
      Globals.rmiport = p.getIntOpt("port", Globals.defaultRmiport);
      aRoot = p.getStringOpt("localRoot", "");
      bRoot = p.getStringOpt("remoteRoot", "");
      remoteClassDir = p.getStringOpt("remoteClassDir", "");
      beAServer = p.getNullaryOpt("server");
      init = p.getNullaryOpt("init");
      clearCache = p.getNullaryOpt("clearCache");
      rest = p.getPositionalArgs(); }
    catch (CmdLineException e) {
      showUsage(e.getMessage()); 
      return; }
      
    try {
      Server aServer = null; Server bServer = null;
      String path;

      if (beAServer) {
        ServerImpl.bind(); 
        return; }
      else if (rest.length >= 2) {
        if (rest[0].equals("local")) {
          aPrefix = "A:";
          bPrefix = "B:";
          aServer = ServerImpl.local();
          bServer = aServer; 
	}
        else {
          bPrefix = rest[0] + ":";
          aServer = ServerImpl.local();
          bServer = ServerImpl.createRemote
            (rest[0], remoteClassDir, Globals.rmiport);
	}
      }
      else {
        showUsage("Must provide a hostname (or local) and at least one path");
        return; 
      }
      aFS = aServer.root(aRoot, clearCache);
      bFS = bServer.root(bRoot, clearCache);
      FileSysTabA = aFS.getFileSysInfo();
      FileSysTabB = bFS.getFileSysInfo();
      Hashtable DirtyTableA = (Hashtable)aFS.getDirtyTable();
      Hashtable DirtyTableB = (Hashtable)bFS.getDirtyTable();
      arootpath = aRoot;// we need these later in snc(...)
      brootpath = bRoot;

      // at this point have locally all the info needed by the snc function
      
      for (int j=1; j<rest.length; j++) {
        if (init) {
          Trace.always("Marking " + rest[j] + " synchronized");

	  String afullpath = arootpath + File.separator + rest[j];
	  String bfullpath = brootpath +  File.separator + rest[j];
	  FileInfo afinfo = (FileInfo)(FileSysTabA.get(arootpath + 
					     File.separator + rest[j]));
	  FileInfo bfinfo = (FileInfo)(FileSysTabB.get(brootpath + 
					     File.separator + rest[j]));
	  int dirid = afinfo.getdirIds();
	  long timesnc = afinfo.getsncTime();
          markEverythingSynchro(FileSysTabA,afullpath,afinfo,dirid, timesnc);
	  markEverythingSynchro(FileSysTabB,bfullpath,bfinfo,dirid, timesnc); }

        else {
	  //sncTopLevel(aFS.lookup(rest[j]), bFS.lookup(rest[j]), preference);
	  //	sncTopLevel(Xb  

	  // call snc for the file that is being synchronized with 
	  //the preference; all other needed info should be global
	  snc(rest[j],preference);

	}
      }

      ///////
      aFS.sncFinalize();
      // have to send over the modified table such that it will
      // get saved into the cacheFile.....
      bFS.sncFinalize(FileSysTabB);

      Trace.trace("Snc complete");
    }
    catch (SncFailure e) {
      Trace.always("Snc failure:\n" + e.getMessage()); }
    catch (RemoteException e) {
      Trace.always("Remote exception:\n" + e.getMessage()); }

    ProcessMgr.killSubprocesses();
    System.exit(0);  // Why should this be needed? 
  }
}

