/**
 * Class VimKeyEvent
 *
 *  Stores keyCode, character and modifiers.
 *  This class is both used to represent 
 *  and to replay incoming/recorded events
 *
 *  @param event    the native browser event
 */
function VimKeyEvent(event) {
    var evt = new xEvent(event);
    this.keyCode = evt.keyCode;
    this.character = String.fromCharCode(this.keyCode);
    this.modifiers = 0x0;

    if(event.shiftKey) {
        this.character = this.character.toUpperCase();
        this.modifiers = this.modifiers | 0x1;
    } else {
        this.character = this.character.toLowerCase();
    }
    if(event.ctrlKey || this.keyCode == 17) {
        this.modifiers = this.modifiers | 0x2;
    }
    if(event.altKey || this.keyCode == 16) {
        this.modifiers = this.modifiers | 0x4;
    }

    /**
     * @return  a representation for this event, based on the model "Key(code,'character',modifiers)"
     */
    this.toString = function() {
        return "Key("+this.keyCode+",'"+this.character+"',"+this.modifiers+")";
    }
}

/**
 * Class FakeVimKeyEvent
 *
 *  Fakes VimKeyEvent, for the weird
 *  replay usecases, such as up/down/left/right
 *  keypress replay
 */
function FakeVimKeyEvent(character) {
    this.keyCode = 32;
    this.character = character;
    this.modifiers = 0x0;
}

/**
 * Class Vim
 *
 *  a text editor :)
 */
