/*
 * 
 *      Author: alexrayne <alexraynepe196@gmail.com>
  ------------------------------------------------------------------------
    Copyright (c) alexrayne

   All rights reserved.
   Redistribution and use in source and binary forms, with or without
   modification, are permitted provided that the following conditions are met:
   - Redistributions of source code must retain the above copyright
     notice, this list of conditions and the following disclaimer.
   - Redistributions in binary form must reproduce the above copyright
     notice, this list of conditions and the following disclaimer in the
     documentation and/or other materials provided with the distribution.
   - Neither the name of ARM nor the names of its contributors may be used
     to endorse or promote products derived from this software without
     specific prior written permission.
   *
   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
   AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
   ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS AND CONTRIBUTORS BE
   LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
   CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
   SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
   INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
   CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
   ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   POSSIBILITY OF SUCH DAMAGE. *
*/
#include <c_compat.h>
#include <cassert>
#include <ctype.h>
#include <cstring>
#include <cmath>
#include "cli.hpp"

using namespace cli;


// если задан этот параметр, то CLICommand будет разрегистрироваться при удалении
// если система команд статична, десткруктор не будет пользоваться разрегистрацией
//#define CLI_DYNAMICS_CMD

//virtual 
CLICommand::~CLICommand(){
#ifdef CLI_DYNAMICS_CMD
    unregisterCmd();
#endif
}

//virtual
int CLICommand::cli_process(CLI_shell* shell, const_line cmd){
    (void)shell;
    const_line line = cli::after_name(cmd, cmd_name);
    if (line > cmd)
        return line - cmd;
    else return 0;
};

bool CLICommandBase::registerCmd(CLI_shell* shell){
  if (shell == nullptr)
    return cli::registerCmd(this);
  else
    return shell->registerCmd(this);
}

bool CLICommandBase::unregisterCmd(CLI_shell* shell){
    if (shell == nullptr)
      return cli::unregisterCmd(this);
    else
      return shell->unregisterCmd(this);
}

//virtual
void CLICommand::cli_help(CLI_shell* shell){
    shell->printf( "%s \n", cmd_name );
}

//* печать краткого хелпа команд. выдает только команду
void CLICommand::cli_brief(CLI_shell* shell){
    shell->putln(cmd_name);
}


CLICommand::const_line CLICommand::after_cmd(const_line cmd) const {
    return cli::after_name(cmd, cmd_name);
}



static const unsigned cli_cmd_reserve = 4;
CLI_commands::CLI_commands(){
  reserve(cli_cmd_reserve);
};

CLI_commands::CLI_commands(unsigned _reserves){
  reserve(_reserves);
}

bool CLI_commands::append(CLICommandBase* cmd){
    if (cmd == nullptr)
        return false;
    return append(*cmd);
}

bool CLI_commands::append(CLICommandBase& cmd){
    if (is_contains(cmd))
        return false;
    push_back(&cmd);
    return back() == &cmd;
}

void CLI_commands::help(CLI_shell* shell){
  shell->puts("avail commands:\n");
  list_brief(shell);
}

void CLI_commands::list_brief(CLI_shell* shell){
  for(iterator _it = this->begin(); _it != this->end(); ++_it){
    cmd_t* cmd = *_it;
    if (cmd == nullptr)
        continue;
    if (cmd->cmd_name == nullptr){
        cmd->cli_brief(shell);
    }
    else if ( cmd->cmd_name[0] == '\0'){
        cmd->cli_brief(shell);
    }
    else
        shell->putln(cmd->cmd_name);
  }
}

int CLI_commands::cli_process(CLI_shell* shell, CLI_commands::const_line line){
    if (empty())
        return 0;
    assert(line != nullptr);
  for(iterator _it = this->begin(); _it != this->end(); ++_it){
    CLICommand *cmd = *_it;
    if (cmd == nullptr)
        continue;
    int res = cmd->cli_process(shell, line);
    if (res != 0)
      return res;
  }
  return 0;
}


CLICommand* CLI_commands::lookup(CLI_commands::const_line cmd) const{
  if (cmd == nullptr)
      return nullptr;
  const_line name = cmd + space_len(cmd, ~0u);
  for(const_iterator _it = this->cbegin(); _it != this->cend(); ++_it){
    CLICommand *cmd = *_it;
    if (cmd == nullptr)
        continue;
    if (cmd->is_cmd_starts(name))
      return cmd;
  }
  return nullptr;
}

bool CLI_commands::is_contains(const CLICommand& cmd) const{
    for(const_iterator _it = this->cbegin(); _it != this->cend(); ++_it)
        if (*_it == &cmd)
            return true;
    return false;
}

bool CLI_commands::remove(cmd_t &cmd){
    for(const_iterator _it = this->cbegin(); _it != this->cend(); ++_it)
        if (*_it == &cmd){
            this->erase(_it);
            return true;
        }
    return false;
}




cli::CLI_Help::CLI_Help(CLI_commands* dst)
: inherited("help")
{
  dst->append(this);
}

