package org.freehep.swing.treetable.test;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.text.NumberFormat;

import javax.swing.ButtonGroup;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JRadioButtonMenuItem;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.border.BevelBorder;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeExpansionListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.tree.TreePath;

import org.freehep.swing.treetable.JTreeTable;

/**
 * Assembles the UI. The UI consists of a JTreeTable and a status label.
 * As nodes are loaded by the FileSystemModel2, in a background thread,
 * the status label updates as well as the renderer to draw the node that
 * is being loaded differently.
 *
 * @author Scott Violet
 * @author Philip Milne
 */
public class TreeTableExample2
{
   /** Number of instances of TreeTableExample2. */
   protected static int         ttCount;
   
   /** Model for the JTreeTable. */
   protected FileSystemModel2    model;
   /** Used to represent the model. */
   protected JTreeTable         treeTable;
   /** Row the is being reloaded. */
   protected int                reloadRow;
   /** TreePath being reloaded. */
   protected TreePath           reloadPath;
   /** A counter increment as the Timer fies and the same path is
    * being reloaded. */
   protected int                reloadCounter;
   /** Timer used to update reload state. */
   protected Timer              timer;
   /** Used to indicate status. */
   protected JLabel             statusLabel;
   /** Frame containing everything. */
   protected JFrame             frame;
   /** Path created with. */
   protected String             path;
   
   
   public TreeTableExample2(String path)
   {
      this.path = path;
      ttCount++;
      
      frame = createFrame();
      
      Container      cPane = frame.getContentPane();
      JMenuBar       mb = createMenuBar();
      
      model = createModel(path);
      treeTable = createTreeTable();
      statusLabel = createStatusLabel();
      cPane.add(new JScrollPane(treeTable));
      cPane.add(statusLabel, BorderLayout.SOUTH);
      
      reloadRow = -1;
      frame.setJMenuBar(mb);
      frame.pack();
      frame.setVisible(true);
      SwingUtilities.invokeLater(new Runnable()
      {
         public void run()
         {
            reload(model.getRoot());
         }
      });
   }
   
   /**
    * Creates and return a JLabel that is used to indicate the status
    * of loading.
    */
   protected JLabel createStatusLabel()
   {
      JLabel         retLabel = new JLabel(" ");
      
      retLabel.setHorizontalAlignment(JLabel.RIGHT);
      retLabel.setBorder(new BevelBorder(BevelBorder.LOWERED));
      return retLabel;
   }
   
   /**
    * Creates and returns the instanceof JTreeTable that will be used.
    * This also creates, but does not start, the Timer that is used to
    * update the display as files are loaded.
    */
   protected JTreeTable createTreeTable()
   {
      JTreeTable       treeTable = new JTreeTable(model);
      
      treeTable.getColumnModel().getColumn(1).setCellRenderer
      (new IndicatorRenderer());
      
      Reloader rl = new Reloader();
      
      timer = new Timer(700, rl);
      timer.setRepeats(true);
      treeTable.getTree().addTreeExpansionListener(rl);
      return treeTable;
   }
   
   /**
    * Creates the FileSystemModel2 that will be used.
    */
   protected FileSystemModel2 createModel(String path)
   {
      return new FileSystemModel2(path);
   }
   
   /**
    * Creates the JFrame that will contain everything.
    */
   protected JFrame createFrame()
   {
      JFrame       retFrame = new JFrame("TreeTable II");
      
      retFrame.addWindowListener(new WindowAdapter()
      {
         public void windowClosing(WindowEvent we)
         {
            if (--ttCount == 0)
            {
               System.exit(0);
            }
         }
      });
      return retFrame;
   }
   
