/*
 * 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.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.ReadableByteChannel;

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

/**
 * Basic methods for classes handling locked IO on files monitored by all
 * (simpleclient) clients in a vamsas session.
 * 
 * @author jimp TODO: support non nio file locking capable systems
 */
public class SessionFile {
  private static Log log = LogFactory.getLog(SessionFile.class);

  protected File sessionFile;

  protected Lock fileLock = null;

  protected SessionFile(File file) {
    super();
    sessionFile = file;
  }

  protected boolean lockFile(Lock extantlock) {
    if (fileLock != null && !fileLock.isLocked()) {
      fileLock.release();// tidy up invalid lock
      fileLock = null;
    }
    if (extantlock != null && extantlock.isLocked())
      fileLock = extantlock; // THIS IS BROKEN - lockFile then nulls the lock.
    return lockFile();
  }

  private boolean ensureSessionFile() {
    if (sessionFile != null) {
      if (!sessionFile.exists()) {
        // create new file
        try {
          if (!sessionFile.createNewFile()) {
            log.error("Failed to create file prior to locking: " + sessionFile);
            return false;
          }
        } catch (IOException e) {
          log.error("Exception when trying to create file " + sessionFile, e);
          return false;
        }
      }
      return true;
    }
    log.error("ensureSessionFile called for non-initialised SessionFile!");
    return false;
  }

  /**
   * Get a lock for the SessionFile
   * 
   * @return true if lock was made
   */
  protected boolean lockFile() {
    if (fileLock != null) {
      if (fileLock.isLocked()) {
        if (!ensureSessionFile())
          return false;
        return true;
      } else {
        // lock failed for some reason.
        fileLock.release();
        log
            .info("Unexpected session file lock failure. Trying to get it again.");
        fileLock = null;
      }
    }
    if (!ensureSessionFile())
      return false;
    // TODO: see if we need to loop-wait for locks or they just block until
    // lock is made...
    long tries = 5000;
    do {
      tries--;
      if (fileLock == null || !fileLock.isLocked()) {
        // try { Thread.sleep(1); } catch (Exception e) {};
        fileLock = LockFactory.getLock(sessionFile, true); // TODO: wait around
                                                           // if we can't get
                                                           // the lock.
      }
    } while (tries > 0 && !fileLock.isLocked());
    if (!fileLock.isLocked())
      log.error("Failed to get lock for " + sessionFile);
    // fileLock = new Lock(sessionFile);
    return fileLock.isLocked();
  }

  /**
   * Explicitly release the SessionFile's lock.
   * 
   * @return true if lock was released.
   */
  protected void unlockFile() {
    if (fileLock != null) {
      fileLock.release();
      fileLock = null;
    }
  }

  /**
   * Makes a backup of the sessionFile.
   * 
   * @return Backed up SessionFile or null if failed to make backup.
   */
  protected File backupSessionFile() {
    return backupSessionFile(fileLock, sessionFile.getName(), ".old",
        sessionFile.getParentFile());
  }

  protected File backupSessionFile(Lock extantLock, String backupPrefix,
      String backupSuffix, File backupDir) {
    File tempfile = null;
    if (lockFile(extantLock)) {
      try {
        tempfile = File.createTempFile(backupPrefix, backupSuffix, backupDir);
        long sourceln = fileLock.length();
        if (sourceln > 0) {
          FileOutputStream tos = new FileOutputStream(tempfile);
          ReadableByteChannel channel;
          channel = fileLock.getRaChannel().position(0);
          long ntrans = 0;
          while (ntrans < sourceln) {
            long tlen;
            ntrans += tlen = tos.getChannel().transferFrom(channel, ntrans,
                sourceln);
            if (log.isDebugEnabled()) {
              log.debug("Transferred " + tlen + " out of " + sourceln
                  + " bytes");
            }
          }
          tos.close();
          if (!channel.isOpen())
            throw new Error("LIBRARY PORTABILITY ISSUE: "
                + tos.getChannel().getClass()
                + ".transferFrom closes source channel!");
          if (!lockFile(extantLock))
            throw new Error("LIBRARY PORTABILITY ISSUE: Lost lock for "
                + sessionFile.getName() + " after backup.");

        }
      } catch (FileNotFoundException e1) {
        log.warn("Can't create temp file for " + sessionFile.getName(), e1);
        tempfile = null;
      } catch (IOException e1) {
        log.warn("Error when copying content to temp file for "
            + sessionFile.getName(), e1);
        tempfile = null;
      }
    }
    return tempfile;
  }

  /**
   * Replaces data in sessionFile with data from file handled by another
   * sessionFile passes up any exceptions.
   * 
   * @param newData
   *          source for new data
   */
  protected void updateFrom(Lock extantLock, SessionFile newData)
      throws IOException {
    log.debug("Updating " + sessionFile.getAbsolutePath() + " from "
        + newData.sessionFile.getAbsolutePath());
    if (newData == null)
      throw new IOException("Null newData object.");
    if (newData.sessionFile == null)
      throw new IOException("Null SessionFile in newData.");

    if (!lockFile(extantLock))
      throw new IOException("Failed to get write lock for " + sessionFile);
    if (!newData.lockFile())
      throw new IOException("Failed to get lock for updateFrom "
          + newData.sessionFile);
    RandomAccessFile nrafile = newData.fileLock.getRaFile();
    nrafile.seek(0);
    RandomAccessFile trafile = fileLock.getRaFile();
    /*
     * long tries=5000; while (trafile==null && --tries>0) {
     * log.debug("Lost lock on "+sessionFile+"! Re-trying for a transfer.");
     * lockFile(); trafile = fileLock.getRaFile(); }
     */
    // TODO JBPNote: attempt to ensure save really saves the VamDoc.jar file
    trafile.seek(0);
    trafile.getChannel()
        .transferFrom(nrafile.getChannel(), 0, nrafile.length());
    // JBPNote: attempt to close the streams to flush the data out
    // trafile.close();
    // nrafile.close();
  }

  /**
   * remove all trace of the sessionFile file
   * 
   */
  protected void eraseExistence() {
    unlockFile();
    if (sessionFile != null) {
      sessionFile.delete();
      sessionFile = null;
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.simpleclient.Lock#getBufferedInputStream(boolean)
   */
  public BufferedInputStream getBufferedInputStream(boolean atStart)
      throws IOException {
    lockFile();
    return fileLock.getBufferedInputStream(atStart);
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.simpleclient.Lock#getBufferedOutputStream(boolean)
   */
  public BufferedOutputStream getBufferedOutputStream(boolean clear)
      throws IOException {
    lockFile();
    return fileLock.getBufferedOutputStream(clear);
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.simpleclient.Lock#getFileInputStream(boolean)
   */
  public FileInputStream getFileInputStream(boolean atStart) throws IOException {
    lockFile();
    return fileLock.getFileInputStream(atStart);
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.simpleclient.Lock#getFileOutputStream(boolean)
   */
  public FileOutputStream getFileOutputStream(boolean clear) throws IOException {
    lockFile();
    return fileLock.getFileOutputStream(clear);
  }

}
