package com.limegroup.gnutella.daap;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.BindException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ScheduledExecutorService;

import javax.jmdns.JmDNS;
import javax.jmdns.ServiceInfo;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.concurrent.ExecutorsHelper;
import org.limewire.core.settings.DaapSettings;
import org.limewire.i18n.I18nMarker;
import org.limewire.inject.EagerSingleton;
import org.limewire.io.NetworkInstanceUtils;
import org.limewire.io.NetworkUtils;
import org.limewire.lifecycle.Asynchronous;
import org.limewire.lifecycle.Join;
import org.limewire.lifecycle.Service;
import org.limewire.lifecycle.ServiceStage;
import org.limewire.listener.EventListener;
import org.limewire.service.MessageService;
import org.limewire.util.FileUtils;

import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;
import com.limegroup.gnutella.ActivityCallback;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.filters.IPFilter;
import com.limegroup.gnutella.library.FileDesc;
import com.limegroup.gnutella.library.FileView;
import com.limegroup.gnutella.library.FileViewChangeEvent;
import com.limegroup.gnutella.library.GnutellaFiles;
import com.limegroup.gnutella.library.IncompleteFileDesc;
import com.limegroup.gnutella.library.LibraryStatusEvent;
import com.limegroup.gnutella.util.LimeWireUtils;
import com.limegroup.gnutella.xml.LimeXMLDocument;
import com.limegroup.gnutella.xml.LimeXMLNames;

import de.kapsi.net.daap.AutoCommitTransaction;
import de.kapsi.net.daap.DaapAuthenticator;
import de.kapsi.net.daap.DaapConfig;
import de.kapsi.net.daap.DaapFilter;
import de.kapsi.net.daap.DaapServer;
import de.kapsi.net.daap.DaapStreamSource;
import de.kapsi.net.daap.DaapUtil;
import de.kapsi.net.daap.Database;
import de.kapsi.net.daap.Library;
import de.kapsi.net.daap.Playlist;
import de.kapsi.net.daap.Song;
import de.kapsi.net.daap.Transaction;

/**
 * This class handles the mDNS registration and acts as an
 * interface between WireShare and DAAP.
 */
@EagerSingleton
public class DaapManager {
    
    private static final Log LOG = LogFactory.getLog(DaapManager.class);
    private final ScheduledExecutorService backgroundExecutor;
    private final com.limegroup.gnutella.library.Library coreLibrary;
    private final FileView gnutellaFileView;
    private final Provider<IPFilter> ipFilter;
    private final Provider<NetworkInstanceUtils> networkInstanceUtils;
    private final Provider<ActivityCallback> activityCallback;
    
    /**
     * A shared processing queue for disk-related tasks.
     */
    private static final ExecutorService DAAP_EVENT_QUEUE = 
                         ExecutorsHelper.newProcessingQueue("DAAPQUEUE");

    private Library library;
    private Database database;
    private Playlist masterPlaylist;
    private Playlist whatsNew;
    private Playlist creativecommons;
    private Playlist videos;
        
    private DaapServer server;
    
    private BonjourService bonjour;
    private AutoCommitTransaction autoCommitTxn;
    
    private boolean enabled = false;
    private int maxPlaylistSize;
    
    private Map<URN, Song> urnToSong;  
    
    @Inject
    public DaapManager( @Named("backgroundExecutor") ScheduledExecutorService backgroundExecutor,
                        Provider<IPFilter> ipFilter,
                        Provider<NetworkInstanceUtils> networkInstanceUtils,
                        Provider<ActivityCallback> activityCallback,
                        @GnutellaFiles FileView gnutellaFileView, 
                        com.limegroup.gnutella.library.Library coreLibrary) {
        this.backgroundExecutor = backgroundExecutor;
        this.coreLibrary = coreLibrary;
        this.ipFilter = ipFilter;
        this.networkInstanceUtils = networkInstanceUtils;
        this.activityCallback = activityCallback;
        this.gnutellaFileView = gnutellaFileView;
    }
    
