/*
 *  
 *
 * Copyright  1990-2006 Sun Microsystems, Inc. All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License version
 * 2 only, as published by the Free Software Foundation. 
 * 
 * This program 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
 * General Public License version 2 for more details (a copy is
 * included at /legal/license.txt). 
 * 
 * You should have received a copy of the GNU General Public License
 * version 2 along with this work; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA 
 * 
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
 * Clara, CA 95054 or visit www.sun.com if you need additional
 * information or have any questions. 
 */
package com.sun.midp.chameleon.input;

import java.util.Vector;
import javax.microedition.lcdui.Display;
import javax.microedition.lcdui.Displayable;
import javax.microedition.lcdui.TextField;

/**
 * The BasicTextInputSession represents the relationship between the 
 * system's key input, the TextInputComponent, the available InputModes, 
 * and the graphical display. 
 */
public class BasicTextInputSession implements 
    TextInputSession, InputModeMediator 
{
    
    /** The currently "active" InputMode */
    protected InputMode currentMode;
    
    /** The set of all possible InputModes */
    protected InputMode[] inputModeSet;
    
    /** The current Display object */
    private Display currentDisplay;

    /** The previous Displayable */
    private Displayable previousScreen;

    /** 
     * If the user has specifically chosen an InputMode, that choice
     * becomes sticky when the InputSession chooses the InputMode to
     * make active.
     */
    protected InputMode stickyMode;
    
    /** The text component receiving the input */
    protected TextInputComponent textComponent;

    
    /**
     * Construct a new BasicTextInputSession
     */
    public BasicTextInputSession() { 
        inputModeSet = InputModeFactory.createInputModes();
    }
    
    /**
     * Start a text input session for the given TextInputComponent.
     * The TextInputComponent can be used to determine the initial
     * input mode, constraints, etc.
     *
     * @param component the TextInputComponent which is receiving text input
     */
    public void beginSession(TextInputComponent component) {
        if (component == null) {
            throw new IllegalArgumentException(
                "Null TextInputComponent in beginSession()");
        }
        
        if (this.textComponent == null) {
            this.textComponent = component;
        } else if (this.textComponent != component) {
            throw new IllegalStateException(
                "InputModeHandler in use by another TextInputComponent");
        }
        
        // Select a suitable InputMode
        selectInputMode();
    }
        
    /**
     * List the appropriate InputModes available for the current input
     * session.  This method may be used by UI components in order to make
     * certain input mode choices available to the user for selection.
     * If this handler is not currently in an active text input session,
     * this method returns null.
     *
     * @return an array of InputModes which are available to use given the
     *         current TextInputComponent and its input constraints
     */
    public InputMode[] getAvailableModes() {
        if (textComponent == null) {
            throw new IllegalStateException(
                "Call to InputModeHandler while outside of a valid session");
        }
        
        int constraints = textComponent.getConstraints();
        Vector v = new Vector();
        for (int i = 0; i < inputModeSet.length; i++) {
            if (inputModeSet[i].supportsConstraints(constraints)) {
                v.addElement(inputModeSet[i]);
            }
        }
        
        if (v.size() == 0) {
            return null;
        }
        
        InputMode[] modes = new InputMode[v.size()];
        v.copyInto(modes);
        
        return modes;
    }
    
    /**
     * Retrieve the InputMode which is the current "active" mode
     * for this TextInputSession. This does not necessarily mean there is 
     * any pending input with the InputMode itself, it means that if this 
     * TextInputSession receives key input, the returned InputMode will be
     * the mode which processes that input.
     *
     * @return the currently "active" InputMode
     */
    public InputMode getCurrentInputMode() {
        return currentMode;
    }
    
    /**
     * Set this TextInputSession's current "active" InputMode to the
     * given mode. The given mode must be one of the InputModes listed
     * in the array of InputModes returned from the getAvailableModes()
     * method of this TextInputSession. Calling this method will terminate
     * any existing input session with the current InputMode and will
     * result in any subsequent key input being processed by the given
     * InputMode. If the given mode is already the current "active"
     * InputMode, this method has no effect. If this TextInputSession
     * is not currently in an input session (ie, there is no active
     * TextInputComponent), this method has no effect.
     *
     * @param mode the InputMode to switch key processing to
     */
    public void setCurrentInputMode(InputMode mode) {
        if (mode == null || mode == currentMode) {
            return;
        }
        
        for (int i = 0; i < inputModeSet.length; i++) {
            if (inputModeSet[i] == mode) {
                try {
                    endInputMode(currentMode);
                    setInputMode(inputModeSet[i]);
                } catch (Throwable t) {
                    // IMPL_NOTE Log exception?
                }
                break;
            }
        }
    }
        
    /**    
     * This method abstracts key processing to a single call (from
     * the assorted key press, release, repeat events). This method
     * should be called from the TextInputComponent to pass along
     * key input from the user. The TextInputComponent is responsible
     * for determining what key events should be processed (ie,
     * key events trigger processing on press or on release).
     * 
     * @param keyCode the numeric code representing the key which was
     *        pressed
     * @param longPress return true if it's long key press otherwise false
     * @return true if the current InputMode processed the key event,
     *         false if the key was not processed at all by the current
     *         InputMode (not all keys apply to input)
     */
    public int processKey(int keyCode, boolean longPress) {
        try {
            return currentMode.processKey(keyCode, longPress);
        } catch (Throwable t) {
            // Since InputModes are pluggable, we'll catch any possible
            // Throwable when calling into one
            // IMPL_NOTE : log the throwable
        }
        return InputMode.KEYCODE_NONE;
    }

    /**
     * return the pending char
     * used to bypass the asynchronous commit mechanism
     * e.g. to immediately commit a char before moving the cursor
     *
     * @return return the pending char
     */
    public char getPendingChar() { 
        return currentMode != null ? currentMode.getPendingChar() : 0;
    }

        
    /**
     * An iterative method to return the next available match given
     * the key processing thus far. If the return value of hasMoreMatches()
     * is true, this method will return a non-null String and will iterate
     * through the entire set of available matches until the set is exhausted.
     *
     * Each subsequent call to processKey() will reset the iterator over
     * the set of available matches regardless if the key resulted in a change
     * to the set.
     *
     * The two methods, hasMoreMatches() and getNextMatch(), can be used by 
     * the User Interface system to retrieve the current set of pending inputs
     * and possibly present a chooser option to the user.
     *
     * @return a String representing the best possible pending
     *         input, or null, if there is no pending input
     */
    public String getNextMatch() {
        try {               
            return currentMode.getNextMatch();
        } catch (Throwable t) {
            // Since InputModes are pluggable, we'll catch any possible
            // Throwable when calling into one
            // IMPL_NOTE : log the throwable
        }
        return null;
    }

    
    /**
     * If the InputMode supports multiple matches and more matches are
     * available this method will return true, false otherwise.
     * 
     * @return true if the current InputMode supports multiple matches and
     *         there are currently more matches available
     */
    public boolean hasMoreMatches() {
        try {
            return currentMode.hasMoreMatches();
        } catch (Throwable t) {
            // Since InputModes are pluggable, we'll catch any possible
            // Throwable when calling into one
            // IMPL_NOTE : log the throwable
        }
        return false;
    }


    /**
     * Gets the possible string matches 
     *
     * @return returns the set of options.
     */
    public String[] getMatchList() {
        return currentMode != null ? currentMode.getMatchList() : new String[0];
    }


    /**
     * End the current text input session and do not commit any pending
     * characters to the buffer.
     */
    public void endSession() {
        log("[BTIS.endSession]");

        if (currentMode != null) {
            endInputMode(currentMode);
            setInputMode(null);
        }
        textComponent = null;
        stickyMode = null;
    }
    
    // ******* Begin InputModeMediator Interface *******    
    /**
     * Called by an InputMode in order to automatically commit the given 
     * input to the Text component. For example, when the timer expires
     * in an AlphaNumeric InputMode it will commit the current pending
     * character.
     * @param input text to commit
     */
    public void commit(String input) {
        if (input != null && textComponent != null) {
            textComponent.commit(input);
        }
    }

    /**
     * Clear the particular number of symbols 
     *
     * @param num number of symbols
     */
    public void clear(int num) {
        if (num == 0) {
            log("WARNING: BasicTextInput.clear calld with 0");
            return;
        }
        textComponent.clear(num);
    }

    /**
     * Called by an InputMode to inform a TextComponent of a sub-inputMode
     * change. 
     */
    public void subInputModeChanged() {
        textComponent.notifyModeChanged();
    }
    
    /**
     * Called by an InputMode in order to signal that the input process
     * has been completed with respect to the InputMode. Subsequent key 
     * input should be handled in a new input session, possibly by the
     * same InputMode or by a different InputMode alltogether. For example,
     * when the timer expires in an AlphaNumeric InputMode, the character
     * is committed and the AlphaNumeric InputMode signals its completion.
     * Further key input may start a new session with the AlphaNumeric
     * InputMode or possibly some other InputMode.
     */
    public void inputModeCompleted() {
        log("[Basic.inputModeCompleted()] >>> ");
        try {
            if (currentMode != null) {
                log("[Basic.inputModeCompleted()] !=null");
                endInputMode(currentMode);
                setInputMode(null);
            }        
            // Select a suitable InputMode
            selectInputMode();
        } catch (Exception e) {
            e.printStackTrace();
        }
        log("[Basic.inputModeCompleted()] <<<< ");
    }

    // ******* End InputModeMediator Interface *******
    
    /**
     * Based on the constraints of the current TextInputComponent,
     * select the most appropriate InputMode from the list available.
     * This method will also start the session with the InputMode by
     * calling the InputMode's beginInput() method.
     */
    protected void selectInputMode() {
        if (textComponent == null) {
            throw new IllegalStateException(
                "Attempted input on null TextInputComponent");
        }

        int constraints = textComponent.getConstraints();
        
	InputMode newMode = null;
       
        if (stickyMode != null && stickyMode.supportsConstraints(constraints)) {
            log("[BTIS.selectInputMode] setting mode to sticky:" +
                stickyMode.getName());
            newMode = stickyMode;
        } else {
            log("[BTIS.selectInputMode] not setting mode to sticky");
            for (int i = 0; i < inputModeSet.length; i++) {
                if (inputModeSet[i].supportsConstraints(constraints)) {
                    boolean[][] map = inputModeSet[i].getIsConstraintsMap();
                    int index = 0;
                    String is = textComponent.getInitialInputMode();
                    for (; index < INPUT_SUBSETS.length; index++) {
                        if (INPUT_SUBSETS[index].equals(is))
                            break;
                    }
                    int constraint = constraints &
                        TextField.CONSTRAINT_MASK;
                    if (constraint < TextInputSession.MAX_CONSTRAINTS &&
                        map[index][constraint]) {
                        newMode = inputModeSet[i];
                        break;
                    }
                }
            }
        }

        if (newMode != null) {
            if (newMode != currentMode) {
                endInputMode(currentMode);
                setInputMode(newMode);
            }
        } else {
            throw new IllegalStateException(
             "No InputMode found supporting the current constraints");
        }
    }

    /**
     * Set the required input mode. Sticky mode can be set as the old mode just
     * in case it will have to be reverted back. Text component has to be
     * notified about the mode change. 
     * 
     * @param mode the required input mode 
     */
    private void setInputMode(InputMode mode) {
        InputMode oldMode = currentMode;
        currentMode = mode;

        if (currentMode != null && textComponent != null) {
            currentMode.beginInput(this,
                                   textComponent.getInitialInputMode(),
                                   textComponent.getConstraints());
            if (currentMode.hasDisplayable()) {
                currentDisplay = textComponent.getDisplay();
                previousScreen = currentDisplay.getCurrent();
                currentDisplay.setCurrent(currentMode.getDisplayable());
                stickyMode = oldMode;
            } else {
                stickyMode = currentMode;
            }
            textComponent.notifyModeChanged();
        }
    }
    
    /**
     * End the expired input mode. 
     * 
     * @param mode expired input mode 
     */
    private void endInputMode(InputMode mode) {
        if (mode != null) {
            mode.endInput();
            if (mode.hasDisplayable() && textComponent != null) {
                currentDisplay.setCurrent(previousScreen);
                previousScreen = null;
                currentDisplay = null;
            }
        }
    }

    /**
     * Check if the given char is symbol
     * @param c char 
     * @return true if the char is symbol otherwise false. 
     */
    public boolean isSymbol(char c) {
        return SymbolInputMode.isSymbol(c);
    }


    /**
     * Print the debug message 
     * @param s debug message
     */
    private void log(String s) {
        //        System.out.println(s);
    }

    /**
     * Returns true if the keyCode is used as 'clear'
     * @param keyCode key code
     * @return true if keu code is Clear one, false otherwise
     */
    public boolean isClearKey(int keyCode) {
        return textComponent != null &&
            textComponent.isClearKey(keyCode);
    }

    /**
     * Returns the available size (number of characters) that can be
     * stored in this <code>TextInputComponent</code>.
     * @return available size in characters
     */
    public int getAvailableSize() {
        return textComponent != null ? textComponent.getAvailableSize() : 0;
    }
}

