package processing.mode.java;

import static processing.mode.java.ASTUtils.findAllOccurrences;

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import javax.swing.JDialog;
import javax.swing.JMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JTree;
import javax.swing.WindowConstants;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeModel;

import org.eclipse.jdt.core.dom.IBinding;
import org.eclipse.jdt.core.dom.IMethodBinding;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.SimpleName;

import processing.app.Language;
import processing.app.ui.EditorStatus;
import processing.app.ui.Toolkit;
import processing.app.ui.ZoomTreeCellRenderer;


class ShowUsage {
  final JDialog window;
  final JTree tree;

  final JavaEditor editor;
  final PreprocService pps;

  final Consumer<PreprocSketch> reloadListener;

  IBinding binding;


  ShowUsage(JavaEditor editor, PreprocService pps) {
    this.editor = editor;
    this.pps = pps;

    // Add show usage option
    JMenuItem showUsageItem =
      new JMenuItem(Language.text("editor.popup.show_usage"));
    showUsageItem.addActionListener(e -> handleShowUsage());
    editor.getTextArea().getRightClickPopup().add(showUsageItem);

    reloadListener = this::reloadShowUsage;

    { // Show Usage window
      window = new JDialog(editor);
      window.setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE);
      window.setAutoRequestFocus(false);
      window.addComponentListener(new ComponentAdapter() {
        @Override
        public void componentHidden(ComponentEvent e) {
          binding = null;
          tree.setModel(null);
          pps.unregisterListener(reloadListener);
        }

        @Override
        public void componentShown(ComponentEvent e) {
          pps.registerListener(reloadListener);
        }
      });
      window.setSize(Toolkit.zoom(300, 400));
      window.setFocusableWindowState(false);
      Toolkit.setIcon(window);
      JScrollPane sp2 = new JScrollPane();
      tree = new JTree();
      ZoomTreeCellRenderer renderer =
        new ZoomTreeCellRenderer();
      tree.setCellRenderer(renderer);
      renderer.setLeafIcon(null);
      renderer.setClosedIcon(null);
      renderer.setOpenIcon(null);
      renderer.setBackgroundSelectionColor(new Color(228, 248, 246));
      renderer.setBorderSelectionColor(new Color(0, 0, 0, 0));
      renderer.setTextSelectionColor(Color.BLACK);
      sp2.setViewportView(tree);
      window.add(sp2);
    }

    tree.addTreeSelectionListener(e -> {
      if (tree.getLastSelectedPathComponent() != null) {
        DefaultMutableTreeNode tnode =
          (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();

        if (tnode.getUserObject() instanceof ShowUsageTreeNode) {
          ShowUsageTreeNode node = (ShowUsageTreeNode) tnode.getUserObject();
          editor.highlight(node.tabIndex, node.startTabOffset, node.stopTabOffset);
        }
      }
    });
  }


  // Thread: EDT
  void handleShowUsage() {
    int startOffset = editor.getSelectionStart();
    int stopOffset = editor.getSelectionStop();
    int tabIndex = editor.getSketch().getCurrentCodeIndex();

    pps.whenDoneBlocking(ps -> handleShowUsage(ps, tabIndex, startOffset, stopOffset));
  }


  // Thread: worker
  void handleShowUsage(PreprocSketch ps, int tabIndex,
                       int startTabOffset, int stopTabOffset) {
    // Map offsets
    int startJavaOffset = ps.tabOffsetToJavaOffset(tabIndex, startTabOffset);
    int stopJavaOffset = ps.tabOffsetToJavaOffset(tabIndex, stopTabOffset);

    // Find the node
    SimpleName name = ASTUtils.getSimpleNameAt(ps.compilationUnit, startJavaOffset, stopJavaOffset);
    if (name == null) {
      editor.statusMessage("Cannot find any name under cursor", EditorStatus.NOTICE);
      return;
    }

    // Find binding
    IBinding binding = ASTUtils.resolveBinding(name);
    if (binding == null) {
      editor.statusMessage("Cannot find usages, try to fix errors in your code first",
                           EditorStatus.NOTICE);
      return;
    }

    findUsageAndUpdateTree(ps, binding);
  }


