/*
 * Copyright 2005 Sun Microsystems, Inc. All  Rights Reserved.
 *  
 * Redistribution and use in source and binary forms, with or 
 * without modification, are permitted provided that the following 
 * conditions are met:
 * 
 * -Redistributions of source code must retain the above copyright  
 *  notice, this list of conditions and the following disclaimer.
 * 
 * -Redistribution in binary form must reproduce the above copyright 
 *  notice, this list of conditions and the following disclaimer in 
 *  the documentation and/or other materials provided with the 
 *  distribution.
 *  
 * Neither the name of Sun Microsystems, Inc. or the names of 
 * contributors may be used to endorse or promote products derived 
 * from this software without specific prior written permission.
 * 
 * This software is provided "AS IS," without a warranty of any 
 * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND 
 * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY 
 * EXCLUDED. SUN AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY 
 * DAMAGES OR LIABILITIES  SUFFERED BY LICENSEE AS A RESULT OF OR 
 * RELATING TO USE, MODIFICATION OR DISTRIBUTION OF THE SOFTWARE OR 
 * ITS DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE 
 * FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, 
 * SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER 
 * CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF 
 * THE USE OF OR INABILITY TO USE SOFTWARE, EVEN IF SUN HAS BEEN 
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 *  
 * You acknowledge that Software is not designed, licensed or 
 * intended for use in the design, construction, operation or 
 * maintenance of any nuclear facility. 
 */

package com.sun.demos.cityim.internal;

import java.awt.AWTEvent;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.InputEvent;
import java.awt.event.InputMethodEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.font.TextAttribute;
import java.awt.font.TextHitInfo;
import java.awt.im.InputMethodHighlight;
import java.awt.im.spi.InputMethod;
import java.awt.im.spi.InputMethodContext;
import java.io.InputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.text.AttributedString;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Locale;
import java.util.Properties;
import java.util.Vector;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.PrivilegedExceptionAction;
import java.security.PrivilegedActionException;
import java.text.MessageFormat;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;


public class CityInputMethod implements InputMethod {

    private static Locale YOMI = new Locale("ja", "JP", "YOMI");
    private static Locale[] SUPPORTED_LOCALES = {
        Locale.ENGLISH, Locale.GERMAN,
        Locale.JAPANESE, YOMI,
        Locale.SIMPLIFIED_CHINESE, Locale.TRADITIONAL_CHINESE
    };
    private static Locale[] LOOKUP_LOCALES = {
        Locale.ENGLISH, Locale.GERMAN,
        Locale.JAPANESE, YOMI,
        Locale.CHINESE,
        Locale.SIMPLIFIED_CHINESE, Locale.TRADITIONAL_CHINESE
    };

    // lookup tables - shared by all instances of this input method    
    private static Properties cityNames;
    private static Properties cityAliases;
    private static Properties cityLanguages;
    private static Properties templates;

    // windows - shared by all instances of this input method
    private static Window statusWindow;
    // current or last statusWindow owner instance
    private static CityInputMethod statusWindowOwner;
    // true if Solaris style; false if PC style
    private static boolean attachedStatusWindow = false;
    // remember the statusWindow location per instance
    private Rectangle clientWindowLocation;
    // status window location in PC style
    private static Point globalStatusWindowLocation;
    // keep live city input method instances (synchronized using statusWindow)
    private static HashSet cityInputMethodInstances = new HashSet(5);
    // non-null if Swing input method JFrame is available
    static Method methodCreateInputMethodJFrame = null;
    static {
	methodCreateInputMethodJFrame = 
	    (Method)AccessController.doPrivileged(new PrivilegedAction() {
	    public Object run() {
		try {
		    Class[] params = new Class[2];
		    params[0] = String.class;
		    params[1] = Boolean.TYPE;
		    return InputMethodContext.class.getMethod("createInputMethodJFrame", params);
		}  catch (NoSuchMethodException e) {
		    return null;
		}
	    }
	});
    }
    // input method window titles
    static final String statusWindowTitle = "City Input Method";
    static final String lookupWindowTitle = "Lookup Window";

