
/// Copyright 2006 MBARI.
/// MBARI Proprietary Information. All rights reserved.

#include <ctype.h>  /// toupper(), isalnum(), isspace()
#include <string.h> /// strcspn(), strncmp(), strchr()
#include "ui.h"
#include "commands.h"
#include "ioserial.h"
#include "timers.h"
#include "iodigital.h"
#include "config.h"

/// LOCAL DATA
Timer UserTOtmr; /// for user interface timeout
char CmdLine[UI_CMD_BUF_BYTES];    /// buffer for user input
char HostLine[UI_CMD_BUF_BYTES];   /// buffer for host cmds

return_code_t g_cmd_status=ERR_SUCCESS;

#ifdef WITH_UI_HISTORY

// command history buffer
static char history_queue[UI_HIST_QUEUE_LEN][UI_CMD_BUF_BYTES]={{0},{0}};

static int hist_ocur=-1;
static int hist_icur=0;
static bool esc=false;
static bool uparrow=false;
static bool dnarrow=false;
static bool cursor=false;
static int ocount=0;
static int hcount=0;
static int curcount=0;

void ui_hist_show(bool verbose)
{
    if(verbose){
    OUT_CTTY("\r\n");
    OUT_CTTY("  queue [%p]\r\n",history_queue);
    OUT_CTTY("  mem   [%02d][%02d]\r\n",UI_HIST_QUEUE_LEN,UI_CMD_BUF_BYTES);
    OUT_CTTY("  icur  [%02d/%p]\r\n",hist_icur,&history_queue[hist_icur]);
    OUT_CTTY("  ocur  [%02d/%p]\r\n\r\n",hist_ocur,&history_queue[hist_ocur]);
    }
    int i=0;
    for(i=0;i<UI_HIST_QUEUE_LEN;i++)
    OUT_CTTY("  %02d %s\r\n",i,history_queue[i]);
    OUT_CTTY("\r\n");

}

return_code_t ui_hist_exec(int id)
{

    return_code_t retval=ERR_FAILURE;
    if(id>=0 && id<UI_HIST_QUEUE_LEN){

        char *cmd = history_queue[id];
        if(strlen(cmd)>0){
            OUT_CTTY("\r\n%s\r\n",cmd);
            retval=uiParse(cmd,NULL);
        }
    }
    return retval;
}

void ui_hist_ocur_inc()
{
    if(hist_ocur>=0){
        // point to previous (wrap at 0)
        int next= ((hist_ocur-1)<0?UI_HIST_QUEUE_LEN-1:(hist_ocur-1));
        // if buffer not full, find last entry
        while(strlen(history_queue[next])<=0 && next>0){
            next--;
        }
        hist_ocur=next;

    }// else no history
}
void ui_hist_ocur_dec()
{
    if(hist_ocur>=0){
        // point to previous (wrap at end)
        int next= ((hist_ocur+1)>=UI_HIST_QUEUE_LEN?0:(hist_ocur+1));
         // if buffer not full, find last entry
        while(strlen(history_queue[next])<=0){
            next++;
            if(next>=UI_HIST_QUEUE_LEN)next=0;
        }
       hist_ocur=next;

    }// else no history
}

void ui_hist_icur_inc()
{
    hist_icur=( (hist_icur+1)>=UI_HIST_QUEUE_LEN ? 0 : (hist_icur+1));
}

void ui_hist_put(char *line)
{
    if(NULL!=line){
        int last = ((hist_icur-1)<=0?UI_HIST_QUEUE_LEN-1:(hist_icur-1));

        if(strcmp(history_queue[last],line)!=0){
            // add to buffer if different from previous
            memcpy(history_queue[hist_icur],line,strlen(line)+1);
            hist_ocur=hist_icur;
            ui_hist_icur_inc();
        }
    }
}

char *ui_hist_next()
{
    return (hist_ocur>=0?history_queue[hist_ocur]:NULL);
}

int ui_hist_olen()
{

    return (hist_ocur>=0 ? strlen(history_queue[hist_ocur]) : 0);
}

#endif //WITH_UI_HISTORY

void uiStartTmr() {
    tkTmrSet(&UserTOtmr, USER_TIME_OUT);
    tkTmrStart(&UserTOtmr);
}


int uiDidUITO() {
    if(0==tkTmrUpdate(&UserTOtmr)){
        return(TRUE);
    }else{
        return (FALSE);
    }
}


/// check if string @p str (up to first space) matches any of the parts of
/// @p name separated by '|'. Case sensitive, so give only caps please (names
/// are in caps).

