import java.awt.*;
import java.awt.event.*;
import java.util.Vector;


/************************************************************************
 * A light-weight component representing a text field which only
 * accepts digits of its currently active number system, or
 * six additional letters, in case of hexadecimal mode.
 * @author <a href=mailto:co1-002@pool.math.tu-berlin.de>Christian
 *         Liebchen</a>
 ************************************************************************/
public class HexIntTextField extends Canvas implements KeyListener {
    
    // ========================================================== constants ===
    
    private static final int FONTSIZE=14;
    private static final int CHARWIDTH=8;
    private static final int DIST=3;
    public static final int BIN=2;
    public static final int OCT=8;
    public static final int DEC=10;
    public static final int HEX=16;
    
    // =================================================== obejct variables ===
    
    private StringBuffer shown=new StringBuffer("");
    private int caretPosition=0;
    private Font font=null;
    private String[] fontnames={"Courier", "Default"};
    private Vector listeners=new Vector();
    private int mode=HEX;
    
    // ===================================================== initialization ===
    
    private void init() {
        setSize(100, FONTSIZE+2*DIST);
        setBackground(Color.WHITE);
        setFocusable(true);
        
        // This is the REASON for this whole class!
        addKeyListener(this);
        
        // Just ask for repaint, after focus has changed -
        //   this suffices, because we query hasFocus at every paint...
        addFocusListener(new FocusEventRepainter());
        
        // Set up an aggressive mouse listener, which pops up a dialog,
        //   after mouse has been pressed on a focussed HexIntTextField
        addMouseListener(new MouseAdapter() {
            public void mousePressed(MouseEvent e) {
                Component target=e.getComponent();
                if (target.hasFocus()) {
                    if (e.getButton()==MouseEvent.BUTTON1) {
                        int caretCandidate=e.getX()/CHARWIDTH;
                        if (caretCandidate>shown.length()) {
                            caretCandidate=shown.length();
                        } else if (caretCandidate<0) {
                            caretCandidate=0;
                        }
                        caretPosition=caretCandidate;
                        target.repaint();
                    }
                }
            }
        });
        GraphicsEnvironment e=GraphicsEnvironment.getLocalGraphicsEnvironment();
        String[] fonts=e.getAvailableFontFamilyNames();
        for (int j=0; j<fontnames.length && font==null; j++) {
            for (int i=0; i<fonts.length && font==null; i++) {
                if (fontnames[j].equalsIgnoreCase(fonts[i])) {
                    font=new Font(fonts[i], Font.BOLD, FONTSIZE);
                }
            }
        }
    }
    
    public HexIntTextField() {
        init();
    }
    
    // ================================================= set/get Value/Text ===
    
    public String getText() {
        return shown.toString();
    }
    
    /********************************************************************
     * Sets the text for this <code>HexIntTextField</code>.
     * @param text the new text for this <code>HexIntTextField</code>
     * @throws IllegalArgumentException if an invalid <code>String</code>
     *         is given, where currently we do only accept
     *         <code>String</code>s encoding integers in binary,
     *         octal, decimal, or hexadecimal representation,
     *         see <code>getMode()</code>, with an optional
     *         minus sign at the beginning
     * @see #getMode()
     ********************************************************************/
    public void setText(String text) throws IllegalArgumentException {
        StringBuffer cand=new StringBuffer("");
        boolean okay=true;
        for (int i=0; i<text.length() && okay; i++) {
            char c=getModifiedChar(text.charAt(i));
            
            // avoid repeated insertChar, because we want the text to
            //   appear at once...
            if (c=='-' && i!=0) {
                okay=false;
            } else if (accept(c)) {
                cand.append(c);
            } else {
                okay=false;
            }
        }
        if (okay) {
            shown=cand;
            caretPosition=shown.length();
            repaint();
        } else {
            Toolkit.getDefaultToolkit().beep();
            throw new IllegalArgumentException("Invalid text: "+text);
        }
    }
    
    public long getValue() {
        if (shown.length()<1) {
            return 0;
        }
        if (shown.length()==1 && shown.charAt(0)=='-') {
            shown.replace(0, 1, "0");
            repaint();
        }
        try {
            return Long.parseLong(shown.toString(), mode);
        } catch (NumberFormatException e) {
            
            // The only problem we can imagine: Strings being tooooooo long!
            if (shown.charAt(0)=='-') {
                return Long.MIN_VALUE;
            } else {
                return Long.MAX_VALUE;
            }
        }
    }
    
    public void setValue(long value) {
        setText(Long.toString(value, mode));
    }
    
