/*
 * Javascript terminal
 * 
 * Copyright (c) 2011-2020 Fabrice Bellard
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
"use strict";

function Term(options)
{
    var width, height, tot_height, scrollback;
    
    function dummy_key_handler()
    {
    }

    width = options.cols ? options.cols : 80;
    height = options.rows ? options.rows : 25;
    scrollback = options.scrollback ? options.scrollback : 0;
    this.font_size = options.fontSize ? options.fontSize : 15;

    this.w = width;
    this.h = height;
    this.cur_h = height; /* current height of the scroll back buffer */
    tot_height = height + scrollback;
    this.tot_h = tot_height; /* maximum height of the scroll back buffer */

    /* y_base and y_disp are index in the circular buffer lines of
       length cur_h. They are defined modulo tot_h, i.e. they wrap
       when cur_h = tot_h. If cur_h < tot_h, y_base is always equal to
       cur_h - h. */
    this.y_base = 0; /* position of the current top screen line in the
                        scroll back buffer */
    this.y_disp = 0; /* position of the top displayed line in the
                        scroll back buffer */
    /* cursor position */
    this.x = 0;
    this.y = 0;
    this.scroll_top = 0;
    this.scroll_bottom = this.h;
    this.cursorstate = 0;
    this.handler = dummy_key_handler;
    this.state = 0;
    this.output_queue = "";
    this.colors = [
        /* normal */
        "#000000",
        "#aa0000",
        "#00aa00",
        "#aa5500",
        "#0000aa",
        "#aa00aa",
        "#00aaaa",
        "#aaaaaa",
        /* bright */
        "#555555",
        "#ff5555",
        "#55ff55",
        "#ffff55",
        "#5555ff",
        "#ff55ff",
        "#55ffff",
        "#ffffff" 
    ];
    /* attributes bits:
       0-3: bg
       4-7: fg
       8: bold
       9: inverse
    */
    this.def_attr = (7 << 4) | 0;
    this.cur_attr = this.def_attr;
    this.is_mac = (navigator.userAgent.indexOf("Mac") >=0 ) ? true : false;
    this.key_rep_state = 0;
    this.key_rep_str = "";
    
    this.utf8 = true;
    this.utf8_state = 0;
    this.utf8_val = 0;

    this.application_cursor = false;
    this.application_keypad = false;
    /* if true, emulate some behaviors of the Linux console */
    this.linux_console = true;

    this.textarea_has_focus = false;
}

Term.prototype.setKeyHandler = function(handler)
{
    this.handler = handler;
}

/* return the size of a character in CSS pixels using the selected font */
function term_get_char_size(parent_el, font_size)
{
    var el, g, ret;
    el = document.createElement("div");
    el.classList.add("term", "term_char_size");
    el.style.fontSize = font_size + "px";
    el.textContent = "W";
    parent_el.appendChild(el);
    g = el.getBoundingClientRect();
    /* the character width & height may not be an integer */
    ret = [g.width, g.height];
    return ret;
}

Term.prototype.open = function(parent_el)
{
    var y, line, i, term, c, row_el, char_size_ret;

    /* set initial content */
    this.lines = new Array();
    c = 32 | (this.def_attr << 16);
    for(y = 0; y < this.cur_h;y++) {
        line = new Array();
        for(i=0;i<this.w;i++)
            line[i] = c;
        this.lines[y] = line;
    }

    char_size_ret = term_get_char_size(parent_el, this.font_size);
    /* size of the character in CSS pixels */
    this.char_width = char_size_ret[0];
    this.char_height = char_size_ret[1];

    this.scrollbar_width = 15;
    
    /* size of term_el in CSS pixels */
    this.term_width = Math.ceil(this.w * this.char_width) +
        this.scrollbar_width;
    this.term_height = Math.ceil(this.h * this.char_height);
        
    /* create the terminal window */
    this.term_el = document.createElement("div");
    this.term_el.className = "term";
    /* XXX: could compute the font metrics */
    this.term_el.style.fontSize = this.font_size + "px";
    this.term_el.style.width = this.term_width + "px";
    this.term_el.style.height = this.term_height + "px";
    /* allow the terminal to take the focus */
    this.term_el.setAttribute("tabindex", "0");
    
    /* scroll bar */
    this.scrollbar_el = document.createElement("div");
    this.scrollbar_el.className = "term_scrollbar";
    this.scrollbar_el.style.width = this.scrollbar_width + "px";
    this.term_el.appendChild(this.scrollbar_el);

    this.track_el = document.createElement("div");
    this.track_el.className = "term_track";
    this.track_el.onmousedown = this.mouseMoveHandler.bind(this);
    this.scrollbar_el.appendChild(this.track_el);
    
    this.thumb_el = document.createElement("div");
    this.thumb_el.className = "term_thumb";
    this.thumb_el.onmousedown = this.mouseDownHandler.bind(this);
    this.track_el.appendChild(this.thumb_el);

    this.end_el = document.createElement("div");
    this.end_el.className = "term_end";
    this.thumb_el.appendChild(this.end_el);

    /* current scrollbar position */
    this.thumb_size = -1;
    this.thumb_pos = -1;
    
    /* terminal content */
    this.content_el = document.createElement("div");
    this.content_el.className = "term_content";
    this.content_el.style.width = (this.w) + "ch";
    this.term_el.appendChild(this.content_el);
    
    this.rows_el = [];
    for(y=0;y<this.h;y++) {
        row_el = document.createElement("div");
        this.rows_el.push(row_el);
        this.content_el.appendChild(row_el);
    }
    
    /* dummy textarea to get the input events and for the virtual
       keyboard on mobile devices */
    this.textarea_el = document.createElement("textarea");
    this.textarea_el.classList.add("term_textarea");
    this.textarea_el.setAttribute("autocorrect", "off");
    this.textarea_el.setAttribute("autocapitalize", "off");
    this.textarea_el.setAttribute("spellcheck", "false");
    this.textarea_el.setAttribute("tabindex", "-1");
    this.term_el.appendChild(this.textarea_el);

    this.parent_el = parent_el;
    parent_el.appendChild(this.term_el);

    this.refresh(0, this.h - 1);
    
    /* textarea_el events */
    // key handler
    this.textarea_el.addEventListener("keydown", 
                                      this.keyDownHandler.bind(this), true);
    this.textarea_el.addEventListener("keyup", 
                                      this.keyUpHandler.bind(this), true);
    /* keypress is deprecated, so use input */
    this.textarea_el.addEventListener("input", 
                                      this.inputHandler.bind(this), true);
    this.textarea_el.addEventListener("focus", 
                                  this.focusHandler.bind(this), true);
    this.textarea_el.addEventListener("blur", 
                                  this.blurHandler.bind(this), true);

    /* term_el events */
    this.term_el.addEventListener("keydown",
                                  this.termKeyDownHandler.bind(this),
                                  true);
    this.term_el.addEventListener("paste", 
                                  this.pasteHandler.bind(this), true);
    this.term_el.addEventListener("mouseup",
                                  this.termMouseUpHandler.bind(this),
                                  true);
    this.term_el.addEventListener("wheel", 
                                  this.wheelHandler.bind(this), false);

    // cursor blinking
    term = this;
    setInterval(function() { term.cursor_timer_cb(); }, 1000);

    this.term_el.focus();
};