  // Thread: worker
  void findUsageAndUpdateTree(PreprocSketch ps, IBinding binding) {

    this.binding = binding;

    // Get label
    String bindingType = "";
    switch (binding.getKind()) {
      case IBinding.METHOD:
        IMethodBinding method = (IMethodBinding) binding;
        if (method.isConstructor()) bindingType = "Constructor";
        else bindingType = "Method";
        break;
      case IBinding.TYPE:
        bindingType = "Type";
        break;
      case IBinding.VARIABLE:
        IVariableBinding variable = (IVariableBinding) binding;
        if (variable.isField()) bindingType = "Field";
        else if (variable.isParameter()) bindingType = "Parameter";
        else if (variable.isEnumConstant()) bindingType = "Enum constant";
        else bindingType = "Local variable";
        break;
    }

    // Find usages, map to tree nodes, add to root node
    String bindingKey = binding.getKey();
    List<SketchInterval> intervals =
        findAllOccurrences(ps.compilationUnit, bindingKey).stream()
            .map(ps::mapJavaToSketch)
            // remove occurrences which fall into generated header
            .filter(ps::inRange)
            // remove empty intervals (happens when occurence was inserted)
            .filter(in -> in.startPdeOffset < in.stopPdeOffset)
            .collect(Collectors.toList());

    int usageCount = intervals.size();

    // Get element name from PDE code if possible, otherwise use one from Java
    String elementName = intervals.stream()
        .findAny()
        .map(si -> ps.pdeCode.substring(si.startPdeOffset, si.stopPdeOffset))
        .orElseGet(binding::getName);

    // Create root node
    DefaultMutableTreeNode rootNode =
        new DefaultMutableTreeNode(bindingType + ": " + elementName);

    intervals.stream()
        // Convert to TreeNodes
        .map(in -> ShowUsageTreeNode.fromSketchInterval(ps, in))
        // Group by tab index
        .collect(Collectors.groupingBy(node -> node.tabIndex))
        // Stream Map Entries of (tab index) <-> (List<ShowUsageTreeNode>)
        .entrySet().stream()
        // Sort by tab index
        .sorted(Comparator.comparing(Map.Entry::getKey))
        .map(entry -> {
          Integer tabIndex = entry.getKey();
          List<ShowUsageTreeNode> nodes = entry.getValue();

          int count = nodes.size();
          String usageLabel = count == 1 ? "usage" : "usages";

          // Create new DefaultMutableTreeNode for this tab
          String tabLabel = "<html><font color=#222222>" +
              ps.sketch.getCode(tabIndex).getPrettyName() +
              "</font> <font color=#999999>" + count + " " + usageLabel + "</font></html>";
          DefaultMutableTreeNode tabNode = new DefaultMutableTreeNode(tabLabel);

          // Stream nodes belonging to this tab
          nodes.stream()
              // Convert TreeNodes to DefaultMutableTreeNodes
              .map(DefaultMutableTreeNode::new)
              // Add all as children of tab node
              .forEach(tabNode::add);
          return tabNode;
        })
        // Add all tab nodes as children of root node
        .forEach(rootNode::add);

    TreeModel treeModel = new DefaultTreeModel(rootNode);

    // Update tree
    EventQueue.invokeLater(() -> {
      tree.setModel(treeModel);

      // Expand all nodes
      for (int i = 0; i < tree.getRowCount(); i++) {
        tree.expandRow(i);
      }

      tree.setRootVisible(true);

      if (!window.isVisible()) {
        window.setVisible(true);
        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        GraphicsDevice defaultScreen = ge.getDefaultScreenDevice();
        Rectangle rect = defaultScreen.getDefaultConfiguration().getBounds();
        int maxX = (int) rect.getMaxX() - window.getWidth();
        int x = Math.min(editor.getX() + editor.getWidth(), maxX);
        int y = (x == maxX) ? 10 : editor.getY();
        window.setLocation(x, y);
      }
      window.toFront();
      window.setTitle("Usage of \"" + elementName + "\" : " +
                          usageCount + " time(s)");
    });
  }


  // Thread: worker
  void reloadShowUsage(PreprocSketch ps) {
    if (binding != null) {
      findUsageAndUpdateTree(ps, binding);
    }
  }


  void hide() {
    window.setVisible(false);
  }


  void dispose() {
    if (window != null) {
      window.dispose();
    }
  }
}


class ShowUsageTreeNode {
  final int tabIndex;
  final int startTabOffset;
  final int stopTabOffset;

  final String text;


  ShowUsageTreeNode(int tabIndex, int startTabOffset, int stopTabOffset, String text) {
    this.tabIndex = tabIndex;
    this.startTabOffset = startTabOffset;
    this.stopTabOffset = stopTabOffset;
    this.text = text;
  }


  static ShowUsageTreeNode fromSketchInterval(PreprocSketch ps, SketchInterval in) {
    int lineStartPdeOffset = ps.pdeCode.lastIndexOf('\n', in.startPdeOffset) + 1;
    int lineStopPdeOffset = ps.pdeCode.indexOf('\n', in.stopPdeOffset);
    if (lineStopPdeOffset == -1) lineStopPdeOffset = ps.pdeCode.length();

    int highlightStartOffset = in.startPdeOffset - lineStartPdeOffset;
    int highlightStopOffset = in.stopPdeOffset - lineStartPdeOffset;

    int tabLine = ps.tabOffsetToTabLine(in.tabIndex, in.startTabOffset);

    // TODO: what a mess
    String line = ps.pdeCode.substring(lineStartPdeOffset, lineStopPdeOffset);
    String pre = line.substring(0, highlightStartOffset)
        .replace("&", "&amp;").replace(">", "&gt;").replace("<", "&lt;");
    String highlight = line.substring(highlightStartOffset, highlightStopOffset)
        .replace("&", "&amp;").replace(">", "&gt;").replace("<", "&lt;");
    String post = line.substring(highlightStopOffset)
        .replace("&", "&amp;").replace(">", "&gt;").replace("<", "&lt;");
    line = pre + "<font color=#222222><b>" + highlight + "</b></font>" + post;
    line = line.trim();


    String text = "<html><font color=#bbbbbb>" +
        (tabLine + 1) + "</font> <font color=#777777>" + line + "</font></html>";

    return new ShowUsageTreeNode(in.tabIndex, in.startTabOffset, in.stopTabOffset, text);
  }

  @Override
  public String toString() {
    return text;
  }
}
