/*
 *  PHEX - The pure-java Gnutella-servent.
 *  Copyright (C) 2001 - 2004 Gregor Koukkoullis ( phex <at> kouk <dot> de )
 *
 *  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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 *  --- CVS Information ---
 *  $Id: SwarmingManager.java,v 1.51 2004/09/03 23:25:16 gregork Exp $
 */
package phex.download.swarming;

import java.io.*;
import java.util.*;

import javax.xml.bind.*;

import phex.common.*;
import phex.common.bandwidth.BandwidthManager;
import phex.download.*;
import phex.event.*;
import phex.utils.*;
import phex.xml.*;


public class SwarmingManager implements Manager
{
    public static final short PRIORITY_MOVE_TO_TOP = 0;
    public static final short PRIORITY_MOVE_UP = 1;
    public static final short PRIORITY_MOVE_DOWN = 2;
    public static final short PRIORITY_MOVE_TO_BOTTOM = 3;

    private short workerCount;
    private ArrayList/*<SWDownloadFile>*/ downloadList;
    
    /**
     * A Map that maps URNs to download files they belong to. This is for
     * performant searching by urn.
     * When accesseing this object locking via the downloadList object is
     * required.
     */
    private HashMap/*<URN, SWDownloadFile>*/ urnToDownloadMap;

    
    private TransferRateService transferRateService;
    private IPCounter ipDownloadCounter;

    /**
     * The temporary worker holds the only worker that is used to check if more
     * workers are required. The temporary worker waits for a valid download set
     * once a valid set is found the worker loses its temporary status and a new
     * temporary worker will be created. This is used to only hold the necessary
     * number of workers.
     */
    private SWDownloadWorker temporaryWorker;

    /**
     * Lock object to lock saving of download lists.
     */
    private static Object saveDownloadListLock = new Object();

    /**
     * Object that holds the save job instance while a save job is running. The
     * reference is null if the job is not running.
     */
    private SaveDownloadListJob saveDownloadListJob;
    
    /**
     * Indicates if the download list has changed since the last time it was
     * saved.
     */
    private boolean downloadListChangedSinceSave;

    private static SwarmingManager instance;

    private SwarmingManager()
    {
        downloadListChangedSinceSave = false;
    }

    public static SwarmingManager getInstance()
    {
        if ( instance == null )
        {
            instance = new SwarmingManager();
        }
        return instance;
    }

    /**
     * This method is called in order to initialize the manager.  Inside
     * this method you can't rely on the availability of other managers.
     * @return true if initialization was successful, false otherwise.
     */
    public boolean initialize()
    {
        workerCount = 0;
        downloadList = new ArrayList( 5 );
        urnToDownloadMap = new HashMap();
        transferRateService = BandwidthManager.getInstance().getTransferRateService();
        ipDownloadCounter = new IPCounter( ServiceManager.sCfg.maxDownloadsPerIP );
        return true;
    }

    /**
     * This method is called in order to perform post initialization of the
     * manager. This method includes all tasks that must be done after initializing
     * all the several managers. Inside this method you can rely on the
     * availability of other managers.
     * @return true if initialization was successful, false otherwise.
     */
    public boolean onPostInitialization()
    {
        LoadDownloadListJob job = new LoadDownloadListJob();
        job.start();
        return true;
    }
    
    /**
     * This method is called after the complete application including GUI completed
     * its startup process. This notification must be used to activate runtime
     * processes that needs to be performed once the application has successfully
     * completed startup.
     */
    public void startupCompletedNotify()
    {
        createRequiredWorker();
        Environment.getInstance().scheduleTimerTask(
            new SaveDownloadListTimer(), SaveDownloadListTimer.TIMER_PERIOD,
            SaveDownloadListTimer.TIMER_PERIOD );
    }

    /**
     * This method is called in order to cleanly shutdown the manager. It
     * should contain all cleanup operations to ensure a nice shutdown of Phex.
     */
    public void shutdown()
    {
        forceSaveDownloadList();
    }

