/*
 * This file is part of the chronometer project.
 * Copyright 2019 Edward V. Emelianov <edward.emelianoff@gmail.com>.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

// Commands parser

#include "adc.h"
#include "GPS.h"
#include "lidar.h"
#include "str.h"
#include "time.h"
#include "usart.h"
#include "usb.h"

// flag to show new GPS message over USB
uint8_t showGPSstr = 0;

extern uint32_t shotms[];

/**
 * @brief cmpstr - the same as strncmp
 * @param s1,s2 - strings to compare
 * @param n - max symbols amount + 1 (!!!!)
 * @return 0 if strings equal or 1/-1
 */
int cmpstr(const char *s1, const char *s2, int n){
    int ret = 0;
    while(--n){
        ret = *s1 - *s2;
        if(ret == 0 && *s1 && *s2){
            ++s1; ++s2;
            continue;
        }
        break;
    }
    return ret;
}

/**
 * @brief getchr - analog of strchr
 * @param str - string to search
 * @param symbol - searching symbol
 * @return pointer to symbol found or NULL
 */
char *getchr(const char *str, char symbol){
    do{
        if(*str == symbol) return (char*)str;
    }while(*(++str));
    return NULL;
}

#define sendu(x) do{sendstring(u2str(x));}while(0)

/*
static void sendi(int32_t I){
    if(I < 0){
        sendchar('-');
        I = -I;
    }
    sendstring(u2str((uint32_t)I));
}*/

// echo '1' if true or '0' if false
static void checkflag(uint8_t f){
    if(f) sendchar('1');
    else sendchar('0');
}

/**
 * @brief showuserconf - show configuration over USB
 */
static void showuserconf(){
    sendstring("DISTMIN="); sendu(the_conf.dist_min);
    sendstring("\nDISTMAX="); sendu(the_conf.dist_max);
    sendstring("\nTRIGLVL="); sendu(the_conf.trigstate);
    sendstring("\nTRIGPAUSE={");
    for(int i = 0; i < TRIGGERS_AMOUNT; ++i){
        if(i) sendstring(", ");
        sendu(the_conf.trigpause[i]);
    }
    sendstring("}\nUSART1SPD="); sendu(the_conf.USART_speed);
    sendstring("\nLIDARSPD="); sendu(the_conf.LIDAR_speed);
    sendstring("\nNFREE=");
    sendu(the_conf.NLfreeWarn);
    sendstring("\nSTREND=");
    if(the_conf.defflags & FLAG_STRENDRN) sendstring("RN");
    else sendchar('N');
    uint8_t f = the_conf.defflags;
    sendstring("\nSAVE_EVENTS=");
    checkflag(f & FLAG_SAVE_EVENTS);
    sendstring("\nGPSPROXY=");
    checkflag(f & FLAG_GPSPROXY);
    sendstring("\nLIDAR=");
    checkflag(!(f & FLAG_NOLIDAR));
    sendstring("\n"); // <-- sendstring @ the end to initialize data transmission
}

/**
 * @brief parse_USBCMD - parsing of string buffer got by USB
 * @param cmd - buffer with commands
 * @return 0 if got command, 1 if command not recognized
 */