    // lookup information - per instance
    private String[] lookupCandidates;
    private Locale[] lookupLocales;
    private int lookupCandidateCount;
    private LookupList lookupList;
    private int lookupSelection;
    
    // per-instance state
    InputMethodContext inputMethodContext;
    private boolean active;
    private boolean disposed;
    private Locale locale;
    private boolean converted;
    private StringBuffer rawText;
    private String convertedText;
    
    private int insertionPoint;
    private String[] rawTextSegs = null;
    private String[] convertedSegs = null;
    private String fmt = null;
    private int fieldPos[][] = null;
    private int segPos[][] = null;
    private int selectedSeg = 0;
    private int numSegs = 0;
    private int committedSeg = -1;
    private int previouslyCommittedCharacterCount = 0;

    public CityInputMethod() throws IOException {
        initializeTables();
        rawText = new StringBuffer();
    }
    
    public void setInputMethodContext(InputMethodContext context) {
        inputMethodContext = context;
	if (statusWindow == null) {
            Window sw;
	    if (methodCreateInputMethodJFrame != null) {
		try {
		    Object[] params = new Object[2];
		    params[0] = statusWindowTitle;
		    params[1] = Boolean.FALSE;
		    sw = (Window)methodCreateInputMethodJFrame.invoke(context, params);
		} catch (Exception e) {
        	    sw = context.createInputMethodWindow(statusWindowTitle, false);
		}
	    } else {
        	sw = context.createInputMethodWindow(statusWindowTitle, false);
	    }
	    JLabel label = new JLabel();
	    label.setOpaque(true);
	    label.setForeground(Color.black);
	    label.setBackground(Color.white);
	    synchronized (this.getClass()) {
		if (statusWindow == null) {
		    statusWindow = sw;
		    statusWindow.addComponentListener(new ComponentListener() {
			public void componentResized(ComponentEvent e) {}
			public void componentMoved(ComponentEvent e) {
			    synchronized (statusWindow) {
				if (!attachedStatusWindow) {
				    Component comp = e.getComponent();
				    if (comp.isVisible()) {
					globalStatusWindowLocation = comp.getLocation();
				    }
				}
			    }
			}
			public void componentShown(ComponentEvent e) {}
			public void componentHidden(ComponentEvent e) {}
		    });
		    label.addMouseListener(new MouseListener() {
			public void mouseClicked(MouseEvent e) {
			    int count = e.getClickCount();
			    if (count >= 2) {
				toggleStatusWindowStyle();
			    }
			}
			public void mousePressed(MouseEvent e) {}
			public void mouseReleased(MouseEvent e) {}
			public void mouseEntered(MouseEvent e) {}
			public void mouseExited(MouseEvent e) {}
		    });
		    if (statusWindow instanceof JFrame) {
			((JFrame)statusWindow).getContentPane().add(label);
		    } else {
			statusWindow.add(label);
		    }
		    statusWindowOwner = this;
		    updateStatusWindow(locale);
		    label.setSize(200, 50);
		    statusWindow.pack();
		}
	    }
	}
	inputMethodContext.enableClientWindowNotification(this, attachedStatusWindow);
	synchronized (statusWindow) {
	    cityInputMethodInstances.add(this);
	}
    }
    
    public boolean setLocale(Locale locale) {
        for (int i = 0; i < SUPPORTED_LOCALES.length; i++) {
            if (locale.equals(SUPPORTED_LOCALES[i])) {
                this.locale = locale;
		if (statusWindow != null) {
		    updateStatusWindow(locale);
		}
                return true;
            }
        }
        return false;
    }
    
    public Locale getLocale() {
        return locale;
    }
    