char* uiStrName(char* str, const char* name) {
    int leng = strcspn(str, " \0");

    if (('|' == name[leng] || '\0' == name[leng]) && //same length?
            0 == strncmp(str, name, leng)) { //string match?
        return (&str[leng]);
    } else if (NULL != (name = strchr(name, '|'))) { //alt name?
        name++; //start after the '|'
        if (('|' == name[leng] || '\0' == name[leng]) &&
                0 == strncmp(str, name, leng)) {
            return (&str[leng]);
        }
    }
    return (NULL);
}

return_code_t ui_parse(int argc, char **argv, const CmdStruct* cmds)
{
    int i;
    char* args;

    if (NULL == cmds) {
        cmds = cmdsRoot;
    }

    // compare name against each name in CmdStruct
    for (i = 0; cmds[i].name[0] != '\0'; i++) {
        if (NULL != (args = uiStrName(argv[0], cmds[i].name))) {
             SYS_LED_ON_CMD();
            return (cmds[i].cmd_fn(argc-1,&argv[1])); //execute func
        }
    }

    return ERR_NO_COMMAND;
}

return_code_t uiParse(char* line, const CmdStruct* cmds)
{
    int i;
    char* args;
    return_code_t retval;
    if (NULL == cmds) {
        cmds = cmdsRoot;
    }
    while (isspace(*line)){
        //skip whitespace
        line++;
    }

//DPRINT("%s - line[%s]\r\n",__FUNCTION__,line);

    // compare name against each name in CmdStruct
    for (i = 0; cmds[i].name[0] != '\0'; i++) {
        if (NULL != (args = uiStrName(line, cmds[i].name))) {
            while (isspace(*args)){
                //skip whitespace
                args++;
            }
            SYS_LED_ON_CMD();
            char *tokens[16]={0};
            int ntoks=ui_tokenize(line,tokens," ",16,false);

            // execute command
            retval=(cmds[i].cmd_fn(ntoks-1,&tokens[1]));
            ui_release_toks(tokens,ntoks);
            g_cmd_status=retval;
            return retval;
        }
    }
    g_cmd_status=ERR_NO_COMMAND;
    return ERR_NO_COMMAND;
}

// uiGetLine buffer counts for each UART
// (i.e. index of buffer input pointer)
// callers must reset it if buffer overflows
//static int char_count[UART_COUNT] = {0,0,0};

