/*
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 * Created on March 25 2003
 */
package org.jboss.cache.aop;


import org.apache.log4j.Logger;
import org.jboss.cache.*;
import org.jgroups.View;

import javax.swing.*;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableColumn;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
import java.util.*;

//import java.util.List;

/**
 * Graphical view of a ReplicatedTree (using the MVC paradigm). An instance of this class needs to be given a
 * reference to the underlying model (ReplicatedTree) and needs to registers as a ReplicatedTreeListener. Changes
 * to the tree structure are propagated from the model to the view (via ReplicatedTreeListener), changes from the
 * GUI (e.g. by a user) are executed on the tree model (which will broadcast the changes to all replicas).<p>
 * The view itself caches only the nodes, but doesn't cache any of the data (HashMap) associated with it. When
 * data needs to be displayed, the underlying tree will be accessed directly.
 *
 * @author Ben Wang
 * @version $Revision: 1.10.4.2 $
 */
public class TreeCacheAopView
{
   TreeCacheAopGui gui_ = null;
   TreeCacheAop cache_ = null;
   static Logger logger_ = Logger.getLogger(TreeCacheAopView.class.getName());

   public TreeCacheAopView(TreeCacheAop cache) throws Exception
   {
      this.cache_ = cache;
   }

   public void start() throws Exception
   {
      if (gui_ == null) {
         logger_.info("start(): creating the GUI");
         gui_ = new TreeCacheAopGui(cache_);
      }
   }

   public void stop()
   {
      if (gui_ != null) {
         logger_.info("stop(): disposing the GUI");
         gui_.dispose();
         gui_ = null;
      }
   }

   void populateTree(String dir) throws Exception
   {
      File file = new File(dir);

      if (!file.exists()) return;

      put(dir, null);

      if (file.isDirectory()) {
         String[] children = file.list();
         if (children != null && children.length > 0) {
            for (int i = 0; i < children.length; i++)
               populateTree(dir + "/" + children[i]);
         }
      }
   }

   void put(String fqn, Map m)
   {
      try {
         cache_.put(fqn, m);
      } catch (Throwable t) {
         System.err.println("TreeCacheAopView.put(): " + t);
      }
   }

   public static void main(String args[])
   {
      TreeCacheAop tree = null;
      TreeCacheAopView demo;
      String start_directory = null;
      String resource="META-INF/replSync-service.xml";

      for(int i=0; i < args.length; i++) {
         if(args[i].equals("-config")) {
            resource=args[++i];
            continue;
         }
         help();
         return;
      }


      try {
         tree = new TreeCacheAop();
         PropertyConfigurator config = new PropertyConfigurator();
         config.configure(tree, resource);

         tree.addTreeCacheListener(new TreeCacheView.MyListener());
         tree.createService();
         tree.startService();

         Runtime.getRuntime().addShutdownHook(new ShutdownThread(tree));

         demo = new TreeCacheAopView(tree);
         demo.start();
         if (start_directory != null && start_directory.length() > 0) {
            demo.populateTree(start_directory);
         }
      } catch (Exception ex) {
         ex.printStackTrace();
      }
   }


   static class ShutdownThread extends Thread
   {
      TreeCacheAop tree = null;

      ShutdownThread(TreeCacheAop tree)
      {
         this.tree = tree;
      }

      public void run()
      {
         tree.stop();
      }
   }

   static void help()
   {
      System.out.println("TreeCacheAopView [-help] [-config <configuration file (XML)]" +
            "[-mbean_name <name of TreeCache MBean>] " +
            "[-start_directory <dirname>] [-props <props>] " +
            "[-use_queue <true/false>] [-queue_interval <ms>] " +
            "[-queue_max_elements <num>]");
   }

}


