/*
 * java-access-bridge for GNOME
 * Copyright 2002 Sun Microsystems Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */
import org.GNOME.Bonobo.*;
import org.GNOME.Accessibility.*;
import org.omg.PortableServer.*;
import org.omg.CORBA.*;
import java.io.*;

public class JNav extends EventListenerImpl implements AccessUtil.EventCallback {

	private Accessible current;
	private org.omg.CORBA.Object tts;
	private static Class synthesisDriverHelperClass;
	private TextListener theTextListener;

	static {
		try {
		synthesisDriverHelperClass = 
			Class.forName ("org.GNOME.Speech.SynthesisDriverHelper");
		} catch (Exception ex) {
			System.out.println ("TTS driver not found.");
			System.out.println (ex);
		}
	}
	
	public JNav () {
		super ();
	}

	private void run () {
		if (AccessUtil.requiredJREVersionFound ()) {
			System.out.println ("Starting JNav.");
			Registry registry = AccessUtil.getRegistryObject ();
			org.GNOME.Accessibility.EventListener listener = 
				EventListenerHelper.narrow (this.tie ());
			registry.registerGlobalEventListener (listener, 
							      "focus:");
			EventListener textListener =
				EventListenerHelper.narrow (
					getTextListener ().tie ());
			registry.registerGlobalEventListener (textListener,
							      "object:text-caret-moved");
			registry.registerGlobalEventListener (textListener,
							      "object:text-changed");
			ListenerUtil.KeyListener commandListener = 
				new ListenerUtil.KeyListener (this);
			KeyDefinition[] keys = new KeyDefinition[10];
			keys[0] = new KeyDefinition (0, 0, "Up", 0);
			keys[1] = new KeyDefinition (0, 0, "Down", 0);
			keys[2] = new KeyDefinition (0, 0, "Left", 0);
			keys[3] = new KeyDefinition (0, 0, "Right", 0);
			keys[4] = new KeyDefinition (0, 0, "Home", 0);
			keys[5] = new KeyDefinition (0, 0, "F1", 0);
			keys[6] = new KeyDefinition (0, 0, "F2", 0);
			keys[7] = new KeyDefinition (0, 0, "Page_Up", 0);
			keys[8] = new KeyDefinition (0, 0, "Page_Down", 0);
			keys[9] = new KeyDefinition (0, 0, "Escape", 0);
			AccessUtil.registerKeyListener (commandListener.objRef (),
							keys, 
							KeyMask.SHIFT, 
							false,
							new EventListenerMode (
								true, true, true));
			tts = getSpeechService ();
			message ("J Navigator ready.");
			AccessUtil.getORB().run();
		}
	}

	public static void main(String args[]) {
		new JNav ().run ();
	}

	public void notifyEvent (Event e) {
		if (e.source != current) {
			current = e.source;
			announce (current);
		}
	}

	public boolean notifyDeviceEvent (DeviceEvent e) {
		System.out.println ("Key " +
				    new KeyMask (e.modifiers) + 
				    "-" + 
				    new Character ((char) e.id) + 
			            " (" + e.event_string + ")");
		if (e.event_string != null && e.event_string.length() > 0) {
			if (e.event_string.equals("Up"))
				navigateUp ();
			else if (e.event_string.equals("Down"))
				navigateDown ();
			else if (e.event_string.equals("Left"))
				navigatePrev ();
			else if (e.event_string.equals("Right"))
				navigateNext ();
			else if (e.event_string.equals("Page_Up"))
				navigateTop ();
			else if (e.event_string.equals("F1"))
				help ();
			else if (e.event_string.equals("F2"))
				toggleCharacterEcho ();
			else if (e.event_string.equals("Page_Down"))
				shutup ();
			else if (e.event_string.equals("Home"))
				describeCurrent ();
			else if (e.event_string.equals("Escape"))
				quit ();
		}
		return true;
	}

	private TextListener getTextListener () {
		if (theTextListener == null)
			theTextListener = new TextListener ();
		return theTextListener;
	}

	private void navigateNext () {
		System.out.println ("prev");
		Accessible next = null;
		if (current != null) {
			int index = current.getIndexInParent ();
			if (index >= 0) {
				Accessible parent = current.parent ();
				if (parent != null)
					next = parent.getChildAtIndex (++index);
			}
		}
		if (next != null)
			moveTo (next);
		else
			message ("Can't move to next sibling");
	}

