package org.limewire.ui.swing.properties;

import java.awt.Color;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.io.File;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.BorderFactory;
import javax.swing.ComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.ScrollPaneConstants;
import javax.swing.UIManager;
import javax.swing.table.DefaultTableModel;
import javax.swing.text.JTextComponent;

import net.miginfocom.swing.MigLayout;

import org.jdesktop.application.Resource;
import org.limewire.bittorrent.Torrent;
import org.limewire.bittorrent.TorrentStatus;
import org.limewire.core.api.Category;
import org.limewire.core.api.FilePropertyKey;
import org.limewire.core.api.URN;
import org.limewire.core.api.download.DownloadItem;
import org.limewire.core.api.endpoint.RemoteHost;
import org.limewire.core.api.library.LocalFileItem;
import org.limewire.core.api.library.MetaDataException;
import org.limewire.core.api.library.MetaDataManager;
import org.limewire.core.api.library.PropertiableFile;
import org.limewire.core.api.properties.PropertyDictionary;
import org.limewire.core.api.search.SearchResult;
import org.limewire.core.api.spam.SpamManager;
import org.limewire.core.api.upload.UploadItem;
import org.limewire.friend.api.Friend;
import org.limewire.ui.swing.action.AbstractAction;
import org.limewire.ui.swing.action.UrlAction;
import org.limewire.ui.swing.components.CollectionBackedComboBoxModel;
import org.limewire.ui.swing.components.FilteredDocument;
import org.limewire.ui.swing.components.FocusJOptionPane;
import org.limewire.ui.swing.components.HyperlinkButton;
import org.limewire.ui.swing.components.TextFieldClipboardControl;
import org.limewire.ui.swing.library.LibraryMediator;
import org.limewire.ui.swing.listener.MousePopupListener;
import org.limewire.ui.swing.properties.FileInfoDialog.FileInfoType;
import org.limewire.ui.swing.search.model.VisualSearchResult;
import org.limewire.ui.swing.table.DefaultLimeTableCellRenderer;
import org.limewire.ui.swing.table.MouseableTable;
import org.limewire.ui.swing.util.FontUtils;
import org.limewire.ui.swing.util.GuiUtils;
import org.limewire.ui.swing.util.I18n;
import org.limewire.ui.swing.util.NativeLaunchUtils;
import org.limewire.util.Base32;
import org.limewire.util.StringUtils;

/**
 * Displays meta data information about the given PropertiableFile. If the file
 * is local, the meta data can be edited and saved as xml. 
 */
public class FileInfoGeneralPanel implements FileInfoPanel {

    @Resource private Color foreground;
    @Resource private Font smallFont;
    @Resource private Font mediumFont;
    @Resource private Font headerFont;
    
    private final JPanel component;
    
    private final FileInfoType type;
    private PropertiableFile propertiableFile;
    private final PropertyDictionary propertyDictionary;
    
    private final SpamManager spamManager;
    private final MetaDataManager metaDataManager;
    private final LibraryMediator libraryMediator;
    private JTextField locationField;

    
    private final Map<FilePropertyKey, JComponent> changedProps = new HashMap<FilePropertyKey, JComponent>();
    
    public FileInfoGeneralPanel(FileInfoType type, PropertiableFile propertiableFile,
            PropertyDictionary propertyDictionary, SpamManager spamManager, MetaDataManager metaDataManager, LibraryMediator libraryMediator) {
        this.type = type;
        this.propertiableFile = propertiableFile;
        
        this.propertyDictionary = propertyDictionary;
        this.spamManager = spamManager;
        this.metaDataManager = metaDataManager;
        this.libraryMediator = libraryMediator;
        
        component = new JPanel(new MigLayout("fillx"));
        
        GuiUtils.assignResources(this);
        
        init();
    }
    
    @Override
	public JComponent getComponent() {
        return component;
    }

    @Override
    public boolean hasChanged() {
        return changedProps.size() != 0 && propertiableFile instanceof LocalFileItem;
    }

