/*
 ************************************************************************
 *
 * Copyright (c) 2003-2004, C&C Research Laboratories, NEC Europe Ltd.
 *
 * Copyright in this software belongs to C&C Research Laboratories,
 * Rathausallee 10, 53757 Sankt Augustin, Germany.
 *
 * This software may not be used, sold, licensed, transferred, copied
 * or reproduced in whole or in part in any manner or form or in or
 * on any media by any person other than in accordance with the terms
 * of the Licence Agreement supplied with the software, or otherwise
 * without the prior written consent of the copyright owners.
 *
 * This software is distributed WITHOUT ANY WARRANTY, without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 * PURPOSE, except where stated in the Licence Agreement supplied with
 * the software.
 *
 *  Created By :           G.A. Kohring
 *  Created for Project :  GEMSS (IST-2001-37153)
 *
 ************************************************************************
 */

package de.nece.ccrle.organizer;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.List;
import java.util.TimerTask;
import java.util.Timer;
import java.util.Vector;
import java.util.logging.Logger;

/**
 * A repository of objects implemented a directory of flat files residing
 * on a local disk.
 *
 * @author Greg Kohring
 */
abstract public class LocalDiskRepository implements Repository {

    private File directory = null;
    private File[] entries = null;

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


    /**
     * A <code>Timer</code> used for controlling the self-monitoring of this
     * this repository. If a subclass doe not need monitoring it can be
     * turned off using the {@link java.util.Timer}'s <code>cancel</code> 
     * method.
     */
    protected Timer timer = null;
    // Period between checks for changes to this repository
    private long period = 6000;        
    private TimerTask monitorTask = null;        

    /**
     * Constructs a new repository using the specified directory name.
     * If the directory pointed to by the specified directory name does 
     * not exist it will be created.
     * 
     * @param directoryName a <code>String</code> specifying the name of a
     *      directory to be used for the repository.
     * @param monitor a <code>boolean</code> variable indicatind whether or
     *      not file change monitoring should be turned on for this 
     *      repository.
     * @throws IOException if an I/O error occurs while opening or
     *      creating the repository.
     */
    public LocalDiskRepository( String directoryName , boolean monitor) 
                throws FileNotFoundException {
        this( new File( directoryName ), monitor );
    }

    /**
     * Constructs a new repository using the specified directory.
     * If the specified directory does not exist it will be created.
     * 
     * @param directory a <code>File</code> specifying the
     *      directory to be used for the repository.
     * @param monitor a <code>boolean</code> variable indicatind whether or
     *      not file change monitoring should be turned on for this 
     *      repository.
     * @throws IOException if an I/O error occurs while opening or
     *      creating the repository.
     */
    public LocalDiskRepository( final File directory , final boolean monitor) 
                        throws FileNotFoundException {
        this.directory = directory;

        checkDirectory( directory );
        initialize();

        timer = new Timer( true );
        if ( monitor ){
            monitorTask = new Monitor( entries );
        	timer.schedule( monitorTask, 0L, period );
        }
    }

    /** 
     * Initializes the repository by creating the list of repository 
     * entries.  If the repository changes, subclasses should call this 
     * method to refresh the list.
     */
    protected void initialize(){
        entries = listing();
    }

    /**
     * Returns a listing of the entries in this disk repository. The default
     * behavior returns a listing of all entries in the directory.
     * Subclasses may override this method to list only those files of
     * interest.
     *
     * @return an array of <code>File</code> objects containing the contents
     *      of the respository.
     */
    protected File[] listing() {
        return directory.listFiles();
    }

    // Make sure input parameter really is a directory, that it does exists
    // and that we can read and write from it.
    private void checkDirectory( File dir ) throws FileNotFoundException {
        if ( dir == null ){
            throw new FileNotFoundException( "repository location is null!" );
        }

        if ( !dir.exists() ){
            if ( !dir.mkdirs() ){
                throw new FileNotFoundException (
                        "Cannot create repository:  " + dir.toString() );
            }
        }

        if ( !dir.isDirectory() ){
            throw new FileNotFoundException( directory.toString() +
                        " is not a directory!" );
        }

        if ( !dir.canRead() ){
            throw new FileNotFoundException( directory.toString() +
                        " not readable!" );
        }

        if ( !dir.canWrite() ){
            throw new FileNotFoundException( directory.toString() +
                        " not writeable!" );
        }

    }

    public File getDirectory() {
        return directory;
    }

    public Iterator iterator(){
        return new Iterator(){
            private int index = 0;
            public boolean hasNext(){
                if ( index < entries.length ){
                    return true;
                } else {
                    return false;
                }
            }
            public Object next(){
                return getItem( entries[index++] );
            }
            public void remove(){
                throw new UnsupportedOperationException();
            }
        };
    }

    public Object[] getKeys() {
        return entries;
    }

    abstract public Object getItem( Object key );

    abstract public boolean add( Object key, Object item );