    public synchronized SWDownloadFile addFileToDownload( RemoteFile remoteFile,
        String filename, String searchTerm )
    {
        SWDownloadFile downloadFile = new SWDownloadFile( filename,
            searchTerm, remoteFile.getFileSize(), remoteFile.getURN() );
        downloadFile.addDownloadCandidate( remoteFile );
        int pos;
        synchronized ( downloadList )
        {
            pos = downloadList.size();
            downloadList.add( pos, downloadFile );
            URN urn = downloadFile.getFileURN();
            if ( urn != null )
            {
                urnToDownloadMap.put( urn, downloadFile );
            }
        }
        fireDownloadFileAdded( pos );
        downloadFile.setStatus( SWDownloadConstants.STATUS_FILE_WAITING );
        createRequiredWorker();

        // save in xml
        triggerSaveDownloadList();

        return downloadFile;
    }

    /**
     * Removes the download file from the download list. Stops all running downlads
     * and deletes all incomplete download files.
     */
    public void removeDownloadFile( SWDownloadFile file )
    {
        file.stopDownload();
        int pos;
        synchronized( downloadList )
        {
            pos = downloadList.indexOf( file );
            if ( pos >= 0 )
            {
                downloadList.remove( pos );
                fireDownloadFileRemoved( pos );
            }
            URN urn = file.getFileURN();
            if ( urn != null )
            {
                urnToDownloadMap.remove( urn );
            }
        }
        file.removeDownloadSegmentFiles();

        // save in xml
        triggerSaveDownloadList();
    }

    /**
     * Removes the download files from the download list. Stops all running downlads
     * and deletes all incomplete download files.
     */
    public void removeDownloadFiles( SWDownloadFile[] files )
    {
        for ( int i = 0; i < files.length; i++ )
        {
            files[i].stopDownload();
            int pos;
            synchronized( downloadList )
            {
                pos = downloadList.indexOf( files[i] );
                if ( pos >= 0 )
                {
                    downloadList.remove( pos );
                    fireDownloadFileRemoved( pos );
                }
                URN urn = files[i].getFileURN();
                if ( urn != null )
                {
                    urnToDownloadMap.remove( urn );
                }
            }
            files[i].removeDownloadSegmentFiles();
        }

        // save in xml
        triggerSaveDownloadList();
    }

    public Integer getDownloadPriority( SWDownloadFile file )
    {
        int pos = downloadList.indexOf( file );
        if ( pos >= 0 )
        {
            return new Integer( pos );
        }
        else
        {
            return null;
        }
    }
    
    /**
     * Updates the priorities of the download files according to the order in 
     * the given download file array.
     * @param files the download file array.
     */
    public void updateDownloadFilePriorities( SWDownloadFile[] files )
    {
        synchronized( downloadList )
        {
            for ( int i = 0; i < files.length; i++ )
            {
                int pos = downloadList.indexOf( files[i] );
                if ( pos >= 0 )
                {
                    int newPos = i;
                    if ( newPos < 0 || newPos >= downloadList.size() )
                    {
                        newPos = pos;
                    }
                    downloadList.remove( pos );
                    downloadList.add( newPos, files[i] );
                    fireDownloadFileRemoved( pos );
                    fireDownloadFileAdded( newPos );
                }
            }
        }
    }

    /**
     * Moves the download file in the hierarchy.
     * 
     * @param moveDirection The move direction. Use one of the constants 
     *        PRIORITY_MOVE_TO_TOP, PRIORITY_MOVE_UP, PRIORITY_MOVE_DOWN or
     *        PRIORITY_MOVE_TO_BOTTOM.
     * @param file The SWDownloadFile to move the priority for.
     * @return the new position.
     */
    public int moveDownloadFilePriority( SWDownloadFile file, short moveDirection )
    {
        synchronized( downloadList )
        {
            int pos = downloadList.indexOf( file );
            if ( pos >= 0 )
            {
                int newPos = pos;
                switch ( moveDirection )
                {
                    case PRIORITY_MOVE_UP:
                        newPos --;
                        break;
                    case PRIORITY_MOVE_DOWN:
                        newPos ++;
                        break;
                    case PRIORITY_MOVE_TO_TOP:
                        newPos = 0;
                        break;
                    case PRIORITY_MOVE_TO_BOTTOM:
                        newPos = downloadList.size() - 1;
                        break;
                }
                if ( newPos < 0 || newPos >= downloadList.size() )
                {
                    return pos;
                }

                downloadList.remove( pos );
                downloadList.add( newPos, file );
                fireDownloadFileRemoved( pos );
                fireDownloadFileAdded( newPos );
                return newPos;
            }
            return pos;
        }
    }

    /**
     * Returns the count of the download files
     */
    public int getDownloadFileCount()
    {
        return downloadList.size();
    }