	private void navigatePrev () {
		System.out.println ("prev");
		Accessible prev = null;
		if (current != null) {
			int index = current.getIndexInParent ();
			if (index > 0) {
				Accessible parent = current.parent ();
				if (parent != null)
					prev = parent.getChildAtIndex (--index);
			}
		}
		if (prev != null)
			moveTo (prev);
		else
			message ("Can't move to previous sibling");
	}

	private void navigateUp () {
		System.out.println ("parent");
		Accessible parent = null;
		if (current != null)
			parent = current.parent ();
		if (parent != null) 
			moveTo (parent);
		else
			message ("Can't move to parent");
	}
	
	private void navigateDown () {
		System.out.println ("first child");
		Accessible child = null;
		if (current != null) 
			child = current.getChildAtIndex (0);
		if (child != null) 
			moveTo (child);
		else
			message ("Can't move to first child.");
	}

	private void navigateTop () {
		Accessible parent = current;
		while (current.getRole() != Role.ROLE_FRAME && parent != null) {
			parent = current.parent ();
			current.unref ();
			current = parent;
		}
		announce (current);
	}

	private void describeCurrent () {
		shutup ();
		announce (current);
		if (current != null) {
			String content = "";
			Text text = AccessUtil.getTextInterface (current);
			if (text != null) {
				content = text.getText (0, -1);
				message ("Text content: " + content);
				text.unref ();
			}
			Selection selection = AccessUtil.getSelectionInterface (current);
			if (selection != null) {
				int n = selection.nSelectedChildren ();
				message ("selectable, " + n + " children selected: ");
				for (int i = 0; i < n; ++i) {
					Accessible child = 
						selection.getSelectedChild (i);
					if (child != null) {
						message (child.name ());
						child.unref ();
					}
				}
				selection.unref ();
			}
			Action action = AccessUtil.getActionInterface (current);
			if (action != null) {
				int n = action.nActions ();
				message ("actions:");
				for (int i = 0; i < n; ++i) {
					message (action.getName (i) + ", " +
						 action.getDescription (i));
				}
				action.unref ();
			}
		} else {
			message ("No currently focussed object!");
		}
	}

	private void shutup () {
		try {
			java.lang.reflect.Method shutup = 
				tts.getClass().getMethod("stop", null);
			shutup.invoke (tts, null);
		} catch (Exception ex) {
			System.out.println (ex);
		}
	}

	private void toggleCharacterEcho () {
		boolean charEchoIsOn = ! getTextListener ().getCharacterEcho ();
		getTextListener ().setCharacterEcho (charEchoIsOn);
		message ("character echo " + 
			(charEchoIsOn ? "on" : "off"));
	}

	private void help () {
		message ("Sorry, help isn't implemented yet.");
	}

	private void quit () {
		message ("J Navigator exiting. Goodbye.");
		AccessUtil.getORB ().shutdown (false);
	}

	private void message (String s) {
		System.out.println ("saying \""+s+"\"");
		try {
			Class[] argTypes = {Class.forName ("java.lang.String")};
			java.lang.reflect.Method say = 
				tts.getClass().getMethod("say", argTypes);
			java.lang.Object[] args = {s};
			say.invoke (tts, args);
		} catch (Exception ex) {
			// should we catch exceptions separately?
		}
	}

	private void moveTo (Accessible accessible) {
		current = accessible;
		announce (current);
	}

	private void announce (Accessible accessible) {
		shutup ();
		if (accessible != null) {
			String name = accessible.name ();
			if (name.length() < 1) name = "nameless";
			message (name + " " + 
				 accessible.getRoleName () + ", " +
				 accessible.description ());
		}
	}

	private org.omg.CORBA.Object getSpeechService () {
		if (tts == null) {
			try {
			tts = AccessUtil.activationObjectReference (
				"IDL:GNOME/Speech/SynthesisDriver:1.0");
			} catch (Throwable e) {
				System.err.println ("Can't find speech service!");
			}
			if (synthesisDriverHelperClass != null) {
				try {
				Class[] argTypes = { 
					Class.forName ("org.omg.CORBA.Object")};
				java.lang.reflect.Method method = 
				synthesisDriverHelperClass.getMethod ("narrow",
								      argTypes);
				java.lang.Object[] args = {tts};
				tts = (org.omg.CORBA.Object) method.invoke (null, 
									    args);
				java.lang.reflect.Method init = 
				tts.getClass().getMethod("driverInit", null);
				init.invoke (tts, null);
				} catch (Exception ex) {
					System.out.println ("reflection error "+ex);
				}
			}
		}
		return tts;
	}

