/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */

/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-15 The Processing Foundation

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2
as published by the Free Software Foundation.

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.
*/

package processing.mode.java;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.util.HashMap;
import java.util.Map;

import javax.swing.DefaultListModel;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.ListSelectionModel;
import javax.swing.border.EmptyBorder;

import processing.app.Base;
import processing.app.Messages;
import processing.app.Mode;
import processing.app.syntax.JEditTextArea;
import processing.app.ui.Theme;
import processing.app.ui.Toolkit;


public class CompletionPanel {
  static final int MAX_HEIGHT = 300;
  static final int ICON_SIZE = 16;

  /** The completion list generated by ASTGenerator */
  final private JList<CompletionCandidate> completionList;

  /** The popup menu in which the suggestion list is shown */
  final private JPopupMenu popupMenu;

  /** Partial word that triggered the code completion */
  final private String subWord;

  /** Position where the completion has to be inserted */
  final private int caretPos;

  /** Scroll pane in which the completion list is displayed */
  final private JScrollPane scrollPane;

  protected JavaEditor editor;

  static protected final int MOUSE_COMPLETION = 10;
  static protected final int KEYBOARD_COMPLETION = 20;

  static public ImageIcon classIcon;
  static public ImageIcon fieldIcon;
  static public ImageIcon methodIcon;
  static public ImageIcon localIcon;

  static private final Map<String, String> iconColors = new HashMap<>();
  static private final Map<String, ImageIcon> iconCache = new HashMap<>();

  static Color selectionBgColor;
  static Color textColor;


  /**
   * Triggers the completion popup
   * @param caret - insertion caret position
   * @param subWord - Partial word that triggered the code completion
   * @param items - completion candidates
   * @param location - Point location where popup list is to be displayed
   */
  public CompletionPanel(JavaEditor editor, int caret, String subWord,
                         DefaultListModel<CompletionCandidate> items,
                         Point location) {
    this.editor = editor;
    this.caretPos = caret;
    if (subWord.indexOf('.') != -1 && subWord.indexOf('.') != subWord.length()-1) {
      this.subWord = subWord.substring(subWord.lastIndexOf('.') + 1);
    } else {
      this.subWord = subWord;
    }

    // more like initTheme(), but using this for consistency
    updateTheme();

    popupMenu = new JPopupMenu();
//    popupMenu.setLightWeightPopupEnabled(true);  // doesn't help?
//    popupMenu.removeAll();  // uh, this is already empty
    popupMenu.setOpaque(false);  // why? [fry 220803]
    // setting border null just makes it use the default 2px border
    popupMenu.setBorder(new EmptyBorder(0, 0, 0, 0));

    scrollPane = new JScrollPane();
    completionList = new JList<>(items) {
      {
        setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        setSelectedIndex(0);
        addMouseListener(new MouseAdapter() {
          @Override
          public void mouseClicked(MouseEvent e) {
            if (e.getClickCount() == 2) {
              insertSelection(MOUSE_COMPLETION);
              setInvisible();
            }
          }
        });
        setCellRenderer(new CustomListRenderer());
        setFocusable(false);
        setFont(Toolkit.getSansFont(12, Font.PLAIN));
      }
    };
    scrollPane.setViewportView(completionList);
    // remove an ugly multi-line border around it
    scrollPane.setBorder(null);

    popupMenu.add(scrollPane, BorderLayout.CENTER);
    Dimension pref = completionList.getPreferredSize();
    popupMenu.setPopupSize(pref.width + classIcon.getIconWidth(),
                           Math.min(pref.height + 2, MAX_HEIGHT));
    popupMenu.setFocusable(false);
    // TODO: Update JavaDoc to completionList.getSelectedValue()
    JavaTextArea textarea = editor.getJavaTextArea();
    popupMenu.show(textarea, location.x, textarea.getBaseline(0, 0) + location.y);
    //log("Suggestion shown: " + System.currentTimeMillis());
  }


  /**
   * Render an icon and store it in the cache, or return the cached
   * version if available to avoid re-rendering several SVG icons
   * each time a completion is triggered.
   */
  static private ImageIcon renderIcon(File dir, String word) {
    String hexColor = Theme.get("editor.completion." + word + ".color");
    String cacheColor = iconColors.getOrDefault(word, null);
    if (!hexColor.equals(cacheColor)) {
      ImageIcon icon =
        Toolkit.renderIcon(new File(dir, word + ".svg"), hexColor, ICON_SIZE);
      iconColors.put(word, hexColor);
      iconCache.put(word, icon);
    }
    return iconCache.get(word);
  }


