/*
 * hal_shell.cpp
 *
 *  Created on: 19 мар. 2019 г.
 *      Author: Lityagin Aleksandr
 *      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 <cassert>
#include "hal_shell.hpp"
#include <OsSync.h>
#include <ctype.h>

#define DEBUG_SHELL 0

#define SHELL_STR(x)   #x
#define SHELL_CAT(a, b) a b

#if DEBUG_SHELL
#define SHELL_DEBUG(msg, ...) printf( SHELL_CAT("(%s)",msg), __VA_ARGS__ )
#else
#define SHELL_DEBUG(...)
#endif



//------------------------------------------------------------------------------
CLI_LineInput::CLI_LineInput(CLIVectorLine& _line)
    : line( &(*_line.begin()) , _line.capacity()-1 )
    , eol(SHELL_EOL), ackeol('\n')
    , prompt_str(nullptr)
    , mode(mNONE)
    , device(nullptr)
{
    edit_by(this);
    //_line - считает длину строки учитывая Z
    // line - считает длину значащих символов строки
    line.resize(_line.size()-1);
    cursor      = &(*line.begin());
    cursor_len  = strnlen(cursor, line.size() );
    //line_ok     = cli::is_eol(cursor + cursor_len) > 0;
    line_ok     = false;
    drop_head(0); // проверю буффер на завершение строки
}

CLI_LineInput::~CLI_LineInput(){
    
}

void CLI_LineInput::init(io_t* adevice){ 
    device = adevice;

    clear();
    drop_head(0); // проверю буффер на завершение строки, нафига?
}

void CLI_LineInput::clear(){
    cursor      = &(*line.begin());
    *cursor     = '\0';
    cursor_len  = 0;
    line.resize(0);
    line_ok     = false;
}

bool CLI_LineInput::put_cursor(char x){
    if (cursor_pos() >= line.capacity())
        return false;

    if (cursor_len == 0){  //push_back
        *cursor++ = x;
        *cursor = '\0';
    }
    else { // overwrite
        *cursor++ = x;
        *cursor = '\0';
        cursor_len--;
    }
    return true;
}

int CLI_LineInput::echo(char x){
    if ((mode & mNO_ECHO) == 0)
        return io()->putChar(x);
    return 0;
}

int CLI_LineInput::echo(const char* x){
    if ((mode & mNO_ECHO) == 0)
        return io()->puts(x);
    return 0;
}

int CLI_LineInput::echo(const char* x, unsigned len){
    if ((mode & mNO_ECHO) == 0)
        return io()->putData(x, len);
    return 0;
}

int CLI_LineInput::echo_prompt(){
    if (prompt_str != nullptr)
        return echo(prompt_str);
    return 0;
}


// отбрасывает строку из буфера
// @return true - есть завершенная строка завершена
bool CLI_LineInput::drop_head(unsigned len){
    if (len > 0) {
        line_ok = false;
        auto haslen = cursor_pos();
        //assert(haslen >= 0);
        unsigned has = haslen;
        line.resize(has+cursor_len+1u);
        line.drop_head(len);

      if ( has > len )
        cursor -= len;
      else {
          cursor = &(*line.begin());
        if ( (has + cursor_len) <= len ){
            cursor_len = 0;
        }
        else
            cursor_len = (has + cursor_len) - len;
      }
    }//if (len > 0)

    // теперь проверю что принята целая строка
    for (auto pc = line.begin(); pc < line.end();){
        char c = *pc++;
        if ((c == '\n') || (c == '\0') ){
            line.resize( (pc-line.begin()) );
            line_ok = true;
        }
    }
    return line_ok;
}

//* добавляет символ в строку, и завершает команду на RETURN или /0
//  отрабатывает BACKSPACE
// @return true - введенная строка завершена
//virtual 
bool CLI_LineInput::input(int x){
    char ch = x;
    const char ascii_bell = 7;
    char c = ch; //этот символ кладу в принимаемую строку

    if (ch == '\b'){
        if (cursor > &(*line.begin()) ){
            cursor--;
            if (cursor_len <= 0){
                echo("\b \b");
            }
            else if ((mode & mNO_ECHO) == 0 
                     || false // не поддерживается пока редактирование строки
                     ) 
            {
                //io()->printf("\b%*s", cursor_len, cursor );
                // нехочу использовать printf, проделаю трюк
                *cursor = '\b';
                io()->putData(cursor, cursor_len);
            }
            line.drop(cursor, 1);
            cursor[cursor_len] = '\0';
        }
        else{
            // невозможно удалить от начала
            echo(ascii_bell);
        }
        return line_ok;
    }
    else if (ch == '\r') {
        // enter посредине строки, обрежу строку
        cursor_len = 0;
        put_cursor('\n');

        if (!line_ok)
            line.resize(cursor_pos());
        line_ok = true;
        if (ackeol != '\0')
            ch = ackeol;
    }
    else if ( (ch == '\0') || (ch == eol) ) {
        // enter посредине строки, обрежу строку
        cursor_len = 0;
        put_cursor('\n');

        if (!line_ok)
            line.resize(cursor_pos());
        line_ok = true;
        if (ackeol != '\0')
            ch = ackeol;
    }
    else if ( iscntrl(ch) ){
        // контрольные символы пропускаю, игнорирую, если не указано иное
        if ((mode & mNO_CONTROLS) == 0){
            c = '\0';
        }
    }
    else if (c != '\0') {
        if ( !put_cursor(c) )
            ch = ascii_bell;
    }

    echo(ch);

    return line_ok;
}

//virtual 
void CLI_LineInput::input_clear(){
    echo('\r');
    io()->send_erase_line(io()->eraseALL);
    echo_prompt();
    /*
    if (len() > 0) {
        echo('\r');
        for (unsigned i = 0; i < len(); i++)
            echo(' ');
        echo('\r');
    }
    */

    clear();
}

