/*
 * This file is part of the Vamsas Client version 0.2. 
 * Copyright 2010 by Jim Procter, Iain Milne, Pierre Marguerite, 
 *  Andrew Waterhouse and Dominik Lindner.
 * 
 * Earlier versions have also been incorporated into Jalview version 2.4 
 * since 2008, and TOPALi version 2 since 2007.
 * 
 * The Vamsas Client is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *  
 * The Vamsas Client is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with the Vamsas Client.  If not, see <http://www.gnu.org/licenses/>.
 */
package uk.ac.vamsas.test;

import uk.ac.vamsas.client.*;
import uk.ac.vamsas.client.picking.IMessageHandler;
import uk.ac.vamsas.client.picking.IPickManager;
import uk.ac.vamsas.client.picking.Message;
import uk.ac.vamsas.objects.core.VAMSAS;
import uk.ac.vamsas.test.objects.Core;

import java.awt.Event;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.Vector;

/**
 * Toy vamsas command line client application demonstrating the API. Currently
 * runs with one argument: ExampleApplication.main(new String("watch")) Test:
 * Start up at least two of these processes independently and they will
 * successively modify and handle update events from the document model.
 * 
 * Behaviour of each client: Event handlers are registered for documentUpdate. -
 * every document update: * the vamsas document will be dumped to standard out
 * using uk.ac.vamsas.test.simpleclient.ArchiveReports * if there are more than
 * 4 vamsas roots, the first sequence in the first alignment of the first
 * dataset of the second vamsas root will be appended with a single gap
 * character and then written back to the session document. A pick thread is
 * started, which sends random CUSTOM pick events every so often (tuning:
 * flooding the pick channel seems to affect the behaviour of other vamsasClient
 * threads, suggesting some object lock contention). A new vamsas root generated
 * from uk.ac.vamsas.test.objects.Core.getDemoVamsas is added to the document.
 * Then a while loop waits around for events until shutdown: - currently it will
 * shutdown after 9 updates (totalUpdates) - an update will be made after every
 * other update that is detected.
 * 
 * Status: PickManager now shuts down correctly. Serial updates for two
 * instances work correctly and are detected under j1.4 (need to test in 1.5 and
 * 1.6).
 * 
 * TODO: test appData get/set methods
 * 
 * TODO: verify and test pickManager and interaction between it and other
 * session events
 * 
 * TODO: add more session interaction events
 * 
 * TODO: test client add/leave events - currently library generates exceptions
 * for sessionlist and clientlist modifications.
 * 
 * @author jimp
 */

public class ExampleApplication {
  private ClientHandle app;

  private UserHandle user; // TODO: make this something defined by the

  // api

  private IClientFactory clientfactory;

  private IClient vorbaclient;

  private byte[] mydata;

  private Vector vamsasObjects;

  private boolean isUpdated = false;

  private boolean isShuttingdown = false;

  private boolean isFinalizing = false;

  private int totalUpdates = 9;

  private uk.ac.vamsas.client.VorbaId recover = null;

  private int calls = 0;

  private long mdatahash = 0;

  private long muserdatahash = 0;

  private void processVamsasDocument(IClientDocument doc) {
    if (doc.getVamsasRoots().length < 4) {
      doc.addVamsasRoot(Core.getDemoVamsas());
    } else {
      try {
        uk.ac.vamsas.objects.core.DataSet ds = doc.getVamsasRoots()[1]
            .getDataSet(0);
        uk.ac.vamsas.objects.core.AlignmentSequence alsq = ds.getAlignment(0)
            .getAlignmentSequence(0);
        if (recover == null) {
          recover = alsq.getVorbaId();
        } else {
          Vobject recoverd = doc.getObject(recover);
          System.out.println("Recovery of " + recover + " was "
              + ((recoverd == null) ? "A FAILURE" : "SUCCESSFUL"));
        }
        System.out.println("Modifying Sequence:\n" + alsq.hashCode());
        alsq.setSequence(alsq.getSequence() + ds.getAlignment(0).getGapChar());
        System.out.println("Modifying Sequence:\n" + alsq.hashCode());
        System.out.println("Modified Sequence:\n" + alsq.getSequence());
        doc.setVamsasRoots(doc.getVamsasRoots());
      } catch (Exception ee) {

      }
    }
    // get this apps 'mydata' if it hasn't got it already.
    System.out.println("Trying to get appdata and modify it.....");
    try {
      processAppData(doc);
      System.out.println(".....Finished.");
    } catch (Exception e) {
      System.err.println("Failed to process appdata for our application.");
      e.printStackTrace(System.err);
    }

    // .. access this application's 'public' mydata' if there is any.
    vorbaclient.updateDocument(doc);
    // merge vamsasObjects with vamsas objects in document

  }

