package net.sf.jabref.gui.search;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.table.TableColumnModel;

import net.sf.jabref.Globals;
import net.sf.jabref.gui.BasePanel;
import net.sf.jabref.gui.GUIGlobals;
import net.sf.jabref.gui.IconTheme;
import net.sf.jabref.gui.JabRefFrame;
import net.sf.jabref.gui.PreviewPanel;
import net.sf.jabref.gui.TransferableBibtexEntry;
import net.sf.jabref.gui.desktop.JabRefDesktop;
import net.sf.jabref.gui.externalfiletype.ExternalFileMenuItem;
import net.sf.jabref.gui.filelist.FileListEntry;
import net.sf.jabref.gui.filelist.FileListTableModel;
import net.sf.jabref.gui.keyboard.KeyBinding;
import net.sf.jabref.gui.maintable.MainTableNameFormatter;
import net.sf.jabref.gui.renderer.GeneralRenderer;
import net.sf.jabref.gui.util.comparator.IconComparator;
import net.sf.jabref.logic.bibtex.comparator.EntryComparator;
import net.sf.jabref.logic.bibtex.comparator.FieldComparator;
import net.sf.jabref.logic.l10n.Localization;
import net.sf.jabref.logic.search.SearchQuery;
import net.sf.jabref.model.entry.BibEntry;
import net.sf.jabref.model.entry.FieldName;
import net.sf.jabref.model.entry.FieldProperty;
import net.sf.jabref.model.entry.InternalBibtexFields;
import net.sf.jabref.model.strings.StringUtil;
import net.sf.jabref.preferences.SearchPreferences;

import ca.odell.glazedlists.BasicEventList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.SortedList;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;
import ca.odell.glazedlists.gui.AbstractTableComparatorChooser;
import ca.odell.glazedlists.gui.AdvancedTableFormat;
import ca.odell.glazedlists.swing.DefaultEventSelectionModel;
import ca.odell.glazedlists.swing.DefaultEventTableModel;
import ca.odell.glazedlists.swing.GlazedListsSwing;
import ca.odell.glazedlists.swing.TableComparatorChooser;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * Dialog to display search results, potentially from more than one BasePanel, with
 * possibility to preview and to locate each entry in the main window.
 */
public class SearchResultFrame {

    private static final Log LOGGER = LogFactory.getLog(SearchResultFrame.class);

    private final JabRefFrame frame;

    private JFrame searchResultFrame;
    private static final String[] FIELDS = new String[] {
            FieldName.AUTHOR, FieldName.TITLE, FieldName.YEAR, FieldName.JOURNAL
    };
    private static final int DATABASE_COL = 0;
    private static final int FILE_COL = 1;
    private static final int URL_COL = 2;
    private static final int PAD = 3;
    private final JLabel fileLabel = new JLabel(IconTheme.JabRefIcon.FILE.getSmallIcon());
    private final JLabel urlLabel = new JLabel(IconTheme.JabRefIcon.WWW.getSmallIcon());

