/*
 * Copyright (C) 2010 Teluu Inc. (http://www.teluu.com)
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <pjlib-util/cli_imp.h>
#include <pjlib-util/cli_telnet.h>
#include <pj/activesock.h>
#include <pj/assert.h>
#include <pj/errno.h>
#include <pj/log.h>
#include <pj/os.h>
#include <pj/pool.h>
#include <pj/string.h>
#include <pj/except.h>
#include <pjlib-util/errno.h>
#include <pjlib-util/scanner.h>
#include <pj/addr_resolv.h>
#include <pj/compat/socket.h>

#if (defined(PJ_WIN32) && PJ_WIN32!=0) || \
    (defined(PJ_WIN64) && PJ_WIN64!=0) || \
    (defined(PJ_WIN32_WINCE) && PJ_WIN32_WINCE!=0)

/* Undefine EADDRINUSE first, we want it equal to WSAEADDRINUSE,
 * while WinSDK 10 defines it to another value.
 */
#undef EADDRINUSE
#define EADDRINUSE WSAEADDRINUSE

#endif

#define CLI_TELNET_BUF_SIZE 256

#define CUT_MSG "<..data truncated..>\r\n"
#define MAX_CUT_MSG_LEN 25

#if 1
    /* Enable some tracing */
    #define THIS_FILE   "cli_telnet.c"
    #define TRACE_(arg) PJ_LOG(3,arg)
#else
    #define TRACE_(arg)
#endif

#define MAX_CLI_TELNET_OPTIONS 256
/** Maximum retry on Telnet Restart **/
#define MAX_RETRY_ON_TELNET_RESTART 100
/** Minimum number of millisecond to wait before retrying to re-bind on
 * telnet restart **/
#define MIN_WAIT_ON_TELNET_RESTART 20
/** Maximum number of millisecod to wait before retrying to re-bind on
 *  telnet restart **/
#define MAX_WAIT_ON_TELNET_RESTART 1000

/**
 * This specify the state for the telnet option negotiation.
 */
enum cli_telnet_option_states
{
    OPT_DISABLE,                /* Option disable */
    OPT_ENABLE,                 /* Option enable */
    OPT_EXPECT_DISABLE,         /* Already send disable req, expecting resp */
    OPT_EXPECT_ENABLE,          /* Already send enable req, expecting resp */
    OPT_EXPECT_DISABLE_REV,     /* Already send disable req, expecting resp,
                                 * need to send enable req */
    OPT_EXPECT_ENABLE_REV       /* Already send enable req, expecting resp,
                                 * need to send disable req */
};

/**
 * This structure contains information for telnet session option negotiation.
 * It contains the local/peer option config and the option negotiation state.
 */
typedef struct cli_telnet_sess_option
{
    /**
     * Local setting for the option.
     * Default: FALSE;
     */
    pj_bool_t local_is_enable;

    /**
     * Remote setting for the option.
     * Default: FALSE;
     */
    pj_bool_t peer_is_enable;

    /**
     * Local state of the option negotiation.
     */
    enum cli_telnet_option_states local_state;

    /**
     * Remote state of the option negotiation.
     */
    enum cli_telnet_option_states peer_state;
} cli_telnet_sess_option;

/**
 * This specify the state of input character parsing.
 */
typedef enum cmd_parse_state
{
    ST_NORMAL,
    ST_CR,
    ST_ESC,
    ST_VT100,
    ST_IAC,
    ST_DO,
    ST_DONT,
    ST_WILL,
    ST_WONT
} cmd_parse_state;

typedef enum cli_telnet_command
{
    SUBNEGO_END     = 240,      /* End of subnegotiation parameters. */
    NOP             = 241,      /* No operation. */
    DATA_MARK       = 242,      /* Marker for NVT cleaning. */
    BREAK           = 243,      /* Indicates that the "break" key was hit. */
    INT_PROCESS     = 244,      /* Suspend, interrupt or abort the process. */
    ABORT_OUTPUT    = 245,      /* Abort output, abort output stream. */
    ARE_YOU_THERE   = 246,      /* Are you there. */
    ERASE_CHAR      = 247,      /* Erase character, erase the current char. */
    ERASE_LINE      = 248,      /* Erase line, erase the current line. */
    GO_AHEAD        = 249,      /* Go ahead, other end can transmit. */
    SUBNEGO_BEGIN   = 250,      /* Subnegotiation begin. */
    WILL            = 251,      /* Accept the use of option. */
    WONT            = 252,      /* Refuse the use of option. */
    DO              = 253,      /* Request to use option. */
    DONT            = 254,      /* Request to not use option. */
    IAC             = 255       /* Interpret as command */
} cli_telnet_command;

enum cli_telnet_options
{
    TRANSMIT_BINARY     = 0,    /* Transmit Binary. */
    TERM_ECHO           = 1,    /* Echo. */
    RECONNECT           = 2,    /* Reconnection. */
    SUPPRESS_GA         = 3,    /* Suppress Go Aheah. */
    MESSAGE_SIZE_NEGO   = 4,    /* Approx Message Size Negotiation. */
    STATUS              = 5,    /* Status. */
    TIMING_MARK         = 6,    /* Timing Mark. */
    RTCE_OPTION         = 7,    /* Remote Controlled Trans and Echo. */
    OUTPUT_LINE_WIDTH   = 8,    /* Output Line Width. */
    OUTPUT_PAGE_SIZE    = 9,    /* Output Page Size. */
    CR_DISPOSITION      = 10,   /* Carriage-Return Disposition. */
    HORI_TABSTOPS       = 11,   /* Horizontal Tabstops. */
    HORI_TAB_DISPO      = 12,   /* Horizontal Tab Disposition. */
    FF_DISP0            = 13,   /* Formfeed Disposition. */
    VERT_TABSTOPS       = 14,   /* Vertical Tabstops. */
    VERT_TAB_DISPO      = 15,   /* Vertical Tab Disposition. */
    LF_DISP0            = 16,   /* Linefeed Disposition. */
    EXT_ASCII           = 17,   /* Extended ASCII. */
    LOGOUT              = 18,   /* Logout. */
    BYTE_MACRO          = 19,   /* Byte Macro. */
    DE_TERMINAL         = 20,   /* Data Entry Terminal. */
    SUPDUP_PROTO        = 21,   /* SUPDUP Protocol. */
    SUPDUP_OUTPUT       = 22,   /* SUPDUP Output. */
    SEND_LOC            = 23,   /* Send Location. */
    TERM_TYPE           = 24,   /* Terminal Type. */
    EOR                 = 25,   /* End of Record. */
    TACACS_UID          = 26,   /* TACACS User Identification. */
    OUTPUT_MARKING      = 27,   /* Output Marking. */
    TTYLOC              = 28,   /* Terminal Location Number. */
    USE_3270_REGIME     = 29,   /* Telnet 3270 Regime. */
    USE_X3_PAD          = 30,   /* X.3 PAD. */
    WINDOW_SIZE         = 31,   /* Window Size. */
    TERM_SPEED          = 32,   /* Terminal Speed. */
    REM_FLOW_CONTROL    = 33,   /* Remote Flow Control. */
    LINE_MODE           = 34,   /* Linemode. */
    X_DISP_LOC          = 35,   /* X Display Location. */
    ENVIRONMENT         = 36,   /* Environment. */
    AUTH                = 37,   /* Authentication. */
    ENCRYPTION          = 38,   /* Encryption Option. */
    NEW_ENVIRONMENT     = 39,   /* New Environment. */
    TN_3270E            = 40,   /* TN3270E. */
    XAUTH               = 41,   /* XAUTH. */
    CHARSET             = 42,   /* CHARSET. */
    REM_SERIAL_PORT     = 43,   /* Telnet Remote Serial Port. */
    COM_PORT_CONTROL    = 44,   /* Com Port Control. */
    SUPP_LOCAL_ECHO     = 45,   /* Telnet Suppress Local Echo. */
    START_TLS           = 46,   /* Telnet Start TLS. */
    KERMIT              = 47,   /* KERMIT. */
    SEND_URL            = 48,   /* SEND-URL. */
    FWD_X               = 49,   /* FORWARD_X. */
    EXT_OPTIONS         = 255   /* Extended-Options-List */
};