    void updateStatusWindow(Locale locale) {
	synchronized (statusWindow) {
	    JLabel label;
	    if (statusWindow instanceof JFrame) {
		label = (JLabel) ((JFrame)statusWindow).getContentPane().getComponent(0);
	    } else {
		label = (JLabel) statusWindow.getComponent(0);
	    }
	    String localeName = locale == null ? "null" : locale.getDisplayName();
	    String text = "Current locale: " + localeName;
	    if (!label.getText().equals(text)) {
		label.setText(text);
		statusWindow.pack();
	    }
	    if (attachedStatusWindow) {
		if (clientWindowLocation != null) {
		    statusWindow.setLocation(clientWindowLocation.x,
					     clientWindowLocation.y + clientWindowLocation.height);
		}
	    } else {
		setPCStyleStatusWindow();
	    }
	}
    }

    private void setPCStyleStatusWindow() {
	synchronized (statusWindow) {
	    if (globalStatusWindowLocation == null) {
		Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
		globalStatusWindowLocation = new Point(d.width - statusWindow.getWidth(),
						       d.height - statusWindow.getHeight() - 25);
	    }
	    statusWindow.setLocation(globalStatusWindowLocation.x, globalStatusWindowLocation.y);
	}
    }

    private void setStatusWindowForeground(Color fg) {
	synchronized (statusWindow) {
	    if (statusWindowOwner != this) {
		return;
	    }
	    JLabel label;
	    if (statusWindow instanceof JFrame) {
		label = (JLabel) ((JFrame)statusWindow).getContentPane().getComponent(0);
	    } else {
		label = (JLabel) statusWindow.getComponent(0);
	    }
	    label.setForeground(fg);
	}
    }

    private void toggleStatusWindowStyle() {
	synchronized (statusWindow) {
	    if (attachedStatusWindow) {
		attachedStatusWindow = false;
		setPCStyleStatusWindow();
	    } else {
		attachedStatusWindow = true;
	    }
	    Iterator itr = cityInputMethodInstances.iterator();
	    while (itr.hasNext()) {
		CityInputMethod im = (CityInputMethod)itr.next();
		im.inputMethodContext.enableClientWindowNotification(im, attachedStatusWindow);
	    }
	}
    }	    

    public void setCharacterSubsets(Character.Subset[] subsets) {
        // ignore
    }

    public void reconvert() {
	// not supported yet
	throw new UnsupportedOperationException();
    }