    @Inject
    void register(org.limewire.lifecycle.ServiceRegistry registry) {
        registry.register(new Service() {
            @Asynchronous(join = Join.NONE)
            public void start() {
                if (DaapSettings.DAAP_ENABLED.getValue()) {
                    try {
                        DaapManager.this.start();
                    } catch (IOException err) {
                        LOG.debug("error starting DAAP", err);
                        MessageService.showError(I18nMarker.marktr("Digital Audio Access Protocol service could not be started.  DAAP has been disabled; it can be restarted by clicking Tools -> Options -> My Files -> Configure (under iTunes)."));
                        DaapSettings.DAAP_ENABLED.setValue(false);
                    }
                }
            }

            @Asynchronous (daemon = false)
            public void stop() {
                DaapManager.this.stop();
            }

            public void initialize() {
                coreLibrary.addManagedListStatusListener(new EventListener<LibraryStatusEvent>() {
                    @Override
                    public void handleEvent(LibraryStatusEvent event) {
                        handleManagedListStatusEvent(event);
                    }
                });
                gnutellaFileView.addListener(new EventListener<FileViewChangeEvent>() {
                    @Override
                    public void handleEvent(FileViewChangeEvent event) {
                        handleFileListEvent(event);
                    }
                });
            }

            public String getServiceName() {
                return "DAAP";
            }
        }).in(ServiceStage.VERY_LATE);
    }
    
    /**
     * Starts the DAAP Server.
     */
    public synchronized void start() throws IOException {
        
        if (!isServerRunning()) {
            
            try {
                
                InetAddress addr = NetworkUtils.getLocalAddress();
                
                bonjour = new BonjourService(addr);
                urnToSong = new HashMap<URN, Song>();
                
                maxPlaylistSize = DaapSettings.DAAP_MAX_LIBRARY_SIZE.getValue();
                
                String name = DaapSettings.DAAP_LIBRARY_NAME.get();
                
                library = new Library(name);
                autoCommitTxn = new AutoCommitTransaction(library);
                
                database = new Database(name);
                whatsNew = new Playlist(activityCallback.get().translate(I18nMarker.marktr("What's New")));
                creativecommons = new Playlist(activityCallback.get().translate(I18nMarker.marktr("Creative Commons")));
                videos = new Playlist(activityCallback.get().translate(I18nMarker.marktr("Video")));
                
                library.addDatabase(null, database);
                database.addPlaylist(null, creativecommons);
                database.addPlaylist(null, whatsNew);
                creativecommons.setSmartPlaylist(null, true);
                whatsNew.setSmartPlaylist(null, true);
                masterPlaylist = database.getMasterPlaylist();

                LimeConfig config = new LimeConfig(addr);
                
                if (DaapSettings.DAAP_REQUIRES_PASSWORD.getValue()) {
                    if (DaapSettings.DAAP_REQUIRES_USERNAME.getValue()) {
                        config.setAuthenticationMethod(DaapConfig.USERNAME_AND_PASSWORD);
                        config.setAuthenticationScheme(DaapConfig.DIGEST_SCHEME);
                    } else {
                        config.setAuthenticationMethod(DaapConfig.PASSWORD);
                        config.setAuthenticationScheme(DaapConfig.BASIC_SCHEME);
                    }
                } else {
                    config.setAuthenticationMethod(DaapConfig.NO_PASSWORD);
                    config.setAuthenticationScheme(DaapConfig.BASIC_SCHEME);
                }
                
                server = new LimeDaapServerNIO(library, config, backgroundExecutor);

                server.setAuthenticator(new LimeAuthenticator());
                server.setStreamSource(new LimeStreamSource());
                server.setFilter(new LimeFilter());
                
                final int maxAttempts = 10;
                
                for(int i = 0; i < maxAttempts; i++) {
                    try {
                        server.bind();
                        break;
                    } catch (BindException bindErr) {
                        if (i < (maxAttempts-1)) {
                            // try next port...
                            config.nextPort();
                        } else {
                            throw bindErr;
                        }
                    }
                }

                server.run();

                bonjour.registerService();

            } catch (IOException err) {
                stop();
                throw err;
            }
        }
        
        if (isServerRunning()) {
            setEnabled(enabled);
        }
    }

    /**
     * Stops the DAAP Server and releases all resources.
     */
    public synchronized void stop() {

        if (bonjour != null)
            bonjour.close();

        if (server != null)
            server.stop();

        if (urnToSong != null)
            urnToSong.clear();

        bonjour = null;
        server = null;
        urnToSong = null;
        library = null;
        whatsNew = null;
        creativecommons = null;
        database = null;
        autoCommitTxn = null;
    }