  private int appdatareads = 0;

  boolean clientappd = true;

  boolean userappd = false;

  private void processAppData(IClientDocument doc) throws Exception {
    appdatareads++;
    boolean writtenonce = false;
    if (doc != null) {
      uk.ac.vamsas.client.IClientAppdata appd = doc.getClientAppdata();
      if (clientappd) {
        if (appd.hasClientAppdata() && !(appdatareads % 2 == 0)) {
          // byte[] cappd = appd.getClientAppdata();
          // if (cappd!=null)
          // System.out.println("Client appdata\n"+cappd.toString()+"\nEnd of
          // Appdata\n");
          System.out.println("Testing read from inputstream");
          String cappds = readData(appd.getClientInputStream());
          System.out
              .println("Client appdata\n" + cappds + "\nEnd of Appdata\n");
        } else {
          if (!writtenonce) {
            String newapd = "Client Appdata:";
            if (appd.hasClientAppdata()) {
              AppDataInputStream is;
              newapd = readData(is = appd.getClientInputStream());
              is.close();
            }
            writtenonce = true;
            // appd.setClientAppdata(makeappData("Client Appdata for
            // "+user.toString()+" written"));
            writeData(appd.getClientOutputStream(), newapd
                + " : Client Appdata for all users written on " + appdatareads
                + " read by " + vorbaclient.getUserHandle());
            System.out.println("Written to ClientAppdata stream.");
          }
        }
      }
      if (userappd) {
        if (appd.hasUserAppdata() && !(appdatareads % 2 == 0)) {
          byte[] cappd = appd.getUserAppdata();
          if (cappd != null)
            System.out.println("User appdata\n" + new String(cappd)
                + "\nEnd of Users' Appdata\n");
          else {
            System.out.println("No user appdata.");
            appd
                .setUserAppdata(("no default - overwritten null byte set on "
                    + appdatareads + " read by " + vorbaclient.getUserHandle() + "")
                    .getBytes());
          }
        } else if (!writtenonce) {
          writtenonce = true;
          byte[] bts = makeappData("User Appdata for " + user + " written on "
              + appdatareads + " read at ");
          System.out.println("Setting appData bytes to\n" + new String(bts)
              + "\nEnd.");
          appd.setUserAppdata(bts);
          System.out.println("Written to UserAppdata stream.");
        }
      }
    }
  }

  private byte[] makeappData(String message) {
    StringBuffer sb = new StringBuffer();
    sb.append(message);
    sb.append("on " + new java.util.Date());
    return sb.toString().getBytes();
  }

  private boolean writeData(AppDataOutputStream os, String message) {
    StringBuffer sb = new StringBuffer();
    sb.append(message);
    sb.append("on " + new java.util.Date());
    try {
      ObjectOutputStream oos = new ObjectOutputStream(os);
      oos.writeObject(sb.toString());
      oos.flush();
      oos.close();
    } catch (Exception e) {
      System.err.println("Problem serialising this message:\n" + sb);
      e.printStackTrace(System.err);
      return false;
    }
    return true;
  }

  private String readData(AppDataInputStream is) {
    if (is != null) {
      try {
        if (is.available() > 0) {
          ObjectInputStream ois = new ObjectInputStream(is);
          String rs = (String) ois.readObject();
          return rs;
        }
      } catch (Exception e) {
        System.err.println("Failed to read a string from input stream!");
        e.printStackTrace(System.err);
      }
    }
    return "";
  }