Term.prototype.refresh_scrollbar = function ()
{
    var total_size, thumb_pos, thumb_size, y, y0;
    total_size = this.term_el.clientHeight;
    thumb_size = Math.ceil(this.h * total_size / this.cur_h);
    /* position of the first line of the scroll back buffer */
    y0 = (this.y_base + this.h) % this.cur_h;
    y = this.y_disp - y0;
    if (y < 0)
        y += this.cur_h;
    thumb_pos = Math.floor(y * total_size / this.cur_h);
    thumb_size = Math.max(thumb_size, 30);
    thumb_size = Math.min(thumb_size, total_size);
    thumb_pos = Math.min(thumb_pos, total_size - thumb_size);
//    console.log("pos=" + thumb_pos + " size=" + thumb_size);
    if (thumb_pos != this.thumb_pos || thumb_size != this.thumb_size) {
        this.thumb_pos = thumb_pos;
        this.thumb_size = thumb_size;
        this.thumb_el.style.top = thumb_pos + "px";
        this.thumb_el.style.height = thumb_size + "px";
    }
}

/* move the text area at the cursor position so that the browser shows
 * the correct position when the virtual keyboard is used */
Term.prototype.move_textarea = function()
{
    var x, y, base_x, base_y, pos;

    pos = this.term_el.getBoundingClientRect();
    base_x = pos.left + window.scrollX;
    base_y = pos.top + window.scrollY;
    /* position relative to the body */
    x = Math.ceil(this.x * this.char_width + base_x);
    y = Math.ceil(this.y * this.char_height + base_y);

    this.textarea_el.style.width = Math.ceil(this.char_width) + "px";
    this.textarea_el.style.height = Math.ceil(this.char_height) + "px";
    this.textarea_el.style.left = x + "px";
    this.textarea_el.style.top = y + "px";
    this.textarea_el.style.zIndex = 1000;
}