class TreeCacheAopGui extends JFrame implements WindowListener, TreeCacheListener,
      TreeSelectionListener, TableModelListener
{
   TreeCacheAop cache_;
   DefaultTreeModel tree_model = null;
   JTree jtree = null;
   DefaultTableModel table_model = new DefaultTableModel();
   JTable table = new JTable(table_model);
   MyNode root = new MyNode(SEP, Fqn.fromString(SEP));
   String props = null;
   String selected_node = null;
   JPanel tablePanel = null;
   JMenu operationsMenu = null;
   JPopupMenu operationsPopup = null;
   JMenuBar menubar = null;
   boolean use_system_exit = false;
   static String SEP = TreeCache.SEPARATOR;
   private static final int KEY_COL_WIDTH = 20;
   private static final int VAL_COL_WIDTH = 300;
   final String STRING = String.class.getName();
   final String MAP = Map.class.getName();
   final String OBJECT = Object.class.getName();
   String currentNodeSelected = null;

   public TreeCacheAopGui(TreeCacheAop cache) throws Exception
   {
      this.cache_ = cache;

      cache_.addTreeCacheListener(this);
      addNotify();
      setTitle("TreeCacheAopGui: mbr=" + getLocalAddress());

      tree_model = new DefaultTreeModel(root);
      jtree = new JTree(tree_model);
      jtree.setDoubleBuffered(true);
      jtree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);

      JPanel panel = new JPanel();
      panel.setLayout(new BorderLayout());
      JScrollPane scroll_pane = new JScrollPane(jtree);
      panel.add(scroll_pane, BorderLayout.CENTER);

      populateTree();

      getContentPane().add(panel, BorderLayout.CENTER);
      addWindowListener(this);

      table_model.setColumnIdentifiers(new String[]{"Name", "Value"});
      table_model.addTableModelListener(this);

      setTableColumnWidths();

      tablePanel = new JPanel();
      tablePanel.setLayout(new BorderLayout());
      tablePanel.add(table.getTableHeader(), BorderLayout.NORTH);
      tablePanel.add(table, BorderLayout.CENTER);

      getContentPane().add(tablePanel, BorderLayout.SOUTH);

      jtree.addTreeSelectionListener(this);//REVISIT

      MouseListener ml = new MouseAdapter()
      {
         public void mouseClicked(MouseEvent e)
         {
            int selRow = jtree.getRowForLocation(e.getX(), e.getY());
            TreePath selPath = jtree.getPathForLocation(e.getX(), e.getY());
            if (selRow != -1) {
               selected_node = makeFQN(selPath.getPath());
               jtree.setSelectionPath(selPath);

               if (e.getModifiers() == java.awt.event.InputEvent.BUTTON3_MASK) {
                  operationsPopup.show(e.getComponent(),
                        e.getX(), e.getY());
               }
            }
         }
      };

      jtree.addMouseListener(ml);

      createMenus();
      setLocation(50, 50);
      setSize(getInsets().left + getInsets().right + 485,
            getInsets().top + getInsets().bottom + 367);

      init();
      setVisible(true);
   }

   void setSystemExit(boolean flag)
   {
      use_system_exit = flag;
   }

   public void windowClosed(WindowEvent event)
   {
   }

   public void windowDeiconified(WindowEvent event)
   {
   }

   public void windowIconified(WindowEvent event)
   {
   }

   public void windowActivated(WindowEvent event)
   {
   }

   public void windowDeactivated(WindowEvent event)
   {
   }

   public void windowOpened(WindowEvent event)
   {
   }

   public void windowClosing(WindowEvent event)
   {
      dispose();
   }


   public void tableChanged(TableModelEvent evt)
   {
      int row, col;
      String key, val;

      if (evt.getType() == TableModelEvent.UPDATE) {
         row = evt.getFirstRow();
         col = evt.getColumn();
         if (col == 0) {  // set()
            key = (String) table_model.getValueAt(row, col);
            val = (String) table_model.getValueAt(row, col + 1);
            if (key != null && val != null) {
               // tree.put(selected_node, key, val);

               try {
                  cache_.put(selected_node, key, val);
               } catch (Exception e) {
                  e.printStackTrace();
               }

            }
         } else {          // add()
            key = (String) table_model.getValueAt(row, col - 1);
            val = (String) table.getValueAt(row, col);
            if (key != null && val != null) {
               put(selected_node, key, val);
            }
         }
      }
   }


   public void valueChanged(TreeSelectionEvent evt)
   {
      TreePath path = evt.getPath();
      String fqn = SEP;
      String component_name;
      Map data = null;

      for (int i = 0; i < path.getPathCount(); i++) {
         component_name = ((MyNode) path.getPathComponent(i)).name;
         if (component_name.equals(SEP))
            continue;
         if (fqn.equals(SEP))
            fqn += component_name;
         else
            fqn = fqn + SEP + component_name;
      }
      data = getData(fqn);
      System.out.println("valueChanged(): fqn: " + fqn + " data: " + data);
      if (data != null) {
         getContentPane().add(tablePanel, BorderLayout.SOUTH);
         populateTable(data);
         validate();
      } else {
         clearTable();
         getContentPane().remove(tablePanel);
         validate();
      }
   }



   /* ------------------ ReplicatedTree.ReplicatedTreeListener interface ------------ */

   public void nodeCreated(Fqn fqn)
   {
      MyNode n, p;

      n = root.add(fqn);
      if (n != null) {
         p = (MyNode) n.getParent();
         tree_model.reload(p);
         jtree.scrollPathToVisible(new TreePath(n.getPath()));
      }
   }

   public void nodeRemoved(Fqn fqn)
   {
      MyNode n;
      TreeNode par;

      n = root.findNode(fqn.toString());
      if (n != null) {
         n.removeAllChildren();
         par = n.getParent();
         n.removeFromParent();
         tree_model.reload(par);
      }
   }

   public void nodeLoaded(Fqn fqn) {
      nodeCreated(fqn);
   }

   public void nodeEvicted(Fqn fqn) {
      nodeRemoved(fqn);
   }

   public void nodeModified(final Fqn fqn)
   {
      // needs to be in a separate thread because Swing thread is different from callback thread,
      // resulting in a lock until lock timeout kicks in
     // new Thread() {
       //  public void run() {
            Map data;
            if (currentNodeSelected != null && !currentNodeSelected.equals(fqn.toString())) return; // Node modified is not visible. Continue...
            data = getData(fqn.toString());
            populateTable(data); // REVISIT
      //   }
      //}.start();

      /*
        poulateTable is the current table being shown is the info of the node. that is modified.
      */
   }

   public void nodeVisited(Fqn fqn)
   {
      //To change body of implemented methods use File | Settings | File Templates.
   }

   public void cacheStarted(TreeCache cache)
   {
      //To change body of implemented methods use File | Settings | File Templates.
   }

   public void cacheStopped(TreeCache cache)
   {
      //To change body of implemented methods use File | Settings | File Templates.
   }

   public void viewChange(final View new_view)
   {
      new Thread()
      {
         public void run()
         {
            Vector mbrship;
            if (new_view != null && (mbrship = new_view.getMembers()) != null) {
               _put(SEP, "members", mbrship);
               _put(SEP, "coordinator", mbrship.firstElement());
            }
         }
      }.start();
   }




   /* ---------------- End of ReplicatedTree.ReplicatedTreeListener interface -------- */

   /*----------------- Runnable implementation to make View change calles in AWT Thread ---*/

   public void run()
   {

   }



   /* ----------------------------- Private Methods ---------------------------------- */

   /**
    * Fetches all data from underlying tree model and display it graphically
    */
   void init()
   {
      Vector mbrship = null;

      addGuiNode(SEP);

      mbrship = getMembers() != null ? (Vector) getMembers().clone() : null;
      if (mbrship != null && mbrship.size() > 0) {
         _put(SEP, "members", mbrship);
         _put(SEP, "coordinator", mbrship.firstElement());
      }
   }


   /**
    * Fetches all data from underlying tree model and display it graphically
    */
   private void populateTree()
   {
      addGuiNode(SEP);
   }


   /**
    * Recursively adds GUI nodes starting from fqn
    */
   void addGuiNode(String fqn)
   {
      Set children;
      String child_name;

      if (fqn == null) return;

      // 1 . Add myself
      root.add(Fqn.fromString(fqn));

      // 2. Then add my children
      children = getChildrenNames(fqn);
      if (children != null) {
         for (Iterator it = children.iterator(); it.hasNext();) {
            child_name = (String) it.next();
            addGuiNode(fqn + SEP + child_name);
         }
      }
   }


   String makeFQN(Object[] path)
   {
      StringBuffer sb = new StringBuffer("");
      String tmp_name;

      if (path == null) return null;
      for (int i = 0; i < path.length; i++) {
         tmp_name = ((MyNode) path[i]).name;
         if (tmp_name.equals(SEP))
            continue;
         else
            sb.append(SEP + tmp_name);
      }
      tmp_name = sb.toString();
      if (tmp_name.length() == 0)
         return SEP;
      else
         return tmp_name;
   }

   void clearTable()
   {
      int num_rows = table.getRowCount();

      if (num_rows > 0) {
         for (int i = 0; i < num_rows; i++)
            table_model.removeRow(0);
         table_model.fireTableRowsDeleted(0, num_rows - 1);
         repaint();
      }
   }


   void populateTable(Map data)
   {
      String key, strval = "<null>";
      Object val;
      int num_rows = 0;
      Map.Entry entry;

      if (data == null) return;
      num_rows = data.size();
      clearTable();

      if (num_rows > 0) {
         for (Iterator it = data.entrySet().iterator(); it.hasNext();) {
            entry = (Map.Entry) it.next();
            key = (String) entry.getKey();
            val = entry.getValue();
            if (val != null) strval = val.toString();
            table_model.addRow(new Object[]{key, strval});
         }
         table_model.fireTableRowsInserted(0, num_rows - 1);
         validate();
      }
   }

   private void setTableColumnWidths()
   {
      table.sizeColumnsToFit(JTable.AUTO_RESIZE_NEXT_COLUMN);
      TableColumn column = null;
      column = table.getColumnModel().getColumn(0);
      column.setMinWidth(KEY_COL_WIDTH);
      column.setPreferredWidth(KEY_COL_WIDTH);
      column = table.getColumnModel().getColumn(1);
      column.setPreferredWidth(VAL_COL_WIDTH);
   }

   private void createMenus()
   {
      menubar = new JMenuBar();
      operationsMenu = new JMenu("Operations");
      AddNodeAction addNode = new AddNodeAction();
      addNode.putValue(AbstractAction.NAME, "Add to this node");
      RemoveNodeAction removeNode = new RemoveNodeAction();
      removeNode.putValue(AbstractAction.NAME, "Remove this node");
      AddModifyDataForNodeAction addModAction = new AddModifyDataForNodeAction();
      addModAction.putValue(AbstractAction.NAME, "Add/Modify data");
      PrintLockInfoAction print_locks = new PrintLockInfoAction();
      print_locks.putValue(AbstractAction.NAME, "Print lock information (stdout)");
      ReleaseAllLocksAction release_locks = new ReleaseAllLocksAction();
      release_locks.putValue(AbstractAction.NAME, "Release all locks");
      ExitAction exitAction = new ExitAction();
      exitAction.putValue(AbstractAction.NAME, "Exit");
      operationsMenu.add(addNode);
      operationsMenu.add(removeNode);
      operationsMenu.add(addModAction);
      operationsMenu.add(print_locks);
      operationsMenu.add(release_locks);
      operationsMenu.add(exitAction);
      menubar.add(operationsMenu);
      setJMenuBar(menubar);

      operationsPopup = new JPopupMenu();
      operationsPopup.add(addNode);
      operationsPopup.add(removeNode);
      operationsPopup.add(addModAction);
   }

   Object getLocalAddress()
   {
      try {
         return cache_.getLocalAddress();
      } catch (Throwable t) {
         System.err.println("TreeCacheAopGui.getLocalAddress(): " + t);
         return null;
      }
   }

   Map getData(String fqn)
   {
      Map data;
      Set keys;
      String key;
      Object value;

      if (fqn == null) return null;
      // Let's track the node displayed.
      currentNodeSelected = fqn;

//      System.out.println("findNode(): fqnStr: " +fqn);
      MyNode node = root.findNode(fqn);
      if (node == null) return null;
      // System.out.println("findNode(): fqnStr: " + fqn + " node fqn: " + node.getFqn().toString());
      keys = getKeys(node.getFqn());
      if (keys == null) return null;
      data = new HashMap();
      for (Iterator it = keys.iterator(); it.hasNext();) {
         key = it.next().toString();
         value = get(node.getFqn(), key);
         if (value != null)
            data.put(key, value);
      }
      return data;
   }


   void put(String fqn, Map m)
   {
      try {
         cache_.put(fqn, m);
      } catch (Throwable t) {
         System.err.println("TreeCacheAopGui.put(): " + t);
      }
   }


   private void put(String fqn, String key, Object value)
   {
      try {
         cache_.put(fqn, key, value);
      } catch (Throwable t) {
         System.err.println("TreeCacheAopGui.put(): " + t);
      }
   }

   void _put(String fqn, String key, Object value)
   {
      try {
         cache_._put(null, fqn, key, value, false);
      } catch (Throwable t) {
         System.err.println("TreeCacheAopGui._put(): " + t);
      }
   }

   Set getKeys(Fqn fqn)
   {
      try {
         return cache_.getKeys(fqn);
      } catch (Throwable t) {
         t.printStackTrace();
         System.err.println("TreeCacheAopGui.getKeys(): " + t);
         return null;
      }
   }

   Object get(Fqn fqn, String key)
   {
      try {
         return cache_.get(fqn, key);
      } catch (Throwable t) {
         System.err.println("TreeCacheAopGui.get(): " + t);
         return null;
      }
   }

   Set getChildrenNames(String fqn)
   {
      try {
         return cache_.getChildrenNames(fqn);
      } catch (Throwable t) {
         System.err.println("TreeCacheAopGui.getChildrenNames(): " + t);
         return null;
      }
   }

   Vector getMembers()
   {
      try {
         return cache_.getMembers();
      } catch (Throwable t) {
         System.err.println("TreeCacheAopGui.getMembers(): " + t);
         return null;
      }
   }


   /* -------------------------- End of Private Methods ------------------------------ */

   /*----------------------- Actions ---------------------------*/
   class ExitAction extends AbstractAction
   {
      public void actionPerformed(ActionEvent e)
      {
         dispose();
         System.exit(0);
      }
   }

   class AddNodeAction extends AbstractAction
   {
      public void actionPerformed(ActionEvent e)
      {
         JTextField fqnTextField = new JTextField();
         if (selected_node != null)
            fqnTextField.setText(selected_node);
         Object[] information = {"Enter fully qualified name",
                                 fqnTextField};
         final String btnString1 = "OK";
         final String btnString2 = "Cancel";
         Object[] options = {btnString1, btnString2};
         int userChoice = JOptionPane.showOptionDialog(null,
               information,
               "Add Node",
               JOptionPane.YES_NO_OPTION,
               JOptionPane.PLAIN_MESSAGE,
               null,
               options,
               options[0]);
         if (userChoice == 0) {
            String userInput = fqnTextField.getText();
            put(userInput, null);
         }
      }
   }


   class PrintLockInfoAction extends AbstractAction
   {
      public void actionPerformed(ActionEvent e)
      {
         System.out.println("\n*** lock information ****\n" + cache_.printLockInfo());
      }
   }

   class ReleaseAllLocksAction extends AbstractAction
   {
      public void actionPerformed(ActionEvent e)
      {
         cache_.releaseAllLocks("/");
      }
   }

   class RemoveNodeAction extends AbstractAction
   {
      public void actionPerformed(ActionEvent e)
      {
         try {
            cache_.remove(selected_node);
         } catch (Throwable t) {
            System.err.println("RemoveNodeAction.actionPerformed(): " + t);
         }
      }
   }

   class AddModifyDataForNodeAction extends AbstractAction
   {
      public void actionPerformed(ActionEvent e)
      {
         Map data = getData(selected_node);
         if (data != null) {
         } else {
            clearTable();
            data = new HashMap();
            data.put("Add Key", "Add Value");

         }
         populateTable(data);
         getContentPane().add(tablePanel, BorderLayout.SOUTH);
         validate();

      }
   }


   class MyNode extends DefaultMutableTreeNode
   {
      String name = "<unnamed>"; // node name
      Fqn fqn;  // corresponding fqn

      MyNode(String name, Fqn fqn)
      {
         this.fqn = fqn;
         this.name = name;
      }

      public Fqn getFqn()
      {
         return fqn;
      }

      /**
       * Adds a new node to the view. Intermediary nodes will be created if they don't yet exist.
       * Returns the first node that was created or null if node already existed
       */
      public MyNode add(Fqn fqn)
      {
         MyNode curr, n, ret = null;
         StringTokenizer tok;
         String child_name;

         if (fqn == null) return null;
         curr = this;
         String fqnStr = fqn.toString();
         tok = new StringTokenizer(fqnStr, TreeCacheAopGui.SEP);

         int i = 0;
         while (tok.hasMoreTokens()) {
            child_name = tok.nextToken();
            n = curr.findChild(child_name);
            if (n == null) {
               n = new MyNode(child_name, fqn.getFqnChild(i + 1));
               if (ret == null) ret = n;
               curr.add(n);
            }
            curr = n;
            i++;
         }
         return ret;
      }


      /**
       * Removes a node from the view. Child nodes will be removed as well
       */
      public void remove(String fqn)
      {
         removeFromParent();
      }


      MyNode findNode(String fqn)
      {
         MyNode curr, n;
         StringTokenizer tok;
         String child_name;

         if (fqn == null) return null;
         curr = this;
         tok = new StringTokenizer(fqn, TreeCacheAopGui.SEP);

         while (tok.hasMoreTokens()) {
            child_name = tok.nextToken();
            n = curr.findChild(child_name);
            if (n == null)
               return null;
            curr = n;
         }
         return curr;
      }


      MyNode findChild(String relative_name)
      {
         MyNode child;

         if (relative_name == null || getChildCount() == 0)
            return null;
         for (int i = 0; i < getChildCount(); i++) {
            child = (MyNode) getChildAt(i);
            if (child.name == null) {
               continue;
            }

            if (child.name.equals(relative_name))
               return child;
         }
         return null;
      }


      String print(int indent)
      {
         StringBuffer sb = new StringBuffer();

         for (int i = 0; i < indent; i++)
            sb.append(" ");
         if (!isRoot()) {
            if (name == null)
               sb.append("/<unnamed>");
            else {
               sb.append(TreeCacheAopGui.SEP + name);
            }
         }
         sb.append("\n");
         if (getChildCount() > 0) {
            if (isRoot())
               indent = 0;
            else
               indent += 4;
            for (int i = 0; i < getChildCount(); i++)
               sb.append(((MyNode) getChildAt(i)).print(indent));
         }
         return sb.toString();
      }


      public String toString()
      {
         return name;
      }

   }


}