//virtual
int cli::CLI_Help::cli_process(CLI_shell* shell, CLICommand::const_line line)
{
    assert(line != nullptr);
  int len = name_starts(line, cmd_name);
  if (len > 0){
    const_line s = line + len;
    s += space_len(s, ~0u);
    len = is_eol(s);
    s += len;
    if (len < 0){
      CLICommand* cmd = shell->lookupCmd(s);
      if (cmd != nullptr){
        s += name_starts(s, cmd->cmd_name);
        cmd->cli_help(shell);
      }
      else
        shell->help();
    }
    else {
      shell->help();
    }
    return s - line;
  }
  return 0;
}
//* печатает хелп команды в шелл
//virtual
void cli::CLI_Help::cli_help(CLI_shell* shell){
  shell->help();
}

const char* const cli::names_onoff[] = { "off", "on", nullptr};
int cli::mutch_onoff(const char* __restrict line
                , const char** __restrict lend
                )
{
    return mutch_names(line, lend, names_onoff);
}

static
const char* const names_bool[] = { "false", "true", nullptr};
int cli::mutch_bool(const char* __restrict line
                , const char** __restrict lend
               )
{
    return mutch_names(line, lend, names_bool);
}


cli::CLI_OnOffCommand::CLI_OnOffCommand()
:inherited()
{}

cli::CLI_OnOffCommand::CLI_OnOffCommand(const_cmd _cmd)
:inherited(_cmd)
{}


//virtual
int cli::CLI_OnOffCommand::cli_process(CLI_shell* shell, const_line cmd){
    int ok = CLICommand::cli_process(shell, cmd);
    if (ok <= 0)
        return 0;

    const_line cmdval = cmd + ok;
    bool value = false;
    const_line line = after_name(cmdval, "on");
    if (line != nullptr)
        value = true;
    else {
        line = after_name(cmdval, "off");
        if (line == nullptr)
            return 0;
    }
    cli_onoff(value);
    return line-cmd;
}
//* печатает хелп команды в шелл
//virtual
void cli::CLI_OnOffCommand::cli_help(CLI_shell* shell){
    shell->puts("on|off \t - enable/disable\n");
}




//-----------------------------------------------------------------------------

__INITPRIO(101) 
CLI_commands CLI_shell::cmds(cli_cmd_reserve);
CLI_Help     CLI_shell::help_cmd(&CLI_shell::cmds);

bool cli::registerCmd(CLICommandBase *cmd){
  return CLI_shell::cmds.append(cmd);
}

bool cli::unregisterCmd(CLICommandBase *cmd){
    return CLI_shell::cmds.remove(*cmd);
}


int cli::space_len(const char* s){
    if (s == nullptr)
        return 0;
  const char* tmp = s;
  for(; (*tmp != 0); tmp++){
    char c = *tmp;
    if (!isspace(c))
      break;
  }
  return tmp - s;
}

int cli::space_len(const char* s, unsigned len){
    if (s == nullptr)
        return 0;
  const char* tmp = s;
  for(; len > 0; len--, tmp++){
    char c = *tmp;
    if (c == 0)
      break;
    if (!isspace(c))
      break;
  }
  return tmp - s;
}

int cli::word_len(const char* s, unsigned len){
    if (s == nullptr)
        return 0;
  const char* tmp = s;
  for(; len > 0; len--, tmp++){
    char c = *tmp;
    if (c == 0)
      break;
    if (isalnum(c)||ispunct(c))
      continue;
    break;
  }
  return tmp - s;
}

//* same, but use <delim> like space - for end of name stop
int cli::word_len(const char* s, unsigned len, char delim){
    if (s == nullptr)
        return 0;
  const char* tmp = s;
  for(; len > 0; len--, tmp++){
    char c = *tmp;
    if (c == 0)
      break;
    if (c == delim)
        break;
    if (isalnum(c)||ispunct(c))
      continue;
    break;
  }
  return tmp - s;
}

//* word_len + check that after word is space or EOL
int cli::name_len(const char* s, unsigned len){
    if (s == nullptr)
        return 0;
  int res = word_len(s, len);
  if (res >= (int)len)
    return res;
  if (is_eol(s+res) >= 0)
    return res;
  if (isspace(s[res]))
    return res;
  return -1;
}

//* look to EOL or Z
int cli::line_len(const char* s, unsigned len){
    if (s == nullptr)
        return 0;
  const char* tmp = s;
  for(; len > 0; len--, tmp++){
    int el = is_eol(tmp);
    if (el < 0)
      continue;
    tmp += el;
    break;
  }
  return tmp - s;
}

//* look for '='
int cli::equal_len(const char* s, unsigned len)
{
    if (s == nullptr)
        return 0;
  const char* tmp = s;
  for(; len > 0; len--, tmp++){
    char c = *tmp;
    if (c == '=')
      break;
  }
  return tmp - s;
}

//* \return < 0 - no eol
//* \returm = 0 - Z ended
//* \return >0  - len of EOL token
int cli::is_eol(const char* s){
    if(s == nullptr)
        return 0;
  char c = *s;
  if (c == '\0')
    return 0;
  if (c == '\n')
    return 1;
  return -1;
}

