// 
//! MarkdownDeep - http://www.toptensoftware.com/markdowndeep
//! Copyright (C) 2010-2011 Topten Software
// 
//   Licensed under the Apache License, Version 2.0 (the "License"); you may not use this product 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.
//

/*
Usage:

// 1. Create the editor an bind to a text area, output div and an optional source view div
        - text area: the text area that user types to.
        - output div: a div where the transformed html will be displayed
        - source view view: an optional div where a "source" view of the rendered html will be placed
      
    var editor=new MarkdownDeepEditor.Editor(textarea_element, output_div, source_div)
    
// 2. Optionally set options

        editor.disableShortCutKeys=true;    // Disable Ctrl+B, Ctrl+I etc...
        editor.disableAutoIndent=true;      // Disable auto indent on enter key
        editor.disableTabHandling=true;     // Disable tab/shift+tab for indent
    
// 3. Optionally install hooks
    
        editor.onPreTransform=function(editor, markdown) {}
        editor.onPostTransform=function(editor, html) {}
        editor.onPostUpdateDom=function(editor) {}

// 4. Optionally create a toolbar/UI that calls editor.InvokeCommand(cmd) where cmd is one of:        

        - "undo",
        - "redo",
        - "bold",
        - "italic",
        - "heading",
        - "code",
        - "ullist",
        - "ollist",
        - "indent",
        - "outdent",
        - "link",
        - "img",
        - "hr",
        - "h0",
        - "h1",
        - "h2",
        - "h3",
        - "h4",
        - "h5",
        - "h6"
        
      eg: editor.InvokeCommand("heading") to toggle heading style of selection
    
*/