/// Read up to len bytes from serPort (e.g. SER_CONS) into "buffer"
/// - does NOT automatically NULL terminate buffer except at EOL (\r\n)
/// - blocks until no bytes available or buffer full.
/// - sets timer USER_TIME_OUT that is reset when bytes received;
///   callers may read using uiDidUITO to implement input timeouts
/// - uses flags to select upper case conversion
/// - skips leading ' '  and '\b'
/// returns 0 if end line is found, -1 otherwise
//bool uiGetLine(char* buffer, int len, int serPort, ui_glflags flags)
//{
//    bool gotLine = false;
//    unsigned char b;    //incoming byte
//    unsigned char bp;   //scratch byte
//
//    while (len>0 &&
//            serPort>=0 && serPort<UART_COUNT &&
//            char_count[serPort]<len &&
//            serGetByte(serPort, &b)) {
//
//        //reset user timeout
//        tkTmrSet(&UserTOtmr, USER_TIME_OUT);
//
//        //don't start with space or backspace
//        if (0 == char_count[serPort] && (b == '\b' || b == ' '))
//            continue;
//
//        switch (b) {
//            case '\r':
//                tkDelayTicks(1);    //wait for one more char
//                serPeek(serPort, &bp);  //check for "\r\n"
//                if ('\n' == bp){
//                    serGetByte(serPort, &bp); //get the extra byte out of buffer
//                }
//                //fall through
//            case '\n': //end of line
//                gotLine = true;
//                buffer[char_count[serPort]] = '\0';
//
//#ifdef WITH_UI_HISTORY
//                if(char_count[serPort]>0){
//                    ui_hist_put(buffer);
//                    ocount=strlen(buffer);
//                }
//#endif // WITH_UI_HISTORY
//
//                char_count[serPort] = 0;
//                break;
//            case '\b': //backspace
//                char_count[serPort] -= 1;
//                // destructive BS - bksp,sp...
//                // second bksp output below
//                serPutChar(serPort, b);
//                serPutChar(serPort, ' ');
//                break;
//#ifdef WITH_UI_HISTORY
//
//            case 0x1B:
//                esc=true;
//                b='{';
//                break;
//            case 0x5B:
//                esc=true;
//                b='[';
//                break;
//            case 'A':
//            case 'B':
//            case 'C':
//            case 'D':
//                if(esc){
//                    if(b=='A'){
//                        if(curcount>0){
//                            ui_hist_ocur_inc();
//                        }
//                        uparrow=true;
//                    }else if(b=='B'){
//                        ui_hist_ocur_dec();
//                        dnarrow=true;
//                    }else{
//                        cursor=true;
//                    }
//                    curcount++;
//                    // clear escape flag
//                    esc=false;
//                }
//                // fall through if not arrow
//#endif // WITH_UI_HISTORY
//            default:
//#ifdef WITH_UI_HISTORY
//                // clear escape flag
//                esc=false;
//                if(!(uparrow||dnarrow||cursor))
//#endif // WITH_UI_HISTORY
//                if ( (len > char_count[serPort])){
//                    // add char to buffer
//                    // transform toupper (per flag)
//                    buffer[char_count[serPort]] = ( (flags&GL_CASE)!=0?b:toupper(b));
//                    char_count[serPort] += 1;
//                }
//                break;
//        }
////        cfgGet(CFG_ECHO, &echo);
////        config_get(CFG_ECHO, &echo);
////        if (echo == 1 || (echo == 2 && serPort == SER_CONS)) {
//
//          if( (flags&GL_ECHO)!=0){
//
// #ifdef WITH_UI_HISTORY
//           if(uparrow || dnarrow){
//                // output the history string
//                char *hstr=ui_hist_next();
//                if(NULL!=hstr){
//                    hcount=ui_hist_olen();
//                    // backspace to start of line
//                    int i=0;
//                    for(i=0;i<char_count[serPort];i++){
//                        serPutChar(serPort,'\b');
//                    }
//
//                    // copy history string to buffer
//                    memcpy(buffer,hstr,hcount);
//                    buffer[hcount]='\0';
//                    char_count[serPort]=hcount;
//                    serPutString(serPort,buffer);
////                    OUT_CTTY("<hc[%d] oc[%d]>",hcount,ocount);
//                     // if new line shorter than old one
//                    // erase characters from end of line
//                    if(ocount>hcount){
//                        for(i=0;i<(ocount-hcount);i++)serPutChar(serPort,' ');
//                        for(i=0;i<(ocount-hcount);i++)serPutChar(serPort,'\b');
//                    }
//                    ocount=hcount;
//                }
//
//                // clear key flags
//                uparrow=false;
//                dnarrow=false;
//               // clear escape flag
//                esc=false;
//           }else if(cursor){
//                cursor=false;
//               // clear escape flag
//                esc=false;
//           }else if(gotLine){
//                curcount=0;
//                serPutString(serPort, "\r\n");
//               // clear escape flag
//                esc=false;
//            }else if(!esc){
//                serPutChar(serPort, b);
//               // clear escape flag
//                esc=false;
//            }
// #else
//            if(gotLine){
//                serPutString(serPort, "\r\n");
//
//            }else{
//                serPutChar(serPort, b);
//            }
//
//#endif // WITH_UI_HISTORY
//
//         }
//
//    }//while(serGetbyte())
//
//    return gotLine;
//}