Term.prototype.refresh = function(ymin, ymax)
{
    var el, y, line, outline, c, w, i, j, cx, attr, last_attr, fg, bg, y1;
    var http_link_len, http_link_str, bold, tmp, inverse;
    
    function is_http_link_char(c)
    {
        var str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~:/?#[]@!$&'()*+,;=%`.";
        return str.indexOf(String.fromCharCode(c)) >= 0;
    }

    function right_trim(str, a)
    {
        var i, n;
        n = a.length;
        i = str.length;
        while (i >= n && str.substr(i - n, n) == a)
            i -= n;
        return str.substr(0, i);
    }
    
    for(y = ymin; y <= ymax; y++) {
        /* convert to HTML string */
        y1 = y + this.y_disp;
        if (y1 >= this.cur_h)
            y1 -= this.cur_h;
        line = this.lines[y1];
        outline = "";
        w = this.w;
        if (y == this.y && this.cursor_state && 
            this.y_disp == this.y_base) {
            cx = this.x;
        } else {
            cx = -1;
        }
        last_attr = this.def_attr;
        http_link_len = 0;
        for(i = 0; i < w; i++) {
            c = line[i];
            attr = c >> 16;
            c &= 0xffff;
            /* test for http link */
            if (c == 0x68 && (w - i) >= 8 && http_link_len == 0) {
                /* test http:// or https:// */
                if ((line[i + 1] & 0xffff) == 0x74 &&
                    (line[i + 2] & 0xffff) == 0x74 &&
                    (line[i + 3] & 0xffff) == 0x70 &&
                    (((line[i + 4] & 0xffff) == 0x3a &&
                      (line[i + 5] & 0xffff) == 0x2f &&
                      (line[i + 6] & 0xffff) == 0x2f) ||
                     ((line[i + 4] & 0xffff) == 0x73 &&
                      (line[i + 5] & 0xffff) == 0x3a &&
                      (line[i + 6] & 0xffff) == 0x2f &&
                      (line[i + 7] & 0xffff) == 0x2f))) {
                    http_link_str = "";
                    j = 0;
                    while ((i + j) < w &&
                           is_http_link_char(line[i + j] & 0xffff)) {
                        http_link_str += String.fromCharCode(line[i + j] & 0xffff);
                        j++;
                    }
                    http_link_len = j;
                    if (last_attr != this.def_attr) {
                        outline += '</span>';
                        last_attr = this.def_attr;
                    }
                    outline += "<a href='" + http_link_str + "' onclick='return Term.prototype.href(this)' target='_blank'>";
                }
            }
            if (i == cx)  {
                attr = -1; /* cursor */
            }
            if (attr != last_attr) {
                if (last_attr != this.def_attr)
                    outline += '</span>';
                if (attr != this.def_attr) {
                    if (attr == -1) {
                        /* cursor */
                        outline += '<span class="term_cursor">';
                    } else {
                        outline += '<span style="';
                        fg = (attr >> 4) & 0xf;
                        bg = attr & 0xf;
                        bold = (attr >> 8) & 1;
                        inverse = (attr >> 9) & 1;
                        if (inverse) {
                            tmp = fg;
                            fg = bg;
                            bg = tmp;
                        }
                        if (bold) {
                            /* metrics are not OK for all fonts, so disabled */
                            /* outline += 'font-weight:bold;'; */
                            /* use the bright color */
                            if (fg < 8)
                                fg += 8;
                        }
                        if (fg != 7) {
                            outline += 'color:' + this.colors[fg] + ';';
                        }
                        if (bg != 0) {
                            outline += 'background-color:' + 
                                this.colors[bg] + ';';
                        }
                        outline += '">';
                    }
                }
            }
            switch(c) {
            case 32:
                outline += "&nbsp;";
                break;
            case 38: // '&'
                outline += "&amp;";
                break;
            case 60: // '<'
                outline += "&lt;";
                break;
            case 62: // '>'
                outline += "&gt;";
                break;
            default:
                if (c < 32) {
                    outline += "&nbsp;";
                } else {
                    outline += String.fromCharCode(c);
                }
                break;
            }
            last_attr = attr;
            if (http_link_len != 0) {
                http_link_len--;
                if (http_link_len == 0) {
                    if (last_attr != this.def_attr) {
                        outline += '</span>';
                        last_attr = this.def_attr;
                    }
                    outline += "</a>";
                }
            }
        }
        if (last_attr != this.def_attr) {
            outline += '</span>';
        }

        /* trim trailing spaces for copy/paste */
        outline = right_trim(outline, "&nbsp;");
        if (outline == "")
            outline = "&nbsp;";
        
        this.rows_el[y].innerHTML = outline;
    }

    this.refresh_scrollbar();
    this.move_textarea();
};

Term.prototype.cursor_timer_cb = function()
{
    this.cursor_state ^= 1;
    this.refresh(this.y, this.y);
};

Term.prototype.show_cursor = function()
{
    if (!this.cursor_state) {
        this.cursor_state = 1;
        this.refresh(this.y, this.y);
    }
};

/* scroll down or up in the scroll back buffer by n lines */
Term.prototype.scroll_disp = function(n)
{
    var i, y1;
    /* slow but it does not really matters */
    if (n >= 0) {
        for(i = 0; i < n; i++) {
            if (this.y_disp == this.y_base)
                break;
            if (++this.y_disp == this.cur_h)
                this.y_disp = 0;
        }
    } else {
        n = -n;
        y1 = this.y_base + this.h;
        if (y1 >= this.cur_h)
            y1 -= this.cur_h;
        for(i = 0; i < n; i++) {
            if (this.y_disp == y1)
                break;
            if (--this.y_disp < 0)
                this.y_disp = this.cur_h - 1;
        }
    }
    this.refresh(0, this.h - 1);
};

