/*-
 * Copyright (c) 2016 Colin Granville. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. 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.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 THE AUTHOR OR 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 <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <ctype.h>
#include <string.h>

#include "swis.h"
#include "file.h"

#ifndef SerialUSB_Info
#define SerialUSB_Info                  0x059c00
#define SerialUSB_FindDeviceByLocation  0x059c01
#define SerialUSB_FindDeviceByVendor    0x059c02
#define SerialUSB_FindDeviceBySerialNumber  0x059c03
#endif

#define SerialUSB_InfoVendor                0
#define SerialUSB_InfoSerialNumber          1
#define SerialUSB_InfoDeviceDescription     2
#define SerialUSB_InfoUnderlyingUSBDevice   3
#define SerialUSB_InfoInterfaceNumber       4
#define SerialUSB_InfoLocation              5

#define LINEBUFFER_HISTORY_COUNT            16
#define LINEBUFFER_SIZE                     1024


static  char    linebuffer_history[LINEBUFFER_HISTORY_COUNT][LINEBUFFER_SIZE];
static  int     linebuffer_history_pos = 0;
static  int     linebuffer_history_count = 0;
static  int     linebuffer_history_last = 0;
static  char    linebuffer_buf[LINEBUFFER_SIZE];
static  char*   linebuffer_ptr = linebuffer_buf;
static  char*   linebuffer_end = linebuffer_buf;

static int casestrcmp(const char* a, const char* b)
{
        int ac, bc;
        for (;;)
        {
                ac = *a++;
                bc = *b++;
                if (ac == 0 || bc == 0) break;
                if (ac >='a' && ac <= 'z') ac += ('A'-'a');
                if (bc >='a' && bc <= 'z') bc += ('A'-'a');
                if (ac != bc) break;
        }
        return ac - bc;
}

static int linebuffer_delete(void)
{
        if (linebuffer_buf == linebuffer_ptr) return 0;
        if (linebuffer_ptr < linebuffer_end)
        {
                char* p;
                _kernel_oswrch(8);
                for (p = linebuffer_ptr; p < linebuffer_end; p++) {p[-1] = *p; _kernel_oswrch(*p);}
                _kernel_oswrch(32);
                _kernel_oswrch(8);
                for (p = linebuffer_ptr; p < linebuffer_end; p++) _kernel_oswrch(8);
        }
        else
        {
                _kernel_oswrch(8);
                _kernel_oswrch(32);
                _kernel_oswrch(8);
        }
        linebuffer_ptr--;
        linebuffer_end--;
        return 1;
}

static int linebuffer_insert(int c)
{
        switch (c)
        {
                case 8: /* delete */
                        linebuffer_delete();
                        break;
                case 10: /* enter */
                case 13:
                        *linebuffer_end = 0;
                        return 0;
                case 'U'-'@': /* clearline */
                        for (;linebuffer_ptr < linebuffer_end; linebuffer_ptr++) _kernel_oswrch(9);
                        while (linebuffer_delete()) {}
                        break;
                case 156: /* move left - ctrl left arrow */
                        if (linebuffer_ptr > linebuffer_buf)
                        {
                                _kernel_oswrch(8);
                                linebuffer_ptr--;
                        }
                        break;
                case 157: /* move right - ctrl right arrow */
                        if (linebuffer_ptr < linebuffer_end)
                        {
                                _kernel_oswrch(9);
                                linebuffer_ptr++;
                        }
                        break;
                case 159: /* history up - ctrl up arrow */
                case 158: /* history_down - ctrl down arrow*/
                {
                        if (linebuffer_history_count == 0) break;
                        for (;linebuffer_ptr < linebuffer_end; linebuffer_ptr++) _kernel_oswrch(9);
                        while (linebuffer_delete()) {}
                        
                        if (c == 159)
                        {
                                if (--linebuffer_history_last < 0) linebuffer_history_last = linebuffer_history_count -1;
                        }
                        else
                        {
                                if (++linebuffer_history_last >= linebuffer_history_count) linebuffer_history_last = 0;
                        }
                        
                        for (char* p = linebuffer_history[linebuffer_history_last]; *p != 0; p++) 
                        {
                                _kernel_oswrch(*p); *linebuffer_ptr++ = *p; linebuffer_end++;
                        }
                        break;
                }
                default:
                        if (!isprint(c)) break;
                        _kernel_oswrch(c);
                        if (linebuffer_ptr != linebuffer_end)
                        {
                                char* p;
                                for (p = linebuffer_ptr; p < linebuffer_end; p++)  _kernel_oswrch(*p);
                                for (; p > linebuffer_ptr; p--) {_kernel_oswrch(8); *p = p[-1];}
                        }
                        *linebuffer_ptr++ = c;
                        linebuffer_end++;
                        break;
        }
        
        return 1;
}