void parse_CMD(char *cmd){
    char *oldcmd = cmd;
#define CMP(a,b)  cmpstr(a, b, sizeof(b))
#define GETNUM(x) do{if(getnum(cmd+sizeof(x)-1, &N)) goto bad_number;}while(0)
    static uint8_t conf_modified = 0;
    uint8_t succeed = 0;
    int32_t N;
    if(!cmd || !*cmd) return;
    IWDG->KR = IWDG_REFRESH;
    if(*cmd == '?' || CMP(cmd, "help") == 0){ // help
        sendstring("Commands:\n"
                 CMD_BUZZER    "S - turn buzzer ON/OFF\n"
                 CMD_CURDIST   " - show current LIDAR distance\n"
                 CMD_DELLOGS   " - delete logs from flash memory\n"
                 CMD_DISTMIN   " - min distance threshold (cm)\n"
                 CMD_DISTMAX   " - max distance threshold (cm)\n"
                 CMD_DUMP      "N - dump 20 last stored events (no x), all (x<1) or x\n"
                 CMD_FLASH     " - FLASH info\n"
                 CMD_GPSPROXY  "S - GPS proxy over USART1 on/off\n"
                 CMD_GPSRESTART " - send Full Cold Restart to GPS\n"
                 CMD_GPSSTAT   " - get GPS status\n"
                 CMD_GPSSTR    " - current GPS data string\n"
                 CMD_LEDS      "S - turn leds on/off (1/0)\n"
                 CMD_LIDAR     "S - switch between LIDAR (1) or command TTY (0)\n"
                 CMD_LIDARSPEED "N - set LIDAR speed to N\n"
                 CMD_GETMCUTEMP " - MCU temperature\n"
                 CMD_NFREE     " - warn when free logs space less than this number (0 - not warn)\n"
                 CMD_RESET     " - reset MCU\n"
                 CMD_SAVEEVTS  "S - save/don't save (1/0) trigger events into flash\n"
                 CMD_SHOWCONF  " - show current configuration\n"
                 CMD_STORECONF " - store new configuration in flash\n"
                 CMD_STREND    "C - string ends with \\n (C=n) or \\r\\n (C=r)\n"
                 CMD_PRINTTIME " - print current time\n"
                 CMD_TRIGLVL   "NS - working trigger N level S\n"
                 CMD_TRGPAUSE  "NP - pause (P, ms) after trigger N shots\n"
                 CMD_TRGTIME   "N - show last trigger N time\n"
                 CMD_USARTSPD  "N - set USART1 speed to N\n"
                 CMD_GETVDD    " - Vdd value\n"
                 );
    }else if(CMP(cmd, CMD_PRINTTIME) == 0){ // print current time
        sendstring(get_time(&current_time, get_millis()));
        sendstring("\n");
    }else if(CMP(cmd, CMD_DISTMIN) == 0){ // set low LIDAR limit
        GETNUM(CMD_DISTMIN);
        if(N < 0 || N > 0xffff) goto bad_number;
        if(the_conf.dist_min != (uint16_t)N){
            conf_modified = 1;
            the_conf.dist_min = (uint16_t) N;
        }
        succeed = 1;
    }else if(CMP(cmd, CMD_DISTMAX) == 0){ // set high LIDAR limit
        GETNUM(CMD_DISTMAX);
        if(N < 0 || N > 0xffff) goto bad_number;
        if(the_conf.dist_max != (uint16_t)N){
            conf_modified = 1;
            the_conf.dist_max = (uint16_t) N;
        }
        succeed = 1;
    }else if(CMP(cmd, CMD_STORECONF) == 0){ // store everything in flash
        if(conf_modified){
            if(store_userconf()){
                sendstring("Error: can't save data!\n");
            }else{
                conf_modified = 0;
                succeed = 1;
            }
        }
    }else if(CMP(cmd, CMD_GPSSTR) == 0){ // show GPS status string
        showGPSstr = 1;
    }else if(CMP(cmd, CMD_TRIGLVL) == 0){ // trigger levels: 0->1 or 1->0
        cmd += sizeof(CMD_TRIGLVL) - 1;
        uint8_t Nt = (uint8_t)(*cmd++ - '0');
        if(Nt > TRIGGERS_AMOUNT - 1) goto bad_number;
        uint8_t state = (uint8_t)(*cmd -'0');
        if(state > 1) goto bad_number;
        uint8_t oldval = the_conf.trigstate;
        if(!state) the_conf.trigstate = oldval & ~(1<<Nt);
        else the_conf.trigstate = (uint8_t)(oldval | (1<<Nt));
        if(oldval != the_conf.trigstate) conf_modified = 1;
        succeed = 1;
    }else if(CMP(cmd, CMD_SHOWCONF) == 0){ // print current configuration
        showuserconf();
    }else if(CMP(cmd, CMD_TRGPAUSE) == 0){ // pause after Nth trigger
        cmd += sizeof(CMD_TRGPAUSE) - 1;
        uint8_t Nt = (uint8_t)(*cmd++ - '0');
        if(Nt > TRIGGERS_AMOUNT - 1) goto bad_number;
        if(getnum(cmd, &N)) goto bad_number;
        if(N < 0 || N > 10000) goto bad_number;
        if(the_conf.trigpause[Nt] != (uint16_t)N){
            conf_modified = 1;
            the_conf.trigpause[Nt] = (uint16_t)N;
        }
        succeed = 1;
    }else if(CMP(cmd, CMD_TRGTIME) == 0){ // last trigger time
        cmd += sizeof(CMD_TRGTIME) - 1;
        uint8_t Nt = (uint8_t)(*cmd++ - '0');
        if(Nt > TRIGGERS_AMOUNT - 1) goto bad_number;
        show_trigger_shot((uint8_t)(1<<Nt));
    }else if(CMP(cmd, CMD_GETVDD) == 0){ // Vdd
        sendstring("VDD=");
        uint32_t vdd = getVdd();
        sendu(vdd/100);
        vdd %= 100;
        if(vdd < 10) sendstring(".0");
        else sendstring(".");
        sendu(vdd);
        sendstring("\n");
    }else if(CMP(cmd, CMD_GETMCUTEMP) == 0){ // ~Tmcu
        int32_t t = getMCUtemp();
        sendstring("MCUTEMP=");
        if(t < 0){
            t = -t;
            sendstring("-");
        }
        sendu(t/10);
        sendstring(".");
        sendu(t%10);
        sendstring("\n");
    }else if(CMP(cmd, CMD_LEDS) == 0){ // turn LEDs on/off
        uint8_t Nt = (uint8_t)(cmd[sizeof(CMD_LEDS) - 1] - '0');
        if(Nt > 1) goto bad_number;
        sendstring("LEDS=");
        if(Nt){
            LEDSon = 1;
            sendstring("ON\n");
        }else{
            LED_off();  // turn off LEDS
            LED1_off(); // by user request
            LEDSon = 0;
            sendstring("OFF\n");
        }
    }else if(CMP(cmd, CMD_GPSRESTART) == 0){ // restart GPS
        sendstring("Send full cold restart to GPS\n");
        GPS_send_FullColdStart();
    }else if(CMP(cmd, CMD_BUZZER) == 0){
        uint8_t Nt = (uint8_t)(cmd[sizeof(CMD_BUZZER) - 1] - '0');
        if(Nt > 1) goto bad_number;
        sendstring("BUZZER=");
        if(Nt){
            buzzer_on = 1;
            sendstring("ON\n");
        }else{
            buzzer_on = 0;
            sendstring("OFF\n");
        }
    }else if(CMP(cmd, CMD_GPSSTAT) == 0){ // GPS status
        sendstring("GPS status: ");
        const char *str = "unknown";
        switch(GPS_status){
            case GPS_NOTFOUND:
                str = "not found";
            break;
            case GPS_WAIT:
                str = "waiting";
            break;
            case GPS_NOT_VALID:
                str = "no satellites";
            break;
            case GPS_VALID:
                str = "valid time";
            break;
        }
        sendstring(str);
        if(Tms - last_corr_time < 1500)
            sendstring(", PPS working\n");
        else
            sendstring(", no PPS\n");
    }else if(CMP(cmd, CMD_USARTSPD) == 0){ // USART speed
        GETNUM(CMD_USARTSPD);
        if(N < 400 || N > 3000000) goto bad_number;
        if(the_conf.USART_speed != (uint32_t)N){
            the_conf.USART_speed = (uint32_t)N;
            conf_modified = 1;
        }
        succeed = 1;
    }else if(CMP(cmd, CMD_LIDARSPEED) == 0){ // LIDAR speed
        GETNUM(CMD_LIDARSPEED);
        if(N < 400 || N > 3000000) goto bad_number;
        if(the_conf.LIDAR_speed != (uint32_t)N){
            the_conf.LIDAR_speed = (uint32_t)N;
            conf_modified = 1;
        }
        succeed = 1;
    }else if(CMP(cmd, CMD_RESET) == 0){ // Reset MCU
        sendstring("Soft reset\n");
        NVIC_SystemReset();
    }else if(CMP(cmd, CMD_STREND) == 0){ // string ends in '\n' or "\r\n"
        char c = cmd[sizeof(CMD_STREND) - 1];
        succeed = 1;
        if(c == 'n' || c == 'N'){
            if(the_conf.defflags & FLAG_STRENDRN){
                conf_modified = 1;
                the_conf.defflags &= ~FLAG_STRENDRN;
            }
        }else if(c == 'r' || c == 'R'){
            if(!(the_conf.defflags & FLAG_STRENDRN)){
                conf_modified = 1;
                the_conf.defflags |= FLAG_STRENDRN;
            }
        }else{
            succeed = 0;
            sendstring("Bad letter, should be 'n' or 'r'\n");
        }
    }else if(CMP(cmd, CMD_FLASH) == 0){ // show flash size
        sendstring("FLASHSIZE=");
        sendu(FLASH_SIZE);
        sendstring("kB\nFLASH_BASE=");
        sendstring(u2hex(FLASH_BASE));
        sendstring("\nFlash_Data=");
        sendstring(u2hex((uint32_t)Flash_Data));
        sendstring("\nvarslen=");
        sendu((uint32_t)&_varslen);
        sendstring("\nCONFsize=");
        sendu(sizeof(user_conf));
        sendstring("\nNconf_records=");
        sendu(maxCnum - 1);
        sendstring("\nlogsstart=");
        sendstring(u2hex((uint32_t)logsstart));
        sendstring("\nLOGsize=");
        sendu(sizeof(event_log));
        sendstring("\nNlogs_records=");
        sendu(maxLnum - 1);
        sendstring("\n");
    }else if(CMP(cmd, CMD_SAVEEVTS) == 0){ // save all events
        if('0' == cmd[sizeof(CMD_SAVEEVTS) - 1]){
            if(the_conf.defflags & FLAG_SAVE_EVENTS){
                conf_modified = 1;
                the_conf.defflags &= ~FLAG_SAVE_EVENTS;
            }
        }else{
            if(!(the_conf.defflags & FLAG_SAVE_EVENTS)){
                conf_modified = 1;
                the_conf.defflags |= FLAG_SAVE_EVENTS;
            }
        }
        succeed = 1;
    }else if(CMP(cmd, CMD_DUMP) == 0){ // dump N last events
        if(getnum(cmd+sizeof(CMD_DUMP)-1, &N)) N = -20; // default - without N
        else N = -N;
        if(N > 0) N = 0;
        if(dump_log(N, -1)) sendstring("Event log empty!\n");
    }else if(CMP(cmd, CMD_NFREE) == 0){ // warn if there's less than N free cells for logs in flash
        GETNUM(CMD_NFREE);
        if(N < 0 || N > 0xffff) goto bad_number;
        if(the_conf.NLfreeWarn != (uint16_t)N){
            conf_modified = 1;
            the_conf.NLfreeWarn = (uint16_t)N;
        }
        succeed = 1;
    }else if(CMP(cmd, CMD_DELLOGS) == 0){ // delete all logs
        if(store_log(NULL)) sendstring("Error during erasing flash\n");
        else sendstring("All logs erased\n");
    }else if(CMP(cmd, CMD_GPSPROXY) == 0){ // proxy GPS data over USART1
        if(cmd[sizeof(CMD_GPSPROXY) - 1] == '0'){
            if(the_conf.defflags & FLAG_GPSPROXY){
                conf_modified = 1;
                the_conf.defflags &= ~FLAG_GPSPROXY;
            }
        }else{
            if(!(the_conf.defflags & FLAG_GPSPROXY)){
                conf_modified = 1;
                the_conf.defflags |= FLAG_GPSPROXY;
            }
        }
        succeed = 1;
    }else if(CMP(cmd, CMD_CURDIST) == 0){ // current LIDAR distance
        sendstring("DIST=");
        sendu(last_lidar_dist);
        sendstring("\nSTREN=");
        sendu(last_lidar_stren);
        sendstring("\nTRIGDIST=");
        sendu(lidar_triggered_dist);
        sendstring("\nTms=");
        sendu(Tms);
        sendstring("\nshotms=");
        sendu(shotms[LIDAR_TRIGGER]);
        sendstring("\n");
    }else if(CMP(cmd, CMD_LIDAR) == 0){ // turn LIDAR on/off
        if(cmd[sizeof(CMD_LIDAR) - 1] == '0'){
            if(!(the_conf.defflags & FLAG_NOLIDAR)){
                conf_modified = 1;
                the_conf.defflags |= FLAG_NOLIDAR;
            }
        }else{
            if(the_conf.defflags & FLAG_NOLIDAR){
                conf_modified = 1;
                the_conf.defflags &= ~FLAG_NOLIDAR;
            }
        }
        succeed = 1;
    }else{
        sendstring("Bad command: ");
        sendstring(oldcmd);
        sendstring("\n");
        return;
    }
    /*else if(CMP(cmd, CMD_) == 0){
        ;
    }*/

    IWDG->KR = IWDG_REFRESH;
    if(succeed) sendstring("Success!\n");
    return;
  bad_number:
    sendstring("Error: bad number!\n");
}