//* suggests that <name> shorter vs <line>
//* \return = 0 - <name> not starts <line>
//* \return > 0 - len of <name> when one completely starts <line>
//* \return < 0 - len of same prefix in <line> and <name>
int cli::str_starts(const char* __restrict line, const char* __restrict name){
  if ((name == nullptr) || (line == nullptr))
    return 0;
  const char* a = line;
  const char* b = name;
  for ( ; *a == *b ; a++, b++){
    if (*a == 0) break;
  }
  if (*b == 0)
    return b - name;
  return -(a - line);
}

//* same as str_starts, ensures that mutched prefix <name> in <line>
//*    finishes by EOL, or space
int cli::name_starts(const char* __restrict line, const char* __restrict name){
  if ((name == nullptr) || (line == nullptr))
    return 0;

  int slen = space_len(line, ~0u);
  line += slen;
  if (is_eol(line) >= 0)
      return 0;

  int res = str_starts(line, name);
  if (res > 0){
    char c = line[res];
    if ( (c == 0)
      //|| isspace(c)
      //|| (c == '\n')
      || (!isalnum(c))
      )
      return res+slen;
    else
      return -res-slen;
  }
  return 0;
}

int cli::names_starts(const char* __restrict line, const char* __restrict name){
  if ((name == nullptr) || (line == nullptr))
    return 0;

  int slen = space_len(line, ~0u);
  line += slen;
  if (is_eol(line) >= 0)
      return 0;

  while (true) {
  name += space_len(name, ~0u);
  if (is_eol(name) >= 0)
      return 0;

  int res = str_starts(line, name);
  if (res > 0){
    char c = line[res];
    if ( (c == 0)
      //|| isspace(c)
      //|| (c == '\n')
      || (!isalnum(c))
      )
      return res+slen;
    else {
        //go to next name variant
        name += res;
    }
  }
  else if (res == 0)
      break;
  else {
      if (name[-1-res] ==' '){
          // found a name in names
          return slen-res;
      }
      //go to next name variant
      //name -= res;
      name += word_len(name, ~0u);
  }

  }//while
  return 0;
}

const char* cli::after_name(const char* __restrict line
                            , const char* __restrict name)
{
    int len = cli::name_starts(line, name);
    if ( len <= 0 )
      return nullptr;
    line += len;
    line += cli::space_len(line, ~0u);
    return line;
}

//* @return true - if string is at end, or at word '--'.
//                 such arg responses as finish of command arguments sequence
bool cli::is_last_arg(const_line line){
    int slen = name_starts(line, "--");
    return (slen == 2);
}


//* \return index of mutched in <names>
//*         , <lend> = position in <line> after mutched
//* \return  = -1 -  if fail mutches
int cli::mutch_names(const char* __restrict line
                    , const char** __restrict lend
                , const char* const * __restrict names)
{
  for (int i = 0; *names != nullptr; i++, names++ ){
    const char* name = *names;
    int len = name_starts(line, name);
    if (len > 0){
      if (lend != nullptr)
        *lend = line + len;
      return i;
    }
  }
  return -1;
}

// \return > 0 - anount of accepted chars, x = parsed value
int cli::take_number(long& x, const char* s){
    char* line = (char*)s;
    line += space_len(line, ~0u);
    if (is_eol(line) >= 0)
        return 0;
    if (*line == '0'){
        if (strchr("xXbBoO", line[1]) != nullptr){
            x = (long)strtoul(line, &line, 0u);
        }
        else
            x = strtol(line, &line, 0);
    }
    else if (*line == '$'){
        x = (long)strtoul(line+1, &line, 16u);
    }
    else {
        x = strtol(line, &line, 0u);
    }
    return line-s;
}

// \return > 0 - anount of accepted chars, x = parsed value
//           s - s updates to new position after number
const char* cli::pass_number(long& x, const char* s){
    int len = take_number(x, s);
    if (len > 0)
        return s + len;
    return nullptr;
}

const char* cli::pass_number(double& x, const char* s){
    char* line = (char*)s;
    line += space_len(line, ~0u);
    if (is_eol(line) >= 0)
        return nullptr;
    char* cmd = line;
    double val = strtod(line, &cmd);
    if (line == cmd){
        return nullptr;
    }
    if (!std::isnormal(val)){
        return nullptr;
    }
    x = val;
    return cmd;
}

const char* cli::pass_number(char mark, long& x, const char* s){
    const char* line = s;

    line += space_len(line, ~0);
    if (is_eol(line) >= 0)
        return nullptr;

    if (*line++ != mark)
        return nullptr;

    int len = take_number(x, line);
    if (len > 0)
        return line + len;

    return nullptr;
}

