/*
 * 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.gui.component.IcyTextField;
import icy.resource.ResourceUtil;
import icy.resource.icon.IcyIcon;
import icy.search.SearchEngine;
import icy.search.SearchEngine.SearchEngineListener;
import icy.search.SearchResult;
import icy.util.StringUtil;

import java.awt.AWTEvent;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.AWTEventListener;
import java.awt.event.ActionEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import java.util.Timer;
import java.util.TimerTask;

import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.InputMap;
import javax.swing.JComponent;
import javax.swing.KeyStroke;

import org.jdesktop.swingx.painter.BusyPainter;

/**
 * @author Thomas Provoost & Stephane.
 */
public class SearchBar extends IcyTextField implements SearchEngineListener
{
    /**
     * 
     */
    private static final long serialVersionUID = -931313822004038942L;

    private static final int DELAY = 20;

    private static final int BUSY_PAINTER_SIZE = 15;
    private static final int BUSY_PAINTER_POINTS = 40;
    private static final int BUSY_PAINTER_TRAIL = 20;

    /** Internal search engine */
    final SearchEngine searchEngine;

    /**
     * GUI
     */
    final SearchResultPanel resultsPanel;
    private final IcyIcon searchIcon;

    /**
     * Internals
     */
    private Timer busyPainterTimer;
    final BusyPainter busyPainter;
    int frame;
    boolean lastSearchingState;
    boolean initialized;

    public SearchBar()
    {
        super();

        initialized = false;

        searchEngine = new SearchEngine();
        searchEngine.addListener(this);

        resultsPanel = new SearchResultPanel(this);
        searchIcon = new IcyIcon(ResourceUtil.ICON_SEARCH, 16);

        // modify margin so we have space for icon
        final Insets margin = getMargin();
        setMargin(new Insets(margin.top, margin.left, margin.bottom, margin.right + 20));

        // focusable only when hit Ctrl + F or clicked at the beginning
        setFocusable(false);

        // SET THE BUSY PAINTER
        busyPainter = new BusyPainter(BUSY_PAINTER_SIZE);
        busyPainter.setFrame(0);
        busyPainter.setPoints(BUSY_PAINTER_POINTS);
        busyPainter.setTrailLength(BUSY_PAINTER_TRAIL);
        busyPainter.setPointShape(new Rectangle2D.Float(0, 0, 2, 1));
        frame = 0;

        lastSearchingState = false;
        busyPainterTimer = new Timer("Search animation timer");

        // ADD LISTENERS
        addTextChangeListener(new TextChangeListener()
        {
            @Override
            public void textChanged(IcyTextField source, boolean validate)
            {
                searchInternal(getText());
            }
        });
        addMouseListener(new MouseAdapter()
        {
            @Override
            public void mouseClicked(MouseEvent e)
            {
                setFocus();
            }
        });

        addFocusListener(new FocusListener()
        {
            @Override
            public void focusLost(FocusEvent e)
            {
                removeFocus();
            }

            @Override
            public void focusGained(FocusEvent e)
            {
                searchInternal(getText());
            }
        });

        // global key listener to catch Ctrl+F in every case (not elegant)
        // getToolkit().addAWTEventListener(new AWTEventListener()
        // {
        // @Override
        // public void eventDispatched(AWTEvent event)
        // {
        // if (event instanceof KeyEvent)
        // {
        // final KeyEvent key = (KeyEvent) event;
        //
        // if (key.getID() == KeyEvent.KEY_PRESSED)
        // {
        // // Handle key presses
        // switch (key.getKeyCode())
        // {
        // case KeyEvent.VK_F:
        // if (EventUtil.isControlDown(key))
        // {
        // setFocus();
        // key.consume();
        // }
        // break;
        // }
        // }
        // }
        // }
        // }, AWTEvent.KEY_EVENT_MASK);

        // global mouse listener to simulate focus lost (not elegant)
        getToolkit().addAWTEventListener(new AWTEventListener()
        {
            @Override
            public void eventDispatched(AWTEvent event)
            {
                if (!initialized || !hasFocus())
                    return;

                if (event instanceof MouseEvent)
                {
                    final MouseEvent evt = (MouseEvent) event;

                    if (evt.getID() == MouseEvent.MOUSE_PRESSED)
                    {
                        final Point pt = evt.getLocationOnScreen();

                        // user clicked outside search panel --> close it
                        if (!isInsideSearchComponents(pt))
                            removeFocus();
                    }
                }
            }
        }, AWTEvent.MOUSE_EVENT_MASK);

        buildActionMap();

        initialized = true;
    }

