/*
 * Implementation of local line editing. Used during username and
 * password input at login time, and also by ldisc during the main
 * session (if the session's virtual terminal is in that mode).
 *
 * Because we're tied into a real GUI terminal (and not a completely
 * standalone line-discipline module that deals purely with byte
 * streams), we can support a slightly richer input interface than
 * plain bytes.
 *
 * In particular, the 'dedicated' flag sent along with every byte is
 * used to distinguish control codes input via Ctrl+letter from the
 * same code input by a dedicated key like Return or Backspace. This
 * allows us to interpret the Ctrl+letter key combination as inputting
 * a literal control character to go into the line buffer, and the
 * dedicated-key version as performing an editing function.
 */

#include "putty.h"
#include "terminal.h"

typedef struct BufChar BufChar;

struct TermLineEditor {
    Terminal *term;
    BufChar *head, *tail;
    unsigned flags;
    bool quote_next_char;
    TermLineEditorCallbackReceiver *receiver;
};

struct BufChar {
    BufChar *prev, *next;

    /* The bytes of the character, to be sent on the wire */
    char wire[6];
    uint8_t nwire;

    /* Whether this character is considered complete */
    bool complete;

    /* Width of the character when it was displayed, in terminal cells */
    uint8_t width;

    /* Whether this character counts as whitespace, for ^W purposes */
    bool space;
};

TermLineEditor *lineedit_new(Terminal *term, unsigned flags,
                             TermLineEditorCallbackReceiver *receiver)
{
    TermLineEditor *le = snew(TermLineEditor);
    le->term = term;
    le->head = le->tail = NULL;
    le->flags = flags;
    le->quote_next_char = false;
    le->receiver = receiver;
    return le;
}

static void bufchar_free(BufChar *bc)
{
    smemclr(bc, sizeof(*bc));
    sfree(bc);
}

static void lineedit_free_buffer(TermLineEditor *le)
{
    while (le->head) {
        BufChar *bc = le->head;
        le->head = bc->next;
        bufchar_free(bc);
    }
    le->tail = NULL;
}

void lineedit_free(TermLineEditor *le)
{
    lineedit_free_buffer(le);
    sfree(le);
}

void lineedit_modify_flags(TermLineEditor *le, unsigned clr, unsigned flip)
{
    le->flags &= ~clr;
    le->flags ^= flip;
}

static void lineedit_term_write(TermLineEditor *le, ptrlen data)
{
    le->receiver->vt->to_terminal(le->receiver, data);
}

static void lineedit_term_newline(TermLineEditor *le)
{
    lineedit_term_write(le, PTRLEN_LITERAL("\x0D\x0A"));
}

static inline void lineedit_send_data(TermLineEditor *le, ptrlen data)
{
    le->receiver->vt->to_backend(le->receiver, data);
}

static inline void lineedit_special(TermLineEditor *le,
                                    SessionSpecialCode code, int arg)
{
    le->receiver->vt->special(le->receiver, code, arg);
}

static inline void lineedit_send_newline(TermLineEditor *le)
{
    le->receiver->vt->newline(le->receiver);
}

static void lineedit_delete_char(TermLineEditor *le)
{
    if (le->tail) {
        BufChar *bc = le->tail;
        le->tail = bc->prev;
        if (!le->tail)
            le->head = NULL;
        else
            le->tail->next = NULL;

        for (unsigned i = 0; i < bc->width; i++)
            lineedit_term_write(le, PTRLEN_LITERAL("\x08 \x08"));

        bufchar_free(bc);
    }
}

static void lineedit_delete_word(TermLineEditor *le)
{
    /*
     * Deleting a word stops at the _start_ of a word, i.e. at any
     * boundary with a space on the left and a non-space on the right.
     */
    if (!le->tail)
        return;

    while (true) {
        bool deleted_char_is_space = le->tail->space;
        lineedit_delete_char(le);
        if (!le->tail)
            break;         /* we've cleared the whole line */
        if (le->tail->space && !deleted_char_is_space)
            break;         /* we've just reached a word boundary */
    }
}