var MarkdownDeepEditor=new function(){

    // private:priv.
    // private:.m_*
    // private:.m_listType
    // private:.m_prefixLen
    

    var ie=false;
    
    // Various keycodes
    var keycode_tab = 9;
    var keycode_enter = 13;
    var keycode_pgup = 33;
    var keycode_pgdn = 34;
    var keycode_home = 36;
    var keycode_end = 35;
    var keycode_left = 37;
    var keycode_right = 39;
    var keycode_up = 38;
    var keycode_down = 40;
    var keycode_backspace = 8;
    var keycode_delete = 46;
    
    // Undo modes for the undo stack
    var undomode_unknown = 0;
    var undomode_text = 1;
    var undomode_erase = 2;
    var undomode_navigate = 3;
    var undomode_whitespace = 4;
    
    // Shortcut keys Ctrl+key
    var shortcut_keys={
        "Z": "undo",
        "Y": "redo",
        "B": "bold",
        "I": "italic",
        "H": "heading",
        "K": "code",
        "U": "ullist",
        "O": "ollist",
        "Q": "indent",
        "E": "outdent",
        "L": "link",
        "G": "img",
        "R": "hr",
        "0": "h0",
        "1": "h1",
        "2": "h2",
        "3": "h3",
        "4": "h4",
        "5": "h5",
        "6": "h6"
    }

    function starts_with(str, match)
    {
        return str.substr(0, match.length)==match;
    }
    
    function ends_with(str, match)
    {
        return str.substr(-match.length)==match;
    }

    function is_whitespace(ch)
    {
        return (ch==' ' || ch=='\t' || ch=='\r' || ch=='\n');
    }
    
    function is_crlf(ch)
    {
        return (ch=='\r' || ch=='\n');
    }
    
    function trim(str)
    {
        var i=0;
        var l=str.length;
        
        while (i<l && is_whitespace(str.charAt(i)))
            i++;
        while (l-1>i && is_whitespace(str.charAt(l-1)))
            l--;
            
        return str.substr(i, l-i);
    }


    // Helper for binding events
    function BindEvent(obj, event, handler)
    {
        if (obj.addEventListener)
        {
            obj.addEventListener(event, handler, false);
        }
        else if (obj.attachEvent)
        {
            obj.attachEvent("on"+event, handler);
        }
    }
    
    // Helper for unbinding events
    function UnbindEvent(obj, event, handler)
    {
        if (obj.removeEventListener)
        {
            obj.removeEventListener(event, handler, false);
        }
        else if (obj.detachEvent)
        {
            obj.detachEvent("on"+event, handler);
        }
    }
    
    function PreventEventDefault(event)
    {
        if (event.preventDefault)
        {
            event.preventDefault();
        }
        if (event.cancelBubble!==undefined)
        {
            event.cancelBubble=true;
            event.keyCode=0;
            event.returnValue=false;
        }
        return false;
    }
    
    function offsetToRangeCharacterMove(el, offset) 
    {
        return offset - (el.value.slice(0, offset).split("\r\n").length - 1);
    }

    // EditorState represents the initial and final state of an edit
    function EditorState()
    {
    }

    priv=EditorState.prototype;

    priv.InitFromTextArea=function(textarea)
    {
        this.m_textarea=textarea;
        if (ie)
        {
            var sel=document.selection.createRange();
            var temp=sel.duplicate();
            temp.moveToElementText(textarea);
            var basepos=-temp.moveStart('character', -10000000);
            
            this.m_selectionStart = -sel.moveStart('character', -10000000)-basepos;
            this.m_selectionEnd = -sel.moveEnd('character', -10000000)-basepos;
            this.m_text=textarea.value.replace(/\r\n/gm,"\n");
        }
        else
        {
            this.m_selectionStart = textarea.selectionStart;
            this.m_selectionEnd = textarea.selectionEnd;
            this.m_text=textarea.value;
        }
    }
    
    priv.Duplicate=function()
    {
        var other=new EditorState();
        other.m_textarea=this.m_textarea;
        other.m_selectionEnd=this.m_selectionEnd;
        other.m_selectionStart=this.m_selectionStart;
        other.m_text=this.m_text;
        return other;
    }
    
    priv.Apply=function()
    {
        if (ie)
        {
            this.m_textarea.value=this.m_text;
            this.m_textarea.focus();
            var r=this.m_textarea.createTextRange();
            r.collapse(true);
            r.moveEnd("character", this.m_selectionEnd);
            r.moveStart("character", this.m_selectionStart);
            r.select();
        }
        else
        {
            // Set the new text 
            var scrollTop=this.m_textarea.scrollTop;
            this.m_textarea.value=this.m_text;
            this.m_textarea.focus();
            this.m_textarea.setSelectionRange(this.m_selectionStart, this.m_selectionEnd);
            this.m_textarea.scrollTop=scrollTop;
        }
    }
    
    priv.ReplaceSelection=function(str)
    {
        this.m_text=this.m_text.substr(0, this.m_selectionStart) + str + this.m_text.substr(this.m_selectionEnd);
        this.m_selectionEnd=this.m_selectionStart + str.length;
    }

    function adjust_pos(pos2, editpos, del, ins)
    {
        if (pos2<editpos)
            return pos2;
        return pos2<editpos+del ? editpos : pos2 + ins - del;
    }
    
    priv.ReplaceAt=function(pos, len, str)
    {
        this.m_text=this.m_text.substr(0, pos) + str + this.m_text.substr(pos+len);
        this.m_selectionStart=adjust_pos(this.m_selectionStart, pos, len, str.length);
        this.m_selectionEnd=adjust_pos(this.m_selectionEnd, pos, len, str.length);
    }
    
    priv.getSelectedText=function()
    {
        return this.m_text.substr(this.m_selectionStart, this.m_selectionEnd-this.m_selectionStart);
    }
    
    priv.InflateSelection=function(ds, de)
    {
        this.m_selectionEnd+=de;
        this.m_selectionStart-=ds;
    }
    
    priv.PreceededBy=function(str)
    {
        return this.m_selectionStart >= str.length && this.m_text.substr(this.m_selectionStart-str.length, str.length)==str;
    }
    
    priv.FollowedBy=function(str)
    {
        return this.m_text.substr(this.m_selectionEnd, str.length)==str;
    }
    
    priv.TrimSelection=function()
    {
        while (is_whitespace(this.m_text.charAt(this.m_selectionStart)))
            this.m_selectionStart++;
        while (this.m_selectionEnd>this.m_selectionStart && is_whitespace(this.m_text.charAt(this.m_selectionEnd-1)))
            this.m_selectionEnd--;
    }
    
    priv.IsStartOfLine=function(pos)
    {
        return pos==0 || is_crlf(this.m_text.charAt(pos-1));
    }
    
    priv.FindStartOfLine=function(pos)
    {
        // Move start of selection back to line start
        while (pos>0 && !is_crlf(this.m_text.charAt(pos-1)))
        {  
            pos--;
        }
        return pos;
    }    
    
    priv.FindEndOfLine=function(pos)
    {
        while (pos<this.m_text.length && !is_crlf(this.m_text.charAt(pos)))
        {
            pos++;
        }
        return pos;
    }
    
    priv.FindNextLine=function(pos)
    {
        return this.SkipEol(this.FindEndOfLine(pos));
    }
    
    priv.SkipWhiteSpace=function(pos)
    {
        while (pos<this.m_text.length && is_whitespace(this.m_text.charAt(pos)))
            pos++;
        return pos;
    }
    
    priv.SkipEol=function(pos)
    {
        if (this.m_text.substr(pos, 2)=="\r\n")
            return pos+2;
        if (is_crlf(this.m_text.charAt(pos)))
            return pos+1;
        return pos;
    }
    
    priv.SkipPreceedingEol=function(pos)
    {
        if (pos>2 && this.m_text.substr(pos-2, 2)=="\r\n")
            return pos-2;
        if (pos>1 && is_crlf(this.m_text.charAt(pos-1)))
            return pos-1;
        return pos;
    }
    
    priv.SelectWholeLines=function()
    {
        // Move selection to start of line
        this.m_selectionStart=this.FindStartOfLine(this.m_selectionStart);
        
        // Move end of selection to start of the next line
        if (!this.IsStartOfLine(this.m_selectionEnd))
        {
            this.m_selectionEnd=this.SkipEol(this.FindEndOfLine(this.m_selectionEnd));
        }
    }
    
    priv.SkipPreceedingWhiteSpace=function(pos)
    {
        while (pos>0 && is_whitespace(this.m_text.charAt(pos-1)))
        {
            pos--;
        }
        return pos;
    }
    
    priv.SkipFollowingWhiteSpace=function(pos)
    {
        while (is_whitespace(this.m_text.charAt(pos)))
        {
            pos++;
        }
        return pos;
    }
    priv.SelectSurroundingWhiteSpace=function()
    {
        this.m_selectionStart=this.SkipPreceedingWhiteSpace(this.m_selectionStart);
        this.m_selectionEnd=this.SkipFollowingWhiteSpace(this.m_selectionEnd);
    }
    
    priv.CheckSimpleSelection=function()
    {
        var text=this.getSelectedText();
        var m=text.match(/\n[ \t\r]*\n/);
        
        if (m)
        {
            alertEx("Bitte mache eine Auswahl, die keinen Zeilenumbruch einschließt", "warning");
            return false;
        }
        
        return true;
    }

    // Check if line is completely blank    
    priv.IsBlankLine=function(p)
    {
        var len=this.m_text.length;
        for (var i=p; i<len; i++)
        {
            var ch=this.m_text[i];
            if (is_crlf(ch))
                return true;
            if (!is_whitespace(this.m_text.charAt(i)))
                return false;
        }
        
        return true;
    }
    
    priv.FindStartOfParagraph=function(pos)
    {
        var savepos=pos;
        
        // Move to start of first line
        pos=this.FindStartOfLine(pos);
        
        if (this.IsBlankLine(pos))
            return pos;

        // Move to first line after blank line
        while (pos>0)
        {
            var p=this.FindStartOfLine(this.SkipPreceedingEol(pos));
            if (p==0)
                break;
            if (this.IsBlankLine(p))
                break;
            pos=p;
        }
        
        // Is it a list?
        if (this.DetectListType(pos).m_prefixLen!=0)
        {
            // Do it again, but stop at line with list prefix
            pos=this.FindStartOfLine(savepos);
            
            // Move to first line after blank line
            while (pos>0)
            {
                if (this.DetectListType(pos).m_prefixLen!=0)
                    return pos;
                    
                // go to line before
                pos=this.FindStartOfLine(this.SkipPreceedingEol(pos));
            }
        }
        
        return pos;
    }
    
    priv.FindEndOfParagraph=function(pos)
    {
        // Skip all lines that aren't blank
        while (pos<this.m_text.length)
        {
            if (this.IsBlankLine(pos))
                break;
                
            pos=this.FindNextLine(pos);
        }
        
        return pos;
    }
    
    // Select the paragraph
    priv.SelectParagraph=function()
    {
        this.m_selectionStart=this.FindStartOfParagraph(this.m_selectionStart);
        this.m_selectionEnd=this.FindEndOfParagraph(this.m_selectionStart);        
    }
    
    // Starting at position pos, return the list type
    // returns { m_listType, m_prefixLen } 
    priv.DetectListType=function(pos)
    {
        var prefix=this.m_text.substr(pos, 10);
        var m=prefix.match(/^\s{0,3}(\*|\d+\.)(?:\ |\t)*/);
        if (!m)
            return {m_listType:"", m_prefixLen:0};
            
        if (m[1]=='*')
            return {m_listType:"*", m_prefixLen:m[0].length};
        else
            return {m_listType:"1", m_prefixLen:m[0].length};
    }
    
    
    

    // Editor
    function Editor(textarea, div_html)
    {
        // Is it IE?
        if (!textarea.setSelectionRange)
        {
            ie=true;
        }
    
        // Initialize
        this.m_lastContent=null;
        this.m_undoStack=[];
        this.m_undoPos=0;
        this.m_undoMode=undomode_navigate;
        this.Markdown=new MarkdownDeep.Markdown();
        this.Markdown.SafeMode=false;
        this.Markdown.ExtraMode=true;
        this.Markdown.NewWindowForLocalLinks=true;
        this.Markdown.NewWindowForExternalLinks=true;
        
        // Store DOM elements
        this.m_textarea=textarea;
        this.m_divHtml=div_html;

        // Bind events
        var ed=this;
        BindEvent(textarea, "keyup", function(){ed.onMarkdownChanged();});
        BindEvent(textarea, "keydown", function(e){return ed.onKeyDown(e);});
        BindEvent(textarea, "paste", function(){ed.onMarkdownChanged();});
        BindEvent(textarea, "input", function(){ed.onMarkdownChanged();});
        BindEvent(textarea, "mousedown", function(){ed.SetUndoMode(undomode_navigate);});

        // Do initial update
        this.onMarkdownChanged();
    }

    var priv=Editor.prototype;
    var pub=Editor.prototype;
    
    
    priv.onKeyDown=function(e)
    {
        var newMode=null;
        var retv=true;
        
        // Normal keys only
        if(e.ctrlKey || e.metaKey)
        {
            var key=String.fromCharCode(e.charCode||e.keyCode);
                    
            // Built in short cut key?
            if (!this.disableShortCutKeys && shortcut_keys[key]!=undefined)
            {
                this.InvokeCommand(shortcut_keys[key]);
                return PreventEventDefault(e);
            }
            
            // Standard keys
            switch (key)
            {
                case "V":   // Paste
                    newMode=undomode_text;
                    break;
                    
                case "X":   // Cut
                    newMode=undomode_erase;
                    break;
            }
        }
        else
        {
            switch (e.keyCode)
            {
                case keycode_tab:
                    if (!this.disableTabHandling)
                    {
                        this.InvokeCommand(e.shiftKey ? "untab" : "tab");
                        return PreventEventDefault(e);
                    }
                    else
                    {
                        newMode=undomode_text;
                    }
                    break;
                        
            
                case keycode_left:
                case keycode_right:
                case keycode_up:
                case keycode_down:
                case keycode_home:
                case keycode_end:
                case keycode_pgup:
                case keycode_pgdn:
                    // Navigation mode
                    newMode=undomode_navigate;
                    break;

                case keycode_backspace:
                case keycode_delete:
                    // Delete mode
                    newMode=undomode_erase;
                    break;
                
                case keycode_enter:
                    // New lines mode
                    newMode=undomode_whitespace;
                    break;
                
                default:
                    // Text mode
                    newMode=undomode_text;
            }
        }

        if (newMode!=null)
            this.SetUndoMode(newMode);

        // Special handling for enter key
        if (!this.disableAutoIndent)
        {
            if (e.keyCode==keycode_enter && (!ie || e.ctrlKey))
            {
                this.IndentNewLine();
            }
        }
    } 

    priv.SetUndoMode=function(newMode)
    {
        // Same mode?
        if (this.m_undoMode==newMode)
            return;
            
        // Enter new mode, after capturing current state
        this.m_undoMode=newMode;
        
        // Capture undo state
        this.CaptureUndoState();
    }

    
    priv.CaptureUndoState=function()
    {
        // Store a copy on the undo stack
        var state=new EditorState();
        state.InitFromTextArea(this.m_textarea);
        this.m_undoStack.splice(this.m_undoPos, this.m_undoStack.length-this.m_undoPos, state);        
        this.m_undoPos=this.m_undoStack.length;
    }
    
    priv.onMarkdownChanged=function(bCreateUndoUnit)
    {
        // Get the markdown, see if it's changed
        var new_content=this.m_textarea.value;
        if (new_content===this.m_lastContent && this.m_lastContent!==null)
	        return;
	        
    	// Call pre hook
    	if (this.onPreTransform)
    	    this.onPreTransform(this, new_content);
    
        // Transform
        var output=this.Markdown.Transform(new_content);

        // Call post hook
    	if (this.onPostTransform)
    	    this.onPostTransform(this, output);

    	// Update the DOM
        if (this.m_divHtml)
            this.m_divHtml.innerHTML=output;
            /*
        if (this.m_divSource)
        {
            this.m_divSource.innerHTML="";
            this.m_divSource.appendChild(document.createTextNode(output));
        }
        */
        
        // Call post update dom handler
        if (this.onPostUpdateDom)
            this.onPostUpdateDom(this);

        // Save previous content
        this.m_lastContent=new_content;
    }

    // Public method, should be called by client code if any of the MarkdownDeep
    // transform options have changed
    pub.onOptionsChanged=function()
    {
        this.m_lastContent=null;
        this.onMarkdownChanged();
    }
    
    pub.cmd_undo=function()
    {
        if (this.m_undoPos > 0)
        {
            // Capture current state at end of undo buffer.
            if (this.m_undoPos==this.m_undoStack.length)
            {
                this.CaptureUndoState();
                this.m_undoPos--;
            }

            this.m_undoPos--;
            this.m_undoStack[this.m_undoPos].Apply();
            this.m_undoMode=undomode_unknown;

            // Update markdown rendering
            this.onMarkdownChanged();
        }
    }
    
    pub.cmd_redo=function()
    {
        if (this.m_undoPos+1 < this.m_undoStack.length)
        {
            this.m_undoPos++;
            this.m_undoStack[this.m_undoPos].Apply();
            this.m_undoMode=undomode_unknown;

            // Update markdown rendering
            this.onMarkdownChanged();

            // We're back at the current state            
            if (this.m_undoPos==this.m_undoStack.length-1)
            {
                this.m_undoStack.pop();
            }
        }
    }
    
    priv.setHeadingLevel=function(state, headingLevel)
    {
        // Select the entire heading
        state.SelectParagraph();
        state.SelectSurroundingWhiteSpace();
        
        // Get the selected text
        var text=state.getSelectedText();
        
        // Trim all whitespace
        text=trim(text);

        var currentHeadingLevel=0;
        var m=text.match(/^(\#+)(.*?)(\#+)?$/);
        if (m)
        {
            text=trim(m[2]);
            currentHeadingLevel=m[1].length;
        }
        else
        {
            m=text.match(/^(.*?)(?:\r\n|\n|\r)\s*(\-*|\=*)$/);
            if (m)
            {
                text=trim(m[1]);
                currentHeadingLevel=m[2].charAt(0)=="=" ? 1 : 0;
            }
            else
            {
                // Remove blank lines        
                text=text.replace(/(\r\n|\n|\r)/gm,"");
                currentHeadingLevel=0;
            }
        }
        
        if (headingLevel==-1)
            headingLevel=(currentHeadingLevel+1) % 4;
        
        // Removing a heading
        var selOffset=0;
        var selLen=0;
        if (headingLevel==0)
        {
            // Deleting selection
            if (text=="Heading")
            {
                state.ReplaceSelection("");
                return true;
            }
            
            selLen=text.length;
            selOffset=0;
        }
        else
        {
            if (text=="")
                text="Heading";

            selOffset=headingLevel+1;
            selLen=text.length;
                
            var h="";
            for (var i=0; i<headingLevel; i++)
                h+="#";
                
            text=h + " " + text + " " + h;
            
        }
        
        // Require blank after
        text+="\n\n";

        if (state.m_selectionStart!=0)
        {
            text="\n\n" + text;
            selOffset+=2;
        }

        // Replace text
        state.ReplaceSelection(text);
        
        // Update selection
        state.m_selectionStart+=selOffset;
        state.m_selectionEnd=state.m_selectionStart + selLen;

        return true;
    }

    pub.cmd_heading = function(state) {
        return this.setHeadingLevel(state, -1);
    };

    pub.cmd_h0 = function(state) {
        return this.setHeadingLevel(state, 0);
    };

    pub.cmd_h1 = function(state) {
        return this.setHeadingLevel(state, 1);
    };

    pub.cmd_h2 = function(state) {
        return this.setHeadingLevel(state, 2);
    };

    pub.cmd_h3 = function(state) {
        return this.setHeadingLevel(state, 3);
    };

    pub.cmd_h4 = function(state) {
        return this.setHeadingLevel(state, 4);
    };

    pub.cmd_h5 = function(state) {
        return this.setHeadingLevel(state, 5);
    };

    pub.cmd_h6 = function(state) {
        return this.setHeadingLevel(state, 6);
    };

    priv.IndentCodeBlock=function(state, indent)
    {
        // Make sure whole lines are selected
        state.SelectWholeLines();
        
        // Get the text, split into lines 
        var lines=state.getSelectedText().split("\n");
        
		// Convert leading tabs to spaces   
        for (var i=0; i<lines.length; i++)
        {
			if (lines[i].charAt(0)=="\t")
			{
				var newLead="";
				var p=0;
				while (lines[i].charAt(p)=="\t")
				{
					newLead+="    ";
					p++;
				}
				
				var newLine=newLead + lines[i].substr(p);
				lines.splice(i, 1, newLine);
			}
        }

        // Toggle indent/unindent?
        if (indent===null)
        {
            var i;
            for (i=0; i<lines.length; i++)
            {
                // Blank lines are allowed
                if (trim(lines[i])=="")
                    continue;
                 
				// Convert leading tabs to spaces   
				if (lines[i].charAt(0)=="\t")
				{
					var newLead="";
					var p=0;
					while (lines[i].charAt(p)=="\t")
					{
						newLead+="    ";
						p++;
					}
					
					var newLine=newLead + lines[i].substr(i);
					lines.splice(i, 1, newLine);
				}
					
                // Tabbed line
                if (!starts_with(lines[i], "    "))
                    break;
            }

            // Are we adding or removing indent
            indent=i!=lines.length;
        }
        
        // Apply the changes
        for (var i=0; i<lines.length; i++)
        {
            // Blank line?
            if (trim(lines[i])=="")
                continue;
                
            // Tabbed line
            var newline=lines[i];
            if (indent)
            {
                newline="    " + lines[i];
            }
            else
            {
                if (starts_with(lines[i], "\t"))
                    newline=lines[i].substr(1);
                else if (starts_with(lines[i], "    "))
                    newline=lines[i].substr(4);
            }
            
            lines.splice(i, 1, newline);
        }
        
        // Replace
        state.ReplaceSelection(lines.join("\n"));
    }
    
    // Code
    pub.cmd_code=function(state)
    {
        // Cursor on a blank line?
		if (state.m_selectionStart==state.m_selectionEnd)
		{
			var line=state.FindStartOfLine(state.m_selectionStart);
			if (state.IsBlankLine(line))
			{
				state.SelectSurroundingWhiteSpace();
				state.ReplaceSelection("\n\n    Code\n\n");
				state.m_selectionStart+=6;
				state.m_selectionEnd=state.m_selectionStart + 4;
				return true;
			}
		}       
	
        // If the current text is preceeded by a non-whitespace, or followed by a non-whitespace
        // then do an inline code
        if (state.getSelectedText().indexOf("\n")<0)
        {
            // Expand selection to include leading/trailing stars
            state.TrimSelection();
            if (state.PreceededBy("`"))
                state.m_selectionStart--;
            if (state.FollowedBy("`"))
                state.m_selectionEnd++;
            return this.bold_or_italic(state, "`");
        }
        
        this.IndentCodeBlock(state, null);    
        return true;        
    }
    
    pub.cmd_tab=function(state)
    {
        if (state.getSelectedText().indexOf("\n")>0)
        {
            this.IndentCodeBlock(state, true);
        }
        else
        {
            // If we're in the leading whitespace of a line
            // insert spaces instead of an actual tab character
            var lineStart=state.FindStartOfLine(state.m_selectionStart);
            var p;
            for (p=lineStart; p<state.m_selectionStart; p++)
            {
                if (state.m_text.charAt(p)!=' ')
                    break;
            }
            
            // All spaces?
            if (p==state.m_selectionStart)
            {
                var spacesToNextTabStop=4-((p-lineStart)%4);
                state.ReplaceSelection("    ".substr(0, spacesToNextTabStop));
            }
            else
            {  
                state.ReplaceSelection("\t");
            }
            state.m_selectionStart=state.m_selectionEnd;
        }
        return true;
    }
    
    pub.cmd_untab=function(state)
    {
        if (state.getSelectedText().indexOf("\n")>0)
        {
            this.IndentCodeBlock(state, false);
            return true;
        }
        return false;
    }
    
    priv.bold_or_italic=function(state, marker)
    {
        var t=state.m_text;
        var ml=marker.length;
        
        // Work out if we're adding or removing bold markers
        var text=state.getSelectedText();
        if (starts_with(text, marker) && ends_with(text, marker))
        {
            // Remove 
            state.ReplaceSelection(text.substr(ml, text.length-ml*2));
        }
        else
        {
            // Add
            state.TrimSelection();
            text=state.getSelectedText();
            if (!text)
                text="text";
            else
                text=text.replace(/(\r\n|\n|\r)/gm,"");
            state.ReplaceSelection(marker + text + marker);
            state.InflateSelection(-ml, -ml);
        }
        return true;
    }
    
    // Bold
    pub.cmd_bold=function(state)
    {
        if (!state.CheckSimpleSelection())
            return false;
        state.TrimSelection();
            
        // Expand selection to include leading/trailing stars
        if (state.PreceededBy("**"))
            state.m_selectionStart-=2;
        if (state.FollowedBy("**"))
            state.m_selectionEnd+=2;
            
        return this.bold_or_italic(state, "**");
    }
    
    // Italic
    pub.cmd_italic=function(state)
    {
        if (!state.CheckSimpleSelection())
            return false;
        state.TrimSelection();
            
        // Expand selection to include leading/trailing stars
        if ((state.PreceededBy("*") && !state.PreceededBy("**")) || state.PreceededBy("***"))
            state.m_selectionStart-=1;
        if ((state.FollowedBy("*") && !state.PreceededBy("**")) || state.FollowedBy("***"))
            state.m_selectionEnd+=1;
            
        return this.bold_or_italic(state, "*");
    }
    
    priv.indent_or_outdent=function(state, outdent)
    {
        if (false && state.m_selectionStart==state.m_selectionEnd)
        {
            state.SelectSurroundingWhiteSpace();
            state.ReplaceSelection("\n\n> Quote\n\n");
            state.m_selectionStart+=4;
            state.m_selectionEnd=state.m_selectionStart+5;
            return true;
        }
        
        // Make sure whole lines are selected
        state.SelectWholeLines();
        
        // Get the text, split into lines and check if all lines
        // are indented
        var lines=state.getSelectedText().split("\n");
        
        // Apply the changes
        for (var i=0; i<lines.length-1; i++)
        {
            // Tabbed line
            var newline=lines[i];
            if (outdent)
            {
                if (starts_with(lines[i], "> "))
                    newline=lines[i].substr(2);
            }
            else
            {
                newline="> " + lines[i];
            }
            
            lines.splice(i, 1, newline);
        }
        
        // Replace
        state.ReplaceSelection(lines.join("\n"));
        
        return true;        
    }
    
    // Quote
    pub.cmd_indent=function(state)
    {
        return this.indent_or_outdent(state, false);
    }
    
    pub.cmd_outdent=function(state)
    {
        return this.indent_or_outdent(state, true);
    }

    priv.handle_list=function(state, type)
    {
        // Build an array of selected line offsets        
        var lines=[];
        if (state.getSelectedText().indexOf("\n")>0)
        {
            state.SelectWholeLines();
            
            var line=state.m_selectionStart;
            lines.push(line);
            
            while (true)
            {
                line=state.FindNextLine(line);
                if (line>=state.m_selectionEnd)
                    break;  
                lines.push(line);
            }
        }
        else
        {
            lines.push(state.FindStartOfLine(state.m_selectionStart));
        }
        
        // Now work out the new list type
        // If the current selection only contains the current list type
        // then remove list items
        var prefix = type=="*" ? "* " : "1. ";
        for (var i=0; i<lines.length; i++)
        {
            var lt=state.DetectListType(lines[i]);
            if (lt.m_listType==type)
            {
                prefix="";
                break;
            }
        }

        // Update the prefix on all lines
        for (var i=lines.length-1; i>=0; i--)
        {
            var line=lines[i];
            var lt=state.DetectListType(line);
            state.ReplaceAt(line, lt.m_prefixLen, prefix);
        }
        
        // We now need to find any surrounding lists and renumber them
        var mdd=new MarkdownDeep.Markdown();
        mdd.ExtraMode=true;
        var listitems=mdd.GetListItems(state.m_text, state.m_selectionStart);
        
        while (listitems!=null)
        {
            // Process each list item
            var dx=0;
            for (var i=0; i<listitems.length-1; i++)
            {
                // Detect the list type
                var lt=state.DetectListType(listitems[i]+dx);
                if (lt.m_listType!="1")
                    break;
                    
                // Format new number prefix
                var newNumber=(i+1).toString() + ". ";
                
                // Replace it
                state.ReplaceAt(listitems[i]+dx, lt.m_prefixLen, newNumber);
                
                // Adjust things if new prefix is different length to the previos
                dx += newNumber.length - lt.m_prefixLen;
            }
            
            
            var newlistitems=mdd.GetListItems(state.m_text, listitems[listitems.length-1]+dx);
            if (newlistitems!=null && newlistitems[0]!=listitems[0])
                listitems=newlistitems;
            else
                listitems=null;
        }
        
        
        // Select lines
        if (lines.length>1)
        {
            state.SelectWholeLines();
        }
        
        return true;
    }


    pub.cmd_ullist = function(state) {
        return this.handle_list(state, "*");
    };

    pub.cmd_ollist = function(state) {
        return this.handle_list(state, "1");
    };

    pub.cmd_link = function(ctx) {
        ctx.TrimSelection();
        if (!ctx.CheckSimpleSelection())
            return false;

        var url = prompt("Bitte gib eine URL an:");
        if (url === null)
            return false;

        var text = ctx.getSelectedText();
        if (text.length == 0) {
            text = "Linktext";
        }

        var str = "[" + text + "](" + url + ")";

        ctx.ReplaceSelection(str);
        ctx.m_selectionStart++;
        ctx.m_selectionEnd = ctx.m_selectionStart + text.length;
        return true;
    };

    pub.cmd_img = function(ctx) {
        ctx.TrimSelection();
        if (!ctx.CheckSimpleSelection())
            return false;

        var url = prompt("Bitte gib eine Bild-Url an");
        if (url === null)
            return false;

        var alttext = ctx.getSelectedText();
        if (alttext.length == 0) {
            alttext = "Bildtext";
        }

        var str = "![" + alttext + "](" + url + ")";

        ctx.ReplaceSelection(str);
        ctx.m_selectionStart += 2;
        ctx.m_selectionEnd = ctx.m_selectionStart + alttext.length;
        return true;
    };

    pub.cmd_hr = function(state) {
        state.SelectSurroundingWhiteSpace();
        if (state.m_selectionStart == 0)
            state.ReplaceSelection("----------\n\n");
        else
            state.ReplaceSelection("\n\n----------\n\n");
        state.m_selectionStart = state.m_selectionEnd;
        ;
        return true;
    };

    pub.IndentNewLine = function() {
        var editor = this;
        var timer;
        var handler = function() {
            window.clearInterval(timer);

            // Create an editor state from the current selection
            var state = new EditorState();
            state.InitFromTextArea(editor.m_textarea);

            // Find start of previous line
            var prevline = state.FindStartOfLine(state.SkipPreceedingEol(state.m_selectionStart));

            // Count spaces and tabs
            var i = prevline;
            while (true) {
                var ch = state.m_text.charAt(i);
                if (ch != ' ' && ch != '\t')
                    break;
                i++;
            }

            // Copy spaces and tabs to the new line
            if (i > prevline) {
                state.ReplaceSelection(state.m_text.substr(prevline, i - prevline));
                state.m_selectionStart = state.m_selectionEnd;
            }

            state.Apply();
        };

        timer = window.setInterval(handler, 1);

        return false;
    };

    pub.cmd_indented_newline = function(state) {
        // Do default new line
        state.ReplaceSelection("\n");
        state.m_selectionStart = state.m_selectionEnd;

        // Find start of previous line
        var prevline = state.FindStartOfLine(state.SkipPreceedingEol(state.m_selectionStart));

        // Count spaces and tabs
        var i = prevline;
        while (true) {
            var ch = state.m_text.charAt(i);
            if (ch != ' ' && ch != '\t')
                break;
            i++;
        }

        // Copy spaces and tabs to the new line
        if (i > prevline) {
            state.ReplaceSelection(state.m_text.substr(prevline, i - prevline));
            state.m_selectionStart = state.m_selectionEnd;
        }

        return true;
    };
    
    // Handle toolbar button
    pub.InvokeCommand = function(id) {
        // Special handling for undo and redo
        if (id == "undo" || id == "redo") {
            this["cmd_" + id]();
            this.m_textarea.focus();
            return;
        }

        // Create an editor state from the current selection
        var state = new EditorState();
        state.InitFromTextArea(this.m_textarea);

        // Create a copy for undo buffer
        var originalState = state.Duplicate();

        // Call the handler and apply changes
        if (this["cmd_" + id](state)) {
            // Save current state on undo stack
            this.m_undoMode = undomode_unknown;
            this.m_undoStack.splice(this.m_undoPos, this.m_undoStack.length - this.m_undoPos, originalState);
            this.m_undoPos++;

            // Apply new state
            state.Apply();

            // Update markdown rendering
            this.onMarkdownChanged();

            return true;
        } else {
            this.m_textarea.focus();
            return false;
        }
    };
    
    delete priv;
    delete pub;

    // Exports
    this.Editor=Editor;
}();