enum terminal_cmd
{
    TC_ESC              = 27,
    TC_UP               = 65,
    TC_DOWN             = 66,
    TC_RIGHT            = 67,
    TC_LEFT             = 68,
    TC_END              = 70,
    TC_HOME             = 72,
    TC_CTRL_C           = 3,
    TC_CR               = 13,
    TC_BS               = 8,
    TC_TAB              = 9,
    TC_QM               = 63,
    TC_BELL             = 7,
    TC_DEL              = 127
};

/**
 * This specify the state of output character parsing.
 */
typedef enum out_parse_state
{
    OP_NORMAL,
    OP_TYPE,
    OP_SHORTCUT,
    OP_CHOICE
} out_parse_state;

/**
 * This structure contains the command line shown to the user.
 * The telnet also needs to maintain and manage command cursor position.
 * Due to that reason, the insert/delete character process from buffer will
 * consider its current cursor position.
 */
typedef struct telnet_recv_buf {
    /**
     * Buffer containing the characters, NULL terminated.
     */
    unsigned char           rbuf[PJ_CLI_MAX_CMDBUF];

    /**
     * Current length of the command line.
     */
    unsigned                len;

    /**
     * Current cursor position.
     */
    unsigned                cur_pos;
} telnet_recv_buf;

/**
 * This structure contains the command history executed by user.
 * Besides storing the command history, it is necessary to be able
 * to browse it.
 */
typedef struct cmd_history
{
    PJ_DECL_LIST_MEMBER(struct cmd_history);
    pj_str_t command;
} cmd_history;

typedef struct cli_telnet_sess
{
    pj_cli_sess             base;
    pj_pool_t               *pool;
    pj_activesock_t         *asock;
    pj_bool_t               authorized;
    pj_ioqueue_op_key_t     op_key;
    pj_mutex_t              *smutex;
    cmd_parse_state         parse_state;
    cli_telnet_sess_option  telnet_option[MAX_CLI_TELNET_OPTIONS];
    cmd_history             *history;
    cmd_history             *active_history;

    telnet_recv_buf         *rcmd;
    unsigned char           buf[CLI_TELNET_BUF_SIZE + MAX_CUT_MSG_LEN];
    unsigned                buf_len;
} cli_telnet_sess;

typedef struct cli_telnet_fe
{
    pj_cli_front_end        base;
    pj_pool_t              *pool;
    pj_cli_telnet_cfg       cfg;
    pj_bool_t               own_ioqueue;
    pj_cli_sess             sess_head;

    pj_activesock_t        *asock;
    pj_thread_t            *worker_thread;
    pj_bool_t               is_quitting;
    pj_mutex_t             *mutex;
} cli_telnet_fe;

/* Forward Declaration */
static pj_status_t telnet_sess_send2(cli_telnet_sess *sess,
                                     const unsigned char *str, int len);

static pj_status_t telnet_sess_send(cli_telnet_sess *sess,
                                    const pj_str_t *str);

static pj_status_t telnet_start(cli_telnet_fe *fe);
static pj_status_t telnet_restart(cli_telnet_fe *tfe);

/**
 * Return the number of characters between the current cursor position
 * to the end of line.
 */
static unsigned recv_buf_right_len(telnet_recv_buf *recv_buf)
{
    return (recv_buf->len - recv_buf->cur_pos);
}

/**
 * Insert character to the receive buffer.
 */
static pj_bool_t recv_buf_insert(telnet_recv_buf *recv_buf,
                                 unsigned char *data)
{
    if (recv_buf->len+1 >= PJ_CLI_MAX_CMDBUF) {
        return PJ_FALSE;
    } else {
        if (*data == '\t' || *data == '?' || *data == '\r') {
            /* Always insert to the end of line */
            recv_buf->rbuf[recv_buf->len] = *data;
        } else {
            /* Insert based on the current cursor pos */
            unsigned cur_pos = recv_buf->cur_pos;
            unsigned rlen = recv_buf_right_len(recv_buf);
            if (rlen > 0) {
                /* Shift right characters */
                pj_memmove(&recv_buf->rbuf[cur_pos+1],
                           &recv_buf->rbuf[cur_pos],
                           rlen+1);
            }
            recv_buf->rbuf[cur_pos] = *data;
        }
        ++recv_buf->cur_pos;
        ++recv_buf->len;
        recv_buf->rbuf[recv_buf->len] = 0;
    }
    return PJ_TRUE;
}

/**
 * Delete character on the previous cursor position of the receive buffer.
 */
static pj_bool_t recv_buf_backspace(telnet_recv_buf *recv_buf)
{
    if ((recv_buf->cur_pos == 0) || (recv_buf->len == 0)) {
        return PJ_FALSE;
    } else {
        unsigned rlen = recv_buf_right_len(recv_buf);
        if (rlen) {
            unsigned cur_pos = recv_buf->cur_pos;
            /* Shift left characters */
            pj_memmove(&recv_buf->rbuf[cur_pos-1], &recv_buf->rbuf[cur_pos],
                       rlen);
        }
        --recv_buf->cur_pos;
        --recv_buf->len;
        recv_buf->rbuf[recv_buf->len] = 0;
    }
    return PJ_TRUE;
}

static int compare_str(void *value, const pj_list_type *nd)
{
    cmd_history *node = (cmd_history*)nd;
    return (pj_strcmp((pj_str_t *)value, &node->command));
}

/**
 * Insert the command to history. If the entered command is not on the list,
 * a new entry will be created. All entered command will be moved to
 * the first entry of the history.
 */
static pj_status_t insert_history(cli_telnet_sess *sess,
                                  char *cmd_val)
{
    cmd_history *in_history;
    pj_str_t cmd;

    PJ_ASSERT_RETURN(sess, PJ_EINVAL);

    cmd = pj_str(cmd_val);
    pj_strtrim(&cmd);
    if (cmd.slen == 0)
        return PJ_SUCCESS;

    /* Find matching history */
    in_history = pj_list_search(sess->history, (void*)&cmd, compare_str);
    if (!in_history) {
        if (pj_list_size(sess->history) < PJ_CLI_MAX_CMD_HISTORY) {
            char *data_history;
            in_history = PJ_POOL_ZALLOC_T(sess->pool, cmd_history);
            pj_list_init(in_history);
            data_history = (char *)pj_pool_calloc(sess->pool,
                           sizeof(char), PJ_CLI_MAX_CMDBUF);
            in_history->command.ptr = data_history;
            in_history->command.slen = 0;
        } else {
            /* Get the oldest history */
            in_history = sess->history->prev;
            pj_list_erase(in_history);
        }
        pj_strncpy(&in_history->command, &cmd, PJ_CLI_MAX_CMDBUF);
    } else {
        pj_list_erase(in_history);
    }
    pj_list_push_front(sess->history, in_history);
    sess->active_history = sess->history;

    return PJ_SUCCESS;
}

/**
 * Get the next or previous history of the shown/active history.
 */
static pj_str_t* get_prev_history(cli_telnet_sess *sess, pj_bool_t is_forward)
{
    pj_str_t *retval;
    pj_size_t history_size;
    cmd_history *node;
    cmd_history *root;

    PJ_ASSERT_RETURN(sess, NULL);

    node = sess->active_history;
    root = sess->history;
    history_size = pj_list_size(sess->history);

    if (history_size == 0) {
        return NULL;
    } else {
        if (is_forward) {
            node = (node->next==root)?node->next->next:node->next;
        } else {
            node = (node->prev==root)?node->prev->prev:node->prev;
        }
        retval = &node->command;
        sess->active_history = node;
    }
    return retval;
}

/*
 * This method is used to send option negotiation command.
 * The commands dealing with option negotiation are
 * three byte sequences, the third byte being the code for the option
 * referenced - (RFC-854).
 */
static pj_bool_t send_telnet_cmd(cli_telnet_sess *sess,
                                 cli_telnet_command cmd,
                                 unsigned char option)
{
    unsigned char buf[3];
    PJ_ASSERT_RETURN(sess, PJ_FALSE);

    buf[0] = IAC;
    buf[1] = cmd;
    buf[2] = option;
    telnet_sess_send2(sess, buf, 3);

    return PJ_TRUE;
}

/**
 * This method will handle sending telnet's ENABLE option negotiation.
 * For local option: send WILL.
 * For remote option: send DO.
 * This method also handle the state transition of the ENABLE
 * negotiation process.
 */
