
package uk.co.wingpath.gui;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import uk.co.wingpath.event.*;
import uk.co.wingpath.event.Event;

public class TextEditor
{
    private JTextComponent field;
    private String value;
    private Verifier verifier;
    private boolean isChanging;
    private boolean settingValue;
    private ValueEventSource listeners;
    private boolean hasFocus;
    private MirrorField mirror;
    private MirroredField mirrored;

    public TextEditor (final JTextComponent field, String value,
        ValueEventSource listeners)
    {
        Event.checkIsEventDispatchThread ();
        this.field = field;
        this.value = value;
        this.listeners = listeners;
        field.setText (value);
        field.setCaretPosition (0);
        verifier = null;
        isChanging = false;
        settingValue = false;
        hasFocus = false;
        mirror = null;
        mirrored = null;

        field.setInputVerifier (
            new InputVerifier ()
            {
                @Override
                public boolean shouldYieldFocus (JComponent component)
                {
                    return checkValue ();
                }

                @Override
                public boolean verify (JComponent component)
                {
                    if (verifier == null)
                        return true;
                    String val = field.getText ();
                    val = verifier.verify (val, false);
                    return val != null;
                }
            });

        field.getDocument ().addDocumentListener (new DocumentListener ()
            {
                public void insertUpdate (DocumentEvent e)
                {
                    documentUpdate ();
                }

                public void removeUpdate(DocumentEvent e)
                {
                    documentUpdate ();
                }

                public void changedUpdate(DocumentEvent e)
                {
                }
            });

        field.addFocusListener (
            new FocusListener ()
            {
                public void focusGained (FocusEvent e)
                {
                    if (e.isTemporary () || hasFocus)
                        return;
                    hasFocus = true;
                }

                public void focusLost (FocusEvent e)
                {
                    if (e.isTemporary () || !hasFocus)
                        return;
                    hasFocus = false;
                }
            });

        Gui.addShortCut (field, "cancel", KeyEvent.VK_ESCAPE, 0,
            new AbstractAction ()
            {
                public void actionPerformed (ActionEvent ev)
                {
                    cancelEdit ();
                }
            });
    }

    public void setMirror (MirrorField mirror, MirroredField mirrored)
    {
        this.mirror = mirror;
        this.mirrored = mirrored;
    }

    private boolean isMirrored ()
    {
        return mirror != null && mirror.isMirrored (mirrored);
    }

    private void copyToMirror ()
    {
        if (hasFocus && isMirrored ())
            mirror.displayValue (field.getText ());
    }

    private void documentUpdate ()
    {
        // Calling 'setText' fires DocumentEvents, so we can't assume that
        // a DocumentEvent is caused by the user editing the value.
        if (settingValue)
            return;
        copyToMirror ();
        String val = field.getText ();
        boolean wasChanging = isChanging;
        isChanging = !val.equals (value);
        if (isChanging || wasChanging)
        {
            if (listeners != null)
                listeners.fireValueChanged (this, isChanging);
        }

        // Call the verifier to clear any error message.
        if (verifier != null)
            verifier.verify (val, true);
    }

    /**
    * Checks whether the value that the user has entered is valid.
    * <p>If the user is not editing the value, <code>true</code> is returned.
    * <p>If a verifier has been set, it is called to check the validity of
    * the user-entered value.
    * If no verifier has been set, the value is assumed to be valid.
    * <p>If the user-entered value is valid, it is stored as
    * the value of the component, a value event is fired, and
    * <code>true</code> is returned.
    * If the value is not valid, <code>false</code> is returned.
    * @return <code>false</code> if the user has entered an invalid value,
    * <code>true</code> otherwise.
    * @see #setVerifier(Verifier)
    * @see Verifier
    */
    public boolean checkValue ()
    {
        Event.checkIsEventDispatchThread ();
        if (!isChanging)
            return true;

        String val = field.getText ();
        if (verifier != null)
        {
            val = verifier.verify (val, false);
            if (val == null)
                return false;
        }

        isChanging = false;
        value = val;
        field.setText (value);
        copyToMirror ();
        field.setCaretPosition (0);
        if (listeners != null)
            listeners.fireValueChanged (this, false);
        return true;
    }

    /**
    * Sets the verifier for this component.
    * @param verifier the new verifier
    * @see Verifier
    * @see #checkValue
    */
    public void setVerifier (Verifier verifier)
    {
        this.verifier = verifier;
    }

    public String getValue ()
    {
        return value;
    }

    public void setValue (String val)
    {
        Event.checkIsEventDispatchThread ();
        value = val;
        settingValue = true;
        field.setText (val);
        field.setCaretPosition (0);
        copyToMirror ();
        settingValue = false;
        isChanging = false;
    }

    public void cancelEdit ()
    {
        if (!isChanging)
            return;
        setValue (value);
    }

    public boolean hasValueChanged (String oldValue)
    {
        boolean changed = !field.getText ().equals (oldValue);
        field.setBackground (changed ? Gui.COLOUR_BACKGROUND_UNAPPLIED :
            Gui.COLOUR_BACKGROUND_NORMAL);
        return changed;
    }

    public void displayValue (final String val)
    {
        if (hasFocus)
            throw new IllegalStateException ("displayValue called when hasFocus");
        field.setText (val);
        field.setCaretPosition (0);
    }
}

