package org.limewire.ui.swing.library.table;

import java.awt.Point;
import java.awt.event.MouseEvent;
import java.io.File;
import java.util.List;

import javax.swing.DropMode;
import javax.swing.JLabel;
import javax.swing.JPopupMenu;
import javax.swing.SwingConstants;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;

import org.jdesktop.swingx.table.TableColumnExt;
import org.limewire.collection.glazedlists.GlazedListsFactory;
import org.limewire.core.api.Category;
import org.limewire.core.api.URN;
import org.limewire.core.api.file.CategoryManager;
import org.limewire.core.api.library.FileItem;
import org.limewire.core.api.library.LocalFileItem;
import org.limewire.inject.LazySingleton;
import org.limewire.ui.swing.listener.MousePopupListener;
import org.limewire.ui.swing.table.ColumnStateHandler;
import org.limewire.ui.swing.table.DefaultLimeTableCellRenderer;
import org.limewire.ui.swing.table.FileSizeRenderer;
import org.limewire.ui.swing.table.IconLabelRenderer;
import org.limewire.ui.swing.table.IconLabelRendererFactory;
import org.limewire.ui.swing.table.MouseableTable;
import org.limewire.ui.swing.table.MultilineTooltipRenderer;
import org.limewire.ui.swing.table.NameRenderer;
import org.limewire.ui.swing.table.TableCellHeaderRenderer;
import org.limewire.ui.swing.table.TableColumnSelector;
import org.limewire.ui.swing.table.TableDoubleClickHandler;
import org.limewire.ui.swing.table.TimeRenderer;
import org.limewire.ui.swing.util.EventListJXTableSorting;
import org.limewire.ui.swing.util.I18n;

import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.FilterList;
import ca.odell.glazedlists.ListSelection;
import ca.odell.glazedlists.SortedList;
import ca.odell.glazedlists.matchers.Matcher;
import ca.odell.glazedlists.swing.DefaultEventSelectionModel;

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

@LazySingleton
public class LibraryTable extends MouseableTable {

    private final int rowHeight = 20;

    private LibraryTableModel cachedLibraryTableModel;
    private DefaultEventSelectionModel<LocalFileItem> cachedEventSelectionModel;
    private EventListJXTableSorting cachedTableSorting;
    private SortedList<LocalFileItem> cachedSortedList;
    
    private AbstractLibraryFormat<LocalFileItem> fileItemFormat;
    private ColumnStateHandler columnStateHandler;
    private MousePopupListener mousePopupListener;
    
    private Provider<DefaultLimeTableCellRenderer> defaultCellRenderer;
    private final Provider<NameCategoryRenderer> nameCategoryRenderer;
    private final Provider<TimeRenderer> timeRenderer;
    private final Provider<FileSizeRenderer> fileSizeRenderer;
    private final Provider<NameRenderer> nameRenderer;
    private final Provider<RemoveRenderer> removeRenderer;
    private final Provider<IsPlayingRenderer> isPlayingRenderer;
    private final Provider<LaunchFileAction> launchAction;
    private final IconLabelRenderer iconLabelRenderer;
    private final RemoveEditor removeEditor;
    private final CategoryManager categoryManager;
    
    @Inject
    public LibraryTable(Provider<DefaultLimeTableCellRenderer> defaultCellRenderer,
            Provider<NameCategoryRenderer> nameCategoryRenderer,
            Provider<TimeRenderer> timeRenderer,
            Provider<FileSizeRenderer> fileSizeRenderer,
            Provider<NameRenderer> nameRenderer,
            Provider<LibraryPopupMenu> libraryPopupMenu,
            Provider<RemoveRenderer> removeRenderer,
            Provider<IsPlayingRenderer> isPlayingRenderer,
            Provider<LaunchFileAction> launchAction,
            IconLabelRendererFactory iconLabelRendererFactory,
            RemoveEditor removeEditor,
            DeletionKeyListener deletionKeyListener,
            CategoryManager categoryManager) {
        this.defaultCellRenderer = defaultCellRenderer;
        this.nameCategoryRenderer = nameCategoryRenderer;
        this.timeRenderer = timeRenderer;
        this.fileSizeRenderer = fileSizeRenderer;
        this.nameRenderer = nameRenderer;
        this.removeRenderer = removeRenderer;
        this.isPlayingRenderer = isPlayingRenderer;
        this.launchAction = launchAction;
        this.iconLabelRenderer = iconLabelRendererFactory.createIconRenderer(false);
        this.removeEditor = removeEditor;
        this.categoryManager = categoryManager;

        initTable();
        
        //TODO: anything below here should be initialized outside of the constructor
        mousePopupListener = new MousePopupListener() {
            @Override
            public void handlePopupMouseEvent(MouseEvent e) {
                showHeaderPopupMenu(e.getPoint());
            }
        }; 

        setPopupHandler(new LibraryPopupHandler(this, libraryPopupMenu));
        
        this.addKeyListener(deletionKeyListener);
    }
    