    private final JSplitPane contentPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);

    private final Rectangle toRect = new Rectangle(0, 0, 1, 1);
    private final EventList<BibEntry> entries = new BasicEventList<>();

    private final Map<BibEntry, BasePanel> entryHome = new HashMap<>();
    private DefaultEventTableModel<BibEntry> model;

    private SortedList<BibEntry> sortedEntries;
    private JTable entryTable;
    private PreviewPanel preview;

    private SearchQuery searchQuery;
    private boolean globalSearch;


    public SearchResultFrame(JabRefFrame frame, String title, SearchQuery searchQuery, boolean globalSearch) {
        this.frame = Objects.requireNonNull(frame);
        this.searchQuery = searchQuery;
        this.globalSearch = globalSearch;
        frame.getGlobalSearchBar().setSearchResultFrame(this);
        init(Objects.requireNonNull(title));
    }

    private void init(String title) {
        searchResultFrame = new JFrame();
        searchResultFrame.setTitle(title);
        searchResultFrame.setIconImage(IconTheme.getImage("jabrefIcon48").getImage());

        preview = new PreviewPanel(null, null);

        sortedEntries = new SortedList<>(entries, new EntryComparator(false, true, FieldName.AUTHOR));
        model = (DefaultEventTableModel<BibEntry>) GlazedListsSwing.eventTableModelWithThreadProxyList(sortedEntries,
                new EntryTableFormat());
        entryTable = new JTable(model);

        GeneralRenderer renderer = new GeneralRenderer(Color.white);
        entryTable.setDefaultRenderer(JLabel.class, renderer);
        entryTable.setDefaultRenderer(String.class, renderer);
        setWidths();
        TableComparatorChooser<BibEntry> tableSorter =
                TableComparatorChooser.install(entryTable, sortedEntries,
                        AbstractTableComparatorChooser.MULTIPLE_COLUMN_KEYBOARD);
        setupComparatorChooser(tableSorter);
        JScrollPane sp = new JScrollPane(entryTable);

        final DefaultEventSelectionModel<BibEntry> selectionModel = (DefaultEventSelectionModel<BibEntry>) GlazedListsSwing
                .eventSelectionModelWithThreadProxyList(sortedEntries);
        entryTable.setSelectionModel(selectionModel);
        selectionModel.getSelected().addListEventListener(new EntrySelectionListener());
        entryTable.addMouseListener(new TableClickListener());

        contentPane.setTopComponent(sp);
        contentPane.setBottomComponent(preview);

        // Key bindings:
        AbstractAction closeAction = new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                dispose();
            }
        };

        ActionMap actionMap = contentPane.getActionMap();
        InputMap inputMap = contentPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
        inputMap.put(Globals.getKeyPrefs().getKey(KeyBinding.CLOSE_DIALOG), "close");
        inputMap.put(Globals.getKeyPrefs().getKey(KeyBinding.CLOSE_DATABASE), "close");
        actionMap.put("close", closeAction);

        actionMap = entryTable.getActionMap();
        inputMap = entryTable.getInputMap();
        //Override 'selectNextColumnCell' and 'selectPreviousColumnCell' to move rows instead of cells on TAB
        actionMap.put("selectNextColumnCell", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                selectNextEntry();
            }
        });
        actionMap.put("selectPreviousColumnCell", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                selectPreviousEntry();
            }
        });
        actionMap.put("selectNextRow", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                selectNextEntry();
            }
        });
        actionMap.put("selectPreviousRow", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                selectPreviousEntry();
            }
        });

        String selectFirst = "selectFirst";
        inputMap.put(Globals.getKeyPrefs().getKey(KeyBinding.SELECT_FIRST_ENTRY), selectFirst);
        actionMap.put(selectFirst, new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent event) {
                selectFirstEntry();
            }
        });

        String selectLast = "selectLast";
        inputMap.put(Globals.getKeyPrefs().getKey(KeyBinding.SELECT_LAST_ENTRY), selectLast);
        actionMap.put(selectLast, new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent event) {
                selectLastEntry();
            }
        });

        actionMap.put("copy", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (!selectionModel.getSelected().isEmpty()) {
                    List<BibEntry> bes = selectionModel.getSelected();
                    TransferableBibtexEntry trbe = new TransferableBibtexEntry(bes);
                    // ! look at ClipBoardManager
                    Toolkit.getDefaultToolkit().getSystemClipboard()
                            .setContents(trbe, frame.getCurrentBasePanel());
                    frame.output(Localization.lang("Copied") + ' ' + (bes.size() > 1 ? bes.size() + " "
                            + Localization.lang("entries")
                            : "1 " + Localization.lang("entry") + '.'));
                }
            }
        });

        // override standard enter-action; enter opens the selected entry
        entryTable.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "Enter");
        actionMap.put("Enter", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent ae) {
                BibEntry entry = sortedEntries.get(entryTable.getSelectedRow());
                selectEntryInBasePanel(entry);
            }
        });

        searchResultFrame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowOpened(WindowEvent e) {
                contentPane.setDividerLocation(0.5f);
            }

            @Override
            public void windowClosing(WindowEvent event) {
                dispose();
            }
        });

        searchResultFrame.getContentPane().add(contentPane, BorderLayout.CENTER);

        // Remember and default to last size:
        SearchPreferences searchPreferences = new SearchPreferences(Globals.prefs);
        searchResultFrame.setSize(searchPreferences.getSeachDialogWidth(), searchPreferences.getSeachDialogHeight());
        searchResultFrame.setLocation(searchPreferences.getSearchDialogPosX(), searchPreferences.getSearchDialogPosY());

        searchResultFrame.addComponentListener(new ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent e) {
                new SearchPreferences(Globals.prefs)
                        .setSearchDialogWidth(searchResultFrame.getSize().width)
                        .setSearchDialogHeight(searchResultFrame.getSize().height);
            }

            @Override
            public void componentMoved(ComponentEvent e) {
                new SearchPreferences(Globals.prefs)
                        .setSearchDialogPosX(searchResultFrame.getLocation().x)
                        .setSearchDialogPosY(searchResultFrame.getLocation().y);
            }
        });
    }

    /**
     * Control the visibility of the dialog.
     * @param visible true to show dialog, false to hide.
     */
    public void setVisible(boolean visible) {
        searchResultFrame.setVisible(visible);
    }

    public void selectFirstEntry() {
        selectEntry(0);
    }

    public void selectLastEntry() {
        selectEntry(entryTable.getRowCount() - 1);
    }

    public void selectPreviousEntry() {
        selectEntry((entryTable.getSelectedRow() - 1 + entryTable.getRowCount()) % entryTable.getRowCount());
    }

    public void selectNextEntry() {
        selectEntry((entryTable.getSelectedRow() + 1) % entryTable.getRowCount());
    }

    public void selectEntry(int index) {
        if (index >= 0 && index < entryTable.getRowCount()) {
            entryTable.changeSelection(index, 0, false, false);
        } else {
            contentPane.setDividerLocation(1.0f);
        }
    }

    /**
     * Set up the comparators for each column, so the user can modify sort order
     * by clicking the column labels.
     * @param comparatorChooser The comparator chooser controlling the sort order.
     */
    private void setupComparatorChooser(TableComparatorChooser<BibEntry> comparatorChooser) {
        List<Comparator> comparators;
        // Icon columns:
        for (int i = 0; i < PAD; i++) {
            comparators = comparatorChooser.getComparatorsForColumn(i);
            comparators.clear();
            if (i == FILE_COL) {
                comparators.add(new IconComparator(Collections.singletonList(FieldName.FILE)));
            } else if (i == URL_COL) {
                comparators.add(new IconComparator(Collections.singletonList(FieldName.URL)));
            } else if (i == DATABASE_COL) {
                comparators.add((entry1, entry2) -> {
                    String databaseTitle1 = entryHome.get(entry1).getTabTitle();
                    String databaseTitle2 = entryHome.get(entry2).getTabTitle();
                    return databaseTitle1.compareTo(databaseTitle2);
                });
            }

        }
        // Remaining columns:
        for (int i = PAD; i < (PAD + FIELDS.length); i++) {
            comparators = comparatorChooser.getComparatorsForColumn(i);
            comparators.clear();
            comparators.add(new FieldComparator(FIELDS[i - PAD]));
        }

        sortedEntries.getReadWriteLock().writeLock().lock();
        comparatorChooser.appendComparator(PAD, 0, false);
        sortedEntries.getReadWriteLock().writeLock().unlock();

    }

    /**
     * Set column widths according to which field is shown, and lock icon columns
     * to a suitable width.
     */
    private void setWidths() {
        TableColumnModel cm = entryTable.getColumnModel();
        for (int i = 0; i < PAD + FIELDS.length; i++) {
            switch (i) {
                case FILE_COL:
                case URL_COL:
                    cm.getColumn(i).setPreferredWidth(GUIGlobals.WIDTH_ICON_COL);
                    cm.getColumn(i).setMinWidth(GUIGlobals.WIDTH_ICON_COL);
                    cm.getColumn(i).setMaxWidth(GUIGlobals.WIDTH_ICON_COL);
                    break;
                case DATABASE_COL: {
                    int width = InternalBibtexFields.getFieldLength(FieldName.AUTHOR);
                    cm.getColumn(i).setPreferredWidth(width);
                    break;
                }
                default: {
                    int width = InternalBibtexFields.getFieldLength(FIELDS[i - PAD]);
                    cm.getColumn(i).setPreferredWidth(width);
                    break;
                }
            }
        }
    }

    /**
     * Add a list of entries to the table.
     * @param newEntries The list of entries.
     * @param panel A reference to the BasePanel where the entries belong.
     */
    public void addEntries(List<BibEntry> newEntries, BasePanel panel) {
        for (BibEntry entry : newEntries) {
            addEntry(entry, panel);
        }
    }

    /**
     * Add a single entry to the table.
     * @param entry The entry to add.
     * @param panel A reference to the BasePanel where the entry belongs.
     */
    private void addEntry(BibEntry entry, BasePanel panel) {
        entries.add(entry);
        entryHome.put(entry, panel);

        if (preview.getEntry() == null || !preview.getBasePanel().isPresent()){
            preview.setEntry(entry);
            preview.setBasePanel(panel);
            preview.setDatabaseContext(panel.getBibDatabaseContext());
        }
    }

    private void selectEntryInBasePanel(BibEntry entry){
        BasePanel basePanel = entryHome.get(entry);
        frame.showBasePanel(basePanel);
        basePanel.requestFocus();
        basePanel.highlightEntry(entry);
    }

    public void dispose(){
        frame.getGlobalSearchBar().setSearchResultFrame(null);
        searchResultFrame.dispose();
        frame.getGlobalSearchBar().focus();
    }

    public void focus(){
        entryTable.requestFocus();
    }

    public SearchQuery getSearchQuery() {
        return searchQuery;
    }

    public boolean isGlobalSearch() {
        return globalSearch;
    }

    /**
     * Mouse listener for the entry table. Processes icon clicks to open external
     * files or urls, as well as the opening of the context menu.
     */
    class TableClickListener extends MouseAdapter {

        @Override
        public void mouseReleased(MouseEvent e) {
            if (e.isPopupTrigger()) {
                processPopupTrigger(e);
            }
        }

        @Override
        public void mousePressed(MouseEvent e) {
            if (e.isPopupTrigger()) {
                processPopupTrigger(e);
                return;
            }

            // First find the row on which the user has clicked.
            final int row = entryTable.rowAtPoint(e.getPoint());

            // A double click on an entry should highlight the entry in its BasePanel:
            if (e.getClickCount() == 2) {
                selectEntryInBasePanel(model.getElementAt(row));
            }
        }

        @Override
        public void mouseClicked(MouseEvent e) {
            if (e.isPopupTrigger()) {
                processPopupTrigger(e);
                return;
            }
            //if (e.)
            final int col = entryTable.columnAtPoint(e.getPoint());
            final int row = entryTable.rowAtPoint(e.getPoint());
            if (col < PAD) {
                BibEntry entry = sortedEntries.get(row);
                BasePanel p = entryHome.get(entry);
                switch (col) {
                case FILE_COL:
                    if (entry.hasField(FieldName.FILE)) {
                        FileListTableModel tableModel = new FileListTableModel();
                        entry.getField(FieldName.FILE).ifPresent(tableModel::setContent);
                        if (tableModel.getRowCount() == 0) {
                            return;
                        }
                        FileListEntry fl = tableModel.getEntry(0);
                        (new ExternalFileMenuItem(frame, entry, "", fl.link, null,
                                p.getBibDatabaseContext(), fl.type)).actionPerformed(null);
                    }
                    break;
                case URL_COL:
                    entry.getField(FieldName.URL).ifPresent(link -> { try {
                        JabRefDesktop.openExternalViewer(p.getBibDatabaseContext(), link, FieldName.URL);
                    } catch (IOException ex) {
                            LOGGER.warn("Could not open viewer", ex);
                        }
                    });
                    break;
                default:
                    break;
                }
            }
        }

        /**
         * If the user has signalled the opening of a context menu, the event
         * gets redirected to this method. Here we open a file link menu if the
         * user is pointing at a file link icon. Otherwise a general context
         * menu should be shown.
         * @param e The triggering mouse event.
         */
        public void processPopupTrigger(MouseEvent e) {
            BibEntry entry = sortedEntries.get(entryTable.rowAtPoint(e.getPoint()));
            BasePanel p = entryHome.get(entry);
            int col = entryTable.columnAtPoint(e.getPoint());
            JPopupMenu menu = new JPopupMenu();
            int count = 0;

            if (col == FILE_COL) {
                // We use a FileListTableModel to parse the field content:
                FileListTableModel fileList = new FileListTableModel();
                entry.getField(FieldName.FILE).ifPresent(fileList::setContent);
                // If there are one or more links, open the first one:
                for (int i = 0; i < fileList.getRowCount(); i++) {
                    FileListEntry flEntry = fileList.getEntry(i);
                    String description = flEntry.description;
                    if ((description == null) || (description.trim().isEmpty())) {
                        description = flEntry.link;
                    }
                    menu.add(new ExternalFileMenuItem(p.frame(), entry, description, flEntry.link,
                            flEntry.type.get().getIcon(), p.getBibDatabaseContext(), flEntry.type));
                    count++;
                }

            }

            if (count > 0) {
                menu.show(entryTable, e.getX(), e.getY());
            }
        }
    }

    /**
     * The listener for the Glazed list monitoring the current selection.
     * When selection changes, we need to update the preview panel.
     */
    private class EntrySelectionListener implements ListEventListener<BibEntry> {

        @Override
        public void listChanged(ListEvent<BibEntry> listEvent) {
            if (listEvent.getSourceList().size() == 1) {
                BibEntry entry = listEvent.getSourceList().get(0);
                // Find out which BasePanel the selected entry belongs to:
                BasePanel basePanel = entryHome.get(entry);
                // Update the preview's database context:
                preview.setDatabaseContext(basePanel.getBibDatabaseContext());
                // Update the preview's entry:
                preview.setEntry(entry);
                preview.setBasePanel(entryHome.get(entry));
                preview.setDatabaseContext(entryHome.get(entry).getBibDatabaseContext());
                contentPane.setDividerLocation(0.5f);
                SwingUtilities.invokeLater(() -> preview.scrollRectToVisible(toRect));
            }
        }
    }

    /**
     * TableFormat for the table shown in the dialog. Handles the display of entry
     * fields and icons for linked files and urls.
     */
    private class EntryTableFormat implements AdvancedTableFormat<BibEntry> {

        @Override
        public int getColumnCount() {
            return PAD + FIELDS.length;
        }

        @Override
        public String getColumnName(int column) {
            if (column >= PAD) {
                return StringUtil.capitalizeFirst(FIELDS[column - PAD]);
            } else if (column == DATABASE_COL){
                return Localization.lang("Database");
            } else {
                return "";
            }
        }

        @Override
        public Object getColumnValue(BibEntry entry, int column) {
            if (column < PAD) {
                switch (column) {
                case DATABASE_COL:
                    return entryHome.get(entry).getTabTitle();
                case FILE_COL:
                    if (entry.hasField(FieldName.FILE)) {
                        FileListTableModel tmpModel = new FileListTableModel();
                        entry.getField(FieldName.FILE).ifPresent(tmpModel::setContent);
                        fileLabel.setToolTipText(tmpModel.getToolTipHTMLRepresentation());
                        if (tmpModel.getRowCount() > 0) {
                            if (tmpModel.getEntry(0).type.isPresent()) {
                                fileLabel.setIcon(tmpModel.getEntry(0).type.get().getIcon());
                            } else {
                                fileLabel.setIcon(IconTheme.JabRefIcon.FILE.getSmallIcon());
                            }
                        }
                        return fileLabel;
                    } else {
                        return null;
                    }
                case URL_COL: {
                    Optional<String> urlField = entry.getField(FieldName.URL);
                    if (urlField.isPresent()) {
                        urlLabel.setToolTipText(urlField.get());
                        return urlLabel;
                    }
                    return null;
                }
                default:
                    return null;
                }
            }
            else {
                String field = FIELDS[column - PAD];
                String fieldContent = entry.getLatexFreeField(field).orElse("");

                if (InternalBibtexFields.getFieldProperties(field).contains(FieldProperty.PERSON_NAMES)) {
                    // For name fields, tap into a MainTableFormat instance and use
                    // the same name formatting as is used in the entry table:
                    return MainTableNameFormatter.formatName(fieldContent);
                }
                return fieldContent;
            }
        }

        @Override
        public Class<?> getColumnClass(int i) {
            switch (i) {
                case FILE_COL:
                case URL_COL:
                    return JLabel.class;
                default:
                    return String.class;
            }
        }

        @Override
        public Comparator<?> getColumnComparator(int i) {
            return null;
        }
    }

}