const char* cli::pass_number( char markx, long& x
                        ,char marky, long& y, const char* s)
{
    const char* line = s;
    line += space_len(line, ~0u);
    if (is_eol(line) >= 0)
        return nullptr;

    if (*line++ != markx)
        return nullptr;

    int len = take_number(x, line);
    if (len <= 0)
        return nullptr;

    line += len;
    if (*line++ != marky)
        return nullptr;

    len = take_number(y, line);
    if (len <= 0)
        return nullptr;

    return line + len;
}




//------------------------------------------------------------------------------
//      Commands Set - static array of commands + handle parsers defs

//* \return index of mutched in <names>
//*         , <lend> = position in <line> after mutched
//* \return  = -1 -  if fail mutches, and last item empty
//*          = last idx - if last item have some value
template <>
int cli::mutch_names<CLICommandBase>(const char* __restrict line
                , const char** __restrict lend
                , BaseCommands cmds)
{
    assert(cmds);
    int i = -1;
    for (i = 0; cmds->name != nullptr; i++, cmds++ ){
      const char* name = cmds->name;
      int len = names_starts(line, name);
      if (len > 0){
        if (lend!=nullptr)
            *lend = line + len;
        return i;
      }
    }
    // if last item have some contents - return it
    if ( !cmds->is_empty() )
        return i;
    return -1;
}

//parse line for commands in cmds
template <>
int cli::process<CLICommandBase>(CLICommandBase* self, CLI_shell* shell
                , const char* __restrict line
                , BaseCommands cmds)
{
    const char* lend = line;
    int cmdi = mutch_names(line, &lend, cmds);
    if (cmdi < 0)
        return -1;
    shell->acceptChars(lend);
    cli_processing<CLICommandBase> proc = cmds[cmdi].proc;
    int res = (self->*proc) (shell, lend);
    return res;
}



//-----------------------------------------------------------------------------
//* \return index mutched in <cmds> by <cmds.name> field.
//*         where <cmds.name> - space-separated list of mutch variants
//*         , <lend> = position in <line> after mutched
//* \return  = -1 -  if fail mutches, and last item empty
//*          = last idx - if last item have some value
int cli::mutch_names(const char* __restrict line
                , const char** __restrict lend
                , CLINamesDef cmds)
{
    assert(cmds);
    int i = -1;
    for (i = 0; cmds->name != nullptr; i++, cmds++ ){
      const char* name = cmds->name;
      int len = name_starts(line, name);
      if (len > 0){
        if (lend!=nullptr)
            *lend = line + len;
        return cmds->id;
      }
    }
    // if last item have some contents - return it
    if ( cmds->id != 0 )
        return cmds->id;
    return -1;
}



//-----------------------------------------------------------------------------
#include "cli_args.hpp"

int CLIDeviceCommands::cli_process(CLI_shell* shell, const_line line){
    if (sub_cmds == nullptr)
        return inherited::cli_process(shell, line);

    const_line cmd = after_cmd(line);
    if (cmd == nullptr)
        return 0;

    
    int res = cli_process(shell, cmd, sub_cmds);
    if (res >= 0)
        return res + (cmd-line);
    return res;
}

int CLIDeviceCommands::cli_process(CLI_shell* shell, const_line line, cmds_t   sub_cmds)
{
    if (sub_cmds == nullptr)
        return 0;
    const_line cmd = line;
    if (cmd == nullptr)
        return 0;

    const char* lend = cmd;
    int cmdi = mutch_names(cmd, &lend, sub_cmds);
    if (cmdi < 0){
        //not recognized any cmd
        if (!is_cmd_empty()) {
            shell->acceptChars(cmd);
            return -1;
        }
        else
            return 0;
    }

    cli_processing<CLICommandBase> proc = sub_cmds[cmdi].proc;
    if (proc != nullptr) {
        int res = invoke_sub (proc, shell, lend);
        if (res >= 0)
            return res + (lend-line);
        else {
            shell->acceptChars(lend);
            return res;
        }
    }

    if (sub_cmds[cmdi].dummy == nullptr)
        //пустая команда без обработчика, считаем ok
        return (lend-line);

    if ( cli_process_dummy(shell, (sub_cmds[cmdi])) )
        return (lend-line);
    else {
        shell->acceptChars(lend);
        return -1;
    }
}

bool CLIDeviceCommands::cli_process_dummy(CLI_shell* shell, const cli::CmdItem<CLICommandBase>& cmd)
{
    (void)shell; (void)cmd;
    return false;
}

//* печатает хелп команды в шелл
void CLIDeviceCommands::cli_help(CLI_shell* shell){
    if (sub_cmds) {
        if (!is_cmd_empty())
            shell->printf("%s commands:\n", cmd_name);
        print_full(shell, sub_cmds);
    }
    else inherited::cli_help(shell);
}

void CLIDeviceCommands::cli_brief(CLI_shell* shell){
    if (sub_cmds) {
        if (!is_cmd_empty())
            shell->printf("%s commands:\n", cmd_name);
        print_brief(shell, sub_cmds);
    }
    else inherited::cli_brief(shell);
}


int CLIDeviceCommands::cli_help_cmd(CLI_shell* shell, const_line line){
    cli_help(shell);
    (void)line;
    return 0;
}