    private void initTable() {
        setShowHorizontalLines(false);
        setShowGrid(false, true);
        
        setFillsViewportHeight(true);
        setDragEnabled(true);
        setRowHeight(rowHeight);
        setDropMode(DropMode.ON);
        
        setDoubleClickHandler(new DoubleClickHandler());
        setEnterKeyAction(launchAction.get());
    }
    
    private void uninstallListeners() {
        getTableHeader().removeMouseListener(mousePopupListener);
        
        if(columnStateHandler != null) {
            columnStateHandler.removeListeners();
        }
    }
    
    private void installListeners() {
        // Add mouse listener to display popup menu on column header.  We use 
        // MousePopupListener to detect the popup trigger, which differs on 
        // Windows, Mac, and Linux.
        JTableHeader header = getTableHeader();
        header.addMouseListener(mousePopupListener);
        
        // Install column state handler.
        columnStateHandler = new ColumnStateHandler(this, fileItemFormat);
    }
    
    public void showHeaderPopupMenu(Point p) {
        JPopupMenu menu = new TableColumnSelector(this, fileItemFormat).getPopupMenu();
        menu.show(getTableHeader(), p.x, p.y);
    }
    
    public LibraryTableModel getLibraryTableModel() {
        return cachedLibraryTableModel;
    }
    
    public LocalFileItem getSelectedItem() {
        if(getSelectedRow() >= 0)
            return cachedLibraryTableModel.getElementAt(getSelectedRow());
        else           
            return null;
    }
    
    public void setEventList(EventList<LocalFileItem> eventList, AbstractLibraryFormat<LocalFileItem> tableFormat) {
        uninstallListeners();
        
        fileItemFormat = tableFormat;
        
        SortedList<LocalFileItem> newSortedList = GlazedListsFactory.sortedList(eventList, null);
        LibraryTableModel newLibraryTableModel = new LibraryTableModel(newSortedList, tableFormat);
        DefaultEventSelectionModel<LocalFileItem> newEventSelectionModel = new DefaultEventSelectionModel<LocalFileItem>(newSortedList);
  
        if(cachedTableSorting != null) {
            cachedTableSorting.uninstall();
        }

		// setting the selection model first to ensure table doesn't
		// try to select non-existent rows
        setSelectionModel(newEventSelectionModel);
        setModel(newLibraryTableModel);
        newEventSelectionModel.setSelectionMode(ListSelection.MULTIPLE_INTERVAL_SELECTION_DEFENSIVE);
        
        if(cachedLibraryTableModel != null) {
            cachedEventSelectionModel.dispose();
            cachedLibraryTableModel.dispose();
            cachedSortedList.dispose();
        }
        
        EventListJXTableSorting newTableSorting = EventListJXTableSorting.install(this, newSortedList, tableFormat);
        
        cachedSortedList = newSortedList;
        cachedLibraryTableModel = newLibraryTableModel;
        cachedEventSelectionModel = newEventSelectionModel;
        cachedTableSorting = newTableSorting;
        
        installListeners();
    }
    
    /**
     * Creates a list of playable file items using the specified source list.
     */
    private FilterList<LocalFileItem> createPlayableList(EventList<LocalFileItem> sourceList) {
        // Return filter list for playable files.
        return GlazedListsFactory.filterList(sourceList, new Matcher<LocalFileItem>() {
            @Override
            public boolean matches(LocalFileItem item) {
                Category category = categoryManager.getCategoryForFile(item.getFile());
                return category == Category.AUDIO;
            }
        });
    }
    
    /**
     * Returns the list of playable file items.
     */
    public EventList<LocalFileItem> getPlayableList() {
        return createPlayableList(((LibraryTableModel)getModel()).getAllItems());
    }
    
    public boolean isRowDisabled(int row) {
        FileItem item = getLibraryTableModel().getFileItem(convertRowIndexToModel(row));
        if (item instanceof LocalFileItem) {
            return ((LocalFileItem) item).isIncomplete();
        }
        return item == null;
    }
    