    /**
     * Returns the count of the download files with the given status.
     */
    public int getDownloadFileCount( int status )
    {
        int count = 0;
        synchronized( downloadList )
        {
            Iterator iterator = downloadList.iterator();
            while( iterator.hasNext() )
            {
                SWDownloadFile file = (SWDownloadFile)iterator.next();
                if ( file.getStatus() == status )
                {
                    count ++;
                }
            }
        }
        return count;
    }

    /**
     * Returns a download file at the given index or null if not available.
     */
    public SWDownloadFile getDownloadFile( int index )
    {
        synchronized( downloadList )
        {
            if ( index < 0 || index >= downloadList.size() )
            {
                return null;
            }
            return (SWDownloadFile) downloadList.get( index );
        }
    }

    /**
     * Returns all download files at the given indices. In case one of the
     * indices is out of bounds the returned download file array contains a 
     * null object in at the corresponding position.
     * @param indices the indices to get the download files for.
     * @return Array of SWDownloadFiles, can contain null objects.
     */
    public SWDownloadFile[] getDownloadFilesAt( int[] indices )
    {
        synchronized( downloadList )
        {
            int length = indices.length;
            SWDownloadFile[] files = new SWDownloadFile[ length ];
            for ( int i = 0; i < length; i++ )
            {
                if ( indices[i] < 0 || indices[i] >= downloadList.size() )
                {
                    files[i] = null;
                }
                else
                {
                    files[i] = (SWDownloadFile)downloadList.get( indices[i] );
                }
            }
            return files;
        }
    }

    /**
     * Returns a download files matching the given fileSize and urn.
     * This is used to find a existing download file for new search results.
     * The additional check for fileSize is a security test to identify faulty
     * search results with faked URNs.
     * @param fileSize the required file size
     * @param matchURN the required URN we need to match.
     * @return the found SWDownloadFile or null if not found.
     */
    public SWDownloadFile getDownloadFile( long fileSize, URN matchURN )
    {
        synchronized( downloadList )
        {
            SWDownloadFile file = getDownloadFileByURN( matchURN );
            if ( file != null && file.getTotalDataSize() == fileSize )
            {
                return file;
            }
            return null;
        }
    }

    /**
     * Returns a download file only identified by the URN. This is used to
     * service partial download requests.
     */
    public SWDownloadFile getDownloadFileByURN( URN matchURN )
    {
        SWDownloadFile file;
        synchronized( downloadList )
        {
            file = (SWDownloadFile)urnToDownloadMap.get( matchURN );
            return file;
        }
    }
    
    /**
     * Returns whether a download file with the given URN exists.
     * @return true when a download file with the given URN exists, false otherwise.
     */
    public boolean isURNDownloaded( URN matchURN )
    {
        if ( matchURN == null )
        {
            return false;
        }
        synchronized( downloadList )
        {
            return urnToDownloadMap.containsKey( matchURN );            
        }
    }

    public void releaseCandidateIP( SWDownloadCandidate candidate )
    {
        ipDownloadCounter.relaseIP( candidate.getHostAddress() );
    }

    /**
     * Allocated a download set. The method will block until a complete download
     * set can be obtained.
     */
    public synchronized SWDownloadSet allocateDownloadSet( SWDownloadWorker worker )
    {
        synchronized( downloadList )
        {
            SWDownloadFile downloadFile = null;
            SWDownloadCandidate downloadCandidate = null;

            Iterator iterator = downloadList.iterator();
            while ( iterator.hasNext() )
            {
                downloadFile = (SWDownloadFile) iterator.next();
                if ( !downloadFile.isAbleToBeAllocated() )
                {
                    //Logger.logMessage( Logger.FINEST, Logger.DOWNLOAD,
                    //    "Download file not able to be allocated: "
                    //    + downloadFile );
                    continue;
                }

                downloadCandidate = downloadFile.allocateDownloadCandidate( worker );
                if ( downloadCandidate == null )
                {
                    //Logger.logMessage( Logger.FINEST, Logger.DOWNLOAD,
                    //    "Allocating DownloadSet - No download candidate. "
                    //    + worker.toString() );
                    continue;
                }
                // make sure we dont download more than X times from
                // the same host
                boolean succ = ipDownloadCounter.validateAndCountIP(
                    downloadCandidate.getHostAddress() );
                if ( !succ )
                {
                    downloadFile.releaseDownloadCandidate( downloadCandidate );
                    continue;
                }

                // Only check if there would be a segment allocateable...
                boolean segmentAllocateable = downloadFile.isDownloadSegmentAllocateable(
                    downloadCandidate.getAvailableRangeSet() );
                //downloadSegment = downloadFile.allocateDownloadSegment( worker );
                if ( !segmentAllocateable )
                {
                    //Logger.logMessage( Logger.FINER, Logger.DOWNLOAD,
                    //    "Allocating DownloadSet - No download segment. "
                    //    + worker.toString() );
                    downloadFile.releaseDownloadCandidate( downloadCandidate );
                    continue;
                }

                downloadFile.incrementWorkerCount();

                // build download set
                SWDownloadSet set = new SWDownloadSet( downloadFile,
                    downloadCandidate );
                if ( worker == temporaryWorker )
                {
                    unsetTemporaryWorker();
                }
                return set;
            }
        }
        return null;
    }