//* печатает хелп команды в шелл
void CLIDeviceCommands::print_brief(CLI_shell* shell, cmds_t x){
    const char* ident = "";
    if (!is_cmd_empty())
        ident = "    ";

    for(unsigned i = 0; x[i].name != nullptr; ++i ){
        shell->printf("%s%s\n", ident, x[i].name, x[i].help);
    }
}

void CLIDeviceCommands::print_full(CLI_shell* shell, cmds_t x){
    const char* ident = "";
    if (!is_cmd_empty())
        ident = "    ";

    for(unsigned i = 0; x[i].name != nullptr; ++i ){
        shell->printf("%s%s\t%s\n", ident, x[i].name, x[i].help);
    }
}

// check line starts with cmd_name, and returns line after it, skips spaces
// \return NULL - line not starts with cmd
CLIDeviceCommands::const_line CLIDeviceCommands::after_cmd(const_line line) const{
    if (is_cmd_empty()) {
        if (sub_cmds == nullptr)
            return nullptr;
        else {// пустая команда допустима, если подкоманды определены,
            // тогда они могут работать как первое слово в команде
            const char* lend = line;
            int cmdi = mutch_names(line, &lend, sub_cmds);
            if (cmdi >=0)
                //если субкоманда найдена, то анонимный after_cmd указывает на нее
                return line;
            else
                return nullptr;
        }
    }
    return inherited::after_cmd(line);
}


// \return = NULL - cant parse any
//           s - s updates to new position after all processed words
CLIDeviceCommands::const_line CLIDeviceCommands::process_sequence(CLI_shell* shell
    , const_line s, const SequenceAction<CLICommandBase> act)
{
    const_line cmd = s;
    cmd += cli::space_len(s);
    while (cli::is_no_eol(s)) {
        int wlen = cli::name_len(cmd, ~0u);
        // break args process on '--' or '++'
        if (wlen == 2){
            if ((cmd[0] == '-') && (cmd[1] == '-')){
                cmd += wlen;
                break;
            }
            if ((cmd[0] == '+') && (cmd[1] == '+')){
                cmd += wlen;
                break;
            }
        }
        if (wlen > 0) {
            auto ok = invoke_sub(act, cmd, wlen, shell);
            if (ok == srBreak)
                break;
            cmd += wlen;
            continue;
        }
        else 
            break;
    }
    return cmd;
}

// invoke act on every inte if seq {name=value}*
// \return = NULL - cant parse any
//           s - s updates to new position after all processed words
CLIDeviceCommands::const_line CLIDeviceCommands::process_args(CLI_shell* shell
    , const_line s, const ArgsAction<CLICommandBase> act)
{
    cli::ArgRef arg;
    
    const_line cmd = s;
    cmd += cli::space_len(s);
    while (cli::is_no_eol(cmd)) {
        cmd = arg.pass(cmd);
        if (arg.is_valid()){
            auto ok = invoke_sub(act, arg.cmd, arg.wlen, arg.val, arg.vlen, shell);
            if (ok == srBreak)
                break;
        }
        else {
            if (arg.vlen < 0)
                break;
            // аргумент покрашил строку, нельзя более парсить
            shell->acceptChars(cmd);
            return nullptr;
        }

        cmd += cli::space_len(cmd);
    }
    return cmd;
}


//-----------------------------------------------------------------------------

CLICommandContainer::CLICommandContainer()
{}

CLICommandContainer::CLICommandContainer(const_cmd _cmd, unsigned reserve)
:inherited(_cmd)
, _cmds(reserve)
{}

int CLICommandContainer::cli_process(CLI_shell* shell, const_line line){
    const_line cmd = after_cmd(line);
    if (cmd == nullptr)
        return 0;

    if (cli::is_eol(cmd) >= 0){
        commands().help(shell);
        return (cmd-line);
    }

    CLICommand* cmdi = _cmds.lookup(cmd);
    if (cmdi == nullptr){
        return on_no_subcmd(shell, cmd);
    }

    int res = cmdi->cli_process(shell, cmd);
    if (res >= 0)
        return res + (cmd-line);
    else {
        return res;
    }
}

int CLICommandContainer::on_no_subcmd(CLI_shell* shell, const_line cmd){
    if (!is_cmd_empty()) {
        shell->acceptChars(cmd);
        cli_brief(shell);
        return -1;
    }
    else
        return 0;
}

void CLICommandContainer::cli_help(CLI_shell* shell){
    if (commands().empty()){
        shell->printf("%s have no commands\n", cmd_name);
        return;
    }

    if (!is_cmd_empty()){
        shell->printf("%s commands:\n", cmd_name);
    }
    commands().list_brief(shell);
}

void CLICommandContainer::cli_brief(CLI_shell* shell){
    cli_help(shell);
}


CLICommandContainer::const_line CLICommandContainer::after_cmd(const_line line) const {
    if (is_cmd_empty()) {
        if (_cmds.empty() )
            return nullptr;
        else {// пустая команда допустима, если подкоманды определены,
            // тогда они могут работать как первое слово в команде
            CLICommand* cmdi = _cmds.lookup(line);
            if (cmdi != nullptr)
                return line;
            else
                return nullptr; 
        }
    }
    return inherited::after_cmd(line);
}