    public void selectAndScrollTo(File file) {
    	//ensure table has focus to recieve keyboard events
        requestFocus();
        LibraryTableModel model = getLibraryTableModel();
        for(int y=0; y < model.getRowCount(); y++) {
            FileItem fileItem = model.getElementAt(y);
            if(fileItem instanceof LocalFileItem) {
                if(file.equals(((LocalFileItem)fileItem).getFile())) {
                    getSelectionModel().setSelectionInterval(y, y);
                    break;
                }
            }
        }
        ensureRowVisible(getSelectedRow());
    }
    
    public void selectAndScrollTo(URN urn) {
        //ensure table has focus to recieve keyboard events
        requestFocus();
        LibraryTableModel model = getLibraryTableModel();
        for(int y=0; y < model.getRowCount(); y++) {
            FileItem fileItem = model.getElementAt(y);
            if(urn.equals(fileItem.getUrn())) {
                getSelectionModel().setSelectionInterval(y, y);
                break;
            }
        }
        ensureRowVisible(getSelectedRow());
    }
        
    /**
     * Loads the saved state of the columns. 
     *
     * <p>NOTE: This method must be called after the renderers and editors
     * have been loaded.  The settings must be applied in this order:
     * width/visibility/order.</p>
     */
    public void applySavedColumnSettings(){
        if (columnStateHandler != null) {
            columnStateHandler.setupColumnWidths();
            columnStateHandler.setupColumnVisibility(false);
            columnStateHandler.setupColumnOrder();
        }
    }
    
    public void setupCellRenderers(Category category, AbstractLibraryFormat format) {
        for (int i = 0; i < format.getColumnCount(); i++) {
            Class clazz = format.getColumnClass(i);
            if (clazz == String.class) {
                setCellRenderer(i, defaultCellRenderer.get());
            } 
        }   
        
        if(category != null) {
            switch(category) {
            case AUDIO:
                setHeaderRenderer(AudioTableFormat.LENGTH_INDEX, new TableCellHeaderRenderer(SwingConstants.TRAILING));
                setCellRenderer(AudioTableFormat.PLAY_INDEX, isPlayingRenderer.get());
                setUnsortable(AudioTableFormat.PLAY_INDEX);
                setCellRenderer(AudioTableFormat.SIZE_INDEX, fileSizeRenderer.get());
                setCellRenderer(AudioTableFormat.LENGTH_INDEX, timeRenderer.get());
                setCellRenderer(AudioTableFormat.TITLE_INDEX, nameRenderer.get());
                setCellRenderer(AudioTableFormat.ACTION_INDEX, removeRenderer.get());
                setCellEditor(AudioTableFormat.ACTION_INDEX, removeEditor);
                setUnsortable(AudioTableFormat.ACTION_INDEX);
                break;
            case VIDEO:
                setHeaderRenderer(VideoTableFormat.LENGTH_INDEX, new TableCellHeaderRenderer(SwingConstants.TRAILING));
                setCellRenderer(VideoTableFormat.LENGTH_INDEX, timeRenderer.get());
                setCellRenderer(VideoTableFormat.SIZE_INDEX, fileSizeRenderer.get());
                setCellRenderer(VideoTableFormat.NAME_INDEX, nameRenderer.get());
                setCellRenderer(VideoTableFormat.ACTION_INDEX, removeRenderer.get());
                setCellEditor(VideoTableFormat.ACTION_INDEX, removeEditor);
                setUnsortable(VideoTableFormat.ACTION_INDEX);
                break;
            case IMAGE:
                setCellRenderer(ImageTableFormat.SIZE_INDEX, fileSizeRenderer.get());
                setCellRenderer(ImageTableFormat.ACTION_INDEX, removeRenderer.get());
                setCellEditor(ImageTableFormat.ACTION_INDEX, removeEditor);
                setUnsortable(ImageTableFormat.ACTION_INDEX);
                break;
            case DOCUMENT:
                setCellRenderer(DocumentTableFormat.NAME_INDEX, iconLabelRenderer);
                setCellRenderer(DocumentTableFormat.SIZE_INDEX, fileSizeRenderer.get());
                setCellRenderer(DocumentTableFormat.ACTION_INDEX, removeRenderer.get());
                setCellEditor(DocumentTableFormat.ACTION_INDEX, removeEditor);
                setUnsortable(DocumentTableFormat.ACTION_INDEX);
                break;
            case PROGRAM:
                setCellRenderer(ProgramTableFormat.NAME_INDEX, iconLabelRenderer);
                setCellRenderer(ProgramTableFormat.SIZE_INDEX, fileSizeRenderer.get());
                setCellRenderer(ProgramTableFormat.ACTION_INDEX, removeRenderer.get());
                setCellEditor(ProgramTableFormat.ACTION_INDEX, removeEditor);
                setUnsortable(ProgramTableFormat.ACTION_INDEX);
                break;
            case OTHER:
                setCellRenderer(OtherTableFormat.NAME_INDEX, iconLabelRenderer);
                setCellRenderer(OtherTableFormat.SIZE_INDEX, fileSizeRenderer.get());
                setCellRenderer(OtherTableFormat.ACTION_INDEX, removeRenderer.get());
                setCellEditor(OtherTableFormat.ACTION_INDEX, removeEditor);
                setUnsortable(OtherTableFormat.ACTION_INDEX);
                break;
            case TORRENT:
                
                MultilineTooltipRenderer renderer = new MultilineTooltipRenderer();
                setCellRenderer(TorrentTableFormat.TRACKERS_INDEX, renderer);
                setCellRenderer(TorrentTableFormat.FILES_INDEX, renderer);
                
                setCellRenderer(TorrentTableFormat.NAME_INDEX, iconLabelRenderer);
                setCellRenderer(TorrentTableFormat.SIZE_INDEX, fileSizeRenderer.get());
                setCellRenderer(TorrentTableFormat.ACTION_INDEX, removeRenderer.get());
                setCellEditor(TorrentTableFormat.ACTION_INDEX, removeEditor);
                
                setUnsortable(TorrentTableFormat.ACTION_INDEX);
                setUnsortable(TorrentTableFormat.TRACKERS_INDEX);
                setUnsortable(TorrentTableFormat.FILES_INDEX);
                break;
            default:
                throw new IllegalArgumentException("Unknown category:" + category);
            }
        } else {
            setCellRenderer(AllTableFormat.PLAY_INDEX, isPlayingRenderer.get());
            setUnsortable(AllTableFormat.PLAY_INDEX);
            setCellRenderer(AllTableFormat.NAME_INDEX, nameCategoryRenderer.get());
            setCellRenderer(AllTableFormat.SIZE_INDEX, fileSizeRenderer.get());
            setCellRenderer(AllTableFormat.ACTION_INDEX, removeRenderer.get());
            setCellEditor(AllTableFormat.ACTION_INDEX, removeEditor);
            setUnsortable(AllTableFormat.ACTION_INDEX);
        }
    }
    