    @Override
    public void updatePropertiableFile(PropertiableFile file) {
        this.propertiableFile = file;
        String path = ((LocalFileItem)propertiableFile).getFile().getAbsolutePath();
        locationField.setText(path);
        locationField.setToolTipText(path);
        locationField.setCaretPosition(0);
    }

    @Override
    public void save() {
        switch(type) {
        case LOCAL_FILE: 
            if (hasChanged()) {
                LocalFileItem item = (LocalFileItem) propertiableFile;
                Map<FilePropertyKey, Object> newData = new HashMap<FilePropertyKey, Object>();
                for (FilePropertyKey key : changedProps.keySet()) {
                    JComponent component = changedProps.get(key);
                    if(component instanceof JTextComponent) {
                        newData.put(key, ((JTextComponent)component).getText().trim());
                    } else if(component instanceof JComboBox) {
                        newData.put(key, ((JComboBox)component).getSelectedItem());
                    }
                }
                try {
                    metaDataManager.save(item, newData);
                } catch (MetaDataException e) {
                    String message = I18n.tr("Unable to save metadata changes.");
                    FocusJOptionPane.showMessageDialog(GuiUtils.getMainFrame(), message, I18n.tr("View File Info"), 
                            JOptionPane.INFORMATION_MESSAGE);
                }
            }     
            break;
        }

    }
    
    @Override
    public void dispose() {
        //no listeners registered
    }
    
    private void init() {
        component.setOpaque(false);
        
        createError();

        if(propertiableFile.getCategory() != Category.OTHER) {
            createEditableDetails();
        }

        createLocation();
        
        createUrnSection();

    }

    private void createUrnSection() {
        URN urn = propertiableFile.getUrn();

        switch(propertiableFile.getCategory()) {
            case TORRENT :
                if (urn != null && type == FileInfoType.REMOTE_FILE) {
                    Torrent torrent = (Torrent)propertiableFile.getProperty(FilePropertyKey.TORRENT);
                    String sha1HexString = null; 
                    if (torrent != null) {
                        sha1HexString = torrent.getSha1();
                    }
                    if (sha1HexString != null) {
                        // Manually convert the base 16 string hash to a more recognisable format
                        //  without going into core
                        String payloadHashString = "urn:sha1:" + Base32.encode(StringUtils.fromHexString(sha1HexString));
                        String torrentHashString = urn.toString(); 
                        
                        if (!torrentHashString.equals(payloadHashString)) {
                            JLabel torrentHashLabel = createPlainLabel(I18n.tr("torrent file hash:"));
                            JLabel payloadHashLabel = createPlainLabel(I18n.tr("payload hash:"));
                        
                            FontUtils.bold(torrentHashLabel);
                            FontUtils.bold(payloadHashLabel);
                        
                            component.add(createHeaderLabel(I18n.tr("Hashes")), "wrap");
                            component.add(torrentHashLabel, "split 2, gapright 5");
                            component.add(createLabelField(torrentHashString), "gapleft 0, growx, span, wrap");
                            component.add(payloadHashLabel, "split 2, gapright 5"); 
                            component.add(createLabelField(payloadHashString), "gapleft 0, growx, span, wrap");
                            break;
                        }
                    }
                }
                // else, no payload hash, fall through... 
            default :   
                if (urn != null) {
                    component.add(createHeaderLabel(I18n.tr("Hash")), "wrap");
                    component.add(createLabelField(propertiableFile.getUrn().toString()), "growx, span, wrap");
                }
                break;
        }
    }
    
    /**
     * If a torrent download that is in error. The internal error message will be displayed, 
     * to help debug the situation.
     */
    private void createError() {
        Torrent torrent = (Torrent) propertiableFile.getProperty(FilePropertyKey.TORRENT);
        TorrentStatus status = torrent != null ? torrent.getStatus() : null;
        if(status != null && status.isError()) {
            component.add(createHeaderLabel(I18n.tr("Error")), "span, gaptop 15, wrap");
            component.add(createLabelField(status.getError()), "span, growx, wrap");
        }
    }