Term.prototype.write = function(str)
{
    var s, ymin, ymax;
    
    function update(y) 
    {
        ymin = Math.min(ymin, y);
        ymax = Math.max(ymax, y);
    }

    function get_erase_char()
    {
        var bg_mask, attr;
        bg_mask = 0xf;
        attr = (s.def_attr & ~bg_mask) | (s.cur_attr & bg_mask);
        return 32 | (attr << 16);
    }
    
    function erase_chars(x1, x2, y) {
        var l, i, c, y1;
        y1 = s.y_base + y;
        if (y1 >= s.cur_h)
            y1 -= s.cur_h;
        l = s.lines[y1];
        c = get_erase_char();
        for(i = x1; i < x2; i++)
            l[i] = c;
        update(y);
    }

    function erase_to_eol(x, y) {
        erase_chars(x, s.w, y);
    }
    
    function erase_in_line(n) {
        switch(n) {
        case 0:
            erase_to_eol(s.x, s.y);
            break;
        case 1:
            erase_chars(0, s.x + 1, s.y);
            break;
        case 2:
            erase_chars(0, s.w, s.y);
            break;
        }
    }

    function erase_in_display(n) {
        var y;
        switch(n) {
        case 0:
            erase_to_eol(s.x, s.y);
            for(y = s.y + 1; y < s.h; y++)
                erase_to_eol(0, y);
            break;
        case 1:
            erase_chars(0, s.x + 1, s.y);
            for(y = 0; y < s.y; y++) {
                erase_to_eol(0, y);
            }
            break;
        case 2:
            for(y = 0; y < s.h; y++) {
                erase_to_eol(0, y);
            }
            break;
        }
    }

    
    function delete_chars(n)
    {
        var l, i, c, y1, j;
        y1 = s.y + s.y_base;
        if (y1 >= s.cur_h)
            y1 -= s.cur_h;
        l = s.lines[y1];
        if (n < 1)
            n = 1;
        c = get_erase_char();
        j = s.x + n;
        for(i = s.x; i < s.w; i++) {
            if (j < s.w)
                l[i] = l[j];
            else
                l[i] = c;
            j++;
        }
        update(s.y);
    }

    function insert_chars(n)
    {
        var l, i, c, y1, x1;
        if (n < 1)
            n = 1;
        if (n > s.w - s.x)
            n = s.w - s.x;
        y1 = s.y + s.y_base;
        if (y1 >= s.cur_h)
            y1 -= s.cur_h;
        l = s.lines[y1];
        x1 = s.x + n;
        for(i = s.w - 1; i >= x1; i--)
            l[i] = l[i - n];
        c = get_erase_char();
        for(i = s.x; i < x1; i++)
            l[i] = c;
        update(s.y);
    }
    
    function csi_colors(esc_params)
    {
        var j, n, fg, bg, mask;

        if (esc_params.length == 0) {
            s.cur_attr= s.def_attr;
        } else {
            for(j = 0; j < esc_params.length; j++) {
                n = esc_params[j];
                if (n >= 30 && n <= 37) {
                    /* foreground */
                    fg = n - 30;
                    s.cur_attr = (s.cur_attr & ~(0xf << 4)) | (fg << 4);
                } else if (n >= 40 && n <= 47) {
                    /* background */
                    bg = n - 40;
                    s.cur_attr = (s.cur_attr & ~0xf) | bg;
                } else if (n >= 90 && n <= 97) {
                    /* bright foreground */
                    fg = n - 90 + 8;
                    s.cur_attr = (s.cur_attr & ~(0xf << 4)) | (fg << 4);
                } else if (n >= 100 && n <= 107) {
                    /* bright background */
                    bg = n - 100 + 8;
                    s.cur_attr = (s.cur_attr & ~0xf) | bg;
                } else if (n == 1) {
                    /* bold + bright */
                    s.cur_attr |= (1 << 8);
                } else if (n == 0) {
                    /* default attr */
                    s.cur_attr = s.def_attr;
                } else if (n == 7) {
                    /* inverse */
                    s.cur_attr |= (1 << 9);
                } else if (n == 27) {
                    /* not inverse */
                    s.cur_attr &= ~(1 << 9);
                } else if (n == 39) {
                    /* reset fg */
                    mask = 0x0f << 4;
                    s.cur_attr = (s.cur_attr & ~mask) | (s.def_attr & mask);
                } else if (n == 49) {
                    /* reset bg */
                    mask = 0x0f;
                    s.cur_attr = (s.cur_attr & ~mask) | (s.def_attr & mask);
                }
            }
        }
    }

    function empty_line(y, use_erase_char) {
        var line, c, y1, x;
        if (use_erase_char)
            c = get_erase_char();
        else
            c = 32 | (s.def_attr << 16);
        line = new Array();
        for(x=0;x<s.w;x++)
            line[x] = c;
        y1 = s.y_base + y;
        if (y1 >= s.cur_h)
            y1 -= s.cur_h;
        s.lines[y1] = line;
    }

    function scroll_down(top, bottom, use_erase_char)
    {
        var y, line, y1, y2;
        
        if (top == 0 && bottom == s.h) {
            /* increase height of buffer if possible */
            if (s.cur_h < s.tot_h) {
                s.cur_h++;
            }
            /* move down one line */
            if (++s.y_base == s.cur_h)
                s.y_base = 0;
            s.y_disp = s.y_base;
        } else {
            /* partial scroll */
            for(y = top; y < bottom - 1; y++) {
                y1 = s.y_base + y;
                if (y1 >= s.cur_h)
                    y1 -= s.cur_h;
                y2 = y1 + 1;
                if (y2 >= s.cur_h)
                    y2 -= s.cur_h;
                s.lines[y1] = s.lines[y2];
            }
        }
        empty_line(bottom - 1, use_erase_char);
        update(top);
        update(bottom - 1);
    }

    function scroll_up(top, bottom, use_erase_char) {
        var y, y1, y2;
        /* XXX: could scroll in the history */
        for(y = bottom - 1; y > top; y--) {
            y1 = s.y_base + y;
            if (y1 >= s.cur_h)
                y1 -= s.cur_h;
            y2 = y1 - 1;
            if (y2 >= s.cur_h)
                y2 -= s.cur_h;
            s.lines[y1] = s.lines[y2];
        }
        empty_line(top, use_erase_char);
        update(top);
        update(bottom - 1);
    }
    
    function down_with_scroll() {
        s.y++;
        if (s.y == s.scroll_bottom) {
            s.y--;
            scroll_down(s.scroll_top, s.scroll_bottom, false);
        } else if (s.y >= s.h) {
            s.y--;
            scroll_down(0, s.h, false);
        }
    }

    function up_with_scroll() {
        if (s.y == s.scroll_top) {
            scroll_up(s.scroll_top, s.scroll_bottom, true);
        } else if (s.y == 0) {
            scroll_up(0, s.h, true);
        } else {
            s.y--;
        }
    }

    function insert_lines(n) {
        var y2;
        if (n < 1)
            n = 1;
        if (s.y < s.scroll_bottom)
            y2 = s.scroll_bottom;
        else
            y2 = s.h;
        while (n != 0) {
            scroll_up(s.y, y2, true);
            n--;
        }
    }

    function delete_lines(n) {
        var y2;
        if (n < 1)
            n = 1;
        if (s.y < s.scroll_bottom)
            y2 = s.scroll_bottom;
        else
            y2 = s.h;
        while (n != 0) {
            scroll_down(s.y, y2, true);
            n--;
        }
    }
    
    var TTY_STATE_NORM = 0;
    var TTY_STATE_ESC = 1;
    var TTY_STATE_CSI = 2;
    var TTY_STATE_CHARSET = 3;

    function handle_char(c) {
        var i, l, n, j, y1, y2, x1;
        
        switch(s.state) {
        case TTY_STATE_NORM:
            switch(c) {
            case 10:
                down_with_scroll();
                break;
            case 13:
                s.x = 0;
                break;
            case 8:
                if (s.x > 0) {
                    s.x--;
                }
                break;
            case 9: /* tab */
                n = (s.x + 8) & ~7;
                if (n <= s.w) {
                    s.x = n;
                }
                break;
            case 27:
                s.state = TTY_STATE_ESC;
                break;
            default:
                if (c >= 32) {
                    if (s.x >= s.w) {
                        s.x = 0;
                        down_with_scroll();
                    }
                    y1 = s.y + s.y_base;
                    if (y1 >= s.cur_h)
                        y1 -= s.cur_h;
                    s.lines[y1][s.x] = (c & 0xffff) | 
                        (s.cur_attr << 16);
                    s.x++;
                    update(s.y);
                }
                break;
            }
            break;
        case TTY_STATE_ESC:
            switch(c) {
            case 91: // '['
                s.esc_params = new Array();
                s.cur_param = 0;
                s.esc_prefix = 0;
                s.state = TTY_STATE_CSI;
                break;
            case 40: // '('
            case 41: // ')'
                s.state = TTY_STATE_CHARSET;
                break;
            case 61: // '='
                s.application_keypad = true;
                s.state = TTY_STATE_NORM;
                break;
            case 62: // '>'
                s.application_keypad = false;
                s.state = TTY_STATE_NORM;
                break;
            case 77: // 'M'
                up_with_scroll();
                s.state = TTY_STATE_NORM;
                break;
            default:
                s.state = TTY_STATE_NORM;
                break;
            }
            break;
        case TTY_STATE_CSI:
            if (c >= 48 && c <= 57) { // '0' '9'
                /* numeric */
                s.cur_param = s.cur_param * 10 + c - 48;
            } else {
                if (c == 63) { // '?'
                    s.esc_prefix = c;
                    break;
                }
                /* add parsed parameter */
                s.esc_params[s.esc_params.length] = s.cur_param;
                s.cur_param = 0;
                if (c == 59) // ;
                    break;
                s.state = TTY_STATE_NORM;

                //                console.log("term: csi=" + s.esc_params + " cmd="+c);
                switch(c) {
                case 64: // '@' insert chars
                    insert_chars(s.esc_params[0]);
                    break;
                case 65: // 'A' up
                    n = s.esc_params[0];
                    if (n < 1)
                        n = 1;
                    s.y -= n;
                    if (s.y < 0)
                        s.y = 0;
                    break;
                case 66: // 'B' down
                    n = s.esc_params[0];
                    if (n < 1)
                        n = 1;
                    s.y += n;
                    if (s.y >= s.h)
                        s.y = s.h - 1;
                    break;
                case 67: // 'C' right
                    n = s.esc_params[0];
                    if (n < 1)
                        n = 1;
                    s.x += n;
                    if (s.x >= s.w - 1)
                        s.x = s.w - 1;
                    break;
                case 68: // 'D' left
                    n = s.esc_params[0];
                    if (n < 1)
                        n = 1;
                    s.x -= n;
                    if (s.x < 0)
                        s.x = 0;
                    break;
                case 71: /* 'G' cursor character absolute */
                    x1 = s.esc_params[0] - 1;
                    if (x1 < 0)
                        x1 = 0;
                    else if (x1 >= s.w)
                        x1 = s.w - 1;
                    s.x = x1;
                    break;
                case 72: // 'H' goto xy
                    y1 = s.esc_params[0] - 1;
                    if (s.esc_params.length >= 2)
                        x1 = s.esc_params[1] - 1;
                    else
                        x1 = 0;
                    if (y1 < 0)
                        y1 = 0;
                    else if (y1 >= s.h)
                        y1 = s.h - 1;
                    if (x1 < 0)
                        x1 = 0;
                    else if (x1 >= s.w)
                        x1 = s.w - 1;
                    s.x = x1;
                    s.y = y1;
                    break;
                case 74: // 'J' erase in display
                    erase_in_display(s.esc_params[0]);
                    break;
                case 75: // 'K' erase in line
                    erase_in_line(s.esc_params[0]);
                    break;
                case 76: // 'L' insert lines
                    insert_lines(s.esc_params[0]);
                    break;
                case 77: // 'M' insert lines
                    delete_lines(s.esc_params[0]);
                    break;
                case 80: // 'P'
                    delete_chars(s.esc_params[0]);
                    break;
                case 100: // 'd' line position absolute
                    {
                        y1 = s.esc_params[0] - 1;
                        if (y1 < 0)
                            y1 = 0;
                        else if (y1 >= s.h)
                            y1 = s.h - 1;
                        s.y = y1;
                    }
                    break;
                case 104: // 'h': set mode
                    if (s.esc_prefix == 63 && s.esc_params[0] == 1) {
                        s.application_cursor = true;
                    }
                    break;
                case 108: // 'l': reset mode
                    if (s.esc_prefix == 63 && s.esc_params[0] == 1) {
                        s.application_cursor = false;
                    }
                    break;
                case 109: // 'm': set color
                    csi_colors(s.esc_params);
                    break;
                case 110: // 'n' return the cursor position
                    s.queue_chars("\x1b[" + (s.y + 1) + ";" + (s.x + 1) + "R");
                    break;
                case 114: // 'r' set scroll region
                    y1 = s.esc_params[0] - 1;
                    if (y1 < 0)
                        y1 = 0;
                    else if (y1 >= s.h)
                        y1 = s.h - 1;
                    if (s.esc_params.length >= 2)
                        y2 = s.esc_params[1];
                    else
                        y2 = s.h;
                    if (y2 >= s.h || y2 <= y1)
                        y2 = s.h;
                    s.scroll_top = y1;
                    s.scroll_bottom = y2;
                    s.x = 0;
                    s.y = 0;
                    break;
                default:
                    break;
                }
            }
            break;
        case TTY_STATE_CHARSET:
            /* just ignore */
            s.state = TTY_STATE_NORM;
            break;
        }
    }

    function handle_utf8(c) {
        if (s.utf8_state !== 0 && (c >= 0x80 && c < 0xc0)) {
            s.utf8_val = (s.utf8_val << 6) | (c & 0x3F);
            s.utf8_state--;
            if (s.utf8_state === 0) {
                handle_char(s.utf8_val);
            }
        } else if (c >= 0xc0 && c < 0xf8) {
            s.utf8_state = 1 + (c >= 0xe0) + (c >= 0xf0);
            s.utf8_val = c & ((1 << (6 - s.utf8_state)) - 1);
        } else {
            s.utf8_state = 0;
            handle_char(c);
        }
    }
    
    var i, c, utf8;

    /* update region is in ymin ymax */
    s = this;
    ymin = s.h;
    ymax = -1;
    update(s.y); // remove the cursor
    /* reset top of displayed screen to top of real screen */
    if (s.y_base != s.y_disp) {
        s.y_disp = s.y_base;
        /* force redraw */
        ymin = 0;
        ymax = s.h - 1;
    }
    utf8 = s.utf8;
    for(i = 0; i < str.length; i++) {
        c = str.charCodeAt(i);
        if (utf8)
            handle_utf8(c);
        else
            handle_char(c);
    }
    update(s.y); // show the cursor

    if (ymax >= ymin)
        s.refresh(ymin, ymax);
};