//------------------------------------------------------------------------------
int CLICommandInvoke::cli_process(CLI_shell* shell, const_line line) {
    const_line cmd = after_cmd(line);
    if (cmd == nullptr)
        return 0;
    return (cmd - line) + _f(shell, cmd);
}

void CLICommandInvoke::cli_help(CLI_shell* shell){
    if (help_banner)
        shell->puts(help_banner);
    else
        inherited::cli_help(shell);
}


//------------------------------------------------------------------------------
// drops count chars from position dst
void CLIVectorLine::drop( char* dst , unsigned count){
    if ( (dst >= &(*cbegin()) ) && (dst < &(*cend())) ) {
        unsigned least = &(*cend()) - dst;
        if ( least > count ){
            memcpy( dst, dst+count, (least-count) );
            resize( size() - count);
        }
        else {
            // просто обрежем конец строки
            *dst = '\0';
            resize( dst - &(*begin())+1 );
        }
    }
}

void CLIVectorLine::drop_head(unsigned count){
    unsigned least = size();
    char* dst = &(*begin());
    if ( least > count ){
        memcpy( dst, dst+count, (least-count) );
        resize( size() - count);
    }
    else {
        clear();
    }
}



//------------------------------------------------------------------------------
CLIArgsProcessor::CLIArgsProcessor(const CLINameDef* args_set)
:args((CLIArgsDef)args_set)
,_on_uncknown_arg(srBreak)
,_on_bad_arg(srOK)
{
    assert(args_set != nullptr);
}


CLIArgsProcessor::SeqResult CLIArgsProcessor::on_bad_arg(CLI_shell* shell, ArgID id){
    (void)shell;(void)id;
    return _on_bad_arg;
}

const char* ArgRef::pass(const char* s)
{
    cmd = s;
    val = nullptr;
    wlen = vlen = 0;
    cmd += cli::space_len(s);
    if (cli::is_eol(s) >= 0 ) 
        return nullptr;
        
    wlen = cli::word_len(cmd, ~0u, '=');
    // break args process on '--'
    if (wlen == 2)
        if ((cmd[0] == '-') && (cmd[1] == '-')){
            cmd += wlen;
            vlen = cmdMINUS;
            return cmd;
        }

    if (wlen <= 0) 
        return nullptr;

    val = cmd + wlen;
    if (val[0] != '=')
        return val;

    val++;
    // do not allow space after '='
    //val += cli::space_len(val);
    vlen = 0;
    unsigned waslen = 0;

    while( !isspace(val[vlen]) ) {
    waslen = vlen;
    const_line quote = strchr(val+vlen, '\'');
    const_line qquote = strchr(val+vlen, '"');
    vlen += cli::name_len(val+vlen, ~0u);

    if ((quote) && (vlen > (quote-val))) {
        //opened string
        quote = strchr(quote+1, '\'');
    }
    else if ((qquote) && (vlen > (qquote-val))){
        quote = strchr(qquote+1, '"');
    }
    else {
        //break;
        return val + vlen;
    }

    if (is_eol(quote)>= 0){
        vlen = 0;
        return quote;
    }
    vlen = (quote-val)+1;
    }//while(true)

    s = val + vlen;
    if ((waslen > 0) || (val[0] != val[vlen-1]))
        //this arg is composed by some strings
        return s;
    
    //a single string, trim enclosing comma chars
    if ((val[0] == '\'') || (val[0] == '"')){
        ++val;
        vlen -= 2;
    }
    return s;
}

template <> 
CLIArgsProcessor::SeqResult   CLIArgsProcessor::pass<argLONG>(ArgID id, CLI_shell* shell){
    long x;
    const_line s = pass_number(x, arg.val);
    if (s == nullptr)
        return srBAD;
    return assign_int(shell, id, x);
}

template <> 
CLIArgsProcessor::SeqResult   CLIArgsProcessor::pass<argULONG>(ArgID id, CLI_shell* shell){
    long x;
    const_line s = pass_number(x, arg.val);
    if (s == nullptr)
        return srBAD;
    if (x < 0){
        shell->printf("argument %*s expected >= 0\n", arg.wlen, arg.cmd);
        return on_bad_arg(shell, id);
    }
    return assign_int(shell, id, x);
}

template <> 
CLIArgsProcessor::SeqResult   CLIArgsProcessor::pass<argFLOAT>(ArgID id, CLI_shell* shell){
    double x;
    const_line s = pass_number(x, arg.val);
    if (s == nullptr)
        return srBAD;
    if (!std::isnormal(x)){
        shell->printf("argument %*s expected be a valid float\n", arg.wlen, arg.cmd);
        return on_bad_arg(shell, id);
    }
    return assign_float(shell, id, x);
}