    public void dispatchEvent(AWTEvent event) {
        if (!active && (event instanceof KeyEvent)) {
            System.out.println("CityInputMethod.dispatch called with KeyEvent while not active");
        }
        if (disposed) {
            System.out.println("CityInputMethod.dispatch called after being disposed");
        }
        if (!(event instanceof InputEvent)) {
            System.out.println("CityInputMethod.dispatch called with event that's not an InputEvent");
        }

	/* In case the event has been already consumed or the component is already gone, 
	 * simply returns. 
	 */
	InputEvent inputEvent = (InputEvent) event;
	Component comp = inputEvent.getComponent();
	if (inputEvent.isConsumed() ||
	    null == comp ||
	    !comp.isShowing()) {	   
	    return;
	}

	int eventType = event.getID();
	if (eventType == KeyEvent.KEY_RELEASED) {
	    KeyEvent e = (KeyEvent) event;
            int keyCode = e.getKeyCode();
	    if (lookupList != null) { // if candidate window is active
		if (e.isControlDown()) {
		    if (keyCode == KeyEvent.VK_DOWN) {
		        // Control + Arrow Down commits chunks
			closeLookupWindow();
			commit(selectedSeg);
			e.consume();
		    }
		} else {
		    // select candidate by Arrow Up/Down
		    if (keyCode == KeyEvent.VK_DOWN) {
			if (++lookupSelection == lookupCandidateCount) {
			    lookupSelection = 0;
			}
			selectCandidate(lookupSelection);
			e.consume();
		    } else if (keyCode == KeyEvent.VK_UP) {
			if (--lookupSelection < 0) {
			    lookupSelection = lookupCandidateCount;
			}
			selectCandidate(lookupSelection);
			e.consume();
		    }
		}
	    } else {
		if (e.isControlDown()) {
		    if (keyCode == KeyEvent.VK_DOWN) {
			// Control + Arrow Down commits chunks
			commit(selectedSeg);
			e.consume();
		    }
		} else {
		    // move selected segment by Arrow Right/Left
		    if ((keyCode == KeyEvent.VK_RIGHT) && (converted == true)) {
			if (selectedSeg < (numSegs - 1)) {
			    selectedSeg++;
			    sendText(false);
			    e.consume();
			} else {
			    Toolkit.getDefaultToolkit().beep();
			}
		    } else if ((keyCode == KeyEvent.VK_LEFT) && (converted == true)) {
			if (selectedSeg > (committedSeg + 1)) {
			    selectedSeg--;
			    sendText(false);
			    e.consume();
			} else {
			    Toolkit.getDefaultToolkit().beep();
			}
		    }
		}
	    }
	} else if (eventType == MouseEvent.MOUSE_CLICKED) {
	    MouseEvent e = (MouseEvent) event;
	    Point pnt = comp.getLocationOnScreen();
	    int x = (int)pnt.getX() + e.getX();
	    int y = (int)pnt.getY() + e.getY();
	    TextHitInfo hit = inputMethodContext.getLocationOffset(x,y);
	    if (hit != null) {
	        // within composed text
		if (converted) {
		    selectedSeg = findSegment(hit.getInsertionIndex());
		    sendText(false);
		    e.consume();
		} else {
		    insertionPoint = hit.getInsertionIndex();
		}
	    } else {
	        // if hit outside composition, simply commit all.
		commitAll();
	    }
	} else if (eventType == KeyEvent.KEY_TYPED) {
            KeyEvent e = (KeyEvent) event;
            if (handleCharacter(e.getKeyChar())) {
                e.consume();
            }
        } else if (eventType == KeyEvent.KEY_PRESSED) {
            KeyEvent e = (KeyEvent) event;
            if ((e.getKeyCode() == KeyEvent.VK_ENTER) &&
                (lookupList != null || converted || rawText.length() != 0)) {
                e.consume();
            }
        }
    }
    
    /**
     * find segment at insertion point
     */
    int findSegment(int insertion) {
	for (int i = committedSeg + 1; i < numSegs; i++) {
	    if ((segPos[i][0] < insertion) && (insertion < segPos[i][1])) {
		return i;
	    }
	}
	return 0;
    }

    public void activate() {
        if (active) {
            System.out.println("CityInputMethod.activate called while active");
        }
        active = true;
	synchronized (statusWindow) {
	    statusWindowOwner = this; 
	    updateStatusWindow(locale);
	    if (!statusWindow.isVisible()) {
		statusWindow.setVisible(true);
	    }
	    setStatusWindowForeground(Color.black);
	}
    }
    
    public void deactivate(boolean isTemporary) {
        closeLookupWindow();
        if (!active) {
            System.out.println("CityInputMethod.deactivate called while not active");
        }
	setStatusWindowForeground(Color.lightGray);
        active = false;
    }
    
    public void hideWindows() {
        if (active) {
            System.out.println("CityInputMethod.hideWindows called while active");
        }
        closeLookupWindow();
	synchronized (statusWindow) {
	    if (statusWindowOwner == this) {
		statusWindow.setVisible(false);
	    }
	}
    }
    
    public void removeNotify() {
    }
    
    public void endComposition() {
        if (rawText.length() != 0) {
            commitAll();
        }
        closeLookupWindow();
    }

    public void notifyClientWindowChange(Rectangle location) {
	clientWindowLocation = location;
	synchronized (statusWindow) {
	    if (!attachedStatusWindow || statusWindowOwner != this) {
		return;
	    }
	    if (location != null) {
		statusWindow.setLocation(location.x, location.y+location.height);
		if (!statusWindow.isVisible()) {
		    if (active) {
			setStatusWindowForeground(Color.black);
		    } else {
			setStatusWindowForeground(Color.lightGray);
		    }
		    statusWindow.setVisible(true);
		}
	    } else {
		statusWindow.setVisible(false);
	    }
	}
    }