//неблокирующее считывание из io строку до завершения.
// @return true - введенная строка завершена
bool CLI_LineInput::input_io(){
    int len = device->get_wait();
    for (; len > 0; len--)
    {
        int tmp = device->get_char();
        if (tmp < 0 )
            break;
        if (editor->input(tmp))
            break;
    }
    return line_ok;
}

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

//virtual 
bool cli::CLIEditHistory::input(int x){
    if (!hystory_control(x)){
        if ( self.input(x) ){
            // on valid line break history navigate
            if (history_line == nullptr)
            //if ((x == '\0') || (x == '\r') || (x == '\n'))
            {
                hystory_append();
            }
            else {
                hystory_2top(history_line);
            }
            history_line = nullptr;
            return true;
        }
        else {
            history_line = nullptr;
            return false;
        }
    }
    return false;
}

#include "cli_keys.hpp"

bool cli::CLIEditHistory::hystory_control(int ch)
{

    bool is_arrow = ((ch == keyUP) || (ch == keyDN));
    if (!is_arrow){
        return false;
    }

    input_clear();
    const_line pred_line = nullptr;

        if (ch == keyUP) 
        {
            if (history_line == nullptr){
                history_line = history.next(nullptr);
            }
            pred_line = history.pred(history_line);
            if (pred_line != nullptr)
                history_line = pred_line;
            else
                pred_line = history_line;
        }
        else if (ch == keyDN)
        { 
            if (history_line == nullptr){
                //no history, nothing change
                return true;
            }
            pred_line = history.next(history_line);
            history_line = pred_line;
        }

    if (pred_line != nullptr) {
        //accept history string
        unsigned llen = cli::line_len(pred_line, ~0u);
        self.input(pred_line, llen);
    }

    return true;
}

void cli::CLIEditHistory::hystory_append(){
    if (line()->len() > 1){
        unsigned len = line()->len()-1;
        const_line same_in_hist = history.find_from(line()->line.begin(), len, nullptr);
        if (same_in_hist == nullptr)
            history.append(line()->line.begin(), line()->len()-1 ); // last char is lineEnd
        else
            history.move_2top(same_in_hist);
    }
}

void cli::CLIEditHistory::hystory_2top( const_line x){
    history.move_2top(x);
}

void cli::CLIEditHistory::input_clear(){
    self.input_clear();
}



//==============================================================================
Device_CLI_shell::Device_CLI_shell()
:inherited()
,StdOUT_ProxyPrint(&printer)
, device_terminal(nullptr)
, device(nullptr)
, input(cmdline)
{
}