static pj_bool_t send_enable_option(cli_telnet_sess *sess,
                                    pj_bool_t is_local,
                                    unsigned char option)
{
    cli_telnet_sess_option *sess_option;
    enum cli_telnet_option_states *state;
    PJ_ASSERT_RETURN(sess, PJ_FALSE);

    sess_option = &sess->telnet_option[option];
    state = is_local?(&sess_option->local_state):(&sess_option->peer_state);
    switch (*state) {
        case OPT_ENABLE:
            /* Ignore if already enabled */
            break;
        case OPT_DISABLE:
            *state = OPT_EXPECT_ENABLE;
            send_telnet_cmd(sess, (is_local?WILL:DO), option);
            break;
        case OPT_EXPECT_ENABLE:
            *state = OPT_DISABLE;
            break;
        case OPT_EXPECT_DISABLE:
            *state = OPT_EXPECT_DISABLE_REV;
            break;
        case OPT_EXPECT_ENABLE_REV:
            *state = OPT_EXPECT_ENABLE;
            break;
        case OPT_EXPECT_DISABLE_REV:
            *state = OPT_DISABLE;
            break;
        default:
            return PJ_FALSE;
    }
    return PJ_TRUE;
}

static pj_bool_t send_cmd_do(cli_telnet_sess *sess,
                             unsigned char option)
{
    return send_enable_option(sess, PJ_FALSE, option);
}

static pj_bool_t send_cmd_will(cli_telnet_sess *sess,
                               unsigned char option)
{
    return send_enable_option(sess, PJ_TRUE, option);
}

/**
 * This method will handle receiving telnet's ENABLE option negotiation.
 * This method also handle the state transition of the ENABLE
 * negotiation process.
 */
static pj_bool_t receive_enable_option(cli_telnet_sess *sess,
                                       pj_bool_t is_local,
                                       unsigned char option)
{
    cli_telnet_sess_option *sess_opt;
    enum cli_telnet_option_states *state;
    pj_bool_t opt_ena;
    PJ_ASSERT_RETURN(sess, PJ_FALSE);

    sess_opt = &sess->telnet_option[option];
    state = is_local?(&sess_opt->local_state):(&sess_opt->peer_state);
    opt_ena = is_local?sess_opt->local_is_enable:sess_opt->peer_is_enable;
    switch (*state) {
        case OPT_ENABLE:
            /* Ignore if already enabled */
            break;
        case OPT_DISABLE:
            if (opt_ena) {
                *state = OPT_ENABLE;
                send_telnet_cmd(sess, is_local?WILL:DO, option);
            } else {
                send_telnet_cmd(sess, is_local?WONT:DONT, option);
            }
            break;
        case OPT_EXPECT_ENABLE:
            *state = OPT_ENABLE;
            break;
        case OPT_EXPECT_DISABLE:
            *state = OPT_DISABLE;
            break;
        case OPT_EXPECT_ENABLE_REV:
            *state = OPT_EXPECT_DISABLE;
            send_telnet_cmd(sess, is_local?WONT:DONT, option);
            break;
        case OPT_EXPECT_DISABLE_REV:
            *state = OPT_EXPECT_DISABLE;
            break;
        default:
            return PJ_FALSE;
    }
    return PJ_TRUE;
}

/**
 * This method will handle receiving telnet's DISABLE option negotiation.
 * This method also handle the state transition of the DISABLE
 * negotiation process.
 */
static pj_bool_t receive_disable_option(cli_telnet_sess *sess,
                                        pj_bool_t is_local,
                                        unsigned char option)
{
    cli_telnet_sess_option *sess_opt;
    enum cli_telnet_option_states *state;

    PJ_ASSERT_RETURN(sess, PJ_FALSE);

    sess_opt = &sess->telnet_option[option];
    state = is_local?(&sess_opt->local_state):(&sess_opt->peer_state);

    switch (*state) {
        case OPT_ENABLE:
            /* Disabling option always need to be accepted */
            *state = OPT_DISABLE;
            send_telnet_cmd(sess, is_local?WONT:DONT, option);
            break;
        case OPT_DISABLE:
            /* Ignore if already enabled */
            break;
        case OPT_EXPECT_ENABLE:
        case OPT_EXPECT_DISABLE:
            *state = OPT_DISABLE;
            break;
        case OPT_EXPECT_ENABLE_REV:
            *state = OPT_DISABLE;
            send_telnet_cmd(sess, is_local?WONT:DONT, option);
            break;
        case OPT_EXPECT_DISABLE_REV:
            *state = OPT_EXPECT_ENABLE;
            send_telnet_cmd(sess, is_local?WILL:DO, option);
            break;
        default:
            return PJ_FALSE;
    }
    return PJ_TRUE;
}

static pj_bool_t receive_do(cli_telnet_sess *sess, unsigned char option)
{
    return receive_enable_option(sess, PJ_TRUE, option);
}

static pj_bool_t receive_dont(cli_telnet_sess *sess, unsigned char option)
{
    return receive_disable_option(sess, PJ_TRUE, option);
}

static pj_bool_t receive_will(cli_telnet_sess *sess, unsigned char option)
{
    return receive_enable_option(sess, PJ_FALSE, option);
}

static pj_bool_t receive_wont(cli_telnet_sess *sess, unsigned char option)
{
    return receive_disable_option(sess, PJ_FALSE, option);
}

static void set_local_option(cli_telnet_sess *sess,
                             unsigned char option,
                             pj_bool_t enable)
{
    sess->telnet_option[option].local_is_enable = enable;
}

static void set_peer_option(cli_telnet_sess *sess,
                            unsigned char option,
                            pj_bool_t enable)
{
    sess->telnet_option[option].peer_is_enable = enable;
}

static pj_bool_t is_local_option_state_ena(cli_telnet_sess *sess,
                                           unsigned char option)
{
    return (sess->telnet_option[option].local_state == OPT_ENABLE);
}

static void send_return_key(cli_telnet_sess *sess)
{
    telnet_sess_send2(sess, (unsigned char*)"\r\n", 2);
}

static void send_bell(cli_telnet_sess *sess) {
    static const unsigned char bell = 0x07;
    telnet_sess_send2(sess, &bell, 1);
}

static void send_prompt_str(cli_telnet_sess *sess)
{
    pj_str_t send_data;
    char data_str[128];
    cli_telnet_fe *fe = (cli_telnet_fe *)sess->base.fe;

    send_data.ptr = data_str;
    send_data.slen = 0;

    pj_strcat(&send_data, &fe->cfg.prompt_str);

    telnet_sess_send(sess, &send_data);
}

/*
 * This method is used to send error message to client, including
 * the error position of the source command.
 */
static void send_err_arg(cli_telnet_sess *sess,
                         const pj_cli_exec_info *info,
                         const pj_str_t *msg,
                         pj_bool_t with_return,
                         pj_bool_t with_last_cmd)
{
    pj_str_t send_data;
    char data_str[256];
    pj_size_t len;
    unsigned i;
    cli_telnet_fe *fe = (cli_telnet_fe *)sess->base.fe;

    send_data.ptr = data_str;
    send_data.slen = 0;

    if (with_return)
        pj_strcat2(&send_data, "\r\n");

    len = fe->cfg.prompt_str.slen + info->err_pos;

    /* Set the error pointer mark */
    for (i=0;i<len;++i) {
        pj_strcat2(&send_data, " ");
    }
    pj_strcat2(&send_data, "^");
    pj_strcat2(&send_data, "\r\n");
    pj_strcat(&send_data, msg);
    pj_strcat(&send_data, &fe->cfg.prompt_str);
    if (with_last_cmd)
        pj_strcat2(&send_data, (char *)sess->rcmd->rbuf);

    telnet_sess_send(sess, &send_data);
}

static void send_inv_arg(cli_telnet_sess *sess,
                         const pj_cli_exec_info *info,
                         pj_bool_t with_return,
                         pj_bool_t with_last_cmd)
{
    static const pj_str_t ERR_MSG = {"%Error : Invalid Arguments\r\n", 28};
    send_err_arg(sess, info, &ERR_MSG, with_return, with_last_cmd);
}