static void lineedit_delete_line(TermLineEditor *le)
{
    while (le->tail)
        lineedit_delete_char(le);
    lineedit_special(le, SS_EL, 0);
}

void lineedit_send_line(TermLineEditor *le)
{
    bufchain output;
    bufchain_init(&output);

    for (BufChar *bc = le->head; bc; bc = bc->next)
        bufchain_add(&output, bc->wire, bc->nwire);

    while (bufchain_size(&output) > 0) {
        ptrlen data = bufchain_prefix(&output);
        lineedit_send_data(le, data);
        bufchain_consume(&output, data.len);
    }
    bufchain_clear(&output);

    lineedit_free_buffer(le);
    le->quote_next_char = false;
}

static void lineedit_complete_line(TermLineEditor *le)
{
    lineedit_term_newline(le); 
    lineedit_send_line(le);
    lineedit_send_newline(le);
}

/*
 * Send data to the terminal to display a BufChar. As a side effect,
 * update bc->width to indicate how many character cells we think were
 * taken up by what we just wrote. No other change to bc is made.
 */
static void lineedit_display_bufchar(TermLineEditor *le, BufChar *bc,
                                     unsigned chr)
{
    char buf[6];
    buffer_sink bs[1];
    buffer_sink_init(bs, buf, lenof(buf));

    /* Handle single-byte character set translation. */
    if (!in_utf(le->term) && DIRECT_CHAR(chr)) {
        /*
         * If we're not in UTF-8, i.e. we're in a single-byte
         * character set, then first we must feed the input byte
         * through term_translate, which will tell us whether it's a
         * control character or not. (That varies with the charset:
         * e.g. ISO 8859-1 and Win1252 disagree on a lot of
         * 0x80-0x9F).
         *
         * In principle, we could pass NULL as our term_utf8_decode
         * pointer, on the grounds that since the terminal isn't in
         * UTF-8 mode term_translate shouldn't access it. But that
         * seems needlessly reckless; we'll make up an empty one.
         */
        term_utf8_decode dummy_utf8 = { .state = 0, .chr = 0, .size = 0 };
        chr = term_translate(
            le->term, &dummy_utf8, (unsigned char)chr);

        /*
         * After that, chr will be either a control-character value
         * (00-1F, 7F, 80-9F), or a byte value ORed with one of the
         * CSET_FOO character set indicators. The latter indicates
         * that it's a printing character in this charset, in which
         * case it takes up one character cell.
         */
        if (chr & CSET_MASK) {
            put_byte(bs, chr);
            bc->width = 1;
            goto got_char;
        }
    }

    /*
     * If we got here without taking the 'goto' above, then we're now
     * holding an actual Unicode character.
     */
    assert(!IS_SURROGATE(chr)); /* and it should be an _actual_ one */

    /*
     * Deal with symbolic representations of control characters.
     */

    if (chr < 0x20 || chr == 0x7F) {
        /*
         * Represent C0 controls as '^C' or similar, and 7F as ^?.
         */
        put_byte(bs, '^');
        put_byte(bs, chr ^ 0x40);
        bc->width = 2;
        goto got_char;
    }

    if (chr >= 0x80 && chr < 0xA0) {
        /*
         * Represent C1 controls as <9B> or similar.
         */
        put_fmt(bs, "<%02X>", chr);
        bc->width = 4;
        goto got_char;
    }

    /*
     * And if we get _here_, we're holding a printing (or at least not
     * _control_, even if zero-width) Unicode character, which _must_
     * mean that the terminal is currently in UTF-8 mode (since if it
     * were not then printing characters would have gone through the
     * term_translate case above). So we can just write the UTF-8 for
     * the character - but we must also pay attention to its width in
     * character cells, which might be 0, 1 or 2.
     */
    assert(in_utf(le->term));
    put_utf8_char(bs, chr);
    bc->width = term_char_width(le->term, chr);

  got_char:
    lineedit_term_write(le, make_ptrlen(buf, bs->out - buf));
}

/* Called when we've just added a byte to a UTF-8 character and want
 * to see if it's complete */