Device_CLI_shell::Device_CLI_shell(StdIO_Device* con)
:inherited()
,StdOUT_ProxyPrint(&printer)
, device_terminal(nullptr)
, input(cmdline)
{
  init(con);
}

//virtual 
Device_CLI_shell::~Device_CLI_shell()
{
}

void Device_CLI_shell::addChar(u8 ch){
    //inherited::addChar(ch);
    input.edit()->input(ch);
}

//virtual
int Device_CLI_shell::getLine(char* line, unsigned limit, char eol){
    assert((line != NULL) && (limit > 0));
  char* dst = line;
  int len = inherited::getLine(line, limit, eol);
  if (len > 0){
    dst += len;
    limit -= len;
    if ((limit == 0) || (dst[-1] == eol))
      return len;
  }
  len += readLine(dst, limit);
  return len;
}

int Device_CLI_shell::readLine(char* line, unsigned limit){
    assert((line != NULL) && (limit > 0));

    bool ok = input.line_ok;
    while ( !ok ) {
        int res = io()->get_wait(get_poll_toms);
        if (res < 0)
            return res;
        input.input_io();
        ok = input.line_ok;
    }

    auto len = input.line.size();
    if (limit > len)
        limit = len;
    strncpy(line, input.line.begin(), limit );
    input.drop_head(len);
    return (int)len;
}

//* read string in cmdline from io, until length
int Device_CLI_shell::readStr(char* line, unsigned limit){
    assert((line != NULL) && (limit > 0));

    bool ok = input.line_ok || (input.len() >= limit );

    while ( !ok ) {
        io()->get_wait(get_poll_toms);
        input.input_io();
        ok = input.line_ok || (input.len() >= limit );
    }

    auto len = input.line.size();
    if (!input.line_ok)
        len = input.len();

    if (limit > len)
        limit = len;

    strncpy(line, input.line.begin(), limit );
    input.drop_head(len);
    return (int)len;
}

//virtual
int Device_CLI_shell::puts(const char* s){
  return device->puts(s);
}

// печатает в device
//virtual 
int Device_CLI_shell::putln(const char* s){
    int res = device->puts(s);
    return res + device->putChar(EOL);
}

void Device_CLI_shell::init(StdIO_Device * _device){
  inherited::init();
  device = _device;
  printer.init(device);
  device_terminal.init(_device);
  input.init(&device_terminal);
}

void Device_CLI_shell::clear(){
    input.clear();
    inherited::clear();
}


void Device_CLI_shell::dropProcessed( void ){
    if (cursor_len == 0){
      clear();
      return;
    }
    int accepted = cmdline.size() - cursor_len;
    assert(accepted >= 0);
    input.drop_head(accepted);
    cmdline.resize(input.line.size());
    cursor = &(*cmdline.begin());
    cursor_len = cmdline.size();
}


// TODO: возможно стоит убрать возможность выхода execute, по аварии io,
//      во встроенном приложении execute должен быть вечным?

void Device_CLI_shell::execute(const char* prompt){
    cmdline.resize(cmdlimit);
    input.prompt(prompt);

    while(device){

      if (prompt)
        if (input.echo_prompt() < 0)
            return;

      while (!input.line_ok){
          input.input_io();
          if ( input.io()->get_wait(get_poll_toms) < 0)
              return;
      }

      if (!input.line_ok)
          break;

      unsigned len = input.line.size();
      if (len > 0){
        cmdline.resize(len);
        cursor_len = len;
        cursor = &(*cmdline.begin());
        processCmd();
        dropProcessed();
      }
      else
        sleep_ms(get_poll_toms);
    }
}

//virtual
void Device_CLI_shell::on_fail(const char* s){
    assert(s != NULL);
  char buf[64];
  //int len =
  int LL = cli::line_len(s, ~0u);
  int show_LL = (LL < 10)? LL : 10;
  snprintf(buf, sizeof(buf),"failed parse command at:%.*s...\n"
           , show_LL, s);
  puts(buf);
}

//virtual
bool Device_CLI_shell::ask_yes(void){
  char tmp[2];
  int len = readStr(tmp, 2);
  if (len != 1)
    return false;
  //char c = device->get_char();;
  char c = tmp[0];
  switch (c){
    case 0: return false;
    case 'y':
    case 'Y':
      acceptChars(1);
      return true;
    default:
      acceptChars(1);
      return false;
  }
}