template <> 
CLIArgsProcessor::SeqResult   CLIArgsProcessor::pass<argSTRING>(ArgID id, CLI_shell* shell){
    return assign_str(shell, id, arg.val, arg.vlen);
}

template <> 
CLIArgsProcessor::SeqResult   CLIArgsProcessor::pass<argBOOL>(ArgID id, CLI_shell* shell){
    const_line ok = nullptr;
    int x = mutch_bool(arg.val, &ok);
    if (x < 0)
       x =  mutch_onoff(arg.val, &ok);
    if (x < 0){
        shell->printf("argument %*s expected be a yes|no|on|off\n", arg.wlen, arg.cmd);
        return on_bad_arg(shell, id);
    }
    return assign_bool(shell, id, (bool)x);
}

template <> 
CLIArgsProcessor::SeqResult   CLIArgsProcessor::pass<argONOFF>(ArgID id, CLI_shell* shell){
    const_line ok = nullptr;
    int x =  mutch_onoff(arg.val, &ok);
    if (x < 0){
        shell->printf("argument %*s expected be a on|off\n", arg.wlen, arg.cmd);
        return on_bad_arg(shell, id);
    }
    return assign_bool(shell, id, (bool)x);
}

int CLIArgsProcessor::cli_process(CLI_shell* shell, const_line s){
    const_line cmd = s;
    cmd += cli::space_len(s);

    while (cli::is_no_eol(cmd)) {
        cmd = arg.pass(cmd);
        if (!arg.is_valid()){
            if (arg.vlen < 0)
                //accepted args break by @sa BADCmdID
                break;
            shell->acceptChars(cmd);
            return -1;
        }

        SeqResult ok = srOK;
        CLINameDef::const_cmd mutched_wlen = cmd;
        int id = mutch_names(arg.cmd, &mutched_wlen, args);
        if ( (mutched_wlen-arg.cmd) != arg.wlen ){
            ok = on_uncknown_arg();
        }

        if (ok == srOK){
            auto    style   = CLIArgDef::as_style(id);
            auto    aid     =  CLIArgDef::as_id(id);
            switch ( (int)style ) {
            case argLONG: ok = pass<argLONG>(aid, shell);break;
            case argULONG:ok = pass<argULONG>(aid, shell);break;
            case argFLOAT:ok = pass<argFLOAT>(aid, shell);break;
            case argSTRING:ok = pass<argSTRING>(aid, shell);break;
            case argBOOL:ok = pass<argBOOL>(aid, shell);break;
            case argONOFF:ok = pass<argONOFF>(aid, shell);break;
            default:
                ok = on_bad_arg(shell, aid);
                if (ok != srOK)
                    shell->printf("argument %*s uncknown style\n", arg.wlen, arg.cmd);
            }
        }
        if (ok == srBAD){
            shell->printf("argument %*s unaccepted\n", arg.wlen, arg.cmd);
        }
        else if (ok >= srSTOP) {
            if (ok == srBreak){
                shell->acceptChars(cmd);
                return -1;
            }
            break; // srSTOPed
        }

        cmd += cli::space_len(cmd);
    }//while (cli::is_no_eol(cmd))

    return cmd-s;
}

CLIArgsProcessor::SeqResult CLIArgsProcessor::assign_bool(CLI_shell* shell, ArgID id, bool value){ 
    (void)id;(void)value; (void)shell;
    return srBreak;
}

CLIArgsProcessor::SeqResult CLIArgsProcessor::assign_int(CLI_shell* shell, ArgID id, long value){
    (void)id;(void)value; (void)shell;
    return srBreak;
}

CLIArgsProcessor::SeqResult CLIArgsProcessor::assign_float(CLI_shell* shell, ArgID id, double value){
    (void)id;(void)value; (void)shell;
    return srBreak;
}

CLIArgsProcessor::SeqResult CLIArgsProcessor::assign_str(CLI_shell* shell, ArgID id, const_line value, unsigned len){
    (void)id;(void)value; (void)shell; (void) len;
    return srBreak;
}


//------------------------------------------------------------------------------
CLI_shell::CLI_shell()
: mycmds(cli_cmd_reserve)
{
  init();
};

void CLI_shell::init(){
  cmdline.reserve(cmdlimit);
  clear();
}

bool CLI_shell::registerCmd(CLICommand *cmd){
  return mycmds.append(cmd);
}

bool CLI_shell::unregisterCmd(CLICommandBase *cmd){
    return mycmds.remove(*cmd);
}


CLICommand* CLI_shell::lookupCmd(CLICommand::const_line cmd){
  CLICommand* res = mycmds.lookup(cmd);
  if (res != nullptr)
    return res;
  res = cmds.lookup(cmd);
  return res;
}

void CLI_shell::help(){
  mycmds.help(this);
  cmds.help(this);
}

void CLI_shell::clear(){
  cmdline.resize(1);
  cmdline.front() = '\0';
  cursor = &cmdline.back();
}