    void buildActionMap()
    {
        final InputMap imap = getInputMap(JComponent.WHEN_FOCUSED);
        final ActionMap amap = getActionMap();

        imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), "Cancel");
        imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), "MoveDown");
        imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "MoveUp");
        imap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "Execute");

        amap.put("Cancel", new AbstractAction()
        {
            /**
             * 
             */
            private static final long serialVersionUID = 6690317671269902666L;

            @Override
            public void actionPerformed(ActionEvent e)
            {
                if (initialized)
                    cancelSearch();
            }
        });
        getActionMap().put("MoveDown", new AbstractAction()
        {
            /**
             * 
             */
            private static final long serialVersionUID = 8864361043092897904L;

            @Override
            public void actionPerformed(ActionEvent e)
            {
                if (initialized)
                    moveDown();
            }
        });
        getActionMap().put("MoveUp", new AbstractAction()
        {
            /**
             * 
             */
            private static final long serialVersionUID = 6258168037713535447L;

            @Override
            public void actionPerformed(ActionEvent e)
            {
                if (initialized)
                    moveUp();
            }
        });
        getActionMap().put("Execute", new AbstractAction()
        {
            /**
             * 
             */
            private static final long serialVersionUID = 5363650211730888168L;

            @Override
            public void actionPerformed(ActionEvent e)
            {
                if (initialized)
                    execute();
            }
        });
    }

    public SearchEngine getSearchEngine()
    {
        return searchEngine;
    }

    protected boolean isInsideSearchComponents(Point pt)
    {
        final Rectangle bounds = new Rectangle();

        bounds.setLocation(getLocationOnScreen());
        bounds.setSize(getSize());

        if (bounds.contains(pt))
            return true;

        if (initialized)
        {
            if (resultsPanel.isVisible())
            {
                bounds.setLocation(resultsPanel.getLocationOnScreen());
                bounds.setSize(resultsPanel.getSize());

                return bounds.contains(pt);
            }
        }

        return false;
    }

    public void setFocus()
    {
        if (!hasFocus())
        {
            setFocusable(true);
            requestFocus();
        }
    }

    public void removeFocus()
    {
        if (initialized)
        {
            resultsPanel.close(true);
            setFocusable(false);
        }
    }

    public void cancelSearch()
    {
        setText("");
    }

    // public void search(String text)
    // {
    // final String filter = text.trim();
    //
    // if (StringUtil.isEmpty(filter))
    // searchEngine.cancelSearch();
    // else
    // searchEngine.search(filter);
    // }
    //
    /**
     * Request search for the specified text.
     * 
     * @see SearchEngine#search(String)
     */
    public void search(String text)
    {
        setText(text);
    }

    protected void searchInternal(String text)
    {
        final String filter = text.trim();

        if (StringUtil.isEmpty(filter))
            searchEngine.cancelSearch();
        else
            searchEngine.search(filter);
    }

    protected void execute()
    {
        // result displayed --> launch selected result
        if (resultsPanel.isShowing())
            resultsPanel.executeSelected();
        else
            searchInternal(getText());
    }

    protected void moveDown()
    {
        resultsPanel.moveSelection(1);
    }

    protected void moveUp()
    {
        resultsPanel.moveSelection(-1);
    }

    @Override
    protected void paintComponent(Graphics g)
    {
        super.paintComponent(g);

        Graphics2D g2 = (Graphics2D) g.create();
        int w = getWidth();
        int h = getHeight();

        // set rendering presets
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);

        if (StringUtil.isEmpty(getText()) && !hasFocus())
        {
            // draw "Search" if no focus
            Insets insets = getMargin();
            Color fg = getForeground();

            g2.setColor(new Color(fg.getRed(), fg.getGreen(), fg.getBlue(), 100));
            g2.drawString("Search", insets.left + 2, h - g2.getFontMetrics().getHeight() / 2 + 2);
        }

        if (searchEngine.isSearching())
        {
            // draw loading icon
            g2.translate(w - (BUSY_PAINTER_SIZE + 5), 3);
            busyPainter.paint(g2, this, BUSY_PAINTER_SIZE, BUSY_PAINTER_SIZE);
        }
        else
        {
            // draw search icon
            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.7f));
            searchIcon.paintIcon(this, g2, w - h, 2);
        }

        g2.dispose();
    }

    @Override
    public void resultChanged(SearchEngine source, SearchResult result)
    {
        if (initialized)
            resultsPanel.resultChanged(result);
    }

    @Override
    public void resultsChanged(SearchEngine source)
    {
        if (initialized)
            resultsPanel.resultsChanged();
    }

    @Override
    public void searchStarted(SearchEngine source)
    {
        if (!initialized)
            return;

        resultsPanel.searchStarted();

        // make sure the animation timer for the busy icon is stopped
        busyPainterTimer.cancel();

        // ... and restart it
        final Timer newTimer = new Timer("Search animation timer");
        newTimer.scheduleAtFixedRate(new TimerTask()
        {
            @Override
            public void run()
            {
                frame = (frame + 1) % BUSY_PAINTER_POINTS;
                busyPainter.setFrame(frame);

                final boolean searching = searchEngine.isSearching();

                // this permit to get rid of the small delay between the searchCompleted
                // event and when isSearching() actually returns false
                if (searching || (searching != lastSearchingState))
                    repaint();

                lastSearchingState = searching;
            }
        }, DELAY, DELAY);
        busyPainterTimer = newTimer;

        // for the busy loop animation
        repaint();
    }

    @Override
    public void searchCompleted(SearchEngine source)
    {
        // stop the animation timer for the rotating busy icon
        busyPainterTimer.cancel();

        // for the busy loop animation
        repaint();
    }
}