/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Icy is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.gui.menu.search;

import icy.main.Icy;
import icy.search.SearchEngine;
import icy.search.SearchResult;
import icy.system.thread.ThreadUtil;

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JButton;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JWindow;
import javax.swing.ListSelectionModel;
import javax.swing.Popup;
import javax.swing.PopupFactory;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;

import org.pushingpixels.flamingo.api.common.RichTooltip;
import org.pushingpixels.flamingo.internal.ui.common.JRichTooltipPanel;

/**
 * This class is the most important part of this plugin: it will handle and
 * display all local and online requests when characters are being typed in the {@link SearchBar}.
 * 
 * @author Thomas Provoost & Stephane
 */
public class SearchResultPanel extends JWindow implements ListSelectionListener
{
    /**
     * 
     */
    private static final long serialVersionUID = -7794681892496197765L;

    private static final int ROW_HEIGHT = 48;
    private static final int MAX_ROW = 15;

    /** Associated Search Bar */
    final SearchBar searchBar;

    /** PopupMenu */
    JRichTooltipPanel tooltipPanel;
    Popup tooltip;
    SearchResult toolTipResult;
    boolean toolTipForceRefresh;

    /** GUI */
    final SearchResultTableModel tableModel;
    final JTable table;
    final JButton moreResultBtn;
    final JScrollPane scrollPane;

    /**
     * Internals
     */
    private final Runnable refresher;
    private final Runnable toolTipRefresher;
    boolean firstResultsDisplay;