static char* get_device(void)
{ 
        static struct {char name[32];} device[16];
        char desc[128];
        int n = 0;
        _kernel_swi_regs regs = {{ 9, (int) "devices:$", 0, 1, 0, sizeof(device[n].name), (int) "Serial*" }};
        while (regs.r[4] != -1)
        {
                sprintf(desc, "Motherboard");
                regs.r[2] = (int) device[n].name;
                regs.r[3] = 1;
                _kernel_swi(OS_GBPB, &regs, &regs);
                if (regs.r[3] == 1)
                {
                        int out =  file_open(FILE_OPEN_WRITE, "%s:", device[n].name);

                        if (out != 0)
                        {
                                file_close(out);
                                if (device[n].name[6] == 'U')
                                        _swix(0x059c00 /* SerialUSB_info */,_INR(0,3),device[n].name, 2, desc, sizeof(desc));
                                printf("%2d %-16s %s\n", n + 1, device[n].name, desc);
                                if (++n == 16) break;
                        }
                }
        }
        printf("\n");

        if (n == 0)
        {
                printf("No Devices found\n");
                exit(0);
        }
        if (n == 1) return device[0].name;

        for (;;)
        {
                printf("Select device (1 - %d) ", n);
                fgets(desc, sizeof(desc), stdin);
                int res = atoi(desc);
                if (res >=1 && res <=n) return device[res-1].name;
        }
        exit(0);
}

static int infile = 0;
static int outfile = 0;

static void tidy_up(void)
{
        if (outfile != 0) file_close(outfile);
        if (infile != 0) file_close(infile);
}

static int inkey(size_t csecs)
{
        int key = _kernel_osbyte(129, csecs & 0xff, (csecs >> 8) & 0xff);
        return ((key >> 8) & 0xff) ? -1 : (key & 0xff);
}

#define IOCTL_NODATAIN  (-1u)
#define IOCTL_R_BAUDRATE        (1)
#define IOCTL_R_CONTROL_LINES   (6)
static _kernel_oserror* ioctl(int file,unsigned int reason, unsigned int datain, unsigned int* dataout)
{
        unsigned int res[2];
        res[0] = reason | (dataout != NULL ? (1u<<30) : 0) | (datain != IOCTL_NODATAIN ? (1u<<31) : 0);
        res[1] = datain;
        _kernel_oserror* e = _swix(OS_Args, _INR(0,2), 9, file, res);
        if (e == NULL && dataout) *dataout = res[1];
        return e;
}

#define LF   0
#define CRLF 1
#define LFCR 2
#define CR   3