    /**
     * Restarts the DAAP server and re-registers it via mDNS. This is equivalent
     * to:
     * <p>
     * 
     * <pre>
     * stop();
     * start();
     * init();
     * </pre>
     */
    public synchronized void restart() throws IOException {
        if (isServerRunning())
            stop();

        start();
    }

    /**
     * Shutdown the DAAP service properly. In this case is the main focus on
     * mDNS as in some rare cases iTunes doesn't recognize that LimeWire/DAAP 
     * is no longer online.
     */
    public void doFinalize() {
        stop();
    }

    /**
     * Updates the mDNS service info.
     */
    public synchronized void updateService() throws IOException {

        if (isServerRunning()) {
            bonjour.updateService();

            Transaction txn = library.beginTransaction();
            String name = DaapSettings.DAAP_LIBRARY_NAME.get();
            library.setName(txn, name);
            masterPlaylist.setName(txn, name);
            database.setName(txn, name);
            
            DaapConfig config = server.getConfig();
            if (DaapSettings.DAAP_REQUIRES_PASSWORD.getValue()) {
                if (DaapSettings.DAAP_REQUIRES_USERNAME.getValue()) {
                    config.setAuthenticationMethod(DaapConfig.USERNAME_AND_PASSWORD);
                    config.setAuthenticationScheme(DaapConfig.DIGEST_SCHEME);
                } else {
                    config.setAuthenticationMethod(DaapConfig.PASSWORD);
                    config.setAuthenticationScheme(DaapConfig.BASIC_SCHEME);
                }
            } else {
                config.setAuthenticationMethod(DaapConfig.NO_PASSWORD);
                config.setAuthenticationScheme(DaapConfig.BASIC_SCHEME);
            }
            
            txn.commit();
        }
    }

    /**
     * Disconnects all clients.
     */
    public synchronized void disconnectAll() {
        if (isServerRunning()) {
            server.disconnectAll();
        }
    }

    /**
     * Returns <tt>true</tt> if server is running.
     */
    public synchronized boolean isServerRunning() {
        if (server != null) {
            return server.isRunning();
        }
        return false;
    }

    /**
     * Returns true if the extension of name is a supported file type.
     */
    private static boolean isSupportedAudioFormat(String name) {
        return isSupportedFormat(DaapSettings.DAAP_SUPPORTED_AUDIO_FILE_TYPES.get(), name);
    }
    
    private static boolean isSupportedVideoFormat(String name) {
        return isSupportedFormat(DaapSettings.DAAP_SUPPORTED_VIDEO_FILE_TYPES.get(), name);
    }
    