    /********************************************************************
     * Provides the current mode of this <code>HexIntTextField</code>.
     * @return the current mode of this <code>HexIntTextField</code>,
     *         ie the base of the number system, but symbolic constants
     *         are used
     * @see #BIN
     * @see #OCT
     * @see #DEC
     * @see #HEX
     ********************************************************************/
    public int getMode() {
        return mode;
    }
    
    /********************************************************************
     * Sets the new mode for this <code>HexIntTextField</code>, ie
     * the base of the number system to be modelled by this text field.
     * @param m the new mode for this <code>HexIntTextField</code>,
     *        where symbolic constants should be used
     * @throws IllegalArgumentException if an unknown mode is given,
     *         where known modes currently are
     *         <code>BIN, OCT, DEC,</code> and <code>HEX</code>
     * @see #BIN
     * @see #OCT
     * @see #DEC
     * @see #HEX
     ********************************************************************/
    public void setMode(int m) throws IllegalArgumentException {
        if (m==BIN || m==OCT || m==DEC || m==HEX) {
            long value=getValue();
            mode=m;
            setValue(value);
            repaint();
        } else {
            throw new IllegalArgumentException("Bad mode given: "+mode);
        }
    }
    
    // ======================================= ActionListener functionality ===
    
    public void addActionListener(ActionListener l) {
        listeners.addElement(l);
    }
    
    public void removeActionListener(ActionListener l) {
        listeners.removeElement(l);
    }
    
    /********************************************************************
     * Tells anyone interested in <code>ActionEvent</code>s occuring on
     * this <code>HexIntTextField</code>, when an event took place.
     * More specifically, calls <code>actionPerformed</code> for
     * the registered listeners.
     * @param command describes the event that took place
     * @see ActionListener#actionPerformed(ActionEvent)
     ********************************************************************/
    public void fireActionEvent(String command) {
        setValue(getValue());
        ActionEvent e=new ActionEvent(this,
                                      ActionEvent.ACTION_PERFORMED,
                                      command);
        for (int i=0; i<listeners.size(); i++) {
            ((ActionListener) (listeners.elementAt(i))).actionPerformed(e);
        }
    }
    
    // ==================================== text manipulation functionality ===
    
    /********************************************************************
     * Provide ability to re-interprete characters before inserting
     * them into our <code>HexIntTextField</code>.
     * @param c the character to be re-interpreted occasionally
     * @return <code>c</code> in most cases,
     *         currently only changes minore letters into capitals
     ********************************************************************/
    private char getModifiedChar(char c) {
        
        // Exploit massively that char is an elementary numerical integer
        // type, so that we have comparison and arithmetic!
        if ('a'<=c && c<='f') {
            return (char) (c-'a'+'A');
        }
        return c;
    }
    
    /********************************************************************
     * Tells whether a given character is allowed as input in the
     * current mode of our <code>HexIntTextField</code>.
     * <br/>
     * Notice that we don't know anything about the position to
     * insert the given character, e.g. a minus in the middle of
     * a number will not make any sense. But as minus signs are
     * accepted <i>somewhere</i> this method will return
     * <code>true</code> for them.
     * @param c the character to be inserted into our field
     * @return true iff <code>c=='-'</code>, <code>c in {'A',...,'F'}</code>
     *         and we are in <code>HEX</code> mode, or
     *         <code>c in {'0',...,MIN(mode, 10)-1}</code>
     ********************************************************************/
    private boolean accept(char c) {
        if ('0'<=c && c<('0'+Math.min(mode, 10))) return true;
        
        // Exploit massively that char is an elementary numerical integer
        // type, so that we have comparison and arithmetic!
        if (mode==HEX && 'A'<=c && c<='F') return true;
        if (c=='-') return true;
        return false;
    }
    
    /********************************************************************
     * Tries to insert a given character at the current caret position.
     * The caret steps one forward.
     * @param c the character to be inserted at the caret position
     * @throws IllegalArgumentException if
     *         <ul>
     *         <li>the modified version of the given character is
     *             not accepted</li>
     *         <li>a minus sign is tried to be inserted elsewhere
     *             but not at the beginning</li>
     *         <li>any sign is tried to be inserted before a
     *             leading minus sign</li>
     *         </ul>
     * @see #getModifiedChar(char)
     * @see #accept(char)
     ********************************************************************/
    public void insertChar(char c) throws IllegalArgumentException {
        char modifiedChar=getModifiedChar(c);
        if (accept(modifiedChar)==false) {
            throw new IllegalArgumentException("Character "+c+" not accepted");
        }
        if (c=='-' && caretPosition!=0) {
            throw new IllegalArgumentException("Minus sign not at beginning");
        }
        if (caretPosition==0 && shown.length()>0 && shown.charAt(0)=='-') {
            throw new IllegalArgumentException("Cannot insert before minus");
        }
        shown.insert(caretPosition, modifiedChar);
        caretPosition++;
        repaint();
    }
    