    public void dispose() {
        if (active) {
            System.out.println("CityInputMethod.dispose called while active");
        }
        if (disposed) {
            System.out.println("CityInputMethod.disposed called repeatedly");
        }
        closeLookupWindow();
	synchronized (statusWindow) {
	    cityInputMethodInstances.remove(this);
            if (cityInputMethodInstances.isEmpty()) {
                statusWindow.dispose();
            }
	}
        disposed = true;
    }
    
    public Object getControlObject() {
        return null;
    }
    
    public void setCompositionEnabled(boolean enable) {
	// not supported yet
	throw new UnsupportedOperationException();
    }

    public boolean isCompositionEnabled() {
        // always enabled
	return true;
    }

    private void initializeTables() throws IOException {
        synchronized (this.getClass()) {
            if (templates == null) {
                cityNames = loadProperties("names.properties");
		cityAliases = loadProperties("aliases.properties");
                cityLanguages = loadProperties("languages.properties");
                templates = loadProperties("templates.properties");
            }
        }
    }
    
    /** Use doPrivileged block to avoid the security exceptions 
     *  since the user code which triggers this action, e.g.
     *  open IM selection menu from a ColorChooserDialog, may in the call stack.
     *  Since reading properties file is relatively safe since it is located in the
     *  jar file itself, not from user's local file system.
     *  @exception IOException when reading properties file fails.
     */
    private Properties loadProperties(final String fileName) throws IOException {
	try {
	    return  
		(Properties) AccessController.doPrivileged(new PrivilegedExceptionAction() {
			public Object run() throws IOException {
			    Properties props = new Properties();
			    InputStream stream = this.getClass().getResourceAsStream(fileName);
			    props.load(stream);
			    stream.close();
			    return props;
			}
		    });
	} catch(PrivilegedActionException pae) {
	    throw (IOException)pae.getCause();
	}
    }
    
    /**
     * Attempts to handle a typed character.
     * @return whether the character was handled
     */
    private boolean handleCharacter(char ch) {
        if (lookupList != null) {
            if (ch == ' ') {
                if (++lookupSelection == lookupCandidateCount) {
                    lookupSelection = 0;
                }
                selectCandidate(lookupSelection);
                return true;
            } else if (ch == '\n') {
                commitAll();
                closeLookupWindow();
                return true;
            } else if ('1' <= ch && ch <= '0' + lookupCandidateCount) {
                selectCandidate(ch - '1');
                closeLookupWindow();
                return true;
            } else {
                Toolkit.getDefaultToolkit().beep();
                return true;
            }
        } else if (converted) {
            if (ch == ' ') {
                convertAgain();
                return true;
            } else if (ch == '\n') {
                commitAll();
                return true;
            } else {
                Toolkit.getDefaultToolkit().beep();
                return true;
            }
        } else {
            if (ch == ' ') {
                int length = rawText.length();
                if (length == 3 || length == 6 || length == 9) {
                    convertFirstTime();
                    return true;
                }
            } else if (ch == '\n') {
                if (rawText.length() != 0) {
                    commitAll();
                    return true;
                }
            } else if (ch == '\b') {
                if (insertionPoint > 0) {
		    rawText.deleteCharAt(insertionPoint - 1);
		    --insertionPoint;
                    sendText(false);
                    return true;
                }
            } else if ('a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z') {
		rawText.insert(insertionPoint++, ch);
                sendText(false);
                return true;
            }
            if (rawText.length() != 0) {
                Toolkit.getDefaultToolkit().beep();
                return true;
            }
        }
        return false;
    }
    
    /*
     * Looks up the entry for key in the given table, taken the
     * input method's locale into consideration.
     */
    String lookup(String lookupName, Properties table) {
	String result = null;
        String localeLookupName = lookupName + "_" + locale;
        while (true) {
            result = (String) table.get(localeLookupName);
            if (result != null) {
                break;
            }
            int index = localeLookupName.lastIndexOf("_");
            if (index == -1) {
                break;
            }
            localeLookupName = localeLookupName.substring(0, index);
        }
	return result;
    }