static void send_too_many_arg(cli_telnet_sess *sess,
                              const pj_cli_exec_info *info,
                              pj_bool_t with_return,
                              pj_bool_t with_last_cmd)
{
    static const pj_str_t ERR_MSG = {"%Error : Too Many Arguments\r\n", 29};
    send_err_arg(sess, info, &ERR_MSG, with_return, with_last_cmd);
}

static void send_hint_arg(cli_telnet_sess *sess,
                          pj_str_t *send_data,
                          const pj_str_t *desc,
                          pj_ssize_t cmd_len,
                          pj_ssize_t max_len)
{
    if ((desc) && (desc->slen > 0)) {
        int j;

        for (j=0;j<(max_len-cmd_len);++j) {
            pj_strcat2(send_data, " ");
        }
        pj_strcat2(send_data, "  ");
        pj_strcat(send_data, desc);
        telnet_sess_send(sess, send_data);
        send_data->slen = 0;
    }
}

/*
 * This method is used to notify to the client that the entered command
 * is ambiguous. It will show the matching command as the hint information.
 */
static void send_ambi_arg(cli_telnet_sess *sess,
                          const pj_cli_exec_info *info,
                          pj_bool_t with_return,
                          pj_bool_t with_last_cmd)
{
    unsigned i;
    pj_size_t len;
    pj_str_t send_data;
    char data[1028];
    cli_telnet_fe *fe = (cli_telnet_fe *)sess->base.fe;
    const pj_cli_hint_info *hint = info->hint;
    out_parse_state parse_state = OP_NORMAL;
    pj_ssize_t max_length = 0;
    pj_ssize_t cmd_length = 0;
    static const pj_str_t sc_type = {"sc", 2};
    static const pj_str_t choice_type = {"choice", 6};
    send_data.ptr = data;
    send_data.slen = 0;

    if (with_return)
        pj_strcat2(&send_data, "\r\n");

    len = fe->cfg.prompt_str.slen + info->err_pos;

    for (i=0;i<len;++i) {
        pj_strcat2(&send_data, " ");
    }
    pj_strcat2(&send_data, "^");
    /* Get the max length of the command name */
    for (i=0;i<info->hint_cnt;++i) {
        if (hint[i].type.slen > 0) {
            if (pj_stricmp(&hint[i].type, &sc_type) == 0) {
                if ((i > 0) && (!pj_stricmp(&hint[i-1].desc, &hint[i].desc))) {
                    cmd_length += (hint[i].name.slen + 3);
                } else {
                    cmd_length = hint[i].name.slen;
                }
            } else {
                cmd_length = hint[i].name.slen;
            }
        } else {
            cmd_length = hint[i].name.slen;
        }

        if (cmd_length > max_length) {
            max_length = cmd_length;
        }
    }

    cmd_length = 0;
    /* Build hint information */
    for (i=0;i<info->hint_cnt;++i) {
        if (hint[i].type.slen > 0) {
            if (pj_stricmp(&hint[i].type, &sc_type) == 0) {
                parse_state = OP_SHORTCUT;
            } else if (pj_stricmp(&hint[i].type, &choice_type) == 0) {
                parse_state = OP_CHOICE;
            } else {
                parse_state = OP_TYPE;
            }
        } else {
            parse_state = OP_NORMAL;
        }

        if (parse_state != OP_SHORTCUT) {
            pj_strcat2(&send_data, "\r\n  ");
            cmd_length = hint[i].name.slen;
        }

        switch (parse_state) {
        case OP_CHOICE:
            /* Format : "[Choice Value]  description" */
            pj_strcat2(&send_data, "[");
            pj_strcat(&send_data, &hint[i].name);
            pj_strcat2(&send_data, "]");
            break;
        case OP_TYPE:
            /* Format : "<Argument Type>  description" */
            pj_strcat2(&send_data, "<");
            pj_strcat(&send_data, &hint[i].name);
            pj_strcat2(&send_data, ">");
            break;
        case OP_SHORTCUT:
            /* Format : "Command | sc |  description" */
            {
                cmd_length += hint[i].name.slen;
                if ((i > 0) && (!pj_stricmp(&hint[i-1].desc, &hint[i].desc))) {
                    pj_strcat2(&send_data, " | ");
                    cmd_length += 3;
                } else {
                    pj_strcat2(&send_data, "\r\n  ");
                }
                pj_strcat(&send_data, &hint[i].name);
            }
            break;
        default:
            /* Command */
            pj_strcat(&send_data, &hint[i].name);
            break;
        }

        if ((parse_state == OP_TYPE) || (parse_state == OP_CHOICE) ||
            ((i+1) >= info->hint_cnt) ||
            (pj_strncmp(&hint[i].desc, &hint[i+1].desc, hint[i].desc.slen)))
        {
            /* Add description info */
            send_hint_arg(sess, &send_data,
                          &hint[i].desc, cmd_length,
                          max_length);

            cmd_length = 0;
        }
    }
    pj_strcat2(&send_data, "\r\n");
    pj_strcat(&send_data, &fe->cfg.prompt_str);
    if (with_last_cmd)
        pj_strcat2(&send_data, (char *)sess->rcmd->rbuf);

    telnet_sess_send(sess, &send_data);
}

/*
 * This method is to send command completion of the entered command.
 */
static void send_comp_arg(cli_telnet_sess *sess,
                          pj_cli_exec_info *info)
{
    pj_str_t send_data;
    char data[128];

    pj_strcat2(&info->hint[0].name, " ");

    send_data.ptr = data;
    send_data.slen = 0;

    pj_strcat(&send_data, &info->hint[0].name);

    telnet_sess_send(sess, &send_data);
}

/*
 * This method is to process the alfa numeric character sent by client.
 */
static pj_bool_t handle_alfa_num(cli_telnet_sess *sess, unsigned char *data)
{
    if (is_local_option_state_ena(sess, TERM_ECHO)) {
        if (recv_buf_right_len(sess->rcmd) > 0) {
            /* Cursor is not at EOL, insert character */
            unsigned char echo[5] = {0x1b, 0x5b, 0x31, 0x40, 0x00};
            echo[4] = *data;
            telnet_sess_send2(sess, echo, 5);
        } else {
            /* Append character */
            telnet_sess_send2(sess, data, 1);
        }
        return PJ_TRUE;
    }
    return PJ_FALSE;
}

/*
 * This method is to process the backspace character sent by client.
 */
static pj_bool_t handle_backspace(cli_telnet_sess *sess, unsigned char *data)
{
    unsigned rlen = recv_buf_right_len(sess->rcmd);
    if (recv_buf_backspace(sess->rcmd)) {
        if (rlen) {
            /*
             * Cursor is not at the end of line, move the characters
             * after the cursor to left
             */
            unsigned char echo[5] = {0x00, 0x1b, 0x5b, 0x31, 0x50};
            echo[0] = *data;
            telnet_sess_send2(sess, echo, 5);
        } else {
            unsigned char echo[3] = {0x08, 0x20, 0x08};
            telnet_sess_send2(sess, echo, 3);
        }
        return PJ_TRUE;
    }
    return PJ_FALSE;
}

/*
 * Syntax error handler for parser.
 */
static void on_syntax_error(pj_scanner *scanner)
{
    PJ_UNUSED_ARG(scanner);
    PJ_THROW(PJ_EINVAL);
}

/*
 * This method is to process the backspace character sent by client.
 */
static pj_status_t get_last_token(pj_str_t *cmd, pj_str_t *str)
{
    pj_scanner scanner;
    PJ_USE_EXCEPTION;
    pj_scan_init(&scanner, cmd->ptr, cmd->slen, PJ_SCAN_AUTOSKIP_WS,
                 &on_syntax_error);
    PJ_TRY {
        while (!pj_scan_is_eof(&scanner)) {
            pj_scan_get_until_chr(&scanner, " \t\r\n", str);
        }
    }
    PJ_CATCH_ANY {
        pj_scan_fini(&scanner);
        return PJ_GET_EXCEPTION();
    }
    PJ_END;
    
    pj_scan_fini(&scanner);
    return PJ_SUCCESS;
}

/*
 * This method is to process the tab character sent by client.
 */