bool uiGetLine(char* buffer, int len, char **pcur, int serPort, ui_glflags flags)
{
    // end of line flag (return value)
    bool gotLine = false;
    // input byte
    unsigned char b=0;
   // working cursor (buffer input pointer)
    char *cur=*pcur;

    // validate inputs and loop until
    // buffer full, end of line, or no bytes available
    while (len>0 &&
            serPort>=0 && serPort<UART_COUNT &&
            cur<(buffer+len) &&
            serGetByte(serPort, &b)) {

        // reset timer
        // TODO: pass in timeout
        tkTmrSet(&UserTOtmr, USER_TIME_OUT);

        // ignore leading space or backspace
        if (cur==buffer && (b == '\b' || b == ' ')){
            continue;
        }

        // look-ahead byte
        unsigned char nb=0;
        switch (b) {
            case '\r':
                // delay for next char
                tkDelayTicks(5);
                // check for line end
                serPeek(serPort, &nb);
                if ('\n' == nb){
                    // read the line end
                    serGetByte(serPort, &nb);
                }
                // fall through, (accepts '\r' and/or '\n')
            case '\n':
                //end of line
                gotLine = true;
                *cur = '\0';

#ifdef WITH_UI_HISTORY
                if((cur-buffer)>0 ){
                    // add line to history buffer
                    ui_hist_put(buffer);
                    ocount=strlen(buffer);
                }
#endif // WITH_UI_HISTORY

                cur=buffer;
                break;
            case '\b':
                //backspace
                cur=( cur>buffer ? (cur-1) : buffer);

                // destructive BS - bksp,sp...
                // second bksp output below
                serPutChar(serPort, b);
                serPutChar(serPort, ' ');
                break;
#ifdef WITH_UI_HISTORY
            // history processing
            case 0x1B:
                // escape sequence start (for cursor keys):
                // ESC[A : up
                // ESC[B : down
                // ESC[C : left
                // ESC[D : right
                esc=true;
                b='{';
                break;
            case 0x5B:
                // '[' processed separately, since it
                // may be part of escape sequence
                // if so, should be ignored.

                if (esc==false){
                    if ( (cur-buffer)<len){
                        // if not in escape sequence, process normally
                        // add char to buffer
                        // transform toupper (per flag)
                        *cur = ( (flags&GL_CASE)!=0 ? b : toupper(b));
                        // update cursor, don't overflow
                        cur=( (cur < (buffer+len)) ? cur+1 : buffer+len-1 );
                    }// else buffer full
                }// else ignore
                break;
            case 'A':
            case 'B':
            case 'C':
            case 'D':
                // 'A,B,C,D' processed separately, since they
                // may be part of escape sequence
                // if so, should be ignored.
                if(esc){
                    // arrow/cursor key
                    if(b=='A'){
                        if(curcount>0){
                            ui_hist_ocur_inc();
                        }
                        uparrow=true;
                    }else if(b=='B'){
                        ui_hist_ocur_dec();
                        dnarrow=true;
                    }else{
                        cursor=true;
                    }
                    curcount++;
                    // clear escape flag
                    esc=false;
                }
                // fall through to process normally
                // if not arrow/cursor key
#endif // WITH_UI_HISTORY
            default:
                // process (normal) byte
#ifdef WITH_UI_HISTORY
                // clear escape flag
                esc=false;
                if(!(uparrow||dnarrow||cursor))
                    // not a cursor/arrow key
#endif // WITH_UI_HISTORY
                if ( (cur-buffer)<len){
                    // add char to buffer
                    // transform toupper (per flag)
                    *cur = ( (flags&GL_CASE)!=0 ? b : toupper(b));
                    // update cursor, don't overflow
                    cur=( (cur < (buffer+len)) ? cur+1 : buffer+len-1 );
                }// else buffer full
                break;
        }// switch b

        if( (flags&GL_ECHO)!=0){

    #ifdef WITH_UI_HISTORY
            if(uparrow || dnarrow){
                // output the history string
                char *hstr=ui_hist_next();
                if(NULL!=hstr){
                    hcount=ui_hist_olen();
                    // backspace to start of line
                    int i=0;
                    for(i=0;i<(cur-buffer);i++){
                        serPutChar(serPort,'\b');
                    }

                    // copy history string to buffer
                    memcpy(buffer,hstr,hcount);
                    // hcount *should* always be less than length, but check
                    if(hcount<len){
                        buffer[hcount]='\0';
                        cur=buffer+hcount;
                    }else{
                        EPRINT("history entry exceeds buffer len [%d/%d] - truncating\r\n",hcount,len);
                        buffer[len-1]='\0';
                        cur=buffer+len-1;
                    }
                    serPutString(serPort,buffer);

                    // if new line shorter than old one
                    // erase characters from end of line
                    if(ocount>hcount){
                        for(i=0;i<(ocount-hcount);i++)serPutChar(serPort,' ');
                        for(i=0;i<(ocount-hcount);i++)serPutChar(serPort,'\b');
                    }
                    ocount=hcount;
                }

                // clear key flags
                uparrow=false;
                dnarrow=false;
               // clear escape flag
                esc=false;
            }else if(cursor){
                // ignore cursor
                cursor=false;
               // clear escape flag
                esc=false;
            }else if(gotLine){
                // output EOL
                curcount=0;
                serPutString(serPort, "\r\n");
               // clear escape flag
                esc=false;
            }else if(!esc){
                // output byte
                serPutChar(serPort, b);
               // clear escape flag
                esc=false;
            }
    #else
            if(gotLine){
                serPutString(serPort, "\r\n");

            }else{
                serPutChar(serPort, b);
            }

    #endif // WITH_UI_HISTORY

        }

    }//while...

    // update cursor
    *pcur=cur;

    return gotLine;
}

