/*
 * 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.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInput;
import java.io.DataInputStream;
import java.io.DataOutput;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Vector;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;

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

import uk.ac.vamsas.client.AppDataInputStream;
import uk.ac.vamsas.client.AppDataOutputStream;
import uk.ac.vamsas.client.IClientAppdata;
import uk.ac.vamsas.objects.core.AppData;
import uk.ac.vamsas.objects.core.ApplicationData;
import uk.ac.vamsas.objects.core.User;
import uk.ac.vamsas.objects.utils.AppDataReference;

/**
 * @author jimp Access interface to data chunks read from a VamsasArchiveReader
 *         stream (or byte buffer input stream) or written to a VamsasArchive
 *         stream. // TODO: get VamsasArchiveReader from sclient
 */
public class SimpleClientAppdata implements IClientAppdata {
  private static Log log = LogFactory.getLog(SimpleClientAppdata.class);

  /**
   * has the session's document been accessed to get the AppData entrys?
   */
  protected boolean accessedDocument = false;

  /**
   * has the user datablock been modified ? temporary file containing new user
   * specific application data chunk
   */
  SessionFile newUserData = null;

  JarOutputStream newUserDataStream = null;

  /**
   * has the apps global datablock been modified ? temporary file containing new
   * global application data chunk
   */
  SessionFile newAppData = null;

  JarOutputStream newAppDataStream = null;

  /**
   * set by extractAppData
   */
  protected ApplicationData appsGlobal = null;

  /**
   * set by extractAppData
   */
  protected User usersData = null;

  ClientDocument clientdoc;

  /**
   * state flags - accessed ClientAppdata - accessed UserAppdata => inputStream
   * from embedded xml or jar entry of backup has been created - set
   * ClientAppdata - set UserAppdata => an output stream has been created and
   * written to - or a data chunk has been written. - need flag for switching
   * between embedded and jar entry mode ? - always write a jar entry for a
   * stream. - need code for rewind and overwriting if the set*Appdata methods
   * are called more than once. - need flags for streams to except a call to
   * set*Appdata when an output stream exists and is open. - need
   * 
   * @param clientdoc
   *          The ClientDocument instance that this IClientAppData is accessing
   */
  protected SimpleClientAppdata(ClientDocument clientdoc) {
    if (clientdoc == null) {
      log
          .fatal("Implementation error - Null ClientDocument for SimpleClientAppdata construction.");
      throw new Error(
          "Implementation error - Null ClientDocument for SimpleClientAppdata construction.");
    }
    this.clientdoc = clientdoc;
  }

  /**
   * ensures that the appData information for this client instance has been
   * extracted from the vamsas document provided by clientdoc.
   */
  private void extractAppData() {
    if (!accessedDocument)
      _extractAppData(clientdoc.getVamsasDocument());
  }

  /**
   * gets appropriate app data for the application, if it exists in this dataset
   * Called by every accessor to ensure data has been retrieved from document.
   */
  private void _extractAppData(uk.ac.vamsas.objects.core.VamsasDocument doc) {
    if (doc == null) {
      log.debug("extractAppData called for null document object");
      return;
    }
    if (accessedDocument) {
      return;
    }
    Vector apldataset = AppDataReference.getUserandApplicationsData(doc,
        clientdoc.sclient.getUserHandle(), clientdoc.sclient.getClientHandle());
    accessedDocument = true;
    if (apldataset != null) {
      if (apldataset.size() > 0) {
        AppData clientdat = (AppData) apldataset.get(0);
        if (clientdat instanceof ApplicationData) {
          appsGlobal = (ApplicationData) clientdat;
          if (apldataset.size() > 1) {
            clientdat = (AppData) apldataset.get(1);
            if (clientdat instanceof User) {
              usersData = (User) clientdat;
            }
            if (apldataset.size() > 2)
              log.info("Ignoring additional (" + (apldataset.size() - 2)
                  + ") AppDatas returned by document appdata query.");
          }
        } else {
          log.warn("Unexpected entry in AppDataReference query: id="
              + clientdat.getVorbaId() + " type="
              + clientdat.getClass().getName());
        }
        apldataset.removeAllElements(); // destroy references.
      }
    }
  }