    public boolean remove( Object key ) throws IOException {
        if ( key instanceof String ){
            String fileName = (String) key;
            File file = new File( directory, fileName );
            if ( file.exists() ){
                return file.delete();
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

    public int size() {
        return entries.length;
    }

    public boolean isEmpty() {
        return entries == null ? true : entries.length > 0 ? false : true ;
    }

    public boolean equals( Object o ) {
        if ( o instanceof LocalDiskRepository ){
            LocalDiskRepository ldr = (LocalDiskRepository) o;
            File ldrDir = ldr.getDirectory();
            if ( directory.equals( ldrDir ) ){
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

    public int hashCode() {
        return directory.hashCode();
    }

    /**
     * Retrieves the period between monitoring events.
     *
     * @return a long denoting the period in <em>miliseconds</em>, 
     *      between monitoring events.
     */
    public long getPeriod(){
            return period;
        }

    /**
     * Sets the period between monitoring events.
     *
     * @param period the number of <em>miliseconds</em>, 
     *      between monitoring events.
     */
    public void setPeriod( long period ){
            this.period = period;
            monitorTask.cancel();
            try{
                monitorTask = new Monitor( entries );
            } catch ( FileNotFoundException fnfe ) {
                // should never happen...
                logger.info( fnfe.getMessage() );
            }
        	timer.schedule( monitorTask, 0L, period );
        }

    /**
     * This method will be called when there are any changes to 
     * the file or directory being monitored.
     *
     * @param files a <code>List</code> of <code>File</code> objects 
     *      denoting files which have changed during
     *      the last monitoring period.
     */
    protected abstract void changed( List files );

    /**
     * This method will be called when there are any additions to 
     * the directory being monitored.
     *
     * @param files a <code>List</code> of <code>File</code> objects 
     *      denoting files which have been added during
     *      the last monitoring period.
     */
    protected abstract void added( List files );

    /**
     * This method will be called when there are any deletions from 
     * the directory being monitored.
     *
     * @param files a <code>List</code> of <code>File</code> objects 
     *      denoting files which have been deleted during
     *      the last monitoring period.
     */
    protected abstract void deleted( List files );

    /**
     * Adds the specified listener to the list of listeners this monitor
     * will notify if there are any changes.
     *
     * @param fcl the <code>FileChangeListener</code> to be notified.
     */
    public abstract void addListener( RepositoryChangeListener fcl );

    /**
     * Removes the specified listener from the list of listeners this monitor
     * notifies in the event of any changes.
     *
     * @param fcl the <code>FileChangeListener</code> to be removed.
     */
    public abstract void removeListener(  RepositoryChangeListener fcl );


    /**
     * Monitor files and directories for any changes by external entities.
     */
    private class Monitor extends TimerTask {

        private Map lastModified = null;
        private boolean running = false;
        private Vector changed = null;
        private Vector added   = null;
        private Vector deleted = null;

        /**
         * Construct a monitor for this repository
         *
         * @throws FileNotFoundException if the file denoted by the specified
         *      abstract pathname does not exist.
         */
        public Monitor( File[] files ) throws FileNotFoundException {

            lastModified = Collections.synchronizedMap( new HashMap() );

            changed = new Vector();
            added   = new Vector();
            deleted = new Vector();

            // initialize the table of last modified values

            for ( int f = 0; f < files.length; f++ ){
                File file = files[f];
                Long lastMod = new Long( file.lastModified() );
                lastModified.put( file, lastMod );
            }
        }


        // Generates a list of all files in the specified abstract pathname.
        // If the specified abstract pathname is a directory, then the 
        // directory will be searched recursively.

        private void listAllFiles( List fileList ){
            File[] list = listing();
            for ( int f = 0; f < list.length; f++){
                    fileList.add( list[f] );
            }
        }

        /**
         * Executes a single monitoring event. All the files in the range of 
         * this monitor will checked for changes, additions and deletions.
         * If any modifications are found, any listeners which happen to 
         * be listening are called.
         *
         */
        public void run() {

            // Get the current list of files 

            File[] current = listing();

            // Create new lists of modified entries

            changed.clear();
            added.clear();
            deleted.clear();

            // Check for files changed or added.

            for ( int f = 0; f < current.length; f++ ){
                File file = (File) current[f];
                Long last = (Long) lastModified.get( file );
                if ( last != null ){
                    long lm = last.longValue();

                    Long newMod = new Long( file.lastModified() );
                    long newModVal = newMod.longValue();

                    if ( lm != newModVal ){
                        lastModified.put( file, newMod );
                        changed.add( file );
                    }
                } else {
                    Long newMod = new Long( file.lastModified() );
                    lastModified.put( file, newMod );
                    added.add( file );
                }
            }

            // Check for files deleted.

            synchronized( lastModified ){
                Iterator it = lastModified.keySet().iterator();
                while( it.hasNext() ){
                    File file = (File) it.next();
                    boolean found = false;
                    for ( int f = 0; f < current.length; f++ ){
                        if ( current[f].equals( file ) ){
                            found = true;
                            break;
                        }
                    }
                    if ( !found ){
                        deleted.add( file );
                        it.remove();
                    }
                }
            }

            // if there are any changes, notify all listeners

            if ( !changed.isEmpty() ) changed( changed );
            if ( !added.isEmpty() )   added( added );
            if ( !deleted.isEmpty() ) deleted( deleted );

        }
    }

}