    public SearchResultPanel(final SearchBar sb)
    {
        super(Icy.getMainInterface().getMainFrame());

        searchBar = sb;

        tooltipPanel = null;
        tooltip = null;
        toolTipResult = null;
        toolTipForceRefresh = false;
        firstResultsDisplay = true;

        refresher = new Runnable()
        {
            @Override
            public void run()
            {
                refreshInternal();
            }
        };

        toolTipRefresher = new Runnable()
        {
            @Override
            public void run()
            {
                updateToolTip();
            }
        };

        // build table (display 15 rows max)
        tableModel = new SearchResultTableModel(MAX_ROW);
        table = new JTable(tableModel);

        // sets the different column values and renderers
        final TableColumnModel colModel = table.getColumnModel();
        TableColumn col;

        // provider name column
        col = colModel.getColumn(0);
        col.setCellRenderer(new SearchProducerTableCellRenderer());
        col.setPreferredWidth(140);

        // result text column
        col = colModel.getColumn(1);
        col.setCellRenderer(new SearchResultTableCellRenderer());
        col.setPreferredWidth(600);

        // sets the table properties
        table.setIntercellSpacing(new Dimension(0, 0));
        table.setShowVerticalLines(false);
        table.setShowHorizontalLines(false);
        table.setColumnSelectionAllowed(false);
        table.setTableHeader(null);
        table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        table.getSelectionModel().addListSelectionListener(this);
        table.addMouseListener(new MouseAdapter()
        {
            @Override
            public void mouseClicked(MouseEvent e)
            {
                final SearchResult result = getResultAtPosition(e.getPoint());

                if ((result != null) && result.isEnabled())
                {
                    if (SwingUtilities.isLeftMouseButton(e))
                        result.execute();
                    else
                        result.executeAlternate();

                    close(true);
                    e.consume();
                }
            }

            @Override
            public void mouseExited(MouseEvent e)
            {
                // clear selection
                table.getSelectionModel().removeSelectionInterval(0, table.getRowCount() - 1);
            }

            @Override
            public void mouseEntered(MouseEvent e)
            {
                // select row under mouse position
                final int row = table.rowAtPoint(e.getPoint());

                if (row != -1)
                    table.getSelectionModel().setSelectionInterval(row, row);
                else
                    table.getSelectionModel().removeSelectionInterval(0, table.getRowCount() - 1);
            }
        });
        table.addMouseMotionListener(new MouseAdapter()
        {
            @Override
            public void mouseMoved(MouseEvent e)
            {
                // select row under mouse position
                final int row = table.rowAtPoint(e.getPoint());

                if (row != -1)
                    table.getSelectionModel().setSelectionInterval(row, row);
                else
                    table.getSelectionModel().removeSelectionInterval(0, table.getRowCount() - 1);
            }
        });

        // build GUI
        moreResultBtn = new JButton("");
        moreResultBtn.setHorizontalAlignment(SwingConstants.RIGHT);
        moreResultBtn.setVerticalAlignment(SwingConstants.CENTER);
        moreResultBtn.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                showCompleteList();
            }
        });

        scrollPane = new JScrollPane(table, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
                ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);

        // window used to display quick result list
        setLayout(new BorderLayout());
        add(scrollPane, BorderLayout.CENTER);
        add(moreResultBtn, BorderLayout.SOUTH);
        setPreferredSize(new Dimension(600, 400));
        setAlwaysOnTop(true);
        setVisible(false);
    }

    protected SearchEngine getSearchEngine()
    {
        return searchBar.getSearchEngine();
    }

    /**
     * Returns SearchResult located at specified index.
     */
    protected SearchResult getResult(int index)
    {
        if ((index >= 0) && (index < table.getRowCount()))
            return (SearchResult) table.getValueAt(index, SearchResultTableModel.COL_RESULT_OBJECT);

        return null;
    }

    /**
     * Returns the index in the table for the specified SearchResult (-1 if not found)
     */
    protected int getRowIndex(SearchResult result)
    {
        if (result != null)
        {
            for (int i = 0; i < table.getRowCount(); i++)
                if (result == table.getValueAt(i, SearchResultTableModel.COL_RESULT_OBJECT))
                    return i;
        }

        return -1;
    }

    /**
     * Returns SearchResult located at specified point position.
     */
    protected SearchResult getResultAtPosition(Point pt)
    {
        return getResult(table.rowAtPoint(pt));
    }

    /**
     * Returns selected result
     */
    public SearchResult getSelectedResult()
    {
        return getResult(table.getSelectedRow());
    }

    /**
     * Set selected result
     */
    public void setSelectedResult(SearchResult result)
    {
        final int row = getRowIndex(result);

        if (row != -1)
            table.getSelectionModel().setSelectionInterval(row, row);
        else
            table.getSelectionModel().removeSelectionInterval(0, table.getRowCount() - 1);
    }

    void showCompleteList()
    {
        // no more limit on row count
        tableModel.setMaxRowCount(-1);
        // hide button
        moreResultBtn.setVisible(false);

        // update size
        setSize(600, getPanelHeight());
    }

    void hideToolTip()
    {
        if (tooltip != null)
        {
            tooltip.hide();
            tooltip = null;
            toolTipResult = null;
        }
    }

    /**
     * Calculates and returns panel height.
     */
    int getPanelHeight()
    {
        final Insets margin = getInsets();
        final Insets marginSC = scrollPane.getInsets();
        final Insets marginT = table.getInsets();
        int result;

        result = Math.min(table.getRowCount(), MAX_ROW) * ROW_HEIGHT;
        result += (margin.top + margin.bottom) + (marginSC.top + marginSC.bottom) + (marginT.top + marginT.bottom);
        if (moreResultBtn.isVisible())
            result += moreResultBtn.getPreferredSize().height;

        return result;
    }

    /**
     * Updates the popup menu: asks the tablemodel for the right popupmenu and
     * displays it.
     */
    void updateToolTip()
    {
        final SearchResult searchResult = getSelectedResult();

        // need to be done on EDT
        ThreadUtil.invokeNow(new Runnable()
        {
            @Override
            public void run()
            {
                if (!isVisible() || (searchResult == null))
                {
                    hideToolTip();
                    return;
                }

                final RichTooltip rtp = searchResult.getRichToolTip();

                if (rtp == null)
                {
                    hideToolTip();
                    return;
                }

                // tool tip is not yet visible or result changed --> refresh the tool tip
                if ((tooltip == null) || (searchResult != toolTipResult) || toolTipForceRefresh)
                {
                    // hide out dated tool tip
                    hideToolTip();

                    final Rectangle bounds = getBounds();

                    tooltipPanel = new JRichTooltipPanel(rtp);

                    int x = bounds.x + bounds.width;
                    int y = bounds.y + (ROW_HEIGHT * table.getSelectedRow());

                    // adjust vertical position
                    y -= scrollPane.getVerticalScrollBar().getValue();

                    // show tooltip
                    tooltip = PopupFactory.getSharedInstance().getPopup(Icy.getMainInterface().getMainFrame(),
                            tooltipPanel, x, y);
                    tooltip.show();

                    toolTipResult = searchResult;
                    toolTipForceRefresh = false;
                }
            }
        });
    }

    /**
     * Close the results panel.<br>
     * If <code>reset</br> is true that also reset search.
     */
    public void close(boolean reset)
    {
        // reset search
        if (reset)
            searchBar.cancelSearch();

        // hide popup and panel
        setVisible(false);
        hideToolTip();
    }

    /**
     * Execute selected result.
     * Return false if we don't have any selected result.
     */
    public void executeSelected()
    {
        final SearchResult sr = getSelectedResult();

        if ((sr != null) && sr.isEnabled())
        {
            sr.execute();
            close(true);
        }
    }

    /**
     * Update display
     */
    public void refresh()
    {
        ThreadUtil.runSingle(refresher);
    }

    /**
     * Update display internal
     */
    void refreshInternal()
    {
        final SearchEngine searchEngine = getSearchEngine();
        final List<SearchResult> results;
        final int resultCount;
        final SearchResult selected;

        // quick cancel
        if (searchEngine.getLastSearch().isEmpty())
        {
            results = new ArrayList<SearchResult>();
            resultCount = 0;
            selected = null;
        }
        else
        {
            results = searchEngine.getResults();
            resultCount = results.size();
            // save selected
            selected = getSelectedResult();
        }

        // free a bit of time
        ThreadUtil.sleep(1);

        // need to be done on EDT
        ThreadUtil.invokeNow(new Runnable()
        {
            @Override
            public void run()
            {
                if (resultCount == 0)
                {
                    close(false);
                    return;
                }

                // fix row height (can be changed on LAF change)
                table.setRowHeight(ROW_HEIGHT);
                // refresh data model
                tableModel.setResults(results);

                if (firstResultsDisplay)
                {
                    // limit result list size to MAX_ROW and refresh table data
                    if (tableModel.getMaxRowCount() != MAX_ROW)
                        tableModel.setMaxRowCount(MAX_ROW);
                    else
                        tableModel.fireTableDataChanged();

                    // no more need to re init the limited display
                    firstResultsDisplay = false;
                }
                else
                    tableModel.fireTableDataChanged();

                // restore selected
                setSelectedResult(selected);

                final int maxRow = tableModel.getMaxRowCount();

                // result list do not display all results ?
                if ((maxRow > 0) && (resultCount > maxRow))
                {
                    moreResultBtn.setText(maxRow + " / " + resultCount + " (show all)");
                    moreResultBtn.setVisible(true);
                }
                else
                    moreResultBtn.setVisible(false);

                // update bounds and display window
                final Point p = searchBar.getLocationOnScreen();
                setBounds(p.x, p.y + searchBar.getHeight(), 600, getPanelHeight());

                // show the result list
                setVisible(true);

                // update tooltip
                updateToolTip();
            }
        });
    }

    /**
     * Selection movement in the table: up or down.
     * 
     * @param direction
     *        : should be 1 or -1.
     */
    public void moveSelection(int direction)
    {
        final int rowCount = table.getRowCount();

        if (rowCount == 0)
            return;

        final int rowIndex = table.getSelectedRow();
        final int newIndex;

        if (rowIndex == -1)
        {
            if (direction > 0)
                newIndex = 0;
            else
                newIndex = rowCount - 1;
        }
        else
            newIndex = Math.abs((rowIndex + direction) % rowCount);

        table.setRowSelectionInterval(newIndex, newIndex);
    }

    @Override
    public void valueChanged(ListSelectionEvent e)
    {
        // selection changed --> update tooltip
        ThreadUtil.runSingle(toolTipRefresher);
    }

    public void searchStarted()
    {
        firstResultsDisplay = true;
    }

    public void resultChanged(SearchResult result)
    {
        if (isVisible())
        {
            try
            {
                // only update the specified result
                final int rowIndex = getRowIndex(result);

                if (rowIndex != -1)
                    tableModel.fireTableRowsUpdated(rowIndex, rowIndex);
            }
            catch (Exception e)
            {
                // ignore possible exception here
            }
            
            // refresh toolTip if needed
            if (result == getSelectedResult())
            {
                toolTipForceRefresh = true;
                ThreadUtil.runSingle(toolTipRefresher);
            }
        }
    }

    public void resultsChanged()
    {
        // refresh table
        refresh();
    }
}