  private void addHandlers(IClient avorbaclient) {
    final ExampleApplication me = this;
    // make a non-volatile reference to the client instance.
    final IClient vorbaclient = avorbaclient;
    // register update handler
    vorbaclient.addDocumentUpdateHandler(new PropertyChangeListener() {
      public void propertyChange(PropertyChangeEvent evt) {
        System.out.println("Vamsas document update for "
            + evt.getPropertyName() + ": " + evt.getOldValue() + " to "
            + evt.getNewValue());
        // merge new data into ours.
        // example - output doc
        try {
          IClientDocument cdoc = vorbaclient.getClientDocument();
          if (calls > 2 && cdoc.getVamsasRoots().length > 0
              && !cdoc.getVamsasRoots()[0].is__stored_in_document()) {
            System.err
                .println("Pathological Update Detected - Document is zeroed!");
          }
          calls++;
          uk.ac.vamsas.test.simpleclient.ArchiveReports.rootReport(cdoc
              .getVamsasRoots(), true, System.out);
          // Simple update
          try {
            if (cdoc.getVamsasRoots().length > 2) {
              uk.ac.vamsas.objects.core.DataSet ds = cdoc.getVamsasRoots()[1]
                  .getDataSet(0);
              uk.ac.vamsas.objects.core.AlignmentSequence alsq = ds
                  .getAlignment(0).getAlignmentSequence(0);
              if (alsq.isUpdated())
                System.out.println("Seqeuence was updated since last time.");
              alsq.setSequence(alsq.getSequence()
                  + ds.getAlignment(0).getGapChar());
              System.out.println("Newly Modified Sequence:\n"
                  + alsq.getSequence());
              cdoc.setVamsasRoots(cdoc.getVamsasRoots());
            }
          } catch (Exception ee) {
            System.err.println("Exception whilst updating :");
            ee.printStackTrace(System.err);
          }
          vorbaclient.updateDocument(cdoc);
        } catch (Exception e) {
          System.err
              .println("Exception whilst dumping document tree after an update.");
          e.printStackTrace(System.err);
        }
        isUpdated = true; // tell main thread to reflect change...
      }
    });
    // register close handler
    vorbaclient.addVorbaEventHandler(Events.DOCUMENT_REQUESTTOCLOSE,
        new PropertyChangeListener() {
          public void propertyChange(PropertyChangeEvent evt) {
            System.out.println("Received request to close vamsas document.");
            // ask user for a filename to save it to.
            // Then pass it to the vorba object...
            vorbaclient.storeDocument(new java.io.File("UserLocation"));
          }
        });

    // register some more handlers to monitor the session :

    vorbaclient.addVorbaEventHandler(Events.CLIENT_CREATION,
        new PropertyChangeListener() {
          public void propertyChange(PropertyChangeEvent evt) {
            System.out.println("New Vamsas client for " + evt.getPropertyName()
                + ": " + evt.getOldValue() + " to " + evt.getNewValue());
            // tell app add new client to its list of clients.
          }
        });
    vorbaclient.addVorbaEventHandler(Events.CLIENT_FINALIZATION,
        new PropertyChangeListener() {
          public void propertyChange(PropertyChangeEvent evt) {
            System.out.println("Vamsas client finalizing for "
                + evt.getPropertyName() + ": " + evt.getOldValue() + " to "
                + evt.getNewValue());
            // tell app to update its list of clients to communicate
            // with.
          }
        });
    vorbaclient.addVorbaEventHandler(Events.SESSION_SHUTDOWN,
        new PropertyChangeListener() {
          public void propertyChange(PropertyChangeEvent evt) {
            System.out.println("Session " + evt.getPropertyName()
                + " is shutting down.");
            // tell app to finalize its session data before
            // shutdown.
          }
        });
    vorbaclient.addVorbaEventHandler(Events.DOCUMENT_FINALIZEAPPDATA,
        new PropertyChangeListener() {
          public void propertyChange(PropertyChangeEvent evt) {
            boolean finalized = false;
            System.out
                .println("Application received a DOCUMENT_FINALIZEAPPDATA event.");
            IClientDocument cdoc = null;
            try {
              cdoc = vorbaclient.getClientDocument();
              if (cdoc != null) {
                IClientAppdata apd = cdoc.getClientAppdata();
                if (apd != null) {
                  String userd = "";
                  if (apd.getUserAppdata() != null) {
                    userd = new String(apd.getUserAppdata());
                  }
                  String appdat = me.readData(apd.getClientInputStream());
                  me.writeData(apd.getClientOutputStream(), appdat
                      + "\nUser Data merged\n" + userd + "\n");
                }
                finalized = true;
                vorbaclient.updateDocument(cdoc);
              }
              // tell app to finalize its session data prior to the
              // storage of the current session as an archive.
            } catch (Exception e) {
              if (!finalized) {
                System.err
                    .println("Probable library problem - exception when trying to update document after writing merged appdata.");
                e.printStackTrace(System.err);
              } else {
                System.err
                    .println("Recovering from exception when writing merged appdata");
                e.printStackTrace(System.err);
                try {
                  if (cdoc != null) {
                    vorbaclient.updateDocument(cdoc);
                  }
                } catch (Exception e2) {
                  System.err
                      .println("Probable library problem - exception when trying to update document after an exception when writing merged appdata.");
                  e2.printStackTrace(System.err);
                }
              }
              return;
            }
            System.out.println("Application finalized appdata successfuly.");
          }

        });
  }