/**
 * @brief get_trigger_shot - print on USB message about last trigger shot time
 * @param number  - number of event (if > -1)
 * @param logdata - record from event log
 * @return string with data
 */
char *get_trigger_shot(int number, const event_log *logdata){
    static char buf[64];
    char *bptr = buf;
    if(number > -1){
        bptr = strcp(bptr, u2str(number));
        bptr = strcp(bptr, ": ");
    }
    if(logdata->trigno == LIDAR_TRIGGER){
        bptr = strcp(bptr, "LIDAR, dist=");
        bptr = strcp(bptr, u2str(logdata->lidar_dist));
        bptr = strcp(bptr, ", TRIG" STR(LIDAR_TRIGGER));
    }else{
        bptr = strcp(bptr, "TRIG");
        *bptr++ = '0' + logdata->trigno;
    }
    *bptr++ = '=';
    IWDG->KR = IWDG_REFRESH;
    bptr = strcp(bptr, get_time(&logdata->shottime.Time, logdata->shottime.millis));
    bptr = strcp(bptr, ", len=");
    if(logdata->triglen < 0) bptr = strcp(bptr, ">1s");
    else bptr = strcp(bptr, u2str((uint32_t) logdata->triglen));
    *bptr++ = '\n'; *bptr++ = 0;
    return buf;
}

