package treeRender;

import java.awt.*;
import java.awt.event.*;
import java.lang.reflect.*;
import java.util.*;

import javax.swing.*;
import javax.swing.tree.*;

/**
 * Ramka zawierająca drzewo klas, pole tekstowe pokazujące składowe 
 * wybranej klasy oraz pole tekstowe umożliwiające dodawanie nowych klas 
 * do drzewa.
 */
public class ClassTreeFrame extends JFrame
{
   private static final int DEFAULT_WIDTH = 700;
   private static final int DEFAULT_HEIGHT = 300;

   private DefaultMutableTreeNode root;
   private DefaultTreeModel model;
   private JTree tree;
   private JTextField textField;
   private JTextArea textArea;

   public ClassTreeFrame()
   {
      setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);

      // W korzeniu drzewa znajduje się klasa Object
      root = new DefaultMutableTreeNode(java.lang.Object.class);
      model = new DefaultTreeModel(root);
      tree = new JTree(model);

      // Dodanie tej klasy pozwala wypełnić drzewo jakimiś danymi
      addClass(getClass());

      // Tworzy ikony węzłów
      var renderer = new ClassNameTreeCellRenderer();
      renderer.setClosedIcon(new ImageIcon(getClass().getResource("red-ball.gif")));
      renderer.setOpenIcon(new ImageIcon(getClass().getResource("yellow-ball.gif")));
      renderer.setLeafIcon(new ImageIcon(getClass().getResource("blue-ball.gif")));
      tree.setCellRenderer(renderer);
      
      // Konfiguruje sposób wyboru węzłów
      tree.addTreeSelectionListener(event ->
         {
            // Użytkownik wybrał inny węzeł drzewa i należy zaktualizować opis klasy
            TreePath path = tree.getSelectionPath();
            if (path == null) return;
            var selectedNode = (DefaultMutableTreeNode) path.getLastPathComponent();
            Class<?> c = (Class<?>) selectedNode.getUserObject();
            String description = getFieldDescription(c);
            textArea.setText(description);
         });
      int mode = TreeSelectionModel.SINGLE_TREE_SELECTION;
      tree.getSelectionModel().setSelectionMode(mode);

      // Obszar tekstowy zawierający opis klasy
      textArea = new JTextArea();

      // Dodaje komponenty drzewa i pola tekstowego do panelu
      var panel = new JPanel();
      panel.setLayout(new GridLayout(1, 2));
      panel.add(new JScrollPane(tree));
      panel.add(new JScrollPane(textArea));

      add(panel, BorderLayout.CENTER);

      addTextField();
   }

   /**
    * Dodaje pole tekstowe i przycisk "Dodaj" umożliwiające dodanie 
    * nowej klasy do drzewa.
    */
   public void addTextField()
   {
      var panel = new JPanel();

      ActionListener addListener = event ->
         {
            // Dodaje do drzewa klasę, której nazwa znajduje się w polu tekstowym
            try
            {
               String text = textField.getText();
               addClass(Class.forName(text)); // czyści pole
               textField.setText("");
            }
            catch (ClassNotFoundException e)
            {
               JOptionPane.showMessageDialog(null, "Nie znaleziono klasy");
            }
         };

      // Pole tekstowe, w którym wprowadzane są nazwy nowych klas
      textField = new JTextField(20);
      textField.addActionListener(addListener);
      panel.add(textField);

      var addButton = new JButton("Dodaj");
      addButton.addActionListener(addListener);
      panel.add(addButton);

      add(panel, BorderLayout.SOUTH);
   }

   /**
    * Odnajduje obiekt w drzewie.
    * @param obj poszukiwany obiekt
    * @return węzeł zawierający obiekt lub null, jeśli obiektu nie udało się znaleźć
    */
   public DefaultMutableTreeNode findUserObject(Object obj)
   {
      // Znajduje węzeł zawierający przekazany obiekt 
      var e = (Enumeration<TreeNode>) root.breadthFirstEnumeration();
      while (e.hasMoreElements())
      {
         var node = (DefaultMutableTreeNode) e.nextElement();
         if (node.getUserObject().equals(obj)) return node;
      }
      return null;
   }

   /**
    * Dodaje do drzewa klasę i jej klasy bazowe, których nie ma 
    * jeszcze w drzewie.
    * @param c dodawana klasa
    * @return nowo dodany węzeł
    */
   public DefaultMutableTreeNode addClass(Class<?> c)
   {
      // Dodaje nową klasę do drzewa

      // Pomija typy, które nie są klasami
      if (c.isInterface() || c.isPrimitive()) return null;

      // Jeśli klasa znajduje się już w drzewie, to zwraca jej węzeł
      DefaultMutableTreeNode node = findUserObject(c);
      if (node != null) return node;

      // Jeśli klasa nie znajduje się w drzewie, 
      // to najpierw należy dodać rekurencyjnie do drzewa jej klasy bazowe

      Class<?> s = c.getSuperclass();

      DefaultMutableTreeNode parent;
      if (s == null) parent = root;
      else parent = addClass(s);

      // Dodaje klasę jako węzeł podrzędny
      var newNode = new DefaultMutableTreeNode(c);
      model.insertNodeInto(newNode, parent, parent.getChildCount());

      // Sprawia, że węzeł jest widoczny
      var path = new TreePath(model.getPathToRoot(newNode));
      tree.makeVisible(path);

      return newNode;
   }

   /**
    * Zwraca opis składowych klasy.
    * @param klasa
    * @return łańcuch znaków zawierający nazwy i typy zmiennych
    */
   public static String getFieldDescription(Class<?> c)
   {
      // Korzysta z mechanizmu refleksji
      var r = new StringBuilder();
      Field[] fields = c.getDeclaredFields();
      for (int i = 0; i < fields.length; i++)
      {
         Field f = fields[i];
         if ((f.getModifiers() & Modifier.STATIC) != 0) r.append("static ");
         r.append(f.getType().getName());
         r.append(" ");
         r.append(f.getName());
         r.append("\n");
      }
      return r.toString();
   }
}