static pj_bool_t handle_tab(cli_telnet_sess *sess)
{
    pj_status_t status;
    pj_bool_t retval = PJ_TRUE;
    unsigned len;

    pj_pool_t *pool;
    pj_cli_cmd_val *cmd_val;
    pj_cli_exec_info info;
    pool = pj_pool_create(sess->pool->factory, "handle_tab",
                          PJ_CLI_TELNET_POOL_SIZE, PJ_CLI_TELNET_POOL_INC,
                          NULL);

    cmd_val = PJ_POOL_ZALLOC_T(pool, pj_cli_cmd_val);

    status = pj_cli_sess_parse(&sess->base, (char *)&sess->rcmd->rbuf, cmd_val,
                               pool, &info);

    len = (unsigned)pj_ansi_strlen((char *)sess->rcmd->rbuf);

    switch (status) {
    case PJ_CLI_EINVARG:
        send_inv_arg(sess, &info, PJ_TRUE, PJ_TRUE);
        break;
    case PJ_CLI_ETOOMANYARGS:
        send_too_many_arg(sess, &info, PJ_TRUE, PJ_TRUE);
        break;
    case PJ_CLI_EMISSINGARG:
    case PJ_CLI_EAMBIGUOUS:
        send_ambi_arg(sess, &info, PJ_TRUE, PJ_TRUE);
        break;
    case PJ_SUCCESS:
        if (len > sess->rcmd->cur_pos)
        {
            /* Send the cursor to EOL */
            unsigned rlen = len - sess->rcmd->cur_pos+1;
            unsigned char *data_sent = &sess->rcmd->rbuf[sess->rcmd->cur_pos-1];
            telnet_sess_send2(sess, data_sent, rlen);
        }
        if (info.hint_cnt > 0) {
            /* Complete command */
            pj_str_t cmd = pj_str((char *)sess->rcmd->rbuf);
            pj_str_t last_token;

            if (get_last_token(&cmd, &last_token) == PJ_SUCCESS) {
                /* Hint contains the match to the last command entered */
                pj_str_t *hint_info = &info.hint[0].name;
                pj_strtrim(&last_token);
                if (hint_info->slen >= last_token.slen) {
                    hint_info->slen -= last_token.slen;
                    pj_memmove(hint_info->ptr,
                               &hint_info->ptr[last_token.slen],
                               hint_info->slen);
                }
                send_comp_arg(sess, &info);

                pj_memcpy(&sess->rcmd->rbuf[len], info.hint[0].name.ptr,
                          info.hint[0].name.slen);

                len += (unsigned)info.hint[0].name.slen;
                sess->rcmd->rbuf[len] = 0;
            }
        } else {
            retval = PJ_FALSE;
        }
        break;
    }
    sess->rcmd->len = len;
    sess->rcmd->cur_pos = sess->rcmd->len;

    pj_pool_release(pool);
    return retval;
}

/*
 * This method is to process the return character sent by client.
 */
static pj_bool_t handle_return(cli_telnet_sess *sess)
{
    pj_status_t status;
    pj_bool_t retval = PJ_TRUE;

    pj_pool_t *pool;
    pj_cli_exec_info info;

    send_return_key(sess);
    insert_history(sess, (char *)&sess->rcmd->rbuf);

    pool = pj_pool_create(sess->pool->factory, "handle_return",
                          PJ_CLI_TELNET_POOL_SIZE, PJ_CLI_TELNET_POOL_INC,
                          NULL);

    status = pj_cli_sess_exec(&sess->base, (char *)&sess->rcmd->rbuf,
                              pool, &info);

    switch (status) {
    case PJ_CLI_EINVARG:
        send_inv_arg(sess, &info, PJ_FALSE, PJ_FALSE);
        break;
    case PJ_CLI_ETOOMANYARGS:
        send_too_many_arg(sess, &info, PJ_FALSE, PJ_FALSE);
        break;
    case PJ_CLI_EAMBIGUOUS:
    case PJ_CLI_EMISSINGARG:
        send_ambi_arg(sess, &info, PJ_FALSE, PJ_FALSE);
        break;
    case PJ_CLI_EEXIT:
        retval = PJ_FALSE;
        break;
    case PJ_SUCCESS:
        send_prompt_str(sess);
        break;
    }
    if (retval) {
        sess->rcmd->rbuf[0] = 0;
        sess->rcmd->len = 0;
        sess->rcmd->cur_pos = sess->rcmd->len;
    }

    pj_pool_release(pool);
    return retval;
}

/*
 * This method is to process the right key character sent by client.
 */
static pj_bool_t handle_right_key(cli_telnet_sess *sess)
{
    if (recv_buf_right_len(sess->rcmd)) {
        unsigned char *data = &sess->rcmd->rbuf[sess->rcmd->cur_pos++];
        telnet_sess_send2(sess, data, 1);
        return PJ_TRUE;
    }
    return PJ_FALSE;
}

/*
 * This method is to process the left key character sent by client.
 */
static pj_bool_t handle_left_key(cli_telnet_sess *sess)
{
    static const unsigned char move_cursor_left = 0x08;
    if (sess->rcmd->cur_pos) {
        telnet_sess_send2(sess, &move_cursor_left, 1);
        --sess->rcmd->cur_pos;
        return PJ_TRUE;
    }
    return PJ_FALSE;
}

/*
 * This method is to process the up/down key character sent by client.
 */
static pj_bool_t handle_up_down(cli_telnet_sess *sess, pj_bool_t is_up)
{
    pj_str_t *history;

    PJ_ASSERT_RETURN(sess, PJ_FALSE);

    history = get_prev_history(sess, is_up);
    if (history) {
        pj_str_t send_data;
        char str[PJ_CLI_MAX_CMDBUF];
        enum {
            MOVE_CURSOR_LEFT = 0x08,
            CLEAR_CHAR = 0x20
        };
        send_data.ptr = str;
        send_data.slen = 0;

        /* Move cursor position to the beginning of line */
        if (sess->rcmd->cur_pos > 0) {
            pj_memset(send_data.ptr, MOVE_CURSOR_LEFT, sess->rcmd->cur_pos);
            send_data.slen = sess->rcmd->cur_pos;
        }

        if (sess->rcmd->len > (unsigned)history->slen) {
            /* Clear the command currently shown*/
            unsigned buf_len = sess->rcmd->len;
            pj_memset(&send_data.ptr[send_data.slen], CLEAR_CHAR, buf_len);
            send_data.slen += buf_len;

            /* Move cursor position to the beginning of line */
            pj_memset(&send_data.ptr[send_data.slen], MOVE_CURSOR_LEFT,
                      buf_len);
            send_data.slen += buf_len;
        }
        /* Send data */
        pj_strcat(&send_data, history);
        telnet_sess_send(sess, &send_data);
        pj_ansi_strxcpy2((char*)sess->rcmd->rbuf, history, 
                         sizeof(sess->rcmd->rbuf));
        sess->rcmd->len = (unsigned)history->slen;
        sess->rcmd->cur_pos = sess->rcmd->len;
        return PJ_TRUE;
    }
    return PJ_FALSE;
}

static pj_status_t process_vt100_cmd(cli_telnet_sess *sess,
                                     unsigned char *cmd)
{
    pj_status_t status = PJ_TRUE;
    switch (*cmd) {
        case TC_ESC:
            break;
        case TC_UP:
            status = handle_up_down(sess, PJ_TRUE);
            break;
        case TC_DOWN:
            status = handle_up_down(sess, PJ_FALSE);
            break;
        case TC_RIGHT:
            status = handle_right_key(sess);
            break;
        case TC_LEFT:
            status = handle_left_key(sess);
            break;
        case TC_END:
            break;
        case TC_HOME:
            break;
        case TC_CTRL_C:
            break;
        case TC_CR:
            break;
        case TC_BS:
            break;
        case TC_TAB:
            break;
        case TC_QM:
            break;
        case TC_BELL:
            break;
        case TC_DEL:
            break;
    };
    return status;
}

PJ_DEF(void) pj_cli_telnet_cfg_default(pj_cli_telnet_cfg *param)
{
    pj_assert(param);

    pj_bzero(param, sizeof(*param));
    param->port = PJ_CLI_TELNET_PORT;
    param->log_level = PJ_CLI_TELNET_LOG_LEVEL;
}

/*
 * Send a message to a telnet session
 */