/**
 * @brief show_trigger_shot printout @ USB data with all triggers shot recently (+ save it in flash)
 * @param tshot - each bit consists information about trigger
 */
void show_trigger_shot(uint8_t tshot){
    uint8_t X = 1;
    for(uint8_t i = 0; i < TRIGGERS_AMOUNT && tshot; ++i, X <<= 1){
        IWDG->KR = IWDG_REFRESH;
        if(tshot & X) tshot &= ~X;
        else continue;
        event_log l;
        l.elog_sz = sizeof(event_log);
        l.trigno = i;
        if(i == LIDAR_TRIGGER) l.lidar_dist = lidar_triggered_dist;
        l.shottime = shottime[i];
        l.triglen = triglen[i];
        sendstring(get_trigger_shot(-1, &l));
        if(the_conf.defflags & FLAG_SAVE_EVENTS){
            if(store_log(&l)) sendstring("\n\nError saving event!\n\n");
        }
    }
}

/**
 * @brief strln == strlen
 * @param s - string
 * @return length
 */
int strln(const char *s){
    int i = 0;
    while(*s++) ++i;
    return i;
}

/**
 * @brief strcp - strcpy (be carefull: it doesn't checks destination length!)
 * @param dst - destination
 * @param src - source
 * @return pointer to '\0' @ dst`s end
 */
