/*
 * Copyright (c) 2009, Takeyuki Nagao
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or
 * without modification, are permitted provided that the
 * following conditions are met:
 * 
 *  * Redistributions of source code must retain the above
 *    copyright notice, this list of conditions and the
 *    following disclaimer.
 *  * Redistributions in binary form must reproduce the above
 *    copyright notice, this list of conditions and the
 *    following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *    
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
 * OF SUCH DAMAGE.
 */

package dvi.browser.util;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;

import dvi.event.TDefaultEventModel;
import dvi.event.TEvent;
import dvi.event.TEventModel;
import dvi.event.TEventProcessor;

// TODO: Rewrite the event model using AWT EventListener instead of TEventModel.
public class FileWatch
{
  private static final Logger LOGGER = Logger.getLogger(FileWatch.class.getName());
  private static final FileWatch instance;
  private static final ScheduledExecutorService exe;
  private FileWatch() { }

	public static FileWatch getInstance() { return instance; }
	
	private static class Worker
	implements Runnable
	{
    public void run() {
      try {
        LOGGER.finer("Checking file modification.");
        FileWatch instance = FileWatch.getInstance();
        instance.checkTargets();
      } catch (Exception ex) {
        LOGGER.warning(ex.toString());
      }
    }
	}

	static {
	  instance = new FileWatch();
	  exe = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
      public Thread newThread(Runnable r)
      {
        Thread t = new Thread(r);
        t.setDaemon(true);
        return t;
      }
	  });
	  // TODO: outsource the delay setting.
	  exe.scheduleWithFixedDelay
	    (new Worker(), 0, 500, TimeUnit.MILLISECONDS);
    LOGGER.config("Started FileWatch worker.");
	}

	private static final Set<Target>
	  targets = new HashSet<Target>();

	public synchronized void addTarget(Target target)
	throws IOException
	{
	  if (target == null) return;
	  targets.add(target);
	}
	
	public synchronized void removeTarget(Target target)
	throws IOException
	{
	  if (target == null) return;
    targets.remove(target);
	}

	protected void checkTargets()
	{
		for (Target target : list()) {
		  try {
			  target.check();
			} catch (Exception ex) {
			  LOGGER.warning(ex.toString());
			}
		}
	}

	public synchronized List<Target> list()
	{
	  // We return a defensive copy.
	  ArrayList<Target> list = new ArrayList<Target>();
	  list.addAll(targets);
		return list;
	}

	@SuppressWarnings("unused")
	public static class Target
	implements TEventProcessor
	{
	  private final TEventModel em = new TDefaultEventModel();
	  public TEventModel getEventModel() { return em; }

	  private final long startTimestamp;
	  private final long startTime;
	  private long lastTimestamp;
	  private long lastChecked;
	  private boolean lastExists;
	  private int count = 0;
	  private boolean initial = true;

	  private final File file;
	  public Target(File file)
		throws IOException
		{
		  this.file = canonicalize(file);
			if (this.file == null)
			  throw new IllegalArgumentException
				  ("invalid file: " + file.getPath());
			lastChecked = startTime = System.currentTimeMillis();
			lastTimestamp = startTimestamp = file.lastModified();
			lastExists = file.exists();
		}

		public File file()
		{
		  return file;
		}

	  protected File canonicalize(File file)
	  throws IOException
	  {
	    return new File(file.getCanonicalPath());
	  }

		protected void check() {
	    try {
			  boolean exists = file.exists();
				if (lastExists && !exists) {
				  handleRemoval();
			    lastExists = exists;
				} else if (!lastExists && exists) {
				  handleCreation();
			    lastExists = exists;
				}
				{
  			  long ts = file.lastModified();
  			  if (ts != lastTimestamp) {
  			    if (isContentReady(file)) {
  				    lastTimestamp = ts;
  				    count++;
  					  handleModification();
  			    } else {
  			      // The file is under modification.
  			      // We postpone the modification event until it finishes.
  			    }
  			  }
  			  lastChecked = System.currentTimeMillis();
  			  initial = false;
				}
		  } catch (Exception ex) {
		    LOGGER.warning(ex.toString());
		  }
		}
		
		protected boolean isContentReady(File file)
		{
		  return true;
		}

		protected void handleModification()
		{
		  try {
				getEventModel().processEvent
				  (new Modified(this));
			} catch (Exception ex) {
        LOGGER.warning(ex.toString());
			}
		}

		protected void handleRemoval()
		{
		  try {
				getEventModel().processEvent
				  (new Removed(this));
      } catch (Exception ex) {
        LOGGER.warning(ex.toString());
			}
		}

		protected void handleCreation()
		{
		  try {
				getEventModel().processEvent
				  (new Created(this));
      } catch (Exception ex) {
        LOGGER.warning(ex.toString());
			}
		}

		public String toString()
		{
		  return getClass().getName()
			  + "[file=" + file.getPath()
				+ " timestamp=" + lastTimestamp
				+ " count=" + count
				+ "]";
		}
	}

	public static class Modified extends TEvent {
		private static final long serialVersionUID = -740289683404400167L;
		public Modified(Object source) { super(source); }
	}
	public static class Created extends TEvent {
		private static final long serialVersionUID = 1617336492784766920L;
		public Created(Object source) { super(source); }
	}
	public static class Removed extends TEvent {
		private static final long serialVersionUID = -5052791614994332506L;
		public Removed(Object source) { super(source); }
	}
}