  /**
   * This is private because there's no need to call updateTheme() externally
   * because the theme will never be updated *while* the window is open.
   */
  private void updateTheme() {
    Mode mode = editor.getMode();
    File dir = new File(mode.getFolder(), "theme/completion");

    classIcon = renderIcon(dir, "class");
    fieldIcon = renderIcon(dir, "field");
    localIcon = renderIcon(dir, "local");
    methodIcon = renderIcon(dir, "method");

    selectionBgColor = new Color(0xffF0F0F0);
    textColor = new Color(0xff222222);
  }


  public boolean isVisible() {
    return popupMenu.isVisible();
  }


  public void setInvisible() {
    popupMenu.setVisible(false);
  }


  /**
   * Inserts the CompletionCandidate chosen from the suggestion list
   * @param completionSource - whether being completed via key press or mouse click.
   * @return true - if code was successfully inserted at the caret position
   */
  protected boolean insertSelection(int completionSource) {
    if (completionList.getSelectedValue() != null) {
      try {
        // If user types 'abc.', sub word becomes '.' and null is returned
        String currentSubWord = fetchCurrentSubWord();
        int currentSubWordLen =
            (currentSubWord == null) ? 0 : currentSubWord.length();
        String selectedSuggestion =
          completionList.getSelectedValue().getCompletionString();

        if (currentSubWord != null) {
          selectedSuggestion = selectedSuggestion.substring(currentSubWordLen);
        } else {
          currentSubWord = "";
        }

        String completionString =
          completionList.getSelectedValue().getCompletionString();
        if (selectedSuggestion.endsWith(" )")) { // the case of single param methods
          // selectedSuggestion = ")";
          if (completionString.endsWith(" )")) {
            completionString =
              completionString.substring(0, completionString.length() - 2) + ")";
          }
        }

        boolean mouseClickOnOverloadedMethods = false;
        if (completionSource == MOUSE_COMPLETION) {
          // The case of overloaded methods, displayed as 'foo(...)'
          // They have completion strings as 'foo('. See #2755
          if (completionString.endsWith("(")) {
            mouseClickOnOverloadedMethods = true;
          }
        }

        JavaTextArea textArea = editor.getJavaTextArea();
        Messages.err(subWord + " <= sub word, Inserting suggestion=> " +
          selectedSuggestion + " Current sub: " + currentSubWord);
        if (currentSubWord.length() > 0) {
          textArea.getDocument().remove(caretPos - currentSubWordLen,
                                        currentSubWordLen);
        }

        textArea.getDocument().insertString(caretPos - currentSubWordLen,
                                            completionString, null);
        if (selectedSuggestion.endsWith(")") && !selectedSuggestion.endsWith("()")) {
          // place the caret between '( and first ','
          int x = selectedSuggestion.indexOf(',');
          if (x == -1) {
            // the case of single param methods, containing no ','
            textArea.setCaretPosition(textArea.getCaretPosition() - 1); // just before ')'
          } else {
            textArea.setCaretPosition(caretPos + x);
          }
        }

        // https://github.com/processing/processing/issues/2755
        if (!completionList.getSelectedValue().getLabel().contains("...")) {
          setInvisible();
        }
        if (mouseClickOnOverloadedMethods) {
          ((JavaTextArea) editor.getTextArea()).fetchPhrase();
        }
        return true;

      } catch (Exception e) {
        e.printStackTrace();
      }
      setInvisible();
    }
    return false;
  }