char *strcp(char* dst, const char *src){
    int l = strln(src);
    if(l < 1) return dst;
    while((*dst++ = *src++));
    return dst - 1;
}


// read `buf` and get first integer `N` in it
// @return 0 if all OK or 1 if there's not a number; omit spaces and '='
int getnum(const char *buf, int32_t *N){
    char c;
    int positive = -1;
    int32_t val = 0;
    while((c = *buf++)){
        if(c == '\t' || c == ' ' || c == '='){
            if(positive < 0) continue; // beginning spaces
            else break; // spaces after number
        }
        if(c == '-'){
            if(positive < 0){
                positive = 0;
                continue;
            }else break; // there already was `-` or number
        }
        if(c < '0' || c > '9') break;
        if(positive < 0) positive = 1;
        val = val * 10 + (int32_t)(c - '0');
    }
    if(positive != -1){
        if(positive == 0){
            if(val == 0) return 1; // single '-'
            val = -val;
        }
        *N = val;
    }else return 1;
    return 0;
}

static char strbuf[11];
// return string buffer (strbuf) with val
char *u2str(uint32_t val){
    char *bufptr = &strbuf[10];
    *bufptr = 0;
    if(!val){
        *(--bufptr) = '0';
    }else{
        while(val){
            *(--bufptr) = val % 10 + '0';
            val /= 10;
        }
    }
    return bufptr;
}