static void lineedit_check_utf8_complete(TermLineEditor *le, BufChar *bc)
{
    BinarySource src[1];
    BinarySource_BARE_INIT(src, bc->wire, bc->nwire);
    DecodeUTF8Failure err;
    unsigned chr = decode_utf8(src, &err);
    if (err == DUTF8_E_OUT_OF_DATA)
        return;                        /* not complete yet */

    /* Any other error code is regarded as complete, and we just
     * display the character as the U+FFFD that decode_utf8 will have
     * returned anyway */
    bc->complete = true;
    bc->space = (chr == ' ');
    lineedit_display_bufchar(le, bc, chr);
}

static void lineedit_input_printing_char(TermLineEditor *le, char ch);

static void lineedit_redraw_line(TermLineEditor *le)
{
    /* FIXME: I'm not 100% sure this is the behaviour I really want in
     * this situation, but it's at least very simple to implement */
    BufChar *prevhead = le->head;
    le->head = le->tail = NULL;
    while (prevhead) {
        BufChar *bc = prevhead;
        prevhead = prevhead->next;

        for (unsigned i = 0; i < bc->nwire; i++)
            lineedit_input_printing_char(le, bc->wire[i]);
        bufchar_free(bc);
    }
}

#define CTRL(c) ((char) (0x40 ^ (unsigned char)c))

void lineedit_input(TermLineEditor *le, char ch, bool dedicated)
{
    if (le->quote_next_char) {
        /*
         * If the previous keypress was ^V, 'quoting' the next
         * character to be treated literally, then skip all the
         * editing-control processing, and clear that flag.
         */
        le->quote_next_char = false;
    } else {
        /*
         * Input events that are only valid with the 'dedicated' flag.
         * These are limited to the control codes that _have_
         * dedicated keys.
         *
         * Any case we actually handle here ends with a 'return'
         * statement, so that if we fall out of the end of this switch
         * at all, it's because the byte hasn't been handled here and
         * will fall into the next switch dealing with ordinary input.
         */
        if (dedicated) {
            switch (ch) {
                /*
                 * The Backspace key.
                 *
                 * Since our terminal can be configured to send either
                 * ^H or 7F (aka ^?) via the backspace key, we accept
                 * both.
                 *
                 * (We could query the Terminal's configuration here
                 * and accept only the one of those codes that the
                 * terminal is currently set to. But it's pointless,
                 * because whichever one the terminal isn't set to,
                 * the front end won't be sending it with
                 * dedicated=true anyway.)
                 */
              case CTRL('H'):
              case 0x7F:
                lineedit_delete_char(le);
                return;

                /*
                 * The Return key.
                 */
              case CTRL('M'):
                lineedit_complete_line(le);
                return;
            }
        }

        /*
         * Editing and special functions in response to ordinary keys
         * or Ctrl+key combinations. Many editing functions have to be
         * supported in this mode, like ^W and ^U, because there are
         * no dedicated keys that generate the same control codes
         * anyway.
         *
         * Again, we return if the key was handled. The final
         * processing of ordinary data to go into the input buffer
         * happens if we break from this switch.
         */
        switch (ch) {
          case CTRL('W'):
            lineedit_delete_word(le);
            return;

          case CTRL('U'):
            lineedit_delete_line(le);
            return;

          case CTRL('['):
            if (!(le->flags & LE_ESC_ERASES))
                break;             /* treat as normal input */
            lineedit_delete_line(le);
            return;

          case CTRL('R'):
            lineedit_term_write(le, PTRLEN_LITERAL("^R"));
            lineedit_term_newline(le);
            lineedit_redraw_line(le);
            return;

          case CTRL('V'):
            le->quote_next_char = true;
            return;

          case CTRL('C'):
            lineedit_delete_line(le);
            if (!(le->flags & LE_INTERRUPT))
                break;                 /* treat as normal input */
            lineedit_special(le, SS_IP, 0);
            return;

          case CTRL('Z'):
            lineedit_delete_line(le);
            if (!(le->flags & LE_SUSPEND))
                break;                 /* treat as normal input */
            lineedit_special(le, SS_SUSP, 0);
            return;

          case CTRL('\\'):
            lineedit_delete_line(le);
            if (!(le->flags & LE_ABORT))
                break;                 /* treat as normal input */
            lineedit_special(le, SS_ABORT, 0);
            return;

          case CTRL('D'):
            if (le->flags & LE_EOF_ALWAYS) {
                /* Our client wants to treat ^D / EOF as a special
                 * character in their own way. Just send an EOF
                 * special. */
                lineedit_special(le, SS_EOF, 0);
                return;
            }

            /*
             * Otherwise, ^D has the same behaviour as in Unix tty
             * line editing: if the edit buffer is non-empty then it's
             * sent immediately without a newline, and if it is empty
             * then an EOF is sent.
             */
            if (le->head) {
                lineedit_send_line(le);
                return;
            }

            lineedit_special(le, SS_EOF, 0);
            return;

          case CTRL('J'):
            if (le->flags & LE_CRLF_NEWLINE) {
                /*
                 * If the previous character in the buffer is a
                 * literal Ctrl-M, and now the user sends Ctrl-J, then
                 * we amalgamate both into a newline event.
                 */
                if (le->tail && le->tail->nwire == 1 && 
                    le->tail->wire[0] == CTRL('M')) {
                    lineedit_delete_char(le); /* erase ^J from buffer */
                    lineedit_complete_line(le);
                    return;
                }
            } else {
                /* If we're not in LE_CRLF_NEWLINE mode, then ^J by
                 * itself acts as a full newline character */
                lineedit_complete_line(le);
                return;
            }


          case CTRL('M'):
            if (le->flags & LE_CRLF_NEWLINE) {
                /* In this mode, ^M is literal, and can combine with
                 * ^J (see case above). So do nothing, and fall
                 * through into the 'treat it literally' code, */
            } else {
                /* If we're not in LE_CRLF_NEWLINE mode, then ^M by
                 * itself acts as a full newline character */
                lineedit_complete_line(le);
                return;
            }
        }
    }

    /*
     * If we get to here, we've exhausted the options for treating our
     * character as an editing or special function of any kind. Treat
     * it as a printing character, or part of one.
     */
    lineedit_input_printing_char(le, ch);
}

