content: /*
   *  Licensed to the Apache Software Foundation (ASF) under one
   *  or more contributor license agreements.  See the NOTICE file
   *  distributed with this work for additional information
   *  regarding copyright ownership.  The ASF licenses this file
   *  to you under the Apache License, Version 2.0 (the
   *  "License"); you may not use this file except in compliance
   *  with the License.  You may obtain a copy of the License at
   *
   *        http://www.apache.org/licenses/LICENSE-2.0
   *
   *  Unless required by applicable law or agreed to in writing,
   *  software distributed under the License is distributed on an
   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   *  KIND, either express or implied.  See the License for the
   *  specific language governing permissions and limitations
   *  under the License.
   */
  package org.apache.causeway.commons.internal.debug.xray;
  
  import java.awt.BorderLayout;
  import java.awt.Component;
  import java.awt.Dimension;
  import java.awt.Font;
  import java.awt.event.KeyEvent;
  import java.awt.event.KeyListener;
  import java.awt.event.MouseEvent;
  import java.awt.event.MouseListener;
  import java.awt.event.WindowAdapter;
  import java.awt.event.WindowEvent;
  import java.net.URL;
  import java.util.Map;
  import java.util.Optional;
  import java.util.concurrent.CountDownLatch;
  import java.util.concurrent.atomic.AtomicBoolean;
  import java.util.function.Consumer;
  import java.util.stream.IntStream;
  import java.util.stream.Stream;
  
  import javax.swing.BoxLayout;
  import javax.swing.ImageIcon;
  import javax.swing.JFrame;
  import javax.swing.JLabel;
  import javax.swing.JMenuItem;
  import javax.swing.JPanel;
  import javax.swing.JPopupMenu;
  import javax.swing.JScrollPane;
  import javax.swing.JSplitPane;
  import javax.swing.JTextArea;
  import javax.swing.JTree;
  import javax.swing.ScrollPaneConstants;
  import javax.swing.SwingUtilities;
  import javax.swing.event.TreeSelectionEvent;
  import javax.swing.tree.DefaultMutableTreeNode;
  import javax.swing.tree.DefaultTreeCellRenderer;
  import javax.swing.tree.DefaultTreeModel;
  import javax.swing.tree.TreeCellRenderer;
  
  import org.apache.causeway.commons.collections.Can;
  import org.apache.causeway.commons.internal.base._Casts;
  import org.apache.causeway.commons.internal.collections._Maps;
  import org.apache.causeway.commons.internal.debug.xray.XrayModel.HasIdAndLabel;
  import org.apache.causeway.commons.internal.debug.xray.XrayModel.Stickiness;
  
  import lombok.RequiredArgsConstructor;
  
  public class XrayUi extends JFrame {
  
      private static final long serialVersionUID = 1L;
      private final JTree tree;
      private final DefaultMutableTreeNode root;
      private final XrayModel xrayModel;
  
      private static XrayUi INSTANCE;
  
      private static AtomicBoolean startRequested = new AtomicBoolean();
      private static CountDownLatch latch = null;
  
      public static void start(final int defaultCloseOperation) {
          var alreadyRequested = startRequested.getAndSet(true);
          if(!alreadyRequested) {
              latch = new CountDownLatch(1);
              SwingUtilities.invokeLater(()->new XrayUi(defaultCloseOperation));
          }
      }
  
      public static void updateModel(final Consumer<XrayModel> consumer) {
          if(startRequested.get()) {
              SwingUtilities.invokeLater(()->{
                  consumer.accept(INSTANCE.xrayModel);
                  ((DefaultTreeModel)INSTANCE.tree.getModel()).reload();
                  _SwingUtil.setTreeExpandedState(INSTANCE.tree, true);
              });
          }
      }
  
      public static void waitForShutdown() {
          if(latch==null
                  || INSTANCE == null) {
              return;
          }
          System.err.println("Waiting for XrayUi to shut down...");
          try {
              latch.await();
          } catch (InterruptedException e) {
              e.printStackTrace();
              // Restore interrupted state...
              Thread.currentThread().interrupt();
          }
      }
  
      public static boolean isXrayEnabled() {
          return startRequested.get();
      }
  
      protected XrayUi(final int defaultCloseOperation) {
  
          //create the root node
          root = new DefaultMutableTreeNode("X-ray");
  
          xrayModel = new XrayModelSimple(root);
  
          //create the tree by passing in the root node
          tree = new JTree(root);
  
          tree.setShowsRootHandles(false);
  
          var detailPanel = layoutUIAndGetDetailPanel(tree);
  
          tree.getSelectionModel().addTreeSelectionListener((final TreeSelectionEvent e) -> {
  
              var selPath = e.getNewLeadSelectionPath();
              if(selPath==null) {
                  return; // ignore event
              }
              var selectedNode = (DefaultMutableTreeNode) selPath.getLastPathComponent();
              var userObject = selectedNode.getUserObject();
  
              //detailPanel.removeAll();
  
              if(userObject instanceof XrayDataModel) {
                  ((XrayDataModel) userObject).render(detailPanel);
              } else {
                  var infoPanel = new JPanel();
                  infoPanel.add(new JLabel("Details"));
                  detailPanel.setViewportView(infoPanel);
              }
  
              detailPanel.revalidate();
              detailPanel.repaint();
  
              //System.out.println("selected: " + selectedNode.toString());
          });
  
          var popupMenu = new JPopupMenu();
  
          var clearThreadsAction = popupMenu.add(new JMenuItem("Clear Threads"));
          clearThreadsAction.addActionListener(e -> doClearThreads());
  
          var callStackMergeAction = popupMenu.add(new JMenuItem("Merge Logged Call-Stack"));
          callStackMergeAction.addActionListener(e -> doMergeCallStacksOnSelectedNodes());
  
          var deleteAction = popupMenu.add(new JMenuItem("Delete"));
          deleteAction.addActionListener(e -> doRemoveSelectedNodes());
  
          tree.setCellRenderer(new XrayTreeCellRenderer(
                  (DefaultTreeCellRenderer) tree.getCellRenderer(),
                  iconCache));
  
          tree.addMouseListener(new MouseListener() {
  
              @Override public void mouseReleased(final MouseEvent e) {}
              @Override public void mousePressed(final MouseEvent e) {}
              @Override public void mouseExited(final MouseEvent e) {}
              @Override public void mouseEntered(final MouseEvent e) {}
  
              @Override
              public void mouseClicked(final MouseEvent e) {
                  if (SwingUtilities.isRightMouseButton(e)) {
                      popupMenu.show(e.getComponent(), e.getX(), e.getY());
                  }
              }
          });
  
          tree.addKeyListener(new KeyListener() {
  
              @Override public void keyReleased(final KeyEvent e) {}
              @Override public void keyTyped(final KeyEvent e) {}
  
              @Override
              public void keyPressed(final KeyEvent e) {
                  if(e.getKeyCode() == KeyEvent.VK_DELETE) {
                      doRemoveSelectedNodes();
                      return;
                  }
                  if(e.getKeyCode() == KeyEvent.VK_F5) {
                      doClearThreads();
                      return;
                  }
              }
  
          });
  
          // report key bindings to the UI
          {
              var root = xrayModel.rootNode();
              var env = xrayModel.addDataNode(root,
                      new XrayDataModel.KeyValue("causeway-xray-keys", "X-ray Keybindings", Stickiness.CANNOT_DELETE_NODE));
              env.getData().put("F5", "Clear Threads");
              env.getData().put("DELETE", "Delete Selected Nodes");
          }
  
          this.setDefaultCloseOperation(defaultCloseOperation);
          this.setTitle("X-ray Viewer (Apache Causeway™)");
          this.pack();
          this.setSize(800, 600);
  
          this.setLocationRelativeTo(null);
          this.setVisible(true);
  
          INSTANCE = this;
  
          addWindowListener(new WindowAdapter() {
              @Override
              public void windowClosing(final WindowEvent e) {
                  latch.countDown();
              }
          });
      }
  
      private Stream<DefaultMutableTreeNode> streamSelectedNodes() {
          return Can.ofArray(tree.getSelectionModel().getSelectionPaths())
                  .stream()
                  .map(path->(DefaultMutableTreeNode)path.getLastPathComponent());
      }
  
      private Stream<DefaultMutableTreeNode> streamChildrenOf(final DefaultMutableTreeNode node) {
          return IntStream.range(0, node.getChildCount())
          .mapToObj(root::getChildAt)
          .map(DefaultMutableTreeNode.class::cast);
      }
  
      private Optional<HasIdAndLabel> extractUserObject(final DefaultMutableTreeNode node) {
          return _Casts.castTo(HasIdAndLabel.class, node.getUserObject());
      }
  
      private boolean canRemoveNode(final DefaultMutableTreeNode node) {
          if(node.getParent()==null) {
              return false; // don't remove root
          }
          return extractUserObject(node)
          .map(HasIdAndLabel::getStickiness)
          .map(stickiness->stickiness.isCanDeleteNode())
          .orElse(true); // default: allow removal
      }
  
      private void removeNode(final DefaultMutableTreeNode nodeToBeRemoved) {
          if(canRemoveNode(nodeToBeRemoved)) {
              ((DefaultTreeModel)tree.getModel()).removeNodeFromParent(nodeToBeRemoved);
              xrayModel.remove(nodeToBeRemoved);
          }
      }
  
      private void doClearThreads(){
          var root = (DefaultMutableTreeNode) tree.getModel().getRoot();
          var threadNodes = streamChildrenOf(root)
          .filter(node->extractUserObject(node)
                  .map(HasIdAndLabel::getId)
                  .map(id->id.startsWith("thread-"))
                  .orElse(false))
          .collect(Can.toCan()); // collect into can, before processing (otherwise concurrent modification)
  
          threadNodes.forEach(this::removeNode);
      }
  
      private void doRemoveSelectedNodes() {
          streamSelectedNodes().forEach(this::removeNode);
      }
  
      private void doMergeCallStacksOnSelectedNodes() {
          var logEntries = streamSelectedNodes()
          .filter(node->node.getUserObject() instanceof XrayDataModel.LogEntry)
          .map(node->(XrayDataModel.LogEntry)node.getUserObject())
          .collect(Can.toCan());
  
          if(!logEntries.getCardinality().isMultiple()) {
              System.err.println("must select at least 2 logs for merging");
              return;
          }
  
          var callStackMerger = new _CallStackMerger(logEntries);
  
          JFrame frame = new JFrame("Merged Log View");
          JPanel panel = new JPanel();
          panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
          panel.setOpaque(true);
  //        var canvas = _SwingUtil.canvas(g->{
  //            g.setColor(Color.GRAY);
  //            g.fill(g.getClip());
  //            callStackMerger.render(g);
  //        });
  //        JScrollPane scroller = new JScrollPane(canvas);
  
          //Create a text area.
          JTextArea textArea = new JTextArea("no content");
          textArea.setFont(new Font("Serif", Font.PLAIN, 16));
          textArea.setLineWrap(true);
          textArea.setWrapStyleWord(true);
          JScrollPane scroller = new JScrollPane(textArea);
          callStackMerger.render(textArea);
  
          scroller.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
          scroller.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
          panel.add(scroller);
          frame.getContentPane().add(BorderLayout.CENTER, panel);
          frame.setPreferredSize(new Dimension(800, 600));
          frame.pack();
          frame.setLocationByPlatform(true);
          frame.setVisible(true);
          frame.setResizable(true);
          frame.setVisible(true);
      }
  
      private JScrollPane layoutUIAndGetDetailPanel(final JTree masterTree) {
  
          JScrollPane masterScrollPane = new JScrollPane(masterTree);
          JScrollPane detailScrollPane = new JScrollPane();
  
          //Create a split pane with the two scroll panes in it.
          var splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
                                     masterScrollPane, detailScrollPane);
          splitPane.setOneTouchExpandable(true);
          splitPane.setDividerLocation(260);
  
          //Provide minimum sizes for the two components in the split pane.
          Dimension minimumSize = new Dimension(100, 50);
          masterScrollPane.setMinimumSize(minimumSize);
          detailScrollPane.setMinimumSize(minimumSize);
  
          detailScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
          detailScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
          detailScrollPane.getVerticalScrollBar().setUnitIncrement(8);
  
          //Provide a preferred size for the split pane.
          splitPane.setPreferredSize(new Dimension(800, 600));
  
          getContentPane().add(splitPane);
  
          return detailScrollPane;
      }
  
      // -- CUSTOM TREE NODE ICONS
  
      private final Map<String, Optional<ImageIcon>> iconCache = _Maps.newConcurrentHashMap();
  
      @RequiredArgsConstructor
      class XrayTreeCellRenderer implements TreeCellRenderer {
  
          final DefaultTreeCellRenderer delegate;
          final Map<String, Optional<ImageIcon>> iconCache;
  
          @Override
          public Component getTreeCellRendererComponent(
                  final JTree tree,
                  final Object value,
                  final boolean selected,
                  final boolean expanded,
                  final boolean leaf,
                  final int row,
                  final boolean hasFocus) {
  
              var label = (DefaultTreeCellRenderer)
                      delegate.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
  
              Object o = ((DefaultMutableTreeNode) value).getUserObject();
              if (o instanceof XrayDataModel) {
                  XrayDataModel dataModel = (XrayDataModel) o;
                  var imageIcon = iconCache.computeIfAbsent(dataModel.getIconResource(), iconResource->{
                      URL imageUrl = getClass().getResource(dataModel.getIconResource());
                      return Optional.ofNullable(imageUrl)
                              .map(ImageIcon::new);
                  });
                  imageIcon.ifPresent(label::setIcon);
                  label.setText(dataModel.getLabel());
              }
              return label;
          }
      }
  
  }
  