    private void createEditableDetails() {
               
        JScrollPane descriptionScrollPane = new JScrollPane(createEditableTextArea(propertiableFile.getPropertyString(FilePropertyKey.DESCRIPTION), FilePropertyKey.DESCRIPTION), 
                ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        
        switch(propertiableFile.getCategory()) {
        case AUDIO:
            component.add(createHeaderLabel(I18n.tr("Details")), "span, wrap");
            component.add(createPlainLabel(I18n.tr("Title")), "growx, pushx 100");
            component.add(createPlainLabel(I18n.tr("Artist")), "growx, span 3, pushx 50, wrap");
            component.add(createEditableTextField(propertiableFile.getPropertyString(FilePropertyKey.TITLE), FilePropertyKey.TITLE), "growx, pushx 100, gapright unrelated");
            component.add(createEditableTextField(propertiableFile.getPropertyString(FilePropertyKey.AUTHOR), FilePropertyKey.AUTHOR), "growx, span 3, pushx 50, wrap");
            //second line
            component.add(createPlainLabel(I18n.tr("Album")), "pushx 100");
            component.add(createPlainLabel(I18n.tr("Genre")), "pushx 17");
            component.add(createPlainLabel(I18n.tr("Year")), "pushx 17");
            component.add(createPlainLabel(I18n.tr("Track")), "pushx 17, wrap");
            
            component.add(createEditableTextField(propertiableFile.getPropertyString(FilePropertyKey.ALBUM), FilePropertyKey.ALBUM), "growx, pushx 100, gapright unrelated");
            component.add(createEditableComboBox(propertiableFile.getPropertyString(FilePropertyKey.GENRE), getGenres(propertiableFile), FilePropertyKey.GENRE), "growx, pushx 17, gapright unrelated");
            component.add(createEditableTextField(propertiableFile.getPropertyString(FilePropertyKey.YEAR), FilePropertyKey.YEAR), "growx, pushx 17, wmin 35, gapright unrelated");
            component.add(createEditableTextField(propertiableFile.getPropertyString(FilePropertyKey.TRACK_NUMBER), FilePropertyKey.TRACK_NUMBER), "growx, wmin 35, pushx 17, wrap");

            //TODO: temporarily taken out since we aren't reading/writing the description field in id3 tags
            //third line
            component.add(createPlainLabel(I18n.tr("Description")), "span, wrap");
            component.add(descriptionScrollPane, "span, growx, hmin 42");
            break;

        case VIDEO:
            component.add(createHeaderLabel(I18n.tr("Details")), "span, wrap");
            component.add(createPlainLabel(I18n.tr("Title")), "wrap");
            component.add(createEditableTextField(propertiableFile.getPropertyString(FilePropertyKey.TITLE), FilePropertyKey.TITLE), "span, growx, wrap");
            component.add(createPlainLabel(I18n.tr("Genre")), "pushx 35");
            component.add(createPlainLabel(I18n.tr("Rating")), "pushx 35");
            component.add(createPlainLabel(I18n.tr("Year")), "pushx 30, wrap");
            component.add(createEditableComboBox(propertiableFile.getPropertyString(FilePropertyKey.GENRE), getGenres(propertiableFile), FilePropertyKey.GENRE), "growx, pushx 35, gapright unrelated");
            component.add(createEditableComboBox(propertiableFile.getPropertyString(FilePropertyKey.RATING), getRatings(propertiableFile), FilePropertyKey.RATING), "growx, pushx 35, gapright unrelated");
            component.add(createEditableTextField(propertiableFile.getPropertyString(FilePropertyKey.YEAR), FilePropertyKey.YEAR), "growx, wmin 35, pushx 30, wrap");
            component.add(createPlainLabel(I18n.tr("Description")), "wrap");
            component.add(descriptionScrollPane, "span, growx, hmin 42, wrap");
            break;
        case IMAGE:
            component.add(createHeaderLabel(I18n.tr("Details")), "span, wrap");
            component.add(createPlainLabel(I18n.tr("Title")), "wrap");
            component.add(createEditableTextField(propertiableFile.getPropertyString(FilePropertyKey.TITLE), FilePropertyKey.TITLE), "span, growx, wrap");
            component.add(createPlainLabel(I18n.tr("Description")), "wrap");
            component.add(descriptionScrollPane, "span, growx, hmin 42, wrap");
            break;
        case DOCUMENT:
            component.add(createHeaderLabel(I18n.tr("Details")), "span, wrap");
            component.add(createPlainLabel(I18n.tr("Author")), "wrap");
            component.add(createEditableTextField(propertiableFile.getPropertyString(FilePropertyKey.AUTHOR), FilePropertyKey.AUTHOR), "span, growx, wrap");
            component.add(createPlainLabel(I18n.tr("Description")), "wrap");
            component.add(descriptionScrollPane, "span, growx, hmin 42, wrap");
            break;
        case PROGRAM:
            component.add(createHeaderLabel(I18n.tr("Details")), "span, wrap");
            component.add(createPlainLabel(I18n.tr("Title")), "wrap");
            component.add(createEditableTextField(propertiableFile.getPropertyString(FilePropertyKey.TITLE), FilePropertyKey.TITLE), "growx, span, wrap");
            component.add(createPlainLabel(I18n.tr("Platform")), "growx 30");
            component.add(createPlainLabel(I18n.tr("Company")), "growx 70, wrap");
            component.add(createEditableComboBox(propertiableFile.getPropertyString(FilePropertyKey.PLATFORM), getPlatforms(propertiableFile), FilePropertyKey.PLATFORM), "growx 30, gapright 5");
            component.add(createEditableTextField(propertiableFile.getPropertyString(FilePropertyKey.COMPANY), FilePropertyKey.COMPANY), "growx 70, wrap");
            break;
        case OTHER:
            break;
        }
    }
    
    private void createLocation() {
        switch(type) {
        case LOCAL_FILE:
            if(propertiableFile instanceof LocalFileItem) {
                HyperlinkButton locateOnDisk = new HyperlinkButton(
                    new AbstractAction(I18n.tr("Locate on Disk")) {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                            NativeLaunchUtils.launchExplorer(((LocalFileItem)propertiableFile).getFile());
                        }
                    });
                
                HyperlinkButton locateInLibrary = new HyperlinkButton( 
                    new AbstractAction(I18n.tr("Locate in Library")) {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                            component.getRootPane().getParent().setVisible(false);
                            libraryMediator.selectInLibrary(((LocalFileItem)propertiableFile).getFile());
                        }
                    });
            
                component.add(createHeaderLabel(I18n.tr("Location")), "gaptop 15");
                component.add(locateOnDisk, "span, alignx right, split");
                component.add(locateInLibrary, "gapleft 15, wrap");
                
                locationField = createLabelField("");
                updatePropertiableFile(propertiableFile);
                
                component.add(locationField, "span, growx, wrap");
            }
            break;
        case REMOTE_FILE:
            
            
            component.add(createHeaderLabel(I18n.tr("Location")), "span, gaptop 15, wrap");
            
            if(propertiableFile instanceof VisualSearchResult) {
                
                VisualSearchResult vsr = ((VisualSearchResult)propertiableFile);
                                
                final ReadOnlyTableModel model = new ReadOnlyTableModel();
                final MouseableTable table = new MouseableTable(model);
                table.setDefaultRenderer(Object.class, new DefaultLimeTableCellRenderer());
                
                model.setColumnIdentifiers(new Object[] { I18n.tr("Name"), I18n.tr("Address"), I18n.tr("Filename") });
    
                // Find a referrer link
                Object referrer = null;
                
                for (SearchResult result : vsr.getCoreSearchResults()) {
                    
                    referrer = result.getProperty(FilePropertyKey.REFERRER);
                    
                    // Don't need to display table if we have a referrer
                    if (referrer != null) {
                        break;
                    }
                    
                    RemoteHost host = result.getSource();
                    Friend f = host.getFriendPresence().getFriend();
                    if (f.getName() != null || f.getRenderName() != null) {
                        model.addRow(new Object[] {
                            f.getRenderName(),
                            f.getName(),
                            result.getFileName()
                        });
                    }
                }
                
                // The referrer takes precedence since it indicates a torrent web search
                if (referrer instanceof URI) {
                    URI referrerURI = (URI) referrer;
                    component.add(new HyperlinkButton(new UrlAction(I18n.tr("Open Website"), referrerURI.toASCIIString())), "gapbottom 15, span, wrap");
                } else if (model.getRowCount() != 0) {
                    component.add(new JScrollPane(table), "span, grow, wrap");
                
                    table.addMouseListener(new MousePopupListener() {
                        @Override
                        public void handlePopupMouseEvent(final MouseEvent e) {
                            JPopupMenu blockingMenu = new JPopupMenu();
                            blockingMenu.add(new AbstractAction(I18n.tr("Block Address")) {
                                @Override
                                public void actionPerformed(ActionEvent actionEvent) {
                                    int blockRow = table.rowAtPoint(e.getPoint());
                                    table.getSelectionModel().setSelectionInterval(blockRow, blockRow);
                                    Object value = model.getValueAt(blockRow, 1);
                                    if (value != null) {
                                        addToFilterList(value.toString());
                                    }
                                }
                            });
                            blockingMenu.show(table, e.getX(), e.getY());
                        }
                    });
                } else {
                    component.add(createPlainLabel(I18n.tr("Unable to Locate")), "gapbottom 15, span, wrap");
                }
            }
            break;
        case DOWNLOADING_FILE:
        case UPLOADING_FILE:

            HyperlinkButton locateOnDisk2 = new HyperlinkButton(
                    new AbstractAction(I18n.tr("Locate on Disk")) {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                            if (propertiableFile instanceof DownloadItem) {
                                if (((DownloadItem)propertiableFile).getDownloadingFile() != null) {
                                    NativeLaunchUtils.launchExplorer(((DownloadItem)propertiableFile).getDownloadingFile());
                                } 
                            } else if (propertiableFile instanceof UploadItem) {
                                if (((UploadItem)propertiableFile).getFile() != null) {
                                    NativeLaunchUtils.launchExplorer(((UploadItem)propertiableFile).getFile());
                                } 
                            }
                        }
                    });