Term.prototype.writeln = function (str)
{
    this.write(str + '\r\n');
};

Term.prototype.interceptBrowserExit = function (ev)
{
    /* At least avoid exiting the navigator if Ctrl-Q or Ctrl-W are
     * pressed */
    if (ev.ctrlKey) {
        window.onbeforeunload = function() {
            window.onbeforeunload = null;
            return "CTRL-W or Ctrl-Q cannot be sent to the emulator.";
        };
    } else {
        window.onbeforeunload = null;
    }
}

Term.prototype.keyDownHandler = function (ev)
{
    var str;
    this.interceptBrowserExit(ev);

    str="";
    switch(ev.keyCode) {
    case 8: /* backspace */
        str = "\x7f";
        break;
    case 9: /* tab */
        str = "\x09";
        break;
    case 13: /* enter */
        str = "\x0d";
        break;
    case 27: /* escape */
        str = "\x1b";
        break;
    case 37: /* left */
        if (ev.ctrlKey) {
            str = "\x1b[1;5D";
        } else if (this.application_cursor) {
            str = "\x1bOD";
        } else {
            str = "\x1b[D";
        }
        break;
    case 39: /* right */
        if (ev.ctrlKey) {
            str = "\x1b[1;5C";
        } else if (this.application_cursor) {
            str = "\x1bOC";
        } else {
            str = "\x1b[C";
        }
        break;
    case 38: /* up */
        if (ev.ctrlKey) {
            this.scroll_disp(-1);
        } else if (this.application_cursor) {
            str = "\x1bOA";
        } else {
            str = "\x1b[A";
        }
        break;
    case 40: /* down */
        if (ev.ctrlKey) {
            this.scroll_disp(1);
        } else if (this.application_cursor) {
            str = "\x1bOB";
        } else {
            str = "\x1b[B";
        }
        break;
    case 46: /* delete */
        str = "\x1b[3~";
        break;
    case 45: /* insert */
        str = "\x1b[2~";
        break;
    case 36: /* home */
        if (this.linux_console)
            str = "\x1b[1~";
        else if (this.application_keypad)
            str = "\x1bOH";
        else
            str = "\x1b[H";
        break;
    case 35: /* end */
        if (this.linux_console)
            str = "\x1b[4~";
        else if (this.application_keypad)
            str = "\x1bOF";
        else
            str = "\x1b[F";
        break;
    case 33: /* page up */
        if (ev.ctrlKey) {
            this.scroll_disp(-(this.h - 1));
        } else {
            str = "\x1b[5~";
        }
        break;
    case 34: /* page down */
        if (ev.ctrlKey) {
            this.scroll_disp(this.h - 1);
        } else {
            str = "\x1b[6~";
        }
        break;
    default:
        if (ev.ctrlKey) {
            /* ctrl + key */
            if (ev.keyCode >= 65 && ev.keyCode <= 90) {
                str = String.fromCharCode(ev.keyCode - 64);
            } else if (ev.keyCode == 32) {
                str = String.fromCharCode(0);
            }
        } else if ((!this.is_mac && ev.altKey) ||
                   (this.is_mac && ev.metaKey)) {
            /* meta + key (Note: we only send lower case) */
            if (ev.keyCode >= 65 && ev.keyCode <= 90) {
                str = "\x1b" + String.fromCharCode(ev.keyCode + 32);
            }
        }
        break;
    }
    //    console.log("keydown: keycode=" + ev.keyCode + " charcode=" + ev.charCode + " str=" + str + " ctrl=" + ev.ctrlKey + " alt=" + ev.altKey + " meta=" + ev.metaKey);
    if (str) {
        if (ev.stopPropagation)
            ev.stopPropagation();
        if (ev.preventDefault)
            ev.preventDefault();

        this.show_cursor();
        this.key_rep_state = 1;
        this.key_rep_str = str;
        this.handler(str);
        return false;
    } else {
        this.key_rep_state = 0;
        return true;
    }
};