static int s_do_ui(serial_port_t port, char *line_buf, char **pcur, int len, bool echo)
{
    int retval = ERR_NO_LINE;

    if(NULL!=line_buf && NULL!=pcur ){

        if(*pcur==NULL){
           // initialize cursor
           *pcur=line_buf;
        }

        if (uiGetLine(line_buf, len, pcur, port, (echo?GL_ECHO:GL_NONE))) {
            // line end received

            if(port==SER_HOST && strlen(line_buf)>0){
                // echo host input lines to console
                serPutString(SER_CONS, "\r\nhost<");
                serPutString(SER_CONS, line_buf);
                serPutString(SER_CONS, ">\r\n");
                UI_PROMPT(SER_CONS);
            }

            // parse and execute line
            if ('\0' == *line_buf){
                retval = ERR_EMPTY_LINE;
            }else{
                retval = uiParse(line_buf, NULL);
            }
            // reset cursor
            *pcur=line_buf;
        }else{
            // line pending, check for overflow
            if( (*pcur-line_buf)>=len ){
                OUT_CTTY("\r\nERR - input buffer full - max line len [%d]\r\n",len);
                memset(line_buf,'\0',len);
                *pcur=line_buf;
            }
        }

        // display ERR code and prompt
        switch (retval) {
            case ERR_NO_LINE:
            case ERR_SUCCESS:
            case ERR_EMPTY_LINE:
                break;
            case ERR_FAILURE:
                serPutString(port, "FAILED\r\n");
                break;
            case ERR_NO_COMMAND:
                serPutString(port, "NO COMMAND\r\n");
                break;
            case ERR_NO_SUB_COMMAND:
                serPutString(port, "NO SUB COMMAND\r\n");
                break;
            default:
                serPutString(port, "SOME ERROR\r\n");
        }

        if (retval != ERR_NO_LINE) {
            UI_PROMPT(port);
        }
    }else{
        OUT_CTTY("ERR - invalid arg [x%p][x%p]\r\n",line_buf,*pcur);
    }
    return retval;
}

int ui_console_update()
{
    g_ctty = SER_CONS;
    static char *cur=NULL;
    uint16_t echo=ECHO_ALL;
    // use configuration for echo
    config_get(CK_ECHO, &echo);

    return s_do_ui(SER_CONS,CmdLine,&cur,UI_CMD_BUF_BYTES,((echo&ECHO_CONS)!=0));
}

int ui_host_update()
{
    g_ctty = SER_HOST;
   static char *cur=NULL;
    uint16_t echo=ECHO_ALL;
    // use configuration for echo
    config_get(CK_ECHO,&echo);

    return s_do_ui(SER_HOST,HostLine,&cur,UI_CMD_BUF_BYTES,((echo&ECHO_HOST)!=0));
}

char *ui_unescape(char *src)
{
    char *retval=NULL;
    if(NULL!=src){
        char *px=src;
        char *ps=NULL;

        while(*px!='\0'){
            bool do_copy=true;
            if(*px=='\\' && *(px+1)!='\0'){
                ps=px+2;
                unsigned int u=0;
                switch(*(px+1)){
                    case '$':
                        *px='$';
                        break;
                    case '\"':
                        *px='\"';
                        break;
                    case '\'':
                        *px='\'';
                        break;
                    case '\\':
                        // no change
                        break;
                    case 't':
                    case 'T':
                        *px='\t';
                        break;
                    case 'b':
                    case 'B':
                        *px='\b';
                        break;
                    case 'f':
                    case 'F':
                        *px='\f';
                        break;
                    case 'r':
                    case 'R':
                        *px='\r';
                        break;
                    case 'n':
                    case 'N':
                        *px='\n';
                        break;
                    case 'x':
                    case 'X':
                        if(sscanf(px+2,"%02x",&u)==1 || sscanf(px+2,"%02X",&u)==1){
                            *px=(char)u;
                            ps+=2;
                        }else{
                            do_copy=false;
                        }
                        break;
                    default:
                        do_copy=false;
                        break;
                }
                if(do_copy ){
//                    DPRINT("px[%s] ps[%s/%d]\r\n",px,ps,strlen(ps));
                    if(strlen(ps)>=1){
                        size_t n=strlen(ps);
                        memmove(px+1,ps,n+1);
                        *(px+1+n)='\0';
                    }else{
                        *(px+1)='\0';
                    }
                    do_copy=false;
                }
            }
             px++;
        }
        retval=src;
    }
    return retval;
}

