package org.limewire.ui.swing.downloads;

import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.swing.AbstractAction;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JPopupMenu;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;

import org.limewire.collection.glazedlists.GlazedListsFactory;
import org.limewire.core.api.URN;
import org.limewire.core.api.download.DownloadItem;
import org.limewire.core.api.download.DownloadListManager;
import org.limewire.core.api.download.DownloadState;
import org.limewire.core.settings.SharingSettings;
import org.limewire.inject.LazySingleton;
import org.limewire.setting.evt.SettingEvent;
import org.limewire.setting.evt.SettingListener;
import org.limewire.ui.swing.components.HyperlinkButton;
import org.limewire.ui.swing.downloads.table.DownloadStateExcluder;
import org.limewire.ui.swing.downloads.table.DownloadStateMatcher;
import org.limewire.ui.swing.settings.SwingUiSettings;
import org.limewire.ui.swing.transfer.TransferTrayNavigator;
import org.limewire.ui.swing.util.I18n;
import org.limewire.ui.swing.util.SwingUtils;
import org.limewire.util.FileUtils;

import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.SortedList;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;

import com.google.inject.Inject;
import com.google.inject.Provider;

@LazySingleton
public class DownloadMediator {
    
    public static enum SortOrder {ORDER_ADDED, NAME, PROGRESS, TIME_REMAINING, SPEED, STATUS, FILE_TYPE, EXTENSION};

    /**
     * unfiltered - common to all tables
     */
    private final SortedList<DownloadItem> downloadsCommonBaseList;
	private final DownloadListManager downloadListManager;
	private final Provider<MainDownloadPanel> downloadPanelFactory;
	private final Provider<DownloadHeaderPopupMenu> headerPopupMenuFactory;
	private final Provider<TransferTrayNavigator> transferTrayNavigator;
	
	private EventList<DownloadItem> activeList;
	private JButton clearFinishedButton;
	private JButton fixStalledButton;
	private List<JButton> headerButtons;
	private DownloadHeaderPopupMenu headerPopupMenu;
	
    private final Set<SortOrder> sortInspectionSet = new HashSet<SortOrder>();
    
	@Inject
	public DownloadMediator(DownloadListManager downloadManager,
	        Provider<MainDownloadPanel> downloadPanelFactory,
	        Provider<DownloadHeaderPopupMenu> headerPopupMenuFactory, 
	        Provider<TransferTrayNavigator> transferTrayNavigator) {
	    this.downloadListManager = downloadManager;
	    this.downloadPanelFactory = downloadPanelFactory;
	    this.headerPopupMenuFactory = headerPopupMenuFactory;
	    this.transferTrayNavigator = transferTrayNavigator;
	    
	    EventList<DownloadItem> baseList = GlazedListsFactory.filterList(downloadManager.getSwingThreadSafeDownloads(), new DownloadStateExcluder(DownloadState.CANCELLED));
	    downloadsCommonBaseList = GlazedListsFactory.sortedList(baseList, getSortComparator(getSortOrder(), isSortAscending()));
	}
    