    private static boolean isSupportedFormat(String[] types, String name) {
        for (String type : types) {
            if (name.endsWith(type)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Handles a change event.
     */
    private synchronized void handleChangeEvent(FileViewChangeEvent evt) {
        Song song = urnToSong.remove(evt.getOldValue().getSHA1Urn());
        if (song != null) {
            urnToSong.put(evt.getFileDesc().getSHA1Urn(), song);
            
            // See if the URNs changed -- if so, we can just rename.
            boolean rename = evt.getOldValue().getSHA1Urn().equals(evt.getFileDesc().getSHA1Urn());
            if(rename) {
                song.setAttachment(evt.getFileDesc());
            } else {                
                String name = evt.getFileDesc().getFileName().toLowerCase(Locale.US);
                
                if (isSupportedAudioFormat(name)) {
                    updateSongAudioMeta(autoCommitTxn, song, evt.getFileDesc());
                } else if (isSupportedVideoFormat(name)) {
                    updateSongVideoMeta(autoCommitTxn, song, evt.getFileDesc());
                } else {
                    database.removeSong(autoCommitTxn, song);
                }
                // auto commit
            }
            
        }
    }
    
    /** Handles a change in metadata event. */
    private synchronized void handleMetaChangeEvent(FileViewChangeEvent evt) {
        URN urn = evt.getFileDesc().getSHA1Urn();
        Song song = urnToSong.get(urn);
        if (song != null) {
            String name = evt.getFileDesc().getFileName().toLowerCase(Locale.US);                
            if (isSupportedAudioFormat(name)) {
                updateSongAudioMeta(autoCommitTxn, song, evt.getFileDesc());
            } else if (isSupportedVideoFormat(name)) {
                updateSongVideoMeta(autoCommitTxn, song, evt.getFileDesc());
            } else {
                database.removeSong(autoCommitTxn, song);
            }
        } else {
            handleAddEvent(evt);
        }
    }

    /**
     * Handles an add event.
     */
    private synchronized void handleAddEvent(FileViewChangeEvent evt) {
        // Transactions synchronize on the Library. So if there's
        // an ongoing commit we may get a ConcurrentModificationException
        // because Database has to iterate through all Playlists and
        // count the Songs.
        synchronized (library) {
            if (database.getSongCount() >= maxPlaylistSize) {
                return;
            }
        }
        
        FileDesc fileDesc = evt.getFileDesc();
        if (!(fileDesc instanceof IncompleteFileDesc)) {

            String name = fileDesc.getFileName().toLowerCase(Locale.US);

            Song song = null;
            
            if (isSupportedAudioFormat(name)) {
                song = createSong(fileDesc, true);
            } else if (isSupportedVideoFormat(name)) {
                song = createSong(fileDesc, false);
            }
            
            if (song != null) {
                urnToSong.put(fileDesc.getSHA1Urn(), song);
                
                database.getMasterPlaylist().addSong(autoCommitTxn, song);
                whatsNew.addSong(autoCommitTxn, song);
                
                if (fileDesc.isLicensed()) {
                    creativecommons.addSong(autoCommitTxn, song);
                }

                if (isSupportedVideoFormat(name)) {
                    videos.addSong(autoCommitTxn, song);
                }
                
                // auto commit
            }
        }
    }
    
    /**
     * Handles a remove event.
     */
    private synchronized void handleRemoveEvent(FileViewChangeEvent evt) {
        Song song = urnToSong.remove(evt.getFileDesc().getSHA1Urn());

        if (song != null) {
            database.removeSong(autoCommitTxn, song);
            song.setAttachment(null);
            
            // auto commit
        }
    }
    
    private synchronized void handleClearEvent() {
        for(Song song : urnToSong.values()) {
            if(song != null) {
                database.removeSong(autoCommitTxn, song);
                song.setAttachment(null);
            }
        }
        urnToSong.clear();
    }
    
    public synchronized boolean isEnabled() {
        return enabled;
    }
    
    private synchronized void setEnabled(boolean enabled) {
        
        this.enabled = enabled;
        
        if (!enabled || !isServerRunning())
            return;
        
        Map<URN, Song> tmpUrnToSong = new HashMap<URN, Song>();
        
        int size = masterPlaylist.getSongCount();        
        Transaction txn = library.beginTransaction();    
   
        gnutellaFileView.getReadLock().lock();
        try {
            for(FileDesc fd : gnutellaFileView) {
                String name = fd.getFileName().toLowerCase(Locale.US);
                boolean audio = isSupportedAudioFormat(name);
                
                if(!audio && !isSupportedVideoFormat(name)) {
                    continue;
                }
                
                URN urn = fd.getSHA1Urn();
                
                // 1)
                // _Remove_ URN from the current 'map'...
                Song song = urnToSong.remove(urn);
                    
                // Check if URN is already in the tmpMap.
                // If so do nothing as we don't want add 
                // the same file multiple times...
                if(tmpUrnToSong.containsKey(urn)) {
                    continue;
                }
                
                // This URN was already mapped with a Song.
                // Save the Song (again) and update the meta
                // data if necessary
                if (song != null) {
                    tmpUrnToSong.put(urn, song);
                    
                    if (audio) {
                        updateSongAudioMeta(txn, song, fd);
                    } else {
                        updateSongVideoMeta(txn, song, fd);
                    }
                    
                } else if (size < maxPlaylistSize) {
    
                    song = createSong(fd, audio);
                    tmpUrnToSong.put(urn, song);
                    database.getMasterPlaylist().addSong(txn, song);
                    
                    if (fd.isLicensed()) {
                        creativecommons.addSong(txn, song);
                    }
                    
                    if (isSupportedVideoFormat(name)) {
                        videos.addSong(txn, song);
                    }
                    
                    size++;
                }
            }
        } finally {
            gnutellaFileView.getReadLock().unlock();
        }
        
        // See 1)
        // As all known URNs were removed from 'map' only
        // deleted FileDesc URNs can be leftover! We must 
        // remove the associated Songs from the Library now
        for(Song song : urnToSong.values()) {
            database.removeSong(txn, song);
            song.setAttachment(null);
        }
        
        urnToSong.clear();
        urnToSong = tmpUrnToSong; // tempMap is the new 'map'

        txn.commit();
    }
    
    /**
     * Create a Song and sets its meta data with
     * the data which is retrieved from the FileDesc.
     */
    private Song createSong(FileDesc desc, boolean audio) {
        
        Song song = new Song(desc.getFileName());
        
        song.setSize(null, desc.getFileSize() & 0xFFFFFFFFL);
        song.setDateAdded(null, System.currentTimeMillis()/1000L);
        
        File file = desc.getFile();
        String ext = FileUtils.getFileExtension(file);
        
        if (!audio) {
            song.setHasVideo(null, true);
        }
        
        if (!ext.isEmpty()) {
            // Note: This is required for formats other than MP3
            // For example AAC (.m4a) files won't play if no
            // format is set. As far as I can tell from the iTunes
            // 'Get Info' dialog are Songs assumed as MP3 until
            // a format is set explicit.
            ext = ext.toLowerCase(Locale.US);
            if (!ext.endsWith("mp3"))
                song.setFormat(null, ext);

            if (audio) {
                updateSongAudioMeta(null, song, desc);
            } else {
                updateSongVideoMeta(null, song, desc);
            }
            
        } else {
            song.setAttachment(desc);
        }

        return song;
    }
    
    private boolean updateSongVideoMeta(Transaction txn, Song song, FileDesc desc) {
        
        song.setAttachment(desc);
        LimeXMLDocument doc = desc.getXMLDocument(LimeXMLNames.VIDEO_SCHEMA);
        
        if (doc == null) {
            return false;
        }
        
        boolean update = false;
        
        String title = doc.getValue(LimeXMLNames.VIDEO_TITLE);
        //String type = doc.getValue(LimeXMLNames.VIDEO_TYPE);
        String year = doc.getValue(LimeXMLNames.VIDEO_YEAR);
        String rating = doc.getValue(LimeXMLNames.VIDEO_RATING);
        String length = doc.getValue(LimeXMLNames.VIDEO_LENGTH);
        //String comments = doc.getValue(LimeXMLNames.VIDEO_COMMENTS);
        //String licensetype = doc.getValue(LimeXMLNames.VIDEO_LICENSETYPE);
        String license = doc.getValue(LimeXMLNames.VIDEO_LICENSE);
        //String height = doc.getValue(LimeXMLNames.VIDEO_HEIGHT);
        //String width = doc.getValue(LimeXMLNames.VIDEO_WIDTH);
        String bitrate = doc.getValue(LimeXMLNames.VIDEO_BITRATE);
        //String action = doc.getValue(LimeXMLNames.VIDEO_ACTION);
        String director = doc.getValue(LimeXMLNames.VIDEO_DIRECTOR);
        //String studio = doc.getValue(LimeXMLNames.VIDEO_STUDIO);
        //String language = doc.getValue(LimeXMLNames.VIDEO_LANGUAGE);
        //String stars = doc.getValue(LimeXMLNames.VIDEO_STARS);
        //String producer = doc.getValue(LimeXMLNames.VIDEO_PRODUCE);
        //String subtitles = doc.getValue(LimeXMLNames.VIDEO_SUBTITLES);
        
        if (title != null) {
            String currentTitle = song.getName();
            if (currentTitle == null || !title.equals(currentTitle)) {
                update = true;
                song.setName(txn, title);
            }
        }
        
        int currentBitrate = song.getBitrate();
        if (bitrate != null) {
            try {
                int num = Integer.parseInt(bitrate);
                if (num > 0 && num != currentBitrate) {
                    update = true;
                    song.setBitrate(txn, num);
                }
            } catch (NumberFormatException err) {}
        } else if (currentBitrate != 0) {
            update = true;
            song.setBitrate(txn, 0);
        }
        
        long currentLength = song.getTime();
        if (length != null) {
            try {
                // iTunes expects the song length in milliseconds
                int num = (int)(Integer.parseInt(length)*1000L);
                if (num > 0 && num != currentLength) {
                    update = true;
                    song.setTime(txn, num);
                }
            } catch (NumberFormatException err) {}
        } else if (currentLength != 0) {
            update = true;
            song.setTime(txn, 0);
        }
        
        int currentYear = song.getYear();
        if (year != null) {
            try {
                int num = Integer.parseInt(year);
                if (num > 0 && num != currentYear) {
                    update = true;
                    song.setYear(txn, num);
                }
            } catch (NumberFormatException err) {}
        } else if (currentYear != 0) {
            update = true;
            song.setYear(txn, 0);
        }
        
        // Genre = License
        String currentGenre = song.getGenre();
        if (license != null) {
            if (currentGenre == null || !license.equals(currentGenre)) {
                update = true;
                song.setGenre(txn, license);
            }
        } else if (currentGenre != null) {
            update = true;
            song.setGenre(txn, null);
        }
        
        // Artist = Director
        String currentArtist = song.getArtist();
        if (director != null) {
            if (currentArtist == null || !director.equals(currentArtist)) {
                update = true;
                song.setArtist(txn, director);
            }
        } else if (currentArtist != null) {
            update = true;
            song.setArtist(txn, null);
        }
        
        // Rating = Album
        String currentAlbum = song.getAlbum();
        if (rating != null) {
            if (currentAlbum == null || !rating.equals(currentAlbum)) {
                update = true;
                song.setAlbum(txn, rating);
            }
        } else if (currentAlbum != null) {
            update = true;
            song.setAlbum(txn, null);
        }
        
        return update;
    }
    
    /**
     * Sets the audio meta data.
     */
    private boolean updateSongAudioMeta(Transaction txn, Song song, FileDesc desc) {
        
        song.setAttachment(desc);
        
        LimeXMLDocument doc = desc.getXMLDocument(LimeXMLNames.AUDIO_SCHEMA);
        
        if (doc == null)
            return false;
        
        boolean update = false;
        
        String title = doc.getValue(LimeXMLNames.AUDIO_TITLE);
        String track = doc.getValue(LimeXMLNames.AUDIO_TRACK);
        String artist = doc.getValue(LimeXMLNames.AUDIO_ARTIST);
        String album = doc.getValue(LimeXMLNames.AUDIO_ALBUM);
        String genre = doc.getValue(LimeXMLNames.AUDIO_GENRE);
        String bitrate = doc.getValue(LimeXMLNames.AUDIO_BITRATE);
        //String comments = doc.getValue(LimeXMLNames.AUDIO_COMMENTS);
        String time = doc.getValue(LimeXMLNames.AUDIO_SECONDS);
        String year = doc.getValue(LimeXMLNames.AUDIO_YEAR);
        
        if (title != null) {
            String currentTitle = song.getName();
            if (currentTitle == null || !title.equals(currentTitle)) {
                update = true;
                song.setName(txn, title);
            }
        }
        
        int currentTrack = song.getTrackNumber();
        if (track != null) {
            try {
                int num = Integer.parseInt(track);
                if (num > 0 && num != currentTrack) {
                    update = true;
                    song.setTrackNumber(txn, num);
                }
            } catch (NumberFormatException err) {}
        } else if (currentTrack != 0) {
            update = true;
            song.setTrackNumber(txn, 0);
        }
        
        String currentArtist = song.getArtist();
        if (artist != null) {
            if (currentArtist == null || !artist.equals(currentArtist)) {
                update = true;
                song.setArtist(txn, artist);
            }
        } else if (currentArtist != null) {
            update = true;
            song.setArtist(txn, null);
        }
        
        String currentAlbum = song.getAlbum();
        if (album != null) {
            if (currentAlbum == null || !album.equals(currentAlbum)) {
                update = true;
                song.setAlbum(txn, album);
            }
        } else if (currentAlbum != null) {
            update = true;
            song.setAlbum(txn, null);
        }
        
        String currentGenre = song.getGenre();
        if (genre != null) {
            if (currentGenre == null || !genre.equals(currentGenre)) {
                update = true;
                song.setGenre(txn, genre);
            }
        } else if (currentGenre != null) {
            update = true;
            song.setGenre(txn, null);
        }
        
        /*String currentComments = song.getComment();
        if (comments != null) {
            if (currentComments == null || !comments.equals(currentComments)) {
                update = true;
                song.setComment(txn, comments);
            }
        } else if (currentComments != null) {
            update = true;
            song.setComment(txn, null);
        }*/
        
        int currentBitrate = song.getBitrate();
        if (bitrate != null) {
            try {
                int num = Integer.parseInt(bitrate);
                if (num > 0 && num != currentBitrate) {
                    update = true;
                    song.setBitrate(txn, num);
                }
            } catch (NumberFormatException err) {}
        } else if (currentBitrate != 0) {
            update = true;
            song.setBitrate(txn, 0);
        }
        
        long currentTime = song.getTime();
        if (time != null) {
            try {
                // iTunes expects the song length in milliseconds
                long num = Integer.parseInt(time)*1000l;
                if (num > 0 && num != currentTime) {
                    update = true;
                    song.setTime(txn, num);
                }
            } catch (NumberFormatException err) {}
        } else if (currentTime != 0) {
            update = true;
            song.setTime(txn, 0);
        }
        
        int currentYear = song.getYear();
        if (year != null) {
            try {
                int num = Integer.parseInt(year);
                if (num > 0 && num != currentYear) {
                    update = true;
                    song.setYear(txn, num);
                }
            } catch (NumberFormatException err) {}
        } else if (currentYear != 0) {
            update = true;
            song.setYear(txn, 0);
        }
        
        // iTunes expects the date/time in seconds
        int mod = (int)(desc.lastModified()/1000);
        if (song.getDateModified() != mod) {
            update = true;
            song.setDateModified(txn, mod);
        }

        return update;
    }
    
    /**
     * Handles the audio stream.
     */
    private final static class LimeStreamSource implements DaapStreamSource {
        
        public Object getSource(Song song) throws IOException {
            FileDesc fileDesc = (FileDesc)song.getAttachment();

            if(fileDesc != null)
                return new FileInputStream(fileDesc.getFile());
            
            return null;
        }
    }
    
    /**
     * Implements the DaapAuthenticator.
     */
    private final static class LimeAuthenticator implements DaapAuthenticator {
        
        public boolean authenticate(String username, String password, String uri, String nonce) {
            
            if (uri == null && nonce == null) {
                // BASIC
                return DaapSettings.DAAP_PASSWORD.equals(password);
            } else if (uri != null && nonce != null) {
                // DIGEST
                String ha1 = DaapSettings.DAAP_PASSWORD.get();
                if (ha1.startsWith("MD5/")) {
                    ha1 = ha1.substring(4);
                }
                String ha2 = DaapUtil.calculateHA2(uri);
                String digest = DaapUtil.digest(ha1, ha2, nonce);
                return digest.equalsIgnoreCase(password);
            } else {
                if (LOG.isErrorEnabled()) {
                    LOG.error("Unknown scheme!");
                }
            }
            
            return false;
        }
    }
    
    /**
     * The DAAP Library should be only accessible from the LAN
     * as we can not guarantee for the required bandwidth and it
     * could be used to bypass Gnutella etc. Note: iTunes can't
     * connect to DAAP Libraries outside of the LAN but certain
     * iTunes download tools can.
     */
    private final class LimeFilter implements DaapFilter {

        public boolean accept(InetAddress address) {
            
            try {
                return (networkInstanceUtils.get().isVeryCloseIP(address)
                        || networkInstanceUtils.get().isPrivateAddress(address))
                        && ipFilter.get().allow(address.getAddress());
            } catch (IllegalArgumentException err) {
                LOG.error(err);
                return false;
            }
        }
    }

    /**
     * A WireShare specific implementation of DaapConfig.
     */
    private final static class LimeConfig extends DaapConfig {

        private InetAddress addr;

        public LimeConfig(InetAddress addr) {
            this.addr = addr;

            // Reset PORT to default value to prevent increasing
            // it to infinity
            DaapSettings.DAAP_PORT.revertToDefault();
        }

        @Override
        public String getServerName() {
            return LimeWireUtils.getHttpServer();
        }

        public void nextPort() {
            int port = DaapSettings.DAAP_PORT.getValue();
            DaapSettings.DAAP_PORT.setValue(port + 1);
        }

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

        @Override
        public InetSocketAddress getInetSocketAddress() {
            int port = DaapSettings.DAAP_PORT.getValue();
            return new InetSocketAddress(addr, port);
        }

        @Override
        public int getMaxConnections() {
            return DaapSettings.DAAP_MAX_CONNECTIONS.getValue();
        }
    }

    /**
     * Helps us to publicize and update the DAAP Service via mDNS.
     */
    private final static class BonjourService {

        private static final String VERSION = "Version";

        private static final String MACHINE_NAME = "Machine Name";

        private static final String PASSWORD = "Password";

        private final JmDNS zeroConf;

        private ServiceInfo serviceInfo;

        public BonjourService(InetAddress addr) throws IOException {
            zeroConf = new JmDNS(addr);
        }

        public boolean isRegistered() {
            return (serviceInfo != null);
        }

        private ServiceInfo createServiceInfo() {

            String type = DaapSettings.DAAP_TYPE_NAME.get();
            String name = DaapSettings.DAAP_SERVICE_NAME.get();

            int port = DaapSettings.DAAP_PORT.getValue();
            int weight = DaapSettings.DAAP_WEIGHT.getValue();
            int priority = DaapSettings.DAAP_PRIORITY.getValue();

            boolean password = DaapSettings.DAAP_REQUIRES_PASSWORD.getValue();

            Hashtable<String, String> props = new Hashtable<String, String>();

            // Greys the share and the playlist names when iTunes's
            // protocol version is different from this version. It's
            // only a nice visual effect and has no impact to the
            // ability to connect this server! Disabled because
            // iTunes 4.2 is still widespread...
            props.put(VERSION, Integer.toString(DaapUtil.DAAP_VERSION_3));

            // This is the initial share name
            props.put(MACHINE_NAME, name);

            // shows the small lock if Service is protected
            // by a password!
            props.put(PASSWORD, Boolean.toString(password));

            String qualifiedName = null;

            // This isn't really required but as iTunes
            // does it in this way I'm doing it too...
            if (password) {
                qualifiedName = name + "_PW." + type;
            } else {
                qualifiedName = name + "." + type;
            }

            return new ServiceInfo(type, qualifiedName, port,
                    weight, priority, props);
        }

        public void registerService() throws IOException {

            if (isRegistered())
                throw new IOException();

            ServiceInfo serviceInfo = createServiceInfo();
            zeroConf.registerService(serviceInfo);
            this.serviceInfo = serviceInfo;
        }

        public void unregisterService() {
            if (!isRegistered())
                return;

            zeroConf.unregisterService(serviceInfo);
            serviceInfo = null;
        }

        public void updateService() throws IOException {
            if (!isRegistered())
                throw new IOException();

            if (serviceInfo.getPort() != DaapSettings.DAAP_PORT.getValue())
                unregisterService();

            ServiceInfo serviceInfo = createServiceInfo();
            zeroConf.registerService(serviceInfo);

            this.serviceInfo = serviceInfo;
        }

        public void close() {
            unregisterService();
            zeroConf.close();
        }   
    }

    /**
     * Listens for events from FileManager.
     */
    private void handleManagedListStatusEvent(final LibraryStatusEvent evt) {
        
        DAAP_EVENT_QUEUE.execute(new Runnable(){
            public void run(){
                switch(evt.getType()) {
                    case LOAD_COMPLETE:
                        setEnabled(true);
                }
            }
        });
    }
    
    private void handleFileListEvent(final FileViewChangeEvent evt) {
        // if Daap isn't enabled ignore events
        if (!DaapSettings.DAAP_ENABLED.getValue())
            return;

        DAAP_EVENT_QUEUE.execute(new Runnable() {
            public void run() {

                if (!isEnabled() || !isServerRunning())
                    return;

                switch (evt.getType()) {
                case FILE_CHANGED:
                    handleChangeEvent(evt);
                    break;
                case FILE_ADDED:
                    handleAddEvent(evt);
                    break;
                case FILE_REMOVED:
                    handleRemoveEvent(evt);
                    break;
                case FILES_CLEARED:
                    handleClearEvent();
                    break;
                case FILE_META_CHANGED:
                    handleMetaChangeEvent(evt);
                    break;
                }
            }
        });
    }
}