static pj_status_t telnet_sess_send(cli_telnet_sess *sess,
                                    const pj_str_t *str)
{
    pj_ssize_t sz;
    pj_status_t status = PJ_SUCCESS;

    sz = str->slen;
    if (!sz)
        return PJ_SUCCESS;

    pj_mutex_lock(sess->smutex);

    if (sess->buf_len == 0)
        status = pj_activesock_send(sess->asock, &sess->op_key,
                                    str->ptr, &sz, 0);
    /* If we cannot send now, append it at the end of the buffer
     * to be sent later.
     */
    if (sess->buf_len > 0 ||
        (status != PJ_SUCCESS && status != PJ_EPENDING))
    {
        int clen = (int)sz;

        if (sess->buf_len + clen > CLI_TELNET_BUF_SIZE)
            clen = CLI_TELNET_BUF_SIZE - sess->buf_len;
        if (clen > 0)
            pj_memmove(sess->buf + sess->buf_len, str->ptr, clen);
        if (clen < sz) {
            pj_ansi_snprintf((char *)sess->buf + CLI_TELNET_BUF_SIZE,
                             MAX_CUT_MSG_LEN, CUT_MSG);
            sess->buf_len = (unsigned)(CLI_TELNET_BUF_SIZE +
                            pj_ansi_strlen((char *)sess->buf+
                                            CLI_TELNET_BUF_SIZE));
        } else
            sess->buf_len += clen;
    } else if (status == PJ_SUCCESS && sz < str->slen) {
        pj_mutex_unlock(sess->smutex);
        return PJ_CLI_ETELNETLOST;
    }

    pj_mutex_unlock(sess->smutex);

    return PJ_SUCCESS;
}

/*
 * Send a message to a telnet session with formatted text
 * (add single linefeed character with carriage return)
 */
static pj_status_t telnet_sess_send_with_format(cli_telnet_sess *sess,
                                                const pj_str_t *str)
{
    pj_scanner scanner;
    pj_str_t out_str;
    static const pj_str_t CR_LF = {("\r\n"), 2};
    int str_len = 0;
    char *str_begin = 0;

    PJ_USE_EXCEPTION;

    pj_scan_init(&scanner, str->ptr, str->slen,
                 PJ_SCAN_AUTOSKIP_WS, &on_syntax_error);

    str_begin = scanner.begin;

    PJ_TRY {
        while (!pj_scan_is_eof(&scanner)) {
            pj_scan_get_until_ch(&scanner, '\n', &out_str);
            str_len = (int)(scanner.curptr - str_begin);
            if (*scanner.curptr == '\n') {
                if ((str_len > 1) && (out_str.ptr[str_len-2] == '\r'))
                {
                    continue;
                } else {
                    int str_pos = (int)(str_begin - scanner.begin);

                    if (str_len > 0) {
                        pj_str_t s;
                        pj_strset(&s, &str->ptr[str_pos], str_len);
                        telnet_sess_send(sess, &s);
                    }
                    telnet_sess_send(sess, &CR_LF);

                    if (!pj_scan_is_eof(&scanner)) {
                        pj_scan_advance_n(&scanner, 1, PJ_TRUE);
                        str_begin = scanner.curptr;
                    }
                }
            } else {
                pj_str_t s;
                int str_pos = (int)(str_begin - scanner.begin);

                pj_strset(&s, &str->ptr[str_pos], str_len);
                telnet_sess_send(sess, &s);
            }
        }
    }
    PJ_CATCH_ANY {
        pj_scan_fini(&scanner);
        return (PJ_GET_EXCEPTION());
    }
    PJ_END;

    pj_scan_fini(&scanner);
    return PJ_SUCCESS;
}

static pj_status_t telnet_sess_send2(cli_telnet_sess *sess,
                                     const unsigned char *str, int len)
{
    pj_str_t s;

    pj_strset(&s, (char *)str, len);
    return telnet_sess_send(sess, &s);
}

static void telnet_sess_destroy(pj_cli_sess *sess)
{
    cli_telnet_sess *tsess = (cli_telnet_sess *)sess;
    pj_mutex_t *mutex = ((cli_telnet_fe *)sess->fe)->mutex;

    pj_mutex_lock(mutex);
    pj_list_erase(sess);
    pj_mutex_unlock(mutex);

    pj_mutex_lock(tsess->smutex);
    pj_mutex_unlock(tsess->smutex);
    pj_activesock_close(tsess->asock);
    pj_mutex_destroy(tsess->smutex);
    pj_pool_release(tsess->pool);
}

static void telnet_fe_write_log(pj_cli_front_end *fe, int level,
                                const char *data, pj_size_t len)
{
    cli_telnet_fe *tfe = (cli_telnet_fe *)fe;
    pj_cli_sess *sess;

    pj_mutex_lock(tfe->mutex);

    sess = tfe->sess_head.next;
    while (sess != &tfe->sess_head) {
        cli_telnet_sess *tsess = (cli_telnet_sess *)sess;

        sess = sess->next;
        if (tsess->base.log_level >= level) {
            pj_str_t s;

            pj_strset(&s, (char *)data, len);
            telnet_sess_send_with_format(tsess, &s);
        }
    }

    pj_mutex_unlock(tfe->mutex);
}

static void telnet_fe_destroy(pj_cli_front_end *fe)
{
    cli_telnet_fe *tfe = (cli_telnet_fe *)fe;
    pj_cli_sess *sess;

    tfe->is_quitting = PJ_TRUE;
    if (tfe->worker_thread) {
        pj_thread_join(tfe->worker_thread);
    }

    pj_mutex_lock(tfe->mutex);

    /* Destroy all the sessions */
    sess = tfe->sess_head.next;
    while (sess != &tfe->sess_head) {
        (*sess->op->destroy)(sess);
        sess = tfe->sess_head.next;
    }

    pj_mutex_unlock(tfe->mutex);

    if (tfe->asock) {
        pj_activesock_close(tfe->asock);
        tfe->asock = NULL;
    }

    if (tfe->own_ioqueue && tfe->cfg.ioqueue) {
        pj_ioqueue_destroy(tfe->cfg.ioqueue);
        tfe->cfg.ioqueue = NULL;
    }

    if (tfe->worker_thread) {
        pj_thread_destroy(tfe->worker_thread);
        tfe->worker_thread = NULL;
    }

    pj_mutex_destroy(tfe->mutex);

    pj_pool_release(tfe->pool);
}

static int poll_worker_thread(void *p)
{
    cli_telnet_fe *fe = (cli_telnet_fe *)p;

    while (!fe->is_quitting) {
        pj_time_val delay = {0, 50};
        pj_ioqueue_poll(fe->cfg.ioqueue, &delay);
    }

    return 0;
}

static pj_bool_t telnet_sess_on_data_sent(pj_activesock_t *asock,
                                          pj_ioqueue_op_key_t *op_key,
                                          pj_ssize_t sent)
{
    cli_telnet_sess *sess = (cli_telnet_sess *)
                            pj_activesock_get_user_data(asock);

    PJ_UNUSED_ARG(op_key);

    if (sent <= 0) {
        TRACE_((THIS_FILE, "Error On data send"));
        pj_cli_sess_end_session(&sess->base);
        return PJ_FALSE;
    }

    pj_mutex_lock(sess->smutex);

    if (sess->buf_len) {
        int len = sess->buf_len;

        sess->buf_len = 0;
        if (telnet_sess_send2(sess, sess->buf, len) != PJ_SUCCESS) {
            pj_mutex_unlock(sess->smutex);
            pj_cli_sess_end_session(&sess->base);
            return PJ_FALSE;
        }
    }

    pj_mutex_unlock(sess->smutex);

    return PJ_TRUE;
}

