package com.limegroup.bittorrent;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.URI;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import org.limewire.bittorrent.Torrent;
import org.limewire.bittorrent.TorrentEvent;
import org.limewire.bittorrent.TorrentEventType;
import org.limewire.bittorrent.TorrentFileEntry;
import org.limewire.bittorrent.TorrentInfo;
import org.limewire.bittorrent.TorrentManager;
import org.limewire.bittorrent.TorrentParams;
import org.limewire.bittorrent.TorrentPeer;
import org.limewire.bittorrent.TorrentState;
import org.limewire.bittorrent.TorrentStatus;
import org.limewire.bittorrent.util.TorrentUtil;
import org.limewire.core.api.download.DownloadPiecesInfo;
import org.limewire.core.api.download.SaveLocationManager;
import org.limewire.core.api.file.CategoryManager;
import org.limewire.core.api.transfer.SourceInfo;
import org.limewire.core.settings.BittorrentSettings;
import org.limewire.core.settings.SharingSettings;
import org.limewire.io.Address;
import org.limewire.io.ConnectableImpl;
import org.limewire.io.GUID;
import org.limewire.io.InvalidDataException;
import org.limewire.io.IpPortImpl;
import org.limewire.libtorrent.LibTorrentParams;
import org.limewire.listener.AsynchronousMulticasterImpl;
import org.limewire.listener.EventListener;
import org.limewire.listener.EventMulticaster;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.util.FileUtils;
import org.limewire.util.StringUtils;

import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;
import com.limegroup.gnutella.DownloadCallback;
import com.limegroup.gnutella.DownloadManager;
import com.limegroup.gnutella.InsufficientDataException;
import com.limegroup.gnutella.RemoteFileDesc;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.downloader.AbstractCoreDownloader;
import com.limegroup.gnutella.downloader.DownloadStateEvent;
import com.limegroup.gnutella.downloader.DownloaderType;
import com.limegroup.gnutella.downloader.IncompleteFileManager;
import com.limegroup.gnutella.downloader.serial.BTDownloadMemento;
import com.limegroup.gnutella.downloader.serial.BTMetaInfoMemento;
import com.limegroup.gnutella.downloader.serial.DownloadMemento;
import com.limegroup.gnutella.downloader.serial.LibTorrentBTDownloadMemento;
import com.limegroup.gnutella.downloader.serial.LibTorrentBTDownloadMementoImpl;
import com.limegroup.gnutella.library.FileCollection;
import com.limegroup.gnutella.library.GnutellaFiles;
import com.limegroup.gnutella.library.Library;
import com.limegroup.gnutella.malware.DangerousFileChecker;

/**
 * Wraps the Torrent class in the Downloader interface to enable the gui to
 * treat the torrent downloader as a normal downloader.
 */