//* продвигает курсор парсера в командной строке
const char* CLI_shell::acceptChars(int len){
  if (len < 0){
      // rewind
      unsigned back = (unsigned)-len;
      if (cmdline.is_in(cursor)){
          assert( -len <= ( cursor - &(cmdline.front()) ) );
          cursor     -= back;
          cursor_len += back;
      }
      else {
          // TODO -это надо ловить асертом?
          cursor     -= back;
          cursor_len += back;
      }
      return cursor;
  }

  unsigned forward = (unsigned)len;
  if (cursor_len >= forward){
    cursor     += forward;
    cursor_len -= forward;
  }
  else {
    cursor     += cursor_len;
    cursor_len = 0;
    if (cursor == &(*cmdline.end()) )
      clear();
  }
  return cursor;
}

const char* CLI_shell::acceptChars(const char* line){
    if (line <= cursor)
        return cursor;
  int len = line - cursor;
  if (len < 0)
    return nullptr;
  return acceptChars(len);
}


//* добавляет символ в строку
void CLI_shell::addChar(u8 ch){
  if (cmdline.size() >= cmdlimit)
      return;
  cmdline.back() = ch;
  cmdline.push_back('\0');
};

//* добавляет символ в начало строки
void CLI_shell::unwindChar(u8 ch){
    if (cursor <= cmdline.data() )
        return;
    cursor--;
    *(char*)(cursor) = ch;
    cursor_len++;
}

//* исполняет набранную в cmdline строку
void CLI_shell::processCmd(){
    if (cmdline.size() <= 0)
        return;

  processLine( &(*cmdline.begin()), cmdline.size());
  acceptChars(cli::space_len(cursor,cursor_len));
}

void CLI_shell::dropProcessed( void ){
    if (cursor_len == 0){
      clear();
      return;
    }

    unsigned accepted = cmdline.size();
    CLI_ASSERT(accepted >= cursor_len);
    accepted -= cursor_len;

    cmdline.drop_head(accepted);
    cursor = &(*cmdline.begin());
    cursor_len = cmdline.size();
}

#if CLI_SHELL_EXCEPITONALE
#define TRY()   try
#define CATCH() catch(...)
#else
#define TRY()
#define CATCH() if(false)
#endif

int CLI_shell::processLine(const char* line, unsigned len){
    assert( (line != NULL) && (len > 0) );
  cursor = line;
  cursor_len = len;
  for (acceptChars(space_len(cursor, cursor_len))
      ; cursor_len > 0
      ; acceptChars(space_len(cursor, cursor_len)) )
  {
      int ateol = is_eol(cursor);
      if (ateol > 0)
        continue;
      else if (ateol == 0){
          if (cursor_len == 0)
            break;
          // skip lineZ, for new line
          cursor++;
          cursor_len--;
      }

#ifdef LOGAPI
    debug.log(vINFO, "cli:cmd:%.*s\n", linelen, cursor);
    debug.flush();
#endif
    int parsed = 0;
    TRY() {
        parsed = mycmds.cli_process(this, cursor);
    }
    CATCH(){
        parsed = 0;
    }
    if (parsed > 0){
      cursor = acceptChars(parsed);
      continue;
    }
    else if (parsed < 0)
      break;

    parsed = cmds.cli_process(this, cursor);
    if (parsed > 0){
      cursor = acceptChars(parsed);
      continue;
    }
    else if (parsed < -1)
      break;
    else {
      int LL = cli::line_len(cursor, cursor_len);
      int show_LL = (LL < 16)? LL : 16;
      printf("uncknown cli command:%.*s...\n", show_LL, cursor);
      acceptChars(LL);
      break;
    }
  }
  return (line - cursor);
}

//virtual
int CLI_shell::getLine(char* line, unsigned limit, char eol){
  assert((line != NULL) && (limit > 0));
  if (cursor_len <= 0){
    *line = '\0';
    return 0;
  }
  // использую свое копирование, чтобы не бегать по строке много раз
  //    найти конец + копировать строку. 
  //    делаю все это одним циклом
  const char* src = cursor;
  char* dst = line;
  limit--;
  if (line == cursor){
      //accept line from cursor
      for ( ; limit > 0; limit--){
        char c = *src;
        if (c == '\0')
            break;
        src++;
        if ( c == eol )
            break;
      }
      dst = (char*)src;
  }
  else // copy line from cursor
  for ( ; limit > 0; limit--){
    char c = *src++;
    if (c == '\0')
        break;
    *dst++ = c;
    if ( c == eol )
    {
        break;
    }
  }
  *dst = '\0';
  return dst - line;
}

//virtual
int CLI_shell::puts(const char* s){
  (void)s;
  return 0;
}

// добавляет EOL к концу распечатанного
//virtual 
int CLI_shell::putln(const char* s){
    (void)s;
    return 0;
  }

//virtual
bool CLI_shell::ask_yes(void){
  if (cursor_len <= 0)
    return false;
  acceptChars( cli::space_len(cursor, cursor_len) );
  char c = *cursor;
  switch (c){
    case 0: return false;
    case 'y':
    case 'Y':
      acceptChars(1);
      return true;
    default:
      acceptChars(1);
      return false;
  }
}