int ui_tokenize(char *src, char **dest, char *del, int ntok, bool preserve_quote)
{
    int toks=0;
    if(NULL!=src && NULL!=dest && NULL!=del && ntok>0){

        // cursors: 0: token start 1: token end 2: probe (e.g. quote ends) 3,4: quoted var expansion
        char *cur[5] = {src,NULL,NULL,NULL};
        char *cpx=NULL;
        char quote=0;
        int len=strlen(src);

        // OUTER LOOP: scan src with token cursor cur[0]]
        // resolving quotes (single-nested quotes supported) and variable substitution
        while(*cur[0]!='\0' && cur[0]<(src+len) && toks<ntok){

            // cur[0] : find token start (non-space)
            while(isspace(*cur[0]) && *cur[0]!='\0')cur[0]++;
            // cur[1] : find nominal token end (whitespace, NULL)
            cur[1]=cur[0]+1;
            while(!isspace(*cur[1]) && *cur[1]!='\0')cur[1]++;

            // test for variable sub
            if(*cur[0]=='$' && cur[0]>src && *(cur[0]-1)!='\\'){
                // VARIABLE SUB
                // is a variable
                // replace with value string
                *cur[1]='\0';
                char *pval = NULL;
                // let config_str_val provide a buffer (pval=NULL); it need not be
                // released, but the string must be copied since subsequent
                // calls to config_str_val will overwrite it.
                // NOTE: caller must release the token buffer using pfree())
                if(config_str_val(cur[0]+1,&pval,NULL)==ERR_SUCCESS && NULL!=pval){
//                    DPRINT("cur[%s] pval[%s]\r\n",cur[0]+1,pval);
                    char *vcpy = (char *)palloc(strlen(pval)+1);
                    sprintf(vcpy,"%s",pval);
                    dest[toks++]=vcpy;
                }else{
                    // var not found - treat as literal
                    WPRINT("invalid var[%s]",cur[0]);
                    dest[toks++]=cur[0];
                }

            }else if( *cur[0]=='\"' || *cur[0]=='\'' ){
                // TOKEN BEGINS W/ QUOTE
                int qtok=toks;
                // get opening quote character
                quote=*cur[0];

                // find closing quote token (or end of string)
                cur[1]=cur[0]+1;
                while(*cur[1]!='\0' && *cur[1]!=quote){
                    cur[1]++;
                }

                if(*cur[1]!=quote){
                    // CLOSING QUOTE MISSING [unrecoverable error]
                    // count as one token and move on
                    WMSG("closing quote expected");
                    char *qstr = NULL;
                    if( (qstr=pstralloc(cur[0]+1,cur[1]))!=NULL){
                        dest[toks++]=qstr;
                    }else{
                        EMSG("alloc failed");
                        *cur[1]='\0';
                        dest[toks++]=cur[0];
                    }
                }else{
                    // CLOSING QUOTE OK
                    if(preserve_quote){
                        // add start quote token
                        char *qstr=NULL;
                        if( (qstr=pstrdup("\""))!=NULL){
                            dest[toks++]=qstr;
                        }else{
                            EMSG("alloc failed(pBQUOT)");
                        }
                    }

                    // change end quote to null (will restore if preserved)
                    *cur[1]='\0';

                    // find and expand variable substitution, if any...
                    // ttok: text cursors
                    // vtok: variable cursors
                    // start of token
                    char *ttok[2]={NULL,NULL};
                    char *vtok[2]={NULL,NULL};

                    // start of token
                    ttok[0]=cur[0]+1;
                    // end of token
                    ttok[1]=ttok[0];

                    while(ttok[1]<cur[1]){
                        // test text token for variable start
                        if(*ttok[0]=='$'){
                            // VARIABLE START BYTE FOUND ('$')
                            // find variable end or string end
                            vtok[0]=ttok[1];
                            vtok[1]=vtok[0]+1;
                            while( (isalnum(*vtok[1]) || *vtok[1]=='_'||
                                    *vtok[1]=='.' || *vtok[1]=='-') &&
                                    vtok[1]<cur[1] ){
                                vtok[1]++;
                            }

                            // if var, add val token
                            // get key (vstr) memory
                            char *vstr=pstralloc(vtok[0]+1,vtok[1]-1);
                            if(NULL!=vstr){
                                // look up variable value as string
                                char *pval=NULL;
                                if(config_str_val(vstr,&pval,NULL)==ERR_SUCCESS && NULL!=pval){
                                    // VALUE FOUND:
                                    // update pointers
                                    ttok[0]=vtok[1];
                                    ttok[1]=ttok[0];
                                    // add variable string value token
                                    dest[toks++]=pstrdup(pval);
                                }else{
                                    // VALUE NOT FOUND
                                    WPRINT("invalid var[%s]",vstr);
                                    // move end to end of text token and
                                    // continue search for text token end
                                    ttok[1]=vtok[1];
                                }
                                pfree(vstr);
                            }
                        }
                        // resume processing
                        if(*ttok[0]!='$'){
                            // ORDINARY TOKEN
                            // find end of text token segment, var start,
                            // or end of quote token
                            while( *ttok[1]!='$' && ttok[1]<cur[1] && *ttok[1]!='\0' ) {
                                ttok[1]++;
                            }
                            // test for variable
                            if( *ttok[1]=='$'  && isalpha(*(ttok[1]+1)) && *(ttok[1]-1)!='\\'){
                                // VARIABLE START BYTE FOUND ('$')
                                // find ends
                                vtok[0]=ttok[1];
                                vtok[1]=vtok[0]+1;
                                while( (isalnum(*vtok[1]) || *vtok[1]=='_' ||
                                        *vtok[1]=='.' || *vtok[1]=='-') &&
                                        vtok[1]<cur[1] ){
                                    vtok[1]++;
                                }

                                // if var, add val token
                                // get key (vstr) memory
                                char *vstr=pstralloc(vtok[0]+1,vtok[1]-1);
                                if(NULL!=vstr){
                                    // look up variable value as string
                                    char *pval=NULL;
                                    if(config_str_val(vstr,&pval,NULL)==ERR_SUCCESS && NULL!=pval){
                                        char *tval=pstralloc(ttok[0],ttok[1]-1);
                                        if(NULL!=tval){
                                            // VALUE FOUND:
                                            // update pointers
                                            dest[toks++]=tval;
                                            dest[toks++]=pstrdup(pval);
                                            ttok[0]=vtok[1];
                                            ttok[1]=ttok[0];
                                        }else{
                                            EMSG("alloc failed");
                                            pfree(pval);
                                        }
                                    }else{
                                       // VALUE NOT FOUND
                                        WPRINT("invalid var[%s]",vstr);
                                        // move end to end of text token and
                                        // continue search for text token end
                                        ttok[1]=vtok[1];
                                    }
                                    pfree(vstr);
                                }
                            }else{
                                ttok[1]++;
                            }
                        } //if(*ttok[0]!='$')
                    }//while(ttok[1]<cur[1])

                    // add remaining text token, if any
                    char *tval=pstralloc(ttok[0],ttok[1]);

                    if(NULL!=tval){
                           dest[toks++]=tval;
                    }else{
                        EMSG("alloc failed");
                    }

                    if(preserve_quote){
                        // restore closing quote if preserved (add as token)
                        char *qstr=NULL;
                        if( (qstr=pstrdup("\""))!=NULL){
                            dest[toks++]=qstr;
                        }else{
                            EMSG("alloc failed(pEQUOT)");
                        }
                    }

                }// else closed quote OK

                char *s = pargcat(toks-qtok,dest+qtok,"");
              MPRINT(MOD_TOKS,"%d - argcat dest[%s] toks[%d] qtok[%d]\r\n",__LINE__,s,toks,qtok);
                int z=0;
                for(z=qtok;z<toks;z++)pfree(dest[z]);
                dest[qtok]=s;
                toks=qtok+1;
                // END CASE: TOKEN BEGINS W/ QUOTE
            }else{
                // TOKEN DOES NOT BEGIN W/ QUOTE
//                OUT_CTTY("%d - cur0[%s] cur1[%s]\r\n",__LINE__,cur[0],cur[1]);
                // save whitespace @ cur[0]]
                char sav_spc=*cur[1];
                // set cur[1] to NULL
                *cur[1]='\0';

                // test token for embedded quote
                if( (NULL!=(cpx=strstr(cur[0],"\"")) || NULL!=(cpx=strstr(cur[0],"'"))) ){
                    // TOKEN CONTAINS EMBEDDED QUOTE
                    // restore whitespace @ cur[0]]
                    *cur[1]=sav_spc;
                    int qtok=toks;

                     // get open quote char
                    quote=*cpx;

                    // find closing quote token (or end of string)
                    cur[1]=cpx+1;
                    while(*cur[1]!='\0' && *cur[1]!=quote){
                        cur[1]++;
                    }

                    // test whether closing quote found
                    if(*cur[1]!=quote){
                        // CLOSING QUOTE MISSING [unrecoverable error]
                        // count as one token and move on
                       WMSG("closing quote expected");
                        // no substitution
                        char *qstr = NULL;

                        if( (qstr=pstralloc(cur[0],cur[1]))!=NULL){
                            dest[toks++]=qstr;
                        }else{
                            EMSG("alloc failed");
                            *cur[1]='\0';
                            dest[toks++]=cur[0];
                        }
                    }else{
                        // CLOSING QUOTE OK
                        // always preserve embedded quotes
                        // (don't null end quote, don't add quote to start of token)

                        // find end of token if it continues beyond quote
                        while(*cur[1]!='\0' && !isspace(*(cur[1]+1))  )
                        cur[1]++;


                        // find and expand variable substitution, if any...
                        // ttok: text cursors
                        // vtok: variable cursors
                        // start of token
                        char *ttok[2]={NULL,NULL};
                        char *vtok[2]={NULL,NULL};
                       ttok[0]=cur[0];
                        // end of token
                        ttok[1]=ttok[0];

                        while(ttok[1]<cur[1]){
                            if(*ttok[0]=='$'){
                                // TOKEN WITH VAR
                                // text segment begins var char ('$')
                                // find ends
                                vtok[0]=ttok[1];
                                vtok[1]=vtok[0]+1;
                                while( (isalnum(*vtok[1]) || *vtok[1]=='_' ||
                                        *vtok[1]=='.' || *vtok[1]=='-') &&
                                        vtok[1]<cur[1] ){
                                    vtok[1]++;
                                }

                                // if var, expand value token
                                char *vstr=pstralloc(vtok[0]+1,vtok[1]-1);
                                if(NULL!=vstr){
                                    char *pval=NULL;
                                    if(config_str_val(vstr,&pval,NULL)==ERR_SUCCESS && NULL!=pval){
                                        ttok[0]=vtok[1];
                                        ttok[1]=ttok[0];
                                        dest[toks++]=pstrdup(pval);
                                    }else{
                                        // if not var, move end to end of text token and
                                        // continue search for text token end
                                       WPRINT("invalid var[%s]",vstr);
                                        ttok[1]=vtok[1];
                                    }
                                    pfree(vstr);
                                }
                            }
                            if(*ttok[0]!='$'){
                                // text token normal
                                // find end of text segment or end of quote token
                                 while( *ttok[1]!='$' && ttok[1]<cur[1] && *ttok[1]!='\0' ) {
                                    ttok[1]++;
                                }
                                if( *ttok[1]=='$'  && isalpha(*(ttok[1]+1)) && *(ttok[1]-1)!='\\'){
                                    // found var char ('$')
                                    // find ends
                                    vtok[0]=ttok[1];
                                    vtok[1]=vtok[0]+1;
                                   while( (isalnum(*vtok[1]) || *vtok[1]=='_' || *vtok[1]=='.' || *vtok[1]=='-') && vtok[1]<cur[1] )vtok[1]++;

                                    // if var, add val token
                                    char *vstr=pstralloc(vtok[0]+1,vtok[1]-1);
                                    if(NULL!=vstr){
                                        char *pval=NULL;
                                        if(config_str_val(vstr,&pval,NULL)==ERR_SUCCESS && NULL!=pval){
                                            char *tval=pstralloc(ttok[0],ttok[1]-1);
                                            if(NULL!=tval){
                                                dest[toks++]=tval;
                                                dest[toks++]=pstrdup(pval);
                                                ttok[0]=vtok[1];
                                                ttok[1]=ttok[0];
                                            }else{
                                                EMSG("alloc failed");
                                                pfree(pval);
                                            }
                                        }else{
                                            // if not var, move end to end of text token and
                                            // continue search for text token end
                                           WPRINT("invalid var[%s]",vstr);
                                            ttok[1]=vtok[1];
                                        }
                                        pfree(vstr);
                                    }
                                }else{
                                    ttok[1]++;
                                }
                            } //if(*ttok[0]!='$')
                        }//while(ttok[1]<cur[1])

                        // add remaining text token, if any
                        char *tval=pstralloc(ttok[0],ttok[1]);

                        if(NULL!=tval){
                            dest[toks++]=tval;
                        }else{
                            EMSG("alloc failed");
                        }
                    }// END CASE: CLOSING QUOTE OK

                    char *s = pargcat(toks-qtok,dest+qtok,"");
                    MPRINT(MOD_TOKS,"%d - argcat dest[%s] toks[%d] qtok[%d]\r\n",__LINE__,s,toks,qtok);
                    int z=0;
                    for(z=qtok;z<toks;z++){
                        pfree(dest[z]);
                    }
                    dest[qtok]=s;
                    toks=qtok+1;
                    // END CONTAINS QUOTE CASE
                }else{
                    // ORDINARY TOKEN (no quotes, var-sub)
                    *cur[1]='\0';
                    dest[toks++]=cur[0];
                }
            }// END CASE: TOKEN DOES NOT BEGIN W/ QUOTE

            // update cursors
            // at this point, cur[0] points to start of current token
            // and cur[0] points to end.]
            // position cur[0] just after the current token
            cur[0]=cur[1]+1;
        }// END OUTER cur[0] loop
    }// invalid arg
    return toks;
}

void ui_release_toks(char **tokens, int ntoks)
{
    if(NULL!=tokens && ntoks>0){
        int i=0;
        for(i=0;i<ntoks;i++){
            // OK to call pfree on unallocated memory
            // it will just ignore it
            if(NULL!=tokens[i]){
                pfree(tokens[i]);
            }
        }
    }
    return;
}