/*
 * 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.client.simpleclient;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;

import java.nio.channels.OverlappingFileLockException;
import java.util.Hashtable;
import java.util.Vector;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import uk.ac.vamsas.client.ClientHandle;
import uk.ac.vamsas.client.Events;
import uk.ac.vamsas.client.IClient;
import uk.ac.vamsas.client.IClientDocument;
import uk.ac.vamsas.client.IObjectUpdate;
import uk.ac.vamsas.client.InvalidSessionDocumentException;
import uk.ac.vamsas.client.InvalidSessionUrnException;
import uk.ac.vamsas.client.SessionHandle;
import uk.ac.vamsas.client.UserHandle;
import uk.ac.vamsas.client.picking.IPickManager;
import uk.ac.vamsas.objects.core.Entry;
import uk.ac.vamsas.objects.core.VamsasDocument;
import uk.ac.vamsas.objects.utils.ProvenanceStuff;

/**
 * @author jimp
 */
public class SimpleClient implements IClient {

  private static Log log = LogFactory.getLog(SimpleClient.class);

  protected UserHandle user = null;

  protected SessionUrn session = null;

  protected VamsasSession _session;

  protected ClientHandle client = null;

  protected EventGeneratorThread evgen = null;

  protected ClientDocument cdocument = null;

  private Lock activeClientFilelock = null;

  private File clientlockFile = null;

  /**
   * object hash table that persists in each client holding vorbaIds and hash
   * values after a document write
   */
  protected Hashtable extantobjects = null;

  /**
   * construct a transient IdFactory instance - this should last only as long as
   * the SimpleClient object holds the lock on the vamsas document being
   * created/manipulated.
   * 
   * @return
   */
  private IdFactory makeVorbaIdFactory() {
    if (extantobjects == null)
      extantobjects = new Hashtable();
    return new IdFactory(getSessionHandle(), client, user, extantobjects);
  }

  /**
   * construct SimpleClient for user, client and VamsasSession directory use the
   * SimpleClientFactory rather than this constructor directly.
   * 
   * @param user
   * @param client
   * @param sess
   */
  protected SimpleClient(UserHandle user, ClientHandle client,
      VamsasSession sess) throws InvalidSessionUrnException {
    // TODO: validate user/client/session
    _session = sess;
    this.user = user;
    this.client = client;
    // try {
    log.debug("Creating new session for " + _session);
    session = new SessionUrn(_session);
    log.debug("Creating new Event Generator");
    evgen = new EventGeneratorThread(_session, this, handlers);
    /*
     * } catch (MalformedURLException e) {
     * log.error("Couldn't form a valid SessionUrn object!",e); throw new
     * InvalidSessionUrnException(_session.toString()); }
     */
    log
        .debug("SimpleClient constructed for session "
            + session.getSessionUrn());

  }

  /**
   * construct new SimpleClientsession by importing objects from an existing
   * vamsas document
   * 
   * @param user
   * @param client
   * @param sess
   * @param importingArchive
   * @throws Exception
   *           IOExceptions for Session IO problems, and general Exception if
   *           importing document is invalid. protected SimpleClient(UserHandle
   *           user, ClientHandle client, VamsasSession sess, File
   *           importingArchive) throws Exception { this(user, client, sess); if
   *           (log.isDebugEnabled()) {
   *           log.debug("Attempting to overwrite session document with file: "
   *           +importingArchive); } // TODO: write provenance entry for new
   *           session indicating the import.
   * 
   *           }
   */