  /**
   * LATER: generalize this for different low-level session implementations (it
   * may not always be a Jar)
   * 
   * @param appdata
   * @param docreader
   * @return
   */
  private InputStream getAppDataStream(AppData appdata,
      VamsasArchiveReader docreader) {
    String entryRef = appdata.getDataReference();
    if (entryRef != null) {
      log.debug("Resolving appData reference +" + entryRef);
      InputStream entry = docreader.getAppdataStream(entryRef);
      if (entry != null) {
        return entry;
        // log.warn("Implementation problem - docreader didn't return a JarInputStream entry.");
      }
    } else {
      log
          .debug("GetAppDataStream called for an AppData without a data reference.");
    }
    return null;
  }

  /**
   * yuk - size of buffer used for slurping appData JarEntry into a byte array.
   */
  private final int _TRANSFER_BUFFER = 4096 * 4;

  /**
   * Resolve AppData object to a byte array.
   * 
   * @param appdata
   * @param archiveReader
   * @return null or the application data as a byte array
   */
  private byte[] getAppDataAsByteArray(AppData appdata,
      VamsasArchiveReader docreader) {
    if (appdata.getData() == null) {
      if (docreader == null) {
        log.warn("Silently failing getAppDataAsByteArray with null docreader.",
            new Exception());
        return null;
      }
      // resolve and load data
      InputStream entry = getAppDataStream(appdata, docreader);
      ByteArrayOutputStream bytes = new ByteArrayOutputStream();
      try {
        byte buff[] = new byte[_TRANSFER_BUFFER];
        int olen = 0;
        while (entry != null && entry.available() > 0) {
          int len = entry.read(buff, 0, _TRANSFER_BUFFER);
          if (len > -1) {
            bytes.write(buff, 0, len);
            olen += len;
          }
        }
        buff = null;
      } catch (Exception e) {
        log
            .warn(
                "Unexpected exception - probable truncation when accessing VamsasDocument entry "
                    + appdata.getDataReference(), e);
      }
      if (bytes.size() > 0) {
        // LATER: deal with probable OutOfMemoryErrors here
        log.debug("Got " + bytes.size() + " bytes from AppDataReference "
            + appdata.getDataReference());
        byte data[] = bytes.toByteArray();
        bytes = null;
        return data;
      }
      return null;
    } else {
      log.debug("Returning inline AppData block for " + appdata.getVorbaId());
      return appdata.getData();
    }
  }

  /**
   * internal method for getting a DataInputStream from an AppData object.
   * 
   * @param appdata
   * @param docreader
   * @return data in object or null if no data is accessible
   */
  private AppDataInputStream getAppDataAsDataInputStream(AppData appdata,
      VamsasArchiveReader docreader) {
    if (appdata != null && docreader != null) {
      String entryRef = appdata.getDataReference();
      if (entryRef != null) {
        log.debug("Resolving AppData reference for " + entryRef);
        InputStream jstrm = docreader.getAppdataStream(entryRef);
        if (jstrm != null)
          return new AppDataInputStream(jstrm);
        else {
          log.debug("Returning null input stream for unresolved reference ("
              + entryRef + ") id=" + appdata.getVorbaId());
          return null;
        }
      } else {
        // return a byteArray input stream
        byte[] data = appdata.getData();
        if (data.length > 0) {
          ByteArrayInputStream stream = new ByteArrayInputStream(data);
          return new AppDataInputStream(stream);
        } else {
          log
              .debug("Returning null input stream for empty Appdata data block in id="
                  + appdata.getVorbaId());
          return null;
        }
      }
    } else {
      log.debug("Returning null DataInputStream for appdata entry:"
          + appdata.getVorbaId());
    }
    return null;
  }