	/**
	 * Registers listeners to update state.
	 */
    @Inject
    void register() {
        // Add listener to display download table when download added.
        downloadListManager.addPropertyChangeListener(new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                if (DownloadListManager.DOWNLOAD_ADDED.equals(evt.getPropertyName())) {
                    transferTrayNavigator.get().selectDownloads();
                }
            }
        });
        
        // Add setting listener to clear finished downloads.  When set, we
        // clear finished downloads and hide the "clear finished" button.
        SharingSettings.CLEAR_DOWNLOAD.addSettingListener(new SettingListener() {
            @Override
            public void settingChanged(SettingEvent evt) {
                SwingUtils.invokeNowOrLater(new Runnable() {
                    @Override
                    public void run() {
                        boolean clearDownloads = SharingSettings.CLEAR_DOWNLOAD.getValue();
                        if (clearDownloads) {
                            clearFinished();
                        }
                        if (clearFinishedButton != null) {
                            clearFinishedButton.setVisible(!clearDownloads);
                        }
                    }
                });
            }
        });

        // Add list listeners to enable/show header buttons.
        EventList<DownloadItem> doneList = GlazedListsFactory.filterList(getDownloadList(), 
                new DownloadStateMatcher(DownloadState.DONE,
                        DownloadState.DANGEROUS));
        EventList<DownloadItem> stalledList = GlazedListsFactory.filterList(getDownloadList(), 
                new DownloadStateMatcher(DownloadState.STALLED));

        doneList.addListEventListener(new ListEventListener<DownloadItem>() {
            @Override
            public void listChanged(ListEvent<DownloadItem> listChanges) {
                if (clearFinishedButton != null) {
                    clearFinishedButton.setEnabled(listChanges.getSourceList().size() > 0);
                }
            }
        });
        
        stalledList.addListEventListener(new ListEventListener<DownloadItem>() {
            @Override
            public void listChanged(ListEvent<DownloadItem> listChanges) {
                if (fixStalledButton != null) {
                    fixStalledButton.setVisible(listChanges.getSourceList().size() != 0);
                }
            }
        });
    }
    
	public JComponent getComponent() {
	    return downloadPanelFactory.get();
	}
	
	public boolean isSortAscending() {
	    return SwingUiSettings.DOWNLOAD_SORT_ASCENDING.getValue();
	}
	
	public SortOrder getSortOrder() {
	    try {
	        String sortKey = SwingUiSettings.DOWNLOAD_SORT_KEY.get();
	        return SortOrder.valueOf(sortKey);
	    } catch (IllegalArgumentException ex) {
            // Return default order if setting is invalid.
	        return SortOrder.ORDER_ADDED;
	    }
	}
	
	public void setSortOrder(SortOrder order, boolean isAscending){
        // Save sort settings.
	    SwingUiSettings.DOWNLOAD_SORT_KEY.set(order.toString());
	    SwingUiSettings.DOWNLOAD_SORT_ASCENDING.setValue(isAscending);
	    
        // Apply sort order.
	    downloadsCommonBaseList.setComparator(getSortComparator(order, isAscending));
	    
	    sortInspectionSet.add(order);
	}
	
    /**
     * Returns a comparator for the specified sort key and direction.
     */
	private Comparator<DownloadItem> getSortComparator(SortOrder sortOrder, boolean ascending) {
        Comparator<DownloadItem> comparator;
        switch (sortOrder) {
        case ORDER_ADDED:
            comparator = new OrderAddedComparator();
            break;
        case NAME:
            comparator = new NameComparator();
            break;
        case PROGRESS:
            comparator = new ProgressComparator();
            break;
        case TIME_REMAINING:
            comparator = new TimeRemainingComparator();
            break;
        case SPEED:
            comparator = new SpeedComparator();
            break;
        case STATUS:
            comparator = new DownloadStateComparator();
            break;
        case FILE_TYPE:
            comparator = new FileTypeComparator();
            break;
        case EXTENSION:
            comparator = new FileExtensionComparator();
            break;
        default:
            throw new IllegalArgumentException("Unknown SortOrder: " + sortOrder);
        }
        
        if (ascending) {
            return comparator;
        } else {
            return new DescendingComparator(comparator);
        }
	}

	public void pauseAll() {
        downloadsCommonBaseList.getReadWriteLock().writeLock().lock();
        try {
            for (DownloadItem item : downloadsCommonBaseList) {
                if (item.getState().isPausable()) {
                    item.pause();
                }
            }
        } finally {
            downloadsCommonBaseList.getReadWriteLock().writeLock().unlock();
        }
    }

	public void resumeAll() {
        downloadsCommonBaseList.getReadWriteLock().writeLock().lock();
        try {
            for (DownloadItem item : downloadsCommonBaseList) {
                if (item.getState().isResumable()) {
                    item.resume();
                }
            }
        } finally {
            downloadsCommonBaseList.getReadWriteLock().writeLock().unlock();
        }
    }
	
    /**
     * Returns a list of active download items.
     */
	public EventList<DownloadItem> getActiveList() {
	    if (activeList == null) {
	        activeList = GlazedListsFactory.filterList(downloadsCommonBaseList, 
	                new DownloadStateExcluder(DownloadState.ERROR,
	                        DownloadState.DONE,
	                        DownloadState.CANCELLED,
	                        DownloadState.DANGEROUS));
	    }
	    return activeList;
	}
	
    /**
     * Returns a sorted list of downloads.
     */
	public EventList<DownloadItem> getDownloadList() {
	    return downloadsCommonBaseList;
	}
	
    /**
     * Returns a list of header buttons.
     */
	public List<JButton> getHeaderButtons() {
	    if (headerButtons == null) {
	        // Create buttons.
            fixStalledButton = new HyperlinkButton(new FixStalledAction());
            fixStalledButton.setVisible(false);
	        
            clearFinishedButton = new HyperlinkButton(new ClearFinishedAction());
            clearFinishedButton.setEnabled(false);
	        
            // Add buttons to list.
            headerButtons = new ArrayList<JButton>();
            headerButtons.add(fixStalledButton);
            headerButtons.add(clearFinishedButton);
	    }
	    return headerButtons;
	}
	
    /**
     * Returns the header popup menu associated with the downloads table.
     */
	public JPopupMenu getHeaderPopupMenu() {
	    if (headerPopupMenu == null) {
	        headerPopupMenu = headerPopupMenuFactory.get();
	        headerPopupMenu.addPopupMenuListener(new PopupMenuListener() {
	            @Override
	            public void popupMenuCanceled(PopupMenuEvent e) {
	                headerPopupMenu.removeAll();
	            }
	            @Override
	            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
	                headerPopupMenu.removeAll();
	            }
	            @Override
	            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
	                headerPopupMenu.populate();
	            }
	        });
	    }
	    return headerPopupMenu;
	}
	
	public void clearFinished() {
	    downloadListManager.clearFinished();
	}

    public void fixStalled() {
        List<DownloadItem> items = getMatchingDownloadItems(DownloadState.STALLED);
        for (DownloadItem item : items) {
            item.resume();
        }
    }

    public void cancelStalled() {
        cancelMatchingDownloadItems(DownloadState.STALLED);
    }

    public void cancelError() {
        cancelMatchingDownloadItems(DownloadState.ERROR);
    }

    public void cancelAll() {
        cancelMatchingDownloadItems(null);
    }
    
    public boolean hasResumable() {
        downloadsCommonBaseList.getReadWriteLock().writeLock().lock();
        try {
            for (DownloadItem item : downloadsCommonBaseList) {
                if(item.getState().isResumable())
                    return true;
            }
        } finally {
            downloadsCommonBaseList.getReadWriteLock().writeLock().unlock();
        }
        return false;
    }
    
    public boolean hasPausable() {
        downloadsCommonBaseList.getReadWriteLock().writeLock().lock();
        try {
            for (DownloadItem item : downloadsCommonBaseList) {
                if(item.getState().isPausable())
                    return true;
            }
        } finally {
            downloadsCommonBaseList.getReadWriteLock().writeLock().unlock();
        }
        return false;
    }
    
    public boolean containsState(DownloadState state) {
        return getMatchingDownloadItems(state).size() > 0;
    }
    
    /**
     * 
     * @param state The state of the DownloadItems to be canceled.  Null will cancel all.
     */
    private void cancelMatchingDownloadItems(DownloadState state){
        List<DownloadItem> items = getMatchingDownloadItems(state);
        for(DownloadItem item : items){
            item.cancel();
        }
    }
    
    /**
     * 
     * @param state null will return all DownloadItems
     * @return a List of all DownloadItems in the specified DownloadState
     */
    private List<DownloadItem> getMatchingDownloadItems(DownloadState state) {
        if (state == null) {
            return new ArrayList<DownloadItem>(downloadsCommonBaseList);
        }
        
        List<DownloadItem> matchingItems = new ArrayList<DownloadItem>();
        for (DownloadItem item : downloadsCommonBaseList) {
            if (item.getState() == state) {
                matchingItems.add(item);
            }
        }
        
        return matchingItems;
    }
    
    /**
     * Action to clear all finished downloads.
     */
    private class ClearFinishedAction extends AbstractAction {

        public ClearFinishedAction() {
            super(I18n.tr("Clear Finished"));
        }
        
        @Override
        public void actionPerformed(ActionEvent e) {
            clearFinished();
        }
    }
    
    /**
     * Action to fix all stalled downloads.
     */
    private class FixStalledAction extends AbstractAction {

        public FixStalledAction() {
            super(I18n.tr("Fix Stalled"));
        }
        
        @Override
        public void actionPerformed(ActionEvent e) {
            fixStalled();
        }
    }  
    
    private static class OrderAddedComparator implements Comparator<DownloadItem>{
        
        @Override
        public int compare(DownloadItem o1, DownloadItem o2) { 
            if (o1 == o2){
                return 0;
            }
            return o2.getStartDate().compareTo(o1.getStartDate());
        }      
    }
    
    private static class NameComparator implements Comparator<DownloadItem>{
        
        @Override
        public int compare(DownloadItem o1, DownloadItem o2) {
            if (o1 == o2){
                return 0;
            }

            return o1.getTitle().compareTo(o2.getTitle());
        }   
     
    } 
  
    private static class ProgressComparator implements Comparator<DownloadItem>{
        
        @Override
        public int compare(DownloadItem o1, DownloadItem o2) {
            if (o1 == o2){
                return 0;
            }

            return o1.getPercentComplete() - o2.getPercentComplete();
        }   
     
    } 
    
    private static class TimeRemainingComparator implements Comparator<DownloadItem>{
        
        @Override
        public int compare(DownloadItem o1, DownloadItem o2) {
            if (o1 == o2){
                return 0;
            }

            return (int)(o1.getRemainingDownloadTime() - o2.getRemainingDownloadTime());
        }   
     
    }
    
    
    private static class SpeedComparator implements Comparator<DownloadItem>{
        
        @Override
        public int compare(DownloadItem o1, DownloadItem o2) {
            if (o1 == o2){
                return 0;
            }

            return (int)o2.getDownloadSpeed() - (int)o1.getDownloadSpeed();
        }   
     
    }
    
    
    private static class FileTypeComparator implements Comparator<DownloadItem>{
        
        @Override
        public int compare(DownloadItem o1, DownloadItem o2) {
            if (o1 == o2){
                return 0;
            }

            return o1.getCategory().compareTo(o2.getCategory());
        }   
     
    } 

    
    private static class FileExtensionComparator implements Comparator<DownloadItem> {
        
        @Override
        public int compare(DownloadItem o1, DownloadItem o2) {
            if (o1 == o2){
                return 0;
            }

            return FileUtils.getFileExtension(o1.getDownloadingFile()).compareTo(FileUtils.getFileExtension(o2.getDownloadingFile()));
        }   
     
    }
    
    private static class DescendingComparator implements Comparator<DownloadItem>{
        private Comparator<DownloadItem> delegate;

        public DescendingComparator(Comparator<DownloadItem> delegate){
            this.delegate = delegate;
        }

        @Override
        public int compare(DownloadItem o1, DownloadItem o2) {
            return -1 * delegate.compare(o1, o2);
        }
    }

    public void selectAndScrollTo(URN urn) {
        transferTrayNavigator.get().selectDownloads();
        downloadPanelFactory.get().selectAndScrollTo(urn);
    }
    
}