function Vim(ele) {
    this.e = ele;
    
    /**
     * Events history
     */
    this.history = new Array();

    /**
     * Modes:
     *  NORMAL  vim normal mode
     *  INSERT  vim insert mode
     *  PENDING vim key pending mode
     */
    this.mode = 'INSERT';

    /**
     * Recording:
     *  true    vim is currently recording
     *  false   vim is currently not recording
     */
    this.recording = 0;
    this.record = new Array();
    this.buffers = new Array();
    this.buffer = '*';

    /**
     * Pending events
     */
    this.pending = new Array();

    /**
     * Pending count
     */
    this.pendingFigure = -1;

    /**
     * Last seen search pattern
     */
    this.currentPattern = /./;

    /**
     * Last till command
     */
    this.lastTill = '';
    this.lastTillCommand = '';

    /**
     * @return debug info for this vim object (id, position, mode, history)
     */
    this.getInfo = function() {
        result = "Vim("+this.e.id+") position="+this.getCaretPosition()+", mode="+this.mode+"\n";
        result += "history = "+this.history+"\n";
        return result;
    }

    /**
     * Setup and show a message
     * at the bottom of the display
     * 
     * @param message   the message to complain
     */
    this.setBlather = function(message) {
        this.blather = message;
        if(this.blather.length>0) {
            this.commandBox.innerHTML = ' '+this.blather;
            xLeft(this.commandBox, xPageX(this.e));
            xTop(this.commandBox,  xPageY(this.e)+xHeight(this.e) - 15);
            xWidth(this.commandBox, xWidth(this.e)-3);
            xShow(this.commandBox);
        } else {
            xHide(this.commandBox);
        }
    }

    /**
     * Handle an incoming keystroke event.
     *
     *  Resolve the vim object and
     *  fire the key stroke upon it
     *
     *  @param event    the native browser event
     */
    this.vimKeyUp = function(event) {
        var evt = new xEvent(event);
        return evt.target.vimObject.fireKeyStroke(new VimKeyEvent(event), event);
    }

    /**
     * Fire a key stroke.
     *
     *  This methods synces any internal data before
     *  launching the event interpretation.
     *
     *  First clean any error message,
     *  and stop if key is not safe :
     *  [ALTGR], [Mod3] etc.
     *
     *  If in recording mode, store the event in the record buffer.
     *  If in pending mode, store the event in the pending subbuffer for later use.
     *  Else store the event in history
     *   Run applyKeyStroke
     *  If any error occured, show blather message
     *
     *  @param vimEvent the cooked vim event
     *  @param event    the native browser event
     */
    this.fireKeyStroke = function(vimEvent, event) {
        this.setBlather('');

        if(! this.isDeadKey(vimEvent)) {
            // record
            if(this.recording) {
                this.record[this.record.length] = vimEvent;
            }

            // launch key stroke
            if(this.mode == 'PENDING') {
                this.pending[this.pending.length] = vimEvent;
            } else {
                this.history[this.history.length] = vimEvent;
            }
            var result = this.applyKeyStroke(vimEvent, event);

            // display message
            if(this.blather.length>0) {
                this.setBlather(this.blather);
            }
        }
        return result;
    }

    /**
     * Applies a key stroke.
     *
     *  This methods manage key event handling,
     *  according to the current Mode :
     *  - INSERT mode :     applyInsertKeyStroke
     *  - NORMAL mode :     applyNormalKeyStroke
     *  - PENDING mode :    applyPendingKeyStroke
     *  
     *  If the routine called returns true,
     *  it then cancel the event 'browser effect'
     *  e.g. when pressing [j], caret goes down
     *  and no 'j' is printed.
     *
     *  @param vimEvent the cooked vim event
     *  @param event    the native browser event
     */
    this.applyKeyStroke = function(vimEvent, event) {
        var canceled = false;

        if(this.mode == 'INSERT') {
            canceled = canceled || this.applyInsertKeyStroke(vimEvent, event);
        } else if(this.mode == 'NORMAL') {
            canceled = canceled || this.applyNormalKeyStroke(vimEvent);
        } else if(this.mode == 'PENDING') {
            canceled = canceled || this.applyPendingKeyStroke(vimEvent, this.pending[0]);
        } else {
            this.blather += "Unknown mode '"+this.mode+"', failing back to normal mode. ";
            canceled = true;
            this.setMode('NORMAL');
        }

        if(canceled && event) {
            this.cancelEvent(event);
        }
        return(! canceled);
    }

    /**
     * Applies an INSERT mode key stroke.
     *
     *  Mainly:
     *  - Cancel [ESC] keypress event and fall back to NORMAL mode.
     *  - Cancel [TAB] keypress and insert 4 spaces
     *
     *  If in replay mode, the event character
     *  is added 'by hand' to the textarea
     *
     *  @param vimEvent the cooked vim event
     *  @param event    the native browser event
     */
    this.applyInsertKeyStroke = function(vimEvent, event) {
        var canceled = false;
        
        if(vimEvent.keyCode == 27) {
            // [ESC] returns to command mode
            canceled = true;
            var pos = this.getCaretPosition();
            var start = this.getStartOfLine();
            if(pos != start) {
                this.setCaretPosition(this.getCaretPosition()-1);
            }
            this.setMode('NORMAL');

        } else if(vimEvent.keyCode == 9) {
            // [TAB]
            canceled = true;
            var pos = this.getCaretPosition();
            this.e.value = this.e.value.substring(0,pos) + '    ' + this.e.value.substring(pos,this.e.value.length);
            this.setCaretPosition(pos+4);

        } else if(!event) {
            // replaying
            /* if((vimEvent.keyCode >= 37) && (vimEvent.keyCode <= 40)) {
                this.applyNormalKeyStroke(new FakeVimKeyEvent('hklj'.charAt(vimEvent.keyCode-37)));
            } */
            var pos = this.getCaretPosition();
            this.e.value = this.e.value.substring(0,pos) + vimEvent.character + this.e.value.substring(pos,this.e.value.length);
            this.setCaretPosition(pos+1);
        }

        return canceled;
    }

    /**
     * Applies an NORMAL mode key stroke.
     *
     *  - done:        iaAoO0$hlkjGeEwWxXDC 
     *  - to pending:     rtTfFgcd0123456789
     *
     *  @param vimEvent the cooked vim event
     */
    this.applyNormalKeyStroke = function(vimEvent) {
        var canceled = true;
        var pending = new Array();

        // --------------------
        // Insert mode commands
        if(vimEvent.character == 'i') {
            // [i] returns to insert mode
            this.setMode('INSERT');
        } else if(vimEvent.character == 'a') {
            // [a] forwards one character and returns to insert mode
            if(this.getCaretPosition() != this.getEndOfLine()) {
                this.setCaretPosition(this.getCaretPosition()+1);
            }
            this.setMode('INSERT');
        } else if(vimEvent.character == 'A') {
            // [A] forwards to the end of the line and returns to insert mode
            this.setMode('INSERT');
            this.setCaretPosition(this.getEndOfLine());
        } else if(vimEvent.character == 'o') {
            // [o] adds a new line after the current line
            var pos = this.getEndOfLine()+1;
            this.e.value = this.e.value.substring(0,pos) + '\n' + this.e.value.substring(pos,this.e.value.length);
            this.setCaretPosition(pos);
            this.setMode('INSERT');
        } else if(vimEvent.character == 'O') {
            // [o] adds a new line before the current line
            var pos = this.getStartOfLine();
            this.e.value = this.e.value.substring(0,pos) + '\n' + this.e.value.substring(pos,this.e.value.length);
            this.setCaretPosition(pos);
            this.setMode('INSERT');


        // ---------------
        // Motion commands
        } else if(vimEvent.character == '0') {
            // [0] forwards to the start of the line
            this.setCaretPosition(this.getStartOfLine());
        } else if(vimEvent.character == '^') {
            // [^] forwards to the first non-blank character in the line
            var pos2 = this.getEndOfLine();
            var pos1 = this.getStartOfLine();
            while((pos1 < pos2) && this.e.value.substr(pos1,1).match(/\s/)) {
                pos1++;
            }
            this.setCaretPosition(pos1);
        } else if(vimEvent.character == '$') {
            // [$] forwards to the end of the line
            var pos = this.getEndOfLine();
            var index, i;
            for(i = ((this.pendingFigure==-1)?1:this.pendingFigure); i>1; i--) {
                index = this.e.value.indexOf('\n',pos+1);
                if(index == -1) {
                    i = 0;
                } else {
                    pos = index;
                }
            }
            this.pendingFigure = -1;
            this.setCaretPosition(pos);
        } else if(vimEvent.character == 'h') {
            // [h] go backward one character
            this.setCaretPosition(this.getCaretPosition()-1);
        } else if(vimEvent.character == 'l') {
            // [l] go forward one character
            this.setCaretPosition(this.getCaretPosition()+1);
        } else if(vimEvent.character == 'k') {
            // [k] go backward one line
            var pos = this.getCaretPosition();
            var start1 = this.getStartOfLine(pos);
            var l1 = pos-start1;
            var start2 = this.getStartOfLine(start1-1);
            var end2 = this.getEndOfLine(start2);
            var l2 = end2-start2;
            
            this.setCaretPosition(start2 + Math.min(l1,l2));
        } else if(vimEvent.character == 'j') {
            // [j] go forward one line
            var pos = this.getCaretPosition();
            var start1 = this.getStartOfLine(pos);
            var l1 = pos-start1;
            var start2 = this.getEndOfLine(pos)+1;
            var end2 = this.getEndOfLine(start2);
            var l2 = end2-start2;
            
            this.setCaretPosition(start2 + Math.min(l1,l2));
        } else if(vimEvent.character == '-') {
            // [-] goes up one line on the first non-blank character
            this.fireKeyStroke(new FakeVimKeyEvent('k'));
            this.fireKeyStroke(new FakeVimKeyEvent('^'));
        } else if(vimEvent.character == '+') {
            // [+] goes down one line on the first non-blank character
            this.fireKeyStroke(new FakeVimKeyEvent('j'));
            this.fireKeyStroke(new FakeVimKeyEvent('^'));
        } else if(vimEvent.character == '|') {
            // [|] forwards to the Nth character of the line
            var pos2 = this.getEndOfLine();
            var pos1 = this.getStartOfLine();
            var count = 0;

            if(this.pendingFigure!=-1) {
                count = this.pendingFigure-1;
            }
            if(pos2 - pos1 < count) {
                count = pos2 - pos1;
            }
            this.setCaretPosition(pos1+count);
        } else if(vimEvent.character == 'G') {
            // [l] go forward one character
            this.setCaretPosition(this.e.value.length);
            this.setCaretPosition(this.getStartOfLine());
        } else if(vimEvent.character == 'e') {
            // {count} [e] forwards to the end of a word
            var pos = this.getCaretPosition()+1;
            var match, m2, i;

            for(i = ((this.pendingFigure==-1)?1:this.pendingFigure); i>0; i--) {
                if(this.e.value.substr(pos,1).match(/\s/)) {
                    pos += this.e.value.substring( pos,this.e.value.length).search(/\s[^\s]/)+1;
                }
                if(this.e.value.substr(pos,1).match(/\w/)) {
                    match = this.e.value.substring(pos,this.e.value.length).search(/\w[^\w]/);
                } else {
                    match = this.e.value.substring(pos,this.e.value.length).search(/[^\w]\w/);
                    m2 = this.e.value.substr(pos,match+1).search(/[^\s]\s/);
                    if(m2 != -1) {
                        match = m2;
                    }
                }
                pos = match + pos;
            }
            this.pendingFigure = -1;

            this.setCaretPosition(pos);
        } else if(vimEvent.character == 'E') {
            // {count} [E] forwards to the end of a Word
            var pos = this.getCaretPosition()+1;
            var match, i;

            for(i = ((this.pendingFigure==-1)?1:this.pendingFigure); i>0; i--) {
                match = this.e.value.substring(pos,this.e.value.length).search(/[^\s]\s/);
                pos = match + pos;
            }
            this.pendingFigure = -1;

            this.setCaretPosition(pos);
        } else if(vimEvent.character == 'w') {
            // {count} [w] forwards to the start of a word
            var pos = this.getCaretPosition();
            var match, m2, i;

            for(i = ((this.pendingFigure==-1)?1:this.pendingFigure); i>0; i--) {
                if(this.e.value.substr(pos,1).match(/\w/)) {
                    match = this.e.value.substring(pos,this.e.value.length).search(/\w[^\w]/)+1;
                    if(this.e.value.substring(pos+match,this.e.value.length).match(/^\s/)) {
                        match = match+this.e.value.substring(pos+match,this.e.value.length).search(/[^\s]/);
                    }
                } else {
                    match = this.e.value.substring(pos,this.e.value.length).search(/[^\w]\w/)+1;
                    m2 = this.e.value.substr(pos,match).search(/\s/);
                    if(m2 != -1) {
                        match = m2 + this.e.value.substr(pos+m2, match-m2+1).search(/[^\s]/);
                    }
                }
                pos = match + pos;
            }
            this.pendingFigure = -1;

            this.setCaretPosition(pos);
        } else if(vimEvent.character == 'W') {
            // {count} [W] forwards to the start of a Word
            var pos = this.getCaretPosition();
            var match, i;

            for(i = ((this.pendingFigure==-1)?1:this.pendingFigure); i>0; i--) {
                match = this.e.value.substring(pos,this.e.value.length).search(/\s[^\s]/)+1;
                pos = match + pos;
            }
            this.pendingFigure = -1;

            this.setCaretPosition(pos);
        } else if(vimEvent.character == ';') {
            // [;] repeats last till
            if(this.lastTill != '') {
                this.fireKeyStroke(new FakeVimKeyEvent(this.lastTillCommand));
                this.fireKeyStroke(new FakeVimKeyEvent(this.lastTill));
            } else {
                this.blather += "No till command recorded. ";
            }
        } else if(vimEvent.character == ',') {
            // [,] repeats last till in opposite direction
            var origcommand = this.lastTillCommand;
            var command = (
                    (origcommand == 'f')?'F' :
                    (origcommand == 'F')?'f' :
                    (origcommand == 't')?'T' :
                    't'
                );
            if(this.lastTill != '') {
                this.fireKeyStroke(new FakeVimKeyEvent(command));
                this.fireKeyStroke(new FakeVimKeyEvent(this.lastTill));
                this.lastTillCommand = origcommand;
            } else {
                this.blather += "No till command recorded. ";
            }
        } else if(vimEvent.character == 'b') {
            // {count} [b] forwards to the start of a word
            var match, m2, i;
            var orig = this.getCaretPosition();
            var value = reverse_string(this.e.value.substring(0, orig));
            var pos = 0;

            for(i = ((this.pendingFigure==-1)?1:this.pendingFigure); i>0; i--) {
                if(value.substr(pos,1).match(/\s/)) {
                    pos += value.substring( pos,value.length).search(/\s[^\s]/)+1;
                }
                if(value.substr(pos,1).match(/\w/)) {
                    match = value.substring(pos,value.length).search(/\w[^\w]/);
                } else {
                    match = value.substring(pos,value.length).search(/[^\w]\w/);
                    m2 = value.substr(pos,match+1).search(/[^\s]\s/);
                    if(m2 != -1) {
                        match = m2;
                    }
                }
                pos = match + pos + 1;
            }

            this.pendingFigure = -1;
            this.setCaretPosition(orig-pos);

        } else if(vimEvent.character == 'B') {
            // {count} [B] forwards to the start of a Word
            var orig = this.getCaretPosition();
            var value = reverse_string(this.e.value.substring(0, orig));
            var pos = 0;
            var match, i;

            for(i = ((this.pendingFigure==-1)?1:this.pendingFigure); i>0; i--) {
                match = value.substring(pos,value.length).search(/[^\s]\s/) + 1;
                pos = match + pos;
            }
            this.pendingFigure = -1;
            this.setCaretPosition(orig - pos);

        // ---------------
        // Action commands
        } else if(vimEvent.character == 'x') {
            // [x] deletes one char
            var pos = this.getCaretPosition();
            var end = this.getEndOfLine();
            if(pos != end) {
                this.e.value = this.e.value.substring(0,pos) + this.e.value.substring(pos+1,this.e.value.length);
                this.setCaretPosition(pos);
            }
        } else if(vimEvent.character == 'X') {
            // [X] deletes one char backward
            var pos = this.getCaretPosition();
            var start = this.getStartOfLine();
            if(pos != start) {
                this.e.value = this.e.value.substring(0,pos-1) + this.e.value.substring(pos,this.e.value.length);
                this.setCaretPosition(pos-1);
            }
        } else if(vimEvent.character == 'D') {
            // [D] deletes the end of the line
            var pos = this.getCaretPosition();
            this.e.value = this.e.value.substring(0,pos) + 
                this.e.value.substring(this.getEndOfLine(),this.e.value.length);
            this.setCaretPosition(pos);
        } else if(vimEvent.character == 'C') {
            // [C] changes the end of the line
            this.setMode('INSERT');
            var pos = this.getCaretPosition();
            this.e.value = this.e.value.substring(0,pos) + 
                this.e.value.substring(this.getEndOfLine(),this.e.value.length);
            this.setCaretPosition(pos);

        // -----------------------
        // Commands with arguments
        } else if(vimEvent.character == 'q') {
            // [q] starts/stop recording
            if(this.recording) {
                this.recording = 0;
                this.record.length--;
                this.buffers[this.buffer] = this.record;
                this.record = new Array();
            } else {
                this.setMode('PENDING');
            }

        } else if(vimEvent.character == '\@') {
            // [@] replays a recorded buffer
            this.setMode('PENDING');
        } else if(vimEvent.character == 'r') {
            // [r] replaces one character
            this.setMode('PENDING');
        } else if(vimEvent.character == 't') {
            // [t] forwards just before a character
            this.setMode('PENDING');
        } else if(vimEvent.character == 'T') {
            // [T] backwards just after a character
            this.setMode('PENDING');
        } else if(vimEvent.character == 'f') {
            // [f] forwards to one character
            this.setMode('PENDING');
        } else if(vimEvent.character == 'F') {
            // [F] backwards to one character
            this.setMode('PENDING');
        } else if(vimEvent.character == 'g') {
            // [g] goes somewhere
            this.setMode('PENDING');
        } else if(vimEvent.character == 'd') {
            // [d] deletes something
            this.setMode('PENDING');
        } else if(vimEvent.character == 'c') {
            // [d] deletes something
            this.setMode('PENDING');
        } else if(vimEvent.character == '%') {
            // [%] goes to percentage line
            if(this.pendingFigure!=-1) {
                var line = this.e.value.split('\n').length;
                var pos = 0;

                line = (this.pendingFigure * line) / 100 + 1;
                for(i = line; i>1; i--) {
                    index = this.e.value.indexOf('\n',pos+1);
                    if(index == -1) {
                        i = 0;
                    } else {
                        pos = index+1;
                    }
                }
                this.pendingFigure = -1;
                this.setCaretPosition(pos);
            } else {
                this.blather += "[%] Command not implemented yet";
            }

        } else if(this.isNumber(vimEvent)) {
            this.setMode('PENDING');
                
        } else if(vimEvent.character == ':') {
            this.blather += "Command mode not implemented yet";
                
        // --------------------------
        // Mirrored textarea commands
        } else if(vimEvent.keyCode >= 37 && vimEvent.keyCode <= 40) {
            // arrow keys
            canceled = false;
        } else if(vimEvent.keyCode >= 33 && vimEvent.keyCode <= 34) {
            // pageup, pagedown
            canceled = false;
        } else {
            this.blather += "Unknown normal mode key, '"+vimEvent.character+"'. [char: "+vimEvent.keyCode+"] ";
        }

        if(this.mode == 'PENDING') {
            this.pending = new Array();
            this.pending[0] = vimEvent;
            this.history.length--;
        }
        return canceled;
    }

    /**
     * Applies an PENDING mode key stroke.
     *
     *  - done:        rtTfFgcd
     *  - to normal:      [ESC]
     *
     *  @param vimEvent the cooked vim event that last occured
     *  @param srcEvent the cooked vim event that launched pending mode
     */
    this.applyPendingKeyStroke = function(vimEvent, srcEvent) {
        var canceled = true;

        if(vimEvent.keyCode == 27) {
            // [ESC] returns to command mode
            this.pendingFigure = -1
            this.setMode('NORMAL');

        } else if(this.isNumber(srcEvent)) {
            // {count}
            if(! this.isNumber(vimEvent)) {
                this.pendingFigure = this.pendingNumber(0);
                this.pending = new Array();
                this.history[this.history.length] = vimEvent;
                this.setMode('NORMAL');
                this.applyNormalKeyStroke(vimEvent);
            }

        } else if(srcEvent.character == 'q') {
            // [q] starts/stop recording
            this.setMode('NORMAL');
            if(vimEvent.character.length != 1) {
                this.blather += "Wrong buffer name '"+vimEvent.character+"'. ";
            } else {
                this.recording = 1;
                this.buffer = vimEvent.character;
                this.record = new Array();
            }

        } else if(srcEvent.character == '\@') {
            // [@] replays a recorded buffer
            this.setMode('NORMAL');
            if(vimEvent.character.length != 1) {
                this.blather += "Wrong buffer name '"+vimEvent.character+"'. ";
            } else {
                var buffer = this.buffers[vimEvent.character];
                if(buffer) {
                    for(var i=0; i<buffer.length; i++) {
                        this.fireKeyStroke(buffer[i]);
                    }
                } else {
                    this.blather += "Empty buffer, '"+vimEvent.character+"'. ";
                }
            }

        } else if(srcEvent.character == 'r') {
            this.setMode('NORMAL');
            var pos = this.getCaretPosition();
            if(pos != this.getEndOfLine())
                this.e.value = this.e.value.substring(0,pos) + vimEvent.character + this.e.value.substring(pos+1,this.e.value.length);
            this.setCaretPosition(pos);

        } else if(srcEvent.character == 't') {
            this.lastTill = vimEvent.character;
            this.lastTillCommand = srcEvent.character;

            this.setMode('NORMAL');
            var eol = this.e.value.substring(this.getCaretPosition()+1, this.getEndOfLine());
            var match = eol.indexOf(vimEvent.character);
            if(match != -1) {
                this.setCaretPosition(this.getCaretPosition() + match);
            } else {
                this.blather += "Character not found, '"+vimEvent.character+"'. ";
            }
        } else if(srcEvent.character == 'T') {
            this.lastTill = vimEvent.character;
            this.lastTillCommand = srcEvent.character;

            this.setMode('NORMAL');
            var sol = this.e.value.substring(this.getStartOfLine(), this.getCaretPosition());
            sol = reverse_string(sol)
            var match = sol.indexOf(vimEvent.character);
            if(match != -1) {
                this.setCaretPosition(this.getCaretPosition() - match);
            } else {
                this.blather += "Character not found, '"+vimEvent.character+"'. ";
            }
        } else if(srcEvent.character == 'f') {
            this.lastTill = vimEvent.character;
            this.lastTillCommand = srcEvent.character;

            this.setMode('NORMAL');
            var eol = this.e.value.substring(this.getCaretPosition()+1, this.getEndOfLine());
            var match = eol.indexOf(vimEvent.character);
            if(match != -1) {
                this.setCaretPosition(this.getCaretPosition()+1 + match);
            } else {
                this.blather += "Character not found, '"+vimEvent.character+"'. ";
            }
        } else if(srcEvent.character == 'F') {
            this.lastTill = vimEvent.character;
            this.lastTillCommand = srcEvent.character;

            this.setMode('NORMAL');
            var sol = this.e.value.substring(this.getStartOfLine(), this.getCaretPosition());
            sol = reverse_string(sol)
            var match = sol.indexOf(vimEvent.character);
            if(match != -1) {
                this.setCaretPosition(this.getCaretPosition() - (match+1));
            } else {
                this.blather += "Character not found, '"+vimEvent.character+"'. ";
            }

        } else if(srcEvent.character == 'g') {
            // {count} [g] goes somewhere
            if(vimEvent.character == 'g') {
                this.setMode('NORMAL');
                var pos = 0;

                for(i = ((this.pendingFigure==-1)?1:this.pendingFigure); i>1; i--) {
                    index = this.e.value.indexOf('\n',pos+1);
                    if(index == -1) {
                        i = 0;
                    } else {
                        pos = index+1;
                    }
                }
                this.pendingFigure = -1;
                this.setCaretPosition(pos);
            } else if(vimEvent.character == '0') {
                this.setMode('NORMAL');
                this.setCaretPosition(this.getStartOfLine());
            } else if(vimEvent.character == '$') {
                this.setMode('NORMAL');
                var index, i;
                for(i = ((this.pendingFigure==-1)?1:this.pendingFigure); i>1; i--) {
                    index = this.e.value.indexOf('\n',pos+1);
                    if(index == -1) {
                        i = 0;
                    } else {
                        pos = index;
                    }
                }
                this.pendingFigure = -1;
                this.setCaretPosition(pos);
            } else if(vimEvent.character == 'm') {
                this.setMode('NORMAL');
                var pos2 = this.getEndOfLine();
                var pos1 = this.getStartOfLine();
                var width = this.e.cols/2;

                if(pos2 - pos1 > width) {
                    pos1 += width;
                } else {
                    pos1 = pos2;
                }
                this.setCaretPosition(pos1);
            } else {
                this.setMode('NORMAL');
                this.blather += "Unknown g command, '"+vimEvent.character+"'. ";
            }

        } else if(srcEvent.character == 'd' || srcEvent.character == 'c') {
            if(this.isNumber(vimEvent)) {
                if(this.pendingFigure != -1) {
                    this.blather += "Unknown command, {count} "+srcEvent.character+" {count}";
                    this.setMode('NORMAL');
                    this.pendingFigure = -1;
                }

            } else {
                var start = this.getStartOfLine();
                var end = this.getEndOfLine();
                this.setMode('NORMAL');
                if((this.pendingFigure == -1) && (this.pending.length > 2)) {
                    this.pendingFigure = this.pendingNumber();
                }
                if(srcEvent.character == vimEvent.character) {
                    var index = end;
                    var i;

                    for(i = ((this.pendingFigure==-1)?1:this.pendingFigure); i>1; i--) {
                        index = this.e.value.indexOf('\n',index+1);
                        if(index == -1) {
                            i = 0;
                        } else {
                            end = index+1;
                        }
                    }
                    this.pendingFigure = -1;
                    this.e.value = (
                            this.e.value.substring(0,start) + 
                            this.e.value.substring(end+(srcEvent.character == 'd'),this.e.value.length)
                            );
                    this.setCaretPosition(start);
                    this.setMode((srcEvent.character == 'c')?'INSERT':'NORMAL');
                } else if ((vimEvent.character == 'e') || (vimEvent.character == 'E') || 
                           (vimEvent.character == 'w') || (vimEvent.character == 'W') || 
                           (vimEvent.character == '$')) {
                    var pos1 = this.getCaretPosition();
                    this.applyNormalKeyStroke(vimEvent);
                    var pos2 = this.getCaretPosition();
                    this.e.value = this.e.value.substring(0,pos1) + this.e.value.substring(pos2,this.e.value.length);
                    this.setCaretPosition(pos1);
                    this.setMode((srcEvent.character == 'c')?'INSERT':'NORMAL');
                        
                } else {
                    this.blather += "Unknown "+((srcEvent.character == 'c')?'change':'delete')+" key '"+vimEvent.character+"'. ";
                }
            }
        } else {
            this.blather += "Unknown pending mode key, '"+vimEvent.character+"'. ";
        }

        if(this.mode != 'PENDING') {
            this.pending = new Array();
        }
        return canceled;
    }

    /**
     * Dead key detector
     *
     *  Returns true if the vimEvent keycode
     *  is 0. This means that a key not recognized
     *  by the browser but existing in the OS has been
     *  pressed, which may happen for 'MediaPlay' and
     *  siblings 'AudioXxx'.
     *  Inside moz, attributes 'keyCode' and 'which'
     *  get scanned, so keys are handled. Inside IE
     *  and others, 'keyCode' seems sufficient.
     *
     *  @param vimEvent the cooked vim event
     *
     * @return true if the key must not be handled
     */
    this.isDeadKey = function(vimEvent) {
        return (vimEvent.keyCode == 0);
    }


    /**
     * Number event detector
     *
     *  @param vimEvent the cooked vim event
     *
     * @return true if the event character is '0','1',... or '9'
     */
    this.isNumber = function(vimEvent) {
        return ((vimEvent.character.length == 1) &&
                (vimEvent.character >= '0') &&
                (vimEvent.character <= '9'));
    }

    /**
     * Number event collector
     *
     *  Scans the 'pending' event array to collect
     *  a number representation. This representation
     *  will be used as the {count} argument of
     *  a vim command
     *
     *  @param begin    the first index of the slice to scan in the pending array, defaults to 1
     *  @param end      the last index of the slice to scan in the pending array, defaults to length-1
     *
     * @return  a number parsed in the pending array, or -1
     */
    this.pendingNumber = function(begin,end) {
        var number = "0";

        if(arguments.length < 1) {
            begin = 1;
        }
        if(arguments.length < 2) {
            end = this.pending.length-1;
        }
        for(var i=begin; i<end; i++) {
            number += this.pending[i].character;
        }
        if(number.length > 1) {
            number = number.substring(1);
        }
        number = parseInt(number);
        if(!number) return -1;
        return number;
    }

    /**
     * Finds start of line
     *
     *  @param pos  the position to start at
     *
     * @return the first position to the left after a '\n', or 0
     */
    this.getStartOfLine = function(pos) {
        if(arguments.length < 1) {
            pos = this.getCaretPosition();
        }
        pos--;
        while(pos>=0 && this.e.value.charAt(pos) != '\n')
            pos--;
        return pos+1;
    }

    /**
     * Finds end of line
     *
     *  @param pos  the position to start at
     *
     * @return the first position to the right before a '\n', or textarea length
     */
    this.getEndOfLine = function(pos) {
        var match;

        if(arguments.length < 1) {
            pos = this.getCaretPosition();
        }
        match = this.e.value.substring(pos,this.e.value.length).search(/\n/);
        if(match+1) {
            return (pos + match);
        } else {
            return this.e.value.length;
        }
    }

    /**
     * Finds line
     *
     *  @param pos  the position to start at
     *
     * @return the current line's text
     */
    this.getLine = function(pos) {
        if(arguments.length < 1) {
            pos = this.getCaretPosition();
        }
        return(this.e.value.substring(this.getStartOfLine(), this.getEndOfLine()));
    }

    /**
     * Cancels an event
     *
     *  If under a moz clone, use event.preventDefault().
     *  Whatever, set event.returnValue to false and
     *  event.cancelBubble to true to respectively
     *  disable event action and disable event propagation.
     *  
     *  @param event    the native browser event
     */
    this.cancelEvent = function(event) {
        if(event.preventDefault) {
            event.preventDefault(); // Moz
        }
        event.returnValue = false;  // Ie
        event.cancelBubble = true;
    }

    /**
     * Sets the editing mode
     *
     *  @param mode    'INSERT', 'NORMAL', or 'PENDING'
     */
    this.setMode = function(mode) {
        this.mode = mode;
    }

    /**
     * Sets the caret position
     *
     *  @param position    the position to point at
     */
    this.setCaretPosition = function(position) {
        var linenumber;

        linenumber = this.e.value.substr(0,position).match('\n','g');
        if(linenumber) { 
            linenumber = linenumber.length; 
        } else {
            linenumber = 0; 
        }
        this.e.focus();
        this.setSelectionRange(position, position);
        this.e.scrollTop = (linenumber*13-xHeight(this.e)/2);
    }

    /**
     * Gets the caret position
     *
     * @return the current caret position, or 0
     */
    this.getCaretPosition = function() {
        if (this.e.selectionStart) {
            return this.e.selectionStart;
        } else if(this.e.createTextRange) {
            return document.selection.createRange().duplicate().text.length-1;
        }
        return 0;
    }

    /**
     * Select a text range.
     *
     *  @param selectionStart   start point
     *  @param selectionEnd     end point
     *
     * @return the current caret position, or 0
     */
    this.setSelectionRange = function(selectionStart, selectionEnd) {
        if (this.e.setSelectionRange) {
            this.e.focus();
            this.e.setSelectionRange(selectionStart, selectionEnd);
        } else if (this.e.createTextRange) {
            var range = this.e.createTextRange();
            range.collapse(true);
            range.moveEnd('character', selectionEnd);
            range.moveStart('character', selectionStart);
            range.select();
        }
    }

    /**
     * Finish initialization
     */
    this.blather = '';

    this.commandBox = document.createElement('DIV');
    this.commandBox.setAttribute('style', 'border:1px solid black;position:absolute;visibility:hidden;height:12px;background-color:peachpuff;font-family: arial;font-size: 10px;overflow:hidden;color:#303030,top:0px');
    this.e.parentNode.appendChild(this.commandBox);
    xAddEventListener(ele,'keypress',this.vimKeyUp,1);
}

/**
 * Reverse a string
 *
 * @param text  the string to get a reversed copy of
 * @return      the input text, from right to left
 */
function reverse_string(text) {
    if(text) {
        var result='';
        for (var i=text.length-1; i>=0; i--) {
            result += text.charAt(i);
        }
        return result;
    } else {
        return '';
    }
}

/**
 * Initialize a Vim component over an existing textarea
 *
 *  Example use: initVim(document.getElementById('mytextarea'));
 * 
 * @param ele   the document textarea element to add a JSVim event filter to
 */
function initVim(ele) {
    ele.vimObject = new Vim(ele);
}