    String findAlias(String lookupName) {
        lookupName = lookupName.toUpperCase();
	return cityAliases.getProperty(lookupName, lookupName);
    }

    void convertFirstTime() {
	numSegs = rawText.length() / 3;
	rawTextSegs = new String[numSegs];
	convertedSegs = new String[numSegs];
	for (int i = 0; i < numSegs; ++i) {
	    rawTextSegs[i] = rawText.substring(i * 3, (i + 1) *3);
	    String alias = findAlias(rawTextSegs[i]);
	    String result = lookup(alias, cityNames);
	    if (result != null) {
		convertedSegs[i] = result;
	    } else {
		convertedSegs[i] = rawText.substring(i * 3, (i + 1) * 3);
	    }
	}
	converted = true;
	sendText(false);
    }
	
    void convertAgain() {
        String lookupName;
	lookupName = rawTextSegs[selectedSeg];
	// if converted string is same as original, it's not in word list. We skip
	// further conversion.
	if (!lookupName.equals(convertedSegs[selectedSeg])) {
	    lookupName = findAlias(lookupName);
	    lookupCandidates = new String[LOOKUP_LOCALES.length];
	    lookupLocales = new Locale[LOOKUP_LOCALES.length];
	    lookupCandidateCount = 0;
	    lookupSelection = 0;
	    for (int i = 0; i < LOOKUP_LOCALES.length; i++) {
		Locale iLocale = LOOKUP_LOCALES[i];
		String localeLookupName = lookupName + '_' + iLocale;
		String localeConvertedText = (String) cityNames.get(localeLookupName);
		if (localeConvertedText != null) {
		    lookupCandidates[lookupCandidateCount] = localeConvertedText;
		    lookupLocales[lookupCandidateCount] = iLocale;
		    lookupCandidateCount++;
		} else if (iLocale.equals(Locale.ENGLISH)) {
		    localeConvertedText = (String) cityNames.get(lookupName);
		    if (localeConvertedText != null) {
			lookupCandidates[lookupCandidateCount] = localeConvertedText;
			lookupLocales[lookupCandidateCount] = iLocale;
			lookupCandidateCount++;
		    }
		}
		if (convertedSegs[selectedSeg].equals(localeConvertedText)) {
		    lookupSelection = lookupCandidateCount - 1;
		}
	    }
	    openLookupWindow();
	} else {
	    Toolkit.getDefaultToolkit().beep();
	}
    }

    /* commits all chunks up to the specified index */  
    private void commit(int index) {
	if (index >= (numSegs - 1)) {
	    // if this is the last segment, commit all
	    commitAll();
	} else {
	    committedSeg = index;
	    selectedSeg = committedSeg + 1;
	    sendText(true);
	}
    }
    
    /* commits all chunks */
    void commitAll() {
	committedSeg = numSegs - 1;
        sendText(true);
	// once composed text is committed, reinitialize all variables
	rawText.setLength(0);
        convertedText = null;
        converted = false;

	rawTextSegs = null;
	convertedSegs = null;
	fmt = null;
	fieldPos = null;
	segPos = null;
	selectedSeg = 0;
	insertionPoint = rawText.length();
	numSegs = 0;
	committedSeg = -1;
	previouslyCommittedCharacterCount = 0;
    }
    
    void parseFormat() {
	Vector vec = new Vector();
	int[] elem = null;

	for(int i = 0; i < fmt.length(); i++) {
	    if (fmt.charAt(i) == '{') {
		elem = new int[2];
		elem[0] = i;
	    } else if (fmt.charAt(i) == '}') {
		elem[1] = i;
		vec.add(elem);
	    }
	}
	if (vec.size() != 0) {
	    fieldPos = new int[vec.size()][];
	    vec.toArray(fieldPos);
	}
    }