static void lineedit_input_printing_char(TermLineEditor *le, char ch)
{
    /*
     * Append ch to the line buffer, either as a new BufChar or by
     * adding it to a previous one containing an as yet incomplete
     * UTF-8 encoding.
     */
    if (le->tail && !le->tail->complete) {
        BufChar *bc = le->tail;

        /*
         * If we're in UTF-8 mode, and ch is a UTF-8 continuation
         * byte, then we can append it to bc, which we've just checked
         * is missing at least one of those.
         */
        if (in_utf(le->term) && (unsigned char)ch - 0x80U < 0x40) {
            assert(bc->nwire < lenof(bc->wire));
            bc->wire[bc->nwire++] = ch;
            lineedit_check_utf8_complete(le, bc);
            return;
        }

        /*
         * Otherwise, the previous incomplete character can't be
         * extended. Mark it as complete, and if possible, display it
         * as a replacement character indicating that something weird
         * happened.
         */
        bc->complete = true;
        if (in_utf(le->term))
            lineedit_display_bufchar(le, bc, 0xFFFD);

        /*
         * But we still haven't processed the byte we're holding. Fall
         * through to the next step, where we make a fresh BufChar for
         * it.
         */
    }

    /*
     * Make a fresh BufChar.
     */
    BufChar *bc = snew(BufChar);
    bc->prev = le->tail;
    le->tail = bc;
    if (bc->prev)
        bc->prev->next = bc;
    else
        le->head = bc;
    bc->next = NULL;
    bc->complete = false;
    bc->space = false;

    bc->nwire = 1;
    bc->wire[0] = ch;
    if (in_utf(le->term)) {
        lineedit_check_utf8_complete(le, bc);
    } else {
        bc->complete = true;    /* always, in a single-byte charset */
        bc->space = (bc->wire[0] == ' ');
        lineedit_display_bufchar(le, bc, CSET_ASCII | (unsigned char)ch);
    }
}
