/*
 * 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.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.FileChannel;
import java.nio.channels.ReadableByteChannel;

/**
 * File based Locking mechanism to get around some bizarre limitations of
 * JarEntry seeking. Abstract locks have a target file, to which access is
 * controlled when a lock is held. Native locks on WindowsXP seem to conflict
 * with Jar seek operations, so a file lock creates an advisory lock. Method: A
 * lock file is created, if it doesn't already exist - the naming convention is
 * TargetFile+suffixSeparator+_LockSuffix. A lock is obtained by locking the
 * lock file with a native lock. The NativeLock is used for this.
 * 
 * @author JimP
 * 
 */
public class FileLock extends Lock {
  private File _lock = null;

  protected static String _LockSuffix = "lck";

  private NativeLock advisory = null;

  /**
   * ensure that the _lock file exists and create a lock
   */
  private boolean ensureLockFile(boolean block) {
    if (_lock == null)
      return false;
    if (advisory != null && advisory.isLocked())
      return true;
    try {
      advisory = new NativeLock(_lock, block);
    } catch (Exception e) {
      if (!_lock.exists()) {
        // advisory cannot be created. this is serious.
        log.fatal("Failed to create advisory lock file " + _lock, e);
        throw new Error("Failed to create advisory lock file " + _lock);
      }
    }
    return (advisory != null) && advisory.isLocked();
  }

  /**
   * call to clear up a filelock file after its been made
   * 
   */
  private void tidy() {
    if (_lock != null) {
      if (advisory != null) {
        File tgt = advisory.target;
        // TODO: fix occasional exceptions raised here (usually on JVM shutdown)
        if (tgt != null) {
          try {
            tgt.deleteOnExit(); // release will null the target
          } catch (NullPointerException e)
          {
            // ignore - TODO: fix nulls 
          }
        }
        advisory.release(true);
      }
      advisory = null;
      _lock = null;
    }
  }

  /**
   * @param lockfile
   * @param block
   *          true means thread blocks until FileLock is obtained.
   */
  public FileLock(File lockfile, boolean block) {
    super(lockfile);
    // try and get a lock.
    try {
      _lock = make_lockForTarget(lockfile);
      if (!ensureLockFile(block)) {
        log.debug("Couldn't get lock on " + _lock);
        tidy();
        return;
      }
      // create target file ready to be written to if necessary.
      if (!lockfile.exists())
        if (!lockfile.createNewFile()) {
          log.warn("Failed to create locked file " + lockfile);
          return;
        }
      // openRaFile();
    } catch (FileNotFoundException e) {
      //
      log.debug("FileLock failed with target=" + lockfile
          + " and lockfile suffix of " + _LockSuffix);
      // log.error("Error! Couldn't create a lockfile at "
      // + lockfile.getAbsolutePath(), e);
    } catch (IOException e) {
      log.error("Error! Problems with IO when creating a lock on "
          + lockfile.getAbsolutePath(), e);
    }
  }

  /**
   * 
   * @param lockfile
   *          target of lock
   * @return file object that will be locked for lockfile
   */
  private File make_lockForTarget(File lockfile) {
    return new File(lockfile.getParentFile(), lockfile.getName() + "."
        + _LockSuffix);
  }

  private boolean openRaFile() throws IOException {
    if (target == null)
      return false;
    if (advisory == null || !advisory.isLocked())
      return false;
    if (rafile == null || rafile.getFD() == null || !rafile.getFD().valid()) {
      rafile = new RandomAccessFile(target, "rw");
    } else {
      if (log.isDebugEnabled())
        log.debug("Reusing existing RandomAccessFile on " + target);
    }
    return (rafile.getChannel() != null) && rafile.getChannel().isOpen();
  }

  public boolean isLocked() {
    if (advisory != null) {
      if (advisory.isLocked())
        return true;
      advisory = null;
      if (log.isDebugEnabled())
        log.debug("Lockfile " + _lock + " unexpectedly deleted ?");
    }
    return false;
  }

  public void release() {
    release(true);
  }

  public void release(boolean closeChannel) {
    if (!isLocked())
      return;
    if (rafile != null) {
      if (closeChannel) {
        try {
          rafile.close();
        } catch (Exception e) {
          log.debug("Unexpected exception whilst closing RandomAccessFile on "
              + target, e);
        }
        rafile = null; // do not hold reference to rafile anymore either
      }
      if (log.isDebugEnabled())
        log.debug("Releasing advisory lock on " + target);
      // TODO: LATER: verify this change in closeChannel semantics really is
      // correct - ArchiveClient works correctly
    }
    tidy();
  }

  public FileInputStream getFileInputStream(boolean atStart) throws IOException {
    if (!isLocked()) {
      log.debug("Don't hold lock on " + target
          + " to get locked FileInputStream.");
      return null;
    }
    openRaFile();
    if (atStart)
      rafile.seek(0);
    return new FileInputStream(rafile.getFD());
  }

  public FileOutputStream getFileOutputStream(boolean clear) throws IOException {
    if (!isLocked()) {
      log.debug("Don't hold lock on " + target
          + " to get locked FileOutputStream.");
      return null;
    }
    openRaFile();
    if (clear) {
      rafile.seek(0);
      rafile.setLength(0);
    } else
      rafile.seek(rafile.length());
    return new LockedFileOutputStream(rafile.getFD());
  }

  public BufferedOutputStream getBufferedOutputStream(boolean clear)
      throws IOException {
    log.debug("Getting BufferedOutputStream (clear=" + clear + ")");
    FileOutputStream fos = getFileOutputStream(clear);
    if (fos != null)
      return new BufferedOutputStream(fos);
    return null;
  }

  /*
   * (non-Javadoc)
   * 
   * @see uk.ac.vamsas.client.simpleclient.Lock#getLength()
   */
  public long length() {
    if (isLocked()) {
      if (!target.exists()) {
        try {
          target.createNewFile();
        } catch (Exception e) {
          log.error("Invalid lock:Failed to create target file " + target);
          tidy();
          return -1;
        }
        return 0;
      }
      return target.length();
    }
    return -1;
  }

  protected void finalize() throws Throwable {
    release(true); // we explicitly lose the lock here.
    super.finalize();
  }

  public RandomAccessFile getRaFile() throws IOException {
    if (isLocked() && openRaFile()) {
      return rafile;
    }
    log.debug("Failed to getRaFile on target " + target);
    return null;
  }

  public FileChannel getRaChannel() throws IOException {
    if (isLocked() && openRaFile()) {
      return rafile.getChannel();
    }
    log.debug("Failed to getRaChannel on target " + target);
    return null;
  }

  public boolean isTargetLockFile(File afile) {
    if (isLocked()) {
      if (target.equals(afile) || make_lockForTarget(target).equals(afile))
        return true;
    }
    return false;
  }
}