    /**
     * Assigns the specified header renderer to the specified column in the 
     * Table view column model.   
     */
    protected void setHeaderRenderer(int column, TableCellRenderer headerRenderer) {
        TableColumnModel tcm = getColumnModel();
        TableColumn tc = tcm.getColumn(column);
        tc.setHeaderRenderer(headerRenderer);
    }
    
    private void setCellRenderer(int column, TableCellRenderer renderer) {
        getColumnModel().getColumn(column).setCellRenderer(renderer);
    }
    
    private void setCellEditor(int column, TableCellEditor editor) {
        getColumnModel().getColumn(column).setCellEditor(editor);
    }
    
    /** Prevents the given column from being sortable. */
    private void setUnsortable(int column) {
        ((TableColumnExt)getColumnModel().getColumn(column)).setSortable(false);
    }

    /** Returns all currently selected LocalFileItems. */
    public List<LocalFileItem> getSelection() {
        return cachedEventSelectionModel.getSelected();
    }
    
    /**
     * Handles double clicking a row in the library table.
     */
    private class DoubleClickHandler implements TableDoubleClickHandler{
        @Override
        public void handleDoubleClick(int row) {
            if (getSelectedItem() == null) { 
                getSelectionModel().setSelectionInterval(row, row);
            }
            launchAction.get().actionPerformed(null);
        }
    }
    
    @Override
    protected String getToolTipText(int row, int col) {
        Object value = getValueAt(row, col);
        if(value != null && value instanceof LocalFileItem) {
            LocalFileItem localFileItem = (LocalFileItem) value;
            if(!localFileItem.isLoaded()) {
                return I18n.tr("This file is still processing.");
            } else if(!localFileItem.isShareable()) {
                return I18n.tr("This file can not be shared.");
            }
        }  
        return super.getToolTipText(row, col);
    }
}