    /********************************************************************
     * Moves the caret one step left of right.
     * <br/>
     * Notice that this method is robust, ie doesn't move beyond the text field.
     * @param forward moves the caret right, iff true
     ********************************************************************/
    public void stepCaret(boolean forward) {
        if (forward) {
            if (caretPosition<shown.length()) {
                caretPosition++;
                repaint();
            }
        } else {
            if (caretPosition>0) {
                caretPosition--;
                repaint();
            }
        }
    }
    
    /********************************************************************
     * Removes the character left or right beside the current caret position.
     * If <code>forward==false</code> the caret steps one backward.
     * <br/>
     * Notice that this method is robust, ie doesn't erase outside
     * text field.
     * @param forward removes the character right of the caret, iff true
     ********************************************************************/
    public void removeChar(boolean forward) {
        if (forward) {
            if (caretPosition<shown.length()) {
                shown.deleteCharAt(caretPosition);
                repaint();
            }
        } else {
            if (caretPosition>0) {
                caretPosition--;
                shown.deleteCharAt(caretPosition);
                repaint();
            }
        }
    }
    
    public void moveHome() {
        if (caretPosition>0) {
            caretPosition=0;
            repaint();
        }
    }
    
    public void moveEnd() {
        if (caretPosition<shown.length()) {
            caretPosition=shown.length();
            repaint();
        }
    }
    
    // ========================================= KeyListener implementation ===
    
    /********************************************************************
     * Location of our main work. Splits into at most one method of
     * navigation, erasing, actionning, or insertion functionality.
     * @see #moveEnd()
     * @see #moveHome()
     * @see #stepCaret(boolean)
     * @see #removeChar(boolean)
     * @see #fireActionEvent(String)
     * @see #insertChar(char)
     ********************************************************************/
    public void keyPressed(KeyEvent e) {
        
        // Only few keys are interpreted specially...
        switch (e.getKeyCode()) {
            
        // We support elementary navigation within our field...
        case KeyEvent.VK_END : moveEnd(); break;
        case KeyEvent.VK_HOME : moveHome(); break;
        case KeyEvent.VK_LEFT : stepCaret(false); break;
        case KeyEvent.VK_RIGHT : stepCaret(true); break;
            
        // Moreover, we provide primitive erasing functionality...
        case KeyEvent.VK_DELETE : removeChar(true); break;
        case KeyEvent.VK_BACK_SPACE : removeChar(false); break;
        
        // As we do want to generate ActionEvents, we must encounter ENTER...
        case KeyEvent.VK_ENTER : fireActionEvent("ENTER"); break;
            
        // SHIFT and CAPS_LOCK generate KeyEvents which we just ignore...
        case KeyEvent.VK_SHIFT : break;
        case KeyEvent.VK_CAPS_LOCK : break;
            
        // Any other key is tried to be inserted into our StringBuffer!
        default : try {
                insertChar(e.getKeyChar());
            } catch (IllegalArgumentException ex) {
                Toolkit.getDefaultToolkit().beep();
            }
        }
    }
    
    
    /********************************************************************
     * Without effect.
     ********************************************************************/
    public void keyTyped(KeyEvent e) { }
    
    /********************************************************************
     * Without effect.
     ********************************************************************/
    public void keyReleased(KeyEvent e) { }
    
    // ================================================= GUI representation ===
    
    /********************************************************************
     * Paints this <code>Canvas</code> to appear as a text field!
     ********************************************************************/
    public void paint(Graphics g) {
        
        // We have to paint the whole canvas.
        // Our background has been set to WHITE.
        // Hence, we still need a border, whoms color
        //   depends on whether we have focus, or not...
        g.setColor(hasFocus() ? Color.BLACK : Color.LIGHT_GRAY);
        Dimension d=getSize();
        g.drawRect(0, 0, d.width-1, d.height-1);
        
        // Then we must paint every character of our StringBuffer...
        g.setColor(Color.BLACK);
        g.setFont(font);
        for (int i=0; i<shown.length(); i++) {
            g.drawChars(new char[] {shown.charAt(i)}, 0, 1,
                        DIST+i*CHARWIDTH, d.height-(DIST+1));
        }
        
        // Finally, we provide a small caret at the appropriate position...
        g.drawLine(DIST+caretPosition*CHARWIDTH, 2*DIST,
                   DIST+caretPosition*CHARWIDTH, d.height-(DIST+1));
    }
}