public class BTDownloaderImpl extends AbstractCoreDownloader implements BTDownloader,
        EventListener<TorrentEvent> {

    private static final Log LOG = LogFactory.getLog(BTDownloaderImpl.class);

    private static final AtomicInteger torrentsStarted = new AtomicInteger();

    private static final AtomicInteger torrentsFinished = new AtomicInteger();

    private final DownloadManager downloadManager;
    
    private volatile Torrent torrent;
    private final BTUploaderFactory btUploaderFactory;
    private final AtomicBoolean finishing = new AtomicBoolean(false);
    private final AtomicBoolean complete = new AtomicBoolean(false);
    private final Library library;
    private final EventMulticaster<DownloadStateEvent> listeners;
    private final AtomicReference<DownloadState> lastState = new AtomicReference<DownloadState>(
            DownloadState.QUEUED);
    private final FileCollection gnutellaFileCollection;
    private final Provider<TorrentManager> torrentManager;
    private final Provider<TorrentUploadManager> torrentUploadManager;
    private final Provider<DangerousFileChecker> dangerousFileChecker;
    private final Provider<DownloadCallback> downloadCallback;

    /**
     * Torrent info hash based URN used as a cache for getSha1Urn().
     */
    private volatile URN urn = null;

    @Inject
    BTDownloaderImpl(SaveLocationManager saveLocationManager, DownloadManager downloadManager,
            BTUploaderFactory btUploaderFactory, Library library, 
            @Named("fastExecutor") ScheduledExecutorService fastExecutor,
            @GnutellaFiles FileCollection gnutellaFileCollection,
            Provider<TorrentManager> torrentManager,
            Provider<TorrentUploadManager> torrentUploadManager,
            Provider<DangerousFileChecker> dangerousFileChecker,
            Provider<DownloadCallback> downloadCallback,
            CategoryManager categoryManager) {
        super(saveLocationManager, categoryManager);
        this.downloadManager = downloadManager;
        this.btUploaderFactory = btUploaderFactory;
        this.library = library;
        this.gnutellaFileCollection = gnutellaFileCollection;
        this.listeners = new AsynchronousMulticasterImpl<DownloadStateEvent>(fastExecutor);
        this.torrentManager = torrentManager;
        this.torrentUploadManager = torrentUploadManager;
        this.dangerousFileChecker = dangerousFileChecker;
        this.downloadCallback = downloadCallback;
    }

    @Override
    public void handleEvent(TorrentEvent event) {
        if (TorrentEventType.COMPLETED == event.getType() && !complete.get()) {
            LOG.debug("Finished");
            finishing.set(true);
            torrentsFinished.incrementAndGet();
            if (isDangerous()) {
                return;
            }
            FileUtils.forceDeleteRecursive(getSaveFile());
            File completeDir = getSaveFile().getParentFile();
            torrent.getLock().lock();
            try {
                torrent.moveTorrent(completeDir);

                File torrentUploadFolder = BittorrentSettings.TORRENT_UPLOADS_FOLDER.get();

                File oldTorrentFile = torrent.getTorrentFile();
                if (oldTorrentFile != null) {
                    File newTorrentFile = new File(torrentUploadFolder, oldTorrentFile.getName());
                    torrent.setTorrentFile(newTorrentFile);
                    FileUtils.copy(oldTorrentFile, newTorrentFile);
                    FileUtils.forceDelete(oldTorrentFile);
                }

                File oldFastResumeFile = torrent.getFastResumeFile();
                if (oldFastResumeFile != null) {
                    File newFastResumeFile = new File(torrentUploadFolder, oldFastResumeFile.getName());
                    torrent.setFastResumeFile(newFastResumeFile);
                    FileUtils.copy(oldFastResumeFile, newFastResumeFile);
                    FileUtils.forceDelete(oldFastResumeFile);
                }
            } finally {
                torrent.getLock().unlock();
            }
            createUploadMemento();
            cleanupPriorityZeroFiles();
            File completeFile = getSaveFile();
            addFileToCollections(completeFile);
            complete.set(true);
            deleteIncompleteFiles();
            lastState.set(DownloadState.COMPLETE);
            listeners.broadcast(new DownloadStateEvent(this, DownloadState.COMPLETE));
            BTDownloaderImpl.this.downloadManager.remove(BTDownloaderImpl.this, true);
            torrent.removeListener(BTDownloaderImpl.this);
        } else if (TorrentEventType.STOPPED == event.getType()) {
            torrent.removeListener(this);
            // Was the torrent stopped because of a dangerous file?
            if (lastState.get() != DownloadState.DANGEROUS) {
            	lastState.set(DownloadState.ABORTED);
            	listeners.broadcast(new DownloadStateEvent(this, DownloadState.ABORTED));
            }
            BTDownloaderImpl.this.downloadManager.remove(BTDownloaderImpl.this, true);
            deleteIncompleteFiles();
        } else if (TorrentEventType.FAST_RESUME_FILE_SAVED == event.getType()) {
            // nothing to do now.
        } else if (TorrentEventType.STARTED == event.getType()) {
            torrentsStarted.incrementAndGet();
        } else if (TorrentEventType.META_DATA_RECEIVED == event.getType() && getTorrentFile() == null) {
            // Hack to either cancel the torrent incase of collision or
            //  fix the save path after metadata is received in a torrent 
            //  file-less download
            if (getSaveFile().exists()) {
                stop();
            } else {
                String newName = event.getTorrent().getName();
                setSaveFileInternal(new File(getSaveFile().getParentFile(), newName));
                setDefaultFileName(newName);
            }
        }
        else {
            DownloadState currentState = getState();
            if (lastState.getAndSet(currentState) != currentState) {
                listeners.broadcast(new DownloadStateEvent(this, currentState));
            }
        }
    }

    /**
     * Returns true if the given file is dangerous, after stopping the download.
     */
    private boolean isDangerous() {
    	for(File f : getIncompleteFiles()) {
    		if(isDangerous(f))
    			return true;
    	}
    	return false;
    }

	private void createUploadMemento() {
        try {
            torrentUploadManager.get().writeMemento(torrent);
            torrent.setAutoManaged(true);
        } catch (IOException e) {
            LOG.error("Error saving torrent upload menento for torrent: " + torrent.getName(), e);
            // non-fatal, upload will just not be loaded on application
            // restart
        }
    }

    /**
     * Returns true if the given file is dangerous, after stopping the download.
     */
    private boolean isDangerous(File file) {
        if(LOG.isDebugEnabled())
            LOG.debug("Checking whether " + file + " is dangerous");
        if (dangerousFileChecker.get().isDangerous(file)) {
            if(LOG.isDebugEnabled())
                LOG.debug(file + " is dangerous");
            lastState.set(DownloadState.DANGEROUS);
            listeners.broadcast(new DownloadStateEvent(this, DownloadState.DANGEROUS));
            // This will cause TorrentEvent.STOPPED
            torrent.stop();
            return true;
        }
        return false;
    }


    /**
     * Checks to see if this torrent has any priority zero files and removes
     * them.
     */
    private void cleanupPriorityZeroFiles() {
        LOG.debug("Cleaning up zero priority files");
        boolean hasAnyPriorityZero = false;

        List<TorrentFileEntry> fileEntries = torrent.getTorrentFileEntries();
        for (TorrentFileEntry fileEntry : fileEntries) {
            if (fileEntry.getPriority() == 0) {
                hasAnyPriorityZero = true;
                break;
            }
        }

        if (hasAnyPriorityZero) {
            for (TorrentFileEntry fileEntry : fileEntries) {
                if (fileEntry.getPriority() == 0) {
                    File torrentDataFile = torrent.getTorrentDataFile(fileEntry);
                    if(LOG.isDebugEnabled())
                        LOG.debug("Deleting " + torrentDataFile);
                    FileUtils.forceDelete(torrentDataFile);
                }
            }

            FileUtils.deleteEmptyDirectories(getSaveFile());
        }
    }

    /**
     * Adds the torrents files to the gnutella share list if the torrent is not
     * private and sharing is enabled, otehrwise the files are added to the
     * library.
     */
    private void addFileToCollections(File completeFile) {

        if (completeFile.isDirectory()) {
            if(LOG.isDebugEnabled())
                LOG.debug("Adding directory " + completeFile + " to library");
            FileFilter torrentFileFilter = new FileFilter() {
                @Override
                public boolean accept(File file) {
                    // library addFile method will filter out any truly
                    // unaddable files.
                    return true;
                }
            };
            library.addFolder(completeFile, torrentFileFilter);
            if (!torrent.isPrivate()
                    && SharingSettings.SHARE_DOWNLOADED_FILES_IN_NON_SHARED_DIRECTORIES.getValue()) {
                gnutellaFileCollection.addFolder(completeFile, torrentFileFilter);
            }
        } else {
            if(LOG.isDebugEnabled())
                LOG.debug("Adding file " + completeFile + " to library");
            library.add(completeFile);
            if (!torrent.isPrivate()
                    && SharingSettings.SHARE_DOWNLOADED_FILES_IN_NON_SHARED_DIRECTORIES.getValue()) {
                gnutellaFileCollection.add(completeFile);
            }
        }
    };

    @Override
    public void init(TorrentParams params) throws IOException {
        LOG.debug("Initializing");
        this.torrent = torrentManager.get().addTorrent(params);
        if(torrent == null) {
            LOG.debug("Error adding torrent to TorrentManager");
            throw new IOException("Error adding torrent to TorrentManager.");
        }
        torrent.addListener(this);

        setDefaultFileName(torrent.getName());
    }
    
    /**
     * Stops a torrent download. If the torrent is in seeding state, it does
     * nothing. (To stop a seeding torrent it must be stopped from the uploads
     * pane)
     */
    @Override
    public void stop() {
        if (!torrent.isFinished()) {
            LOG.debug("Stopping when unfinished");
            torrent.stop();
            downloadManager.remove(this, true);
        } else {
            LOG.debug("Stopping when finished");
            downloadManager.remove(this, true);
        }
    }

    @Override
    public void pause() {
        LOG.debug("Pausing");
        torrent.pause();
    }

    @Override
    public boolean isPaused() {
        return torrent.isPaused();
    }

    @Override
    public boolean isLaunchable() {
        if (isCompleted()) {
            LOG.debug("Launchable: completed");
            return true;
        }

        TorrentInfo torrentInfo = torrent.getTorrentInfo();
        if (torrentInfo == null || torrentInfo.getTorrentFileEntries().size() > 1) {
            LOG.debug("Not launchable: no torrent info or multiple files");
            return false;
        }

        LOG.debug("Launchable: torrent info and only one file");
        return true;
    }

    @Override
    public boolean resume() {
        LOG.debug("Resuming");
        torrent.resume();
        return true;
    }

    @Override
    public File getFile() {
        if (torrent.isFinished()) {
            LOG.debug("Finished: returning save file");
            return getSaveFile();
        } else {
            LOG.debug("Unfinished: returning incomplete file");
            return getIncompleteFile();
        }
    }

    @Override
    public File getIncompleteFile() {
        return new File(SharingSettings.INCOMPLETE_DIRECTORY.get(), torrent.getName());
    }

    @Override
    public File getDownloadFragment(ScanListener listener) {
        if (isCompleted()) {
            LOG.debug("Returning complete file");
            return getSaveFile();
        }

        // Can't preview a multi-file download
        TorrentInfo torrentInfo = torrent.getTorrentInfo();
        if (torrentInfo == null || torrentInfo.getTorrentFileEntries().size() > 1) {
            LOG.debug("No torrent info or multiple files");
            return null;
        }

        // Return a copy of the completed part of the file
        File copy = new File(getIncompleteFile().getParent(),
                IncompleteFileManager.PREVIEW_PREFIX
                + getIncompleteFile().getName());

        // TODO come up with correct size for preview, look at old code checking
        // last verified offsets etc. old code looks wrong though, since the
        // file downloads randomly, the last verified offset does not tell us
        // much.
        long size = Math.min(getIncompleteFile().length(), 2 * 1024 * 1024);
        if (FileUtils.copy(getIncompleteFile(), size, copy) <= 0) {
            LOG.debug("Failed to create preview copy");
            return null;
        }
        LOG.debug("Returning preview copy");
        return copy;
    }
    
    @Override
    public DownloadState getState() {
        switch(lastState.get()) {
        case DANGEROUS:
            return DownloadState.DANGEROUS;
        }

        TorrentStatus status = torrent.getStatus();
        if (!torrent.isStarted() || status == null) {
            return DownloadState.QUEUED;
        }

        TorrentState state = status.getState();

        // complete must be before aborted in order to not remove the download
        // from the list prematurely when teh seed ratio is reached and the
        // torrent is marked as cancelled.
        if (torrent.isFinished()) {
            return DownloadState.COMPLETE;
        }

        if (torrent.isCancelled()) {
            return DownloadState.ABORTED;
        }

        if (status.isError()) {
            if(LOG.isErrorEnabled())
                LOG.error("Error downloading torrent: " + status.getError());
            // gave up maps to stalled in the core api, which is a recoverable
            // error. All torrent downlaods are recoverable.
            return DownloadState.GAVE_UP;
        }

        if (finishing.get()) {
            return DownloadState.SAVING;
        }

        return convertState(state);
    }

    private DownloadState convertState(TorrentState state) {
        switch (state) {
        case DOWNLOADING:
            if (isPaused()) {
                return DownloadState.PAUSED;
            } else {
                return DownloadState.DOWNLOADING;
            }
        case QUEUED_FOR_CHECKING:
            return DownloadState.RESUMING;
        case CHECKING_FILES:
            return DownloadState.RESUMING;
        case SEEDING:
            return DownloadState.COMPLETE;
        case FINISHED:
            return DownloadState.COMPLETE;
        case ALLOCATING:
            return DownloadState.CONNECTING;
        case DOWNLOADING_METADATA:
            return DownloadState.INITIALIZING;
        default:
            throw new IllegalStateException("Unknown libtorrent state: " + state);
        }
    }

    @Override
    public int getRemainingStateTime() {
        // Unused
        return 0;
    }

    @Override
    public long getContentLength() {
        TorrentStatus status = torrent.getStatus();
        long contentLength = status != null ? status.getTotalWanted() : -1;
        if(LOG.isDebugEnabled())
            LOG.debug("Content length: " + contentLength);
        return contentLength;
    }

    @Override
    public long getAmountRead() {
        TorrentStatus status = torrent.getStatus();
        if (status == null) {
            LOG.debug("No status: cannot get amount read");
            return -1;
        } else {
            long amountRead = status.getTotalWantedDone();
            if(LOG.isDebugEnabled())
                LOG.debug("Amount read: " + amountRead);
            return amountRead;
        }
    }
    
    @Override
    public long getAmountVerified() {
        TorrentStatus status = torrent.getStatus();
        if (status == null) {
            LOG.debug("No status: cannot get amount verified");
            return -1;
        } else {
            long amountVerified = status.getTotalWantedDone();
            if(LOG.isDebugEnabled())
                LOG.debug("Amount verified: " + amountVerified);
            return amountVerified;
        }
    }

    @Override
    public long getAmountLost() {
        TorrentStatus status = torrent.getStatus();
        if (status == null) {
            LOG.debug("No status: cannot get amount lost");
            return -1;
        } else {
            long amountLost = status.getTotalFailedDownload();
            if(LOG.isDebugEnabled())
                LOG.debug("Amount lost: " + amountLost);
            return amountLost;
        }
    }

    @Override
    public List<RemoteFileDesc> getRemoteFileDescs() {
        return Collections.emptyList();
    }

    @Override
    public int getQueuePosition() {
        return 1;
    }

    @Override
    public int getQueuedHostCount() {
        return 0;
    }

    @Override
    public GUID getQueryGUID() {
        // Unused for torrents
        return null;
    }

    @Override
    public boolean isCompleted() {
        return complete.get();
    }

    @Override
    public boolean shouldBeRemoved() {
        switch (getState()) {
        case ABORTED:
        case COMPLETE:
        case DANGEROUS:
        	LOG.debug("Should be removed");
        	return true;
        }
        LOG.debug("Should not be removed");
        return false;
    }

    @Override
    public void measureBandwidth() {
        // Unused, we are using the bandwidth reported by libtorrent
    }

    @Override
    public float getMeasuredBandwidth() throws InsufficientDataException {
        float bw = torrent.isPaused() ? 0 : (torrent.getDownloadRate() / 1024);
        if(LOG.isDebugEnabled())
            LOG.debug("Measured bandwidth: " + bw);
        return bw;
    }

    @Override
    public float getAverageBandwidth() {
        // Unused by anything
        float bw = torrent.isPaused() ? 0 : (torrent.getDownloadRate() / 1024);
        if(LOG.isDebugEnabled())
            LOG.debug("Average bandwidth: " + bw);
        return bw;
    }

    @Override
    public boolean isRelocatable() {
        return !isCompleted();
    }

    @Override
    protected File getDefaultSaveFile() {
        File f = new File(SharingSettings.getSaveDirectory(), torrent.getName());
        if(LOG.isDebugEnabled())
            LOG.debug("Default save file: " + f);
        return f;
    }

    @Override
    public URN getSha1Urn() {
        if (urn == null) {
            synchronized (this) {
                if (urn == null) {
                    try {
                        urn = URN.createSha1UrnFromHex(torrent.getSha1());
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        }
        LOG.debug(urn);
        return urn;
    }

    @Override
    public int getNumHosts() {
        int hosts = torrent.getNumPeers();
        if(LOG.isDebugEnabled())
            LOG.debug(hosts + " hosts");
        return hosts;
    }

    @Override
    public List<Address> getSourcesAsAddresses() {

        List<TorrentPeer> peers = torrent.getTorrentPeers();
        List<Address> list = new ArrayList<Address>(peers.size());
        
        for (TorrentPeer peer : peers) {
            String ip = peer.getIPAddress();
            if (ip != null) {
                try {
                    list.add(new ConnectableImpl(new IpPortImpl(ip), false));
                    if(LOG.isDebugEnabled())
                        LOG.debug("Peer: " + ip);
                } catch (UnknownHostException e) {
                    // Discard invalid host
                    if(LOG.isDebugEnabled())
                        LOG.debug("Invalid peer: " + ip);
                }
            }
        }

        return list;
    }

    @Override
    public List<SourceInfo> getSourcesDetails() {
        
        List<TorrentPeer> peers = torrent.getTorrentPeers();
        List<SourceInfo> sourceInfoList = new ArrayList<SourceInfo>(peers.size());
        
        for (TorrentPeer peer : peers) {
            sourceInfoList.add(new TorrentSourceInfoAdapter(peer));
        }
        return sourceInfoList;
    }

    @Override
    public DownloadPiecesInfo getPieceInfo() {
        return new BTDownloadPiecesInfo(torrent);
    }
    
    @Override
    public void initialize() {
    }

    @Override
    public void startDownload() {
        LOG.debug("Starting download");
        btUploaderFactory.createBTUploader(torrent);
        torrent.start();
    }

    @Override
    public void handleInactivity() {
        // nothing happens when we're inactive
    }

    @Override
    public boolean shouldBeRestarted() {
        return true;
    }

    @Override
    public boolean isAlive() {
        return false; // doesn't apply to torrents
    }

    @Override
    public boolean isQueuable() {
        return !isPaused();
    }

    @Override
    public synchronized void finish() {
        LOG.debug("Finishing");
        deleteIncompleteFiles();
    }

    @Override
    public DownloaderType getDownloadType() {
        return DownloaderType.BTDOWNLOADER;
    }

    @Override
    protected DownloadMemento createMemento() {
        LOG.debug("Creating memento");
        return new LibTorrentBTDownloadMementoImpl();
    }

    @Override
    protected void fillInMemento(DownloadMemento memento) {
        LOG.debug("Filling in memento");
        super.fillInMemento(memento);

        LibTorrentBTDownloadMemento btMemento = (LibTorrentBTDownloadMemento) memento;

        btMemento.setName(torrent.getName());
        btMemento.setSha1Urn(getSha1Urn());
        btMemento.setIncompleteFile(getIncompleteFile());
        btMemento.setTrackers(torrent.getTrackerURIS());
        File fastResumeFile = torrent.getFastResumeFile();
        String fastResumePath = fastResumeFile != null ? fastResumeFile.getAbsolutePath() : null;
        btMemento.setFastResumePath(fastResumePath);
        File torrentFile = torrent.getTorrentFile();
        String torrentPath = torrentFile != null ? torrentFile.getAbsolutePath() : null;
        btMemento.setTorrentPath(torrentPath);

        btMemento.setPrivate(torrent.isPrivate());

    }

    public void initFromCurrentMemento(LibTorrentBTDownloadMemento memento)
            throws InvalidDataException {
        LOG.debug("Initializing from memento");
        urn = memento.getSha1Urn();

        if (urn == null) {
            LOG.debug("Null URN");
            throw new InvalidDataException(
                    "Null SHA1 URN retrieved from LibTorrent torrent momento.");
        }

        if (!urn.isSHA1()) {
            LOG.debug("Non-SHA1 URN");
            throw new InvalidDataException(
                    "Non SHA1 URN retrieved from LibTorrent torrent momento.");
        }

        String fastResumePath = memento.getFastResumePath();
        File fastResumeFile = fastResumePath != null ? new File(fastResumePath) : null;

        String torrentPath = memento.getTorrentPath();
        File torrentFile = torrentPath != null ? new File(torrentPath) : null;

        try {
            TorrentParams params = new LibTorrentParams(SharingSettings.INCOMPLETE_DIRECTORY.get(),
                    memento.getName(), StringUtils.toHexString(urn.getBytes()));
            params.setTrackers(memento.getTrackers());
            params.setFastResumeFile(fastResumeFile);
            params.setTorrentFile(torrentFile);
            params.setTorrentDataFile(memento.getIncompleteFile());
            params.setPrivate(memento.isPrivate());

            init(params);
        } catch (IOException e) {
            LOG.debug("Could not initialize downloader (first try)", e);
            // the .torrent file could be invalid, try to initialize just with
            // the memento contents.
            try {
                TorrentParams params = new LibTorrentParams(
                        SharingSettings.INCOMPLETE_DIRECTORY.get(), memento.getName(), 
                        StringUtils.toHexString(urn.getBytes()));
                params.setTrackers(memento.getTrackers());
                params.setFastResumeFile(fastResumeFile);
                params.setTorrentDataFile(memento.getIncompleteFile());
                params.setPrivate(memento.isPrivate());
                init(params);
            } catch (IOException e1) {
                LOG.debug("Could not initialize downloader (second try)", e1);
                throw new InvalidDataException("Could not initialize the BTDownloader", e1);
            }
        }
    }

    public void initFromOldMemento(BTDownloadMemento memento) throws InvalidDataException {
        LOG.debug("Initializing from old memento");
        BTMetaInfoMemento btmetainfo = memento.getBtMetaInfoMemento();

        URI[] trackers = btmetainfo.getTrackers();

        String name = btmetainfo.getFileSystem().getName();

        byte[] infoHash = btmetainfo.getInfoHash();

        URN sha1;
        try {
            sha1 = URN.createSHA1UrnFromBytes(infoHash);
        } catch (IOException e) {
            LOG.debug("Could not create URN", e);
            throw new InvalidDataException(
                    "Could not initialize the BTDownloader, memento hash was invalid", e);
        }

        boolean isPrivate = btmetainfo.isPrivate();

        File saveFile = memento.getSaveFile();
        File saveDir = saveFile == null ? SharingSettings.getSaveDirectory() : saveFile
                .getParentFile();
        saveDir = saveDir == null ? SharingSettings.getSaveDirectory() : saveDir;
        File oldIncompleteFile = btmetainfo.getFileSystem().getIncompleteFile();
        File newIncompleteFile = new File(SharingSettings.INCOMPLETE_DIRECTORY.get(), name);
        if (newIncompleteFile.exists()) {
            LOG.debug("Incomplete file already exists");
            throw new InvalidDataException(
                    "Cannot init memento for BTDownloader, incomplete file already exists: "
                            + newIncompleteFile);
        }

        FileUtils.forceRename(oldIncompleteFile, newIncompleteFile);
        File torrentDir = oldIncompleteFile.getParentFile();
        if (torrentDir.getName().length() == 32) {
            // looks like the old torrent dir
            FileUtils.forceDeleteRecursive(torrentDir);
        }

        try {
            TorrentParams params = new LibTorrentParams(newIncompleteFile.getParentFile(), name,
                    StringUtils.toHexString(sha1.getBytes()));
            params.setTrackers(Arrays.asList(trackers));
            params.setTorrentDataFile(newIncompleteFile);
            params.setPrivate(isPrivate);
            init(params);
        } catch (IOException e) {
            LOG.debug("Could not initialize downloader", e);
            throw new InvalidDataException("Could not initialize the BTDownloader", e);
        }
    }

    @Override
    public synchronized void initFromMemento(DownloadMemento memento) throws InvalidDataException {
        super.initFromMemento(memento);
        if (BTDownloadMemento.class.isInstance(memento)) {
            initFromOldMemento((BTDownloadMemento) memento);
        } else if (LibTorrentBTDownloadMemento.class.isInstance(memento)) {
            initFromCurrentMemento((LibTorrentBTDownloadMemento) memento);
        }
        if (!torrent.isValid()) {
            LOG.debug("Error registering torrent");
            throw new InvalidDataException("Error registering torrent");
        }
    }

    /**
     * Adds basic DownloadStateEvent listener support. Currently only
     * broadcasts, COMPLETED and ABORTED states.
     */
    @Override
    public void addListener(EventListener<DownloadStateEvent> listener) {
        listeners.addListener(listener);
    }

    @Override
    public boolean removeListener(EventListener<DownloadStateEvent> listener) {
        return listeners.removeListener(listener);
    }

    @Override
    public void deleteIncompleteFiles() {
        LOG.debug("Deleting incomplete files");
        if (!complete.get()) {
            LOG.debug("Not complete");
            File incompleteFile = getIncompleteFile();
            if (incompleteFile != null) {
                LOG.debug("Deleting incomplete file");
                FileUtils.forceDeleteRecursive(incompleteFile);
            }
        }
        if(torrent.getTorrentFile() != null && torrent.getTorrentFile().getParentFile().equals(SharingSettings.INCOMPLETE_DIRECTORY.get())) {
            LOG.debug("Deleting torrent file");
            FileUtils.forceDelete(torrent.getTorrentFile());
        }
        if(torrent.getFastResumeFile().getParentFile().equals(SharingSettings.INCOMPLETE_DIRECTORY.get())) {
            LOG.debug("Deleting fast resume file");
            FileUtils.forceDelete(torrent.getFastResumeFile());
        }
    }

    @Override
    public List<File> getCompleteFiles() {
        List<File> completeFiles = TorrentUtil.buildTorrentFiles(torrent, getSaveFile().getParentFile());
        if(LOG.isDebugEnabled()) {
            LOG.debug("Getting complete files:");
            for(File f : completeFiles) {
                LOG.debug(" " + f);
            }
        }
        return completeFiles;
    }

    public List<File> getIncompleteFiles() {
        List<File> incompleteFiles = TorrentUtil.buildTorrentFiles(torrent, getIncompleteFile().getParentFile());
        if(LOG.isDebugEnabled()) {
            LOG.debug("Getting incomplete files:");
            for(File f : incompleteFiles) {
                LOG.debug(" " + f);
            }
        }
        return incompleteFiles;
    }

    @Override
    public boolean conflicts(URN urn, long fileSize, File... file) {
        if (getSha1Urn().equals(urn)) {
            LOG.debug("Conflicts with URN");
            return true;
        }

        for (File f : file) {
            if (conflictsSaveFile(f)) {
                return true;
            }
        }

        LOG.debug("Does not conflict");
        return false;
    }

    @Override
    public boolean conflictsSaveFile(File complete) {
        boolean conflicts = complete.equals(getSaveFile());
        if(conflicts)
            LOG.debug("Conflicts with save file");
        return conflicts;
    }

    @Override
    public boolean conflictsWithIncompleteFile(File incomplete) {
        boolean conflicts = incomplete.equals(getIncompleteFile());
        if(conflicts)
            LOG.debug("Conflicts with incomplete file");
        return conflicts;
    }

    /**
     * No longer relevant in any Downloader.
     */
    @Override
    public int getChunkSize() {
        throw new UnsupportedOperationException("BTDownloaderImpl.getChunkSize() not implemented");
    }

    /**
     * No longer relevant in any Downloader.
     */
    @Override
    public int getAmountPending() {
        throw new UnsupportedOperationException(
                "BTDownloaderImpl.getAmountPending() not implemented");
    }

    @Override
    public File getTorrentFile() {
        File torrentFile = torrent.getTorrentFile();
        if(LOG.isDebugEnabled())
            LOG.debug("Getting torrent file: " + torrentFile);
        return torrentFile;
    }

    public Torrent getTorrent() {
        return torrent;
    }
}