  /**
   * internal method for getting ByteArray from AppData object
   * 
   * @param clientOrUser
   *          - true for returning userData, otherwise return Client AppData.
   * @return null or byte array
   */
  private byte[] _getappdataByteArray(boolean clientOrUser) {
    if (clientdoc == null)
      throw new Error(
          "Implementation error, Improperly initialized SimpleClientAppdata.");
    byte[] data = null;
    String appdName;
    if (!clientOrUser) {
      appdName = "Client's Appdata";
    } else {
      appdName = "User's Appdata";
    }
    log.debug("getting " + appdName + " as a byte array");
    extractAppData();
    AppData object;
    if (!clientOrUser) {
      object = appsGlobal;
    } else {
      object = usersData;
    }
    if (object != null) {
      log.debug("Trying to resolve " + appdName + " object to byte array.");
      data = getAppDataAsByteArray(object, clientdoc.getVamsasArchiveReader());
    }
    if (data == null)
      log
          .debug("Returning null for " + appdName
              + "ClientAppdata byte[] array");
    return data;

  }

  /**
   * common method for Client and User AppData->InputStream accessor
   * 
   * @param clientOrUser
   *          - the appData to resolve - false for client, true for user
   *          appdata.
   * @return null or the DataInputStream desired.
   */
  private AppDataInputStream _getappdataInputStream(boolean clientOrUser) {
    if (clientdoc == null)
      throw new Error(
          "Implementation error, Improperly initialized SimpleClientAppdata.");
    String appdName;
    if (!clientOrUser) {
      appdName = "Client's Appdata";
    } else {
      appdName = "User's Appdata";
    }
    if (log.isDebugEnabled())
      log.debug("getting " + appdName + " as an input stream.");
    extractAppData();
    AppData object;
    if (!clientOrUser) {
      object = appsGlobal;
    } else {
      object = usersData;
    }
    if (object != null) {
      log.debug("Trying to resolve ClientAppdata object to an input stream.");
      return getAppDataAsDataInputStream(object, clientdoc
          .getVamsasArchiveReader());
    }
    log.debug("getClientInputStream returning null.");
    return null;
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClientAppdata#getClientAppdata()
   */
  public byte[] getClientAppdata() {
    return _getappdataByteArray(false);
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClientAppdata#getClientInputStream()
   */
  public AppDataInputStream getClientInputStream() {
    return _getappdataInputStream(false);
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClientAppdata#getUserAppdata()
   */
  public byte[] getUserAppdata() {
    return _getappdataByteArray(true);
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClientAppdata#getUserInputStream()
   */
  public AppDataInputStream getUserInputStream() {
    return _getappdataInputStream(true);
  }

  /**
   * methods for writing new AppData entries.
   */
  private AppDataOutputStream _getAppdataOutputStream(boolean clientOrUser) {
    // Must access document to get any existing references
    extractAppData();
    String apdname;
    SessionFile apdfile = null;
    if (!clientOrUser) {
      apdname = "clientAppData";
      apdfile = newAppData;
    } else {
      apdname = "userAppData";
      apdfile = newUserData;
    }
    try {
      if (apdfile == null) {
        apdfile = clientdoc.sclient._session
            .getTempSessionFile(apdname, ".jar");
        log.debug("Successfully made temp appData file for " + apdname);
      } else {
        // truncate to remove existing data.
        apdfile.fileLock.getRaFile().setLength(0);
        log
            .debug("Successfully truncated existing temp appData for "
                + apdname);
      }
    } catch (Exception e) {
      log.error("Whilst opening temp file in directory "
          + clientdoc.sclient._session.sessionDir, e);
    }
    // we do not make another file for the new entry if one exists already
    if (!clientOrUser) {
      newAppData = apdfile;
    } else {
      newUserData = apdfile;
    }
    try {
      apdfile.lockFile();
      // LATER: Refactor these local AppDatastream IO stuff to their own class.
      JarOutputStream dstrm = new JarOutputStream(apdfile.fileLock
          .getBufferedOutputStream(true));
      if (!clientOrUser) {
        newAppDataStream = dstrm;
      } else {
        newUserDataStream = dstrm;
      }
      dstrm.putNextEntry(new JarEntry("appData_entry.dat"));
      // LATER: there may be trouble ahead if an AppDataOutputStream is written
      // to by one thread when another truncates the file. This situation should
      // be prevented if possible
      return new AppDataOutputStream(dstrm);
    } catch (Exception e) {
      log.error("Whilst opening jar output stream for file "
          + apdfile.sessionFile);
    }
    // tidy up and return null
    apdfile.unlockFile();
    return null;
  }

  /**
   * copy data from the appData jar file to an appropriately referenced jar or
   * Data entry for the given ApplicationData Assumes the JarFile is properly
   * closed.
   * 
   * @param vdoc
   *          session Document handler
   * @param appd
   *          the AppData whose block is being updated
   * @param apdjar
   *          the new data in a Jar written by this class
   */
  protected void updateAnAppdataEntry(VamsasArchive vdoc, AppData appd,
      SessionFile apdjar) throws IOException {
    if (apdjar == null || apdjar.sessionFile == null
        || !apdjar.sessionFile.exists()) {
      throw new IOException("No temporary Appdata to recover and transfer.");
    }
    if (vdoc == null) {
      log.fatal("FATAL! NO DOCUMENT TO WRITE TO!");
      throw new IOException("FATAL! NO DOCUMENT TO WRITE TO!");
    }
    log.debug("Recovering AppData entry from " + apdjar.sessionFile);
    JarInputStream istrm = new JarInputStream(apdjar
        .getBufferedInputStream(true));
    JarEntry je = null;
    while (istrm.available() > 0 && (je = istrm.getNextJarEntry()) != null
        && !je.getName().equals("appData_entry.dat")) {
      if (je != null)
        log.debug("Ignoring extraneous entry " + je.getName());
    }
    if (istrm.available() > 0 && je != null) {
      log.debug("Found appData_entry.dat in Jar");
      String ref = appd.getDataReference();
      if (ref == null) {
        throw new IOException("Null AppData.DataReference passed.");
      }
      log.debug("Writing appData_entry.dat as " + ref);
      if (vdoc.writeAppdataFromStream(ref, istrm)) {
        log.debug("Entry updated successfully.");
      } else {
        throw new IOException(
            "writeAppdataFromStream did not return true - expect future badness."); // LATER
                                                                                    // -
                                                                                    // verify
                                                                                    // why
                                                                                    // this
                                                                                    // might
                                                                                    // occur.
      }
    } else {
      throw new IOException(
          "Couldn't find appData_entry.dat in temporary jar file "
              + apdjar.sessionFile.getAbsolutePath());
    }
    istrm.close();
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClientAppdata#getClientOutputStream()
   */
  public AppDataOutputStream getClientOutputStream() {
    if (clientdoc == null)
      throw new Error(
          "Implementation error, Improperly initialized SimpleClientAppdata.");
    if (log.isDebugEnabled())
      log.debug("trying to getClientOutputStream for "
          + clientdoc.sclient.client.getClientUrn());
    return _getAppdataOutputStream(false);
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClientAppdata#getUserOutputStream()
   */
  public AppDataOutputStream getUserOutputStream() {
    if (clientdoc == null)
      throw new Error(
          "Implementation error, Improperly initialized SimpleClientAppdata.");
    if (log.isDebugEnabled())
      log.debug("trying to getUserOutputStream for ("
          + clientdoc.sclient.getUserHandle().getFullName() + ")"
          + clientdoc.sclient.client.getClientUrn());
    return _getAppdataOutputStream(true);
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClientAppdata#hasClientAppdata()
   */
  public boolean hasClientAppdata() {
    if (clientdoc == null)
      throw new Error(
          "Implementation error, Improperly initialized SimpleClientAppdata.");
    extractAppData();
    // LATER - check validity of a DataReference before we return true
    // TODO: return true if usersData is null but we have already written a new
    // data stream
    if ((appsGlobal != null)
        && (appsGlobal.getDataReference() != null || appsGlobal.getData() != null))
      return true;
    return false;
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClientAppdata#hasUserAppdata()
   */
  public boolean hasUserAppdata() {
    if (clientdoc == null)
      throw new Error(
          "Implementation error, Improperly initialized SimpleClientAppdata.");
    extractAppData();
    // LATER - check validity of a DataReference before we return true
    // TODO: return true if usersData is null but we have already written a new
    // data stream
    if ((usersData != null)
        && (usersData.getDataReference() != null || usersData.getData() != null))
      return true;
    return false;
  }

  private boolean _writeAppDataStream(JarOutputStream ostrm, byte[] data) {
    try {
      if (data != null && data.length > 0)
        ostrm.write(data);
      ostrm.closeEntry();
      return true;
    } catch (Exception e) {
      log.error("Serious! - IO error when writing AppDataStream to file "
          + newAppData.sessionFile, e);
    }
    return false;
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClientAppdata#setClientAppdata(byte[])
   */
  public void setClientAppdata(byte[] data) {
    if (clientdoc == null)
      throw new Error(
          "Implementation error, Improperly initialized SimpleClientAppdata.");
    _getAppdataOutputStream(false);
    if (newAppDataStream == null) {
      // LATER: define an exception for this ? - operation may fail even if file
      // i/o not involved
      log
          .error("Serious! - couldn't open new AppDataStream in session directory "
              + clientdoc.sclient._session.sessionDir);
    } else {
      _writeAppDataStream(newAppDataStream, data);
      // LATER: deal with error case - do we make session read only, or what ?
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.IClientAppdata#setUserAppdata(byte[])
   */
  public void setUserAppdata(byte[] data) {
    if (clientdoc == null)
      throw new Error(
          "Implementation error, Improperly initialized SimpleClientAppdata.");
    _getAppdataOutputStream(true);
    if (newUserDataStream == null) {
      // LATER: define an exception for this ? - operation may fail even if file
      // i/o not involved
      log
          .error("Serious! - couldn't open new UserDataStream in session directory "
              + clientdoc.sclient._session.sessionDir);
    } else {
      _writeAppDataStream(newUserDataStream, data);
      // LATER: deal with error case - do we make session read only, or what ?
    }
  }

  /**
   * flush and close outstanding output streams. - do this before checking data
   * length.
   * 
   * @throws IOException
   */
  protected void closeForWriting() throws IOException {
    if (newAppDataStream != null) {
      newAppDataStream.flush();
      newAppDataStream.closeEntry();
      newAppDataStream.close();
    }
    if (newUserDataStream != null) {
      newUserDataStream.flush();
      newUserDataStream.closeEntry();
      newUserDataStream.close();
    }
  }

  /**
   * 
   * @return true if any AppData blocks have to be updated in session Jar
   */
  protected boolean isModified() {
    // LATER differentiate between core xml modification and Jar Entry
    // modification.
    if ((newAppData != null && newAppData.sessionFile.exists())
        || (newUserData != null && newUserData.sessionFile.exists()))
      return true;
    return false;
  }

  /*
   * (non-Javadoc)
   * 
   * @see java.lang.Object#finalize()
   */
  protected void finalize() throws Throwable {
    if (newAppDataStream != null) {
      newAppDataStream = null;
    }
    if (newAppDataStream != null) {
      newUserDataStream = null;
    }
    if (newAppData != null) {
      newAppData.eraseExistence();
      newAppData = null;
    }
    if (newUserData != null) {
      newUserData.eraseExistence();
      newUserData = null;
    }
    super.finalize();
  }

}