    void formatOutput() {
	if (fmt == null) {
	    String key = "Template" + Integer.toString(numSegs);
	    fmt = lookup(key, templates);
	    parseFormat();
	}
	convertedText = MessageFormat.format(fmt, (Object [])convertedSegs);
	// Figure out converted segment position
	int errors = 0;
	segPos = new int[fieldPos.length][2];
	for (int i = 0; i < fieldPos.length; i++) {
	    int optLen = (fieldPos[i][1] - fieldPos[i][0]) + 1;
	    int diffs = convertedSegs[i].length() - optLen;
	    segPos[i][0] = fieldPos[i][0] + errors;
	    segPos[i][1] = segPos[i][0] + convertedSegs[i].length();
	    errors += diffs;
	}
    }

    void sendText(boolean committed) {
	AttributedString as = null;
	TextHitInfo caret = null;
	int committedCharacterCount = 0;
	int newTotalCommittedCharacterCount = previouslyCommittedCharacterCount;

	if (converted) {
	    formatOutput();
	    if (committed) {
		if (committedSeg == (numSegs - 1)) {
		    newTotalCommittedCharacterCount = convertedText.length();
		} else {
		    newTotalCommittedCharacterCount = segPos[committedSeg + 1][0];
		}
		committedCharacterCount = newTotalCommittedCharacterCount - previouslyCommittedCharacterCount;
	    }
            as = new AttributedString(convertedText.substring(previouslyCommittedCharacterCount));
            for(int i = committedSeg + 1; i < numSegs; i++) {
                InputMethodHighlight highlight;
                if (i == selectedSeg) {
                    highlight = InputMethodHighlight.SELECTED_CONVERTED_TEXT_HIGHLIGHT;
                } else {
                    highlight = InputMethodHighlight.UNSELECTED_CONVERTED_TEXT_HIGHLIGHT;
                }
                as.addAttribute(TextAttribute.INPUT_METHOD_HIGHLIGHT, highlight,
                                segPos[i][0] - previouslyCommittedCharacterCount,
                                segPos[i][1] - previouslyCommittedCharacterCount);
            }
            previouslyCommittedCharacterCount = newTotalCommittedCharacterCount;
	} else {
	    as = new AttributedString(rawText.toString());
	    if (committed) {
	        committedCharacterCount = rawText.length();
	    } else if (rawText.length() != 0) {
		as.addAttribute(TextAttribute.INPUT_METHOD_HIGHLIGHT,
				InputMethodHighlight.SELECTED_RAW_TEXT_HIGHLIGHT);
	        caret = TextHitInfo.leading(insertionPoint);
	    }
	}
	inputMethodContext.dispatchInputMethodEvent(
						    InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
						    as.getIterator(),
						    committedCharacterCount,
						    caret,
						    null);
    }
    
    void selectCandidate(int candidate) {
        lookupSelection = candidate;
        lookupList.selectCandidate(lookupSelection);
        convertedSegs[selectedSeg] = lookupCandidates[lookupSelection];
        sendText(false);
    }
    
    int getSelectedSegmentOffset() {
        return segPos[selectedSeg][0] - previouslyCommittedCharacterCount;
    }
    
    void openLookupWindow() {
        lookupList = new LookupList(this, inputMethodContext, 
				    lookupCandidates, lookupLocales, lookupCandidateCount);
        lookupList.selectCandidate(lookupSelection);
    }
    
    void closeLookupWindow() {
        if (lookupList != null) {
            lookupList.setVisible(false);
            lookupList = null;
        }
    }
}

class LookupList extends JPanel {

    CityInputMethod inputMethod;
    InputMethodContext context;
    Window lookupWindow;
    String[] candidates;
    Locale[] locales;
    int candidateCount;
    int selected;
    
    final int INSIDE_INSET = 4;
    final int LINE_SPACING = 18;
    
