import java.util.*;
import java.io.*;

/** myoPyx<p>
 * A PYX document parser.
 * <P>
 * Still to do:
 * <ul>
 *   <li>Add support for listening for multiple tags with one
 *   addListener() call.</li>
 * </ul>
 * @author Sean Russell &lt;ser@germane-software.com&gt;
 * @version 1.1
 * @date *2002-080
 * @copyright Copyright © 2002 Sean Elliott Russell
 */
public class myoPyx {
	private HashMap _listeners;
	private ArrayList _pi_listeners;

	public myoPyx() {
		_listeners = new HashMap();
		_pi_listeners = new ArrayList();
	}

	/** Parses a Reader stream, generating events that are sent to
	 * myoPyx.Listeners that are added with addListener() or
	 * addPIListener().<p>
	 * <pre>myoPyx p = new myoPyx();
	 * p.addListener( my_listener );
	 * try {
	 *   InputStreamReader input = new InputStreamReader(
	 *     new FileInputStream( "my.pyx" );
	 *   p.parse( input );
	 * } catch (Exception e) {
	 *   e.printStackTrace();
	 * }</pre>
	 * @throws IOException if there is a problem reading from the
	 * reader
	 * @throws Exception if there is a parsing exception, such as
	 * bad markup or bad document formatting.  The error message
	 * will contnain the line number where the error occurred.
	 */
	public void parse( Reader s ) throws IOException, Exception {
		BufferedReader reader;
		if (s instanceof BufferedReader)
			reader = (BufferedReader)s;
		else
			reader = new BufferedReader( s );
		String line = null;
		Stack tags = new Stack();
		String current_tag = null;
		ArrayList listeners = null;
		ArrayList all_listeners = (ArrayList)_listeners.get( " " );
		String text="";
		int line_no = 0;
		HashMap attributes = null;
		while ((line = reader.readLine()) != null) {
			line_no++;
			if (line.length()>1) text = line.substring(1);
			if (line.trim().length() == 0) continue;
			
			// Get the line.
			// If it starts with anything but A and attributes exists
			char first_char = line.charAt(0);
			if (first_char != 'A' && attributes != null) {
				if(current_tag != null) {
					if (all_listeners != null)
						for (Iterator i=all_listeners.iterator();
								i.hasNext();
								((Listener)i.next()).startElement( current_tag, attributes ) );
					if (listeners != null)
						for (Iterator i=listeners.iterator();
								i.hasNext();
								((Listener)i.next()).startElement( current_tag, attributes ) );
				}
				attributes = null;
			}
			
			switch(line.charAt(0)) {		
				case '(': // Tag start
					attributes = new HashMap();
					if (current_tag != null) tags.push( current_tag );
					current_tag = text;
					listeners = (ArrayList)_listeners.get( current_tag );
					break;
				case ')': // Tag end
					if (!text.equals( current_tag ))
						throw new Exception( "Parse error at line ["+line_no+"]\n"+
								"Expected end tag '"+current_tag+"' but got end tag '"+text+"'\n"+
								"Line: '"+line+"'");
					if (all_listeners != null)
						for (Iterator i=all_listeners.iterator();
								i.hasNext();
								((Listener)i.next()).endElement( current_tag ) );
					if (listeners != null)
						for (Iterator i=listeners.iterator();
								i.hasNext();
								((Listener)i.next()).endElement( current_tag ) );
					listeners = null;
					current_tag = null;
					if (tags.size() > 0) {
						current_tag = (String)tags.pop();
						listeners = (ArrayList)_listeners.get( current_tag );
					}
					break;
				case 'A': // Attribute
					int ind = text.indexOf(" ");
					String attr_name = text.substring(0, ind);
					String attr_value = text.substring(ind+1 );
					attributes.put( attr_name, attr_value );
					break;
				case '-': // Text
					if (all_listeners != null)
						for (Iterator i=all_listeners.iterator();
								i.hasNext();
								((Listener)i.next()).text( text ) );
					if (listeners != null)
						for (Iterator i=listeners.iterator();
								i.hasNext();
								((Listener)i.next()).text( text ) );
					break;
				case '?': // Processing instruction
					for (Iterator i=_pi_listeners.iterator(); 
							i.hasNext();
							((Observer)i.next()).update( null, text ));
					break;
				default: // PARSE ERROR
					throw new Exception( "Parse exception at line ["+line_no+"]\n"+
						"Bad markup\nLine: '"+line+"'");
			}
		}
	}

	/** Add a listener for parsing events.
	 * <pre>myoPyx.Listener my_listener = new myoPyx.Listener() {
	 *   public void startElement( String name, HashMap attrs ) {
	 *     System.out.println("MyTag has attributes:");
	 *     for (Iterator i=attrs.keys(); i.hasNext(); ) {
	 *       Object key = i.next();
	 *       System.out.println("\t"+key.toString()+" -> "+
	 *         attrs.get(key).toString());
	 *     }
	 *   }
	 *   public void endElement( String name ) { }
	 *   public void text( String content ) { }
	 * };
	 * myoPyx.Listener all = new myoPyx.Listener() {
	 *   public void startElement( String name, HashMap attrs ) {
	 *     System.out.println("Got tag "+name);
	 *   }
	 *   public void endElement( String name ) { }
	 *   public void text( String content ) { }
	 * };
	 * pyx.addListener( null, all );
	 * pyx.addListener( "MyTag", my_listener );</pre>
	 * Listeners for all events will be notified before listeners of
	 * specific events.  After this, Listeners are notified in the
	 * order they are added.
	 * @param tag_name if null, the listener will be notified of all
	 * events.  If not null, the listener will be notified of only
	 * events pertaining to elements matching the tag_name.
	 */
	public void addListener( String tag_name, myoPyx.Listener listener ) {
		if (tag_name == null) tag_name = " ";
		ArrayList v  = (ArrayList)_listeners.get( tag_name );
		if (v == null) {
			v = new ArrayList();
			_listeners.put( tag_name, v );
		}
		if (!v.contains( listener )) v.add( listener );
	}

	/** Attaches a listener of processing instruction (PI) events.
	 * Uses the Observer intefrace; The first argument (Observable) passed 
	 * to Observer.notify will always be null; the second (Object) will be the
	 * contents of the processing instruction, in raw String form.
	 */
	public void addPIListener( Observer listener ) {
		if (!_pi_listeners.contains( listener )) 
			_pi_listeners.add( listener );
	}

	public void removePIListener( Object listener ) {
		_pi_listeners.remove( listener );
	}
	
	/** Removes a listener.  The tag must match the tag under which the
	 * Listener was added; for example, if a listener is added as both
	 * a "MyTag1" and "MyTag2", then removeListener(null, listener)
	 * will do nothing, and removeListener("MyTag1", listener) will only
	 * remove the listener for events on "MyTag1", not "MyTag2".
	 */
	public void removeListener( String tag_name, Object listener ) {
		ArrayList v = (ArrayList)_listeners.get( tag_name );
		if (v != null) v.remove( listener );
	}

	/** Implement this to create an object that can listen to myoPyx
	 * events.
	 */
	public interface Listener {
		/** Called after the start tag is encountered, after the
		 * attributes have been processed, and before any text, PI's
		 * or other elements are processed.
		 */
		public void startElement( String tag_name, HashMap attributes );
		/** Called when the end tag for an element is encountered.
		 */
		public void endElement( String tag_name );
		/** Called when a text node is encountered.  The text contents are
		 * passed as the argument.
		 */
		public void text( String text );
	}
}