//---------------------------------------------------------------------------
// provide App_CLI_Shell termination command name
#ifndef CLI_SHELL_TERMINATE_CMD
#define CLI_SHELL_TERMINATE_CMD "exit"
#endif

class CLICmd_AppShellExit : public CLICommand{
public:
    typedef App_CLI_Shell   target_shell_t;
    CLICmd_AppShellExit();
    int cli_process(CLI_shell* shell, const_line line);
    void cli_help(CLI_shell* shell);
};

CLICmd_AppShellExit::CLICmd_AppShellExit(): CLICommand( CLI_SHELL_TERMINATE_CMD ){
}

int CLICmd_AppShellExit::cli_process(CLI_shell* shell, const_line line){
    int res = CLICommand::cli_process(shell, line);
    if (res <= 0)
        return res;

    target_shell_t* sh = dynamic_cast<target_shell_t*>(shell);
    if (sh){
        sh->terminate();
        return res;
    }
    return 0;
}

void CLICmd_AppShellExit::cli_help(CLI_shell* shell){
    target_shell_t* sh = dynamic_cast<target_shell_t*>(shell);
    if (sh == nullptr)
        return;
    shell->printf("%s - terminate current shell\n", cmd_name);
}

static 
CLICmd_AppShellExit     cli_exit_cmd;



//-----------------------------------------------------------------------
//static 
App_CLI_Shell::const_cmd App_CLI_Shell::default_shell_name = "shell";

App_CLI_Shell::App_CLI_Shell()
: inherited()
, cmd_t(default_shell_name)
, terminated(false)
{
    init_my_cmd();
}

App_CLI_Shell::App_CLI_Shell(StdIO_Device* _device, const_cmd shell_name)
: inherited(_device)
, cmd_t(shell_name)
, terminated(false)
{
    SHELL_DEBUG(" init\n", shell_name );
    init_my_cmd();
}

void App_CLI_Shell::init_my_cmd(){
    mycmds.append(&cli_exit_cmd);
}

void App_CLI_Shell::execute(const char* prompt){
    SHELL_DEBUG(" start\n", cmd_name );
    if (!device){
        SHELL_DEBUG(" no io\n", cmd_name );
        return ;
    }

    input.prompt(prompt);
    while(device){
      cmdline.resize(cmdlimit);

      SHELL_DEBUG(" prompt\n", cmd_name );
      if (prompt)
        if (input.echo_prompt() < 0)
            return;

      input.input_io();
      SHELL_DEBUG(" pre:%*s:\n", cmd_name, input.len(), input.line.begin() );
      while (!input.line_ok){
          if (is_terminated())
              return;
          if ( input.io()->get_wait(get_poll_toms) < 0)
              return;
          input.input_io();
          SHELL_DEBUG(" in:%*s:\n", cmd_name, input.len(), input.line.begin() );
      }

      unsigned len = input.line.size();
      SHELL_DEBUG(" have:%d\n", cmd_name, input.len());
      cursor = &(*cmdline.begin());
      cursor_len = len;
      if (len > 0){
        cmdline.resize(len);
        processCmd();
      }
      dropProcessed();
      SHELL_DEBUG(" post:%*s:\n", cmd_name, input.len(), input.line.begin() );

      if (is_terminated())
          break;
    }
}

int App_CLI_Shell::readLine(char* line, unsigned limit){
    assert((line != NULL) && (limit > 0));
    bool ok = input.line_ok;
    while ( !ok ) {
        if (is_terminated())
           break;
        int ok = io()->get_wait(get_poll_toms);
        if ( ok < 0)
            return ok;
        input.input_io();
        ok = input.line_ok;
    }

    auto len = input.line.size();
    if (limit > len)
        limit = len;
    strncpy(line, input.line.begin(), limit );
    input.drop_head(len);
    return len;
}

//virtual 
int App_CLI_Shell::cli_process(CLI_shell* shell, const_line cmd){
    (void)shell;
    
    const_line line = cli::after_name(cmd, cmd_name);
    if (line < cmd)
        return 0;
    processLine( line );
    int LL = cli::line_len(cursor, ~0u);
    return cursor + LL - cmd;
}

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