  /*
   * (non-Javadoc) LATER: check that build substitution variables are correct
   * 
   * @see uk.ac.vamsas.client.IClient#getAbout()
   */
  public String getAbout() {
    return new String("VORBA SimpleClient version $version$ build $build$");
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClient#getSessionUrn()
   */
  public String getSessionUrn() {
    return session.getSessionUrn();
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClient#getSessionHandle()
   */
  public SessionHandle getSessionHandle() {
    // TODO: eliminate SessionHandle ? need to refactor interfaces.
    SessionHandle sh = new SessionHandle(session.getSessionUrn());
    return sh;
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClient#getClientHandle()
   */
  public ClientHandle getClientHandle() {
    return client;
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClient#getUserHandle()
   */
  public UserHandle getUserHandle() {
    return user;
  }

  /**
   * 
   * @return user field for a provenance entry
   */
  protected String getProvenanceUser() {
    return new String(user.getFullName());
  }

  /**
   * construct a provenance entry for this client with the specified action
   * string.
   * 
   * @param action
   * @return properly completed provenance entry
   */
  protected Entry getProvenanceEntry(String action) {
    Entry prov = ProvenanceStuff.newProvenanceEntry(client.getClientUrn(),
        getProvenanceUser(), action);
    return prov;
  }

  private Hashtable handlers = initHandlers();

  private Vector listeners = new Vector();

  /**
   * make all the PropertyChangeSupport objects for the events described in
   * uk.ac.vamsas.client.Event
   * 
   * @return
   */
  private static Hashtable initHandlers() {
    Hashtable events = new Hashtable();
    java.util.Iterator evt = Events.EventList.iterator();
    while (evt.hasNext()) {
      Object ths = evt.next();
      events.put(ths, (Object) new PropertyChangeSupport(ths));
    }
    return events;
  }

  /*
   * (non-Javadoc)
   * 
   * @see
   * uk.ac.vamsas.client.IClient#addDocumentUpdateHandler(java.util.EventListener
   * )
   */
  public void addDocumentUpdateHandler(PropertyChangeListener evt) {
    this.addVorbaEventHandler(Events.DOCUMENT_UPDATE, evt);
  }

  boolean finalized = false;

  private void haltPickmanager() {
    if (pickmanager != null) {
      final SimpleClient dying = this;
      new Thread() {
        public void run() {
          SimpleClient.log.debug("Stopping pickManager..");
          dying.pickmanager.shutdown();
          SimpleClient.log.debug("pickManager halted.");
        }
      }.start();
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClient#finalizeClient()
   */
  public void finalizeClient() {
    if (finalized)
      throw new Error(
          "VAMSAS Client Implementation Error: Finalized called twice for same client instance.");

    // mark this instance as finalized
    finalized = true;

    // TODO: determine if this is last client in session

    // TODO: raise events like : ((lst_client && document.request.to.close),
    // (client_finalization), (
    evgen._raise(Events.CLIENT_FINALIZATION, null, this, null);
    // if (handlers.containsKey(Events.))
    // if (handlers.containsKey(Events.CLIENT_FINALIZATION))
    // deregister listeners.
    log.debug("Stopping pickManager");
    haltPickmanager();

    log.debug("Deregistering Client");
    _session.removeClient(this);
    // log.debug("Stopping EventGenerator..");
    // evgen.stopWatching();
    this.cdocument = null;
    SimpleClient.log.debug("EventGenerator halted.");
    log.debug("finalization Complete.");
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClient#getClientDocument()
   */
  public IClientDocument getClientDocument() throws IOException {
    log.debug("getClientDocument");
    if (cdocument != null) {
      // cdocument is non-nill if the ClientDocument.finalise() method hasn't
      // been called.
      return cdocument;
    }
    evgen.disableDocumentWatch();
    VamsasArchive va = null;
    try {
      // LATER: bail out if it takes too long to get the lock ?
      va = _session.getVamsasDocument();
    } catch (IOException e) {
      throw new IOException("Failed to get lock on session document");
    }
    VamsasDocument doc = null;
    IdFactory vorba = null;
    // TODO: LATER: reduce size of vorba ids generated from these parameters to
    // IdFactory (mainly sessionHandle rationalization ?)
    try {
      va.setVorba(vorba = makeVorbaIdFactory());
      // if session currently holds data - read it in - or get a dummy
      log.debug("Accessing document");
      doc = va.getVamsasDocument(getProvenanceUser(),
          "created new session document.", null);
      if (doc != null)
        log.debug("Successfully retrieved document.");
      else
        log.error("Unexpectedly retrieved null document!");
    } catch (Exception e) {
      log.error("Failed to get session document for session directory '"
          + _session.sessionDir + "'", e);
      throw new IOException(
          "Failed to get session document for session directory '"
              + _session.sessionDir + "'");
    }
    // Construct the IClientDocument instance

    cdocument = new ClientDocument(doc, va, vorba, this);
    return cdocument;
  }

  /*
   * (non-Javadoc)
   * 
   * @throws Errors for invalid newdoc parameter
   * 
   * @see
   * uk.ac.vamsas.client.IClient#updateDocument(uk.ac.vamsas.client.IClientDocument
   * )
   */
  public void updateDocument(IClientDocument newdoc) {
    log.debug("updateDocument:");
    // Check validity of simpleclient instance and that it holds a lock on the
    // session's document
    if (!(newdoc instanceof ClientDocument)) {
      throw new Error("Invalid IClientDocument passsed to SimpleClient.");
    }
    if (cdocument == null)
      throw new Error(
          "Client Error - updateDocument() called before getClientDocument() on this SimpleClient instance.");
    if (newdoc != cdocument)
      throw new Error(
          "Client Error - SimpleClient.updateDocument() can only take the IClientDocument instance returned from SimpleClient.getClientDocument()");

    if (evgen.isDocumentWatchEnabled())
      throw new Error(
          "Probable Client Error (did you remember to call SimpleClient.updateDocument(clientdoc) at the end of the document update handler?) - or Library Bug : Document watcher still enabled whilst ClientDocument instance exists.");
    if (cdocument.isInvalidModification()) {
      log
          .info("Client has corrupted the vamsas document. It will not be written back to the session - sorry.");
      // TODO: modify updateDocument signature: We should really raise an
      // exception here to tell the client that it broke the datamodel.
    } else {
      // actually try to write - if necessary.
      if (!cdocument.isModified()) {
        // client document is silently got rid of, with no session update
        // events.
        if (log.isDebugEnabled())
          log.debug("updateDocument for " + session.getSessionUrn()
              + " with unmodified IClientDocument (skipping the write)");
      } else {
        writeSessionDocument();
      }
    }
    // release locks, reset and start to receive events again
    tidyAwaySessionDocumentState();
  }

  /**
   * garbage collect the ClientDocument instance and re-enable watchers.
   */
  protected void tidyAwaySessionDocumentState() {
    try {
      log.debug("Finalizing ClientDocument instance.");
      cdocument.finalize();
    } catch (Throwable e) {
      log.error("Exception when trying to garbage collect ClientDocument for "
          + session.getSessionUrn(), e);
    }
    cdocument = null; // this is probably done by finalize

    try {
      _session.unlockVamsasDocument();
      evgen.enableDocumentWatch();
    } catch (IOException e) {
      log.warn("IO Problems when releasing lock on session document!", e);
      _session.slog
          .error("IO problems when attempting to release lock on session document.");
    }
  }

  /**
   * write the cdocument instance to the session for real.
   */
  private void writeSessionDocument() {
    try {
      boolean updated = cdocument.updateSessionDocument();
      boolean hasContent = cdocument.isModified();
      if (!updated) {
        log
            .warn("Session document did not update properly for session directory "
                + _session.sessionDir);
        // cdocument.archive.cancelArchive(); // LATER: could be done - would
        // need to prevent updateSessionDocument closing the iohandler.
        _session.slog
            .warn("Session Document updated but may not be valid (false return from org.vamsas.simpleclient.ClientDocument.updateSessionDocument()");
      } else {
        log.debug("Document update successful.");
      }
      if (hasContent) {
        _session.setUnsavedFlag();
      }
    } catch (IOException e) {
      log.warn("IO Problems when updating document!", e);
      _session.slog.error("IO problems when attempting to update document.");
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClient#storeDocument(java.io.File)
   */
  public void storeDocument(File location) {
    if (location == null)
      throw new Error(
          "Vamsas Client API Usage Error: storeDocument called with null location.");
    log.debug("StoreDocument to " + location);
    // write storeDocument file to inform other clients that they should raise
    Lock vamlock = evgen.want_to_store();
    // Events.DOCUMENT_FINALIZEAPPDATA
    try {
      _session.writeVamsasDocument(location, vamlock);
      _session.clearUnsavedFlag();
    } catch (Exception e) {
      log.warn("Exception whilst trying to store document in " + location, e);
    }
    vamlock.release();
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClient#addVorbaEventHandler(java.lang.String,
   * java.beans.PropertyChangeListener)
   */
  public void addVorbaEventHandler(String EventChain, PropertyChangeListener evt) {
    if (handlers.containsKey(EventChain)) {
      log.debug("Adding new handler for " + EventChain);
      Object handler;
      ((PropertyChangeSupport) (handler = handlers.get(EventChain)))
          .addPropertyChangeListener(evt);
      listeners.add(handler);
      listeners.add((Object) evt);
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClient#pollUpdate()
   */
  public void pollUpdate() {
    log.debug("pollUpdate");
    if (evgen == null) {
      log.warn("pollUpdate called on incomplete SimpleClient object.");
      return;
    }

    if (!evgen.isWatcherAlive()) {
      log.warn("pollUpdate called before joinSession() - trying to do this.");
      try {
        joinSession();
      } catch (Exception e) {
        log.error("Unexpected exception on default call to joinSession", e);
      }
    }

    // TODO ensure event generator robustly handles these interrupts.
    log.debug("interrrupting event generator.");
    evgen.interruptWatching();
    log.debug("interrrupted event generator.");
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClient#joinSession()
   */
  public void joinSession() throws Exception {
    log.debug("Joining Session.");
    // start the EventGenerator thread.
    if (evgen == null) {
      log.warn("joinSession called on incomplete SimpleClient object.");
      return;
    }
    if (evgen.isWatcherAlive())
      throw new Error(
          "Join session called twice for the same SimpleClient (IClient instance).");
    evgen.startWatching();
    if (evgen.isWatcherAlive())
      log.debug("Started EventGenerator thread.");
    else {
      log.warn("Failed to start EventGenerator thread.");
      throw new Exception(
          "Failed to start event generator thread - client cannot be instantiated.");
    }
    if (evgen.countHandlersFor(Events.DOCUMENT_CREATE) > 0) {
      // TODO: LATER: is this application connecting to a newly created session
      // document ?
      // evgen.raise(Events.DOCUMENT_CREATE);
    }

  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClient#importDocument(java.io.File)
   */
  public void importDocument(File location) {
    // TODO LATER: implement SimpleClient.importDocument()
    // if (numberOfClients<1 or document file is empty/new, then can import
    // directly.)
    // more complex if data is already present in document. Could have a
    // 'clearSession' method, too, or dump and overwrite instead.
    log
        .error("importDocument is not yet implemented for a SimpleClient Session.");

    /*
     * try { this._session.setVamsasDocument(location); } catch (IOException e)
     * { log.error("importDocument failed."); }
     */
  }

  public IObjectUpdate getUpdateHandler(Class rootObject) {
    // TODO Auto-generated method stub
    return null;
  }

  public IObjectUpdate[] getUpdateHandlers() {
    // TODO Auto-generated method stub
    return null;
  }

  public void removeUpdateHandler(Class rootObject) {
    // TODO Auto-generated method stub

  }

  public void setUpdateHandler(IObjectUpdate handler) {
    // TODO Auto-generated method stub

  }

  /**
   * retrieves the current VamsasSession to which belong the client
   * 
   * @return the _session
   */
  protected VamsasSession get_session() {
    return this._session;
  }

  SimplePickManager pickmanager = null;

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClient#getPickManager()
   */
  public IPickManager getPickManager() {
    createPickManager();
    return pickmanager;
  }

  private void createPickManager() {
    if (pickmanager == null) {
      // TODO: Construct PickManager for session using details from sessionURN!
      log.debug("Creating PickManager (not from sessionURN yet)");
      pickmanager = new SimplePickManager(
          new uk.ac.vamsas.client.picking.SocketManager());
    }
  }

  protected void releaseActiveClientFile() throws IOException {

    log.debug("Releasing active client locks");
    if (activeClientFilelock != null) {// Release the lock
      log.debug("Releasing lock on active client lock file");
      activeClientFilelock.release();
      log
          .debug("ReleaseActiveClientFile called when client has no lock on its clientLockFile");
      activeClientFilelock = null;
    } else {
      log
          .debug("ReleaseActiveClientFile called when client has no lock on its clientLockFile");
    }
    if (this.clientlockFile != null) {
      log.debug("trying to delete active client lock file");
      if (this.clientlockFile.exists()) {
        this.clientlockFile.delete();
        log.debug("deleted active client lock file");
      }
    } else {
      log
          .debug("ReleaseActiveClientFile called when client has no clientLockFile");
    }
  }

  protected void createActiveClientFile() throws IOException {
    if (this.clientlockFile != null)
      return;
    log.debug("createActiveClientFile");
    // create, if need, subdirectory to contain client files
    File clientlockFileDir = new File(this.get_session().sessionDir, this
        .get_session().clientFileDirectory);
    if (!clientlockFileDir.exists()) {// the directory does not exist, create it
      if (!clientlockFileDir.mkdirs()) {
        throw new IOException(
            "Failed to create sub directory to session directory  for client lock files'"
                + clientlockFileDir.getAbsolutePath() + "'");
      }
    } else {
      if (!(clientlockFileDir.isDirectory() && clientlockFileDir.canWrite())) {
        throw new IOException(
            "Directory  for client lock files is not a directory or is not accessibl: '"
                + clientlockFileDir.getAbsolutePath() + "'");
      }
    }
    this.clientlockFile = new File(clientlockFileDir, this.getClientHandle()
        .getClientUrn().replaceAll("[:;/\\\\]+", ""));

    log.debug("Creating active client lock file "
        + this.clientlockFile.getAbsolutePath());
    Lock clientLock = uk.ac.vamsas.client.simpleclient.LockFactory.getLock(
        clientlockFile, false);
    if (clientLock == null || !clientLock.isLocked()) {
      log
          .fatal("IMPLEMENTATION ERROR: Couldn't get a lock for the client lock file "
              + clientlockFile);
    }
    activeClientFilelock = clientLock;
  }

  /**
   * @return the clientlockFile
   */
  protected File getClientlockFile() {
    return clientlockFile;
  }

  /**
   * 
   * @return the lock for the client in the session
   */
  protected Lock getClientLock() {
    return activeClientFilelock;
  }

  SimpleClientConfig _config = null;

  public SimpleClientConfig getSimpleClientConfig() {
    if (_config == null) {
      _config = new SimpleClientConfig();
    }
    return _config;
  }

}