    /**
     * Checks if a new local file for the given download file is already
     * used in any other download file ( except the given one of course )
     * If no download file is given the file is checked against all download
     * files.
     */
    public boolean isNewLocalFilenameUsed( SWDownloadFile downloadFile,
        File newLocalFile )
    {
        // Check for duplicate filename in the existing files to download.
        int size = downloadList.size();
        for ( int i = 0; i < size; i++ )
        {
            SWDownloadFile existingFile = (SWDownloadFile)downloadList.get( i );

            // check file name if downloadFile is null or existingFile is
            // not the downloadFile
            if ( downloadFile == null || !(existingFile == downloadFile) )
            {
                if ( existingFile.getDestinationFile().compareTo( newLocalFile ) == 0 )
                {
                    // filename is already used
                    return true;
                }
            }
        }
        return false;
    }
    
    /**
     * Notifies the download manager about a change in the download list which
     * requires a download list save.
     */
    public void notifyDownloadListChange()
    {
        downloadListChangedSinceSave = true;
    }
    
    /**
     * Triggers a save of the download list. The call is not blocking and returns
     * directly, the save process is running in parallel.
     */
    private void triggerSaveDownloadList()
    {
        if ( !downloadListChangedSinceSave )
        {// not changed, no save needed
            return;
        }
        Logger.logMessage( Logger.CONFIG, Logger.DOWNLOAD,
            "Trigger save download list..." );
        synchronized( saveDownloadListLock )
        {
            if ( saveDownloadListJob != null )
            {
                // save download list is already in progress. we rerequest a save.
                saveDownloadListJob.triggerFollowUpSave();
            }
            else
            {
                saveDownloadListJob = new SaveDownloadListJob();
                saveDownloadListJob.start();
            }
        }
    }

    /**
     * Forces a save of the download list. The call returns after the save is
     * completed. Only the shutdown routine is allowed to call this method!
     */
    private void forceSaveDownloadList()
    {
        Logger.logMessage( Logger.CONFIG, Logger.DOWNLOAD,
            "Force save download list..." );
        synchronized( saveDownloadListLock )
        {
            if ( saveDownloadListJob == null )
            {
                saveDownloadListJob = new SaveDownloadListJob();
                saveDownloadListJob.start();
            }
            else
            {
                saveDownloadListJob.triggerFollowUpSave();
                
            }
        }
        try
        {
            saveDownloadListJob.setPriority( Thread.MAX_PRIORITY );
            if ( saveDownloadListJob != null )
            {
                try
                {
                    saveDownloadListJob.join();
                }
                catch ( NullPointerException exp )
                {// thread might be already finished and has set itself to null.
                }
            }
        }
        catch ( InterruptedException exp )
        {
            Logger.logError( exp );
        }
    }

    /**
     * Unsets the current temporary worker since it became active
     * and creates a new temporary woker to continue worker count requirement check.
     */
    private synchronized void unsetTemporaryWorker()
    {
        temporaryWorker.setTemporaryWorker( false );
        temporaryWorker = null;
        createNewTemporaryWorker();
    }