   /**
    * Creates a menu bar.
    */
   protected JMenuBar createMenuBar()
   {
      JMenu            fileMenu = new JMenu("File");
      JMenuItem        menuItem;
      
      menuItem = new JMenuItem("Open");
      menuItem.addActionListener(new ActionListener()
      {
         public void actionPerformed(ActionEvent ae)
         {
            JFileChooser      fc = new JFileChooser(path);
            
            fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
            
            int               result = fc.showOpenDialog(frame);
            
            if (result == JFileChooser.APPROVE_OPTION)
            {
               String      newPath = fc.getSelectedFile().getPath();
               
               new TreeTableExample2(newPath);
            }
         }
      });
      fileMenu.add(menuItem);
      fileMenu.addSeparator();
      
      menuItem = new JMenuItem("Reload");
      menuItem.addActionListener(new ActionListener()
      {
         public void actionPerformed(ActionEvent ae)
         {
            TreePath         path = treeTable.getTree().getSelectionPath();
            
            if (path != null)
            {
               model.stopLoading();
               reload(path.getLastPathComponent());
            }
         }
      });
      fileMenu.add(menuItem);
      
      menuItem = new JMenuItem("Stop");
      menuItem.addActionListener(new ActionListener()
      {
         public void actionPerformed(ActionEvent ae)
         {
            model.stopLoading();
         }
      });
      fileMenu.add(menuItem);
      
      fileMenu.addSeparator();
      
      menuItem = new JMenuItem("Exit");
      menuItem.addActionListener(new ActionListener()
      {
         public void actionPerformed(ActionEvent ae)
         {
            System.exit(0);
         }
      });
      fileMenu.add(menuItem);
      
      
      // Create a menu bar
      JMenuBar        menuBar = new JMenuBar();
      
      menuBar.add(fileMenu);
      
      // Menu for the look and feels (lafs).
      UIManager.LookAndFeelInfo[]        lafs = UIManager.
      getInstalledLookAndFeels();
      ButtonGroup                        lafGroup = new ButtonGroup();
      
      JMenu          optionsMenu = new JMenu("Options");
      
      menuBar.add(optionsMenu);
      
      for(int i = 0; i < lafs.length; i++)
      {
         JRadioButtonMenuItem rb = new JRadioButtonMenuItem(lafs[i].
         getName());
         optionsMenu.add(rb);
         rb.setSelected(UIManager.getLookAndFeel().getName().equals
         (lafs[i].getName()));
         rb.putClientProperty("UIKey", lafs[i]);
         rb.addItemListener(new ItemListener()
         {
            public void itemStateChanged(ItemEvent ae)
            {
               JRadioButtonMenuItem rb2 = (JRadioButtonMenuItem)ae.
               getSource();
               if(rb2.isSelected())
               {
                  UIManager.LookAndFeelInfo       info =
                  (UIManager.LookAndFeelInfo)
                  rb2.getClientProperty("UIKey");
                  try
                  {
                     UIManager.setLookAndFeel(info.getClassName());
                     SwingUtilities.updateComponentTreeUI(frame);
                  }
                  catch (Exception e)
                  {
                     System.err.println("unable to set UI " +
                     e.getMessage());
                  }
               }
            }
         });
         lafGroup.add(rb);
      }
      return menuBar;
   }
   
   /**
    * Invoked to reload the children of a particular node. This will
    * also restart the timer.
    */
   protected void reload(Object node)
   {
      model.reloadChildren(node);
      if (!timer.isRunning())
      {
         timer.start();
      }
   }
   
   /**
    * Updates the status label based on reloadRow.
    */
   protected void updateStatusLabel()
   {
      if (reloadPath != null)
      {
         statusLabel.setText("Reloading: " + model.getPath
         (reloadPath.getLastPathComponent()));
         if ((reloadCounter % 4) < 2)
         {
            statusLabel.setForeground(Color.red);
         }
         else
         {
            statusLabel.setForeground(Color.blue);
         }
      }
      else if (!model.isReloading())
      {
         statusLabel.setText("Total Size: " + NumberFormat.getInstance().
         format(model.getTotalSize(model.getRoot())));
         statusLabel.setForeground(Color.black);
      }
   }
   
   
   /**
    * Reloader is the ActionListener used in the Timer. In response to
    * the timer updating it will reset the reloadRow/reloadPath and
    * generate the necessary event so that the display will update. It
    * also implements the TreeExpansionListener so that if the tree is
    * altered while loading the reloadRow is updated accordingly.
    */
   class Reloader implements ActionListener, TreeExpansionListener
   {
      public void actionPerformed(ActionEvent ae)
      {
         if (!model.isReloading())
         {
            // No longer loading.
            timer.stop();
            if (reloadRow != -1)
            {
               generateChangeEvent(reloadRow);
            }
            reloadRow = -1;
            reloadPath = null;
         }
         else
         {
            // Still loading, see if paths changed.
            TreePath       newPath = model.getPathLoading();
            
            if (newPath == null)
            {
               // Hmm... Will usually indicate the reload thread
               // completed between time we asked if reloading.
               if (reloadRow != -1)
               {
                  generateChangeEvent(reloadRow);
               }
               reloadRow = -1;
               reloadPath = null;
            }
            else
            {
               // Ok, valid path, see if matches last path.
               int        newRow = treeTable.getTree().getRowForPath
               (newPath);
               
               if (newPath.equals(reloadPath))
               {
                  reloadCounter = (reloadCounter + 1) % 8;
                  if (newRow != reloadRow)
                  {
                     int             lastRow = reloadRow;
                     
                     reloadRow = newRow;
                     generateChangeEvent(lastRow);
                  }
                  generateChangeEvent(reloadRow);
               }
               else
               {
                  int          lastRow = reloadRow;
                  
                  reloadCounter = 0;
                  reloadRow = newRow;
                  reloadPath = newPath;
                  if (lastRow != reloadRow)
                  {
                     generateChangeEvent(lastRow);
                  }
                  generateChangeEvent(reloadRow);
               }
            }
         }
         updateStatusLabel();
      }
      