            HyperlinkButton locateInLibrary2 = new HyperlinkButton( 
                    new AbstractAction(I18n.tr("Locate in Library")) {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                            component.getRootPane().getParent().setVisible(false);

                            if (propertiableFile instanceof DownloadItem) {
                                DownloadItem item = (DownloadItem)propertiableFile;
                                libraryMediator.locateInLibrary(item);
                            } else if (propertiableFile instanceof UploadItem) {
                                UploadItem item = (UploadItem)propertiableFile;
                                libraryMediator.locateInLibrary(item);
                            }
                            
                        }
                    });

            component.add(createHeaderLabel(I18n.tr("Location")), "gaptop 15");

            component.add(locateOnDisk2, "span, alignx right, split");
            
            // TODO: is this even ever possible for downloads?  incomplete uploads?
            component.add(locateInLibrary2, "gapleft 15, wrap");

            File launchableFile = null;
            if (propertiableFile instanceof DownloadItem) {
                launchableFile = ((DownloadItem)propertiableFile).getDownloadingFile();
            } else if (propertiableFile instanceof UploadItem) {
                launchableFile = ((UploadItem)propertiableFile).getFile();
            }

            if(launchableFile != null && launchableFile.getAbsoluteFile() != null) {
                component.add(createLabelField(launchableFile.getAbsolutePath()), "span, growx, wrap");
            }
            else {
                component.add(createLabelField(propertiableFile.getFileName()), "span, growx, wrap");
            }
            
            break;
        }
    }
    
    private JLabel createPlainLabel(String text) {
        JLabel label = new JLabel(text);
        label.setFont(smallFont);
        label.setForeground(foreground);
        return label;
    }
    
    private JLabel createHeaderLabel(String text) { 
        JLabel label = new JLabel(text);
        label.setFont(headerFont);
        label.setForeground(foreground);
        return label;
    }
    
    private JTextField createEditableTextField(String text, FilePropertyKey key) {
        JTextField field = new JTextField();
        TextFieldClipboardControl.install(field);
        if(type != FileInfoType.LOCAL_FILE)
            field.setEditable(false);
        else {
            changedProps.put(key, field);
            FilteredDocument.configure(field, key);
        }
        field.setCaretPosition(0);
        field.setFont(mediumFont);
        field.setForeground(foreground);
        field.setText(text);
        return field;
    }
    
    private JTextArea createEditableTextArea(String text, FilePropertyKey key) {
        JTextArea area = new JTextArea(text);
        TextFieldClipboardControl.install(area);
        if(type != FileInfoType.LOCAL_FILE) {
            area.setEditable(false);
            area.setBackground(UIManager.getLookAndFeel().getDefaults().getColor("TextField.disabledBackground"));
        } else {
            changedProps.put(key, area);
        }
        area.setFont(mediumFont);
        area.setForeground(foreground);
        area.setLineWrap(true);
        area.setWrapStyleWord(true);
        return area;
    }
    
    /**
     * Returns a List of values to populate the genre combobox with.
     */
    private List<String> getGenres(final PropertiableFile propertiableSeed) {
        switch (propertiableSeed.getCategory()) {
        case AUDIO:
            return propertyDictionary.getAudioGenres();
        case VIDEO:
            return propertyDictionary.getVideoGenres();
        default:
            return Collections.emptyList();
        }
    }
    
    /**
     * If this combo box is editable, sets the model and selects the box, otherwise
     * returns a non-editable JTextField.
     */
    private JComponent createEditableComboBox(String selection, List<String> model, FilePropertyKey key) {
        if(type != FileInfoType.LOCAL_FILE) {
            return createEditableTextField(selection, null);
        } else {
            JComboBox comboBox = new JComboBox();
            changedProps.put(key, comboBox);
            setupComboBox(comboBox, selection, model);
            return comboBox;
        }
    }
    
    /**
     * Loads a combo box and selects the currently selected item.
     */
    private void setupComboBox(JComboBox comboBox, String current, List<String> possibles) {
        if(current == null) {
            current = "";
        }
        
        comboBox.setForeground(foreground);
        
        // If any are listed, current is non-empty, and possibles doesn't contain, add it in.
        if(!possibles.contains(current) && !current.equals("") && possibles.size() > 0) {
            possibles = new ArrayList<String>(possibles);            
            possibles.add(0, current);
            possibles = Collections.unmodifiableList(possibles);
        }
        
        ComboBoxModel model = new CollectionBackedComboBoxModel(possibles);
        comboBox.setModel(model);
        comboBox.setSelectedItem(current);
    }
    
    /**
     * Returns a list of values for the rating comboBox
     */
    private List<String> getRatings(final PropertiableFile propertiableSeed) {
        switch (propertiableSeed.getCategory()) {
        case VIDEO:
            return propertyDictionary.getVideoRatings();
        default:
            return Collections.emptyList();
        }
    }
    
    /**
     * Returns a list of values for the OS combobox.
     */
    private List<String> getPlatforms(final PropertiableFile propertiableSeed) {
        switch (propertiableSeed.getCategory()) {
        case PROGRAM:
            return propertyDictionary.getApplicationPlatforms();
        default:
            return Collections.emptyList();
        }
    }
    
    /**
     * Blacklists the given ipAddress.
     */
    private void addToFilterList(String ipAddress) {
        spamManager.addToBlackList(ipAddress);
    }
    
    private static class ReadOnlyTableModel extends DefaultTableModel {
        @Override
        public boolean isCellEditable(int row, int column) {
            return false;
        }
    }
    
    private JTextField createLabelField(String text) {
        JTextField field = new JTextField(text);
        TextFieldClipboardControl.install(field);
        field.setCaretPosition(0);
        field.setEditable(false);
        field.setOpaque(false);
        field.setFont(smallFont);
        field.setForeground(foreground);
        field.setBorder(BorderFactory.createEmptyBorder(0,1,0,1));
        return field;
    }
}