    LookupList(CityInputMethod inputMethod, InputMethodContext context, 
	       String[] candidates, Locale[] locales, int candidateCount) {
        this.inputMethod = inputMethod;
        this.context = context;
        this.candidates = candidates;
        this.locales = locales;
        this.candidateCount = candidateCount;
        
	if (inputMethod.methodCreateInputMethodJFrame != null) {
	    try {
		Object[] params = new Object[2];
		params[0] = inputMethod.lookupWindowTitle;
		params[1] = Boolean.TRUE;
		lookupWindow = 
		    (Window)inputMethod.methodCreateInputMethodJFrame.invoke(context, params);
	    } catch (Exception e) {
        	lookupWindow = 
		    context.createInputMethodWindow(inputMethod.lookupWindowTitle, true);
	    }
	} else {
            lookupWindow = context.createInputMethodWindow(inputMethod.lookupWindowTitle, true);
	}
        setFont(new Font("Dialog", Font.PLAIN, 12));
        setPreferredSize(new Dimension(200, candidateCount * LINE_SPACING + 2 * INSIDE_INSET));
	setOpaque(true);
	setForeground(Color.black);
        setBackground(Color.white);
        
        enableEvents(AWTEvent.KEY_EVENT_MASK);
        enableEvents(AWTEvent.MOUSE_EVENT_MASK);
        
	if (lookupWindow instanceof JFrame) {
	    ((JFrame)lookupWindow).getContentPane().add(this);
	} else {
	    lookupWindow.add(this);
	}
	lookupWindow.pack();
        updateWindowLocation();
        lookupWindow.setVisible(true);
    }

    /**
     * Positions the lookup window near (usually below) the
     * insertion point in the component where composition occurs.
     */
    private void updateWindowLocation() {
        Point windowLocation = new Point();
        int textOffset = inputMethod.getSelectedSegmentOffset();
        Rectangle caretRect = context.getTextLocation(TextHitInfo.leading(textOffset));
        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
        Dimension windowSize = lookupWindow.getSize();
        final int SPACING = 2;
    
	if (caretRect.x + windowSize.width > screenSize.width) {
            windowLocation.x = screenSize.width - windowSize.width;
        } else {
            windowLocation.x = caretRect.x;
        }
        
	if (caretRect.y + caretRect.height + SPACING + windowSize.height > screenSize.height) {
            windowLocation.y = caretRect.y - SPACING - windowSize.height;         
        } else {
            windowLocation.y = caretRect.y + caretRect.height + SPACING;
        }

        lookupWindow.setLocation(windowLocation);
    }
    
    void selectCandidate(int candidate) {
        selected = candidate;
        repaint();
    }
    
    public void paint(Graphics g) {
	super.paint(g);
        FontMetrics metrics = g.getFontMetrics();
        int descent = metrics.getDescent();
        int ascent = metrics.getAscent();
        for (int i = 0; i < candidateCount; i++) {
            g.drawString((i + 1) + "   " + candidates[i] + 
                    "  (" + locales[i].getDisplayName() + ")",
                    INSIDE_INSET, LINE_SPACING * (i + 1) + INSIDE_INSET - descent);
        }
        Dimension size = getSize();
        g.drawRect(INSIDE_INSET / 2,
                LINE_SPACING * ( selected + 1) + INSIDE_INSET - (descent + ascent + 1),
                size.width - INSIDE_INSET,
                descent + ascent + 2);
        g.drawRect(0, 0, size.width - 1, size.height - 1);
    }
    
    public void setVisible(boolean visible) {
        if (!visible) {
            lookupWindow.setVisible(false);
            lookupWindow.dispose();
            lookupWindow = null;
        }
        super.setVisible(visible);
    }
    
    protected void processKeyEvent(KeyEvent event) {
        inputMethod.dispatchEvent(event);
    }
    
    protected void processMouseEvent(MouseEvent event) {
        if (event.getID() == MouseEvent.MOUSE_PRESSED) {
            int y = event.getY();
            if (y >= INSIDE_INSET && y < INSIDE_INSET + candidateCount * LINE_SPACING) {
                inputMethod.selectCandidate((y - INSIDE_INSET) / LINE_SPACING);
                inputMethod.closeLookupWindow();
            }
        }
    }
}