static pj_bool_t telnet_sess_on_data_read(pj_activesock_t *asock,
                                          void *data,
                                          pj_size_t size,
                                          pj_status_t status,
                                          pj_size_t *remainder)
{
    cli_telnet_sess *sess = (cli_telnet_sess *)
                            pj_activesock_get_user_data(asock);
    cli_telnet_fe *tfe = (cli_telnet_fe *)sess->base.fe;
    unsigned char *cdata = (unsigned char*)data;
    pj_status_t is_valid = PJ_TRUE;

    PJ_UNUSED_ARG(size);
    PJ_UNUSED_ARG(remainder);

    if (tfe->is_quitting)
        return PJ_FALSE;

    if (status != PJ_SUCCESS && status != PJ_EPENDING) {
        TRACE_((THIS_FILE, "Error on data read %d", status));
        pj_cli_sess_end_session(&sess->base);
        return PJ_FALSE;
    }

    pj_mutex_lock(sess->smutex);

    switch (sess->parse_state) {
        case ST_CR:
            sess->parse_state = ST_NORMAL;
            if (*cdata == 0 || *cdata == '\n') {                
                pj_mutex_unlock(sess->smutex);
                is_valid = handle_return(sess);
                if (!is_valid) {
                    // handle_return() can only return PJ_FALSE if
                    // pj_cli_sess_exec() returns PJ_CLI_EEXIT,
                    // in which case CLI session has been ended by
                    // cmd_handler() of CLI_CMD_EXIT.
                    //
                    // pj_cli_sess_end_session(&sess->base);
                    return PJ_FALSE;
                }
                pj_mutex_lock(sess->smutex);
            }
            break;
        case ST_NORMAL:
            if (*cdata == IAC) {
                sess->parse_state = ST_IAC;
            } else if (*cdata == 127) {
                is_valid = handle_backspace(sess, cdata);
            } else if (*cdata == 27) {
                sess->parse_state = ST_ESC;
            } else {
                if (recv_buf_insert(sess->rcmd, cdata)) {
                    if (*cdata == '\r') {
                        sess->parse_state = ST_CR;
                    } else if ((*cdata == '\t') || (*cdata == '?')) {
                        is_valid = handle_tab(sess);
                    } else if (*cdata > 31 && *cdata < 127) {
                        is_valid = handle_alfa_num(sess, cdata);
                    }
                } else {
                    is_valid = PJ_FALSE;
                }
            }
            break;
        case ST_ESC:
            if (*cdata == 91) {
                sess->parse_state = ST_VT100;
            } else {
                sess->parse_state = ST_NORMAL;
            }
            break;
        case ST_VT100:
            sess->parse_state = ST_NORMAL;
            is_valid = process_vt100_cmd(sess, cdata);
            break;
        case ST_IAC:
            switch ((unsigned) *cdata) {
                case DO:
                    sess->parse_state = ST_DO;
                    break;
                case DONT:
                    sess->parse_state = ST_DONT;
                    break;
                case WILL:
                    sess->parse_state = ST_WILL;
                    break;
                case WONT:
                    sess->parse_state = ST_WONT;
                    break;
                default:
                    sess->parse_state = ST_NORMAL;
                    break;
            }
            break;
        case ST_DO:
            receive_do(sess, *cdata);
            sess->parse_state = ST_NORMAL;
            break;
        case ST_DONT:
            receive_dont(sess, *cdata);
            sess->parse_state = ST_NORMAL;
            break;
        case ST_WILL:
            receive_will(sess, *cdata);
            sess->parse_state = ST_NORMAL;
            break;
        case ST_WONT:
            receive_wont(sess, *cdata);
            sess->parse_state = ST_NORMAL;
            break;
        default:
            sess->parse_state = ST_NORMAL;
            break;
    }
    if (!is_valid) {
        send_bell(sess);
    }

    pj_mutex_unlock(sess->smutex);

    return PJ_TRUE;
}

static pj_bool_t telnet_fe_on_accept(pj_activesock_t *asock,
                                     pj_sock_t newsock,
                                     const pj_sockaddr_t *src_addr,
                                     int src_addr_len,
                                     pj_status_t status)
{
    cli_telnet_fe *fe = (cli_telnet_fe *) pj_activesock_get_user_data(asock);

    pj_status_t sstatus;
    pj_pool_t *pool;
    cli_telnet_sess *sess = NULL;
    pj_activesock_cb asock_cb;

    PJ_UNUSED_ARG(src_addr);
    PJ_UNUSED_ARG(src_addr_len);

    if (fe->is_quitting)
        return PJ_FALSE;

    if (status != PJ_SUCCESS && status != PJ_EPENDING) {
        TRACE_((THIS_FILE, "Error on data accept (status=%d)", status));
        if (status == PJ_ESOCKETSTOP) {
            sstatus = telnet_restart(fe);
            if (sstatus != PJ_SUCCESS) {
                if (fe->own_ioqueue && fe->cfg.ioqueue) {
                    pj_ioqueue_destroy(fe->cfg.ioqueue);
                    fe->cfg.ioqueue = NULL;
                }
                TRACE_((THIS_FILE, "Error restarting telnet (status=%d)",
                        status));
            }
        }

        return PJ_FALSE;
    }

    /* An incoming connection is accepted, create a new session */
    pool = pj_pool_create(fe->pool->factory, "telnet_sess",
                          PJ_CLI_TELNET_POOL_SIZE, PJ_CLI_TELNET_POOL_INC,
                          NULL);
    if (!pool) {
        TRACE_((THIS_FILE,
                "Not enough memory to create a new telnet session"));
        return PJ_TRUE;
    }

    sess = PJ_POOL_ZALLOC_T(pool, cli_telnet_sess);
    sess->pool = pool;
    sess->base.fe = &fe->base;
    sess->base.log_level = fe->cfg.log_level;
    sess->base.op = PJ_POOL_ZALLOC_T(pool, struct pj_cli_sess_op);
    sess->base.op->destroy = &telnet_sess_destroy;
    pj_bzero(&asock_cb, sizeof(asock_cb));
    asock_cb.on_data_read = &telnet_sess_on_data_read;
    asock_cb.on_data_sent = &telnet_sess_on_data_sent;
    sess->rcmd = PJ_POOL_ZALLOC_T(pool, telnet_recv_buf);
    sess->history = PJ_POOL_ZALLOC_T(pool, struct cmd_history);
    pj_list_init(sess->history);
    sess->active_history = sess->history;

    sstatus = pj_mutex_create_recursive(pool, "mutex_telnet_sess",
                                        &sess->smutex);
    if (sstatus != PJ_SUCCESS)
        goto on_exit;

    sstatus = pj_activesock_create(pool, newsock, pj_SOCK_STREAM(),
                                   NULL, fe->cfg.ioqueue,
                                   &asock_cb, sess, &sess->asock);
    if (sstatus != PJ_SUCCESS) {
        TRACE_((THIS_FILE, "Failure creating active socket"));
        goto on_exit;
    }

    pj_memset(sess->telnet_option, 0, sizeof(sess->telnet_option));
    set_local_option(sess, TRANSMIT_BINARY, PJ_TRUE);
    set_local_option(sess, STATUS, PJ_TRUE);
    set_local_option(sess, SUPPRESS_GA, PJ_TRUE);
    set_local_option(sess, TIMING_MARK, PJ_TRUE);
    set_local_option(sess, TERM_SPEED, PJ_TRUE);
    set_local_option(sess, TERM_TYPE, PJ_TRUE);

    set_peer_option(sess, TRANSMIT_BINARY, PJ_TRUE);
    set_peer_option(sess, SUPPRESS_GA, PJ_TRUE);
    set_peer_option(sess, STATUS, PJ_TRUE);
    set_peer_option(sess, TIMING_MARK, PJ_TRUE);
    set_peer_option(sess, TERM_ECHO, PJ_TRUE);

    send_cmd_do(sess, SUPPRESS_GA);
    send_cmd_will(sess, TERM_ECHO);
    send_cmd_will(sess, STATUS);
    send_cmd_will(sess, SUPPRESS_GA);

    /* Send prompt string */
    telnet_sess_send(sess, &fe->cfg.prompt_str);

    /* Start reading for input from the new telnet session */
    sstatus = pj_activesock_start_read(sess->asock, pool, 1, 0);
    if (sstatus != PJ_SUCCESS) {
        TRACE_((THIS_FILE, "Failure reading active socket"));
        goto on_exit;
    }

    pj_ioqueue_op_key_init(&sess->op_key, sizeof(sess->op_key));
    pj_mutex_lock(fe->mutex);
    pj_list_push_back(&fe->sess_head, &sess->base);
    pj_mutex_unlock(fe->mutex);

    return PJ_TRUE;

on_exit:
    if (sess->asock)
        pj_activesock_close(sess->asock);
    else
        pj_sock_close(newsock);

    if (sess->smutex)
        pj_mutex_destroy(sess->smutex);

    pj_pool_release(pool);

    return PJ_TRUE;
}