Term.prototype.keyUpHandler = function (ev)
{
    this.interceptBrowserExit(ev);
};

Term.prototype.to_utf8 = function(s)
{
    var i, n = s.length, r, c;
    r = "";
    for(i = 0; i < n; i++) {
        c = s.charCodeAt(i);
        if (c < 0x80) {
            r += String.fromCharCode(c);
        } else if (c < 0x800) {
            r += String.fromCharCode((c >> 6) | 0xc0, (c & 0x3f) | 0x80);
        } else if (c < 0x10000) {
            r += String.fromCharCode((c >> 12) | 0xe0,
                                     ((c >> 6) & 0x3f) | 0x80,
                                     (c & 0x3f) | 0x80);
        } else {
            r += String.fromCharCode((c >> 18) | 0xf0,
                                     ((c >> 12) & 0x3f) | 0x80,
                                     ((c >> 6) & 0x3f) | 0x80,
                                     (c & 0x3f) | 0x80);
        }
    }
    return r;
}

Term.prototype.inputHandler = function (ev)
{
    var str;
    str = this.textarea_el.value;
    if (str) {
        this.textarea_el.value = "";
        this.show_cursor();
        if (this.utf8)
            str = this.to_utf8(str);
        this.handler(str);
        return false;
    } else {
        return true;
    }
};

