/*
 * Copyright 2011 Vaadin Ltd.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.vaadin.ui;

import java.util.Map;

import com.vaadin.event.FieldEvents.BlurEvent;
import com.vaadin.event.FieldEvents.BlurListener;
import com.vaadin.event.FieldEvents.BlurNotifier;
import com.vaadin.event.FieldEvents.FocusEvent;
import com.vaadin.event.FieldEvents.FocusListener;
import com.vaadin.event.FieldEvents.FocusNotifier;
import com.vaadin.event.FieldEvents.TextChangeEvent;
import com.vaadin.event.FieldEvents.TextChangeListener;
import com.vaadin.event.FieldEvents.TextChangeNotifier;
import com.vaadin.server.LegacyComponent;
import com.vaadin.server.PaintException;
import com.vaadin.server.PaintTarget;
import com.vaadin.shared.ui.textfield.AbstractTextFieldState;
import com.vaadin.shared.ui.textfield.TextFieldConstants;

public abstract class AbstractTextField extends AbstractField<String> implements
        BlurNotifier, FocusNotifier, TextChangeNotifier, LegacyComponent {

    /**
     * Null representation.
     */
    private String nullRepresentation = "null";
    /**
     * Is setting to null from non-null value allowed by setting with null
     * representation .
     */
    private boolean nullSettingAllowed = false;
    /**
     * The text content when the last messages to the server was sent. Cleared
     * when value is changed.
     */
    private String lastKnownTextContent;

    /**
     * The position of the cursor when the last message to the server was sent.
     */
    private int lastKnownCursorPosition;

    /**
     * Flag indicating that a text change event is pending to be triggered.
     * Cleared by {@link #setInternalValue(Object)} and when the event is fired.
     */
    private boolean textChangeEventPending;

    private boolean isFiringTextChangeEvent = false;

    private TextChangeEventMode textChangeEventMode = TextChangeEventMode.LAZY;

    private final int DEFAULT_TEXTCHANGE_TIMEOUT = 400;

    private int textChangeEventTimeout = DEFAULT_TEXTCHANGE_TIMEOUT;

    /**
     * Temporarily holds the new selection position. Cleared on paint.
     */
    private int selectionPosition = -1;

    /**
     * Temporarily holds the new selection length.
     */
    private int selectionLength;

    /**
     * Flag used to determine whether we are currently handling a state change
     * triggered by a user. Used to properly fire text change event before value
     * change event triggered by the client side.
     */
    private boolean changingVariables;

    protected AbstractTextField() {
        super();
    }

    @Override
    protected AbstractTextFieldState getState() {
        return (AbstractTextFieldState) super.getState();
    }

    @Override
    public void beforeClientResponse(boolean initial) {
        super.beforeClientResponse(initial);

        String value = getValue();
        if (value == null) {
            value = getNullRepresentation();
        }
        getState().text = value;
    }

    @Override
    public void paintContent(PaintTarget target) throws PaintException {

        if (selectionPosition != -1) {
            target.addAttribute("selpos", selectionPosition);
            target.addAttribute("sellen", selectionLength);
            selectionPosition = -1;
        }

        if (hasListeners(TextChangeEvent.class)) {
            target.addAttribute(TextFieldConstants.ATTR_TEXTCHANGE_EVENTMODE,
                    getTextChangeEventMode().toString());
            target.addAttribute(TextFieldConstants.ATTR_TEXTCHANGE_TIMEOUT,
                    getTextChangeTimeout());
            if (lastKnownTextContent != null) {
                /*
                 * The field has be repainted for some reason (e.g. caption,
                 * size, stylename), but the value has not been changed since
                 * the last text change event. Let the client side know about
                 * the value the server side knows. Client side may then ignore
                 * the actual value, depending on its state.
                 */
                target.addAttribute(
                        TextFieldConstants.ATTR_NO_VALUE_CHANGE_BETWEEN_PAINTS,
                        true);
            }
        }

    }

    @Override
    public void changeVariables(Object source, Map<String, Object> variables) {
        changingVariables = true;

        try {

            if (variables.containsKey(TextFieldConstants.VAR_CURSOR)) {
                Integer object = (Integer) variables
                        .get(TextFieldConstants.VAR_CURSOR);
                lastKnownCursorPosition = object.intValue();
            }

            if (variables.containsKey(TextFieldConstants.VAR_CUR_TEXT)) {
                /*
                 * NOTE, we might want to develop this further so that on a
                 * value change event the whole text content don't need to be
                 * sent from the client to server. Just "commit" the value from
                 * currentText to the value.
                 */
                handleInputEventTextChange(variables);
            }

            // Sets the text
            if (variables.containsKey("text") && !isReadOnly()) {

                // Only do the setting if the string representation of the value
                // has been updated
                String newValue = (String) variables.get("text");

                // server side check for max length
                if (getMaxLength() != -1 && newValue.length() > getMaxLength()) {
                    newValue = newValue.substring(0, getMaxLength());
                }
                final String oldValue = getValue();
                if (newValue != null
                        && (oldValue == null || isNullSettingAllowed())
                        && newValue.equals(getNullRepresentation())) {
                    newValue = null;
                }
                if (newValue != oldValue
                        && (newValue == null || !newValue.equals(oldValue))) {
                    boolean wasModified = isModified();
                    setValue(newValue, true);

                    // If the modified status changes, or if we have a
                    // formatter, repaint is needed after all.
                    if (wasModified != isModified()) {
                        markAsDirty();
                    }
                }
            }
            firePendingTextChangeEvent();

            if (variables.containsKey(FocusEvent.EVENT_ID)) {
                fireEvent(new FocusEvent(this));
            }
            if (variables.containsKey(BlurEvent.EVENT_ID)) {
                fireEvent(new BlurEvent(this));
            }
        } finally {
            changingVariables = false;

        }

    }

    @Override
    public Class<String> getType() {
        return String.class;
    }

    /**
     * Gets the null-string representation.
     * 
     * <p>
     * The null-valued strings are represented on the user interface by
     * replacing the null value with this string. If the null representation is
     * set null (not 'null' string), painting null value throws exception.
     * </p>
     * 
     * <p>
     * The default value is string 'null'.
     * </p>
     * 
     * @return the String Textual representation for null strings.
     * @see TextField#isNullSettingAllowed()
     */
    public String getNullRepresentation() {
        return nullRepresentation;
    }

    /**
     * Is setting nulls with null-string representation allowed.
     * 
     * <p>
     * If this property is true, writing null-representation string to text
     * field always sets the field value to real null. If this property is
     * false, null setting is not made, but the null values are maintained.
     * Maintenance of null-values is made by only converting the textfield
     * contents to real null, if the text field matches the null-string
     * representation and the current value of the field is null.
     * </p>
     * 
     * <p>
     * By default this setting is false
     * </p>
     * 
     * @return boolean Should the null-string represenation be always converted
     *         to null-values.
     * @see TextField#getNullRepresentation()
     */
    public boolean isNullSettingAllowed() {
        return nullSettingAllowed;
    }

    /**
     * Sets the null-string representation.
     * 
     * <p>
     * The null-valued strings are represented on the user interface by
     * replacing the null value with this string. If the null representation is
     * set null (not 'null' string), painting null value throws exception.
     * </p>
     * 
     * <p>
     * The default value is string 'null'
     * </p>
     * 
     * @param nullRepresentation
     *            Textual representation for null strings.
     * @see TextField#setNullSettingAllowed(boolean)
     */
    public void setNullRepresentation(String nullRepresentation) {
        this.nullRepresentation = nullRepresentation;
        markAsDirty();
    }

    /**
     * Sets the null conversion mode.
     * 
     * <p>
     * If this property is true, writing null-representation string to text
     * field always sets the field value to real null. If this property is
     * false, null setting is not made, but the null values are maintained.
     * Maintenance of null-values is made by only converting the textfield
     * contents to real null, if the text field matches the null-string
     * representation and the current value of the field is null.
     * </p>
     * 
     * <p>
     * By default this setting is false.
     * </p>
     * 
     * @param nullSettingAllowed
     *            Should the null-string representation always be converted to
     *            null-values.
     * @see TextField#getNullRepresentation()
     */
    public void setNullSettingAllowed(boolean nullSettingAllowed) {
        this.nullSettingAllowed = nullSettingAllowed;
        markAsDirty();
    }

    @Override
    protected boolean isEmpty() {
        return super.isEmpty() || getValue().length() == 0;
    }

    /**
     * Returns the maximum number of characters in the field. Value -1 is
     * considered unlimited. Terminal may however have some technical limits.
     * 
     * @return the maxLength
     */
    public int getMaxLength() {
        return getState().maxLength;
    }

    /**
     * Sets the maximum number of characters in the field. Value -1 is
     * considered unlimited. Terminal may however have some technical limits.
     * 
     * @param maxLength
     *            the maxLength to set
     */
    public void setMaxLength(int maxLength) {
        getState().maxLength = maxLength;
    }

    /**
     * Gets the number of columns in the editor. If the number of columns is set
     * 0, the actual number of displayed columns is determined implicitly by the
     * adapter.
     * 
     * @return the number of columns in the editor.
     */
    public int getColumns() {
        return getState().columns;
    }

    /**
     * Sets the number of columns in the editor. If the number of columns is set
     * 0, the actual number of displayed columns is determined implicitly by the
     * adapter.
     * 
     * @param columns
     *            the number of columns to set.
     */
    public void setColumns(int columns) {
        if (columns < 0) {
            columns = 0;
        }
        getState().columns = columns;
    }

    /**
     * Gets the current input prompt.
     * 
     * @see #setInputPrompt(String)
     * @return the current input prompt, or null if not enabled
     */
    public String getInputPrompt() {
        return getState().inputPrompt;
    }

    /**
     * Sets the input prompt - a textual prompt that is displayed when the field
     * would otherwise be empty, to prompt the user for input.
     * 
     * @param inputPrompt
     */
    public void setInputPrompt(String inputPrompt) {
        getState().inputPrompt = inputPrompt;
    }

    /* ** Text Change Events ** */

    private void firePendingTextChangeEvent() {
        if (textChangeEventPending && !isFiringTextChangeEvent) {
            isFiringTextChangeEvent = true;
            textChangeEventPending = false;
            try {
                fireEvent(new TextChangeEventImpl(this));
            } finally {
                isFiringTextChangeEvent = false;
            }
        }
    }

    @Override
    protected void setInternalValue(String newValue) {
        if (changingVariables && !textChangeEventPending) {

            /*
             * TODO check for possible (minor?) issue (not tested)
             * 
             * -field with e.g. PropertyFormatter.
             * 
             * -TextChangeListener and it changes value.
             * 
             * -if formatter again changes the value, do we get an extra
             * simulated text change event ?
             */

            /*
             * Fire a "simulated" text change event before value change event if
             * change is coming from the client side.
             * 
             * Iff there is both value change and textChangeEvent in same
             * variable burst, it is a text field in non immediate mode and the
             * text change event "flushed" queued value change event. In this
             * case textChangeEventPending flag is already on and text change
             * event will be fired after the value change event.
             */
            if (newValue == null && lastKnownTextContent != null
                    && !lastKnownTextContent.equals(getNullRepresentation())) {
                // Value was changed from something to null representation
                lastKnownTextContent = getNullRepresentation();
                textChangeEventPending = true;
            } else if (newValue != null
                    && !newValue.toString().equals(lastKnownTextContent)) {
                // Value was changed to something else than null representation
                lastKnownTextContent = newValue.toString();
                textChangeEventPending = true;
            }
            firePendingTextChangeEvent();
        }

        super.setInternalValue(newValue);
    }

    @Override
    public void setValue(Object newValue) throws ReadOnlyException {
        super.setValue(newValue);
        /*
         * Make sure w reset lastKnownTextContent field on value change. The
         * clearing must happen here as well because TextChangeListener can
         * revert the original value. Client must respect the value in this
         * case. AbstractField optimizes value change if the existing value is
         * reset. Also we need to force repaint if the flag is on.
         */
        if (lastKnownTextContent != null) {
            lastKnownTextContent = null;
            markAsDirty();
        }
    }

    private void handleInputEventTextChange(Map<String, Object> variables) {
        /*
         * TODO we could vastly optimize the communication of values by using
         * some sort of diffs instead of always sending the whole text content.
         * Also on value change events we could use the mechanism.
         */
        String object = (String) variables.get(TextFieldConstants.VAR_CUR_TEXT);
        lastKnownTextContent = object;
        textChangeEventPending = true;
    }

    /**
     * Sets the mode how the TextField triggers {@link TextChangeEvent}s.
     * 
     * @param inputEventMode
     *            the new mode
     * 
     * @see TextChangeEventMode
     */
    public void setTextChangeEventMode(TextChangeEventMode inputEventMode) {
        textChangeEventMode = inputEventMode;
        markAsDirty();
    }

    /**
     * @return the mode used to trigger {@link TextChangeEvent}s.
     */
    public TextChangeEventMode getTextChangeEventMode() {
        return textChangeEventMode;
    }

    /**
     * Different modes how the TextField can trigger {@link TextChangeEvent}s.
     */
    public enum TextChangeEventMode {

        /**
         * An event is triggered on each text content change, most commonly key
         * press events.
         */
        EAGER,
        /**
         * Each text change event in the UI causes the event to be communicated
         * to the application after a timeout. The length of the timeout can be
         * controlled with {@link TextField#setInputEventTimeout(int)}. Only the
         * last input event is reported to the server side if several text
         * change events happen during the timeout.
         * <p>
         * In case of a {@link ValueChangeEvent} the schedule is not kept
         * strictly. Before a {@link ValueChangeEvent} a {@link TextChangeEvent}
         * is triggered if the text content has changed since the previous
         * TextChangeEvent regardless of the schedule.
         */
        TIMEOUT,
        /**
         * An event is triggered when there is a pause of text modifications.
         * The length of the pause can be modified with
         * {@link TextField#setInputEventTimeout(int)}. Like with the
         * {@link #TIMEOUT} mode, an event is forced before
         * {@link ValueChangeEvent}s, even if the user did not keep a pause
         * while entering the text.
         * <p>
         * This is the default mode.
         */
        LAZY
    }

    @Override
    public void addTextChangeListener(TextChangeListener listener) {
        addListener(TextChangeListener.EVENT_ID, TextChangeEvent.class,
                listener, TextChangeListener.EVENT_METHOD);
    }

    /**
     * @deprecated Since 7.0, replaced by
     *             {@link #addTextChangeListener(TextChangeListener)}
     **/
    @Override
    @Deprecated
    public void addListener(TextChangeListener listener) {
        addTextChangeListener(listener);
    }

    @Override
    public void removeTextChangeListener(TextChangeListener listener) {
        removeListener(TextChangeListener.EVENT_ID, TextChangeEvent.class,
                listener);
    }

    /**
     * @deprecated Since 7.0, replaced by
     *             {@link #removeTextChangeListener(TextChangeListener)}
     **/
    @Override
    @Deprecated
    public void removeListener(TextChangeListener listener) {
        removeTextChangeListener(listener);
    }

    /**
     * The text change timeout modifies how often text change events are
     * communicated to the application when {@link #getTextChangeEventMode()} is
     * {@link TextChangeEventMode#LAZY} or {@link TextChangeEventMode#TIMEOUT}.
     * 
     * 
     * @see #getTextChangeEventMode()
     * 
     * @param timeout
     *            the timeout in milliseconds
     */
    public void setTextChangeTimeout(int timeout) {
        textChangeEventTimeout = timeout;
        markAsDirty();
    }

    /**
     * Gets the timeout used to fire {@link TextChangeEvent}s when the
     * {@link #getTextChangeEventMode()} is {@link TextChangeEventMode#LAZY} or
     * {@link TextChangeEventMode#TIMEOUT}.
     * 
     * @return the timeout value in milliseconds
     */
    public int getTextChangeTimeout() {
        return textChangeEventTimeout;
    }

    public static class TextChangeEventImpl extends TextChangeEvent {
        private String curText;
        private int cursorPosition;

        private TextChangeEventImpl(final AbstractTextField tf) {
            super(tf);
            curText = tf.getCurrentTextContent();
            cursorPosition = tf.getCursorPosition();
        }

        @Override
        public AbstractTextField getComponent() {
            return (AbstractTextField) super.getComponent();
        }

        @Override
        public String getText() {
            return curText;
        }

        @Override
        public int getCursorPosition() {
            return cursorPosition;
        }

    }

    /**
     * Gets the current (or the last known) text content in the field.
     * <p>
     * Note the text returned by this method is not necessary the same that is
     * returned by the {@link #getValue()} method. The value is updated when the
     * terminal fires a value change event via e.g. blurring the field or by
     * pressing enter. The value returned by this method is updated also on
     * {@link TextChangeEvent}s. Due to this high dependency to the terminal
     * implementation this method is (at least at this point) not published.
     * 
     * @return the text which is currently displayed in the field.
     */
    private String getCurrentTextContent() {
        if (lastKnownTextContent != null) {
            return lastKnownTextContent;
        } else {
            Object text = getValue();
            if (text == null) {
                return getNullRepresentation();
            }
            return text.toString();
        }
    }

    /**
     * Selects all text in the field.
     * 
     * @since 6.4
     */
    public void selectAll() {
        String text = getValue() == null ? "" : getValue().toString();
        setSelectionRange(0, text.length());
    }

    /**
     * Sets the range of text to be selected.
     * 
     * As a side effect the field will become focused.
     * 
     * @since 6.4
     * 
     * @param pos
     *            the position of the first character to be selected
     * @param length
     *            the number of characters to be selected
     */
    public void setSelectionRange(int pos, int length) {
        selectionPosition = pos;
        selectionLength = length;
        focus();
        markAsDirty();
    }

    /**
     * Sets the cursor position in the field. As a side effect the field will
     * become focused.
     * 
     * @since 6.4
     * 
     * @param pos
     *            the position for the cursor
     * */
    public void setCursorPosition(int pos) {
        setSelectionRange(pos, 0);
        lastKnownCursorPosition = pos;
    }

    /**
     * Returns the last known cursor position of the field.
     * 
     * <p>
     * Note that due to the client server nature or the GWT terminal, Vaadin
     * cannot provide the exact value of the cursor position in most situations.
     * The value is updated only when the client side terminal communicates to
     * TextField, like on {@link ValueChangeEvent}s and {@link TextChangeEvent}
     * s. This may change later if a deep push integration is built to Vaadin.
     * 
     * @return the cursor position
     */
    public int getCursorPosition() {
        return lastKnownCursorPosition;
    }

    @Override
    public void addFocusListener(FocusListener listener) {
        addListener(FocusEvent.EVENT_ID, FocusEvent.class, listener,
                FocusListener.focusMethod);
    }

    /**
     * @deprecated Since 7.0, replaced by
     *             {@link #addFocusListener(FocusListener)}
     **/
    @Override
    @Deprecated
    public void addListener(FocusListener listener) {
        addFocusListener(listener);
    }

    @Override
    public void removeFocusListener(FocusListener listener) {
        removeListener(FocusEvent.EVENT_ID, FocusEvent.class, listener);
    }

    /**
     * @deprecated Since 7.0, replaced by
     *             {@link #removeFocusListener(FocusListener)}
     **/
    @Override
    @Deprecated
    public void removeListener(FocusListener listener) {
        removeFocusListener(listener);
    }

    @Override
    public void addBlurListener(BlurListener listener) {
        addListener(BlurEvent.EVENT_ID, BlurEvent.class, listener,
                BlurListener.blurMethod);
    }

    /**
     * @deprecated Since 7.0, replaced by {@link #addBlurListener(BlurListener)}
     **/
    @Override
    @Deprecated
    public void addListener(BlurListener listener) {
        addBlurListener(listener);
    }

    @Override
    public void removeBlurListener(BlurListener listener) {
        removeListener(BlurEvent.EVENT_ID, BlurEvent.class, listener);
    }

    /**
     * @deprecated Since 7.0, replaced by
     *             {@link #removeBlurListener(BlurListener)}
     **/
    @Override
    @Deprecated
    public void removeListener(BlurListener listener) {
        removeBlurListener(listener);
    }

}