    /**
     * Creates a new temporary worker, that is used to figure out the max.
     * necessary worker count, when the total worker limit has not been reached
     * yet.
     */
    private synchronized void createNewTemporaryWorker()
    {
        if ( workerCount < ServiceManager.sCfg.maxTotalDownloadWorker )
        {
            temporaryWorker = new SWDownloadWorker( );
            temporaryWorker.setTemporaryWorker( true );
            Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
                "Creating new worker: " + temporaryWorker );
            temporaryWorker.startWorker();
            workerCount ++;
        }
    }

    public synchronized void createRequiredWorker()
    {
        if ( temporaryWorker == null )
        {
            createNewTemporaryWorker();
        }
    }

    /**
     * Notifys all workers that are waiting to start downloading.
     */
    public synchronized void notifyWaitingWorkers()
    {
        notifyAll();
    }

    public synchronized void waitForNotify()
    {
        try
        {
            wait( 1000 );
        }
        catch ( InterruptedException exp )
        {// handle interrupted exception accordingly...
            Thread.currentThread().interrupt();
        }
    }

    /**
     * Checks if there are too many workers and stops the worker if needed.
     * Returns true if worker will be stopped false otherwise.
     * Also verifies if there are enough workers available and triggers worker
     * creating if necessary.
     */
    public synchronized boolean checkToStopWorker( SWDownloadWorker worker )
    {
        int requiredCount = Math.min(
            downloadList.size() * ServiceManager.sCfg.maxWorkerPerDownload,
            ServiceManager.sCfg.maxTotalDownloadWorker );
        if ( workerCount > requiredCount )
        {
            if ( worker.isRunning() )
            {// if not already stopped
                worker.stopWorker();                
                workerCount --;
				if ( worker.isTemporaryWorker() )
				{
					temporaryWorker = null;
				}
            }
            return true;
        }
        else if ( workerCount < requiredCount )
        {// we have not enough workers... create some more
            createRequiredWorker();
        }
        return false;
    }

    /**
     * Called from worker if it unexpectedly shuts down.
     */
    public void notifyWorkerShoutdown( SWDownloadWorker worker )
    {
        Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
            "Unexpected worker shutdown: " + worker );
        worker.stopWorker();
        workerCount --;
		if ( worker.isTemporaryWorker() )
		{
			temporaryWorker = null;
		}
        createRequiredWorker();
    }

    private void updateOldXJBDownloadList( XJBOldDownloadList list )
        throws JAXBException
    {
        Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
            "Updating old download list." );

        XJBSWDownloadList newList = ObjectFactory.createXJBSWDownloadList();
        List swDownloadFileList = newList.getSWDownloadFileList();
        Iterator iterator = list.getDownloadFileList().iterator();
        while( iterator.hasNext() )
        {
            XJBOldDownloadFile file = (XJBOldDownloadFile) iterator.next();
            XJBSWDownloadFile newFile = ObjectFactory.createXJBSWDownloadFile();
            newFile.setFileSize( file.getFileSize() );
            newFile.setLocalFileName( file.getLocalFileName() );
            newFile.setSearchTerm( file.getSearchTerm() );
            // handled by verifyStatus()
            newFile.setStatus( SWDownloadConstants.STATUS_FILE_WAITING );

            List candidateList = newFile.getCandidateList();
            Iterator remoteIterator = file.getRemoteFileList().iterator();
            while ( remoteIterator.hasNext() )
            {
                XJBOldRemoteFile remoteFile = (XJBOldRemoteFile) remoteIterator.next();
                XJBSWDownloadCandidate newCandidate = ObjectFactory.createXJBSWDownloadCandidate();
                newCandidate.setFileIndex( remoteFile.getFileIndex() );
                newCandidate.setFileName( remoteFile.getFileName() );
                newCandidate.setGUID( remoteFile.getGuid() );
                newCandidate.setRemoteHost( remoteFile.getRemoteHost() );

                candidateList.add( newCandidate );
            }

            // create segment
            String filename = ServiceManager.sCfg.incompleteDir + File.separator +
                FileUtils.convertToLocalSystemFilename( file.getLocalFileName() ) + ".dl";
            XJBSWDownloadSegment segment = ObjectFactory.createXJBSWDownloadSegment();
            // resolve path
            segment.setIncompleteFileName( new File( filename ).getAbsolutePath() );
            segment.setLength( file.getFileSize() );
            segment.setSegmentNumber( 0 );
            segment.setStartPosition( 0 );
            newFile.getSegmentList().add( segment );

            swDownloadFileList.add( newFile );
        }
        loadXJBSWDownloadList( newList );
    }

    // XJB way
    private void loadXJBSWDownloadList( XJBSWDownloadList list )
        throws JAXBException
    {
        synchronized( downloadList )
        {
            Logger.logMessage( Logger.FINER, Logger.DOWNLOAD,
                "Loading SWDownload xml" );
            downloadList.clear();
            urnToDownloadMap.clear();
            SWDownloadFile file;
            XJBSWDownloadFile xjbFile;
            Iterator iterator = list.getSWDownloadFileList().iterator();
            
            while( iterator.hasNext() )
            {
                try
                {
                    xjbFile = (XJBSWDownloadFile) iterator.next();
                    file = new SWDownloadFile( xjbFile );
                    int pos = downloadList.size();
                    downloadList.add( pos, file );
                    URN urn = file.getFileURN();
                    if ( urn != null )
                    {
                        urnToDownloadMap.put( urn, file );
                    }
                    Logger.logMessage( Logger.FINER, Logger.DOWNLOAD,
                        "Loaded SWDownloadFile: " + file );
                    fireDownloadFileAdded( pos );
                }
                catch ( Exception exp )
                {// catch all exception in case we have an error in the XML
                    Logger.logError( exp,
                        "Error loading a download file from XML." );
                }
            }
        }
    }

    ///////////////////// START event handling methods ////////////////////////

    /**
     * All listeners interested in events.
     */
    private ArrayList listenerList = new ArrayList( 2 );

    public void addDownloadFilesChangeListener( DownloadFilesChangeListener listener )
    {
        listenerList.add( listener );
    }

    public void removeDownloadFilesChangeListener( DownloadFilesChangeListener listener )
    {
        listenerList.remove( listener );
    }

    private void fireDownloadFileChanged( final int position )
    {
        // invoke update in event dispatcher
        AsynchronousDispatcher.invokeLater(
        new Runnable()
        {
            public void run()
            {
                Object[] listeners = listenerList.toArray();
                DownloadFilesChangeListener listener;
                // Process the listeners last to first, notifying
                // those that are interested in this event
                for ( int i = listeners.length - 1; i >= 0; i-- )
                {
                    listener = (DownloadFilesChangeListener)listeners[ i ];
                    listener.downloadFileChanged( position );
                }
            }
        });
    }

    private void fireDownloadFileAdded( final int position )
    {
        // invoke update in event dispatcher
        AsynchronousDispatcher.invokeLater(
        new Runnable()
        {
            public void run()
            {
                Object[] listeners = listenerList.toArray();
                DownloadFilesChangeListener listener;
                // Process the listeners last to first, notifying
                // those that are interested in this event
                for ( int i = listeners.length - 1; i >= 0; i-- )
                {
                    listener = (DownloadFilesChangeListener)listeners[ i ];
                    listener.downloadFileAdded( position );
                }
            }
        });
    }

    private void fireDownloadFileRemoved( final int position )
    {
        // invoke update in event dispatcher
        AsynchronousDispatcher.invokeLater(
        new Runnable()
        {
            public void run()
            {
                Object[] listeners = listenerList.toArray();
                DownloadFilesChangeListener listener;
                // Process the listeners last to first, notifying
                // those that are interested in this event
                for ( int i = listeners.length - 1; i >= 0; i-- )
                {
                    listener = (DownloadFilesChangeListener)listeners[ i ];
                    listener.downloadFileRemoved( position );
                }
            }
        });
    }

    public void fireDownloadFileChanged( SWDownloadFile file )
    {
        int position = downloadList.indexOf( file );
        if ( position >= 0 )
        {
            fireDownloadFileChanged( position );
        }
    }
    ///////////////////// END event handling methods ////////////////////////
    
    private class LoadDownloadListJob extends Thread
    {
        public void run()
        {
            loadDownloadList();
        }
        
        private void loadDownloadList()
        {
            Logger.logMessage( Logger.CONFIG, Logger.DOWNLOAD,
                "Loading download list..." );
    
            // JAXB-BETA way
            File downloadFile = Environment.getInstance().getPhexConfigFile(
                EnvironmentConstants.XML_DOWNLOAD_FILE_NAME );
            File downloadFileBak = new File(downloadFile.getAbsolutePath() + ".bak");
    
            if ( !downloadFile.exists() && !downloadFileBak.exists() )
            {
                Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
                    "No download list file found." );
                return;
            }
            
            XJBPhex phex;
            try
            {
                Logger.logMessage( Logger.FINER, Logger.DOWNLOAD,
                    "Try to load from default download list." );
                phex = XMLBuilder.loadXJBPhexFromFile( downloadFile );
    
                if ( phex == null )
                {
                    Logger.logMessage( Logger.FINER, Logger.DOWNLOAD,
                        "Try to load from backup download list." );
                    phex = XMLBuilder.loadXJBPhexFromFile( downloadFileBak );
                }
                if ( phex == null )
                {
                    Logger.logMessage( Logger.SEVERE, Logger.GLOBAL,
                        "Error loading download list." );
                    return;
                    // fixme - we should barf and let whatever called this
                    // build a sensible file list, or build a sensible file list
                    // here
                }
    
                // update old download list
                XJBOldDownloadList oldList = phex.getDownloadList();
                if ( oldList != null )
                {
                    updateOldXJBDownloadList( oldList );
                }
                else
                {
                    XJBSWDownloadList list = phex.getSWDownloadList();
                    if ( list != null )
                    {
                        loadXJBSWDownloadList( list );
                    }
                    else
                    {
                        Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
                            "No SWDownloadList found." );
                    }
                }
            }
            catch ( JAXBException exp )
            {
                // TODO bring a GUI message that file can't be created
                Throwable linkedException = exp.getLinkedException();
                if ( linkedException != null )
                {
                    Logger.logError( linkedException );
                }
                Logger.logError( exp );
                return;
            }
        }
    }
    
    private class SaveDownloadListTimer extends TimerTask
    {
        // once per minute
        public static final long TIMER_PERIOD = 1000 * 20;
        
        public void run()
        {
            try
            {
                triggerSaveDownloadList();
            }
            catch ( Throwable th )
            {
                Logger.logError(th);
            }
        }
    }

    private class SaveDownloadListJob extends Thread
    {
        private boolean isFollowUpSaveTriggered;

        public SaveDownloadListJob()
        {
            super( ThreadTracking.rootThreadGroup, "SaveDownloadListJob" );
            setPriority( Thread.MIN_PRIORITY );
        }

        public void triggerFollowUpSave()
        {
            isFollowUpSaveTriggered = true;
        }

        /**
         * Saving of the download list is done asynchronously to make sure that there
         * will be no deadlocks happening
         */
        public void run()
        {
            do
            {
                Logger.logMessage( Logger.CONFIG, Logger.DOWNLOAD,
                    "Start saving download list..." );
                downloadListChangedSinceSave = false;
                isFollowUpSaveTriggered = false;
                // JAXB-beta way
                try
                {
                    XJBPhex phex = ObjectFactory.createPhexElement();

                    XJBSWDownloadList list = createXJBSWDownloadList();
                    phex.setSWDownloadList( list );
                    phex.setPhexVersion( VersionUtils.getProgramVersion() );

                    File downloadFile = Environment.getInstance().getPhexConfigFile(
                        EnvironmentConstants.XML_DOWNLOAD_FILE_NAME );
                    File downloadFileBak = new File(downloadFile.getAbsolutePath() + ".bak");

                    XMLBuilder.saveToFile( downloadFileBak, phex );

                    // modified by Matthew Pocock to write to a backup file and copy
                    // over and then delete backup to ensure that at least one valid
                    // download file always exists
                    // copy backup over download file and delete backpup
                    FileUtils.copyFile( downloadFileBak, downloadFile );
                    downloadFileBak.delete();
                }
                catch ( JAXBException exp )
                {
                    // TODO bring a GUI message that file can' t be created
                    Logger.logError( exp );
                }
                catch ( IOException exp )
                {
                    // TODO bring a GUI message that file can't be written
                    Logger.logError( exp );
                }
            }
            while( isFollowUpSaveTriggered );
            Logger.logMessage( Logger.CONFIG, Logger.DOWNLOAD,
                "Finished saving download list..." );

            synchronized( saveDownloadListLock )
            {
                // give created instance free once we are finished..
                saveDownloadListJob = null;
            }
        }

        private XJBSWDownloadList createXJBSWDownloadList()
            throws JAXBException
        {
            XJBSWDownloadList swDownloadList = ObjectFactory.createXJBSWDownloadList();
            synchronized( downloadList )
            {
                Iterator iterator = downloadList.iterator();
                List list = swDownloadList.getSWDownloadFileList();
                while ( iterator.hasNext() )
                {
                    SWDownloadFile file = (SWDownloadFile) iterator.next();
                    list.add( file.createXJBSWDownloadFile() );
                }
            }
            return swDownloadList;
        }
    }
}