  public String Usage = "ExampleApplication :/n [-arena <vamsasFileDirectory>][-session <vamsasSessionURN>] <action> [+<arguments>]\n"
      + "<action> is one of :\n\tsave,new,watch\n"
      + "-arena and -session are not yet supported"; // TODO for release

  String sess = null;

  String importFile = null;

  String outputFile = null;

  boolean newSession = false;

  boolean imported = false;

  private boolean parseArgs(String args[]) {
    if (args.length == 0) {
      return false;
    }
    int cpos = 0;
    boolean parsed = false;
    while (!parsed && cpos < args.length) {
      // TODO: note importDocument not yet supported.
      // if (args[cpos].toLowerCase().equals("load") && cpos + 1 < args.length)
      // {
      // importFile = args[cpos + 1];
      // cpos += 2;
      // continue;
      // }
      if (args[cpos].toLowerCase().equals("new") && cpos + 1 < args.length) {
        importFile = args[cpos + 1];
        newSession = true;
        cpos += 2;
        continue;
      }
      if (args[cpos].toLowerCase().equals("save") && cpos + 1 < args.length) {
        outputFile = args[cpos + 1];
        cpos += 2;
        continue;
      }
      // default behaviour - if not anything else its probably a session urn
      if (!args[cpos].toLowerCase().equals("watch")) {
        sess = args[cpos];
      }
      cpos++;
    }
    return true;
  }

  class ExamplePicker extends Thread {
    String me = null;

    public IPickManager pm = null;

    ExamplePicker(String me, IPickManager pm) {
      this.me = me;
      this.pm = pm;
    }

    public void run() {
      int mcount = 1;
      while (pm != null) {
        try {
          Thread.sleep(1000 + (long) Math.random() * 10000);
        } catch (Exception e) {
        }

        if (pm != null) {
          pm.sendMessage(new uk.ac.vamsas.client.picking.CustomMessage(
              "Message " + mcount++ + " from " + me));
        }
      }
    }

  }

  long shutdown = -1;

