// ============================================================================
// File:               $File$
//
// Project:
//
// Purpose:
//
// Author:             Rammi
//
// Copyright Notice:   (c) 1999-2006  Rammi (rammi@caff.de)
//                     This code is in the public domain.
//                     Use at own risk.
//                     No guarantees given.
//
// Latest change:      $Date: 2006-02-01 13:48:49 +0100 (Mi, 01 Feb 2006) $
//
// History:	       $Log$
//=============================================================================

package de.caff.util.debug;

import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
import java.io.FileWriter;
import java.util.*;


/**
 *  A text window frame in which debugging messages are outputted in cooked format.
 *  It has the for filtering the displayed message for different categories.
 *  
 *  @see Debug
 *
 *  @author Rammi
 */
public class FilteringDebugMessageWindow 
  extends JFrame 
  implements CookedMessageDebugListener 
{
  /** The short names of the debug types used here. */
  private static final String[] TYPE_NAMES = {
    Character.toString(TRACE_CHAR),   // "T",
    Character.toString(MESSAGE_CHAR), // "S",
    Character.toString(WARNING_CHAR), // "W",
    Character.toString(ERROR_CHAR),   // "E",
    Character.toString(FATAL_CHAR),   // "F",
    Character.toString(LOG_CHAR),     // "L",
    Character.toString(ASSERT_CHAR)   // "A"
  };

  /** The text area which displays the messages. */
  private JTextArea      _msgArea;
  /** The end psoition in the text area. */
  private int            _endPos;
  /** List of collected messages. */
  private Collection     _msgList = new LinkedList();
  /** Filtering mask for message types. */
  private long           _filterMask = DEBUG_ALL_MASK;
  /** Tree for handling position where debug messages where issued */
  private PositionTree   _positionTree;
  /** The status line. */
  private JLabel         _statusLine;
  /** The number of displayed messages. */
  private int            _nrDisplayed;
  /** The number of messages of a given type. */
  private int[]          _nrMessages = new int[NR_DEBUG_TYPES];
  /** If true append to window, if false append to stringbuffer. */
  private boolean        _appendWindow = true;
  /** Stringbuffer for intermediate appending during refiltering. */
  private StringBuffer   _refilterBuffer = new StringBuffer();

  /**
   *  A debug message.
   */
  private static class Message {
    /** Message mask (only one bit is set). */
    private long    _mask;
    /** Message text. */
    private String  _message;
    /** Message positon. */
    private String  _pos;
    /** Connected node for position for faster access. */
    private Object  _node;

    /**
     *  Create a message.
     *  @param  type  message type
     *  @param  msg   message text
     *  @param  pos   message position
     */
    public Message(int type, String msg, String pos) {
      _mask    = 0x00000001L << type;
      _message = msg;
      _pos     = pos;
    }

    /**
     *  Set the connected position node.
     *  @param  node  connected node
     */
    public void setNode(Object node) {
      _node = node;
    }

    /**
     *  Get the connected position node.
     *  @return connected node
     */
    public Object getNode() {
      return _node;
    }

    /**
     *  Get a string representation.
     *  This creates the displayed message.
     */
    public String toString() {
      return _message + DebugMessageCook.cookedPosition(_pos);
    }

    /**
     *  Is this message's type contained in the type mask?
     *  @param  mask  type mask
     *  @return the answer 
     */
    public boolean isTypeConform(long mask) {
      return (mask & _mask) != 0;
    }
  }

  /**
   *  A check box for filtering types.
   *  Each box is for a special mask.
   */
  private class FilterCheckBox extends JCheckBox implements ItemListener {
    /** Connected mask. */
    private long _mask;

    /** 
     *  Create check box.
     *  @param  text box's text
     *  @param  mask connected mask
     *  @param  ttt  tooltip text or <code>null</code>
     */
    public FilterCheckBox(String text, long mask, String ttt) {
      super(text, true);
      setSelected((_filterMask & mask) != 0);
      _mask = mask;
      addItemListener(this);
      if (ttt != null) {
	setToolTipText(ttt);
      }
    }

    /**
     *  Item state listener method.
     *  Calls 
     *  {@link FilteringDebugMessageWindow#refilterDisplay refilterDisplay)
     *  after changing the filter mask.
     */
    public void itemStateChanged(ItemEvent e) {
      if (e.getStateChange() == ItemEvent.SELECTED) {
	_filterMask |= _mask;
      }
      else {
	_filterMask &= ~_mask;
      }
      refilterDisplay();
    }
  }

  /**
   *  Panel containing all check boxes for message filtering by type.
   */
  private class MessageTypeFilterBoard extends JPanel {
    /**
     *  Constructor.
     */
    public MessageTypeFilterBoard() {
      setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
      
      add(new FilterCheckBox("Display trace messages",
			     TRACE_FLAG,
			     "Toggles the display of trace debug messages."));

      add(new FilterCheckBox("Display standard messages",
			     MESSAGE_FLAG,
			     "Toggles the display of standard debug messages."));
      
      add(new FilterCheckBox("Display warning messages",
			     WARNING_FLAG,
			     "Toggles the display of warning debug messages."));

      add(new FilterCheckBox("Display error messages",
			     ERROR_FLAG,
			     "Toggles the display of error debug messages."));

      add(new FilterCheckBox("Display fatal error messages",
			     FATAL_FLAG,
			     "Toggles the display of fatal error debug messages."));

      add(new FilterCheckBox("Display logging messages",
			     LOG_FLAG,
			     "Toggles the display of logging messages."));

      add(new FilterCheckBox("Display failed assertion messages",
			     ASSERT_FLAG,
			     "Toggles the display of failed assertion messages."));
    }
  }

  /**
   *  Scrollable panel containing the tree of all known debug positions.
   *  Via the tree it's possible to switch on/off certain messages by their
   *  position.
   */
  private class PositionTree extends JPanel {
    /** The tree containing the positions. */
    private JTree _tree;
    /** Hash map for already known positions. The keys are the
     *  positions (in string form), the values are the tree nodes.
     */
    private HashMap _positions = new HashMap(89);

    /**
     *  A node of the tree. All nodes in the tree are of this type.
     */
    private class PositionNode
            extends DefaultMutableTreeNode
    {
      /** Is this node included in the displayed messages? */
      private boolean _included = true;

      /**
       *  Create node.
       *  @param  obj  node's object
       */
      public PositionNode(Object obj) {
	super(obj);
      }

      /**
       *  Create node.
       *  @param  obj  node's object
       *  @param  b    is this a branch node?
       */
      public PositionNode(Object obj, boolean b) {
	super(obj, b);
      }

      /**
       *  Set the included value (internally).
       *  Issues a node change if value is changed.
       *  @param  inc new value
       */
      private void setIncludedDirect(boolean inc) {
	if (inc != _included) {
	  _included = inc;
	  ((DefaultTreeModel)_tree.getModel()).nodeChanged(this);
	}
      }

      /**
       *  Set the included value.
       *  This is set for the all children, too.
       *  @param  inc new value
       */
      public void setIncluded(boolean inc) {
	setIncludedDirect(inc);

	if (!isLeaf()) {
	  // set children
	  for (Enumeration c = children();  c.hasMoreElements(); ) {
	    ((PositionNode)c.nextElement()).setIncluded(inc);
	  }
	}
	if (false) {
	  if (!inc) {
	    // exclude branch to root
	    for (PositionNode p = (PositionNode)getParent();
		 p != null;
		 p = (PositionNode)p.getParent()) {
	      p.setIncludedDirect(false);
	    }
	  }
	  else {
	    PositionNode parent = (PositionNode)getParent();
	    if (parent != null) {
	      parent.checkChildrenInclude();
	    }
	  }
	}
      }

      /**
       *  Is this node (position) included in the display?
       *  @return the truth ;-)
       */
      public boolean isIncluded() {
	return _included;
      }

      /**
       *  Check whether all children are included. If yes, include
       *  this node, too.
       *  Unused at the moment.
       */
      private void checkChildrenInclude() {
	// if this node is not included and all children are
	// included, check inclusion directly
	if (!_included) {
	  for (Enumeration c = children();  c.hasMoreElements(); ) {
	    if (!((PositionNode)c.nextElement()).isIncluded()) {
	      return;
	    }
	  }
	  // all children are included
	  setIncludedDirect(true);
	  PositionNode parent = (PositionNode)getParent();
	  if (parent != null) {
	    parent.checkChildrenInclude();
	  }
	}
      }

      /**
       * Overridden to make clone public.  Returns a shallow copy of this node;
       * the new node has no parent or children and has a reference to the same
       * user object, if any.
       *
       * @return a copy of this node
       */
      public Object clone()
      {
        return new PositionNode(getUserObject(), allowsChildren);
      }
    }

    /**
     *  Constructor.
     */
    public PositionTree() {
      super(new BorderLayout());
      _tree = new JTree(new DefaultTreeModel(new PositionNode(""), true));
      _tree.putClientProperty("JTree.lineStyle", "Angled");    
      _tree.setRootVisible(false);
      _tree.setShowsRootHandles(true);
      _tree.setCellRenderer(new DefaultTreeCellRenderer() {
	private Font _excludedFont;
	private Font _includedFont;

	private void initFonts(JTree tree) {
	  Font font = tree.getFont();
	  _includedFont = font.deriveFont(Font.BOLD);
	  _excludedFont = font.deriveFont(Font.PLAIN);
	}

	public Component getTreeCellRendererComponent(JTree tree,
						      Object value,
						      boolean selected,
						      boolean expanded,
						      boolean leaf,
						      int row,
						      boolean hasFocus) {
	  PositionNode node = (PositionNode)value;
	  JLabel label = (JLabel)super.getTreeCellRendererComponent(tree,
								    value,
								    selected,
								    expanded,
								    leaf,
								    row,
								    hasFocus);
	  label.setFont(tree.getFont().deriveFont(node.isIncluded() ?
						  Font.BOLD   :
						  Font.PLAIN));
	  return label;
	}
      });
      add(new JScrollPane(_tree), "Center");
      
      JPanel buttonPanel = new JPanel(new GridLayout(0, 2));
      JButton b = new JButton("Include");
      b.addActionListener(new ActionListener() {
	public void actionPerformed(ActionEvent e) {
	  includeSelection();
	}
      });
      b.setToolTipText("Includes the currently selected nodes and branches into the displayed messages");
      buttonPanel.add(b);

      b = new JButton("Exclude");
      b.addActionListener(new ActionListener() {
	public void actionPerformed(ActionEvent e) {
	  excludeSelection();
	}
      });
      b.setToolTipText("Excludes the currently selected nodes and branches from the displayed messages");
      buttonPanel.add(b);

      add(buttonPanel, "South");
    }

    /**
     *  Reset the tree.
     */
    public void clear() {
      _tree.setModel(new DefaultTreeModel(new PositionNode(""), true));
      _positions.clear();
    }

    /**
     *  Append a position string to the tree.
     *  @param  pos  position string (as issued by <code>Throwable.printStackTrace()</code>)
     *  @return node object for this position
     */
    public Object appendPosition(String pos) {
      Object node = _positions.get(pos);
      if (node == null) {
	Object[] elems;
	if ("???".equals(pos)) {
	  elems = new Object[] { pos };
	}
	else {
	  StringTokenizer st = new StringTokenizer(pos, "()");
	  
	  String className = st.nextToken();
	  String lineNr    = st.nextToken();
	  StringTokenizer pst = new StringTokenizer(className, ".");
	  ArrayList tokens = new ArrayList(32);	// should be enough
	  
	  while (pst.hasMoreTokens()) {
	    String tok = pst.nextToken();
	    int dollar = tok.indexOf('$');
	    if (dollar > 0) {
	      tokens.add(tok.substring(0, dollar));
	      tokens.add(tok.substring(dollar));			 
	    }
	    else {
	      tokens.add(tok);
	    }
	  }
	  
	  int    index     = lineNr.indexOf(':')+1;
	  if (index <= 0) {
	    // possibly: "Foo.java, Compiled Code"
	    index = lineNr.indexOf(',')+1;
	    if (index <= 0) {
	      tokens.add(lineNr);
	    }
	    else {
	      tokens.add(lineNr.substring(index).trim());
	    }
	  }
	  else {
	    lineNr = lineNr.substring(index);
	    
	    try {
	      tokens.add(Integer.decode(lineNr));
	    } catch (NumberFormatException x) {
	      tokens.add(lineNr);
	    }
	  }
	  elems = tokens.toArray();
	}
	node = appendPath(elems);
	_positions.put(pos, node);
      }
      return node;
    }

    /**
     *  Append a path of objects to the tree. 
     *  @param  elements  elements of tree path
     *  @return node object for this position
     */
    private Object appendPath(Object[] elements) {
      TreeModel model = _tree.getModel();
      PositionNode  node = (PositionNode)model.getRoot();
      
      return append(node, elements, 0);
    }

    /**
     *  Append a path of objects to a tree node.
     *  @param  node   tree node 
     *  @param  elems  elements of tree path
     *  @param  index  index in elems showing which element to append
     *  @return final node object for the path
     */
    private Object append(PositionNode node, Object[] elems, int index) {
      int insertPos;
      for (insertPos = 0;   insertPos < node.getChildCount();  ++insertPos) {
	PositionNode sub = (PositionNode)node.getChildAt(insertPos);
	Object obj = sub.getUserObject();
	int comp;
	if (obj.getClass() == elems[index].getClass()) {
	  comp = ((Comparable)obj).compareTo(elems[index]);
	}
	else {
	  comp = obj.toString().compareTo(elems[index].toString());
	}
	if (comp == 0) {
	  // found
	  if (++index == elems.length) {
	    return sub;
	  }
	  else {
	    return append(sub, elems, index);
	  }
	}
	else if (comp > 0) {
	  break;
	}
      }
      // insert new node(s)
      DefaultTreeModel model = (DefaultTreeModel)_tree.getModel();
      PositionNode sub = null;
      while (index < elems.length) {
	sub = new PositionNode(elems[index], 
			       index < elems.length-1);
	sub.setIncluded(node.isIncluded());
	model.insertNodeInto(sub, node, insertPos);
	node = sub;
	insertPos = 0;
	++index;
      }
      
      return sub;
    }

    /**
     *  Get a node for a path.
     *  @param path tree path
     *  @return corresponding node
     */
    private PositionNode nodeForPath(TreePath path) {
      return (PositionNode)path.getLastPathComponent();
    }

    /**
     *  Include the current selection (making it displayed).
     */
    private void includeSelection() {
      TreePath[] paths = _tree.getSelectionPaths();
      for (int p = 0;   p < paths.length;  ++p) {
	PositionNode node = nodeForPath(paths[p]);
	node.setIncluded(true);
      }
      refilterDisplay();
    }

    /**
     *  Exclude the current selection (making it removed from display).
     */
    private void excludeSelection() {
      TreePath[] paths = _tree.getSelectionPaths();
      for (int p = 0;   p < paths.length;  ++p) {
	PositionNode node = nodeForPath(paths[p]);
	node.setIncluded(false);
      }
      refilterDisplay();
    }

    /**
     *  Is this message included?.
     *  @param   msg the message in question
     *  @return  the answer
     */
    public boolean allows(Message msg) {
      return ((PositionNode)msg.getNode()).isIncluded();
    }
  }
  
  /**
   *  Constructor.
   */
  public FilteringDebugMessageWindow() {
    super("Debug Messages");

    setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);

    Border tabBorder = BorderFactory.createLoweredBevelBorder();

    _msgArea = new JTextArea(25, 100);
    _msgArea.setFont(new Font("Monospaced", Font.PLAIN, 12));
    _msgArea.setEditable(false);
    JSplitPane split = new JSplitPane();
    getContentPane().add(split);
    split.setLeftComponent(new JScrollPane(_msgArea));

    _statusLine = new JLabel();
    getContentPane().add(BorderLayout.SOUTH, _statusLine);
    _statusLine.setBorder(BorderFactory.createEtchedBorder());

    JPanel panel = new JPanel(new BorderLayout());
    JTabbedPane tabbed = new JTabbedPane();
    tabbed.setPreferredSize(new Dimension(300, 400));
    panel.add(tabbed);
    split.setRightComponent(panel);

    JComponent comp = new DebugLevelSwitchBoard();
    comp.setBorder(tabBorder);
    tabbed.addTab("Global Settings", 
		  null, 
		  comp, 
		  "Allows to change global debug settings");

    comp = new MemoryUsagePanel();
    comp.setBorder(BorderFactory.createCompoundBorder(tabBorder,
						      BorderFactory.
						      createEmptyBorder(3, 3, 
									3, 3)));
    tabbed.addTab("Memory",
		  null,
		  comp,
		  "Memory Usage and Garbage Collection");

    comp = new MessageTypeFilterBoard();
    comp.setBorder(tabBorder);
    tabbed.addTab("Filter by Type", 
		  null,
		  comp,
		  "Filtering of received and future messages by message type");

    comp = _positionTree = new PositionTree();
    comp.setBorder(tabBorder);
    tabbed.addTab("Filter by Position", 
		  null,
		  comp,
		  "Filtering of received and future messages by file position");

    JPanel allButtons  = new JPanel(new BorderLayout());
    allButtons.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
    panel.add("South", allButtons);

    JButton b = new JButton("Print System Properties");
    allButtons.add("North", b);
    b.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
	printProperties();
      }
    });
    b.setToolTipText("Print the system properties in the text window");
    
    JPanel buttonPanel = new JPanel(new GridLayout(1, 0));
    allButtons.add("South", buttonPanel);

    b = new JButton("Clear");
    buttonPanel.add(b);
    b.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
	clear();
      }
    });
    b.setToolTipText("Clears the text window and the message storage");

    b = new JButton("Save...");
    buttonPanel.add(b);
    b.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
	save();
      }
    });
    b.setToolTipText("Allows to save the content of the text window");

    addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
	Debug.removeCookedMessageDebugListener(FilteringDebugMessageWindow.this);
	dispose();
      }
    });
    Debug.addCookedMessageDebugListener(this);

    pack();
    setVisible(true);
  }

  /**
   *  Receive a message and append it to the text window.
   *  @param  msgType  type of message
   *  @param  message  new message
   *  @param  pos      position
   */
  public void receiveCookedMessage(int msgType, String message, String pos) {
    Message msg = addToMessageList(msgType, message, pos);
    possiblyDisplayMessage(msg);
    updateStatus();
  }


  /**
   *  Add a message to the internal list.
   *  @param  msgType  type of message
   *  @param  message  message text
   *  @param  pos      message position
   *  @return the added message
   */
  private Message addToMessageList(int msgType, String message, String pos) {
    _nrMessages[msgType]++;
    Message msg = new Message(msgType, message, pos);
    _msgList.add(msg);
    Object node = _positionTree.appendPosition(pos);
    msg.setNode(node);
    return msg;
  }

  /**
   *  Add a message to the message area if it is not excluded by one of the filters.
   *  @param msg  the message
   */
  private void possiblyDisplayMessage(final Message msg) {
    if (msg.isTypeConform(_filterMask)  &&  _positionTree.allows(msg)) {
      if (SwingUtilities.isEventDispatchThread()) {
	showMessage(msg);
      } 
      else {
	SwingUtilities.invokeLater(new Runnable() {
	  public void run() {
	    showMessage(msg);
	  }
        });
      }
    }
  }

  /**
   *  Really add a message to the message area.
   *  @param msg  the message
   */
  private void showMessage(Message msg) {
    String message = msg.toString();
    append(message);
  }

  /**
   *  Append a text to the message area.
   *  @param  text  text to append
   */
  private void append(String text) {
    ++_nrDisplayed;
    if (_appendWindow) {
      _msgArea.append(text);
      _endPos += text.length();
      _msgArea.setCaretPosition(_endPos);
    }
    else {
      _refilterBuffer.append(text);
    }
  }

  /**
   *  Clear everything.
   */
  private void clear() {
    _msgList.clear();
    Arrays.fill(_nrMessages, 0);
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
	if (false) {
	  _positionTree.clear();
	}
	clearTextWindow();
      }
    });
  }

  /**
   *  Clear text window.
   */
  private void clearTextWindow() {
    _msgArea.setText("");
    _nrDisplayed = 0;
    _endPos = 0;
    updateStatus();
  }

  /**
   *  Update the status line.
   */
  private void updateStatus()
  {
    StringBuffer buf = new StringBuffer();
    buf.
      append(" Shown messages: ").
      append(_nrDisplayed).
      append('/').
      append(_msgList.size()).
      append(" (");

    for (int m = 0;  m < TYPE_NAMES.length;  ++m) {
      if (m != 0) {
	buf.append('+');
      }
      buf.append(_nrMessages[m]).append(TYPE_NAMES[m]);
    }
    buf.append(')');
    _statusLine.setText(buf.toString());
  }

  /**
   *  Save text window content.
   */
  private void save() {
    JFileChooser chooser = new JFileChooser();

    if (chooser.showDialog(this, "Sichern") == JFileChooser.APPROVE_OPTION) {
      File file = chooser.getSelectedFile();
      try {
	      FileWriter out = new FileWriter(file);
	      out.write(_msgArea.getText());
	      // write possibly useful additional information 
	      out.write("\n=== Memory Usage Information ===\n");
	      System.gc();
        Runtime rt = Runtime.getRuntime();
        long freeMem  = rt.freeMemory();
        long totalMem = rt.totalMemory();
        long usedMem  = totalMem - freeMem;
	      out.write("\t used JVM memory: "+usedMem+" Bytes\n");
	      out.write("\t free JVM memory: "+freeMem+" Bytes\n");
	      out.write("\ttotal JVM memory: "+totalMem+" Bytes\n");
	      out.write("\n=== System Information ===\n");
	      for (Iterator it = getSystemPropertyList().iterator();  it.hasNext();   ) {
          out.write("\t"+it.next().toString()+"\n");
        }
        out.write("=== EOF ===\n");
        out.close();
        
	      Debug.message("Debugmeldungen gesichert in \"%1\"", file);
      } catch (Exception x) {
	      Debug.error("Sichern von \"%1\" gescheitert.\nGrund:\n%2", 
		    file, x.getMessage());
      }
    }
  }

  /**
   *  Clear the text window and display all messages which are not excluded by
   *  one of the filters.
   */
  private synchronized void refilterDisplay() {
    try {
      setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
      clearTextWindow();
      _refilterBuffer.setLength(0);
      _appendWindow = false;
      for (Iterator it = _msgList.iterator();   it.hasNext();  ) {
	possiblyDisplayMessage((Message)it.next());
      }
      _appendWindow = true;
      append(_refilterBuffer.toString());
      --_nrDisplayed;		// because the last append was 1 too much
    } finally {
      setCursor(Cursor.getDefaultCursor());
      updateStatus();
    }
  }

  /**
   *  Add the system properties to the debug window.
   */
  private void printProperties() {
    append("SYSTEM PROPERTIES:\n");
    for (Iterator it = getSystemPropertyList().iterator();  it.hasNext();   ) {
      append("\t"+it.next().toString()+"\n");
    }
  }
  
  /**
   *  Get the system properties as a sorted list of Strings in the form <tt>x = y</tt>.
   *  @return  list with system properties
   */
  public static Collection getSystemPropertyList()
  {
    ArrayList   array = new ArrayList();
    for (Enumeration p = System.getProperties().propertyNames();  p.hasMoreElements();  ) {
      String prop = (String)p.nextElement();
      try {
	      array.add(prop+" = \""+System.getProperty(prop)+"\"");
      } catch (SecurityException x) {
	      array.add(prop+" not available due to security restrictions");
      }
    }
    Collections.sort(array);
    
    return array;
  }
}