  private String fetchCurrentSubWord() {
    JEditTextArea ta = editor.getTextArea();
    int off = ta.getCaretPosition();
    //log2("off " + off);
    if (off < 0)
      return null;
    int line = ta.getCaretLine();
    if (line < 0)
      return null;
    String s = ta.getLineText(line);

    int x = ta.getCaretPosition() - ta.getLineStartOffset(line) - 1, x1 = x - 1;
    if (x >= s.length() || x < 0)
      return null; //TODO: Does this check cause problems? Verify.
    Messages.log(" x char: " + s.charAt(x));

    String word = String.valueOf(s.charAt(x));
    if (s.trim().length() == 1) {
      word = word.trim();
      if (word.endsWith("."))
        word = word.substring(0, word.length() - 1);

      return word;
    }

    // If user types 'abc.', sub word becomes '.'
    if (word.equals(".")) return null;

    int i = 0;
    while (true) {
      i++;
      //TODO: currently works on single line only. "a. <new line> b()" won't be detected
      if (x1 >= 0) {
//        if (s.charAt(x1) != ';' && s.charAt(x1) != ',' && s.charAt(x1) != '(')
        if (Character.isLetterOrDigit(s.charAt(x1)) || s.charAt(x1) == '_') {

          word = s.charAt(x1--) + word;

        } else {
          break;
        }
      } else {
        break;
      }
      if (i > 200) {
        // time out!
        break;
      }
    }
    if (Character.isDigit(word.charAt(0)))
      return null;
    word = word.trim();
    if (word.endsWith(".")) {
      word = word.substring(0, word.length() - 1);
    }
    return word;
  }


  /**
   * When up arrow key is pressed, moves the highlighted selection up in the list
   */
  protected void moveUp() {
    if (completionList.getSelectedIndex() == 0) {
      scrollPane.getVerticalScrollBar().setValue(scrollPane.getVerticalScrollBar().getMaximum());
      selectIndex(completionList.getModel().getSize() - 1);

    } else {
      int index = Math.max(completionList.getSelectedIndex() - 1, 0);
      selectIndex(index);
      int step = scrollPane.getVerticalScrollBar().getMaximum()
          / completionList.getModel().getSize();
      scrollPane.getVerticalScrollBar().setValue(scrollPane
                                                 .getVerticalScrollBar()
                                                 .getValue()
                                                 - step);
      // TODO: update JavaDoc to completionList.getSelectedValue()
    }
  }


  /**
   * When down arrow key is pressed, moves the highlighted selection down in the list
   */
  protected void moveDown() {
    if (completionList.getSelectedIndex() == completionList.getModel().getSize() - 1) {
      scrollPane.getVerticalScrollBar().setValue(0);
      selectIndex(0);

    } else {
      int index = Math.min(completionList.getSelectedIndex() + 1,
                           completionList.getModel().getSize() - 1);
      selectIndex(index);
      // TODO: update JavaDoc to completionList.getSelectedValue()
      int step = scrollPane.getVerticalScrollBar().getMaximum() / completionList.getModel().getSize();
      scrollPane.getVerticalScrollBar().setValue(scrollPane.getVerticalScrollBar().getValue() + step);
    }
  }


  private void selectIndex(int index) {
    completionList.setSelectedIndex(index);
  }


  /**
   * Custom cell renderer to display icons along with the completion candidates
   * @author Manindra Moharana <me@mkmoharana.com>
   *
   */
  private static class CustomListRenderer extends javax.swing.DefaultListCellRenderer {

    public Component getListCellRendererComponent(JList<?> list, Object value,
                                                  int index,
                                                  boolean isSelected,
                                                  boolean cellHasFocus) {
      JLabel label = (JLabel) super.getListCellRendererComponent(list, value,
                                                                 index,
                                                                 isSelected,
                                                                 cellHasFocus);
      if (isSelected) {
        label.setBackground(selectionBgColor);
      }
      label.setForeground(textColor);

      if (value instanceof CompletionCandidate) {
        CompletionCandidate cc = (CompletionCandidate) value;
        switch (cc.getType()) {
        case CompletionCandidate.LOCAL_VAR:
          label.setIcon(localIcon);
          break;
        case CompletionCandidate.LOCAL_FIELD:
        case CompletionCandidate.PREDEF_FIELD:
          label.setIcon(fieldIcon);
          break;
        case CompletionCandidate.LOCAL_METHOD:
        case CompletionCandidate.PREDEF_METHOD:
          label.setIcon(methodIcon);
          break;
        case CompletionCandidate.LOCAL_CLASS:
        case CompletionCandidate.PREDEF_CLASS:
          label.setIcon(classIcon);
          break;

        default:
          Messages.log("(CustomListRenderer)Unknown CompletionCandidate type " + cc.getType());
          break;
        }
      } else {
        Messages.log("(CustomListRenderer)Unknown CompletionCandidate object " + value);
      }
      return label;
    }
  }
}