  public void runMe(String[] args) {
    if (!parseArgs(args)) {
      System.err.print(Usage);
    }
    // get IClientFactory
    try {
      clientfactory = new uk.ac.vamsas.client.simpleclient.SimpleClientFactory();
    } catch (IOException e) {
      System.err.println(e + "\n" + Usage);
      System.exit(1);
    }

    // get an Iclient with session data
    app = new ClientHandle("uk.ac.vamsas.test.ExampleApplication", "0.1");
    user = new UserHandle("arnolduser", "deathsdoor");
    if (sess != null && importFile != null) {
      System.err
          .println("Import of an existing document into an existing session is not fully supported yet.");
      System.exit(3);
    }
    try {
      if (sess != null) {
        System.out.println("Connecting to " + sess);
        vorbaclient = clientfactory.getIClient(app, user, sess);
      } else {
        if (newSession) {
          if (importFile == null) {
            System.out.println("Connecting to a new VAMSAS Session.");
            vorbaclient = clientfactory.getNewSessionIClient(app, user);
          } else {
            System.out
                .println("Connecting to a new VAMSAS Session to share data in existing document:"
                    + importFile);
            File vfile = new File(importFile);
            vorbaclient = clientfactory.openAsNewSessionIClient(app, user,
                vfile);
            System.out
                .println("Succesfully imported document into new session.");
            imported = true;
            importFile = null;
          }
        } else {
          vorbaclient = clientfactory.getIClient(app, user);
        }
      }
    } catch (InvalidSessionDocumentException e) {
      System.err.println("Failed to create a session to share " + importFile);
      System.err.println("Sorry it didn't work out. This is what went wrong:");
      e.printStackTrace(System.err);
      System.exit(3);
    } catch (NoDefaultSessionException e) {
      System.err
          .println("There appear to be several sessions to choose from :");
      String[] sessions = clientfactory.getCurrentSessions();
      for (int s = 0; s < sessions.length; s++)
        System.err.println(sessions[s]);
      System.exit(2);
    }
    addHandlers(vorbaclient);
    try {
      vorbaclient.joinSession();
    } catch (Exception se) {
      se.printStackTrace();
      System.err.println(se + " when joining session.\n" + Usage);
      System.exit(1);
    }
    // register an update listener and a close listener.
    // import any data if requested to
    if (importFile != null) {
      File vfile = new File(importFile);
      try {
        vorbaclient.importDocument(vfile);
        imported = true;
      } catch (Exception e) {
        System.err.println("Failed to import file " + importFile);
        System.err.println("Exception received was " + e);
        e.printStackTrace(System.err);
      }

    }
    // Write out any data if requested to
    if (outputFile != null) {
      File vfile = new File(outputFile);
      try {
        vorbaclient.storeDocument(vfile);
      } catch (Exception e) {
        System.err.println("Failed to export session as file " + outputFile);
        System.err.println("Exception received was " + e);
        e.printStackTrace(System.err);
      }
    }
    // get document data
    try {
      IClientDocument cdoc = vorbaclient.getClientDocument();
      if (imported) {
        if (cdoc.getVamsasRoots() == null
            || cdoc.getVamsasRoots()[0].getDataSetCount() < 1) {
          System.err
              .println("Imported an Empty vamsas document. Is this correct ?");
        }
      }
      processVamsasDocument(cdoc);
    } catch (Exception e) {
      System.err
          .println("Unexpected exception when retrieving the client document for the first time!");
      e.printStackTrace(System.err);
      System.exit(1);
    }
    int update = 0;
    ExamplePicker picker = new ExamplePicker(vorbaclient.getClientHandle()
        .getClientUrn(), vorbaclient.getPickManager());

    picker.start();
    if (picker.pm != null) {
      picker.pm.registerMessageHandler(new IMessageHandler() {

        public void handleMessage(Message message) {
          System.out.println("Received |" + message.getRawMessage() + "|");
          shutdown += 100; // hang around for 5 seconds or so before dying
          // naturally.

        }

      });
    }
    shutdown = System.currentTimeMillis() + 10000; // hang around for 10
    // seconds or so before
    // dying naturally.
    while (!isShuttingdown && update < totalUpdates) {
      // do something with data
      // , update document, or something.
      // ..
      if (isUpdated) {
        System.out.println("Update handler called " + (++update) + " times");
        System.out.println("******************************************");
        isUpdated = false; // TODO: saner update det method.
        shutdown = System.currentTimeMillis() + 10000;
        if (update % 2 == 1) {
          try {
            IClientDocument cdoc = vorbaclient.getClientDocument();
            processVamsasDocument(cdoc);
          } catch (Exception e) {
            System.err
                .println("Error when updating document after an even numbered update.");
            e.printStackTrace(System.err);
          }
          try {
            Thread.sleep(200);
          } catch (Exception e) {
          }
        }
      } else {
        if (System.currentTimeMillis() > shutdown) {
          isShuttingdown = true;
        }
      }

      try {
        Thread.sleep(50);
      } catch (Exception e) {
      }

    }
    System.out.println("Finalizing.");
    // call finalizeClient
    vorbaclient.finalizeClient();
    System.out.println("Shutting down picker.");
    picker.pm = null;
    while (picker.isAlive()) {
      System.out.println("Waiting for picker to die...");
      try {
        Thread.sleep(1000);
      } catch (Exception e) {
      }
      ;
    }

    // { meanwhile, eventHandlers are called to do any saves if need be }
    // and all registered listeners will be deregistered to avoid deadlock.

    // finish
    vorbaclient = null;
    clientfactory = null;
    System.out.println("Byee!");
    // try { Thread.sleep(100000); } catch (Exception e) {}
  }

  public static void main(String[] args) {
    ExampleApplication example = new ExampleApplication();
    example.runMe(args);
  }
}
