/*
 * jNPad v0.3 - jNPad's an Simple Text Editor written in Java
 *
 * Copyright (C) 2014-2017  rgs
 *
 * Require JDK 1.6 (or later)
 *
 * This program 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 2 of the License, or (at your option)
 * any later version.
 *
 * This program 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 this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 *
 * Info, Questions, Suggestions & Bugs Report to rgsevero@gmail.com
 */

package jnpad.text;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.BorderFactory;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;

import jnpad.GUIUtilities;
import jnpad.config.Accelerators;
import jnpad.ui.ColorUtilities;
import jnpad.ui.JNPadLabel;
import jnpad.ui.plaf.LAFUtils;
import jnpad.util.Utilities;

/**
 * The Class CompletionPopup.
 *
 * @version 0.3
 * @since   jNPad v0.1
 */
public class CompletionPopup extends JPopupMenu {
  private JLabel              lbMatches;
  private JList               list;
  private String              word;
  private JNPadTextArea       textArea;
  private String[]            completions;
  private static int          maxWidth;

  private Listener            listener         = new Listener();

  /** Logger */
  private static final Logger LOGGER           = Logger.getLogger(CompletionPopup.class.getName());

  /** UID */
  private static final long   serialVersionUID = -4013987311209191365L;

  /**
   * Instantiates a new completion popup.
   *
   * @param textArea EditTextArea
   */
  public CompletionPopup(JNPadTextArea textArea) {
    try {
      this.textArea = textArea;

      list = new JList();
      lbMatches = new JNPadLabel();

      jbInit();
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
  }

  /**
   * Instantiates a new completion popup.
   *
   * @param textArea EditTextArea
   * @param word String
   * @param completions String[]
   */
  public CompletionPopup(JNPadTextArea textArea, String word, String[] completions) {
    try {
      this.textArea = textArea;
      this.word = word;
      
      //this.completions = completions; // Original
      //this.completions = Utilities.clone(completions); // Keep FindBugs happy [v0.1]
      this.completions = Utilities.copyOf(completions); // Keep FindBugs happy [v0.2]

      list = new JList(completions);
      lbMatches = new JNPadLabel(TextBundle.getString("CompletionPopup.label", word)); //$NON-NLS-1$

      jbInit();

      if (completions.length > 0) {
        list.setSelectedIndex(0);
      }

      showCompletionPopup();
    }
    catch (Exception ex) {
      LOGGER.log(Level.WARNING, ex.getMessage(), ex);
    }
  }

  /**
   * Component initialization.
   *
   * @throws Exception the exception
   */
  private void jbInit() throws Exception {
    setLayout(new BorderLayout());

    Color bg, fg;
    if (LAFUtils.isNimbusLAF()) {
      bg = new Color(242, 242, 189);
      fg = ColorUtilities.createPureColor(LAFUtils.getToolTipForeground());
    }
    else {
      bg = LAFUtils.getToolTipBackground();
      fg = LAFUtils.getToolTipForeground();
    }

    Font font = new Font("Monospaced", Font.ITALIC, 11); //$NON-NLS-1$

    lbMatches.setHorizontalAlignment(SwingConstants.CENTER);
    lbMatches.setOpaque(true);
    lbMatches.setBackground(bg.darker());
    lbMatches.setBorder(BorderFactory.createRaisedBevelBorder());
    lbMatches.setFont(font);
    add(lbMatches, BorderLayout.NORTH);

    list.setBackground(bg);
    list.setForeground(fg);
    list.setSelectionBackground(textArea.getSelectionColor());
    list.setSelectionForeground(textArea.getSelectedTextColor());
    list.setFont(textArea.getFont());
    setOpaque(true);
    list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
    list.getSelectionModel().setAnchorSelectionIndex(4);
    list.addMouseListener(new MouseAdapter() {
      @Override
      public void mouseClicked(final MouseEvent e) {
        handleMouseClick(e);
      }
    });

    FontMetrics fm = getFontMetrics(textArea.getFont());
    maxWidth = 50 * fm.charWidth('m');

    JScrollPane scrollPane = new JScrollPane(list);
    add(scrollPane, BorderLayout.CENTER);
  }

  /**
   * Gets the preferred size.
   *
   * @return the preferred size
   * @see javax.swing.JComponent#getPreferredSize()
   */
  @Override
  public Dimension getPreferredSize() {
    Dimension pSize = super.getPreferredSize();
    return new Dimension(Math.min(pSize.width, maxWidth), pSize.height);
  }

  /**
   * Adds the notify.
   *
   * @see javax.swing.JComponent#addNotify()
   */
  @Override
  public void addNotify() {
    //System.out.println("-- addNotify --");
    super.addNotify();
    textArea.addKeyListener(listener);
    if (Accelerators.isUsingCompositeShortcuts()) {
      textArea.setKeyEventInterceptor(listener);
    }
    textArea.getDocument().addDocumentListener(listener);
  }

  /**
   * Removes the notify.
   *
   * @see javax.swing.JComponent#removeNotify()
   */
  @Override
  public void removeNotify() {
    //System.out.println("-- removeNotify --");
    super.removeNotify();
    textArea.removeKeyListener(listener);
    if (Accelerators.isUsingCompositeShortcuts()) {
      textArea.setKeyEventInterceptor(null);
    }
    textArea.getDocument().removeDocumentListener(listener);
  }

  /**
   * Sets the word.
   *
   * @param word the new word
   */
  public void setWord(String word) {
    this.word = word;
    lbMatches.setText(TextBundle.getString("CompletionPopup.label", word)); //$NON-NLS-1$

    //System.out.println("word: " + word);

    if (completions != null) {
      Vector<?> v = CompletionUtilities.refreshListData(word, completions);
      if (v.size() > 0) {
        list.setListData(v);
        list.setSelectedIndex(0);
      }
      else {
        setVisible(false);
      }
    }
  }

  /**
   * Show completions.
   *
   * @param word the word
   * @param completions the completions
   */
  public void showCompletions(String word, String[] completions) {
    if (Utilities.isEmptyString(word) || completions == null || completions.length == 0) {
      setVisible(false);
      return;
    }

    this.word = word;
    
    //this.completions = completions; // Original
    //this.completions = Utilities.clone(completions); // Keep FindBugs happy [v0.1]
    this.completions = Utilities.copyOf(completions); // Keep FindBugs happy [v0.2]

    lbMatches.setText(TextBundle.getString("CompletionPopup.label", word)); //$NON-NLS-1$

    list.setListData(completions);
    if (completions.length > 0) {
      list.setSelectedIndex(0);
    }

    showCompletionPopup();
  }

  /**
   * Show completion popup.
   */
  private void showCompletionPopup() {
    JNPadTextArea txtarea = textArea.getMain(); // trick
    
    pack();

    Point p;

    label0: {
      Rectangle r;
      try {
        r = txtarea.modelToView(txtarea.getCaretPosition());
      }
      catch (BadLocationException blex) {
        Point pt = txtarea.getCaret().getMagicCaretPosition();
        int x = (int) pt.getX();
        int y = (int) pt.getY();
        int popupWidth = getPreferredSize().width;
        int popupHeight = getPreferredSize().height;
        int fontHeight = txtarea.getFont().getSize();
        int widthLimit = txtarea.getSize().width - popupWidth;
        int heightLimit = txtarea.getSize().height - (popupHeight + 2);
        if (x >= widthLimit)
          x -= popupWidth;
        if (y >= heightLimit)
          y -= (popupHeight + fontHeight);
        p = new Point(x, (y + fontHeight + 2));
        break label0;
      }

      p = new Point(r.x, r.y);

      int fontHeight = txtarea.getFont().getSize();

      SwingUtilities.convertPointToScreen(p, txtarea);

      Rectangle screenSize = GUIUtilities.getScreenBoundsForPoint(p);

      screenSize.height = screenSize.height - 30 - fontHeight;

      if (p.y + getPreferredSize().height >= screenSize.height) {
        p.y = p.y - getPreferredSize().height;
      }
      else {
        p.y += fontHeight;
      }

      SwingUtilities.convertPointFromScreen(p, txtarea);
    }

    show(txtarea, p.x, p.y);
    txtarea.requestFocus();
  }

  /**
   * Select next index.
   */
  private void selectNextIndex() {
    int index = list.getSelectedIndex();
    int visibleIndexGap = 3;
    int lastIndex = getListSize() - 1;
    index += 1; // new index
    int gap = lastIndex - index;
    if (gap < 3) {
      visibleIndexGap = gap + 1;
      if (gap < 0) {
        index = lastIndex;
        visibleIndexGap = 0;
      }
    }
    highlightIndex(index, index + visibleIndexGap);
  }

  /**
   * Select previous index.
   */
  private void selectPreviousIndex() {
    int index = list.getSelectedIndex();
    int visibleIndexGap = 3;
    index -= 1;
    if (index < 0) {
      index = 0;
      visibleIndexGap = 0;
    }
    highlightIndex(index, index - visibleIndexGap);
  }

  /**
   * Select next page index.
   */
  private void selectNextPageIndex() {
    int index = list.getSelectedIndex();
    int visibleIndexGap = 3;
    int lastIndex = getListSize() - 1;
    index += 8; // new index
    int gap = lastIndex - index;
    if (gap < 3) {
      visibleIndexGap = gap + 1;
      if (gap < 0) {
        index = lastIndex;
        visibleIndexGap = 0;
      }
    }
    highlightIndex(index, index + visibleIndexGap);
  }

  /**
   * Select previous page index.
   */
  private void selectPreviousPageIndex() {
    int index = list.getSelectedIndex();
    int visibleIndexGap = 3;
    index -= 8;
    if (index < 0) {
      index = 0;
      visibleIndexGap = 0;
    }
    highlightIndex(index, index - visibleIndexGap);
  }

  /**
   * Select home index.
   */
  private void selectHomeIndex() {
    highlightIndex(0, 0);
  }

  /**
   * Select end index.
   */
  private void selectEndIndex() {
    int lastIndex = getListSize() - 1;
    highlightIndex(lastIndex, lastIndex);
  }

  /**
   * Gets the list size.
   *
   * @return the list size
   */
  private int getListSize() {
    ListModel lm = list.getModel();
    return lm.getSize();
  }

  /**
   * Highlight index.
   *
   * @param index the index
   * @param visibleIndex the visible index
   */
  private void highlightIndex(int index, int visibleIndex) {
    list.setSelectedIndex(index);
    list.ensureIndexIsVisible(visibleIndex);
  }

  /**
   * Handle mouse click.
   *
   * @param e MouseEvent
   */
  void handleMouseClick(final MouseEvent e) {
    if (isVisible()) {
      insertSelectedCompletion();
    }
  }

  /**
   * Insert selected completion.
   */
  private void insertSelectedCompletion() {
    String selectedValue = (String) list.getSelectedValue();

    setVisible(false);

    if (Utilities.isNotEmptyString(selectedValue)) {
      try {
        textArea.insert(selectedValue.substring(word.length()), textArea.getCaretPosition());
      }
      catch (Exception ex) {
        LOGGER.log(Level.WARNING, ex.getMessage(), ex);
      }
    }
  }

  /**
   * Handle key pressed.
   *
   * @param e the key event
   */
  void handleKeyPressed(KeyEvent e) {
    int keyCode = e.getKeyCode();
    if (keyCode == KeyEvent.VK_UP) {
      selectPreviousIndex();
    }
    else if (keyCode == KeyEvent.VK_DOWN) {
      selectNextIndex();
    }
    else if (keyCode == KeyEvent.VK_PAGE_UP) {
      selectPreviousPageIndex();
    }
    else if (keyCode == KeyEvent.VK_PAGE_DOWN) {
      selectNextPageIndex();
    }
    else if (keyCode == KeyEvent.VK_HOME) {
      selectHomeIndex();
    }
    else if (keyCode == KeyEvent.VK_END) {
      selectEndIndex();
    }
    else if (keyCode == KeyEvent.VK_ESCAPE || keyCode == KeyEvent.VK_SPACE || keyCode == KeyEvent.VK_TAB) {
      setVisible(false);
    }
    else if (keyCode == KeyEvent.VK_ENTER) {
      if (isVisible()) {
        e.consume();
        insertSelectedCompletion();
      }
      return;
    }
    else if (keyCode == KeyEvent.VK_LEFT || keyCode == KeyEvent.VK_RIGHT) {
      //empty
    }
    else {
      return;
    }
    e.consume();
  }

  /**
   * Handle document event.
   *
   * @param offset the offset
   */
  void handleDocumentEvent(int offset) {
    try {
      String word = CompletionUtilities.getWord(textArea, offset);
      if (Utilities.isBlankString(word)) {
        setVisible(false);
        return;
      }
      setWord(word);
    }
    catch (BadLocationException blex) {
      LOGGER.log(Level.WARNING, blex.getMessage(), blex);
    }
  }

  //////////////////////////////////////////////////////////////////////////////
  /**
   * The Class Listener.
   */
  private class Listener extends KeyAdapter implements DocumentListener {
    /**
     * Key pressed.
     *
     * @param e the key event
     * @see java.awt.event.KeyAdapter#keyPressed(java.awt.event.KeyEvent)
     */
    @Override
    public void keyPressed(KeyEvent e) {
      handleKeyPressed(e);
    }

    /**
     * Changed update.
     *
     * @param e the document event
     * @see javax.swing.event.DocumentListener#changedUpdate(javax.swing.event.DocumentEvent)
     */
    @Override
    public void changedUpdate(DocumentEvent e) {
      //empty
    }

    /**
     * Removes the update.
     *
     * @param e the document event
     * @see javax.swing.event.DocumentListener#removeUpdate(javax.swing.event.DocumentEvent)
     */
    @Override
    public void removeUpdate(DocumentEvent e) {
      handleDocumentEvent(textArea.getCaretPosition() - e.getLength());
    }

    /**
     * Insert update.
     *
     * @param e the document event
     * @see javax.swing.event.DocumentListener#insertUpdate(javax.swing.event.DocumentEvent)
     */
    @Override
    public void insertUpdate(DocumentEvent e) {
      handleDocumentEvent(textArea.getCaretPosition() + e.getLength());
    }
  }
  //////////////////////////////////////////////////////////////////////////////

}