Term.prototype.termKeyDownHandler = function(ev)
{
    this.interceptBrowserExit(ev);
    /* give the focus back to the textarea when a key is pressed */
    this.textarea_el.focus();
}

Term.prototype.termMouseUpHandler = function(ev)
{
    var sel;
    /* if no selection, can switch back up to the textarea focus */
    sel = window.getSelection();
    if (!sel || sel.isCollapsed)
        this.textarea_el.focus();
}

Term.prototype.focusHandler = function (ev)
{
    this.textarea_has_focus = true;
};

Term.prototype.blurHandler = function (ev)
{
    /* allow unloading the page */
    window.onbeforeunload = null;
    this.textarea_has_focus = false;
};

Term.prototype.pasteHandler = function (ev)
{
    var c, str;
    if (!this.textarea_has_focus) {
        c = ev.clipboardData;
        if (c) {
            str = c.getData("text/plain");
            if (this.utf8)
                str = this.to_utf8(str);
            this.queue_chars(str);
            return false;
        }
    }
}

Term.prototype.wheelHandler = function (ev)
{
    if (ev.deltaY < 0)
        this.scroll_disp(-3);
    else if (ev.deltaY > 0)
        this.scroll_disp(3);
    ev.stopPropagation();
}

Term.prototype.mouseDownHandler = function (ev)
{
    this.thumb_el.onmouseup = this.mouseUpHandler.bind(this);
    document.onmousemove = this.mouseMoveHandler.bind(this);
    document.onmouseup = this.mouseUpHandler.bind(this);

    /* disable potential selection */
    document.body.className += " noSelect";
    
    this.mouseMoveHandler(ev);
}

