/*  Copyright (C) 2003-2011 JabRef contributors.
    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program 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 General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
package net.sf.jabref.collab;

import net.sf.jabref.Globals;
import net.sf.jabref.Util;

import java.util.HashMap;
import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.logging.Logger;

/**
 * This thread monitors a set of files, each associated with a FileUpdateListener, for changes
* in the file's last modification time stamp. The
 */
public class FileUpdateMonitor extends Thread {

	private static Logger logger = Logger.getLogger(FileUpdateMonitor.class.getName());

  final int WAIT = 4000;
  static int tmpNum = 0;
  int no = 0;
  HashMap<String, Entry> entries = new HashMap<String, Entry>();
  boolean running;

  public FileUpdateMonitor() {
    setPriority(MIN_PRIORITY);
  }

  public void run() {
    running = true;

    // The running variable is used to make the thread stop when needed.
    while (running) {
      //System.out.println("Polling...");
      Iterator<String> i = entries.keySet().iterator();
      for (;i.hasNext();) {
        Entry e = entries.get(i.next());
        try {
          if (e.hasBeenUpdated())
            e.notifyListener();

          //else
          //System.out.println("File '"+e.file.getPath()+"' not modified.");
        } catch (IOException ex) {
          e.notifyFileRemoved();
        }
      }

      // Sleep for a while before starting a new polling round.
      try {
        sleep(WAIT);
      } catch (InterruptedException ex) {
    	  logger.finest("FileUpdateMonitor has been interrupted.");
    	  /*  the (?) correct way to interrupt threads, according to
    	   *  http://www.roseindia.net/javatutorials/shutting_down_threads_cleanly.shtml
    	   */
    	  Thread.currentThread().interrupt(); // very important
    	  break;
      }
    }
  }

  /**
   * Cause the thread to stop monitoring. It will finish the current round before stopping.
   */
  public void stopMonitoring() {
    running = false;
  }

  /**
   * Add a new file to monitor. Returns a handle for accessing the entry.
   * @param ul FileUpdateListener The listener to notify when the file changes.
   * @param file File The file to monitor.
   * @throws IOException if the file does not exist.
   */
  public String addUpdateListener(FileUpdateListener ul, File file) throws IOException {
     // System.out.println(file.getPath());
    if (!file.exists())
      throw new IOException("File not found");
    no++;
    String key = ""+no;
    entries.put(key, new Entry(ul, file));
    return key;
  }

    /**
     * Forces a check on the file, and returns the result. Does not
     * force a report to all listeners before the next routine check.
     */
    public boolean hasBeenModified(String handle) throws IllegalArgumentException {
	Object o = entries.get(handle);
	if (o == null)
            return false;
        //	    throw new IllegalArgumentException("Entry not found");
	try {
	    return ((Entry)o).hasBeenUpdated();
	} catch (IOException ex) {
	    // Thrown if file has been removed. We return false.
	    return false;
	}
    }

    /**
     * Change the stored timestamp for the given file. If the timestamp equals
     * the file's timestamp on disk, after this call the file will appear to
     * have been modified. Used if a file has been modified, and the change
     * scan fails, in order to ensure successive checks.
     * @param handle the handle to the correct file.
     */
    public void perturbTimestamp(String handle) {
        Object o = entries.get(handle);
        if (o == null)
            return;
        ((Entry)o).timeStamp--;
    }

  /**
   * Removes a listener from the monitor.
   * @param handle String The handle for the listener to remove.
   */
  public void removeUpdateListener(String handle) {
    entries.remove(handle);
  }

  public void updateTimeStamp(String key) throws IllegalArgumentException {
    Object o = entries.get(key);
    if (o == null)
      throw new IllegalArgumentException("Entry not found");
    Entry entry = (Entry)o;
    entry.updateTimeStamp();

  }

  public void changeFile(String key, File file) throws IOException, IllegalArgumentException {
    if (!file.exists())
      throw new IOException("File not found");
    Object o = entries.get(key);
    if (o == null)
      throw new IllegalArgumentException("Entry not found");
    ((Entry)o).file = file;
  }

  /**
   * Method for getting the temporary file used for this database. The tempfile
   * is used for comparison with the changed on-disk version.
   * @param key String The handle for this monitor.
   * @throws IllegalArgumentException If the handle doesn't correspond to an entry.
   * @return File The temporary file.
   */
  public File getTempFile(String key) throws IllegalArgumentException {
    Object o = entries.get(key);
    if (o == null)
      throw new IllegalArgumentException("Entry not found");
    return ((Entry)o).tmpFile;
  }

  /**
   * A class containing the File, the FileUpdateListener and the current time stamp for one file.
   */
  class Entry {
    FileUpdateListener listener;
    File file;
    File tmpFile;
    long timeStamp, fileSize;

    public Entry(FileUpdateListener ul, File f) {
      listener = ul;
      file = f;
      timeStamp = file.lastModified();
      fileSize = file.length();
      tmpFile = getTempFile();
      tmpFile.deleteOnExit();
      copy();
    }

    /**
     * Check if time stamp or the file size has changed.
     * @throws IOException if the file does no longer exist.
     * @return boolean true if the file has changed.
     */
    public boolean hasBeenUpdated() throws IOException {
      long modified = file.lastModified();
      long fileSizeNow = file.length();
      if (modified == 0L)
        throw new IOException("File deleted");
      return timeStamp != modified || fileSize != fileSizeNow;
    }

    public void updateTimeStamp() {
      timeStamp = file.lastModified();
      if (timeStamp == 0L)
        notifyFileRemoved();
      fileSize = file.length();

      copy();
    }

    public boolean copy() {
	
	//Util.pr("<copy file=\""+tmpFile.getPath()+"\">");
      boolean res = false;
      try {
        res = Util.copyFile(file, tmpFile, true);
      } catch (IOException ex) {
        Globals.logger("Cannot copy to temporary file '"+tmpFile.getPath()+"'");
      }
      //Util.pr("</copy>");
      return res;
	
      //return true;
    }

    /**
     * Call the listener method to signal that the file has changed.
     */
    public void notifyListener() {
      // Update time stamp.
      timeStamp = file.lastModified();
      fileSize = file.length();
      listener.fileUpdated();
    }

    /**
     * Call the listener method to signal that the file has been removed.
     */
    public void notifyFileRemoved() {
      listener.fileRemoved();
    }

    /*public void finalize() {
      try {
        tmpFile.delete();
      } catch (Throwable e) {
        Globals.logger("Cannot delete temporary file '"+tmpFile.getPath()+"'");
      }
    }*/
  }

  static synchronized File getTempFile() {
    File f = null;
    // Globals.prefs.get("tempDir")
    //while ((f = File.createTempFile("jabref"+(tmpNum++), null)).exists());
    try {
	    f = File.createTempFile("jabref", null);
        f.deleteOnExit();
	//System.out.println(f.getPath());
    } catch (IOException ex) {
	ex.printStackTrace();
    }
    return f;
  }
}