// return strbuf filled with hex
char *u2hex(uint32_t val){
    char *bufptr = strbuf;
    *bufptr++ = '0';
    *bufptr++ = 'x';
    uint8_t *ptr = (uint8_t*)&val + 3;
    int i, j;
    IWDG->KR = IWDG_REFRESH;
    for(i = 0; i < 4; ++i, --ptr){
        for(j = 1; j > -1; --j){
            register uint8_t half = (*ptr >> (4*j)) & 0x0f;
            if(half < 10) *bufptr++ = half + '0';
            else *bufptr++ = half - 10 + 'a';
        }
    }
    *bufptr = 0;
    return strbuf;
}

static char localbuffer[LOCBUFSZ];
static uint8_t bufidx = 0;
static void transmitlocbuf(){
    localbuffer[bufidx] = 0;
    USB_send(localbuffer);
    if(!(the_conf.defflags & FLAG_GPSPROXY)){ // USART1 isn't a GPS proxy
        usart_send(1, localbuffer);
        transmit_tbuf(1);
    }
    if(the_conf.defflags & FLAG_NOLIDAR){ // USART3 isn't a LIDAR
        usart_send(LIDAR_USART, localbuffer);
        transmit_tbuf(LIDAR_USART);
    }
    bufidx = 0;
}
// add char to buf
void sendchar(char ch){
    localbuffer[bufidx++] = ch;
    if(bufidx >= LOCBUFSZ-1) transmitlocbuf();
}
/**
 * @brief addtobuf - add to local buffer any zero-terminated substring
 * @param str - string to add
 *  it sends data to USB and (due to setup) USART1 when buffer will be full or when meet '\n' at the end of str
 */
void sendstring(const char *str){
    while(*str) sendchar(*str++);
    if(str[-1] == '\n') transmitlocbuf();
}