Term.prototype.mouseMoveHandler = function (ev)
{
    var total_size, pos, new_y_disp, y, y0;
    total_size = this.term_el.clientHeight;
    y = ev.clientY - this.track_el.getBoundingClientRect().top;
    pos = Math.floor((y - (this.thumb_size / 2)) * this.cur_h / total_size);
    new_y_disp = Math.min(Math.max(pos, 0), this.cur_h - this.h);
    /* position of the first line of the scroll back buffer */
    y0 = (this.y_base + this.h) % this.cur_h;
    new_y_disp += y0;
    if (new_y_disp >= this.cur_h)
        new_y_disp -= this.cur_h;
    if (new_y_disp != this.y_disp) {
        this.y_disp = new_y_disp;
        this.refresh(0, this.h - 1);
    }
}

Term.prototype.mouseUpHandler = function (ev)
{
    this.thumb_el.onmouseup = null;
    document.onmouseup = null;
    document.onmousemove = null;
    document.body.className = document.body.className.replace(" noSelect", "");
}

/* output queue to send back asynchronous responses */
Term.prototype.queue_chars = function (str)
{
    this.output_queue += str;
    if (this.output_queue)
        setTimeout(this.outputHandler.bind(this), 0);
};

Term.prototype.outputHandler = function ()
{
    if (this.output_queue) {
        this.handler(this.output_queue);
        this.output_queue = "";
    }
};

Term.prototype.getSize = function ()
{
    return [this.w, this.h];
};

/* resize the terminal (size in pixels). Return true if the display
   size was modified. */
/* XXX: may be simpler to separate the scrollback buffer from the
   screen buffer */
Term.prototype.resizePixel = function (new_width, new_height)
{
    var new_w, new_h, y, x, line, c, row_el, d, new_cur_h, e;
    
    if (new_width == this.term_width && new_height == this.term_height)
        return false;
    new_w = Math.floor((new_width - this.scrollbar_width) /
                       this.char_width);
    new_h = Math.floor(new_height / this.char_height);
    if (new_w <= 0 || new_h <= 0 || new_h > this.tot_h)
        return false;
    
    this.term_width = new_width;
    this.term_height = new_height;
    this.term_el.style.width = this.term_width + "px";
    this.term_el.style.height = this.term_height + "px";

    /* XXX: could keep the EOL positions */
    if (new_w < this.w) {
        /* reduce the line width */
        for(y = 0; y < this.cur_h;y++) {
            line = this.lines[y];
            line = line.slice(0, new_w);
        }
    } else if (new_w > this.w) {
        /* increase the line width */
        c = 32 | (this.def_attr << 16);
        for(y = 0; y < this.cur_h;y++) {
            line = this.lines[y];
            for(x = this.w; x < new_w; x++)
                line[x] = c;
        }
    }

    if (this.x >= new_w)
        this.x = new_w - 1;

    d = new_h - this.h;
    if (d < 0) {
        d = -d;
        /* remove displayed lines */

        /* strip the DOM terminal content */
        for(y = new_h; y < this.h; y++) {
            row_el = this.rows_el[y];
            this.content_el.removeChild(row_el);
        }
        this.rows_el = this.rows_el.slice(0, new_h);

        /* adjust cursor position if needed */
        if (this.y >= new_h) {
            if (d > this.y)
                d = this.y;
            this.y -= d;
            this.y_base += d;
            if (this.y_base >= this.tot_h)
                this.y_base -= this.tot_h;
        }

        if (this.scroll_bottom > new_h)
            this.scroll_bottom = new_h;
        /* fail safe for scroll top */
        if (this.scroll_top >= this.scroll_bottom)
            this.scroll_top = 0;
        
    } else if (d > 0) {
        /* add displayed lines */

        if (this.cur_h == this.tot_h) {
            if (d > this.tot_h - this.h)
                d = this.tot_h - this.h;
        } else {
            if (d > this.y_base)
                d = this.y_base;
        }
        this.y_base -= d;
        if (this.y_base < 0)
            this.y_base += this.tot_h;
        this.y += d;

        if (this.scroll_bottom == this.h)
            this.scroll_bottom = new_h;
        
        /* extend the DOM terminal content */
        for(y = this.h; y < new_h; y++) {
            row_el = document.createElement("div");
            this.rows_el.push(row_el);
            this.content_el.appendChild(row_el);
        }
    }

    if (this.cur_h < this.tot_h) {
        new_cur_h = this.y_base + new_h;
        if (new_cur_h < this.cur_h) {
            /* remove lines in the scroll back buffer */
            this.lines = this.lines.slice(0, new_cur_h);
        } else if (new_cur_h > this.cur_h) {
            /* add lines in the scroll back buffer */
            c = 32 | (this.def_attr << 16);
            for(y = this.cur_h; y < new_cur_h; y++) {
                line = new Array();
                for(x = 0; x < new_w; x++)
                    line[x] = c;
                this.lines[y] = line;
            }
        }
        this.cur_h = new_cur_h;
    }
        
    this.w = new_w;
    this.h = new_h;

    if (this.y >= this.h)
        this.y = this.h - 1;

    /* reset display position */
    this.y_disp = this.y_base;
/*    
      console.log("lines.length", this.lines.length, "cur_h", this.cur_h,
      "y_base", this.y_base, "h", this.h,
      "scroll_bottom", this.scroll_bottom);
*/  
    this.refresh(0, this.h - 1);
    return true;
}