      /**
       * Generates and update event for the specified row. FileSystemModel2
       * could do this, but it would not know when the row has changed
       * as a result of expanding/collapsing nodes in the tree.
       */
      protected void generateChangeEvent(int row)
      {
         if (row != -1)
         {
            AbstractTableModel     tModel = (AbstractTableModel)treeTable.
            getModel();
            
            tModel.fireTableChanged(new TableModelEvent
            (tModel, row, row, 1));
         }
      }
      
      //
      // TreeExpansionListener
      //
      
      /**
       * Invoked when the tree has expanded.
       */
      public void treeExpanded(TreeExpansionEvent te)
      {
         updateRow();
      }
      
      /**
       * Invoked when the tree has collapsed.
       */
      public void treeCollapsed(TreeExpansionEvent te)
      {
         updateRow();
      }
      
      /**
       * Updates the reloadRow and path, this does not genernate a
       * change event.
       */
      protected void updateRow()
      {
         reloadPath = model.getPathLoading();
         
         if (reloadPath != null)
         {
            reloadRow = treeTable.getTree().getRowForPath(reloadPath);
         }
      }
   }
   
   
   /**
    * A renderer that will give an indicator when a cell is being reloaded.
    */
   class IndicatorRenderer extends DefaultTableCellRenderer
   {
      /** Makes sure the number of displayed in an internationalized
       * manner. */
      protected NumberFormat       formatter;
      /** Row that is currently being painted. */
      protected int                lastRow;
      
      
      IndicatorRenderer()
      {
         setHorizontalAlignment(JLabel.RIGHT);
         formatter = NumberFormat.getInstance();
      }
      
      /**
       * Invoked as part of DefaultTableCellRenderers implemention. Sets
       * the text of the label.
       */
      public void setValue(Object value)
      {
         setText((value == null) ? "---" : formatter.format(value));
      }
      
      /**
       * Returns this.
       */
      public Component getTableCellRendererComponent(JTable table,
      Object value, boolean isSelected, boolean hasFocus,
      int row, int column)
      {
         super.getTableCellRendererComponent(table, value, isSelected,
         hasFocus, row, column);
         lastRow = row;
         return this;
      }
      
      /**
       * If the row being painted is also being reloaded this will draw
       * a little indicator.
       */
      public void paint(Graphics g)
      {
         if (lastRow == reloadRow)
         {
            int       width = getWidth();
            int       height = getHeight();
            
            g.setColor(getBackground());
            g.fillRect(0, 0, width, height);
            g.setColor(getForeground());
            
            int       diameter = Math.min(width, height);
            
            if (reloadCounter < 5)
            {
               g.fillArc((width - diameter) / 2, (height - diameter) / 2,
               diameter, diameter, 90, -(reloadCounter * 90));
            }
            else
            {
               g.fillArc((width - diameter) / 2, (height - diameter) / 2,
               diameter, diameter, 90,
               (4 - reloadCounter % 4) * 90);
            }
         }
         else
         {
            super.paint(g);
         }
      }
   }
   
   
   public static void main(String[] args)
   {
      if (args.length > 0)
      {
         for (int counter = args.length - 1; counter >= 0; counter--)
         {
            new TreeTableExample2(args[counter]);
         }
      }
      else
      {
         String            path;
         
         try
         {
            path = System.getProperty("user.home");
            if (path != null)
            {
               new TreeTableExample2(path);
            }
         }
         catch (SecurityException se)
         {
            path = null;
         }
         if (path == null)
         {
            System.out.println("Could not determine home directory");
         }
      }
   }
}