PJ_DEF(pj_status_t) pj_cli_telnet_create(pj_cli_t *cli,
                                         pj_cli_telnet_cfg *param,
                                         pj_cli_front_end **p_fe)
{
    cli_telnet_fe *fe;
    pj_pool_t *pool;
    pj_status_t status;

    PJ_ASSERT_RETURN(cli, PJ_EINVAL);

    pool = pj_pool_create(pj_cli_get_param(cli)->pf, "telnet_fe",
                          PJ_CLI_TELNET_POOL_SIZE, PJ_CLI_TELNET_POOL_INC,
                          NULL);
    fe = PJ_POOL_ZALLOC_T(pool, cli_telnet_fe);
    if (!fe)
        return PJ_ENOMEM;

    fe->base.op = PJ_POOL_ZALLOC_T(pool, struct pj_cli_front_end_op);

    if (!param)
        pj_cli_telnet_cfg_default(&fe->cfg);
    else
        pj_memcpy(&fe->cfg, param, sizeof(*param));

    pj_list_init(&fe->sess_head);
    fe->base.cli = cli;
    fe->base.type = PJ_CLI_TELNET_FRONT_END;
    fe->base.op->on_write_log = &telnet_fe_write_log;
    fe->base.op->on_destroy = &telnet_fe_destroy;
    fe->pool = pool;

    if (!fe->cfg.ioqueue) {
        /* Create own ioqueue if application doesn't supply one */
        status = pj_ioqueue_create(pool, 8, &fe->cfg.ioqueue);
        if (status != PJ_SUCCESS)
            goto on_exit;
        fe->own_ioqueue = PJ_TRUE;
    }

    status = pj_mutex_create_recursive(pool, "mutex_telnet_fe", &fe->mutex);
    if (status != PJ_SUCCESS)
        goto on_exit;

    /* Start telnet daemon */
    status = telnet_start(fe);
    if (status != PJ_SUCCESS)
        goto on_exit;

    pj_cli_register_front_end(cli, &fe->base);

    if (p_fe)
        *p_fe = &fe->base;

    TRACE_((THIS_FILE, "Telnet started"));

    return PJ_SUCCESS;

on_exit:
    if (fe->own_ioqueue && fe->cfg.ioqueue) {
        pj_ioqueue_destroy(fe->cfg.ioqueue);
        fe->cfg.ioqueue = NULL;
    }

    if (fe->mutex) {
        pj_mutex_destroy(fe->mutex);
        fe->mutex = NULL;
    }

    pj_pool_release(pool);
    return status;
}

static pj_status_t telnet_start(cli_telnet_fe *fe)
{
    pj_sock_t sock = PJ_INVALID_SOCKET;
    pj_activesock_cb asock_cb;
    pj_sockaddr_in addr;
    pj_status_t status;
    int val;
    int restart_retry;
    unsigned msec;

    /* Start telnet daemon */
    fe->asock = NULL;
    status = pj_sock_socket(pj_AF_INET(), pj_SOCK_STREAM(), 0, &sock);

    if (status != PJ_SUCCESS)
        goto on_exit;

    pj_sockaddr_in_init(&addr, NULL, fe->cfg.port);

    val = 1;
    status = pj_sock_setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,
                                &val, sizeof(val));

    if (status != PJ_SUCCESS) {
        PJ_PERROR(3, (THIS_FILE, status, "Failed setting socket options"));
    }

    /* The loop is silly, but what else can we do? */
    for (msec=MIN_WAIT_ON_TELNET_RESTART, restart_retry=0;
         restart_retry < MAX_RETRY_ON_TELNET_RESTART;
         ++restart_retry, msec=(msec<MAX_WAIT_ON_TELNET_RESTART?
                          msec*2 : MAX_WAIT_ON_TELNET_RESTART))
    {
        status = pj_sock_bind(sock, &addr, sizeof(addr));
        if (status != PJ_STATUS_FROM_OS(EADDRINUSE))
            break;
        PJ_LOG(4,(THIS_FILE, "Address is still in use, retrying.."));
        pj_thread_sleep(msec);
    }

    if (status == PJ_SUCCESS) {
        int addr_len = sizeof(addr);

        status = pj_sock_getsockname(sock, &addr, &addr_len);
        if (status != PJ_SUCCESS)
            goto on_exit;

        fe->cfg.port = pj_sockaddr_in_get_port(&addr);

        if (fe->cfg.prompt_str.slen == 0) {
            pj_str_t prompt_sign = {"> ", 2};
            char *prompt_data = pj_pool_alloc(fe->pool,
                                              pj_gethostname()->slen+2);
            fe->cfg.prompt_str.ptr = prompt_data;

            pj_strcpy(&fe->cfg.prompt_str, pj_gethostname());
            pj_strcat(&fe->cfg.prompt_str, &prompt_sign);
        }
    } else {
        PJ_PERROR(3, (THIS_FILE, status, "Failed binding the socket"));
        goto on_exit;
    }

    status = pj_sock_listen(sock, 4);
    if (status != PJ_SUCCESS)
        goto on_exit;

    pj_bzero(&asock_cb, sizeof(asock_cb));
    asock_cb.on_accept_complete2 = &telnet_fe_on_accept;
    status = pj_activesock_create(fe->pool, sock, pj_SOCK_STREAM(),
                                  NULL, fe->cfg.ioqueue,
                                  &asock_cb, fe, &fe->asock);
    if (status != PJ_SUCCESS)
        goto on_exit;

    status = pj_activesock_start_accept(fe->asock, fe->pool);
    if (status != PJ_SUCCESS)
        goto on_exit;

    if (fe->own_ioqueue) {
        /* Create our own worker thread */
        status = pj_thread_create(fe->pool, "worker_telnet_fe",
                                  &poll_worker_thread, fe, 0, 0,
                                  &fe->worker_thread);
        if (status != PJ_SUCCESS)
            goto on_exit;
    }

    return PJ_SUCCESS;

on_exit:
    if (fe->cfg.on_started) {
        (*fe->cfg.on_started)(status);
    }

    if (fe->asock) {
        pj_activesock_close(fe->asock);
        fe->asock = NULL;
    } else if (sock != PJ_INVALID_SOCKET) {
        pj_sock_close(sock);
        sock = PJ_INVALID_SOCKET;
    }

    return status;
}

static pj_status_t telnet_restart(cli_telnet_fe *fe)
{
    pj_status_t status;
    pj_cli_sess *sess;

    fe->is_quitting = PJ_TRUE;
    if (fe->worker_thread) {
        pj_thread_join(fe->worker_thread);
        pj_thread_destroy(fe->worker_thread);
        fe->worker_thread = NULL;
    }

    pj_mutex_lock(fe->mutex);

    /* Destroy all the sessions */
    sess = fe->sess_head.next;
    while (sess != &fe->sess_head) {
        (*sess->op->destroy)(sess);
        sess = fe->sess_head.next;
    }

    pj_mutex_unlock(fe->mutex);

    /** Close existing activesock **/
    status = pj_activesock_close(fe->asock);
    if (status != PJ_SUCCESS)
        goto on_exit;

    fe->asock = NULL;
    fe->is_quitting = PJ_FALSE;

    /** Start Telnet **/
    status = telnet_start(fe);
    if (status != PJ_SUCCESS)
        goto on_exit;

    if (fe->cfg.on_started) {
        (*fe->cfg.on_started)(PJ_SUCCESS);
    }

    TRACE_((THIS_FILE, "Telnet restarted"));

on_exit:
    return status;
}

PJ_DEF(pj_status_t) pj_cli_telnet_get_info(pj_cli_front_end *fe,
                                           pj_cli_telnet_info *info)
{
    pj_sockaddr hostip;
    pj_status_t status;
    cli_telnet_fe *tfe = (cli_telnet_fe*) fe;

    PJ_ASSERT_RETURN(fe && (fe->type == PJ_CLI_TELNET_FRONT_END) && info,
                     PJ_EINVAL);

    pj_strset(&info->ip_address, info->buf_, 0);

    status = pj_gethostip(pj_AF_INET(), &hostip);
    if (status != PJ_SUCCESS)
        return status;

    pj_sockaddr_print(&hostip, info->buf_, sizeof(info->buf_), 0);
    pj_strset2(&info->ip_address, info->buf_);

    info->port = tfe->cfg.port;

    return PJ_SUCCESS;
}