int main(int argc, char** argv)
{
        atexit(tidy_up);
        printf("Serialterminal 0.02 (18 Nov 2023)\n\n");
        int echo = 0;
        int tx = 1;
        int rx = 1;
        char* err = NULL;
        char* port = NULL;
        int help = 0;
        char* handshake = "nohandshake";
        char* baud = "baud=115200";
        char* data = "data=8";
        char* stop = "stop=1";
        char* parity = "noparity";
        int newline = 0;
        int hide_escape_code = 0;
        int loopback = 0;
        
        for (int i = 1; i < argc; i++)
        {
                if (argv[i][0] == '-')
                {
                        if (strcmp(argv[i], "-echo") == 0) echo = 1;
                        else if (strcmp(argv[i], "-h") == 0) help = 1;
                        else if (strcmp(argv[i], "-crlf") == 0) newline = CRLF;
                        else if (strcmp(argv[i], "-lfcr") == 0) newline = LFCR;
                        else if (strcmp(argv[i], "-cr") == 0) newline = CR;
                        else if (strcmp(argv[i], "-h") == 0) help = 1;
                        else if (strcmp(argv[i], "-tx") == 0) tx = 1, rx = 0;
                        else if (strcmp(argv[i], "-rx") == 0) tx = 0, rx = 1;
                        else if (strcmp(argv[i], "-loopback") == 0) loopback = 1;
                        else if (strcmp(argv[i], "-dev") == 0)
                        {
                                if (++i == argc)
                                {
                                        err = "Device name missing";
                                        break;
                                }
                                port = argv[i];
                        }
                        else 
                        {
                                err = "Unknown option parameter";
                                break;
                        }
                }
                else
                {
                        if (strcmp(argv[i], "nohandshake") == 0 ||
                            strcmp(argv[i], "rts") == 0 ||
                            strcmp(argv[i], "dtr") == 0) handshake = argv[i];
                        else if (strncmp(argv[i], "baud=", 5) == 0) baud = argv[i];
                        else if (strncmp(argv[i], "data=", 5) == 0) data = argv[i];
                        else if (strncmp(argv[i], "stop=", 5) == 0) stop = argv[i];
                        else if (strcmp(argv[i], "noparity") == 0 ||
                                 strcmp(argv[i], "odd") == 0 ||
                                 strcmp(argv[i], "even") == 0) parity = argv[i];
                        else
                        {
                                err = "Invalid configuration parameter";
                                break;
                        }
                      
                }
        }
        
        if (err || help)
        {
                if (err) printf("%s\n\n",err);
                printf("Syntax:\n");
                printf("\n Serialterminal [configuration options] [-echo] [-dev device_name] [-rx] [-tx] [-h]\n");
                printf("\n Default: nohandshake;baud=115200;data=8;stop=1;noparity\n");
                printf(" Default line ending: line feed - lf (10)\n");
                printf("\n Configuration options:\n\n");
                printf("  nohandshake, rts or dtr.   Set the handshaking\n");
                printf("  baud=<baud rate>.          Set the baud rate eg baud=9600\n");
                printf("  data=<n>                   where <n> is 5,6,7, or 8. Number of bits in a word eg data=8\n");
                printf("  stop=<n>                   where <n> is 1 or 2. Number of stop bits eg stop=1\n");
                printf("  noparity, odd or even.     Sets the parity\n");
                printf("\n Other options:\n\n");
                printf("  -echo                      display the keys pressed.\n");
                printf("  -dev  device_name          specify the device eg -dev SerialUSB1\n");
                printf("  -loopback                  display and reflect bytes received back to sender.\n");
                printf("                             The keyboard is disabled and no processing is done.\n");
                printf("  -rx                        open terminal read only\n");
                printf("  -tx                        open terminal write only\n");
                printf("  -h                         help\n");
                printf("  -crlf                      send carriage return - cr (13) followed by line feed - lf (10) when return is pressed.\n");
                printf("  -lfcr                      send line feed - lf (10) followed by carriage return - cr (13) when return is pressed.\n");
                printf("  -cr                        send carriage return - cr (13) when return is pressed.\n");
                printf("\n Note:\n\n");
                printf("  No spaces are allowed around the = sign.\n");
                printf("  All parameters must be lower case\n");
                printf("\n  -rx and -tx allows you to have separate windows for receiving and sending\n");
                return err ? EXIT_FAILURE : EXIT_SUCCESS;
        }
        
        if (hide_escape_code) printf("Escape codes removed\n");

        if (!rx) echo = 1;
        
        const char* fmt = "%s#noblock;%s;%s;%s;%s;%s:";

        if (port == NULL)
                port = get_device();
        
        int default_uart = -1;
        _swix(OS_Hardware, _INR(8,9) | _OUT(0), 0, 84, &default_uart);
        if (default_uart < 0 || default_uart > 8) default_uart = 0;
        
        char name[12];
        sprintf(name, "Serial%d", default_uart + 1);

        if ((default_uart == 0 && casestrcmp(port, "Serial") == 0) ||
            casestrcmp(port, name) == 0)
        {
                /* 
                 * Default serial port doesn't ignore DCD.
                 * So set here. If not set change buffer size.
                 */
                int state;
                _swix(OS_SerialOp, _INR(0,2) | _OUT(2), 0, 0, -1, &state);
                if ((state & 2) == 0)
                {
                        int flags, start, end;
                        for (int i = 1; i <= 2; i++)
                        {
                                /* Resize default buffers 1 & 2 */
                                _swix(Buffer_GetInfo, _IN(0) | _OUTR(0,2), i, &flags, &start, &end);
                                if (end - start < 2048)
                                {
                                        _swix(Buffer_Deregister, _IN(0), i);
                                        _swix(Buffer_Create, _INR(0,2), flags, 1024*8, i);
                                }
                        }
                        _swix(OS_SerialOp, _INR(0,2), 0, 2, ~2);
                }
        }

        if (tx)
        {
                outfile = file_open(FILE_OPEN_WRITE, fmt, port, handshake, baud, data, stop, parity);
                if (outfile == 0)
                {
                        printf("Failed to open %s for output\n", port);
                        return EXIT_FAILURE;
                }
        }
        
        if (rx)
        {
                infile = file_open(FILE_OPEN_READ, fmt, port, handshake, baud, data, stop, parity);
                if (infile == 0)
                {
                        printf("Failed to open %s for input\n", port);
                        return EXIT_FAILURE;
                }
        }

        
        
        printf(fmt, port, handshake, baud, data, stop, parity);  printf("\n");      
        
        if (rx && tx) printf("Transmit and Receive\n");
        else if (rx) printf("Receive only\n");
        else if (tx) printf("Transmit only\n");
        printf("Key echo %s\n", echo ? "on" : "off");
        printf("Line ending ");
        switch (newline)
        {
                case LF: printf("LF\n"); break;
                case CRLF: printf("CRLF\n"); break;
                case LFCR: printf("LFCR\n"); break;
                case CR: printf("CR\n"); break;
        }
        
        char buffer[1024];
        
        sprintf(buffer, "Motherboard");
        
        _swix(SerialUSB_Info, _INR(0,3), port, SerialUSB_InfoDeviceDescription, buffer, sizeof(buffer));
        printf("%s\n\n", buffer);
        if (loopback) printf("loopback mode (input disabled)\n\n");

        ioctl(infile, IOCTL_R_CONTROL_LINES, 3, NULL); /* Set DTR */
        printf("\n");
        int escape_code_state = 0;
        int linebuffer_on = 0;
        for (;;)
        {
                if (rx)
                {
                        size_t read = 0;
                        if (!file_read(infile, buffer, sizeof(buffer), &read))
                        {
                                return EXIT_FAILURE;
                        }
                                                
                        if (hide_escape_code)
                        {
                            char* out = buffer;
                            char* end = buffer + read;
                            for (char* in = buffer; in < end; in++)
                            {
                                if (escape_code_state == 0)
                                {
                                     if (*in == 27) 
                                        escape_code_state = 1;
                                     else
                                        *out++ = *in;
                                }
                                else if (*in == 'm' || *in == 10 || *in == 13)
                                {
                                     escape_code_state = 0;
                                     if (*in != 'm') *out++ = *in;
                                }
                            }
                            read = out - buffer;
                        }
                        
                        if (read > 0)
                        {
                                if (tx && loopback)
                                {
                                        size_t written = 0;
                                        for (char* buf = buffer; buf < buffer + read; buf += written)
                                        {
                                                if (!file_write(outfile, buf, buffer + read - buf, &written))
                                                        return EXIT_FAILURE;;
                                        }
                                }
                                fwrite(buffer, read, 1, stdout);
                        }
                }
                if (tx)
                {
                        if (loopback) continue;
                        int c = inkey(0);
                        if (c == -1) continue;
                        if (c == 13) c = 10;
                        if (c == 2)
                        {
                            linebuffer_on ^= 1;
                            if (linebuffer_on) linebuffer_end = linebuffer_ptr = linebuffer_buf;
                            printf("\nLinebuffer %s\n\n", (linebuffer_on ? "on" : "off"));
                        }
                        if (linebuffer_on)
                        {
                                if (linebuffer_insert(c)) continue;
                                putchar(c);
                                printf("%s\n\n",linebuffer_buf);

                                size_t written = 0;
                                for (char* buf = linebuffer_buf; buf < linebuffer_end; buf += written )
                                {
                                        if (!file_write(outfile, buf, linebuffer_end - buf, &written)) return EXIT_FAILURE;
                                }
                                
                                if (linebuffer_buf[0] != 0)
                                {
                                        memcpy(linebuffer_history[linebuffer_history_pos], linebuffer_buf, linebuffer_end - linebuffer_buf + 1);
                                        if (++linebuffer_history_pos >= LINEBUFFER_HISTORY_COUNT) linebuffer_history_pos = 0;
                                        if (linebuffer_history_count < LINEBUFFER_HISTORY_COUNT) linebuffer_history_count++;
                                        linebuffer_history_last = linebuffer_history_pos;
                                }
                                
                                linebuffer_end = linebuffer_ptr = linebuffer_buf;
                        }
                        else if (isprint(c))
                        {
                                if (echo) putchar(c);
                                
                                for (size_t written = 0; written != 1;)
                                {
                                        if (!file_write(outfile, (void*) &c, 1, &written)) return EXIT_FAILURE;
                                }
                        }
                        else if (c == 10)
                        {
                                if (echo) putchar(c);
                        }
                        
                        if (c == 10)
                        {
                                if (newline == CR) c = 13;
                                if (newline == CRLF || newline == LFCR)
                                {
                                        if (newline == CRLF) c = 13;
                                        for (size_t written = 0; written != 1;)
                                        {
                                                if (!file_write(outfile, (void*) &c, 1, &written)) return EXIT_FAILURE;
                                        }
                                        c = newline == CRLF ? 10 : 13;
                                }
                                for (size_t written = 0; written != 1;)
                                {
                                        if (!file_write(outfile, (void*) &c, 1, &written)) return EXIT_FAILURE;
                                }
                        }
                }              
        }
        
        return EXIT_SUCCESS;
}