        // From EventListener
        public void unImplemented_ () {}
        public void unImplemented2_ () {}
        public void unImplemented3_ () {}
        public void unImplemented4_ () {}
    
	public class TextListener extends EventListenerImpl {

		private int pOffset = -1;
		private org.omg.CORBA.Object pText = null;
		private org.omg.CORBA.IntHolder wordEndHolder = 
				new org.omg.CORBA.IntHolder ();
		private int prevEnd = 0;
		private boolean echoChars = false;

		public void notifyEvent (Event e) {
			Text text = AccessUtil.getTextInterface (e.source);
			if (e.type.equals ("object:text-caret-moved")) {
				notifyCaretEvent (text, e);
			} else if (e.type.equals ("object:text-changed")) {
				notifyChangeEvent (text, e);
			}
		}
		
		public void notifyCaretEvent (Text text, Event e) {
			int prevOffset = previousOffset (e.source, e.detail1);
			if (prevOffset >= 0 &&
			    java.lang.Math.abs(e.detail1 - prevOffset) == 1) {
				message (new Character ((char)
						text.getCharacterAtOffset (
						e.detail1)).toString ());
			} else { 
				String s;
				TEXT_BOUNDARY_TYPE boundaryType;
				org.omg.CORBA.IntHolder yholder1 = 
					new org.omg.CORBA.IntHolder ();
				org.omg.CORBA.IntHolder yholder2 = 
					new org.omg.CORBA.IntHolder ();
				text.getCharacterExtents (e.detail1,
						  new org.omg.CORBA.IntHolder (),
							  yholder1,
						  new org.omg.CORBA.IntHolder (),
						  new org.omg.CORBA.IntHolder (),
							  (short) 0);
				text.getCharacterExtents (prevOffset,
						  new org.omg.CORBA.IntHolder (),
							  yholder2,
						  new org.omg.CORBA.IntHolder (),
						  new org.omg.CORBA.IntHolder (),
							  (short) 0);
				if (yholder1.value != yholder2.value) 
					boundaryType = 
					TEXT_BOUNDARY_TYPE.TEXT_BOUNDARY_LINE_START;
				else
					boundaryType = 
					TEXT_BOUNDARY_TYPE.TEXT_BOUNDARY_WORD_START;
				s = text.getTextAtOffset (e.detail1,
							  boundaryType,
						  new org.omg.CORBA.IntHolder (),
						  new org.omg.CORBA.IntHolder ());
				message (s);
			}
		}

		public void notifyChangeEvent (Text text, Event e) {
			// heuristic: if the change is at the current
			// offset position (only), and is a single 
			// character, then speak only the (one-character) delta
			if (e.detail2 == 1 && 
			    e.detail1 == (text.caretOffset () - 1)) {
				if (echoChars) 
					message (text.getText (e.detail1, 
							       e.detail1 + 
							       e.detail2 + 1));
			// it might be nicer to speak inserted text word-by-word
				else {
					String s = 
						text.getTextAtOffset (e.detail1 - 1,
					 TEXT_BOUNDARY_TYPE.TEXT_BOUNDARY_WORD_END,
					       new org.omg.CORBA.IntHolder (),
								      wordEndHolder);
					if (wordEndHolder.value == prevEnd) 
						message (s);
					prevEnd = wordEndHolder.value;
				}
			} else
			    message (text.getTextAtOffset (e.detail1,
			       TEXT_BOUNDARY_TYPE.TEXT_BOUNDARY_SENTENCE_START,
					       new org.omg.CORBA.IntHolder (),
					       new org.omg.CORBA.IntHolder ()));
		}

		public int previousOffset (Accessible text, int currentOffset) {
			int retval = -1;
			if (text._is_equivalent (pText))
				retval = pOffset;
			pOffset = currentOffset;
			pText = text;
			return retval;
		}
		
		public void setCharacterEcho (boolean echo) {
			echoChars = echo;
		}

		public boolean getCharacterEcho () {
			return echoChars;
		}

	        // From EventListener
	        public void unImplemented_ () {}
	        public void unImplemented2_ () {}
	        public void unImplemented3_ () {}
	        public void unImplemented4_ () {}
	}
}
