/*
    Serial interface - Windows layer.
    
    Copyright (C) 2006  Robert Lipe, robertlipe@usa.net

    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 2 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, write to the Free Software
    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111 USA

 */

#include "defs.h"
#include "gbser.h"
#include "gbser_private.h"

#include <windows.h>
#include <setupapi.h>

#include <assert.h>
#include <stdarg.h>

typedef struct {
	HANDLE          comport;
	DWORD           timeout;
	unsigned long   magic;

	unsigned char   inbuf[BUFSIZE];
	unsigned        inbuf_used;
} gbser_handle;

#define DEV_PREFIX "\\\\.\\\\"

/* Wrapper to safely cast a void * into a gbser_handle */
static gbser_handle *gbser__get_handle(void *p) {
    gbser_handle *h = (gbser_handle *) p;
	assert(h->magic == MYMAGIC);
    return h;
}

static DWORD mkspeed(unsigned br) {
	switch (br) {
	case   1200: return CBR_1200;
	case   2400: return CBR_2400;
	case   4800: return CBR_4800;
	case   9600: return CBR_9600;
	case  19200: return CBR_19200;
	case  38400: return CBR_38400;
	case  57600: return CBR_57600;
	case 115200: return CBR_115200;
    default:
        fatal("Unsupported serial speed: %d\n", br);
        return 0;   /* keep compiler happy */
	}
}

typedef LARGE_INTEGER hp_time;

static void get_time(hp_time *tv) {
    QueryPerformanceCounter(tv);
}

static double elapsed(hp_time *tv) {
    hp_time now;
    LARGE_INTEGER tps;

    QueryPerformanceFrequency(&tps);
    QueryPerformanceCounter(&now);

    return ((double) (now.QuadPart - tv->QuadPart) / 
            (double) tps.QuadPart) * 1000;
}

static int set_rx_timeout(gbser_handle *h, DWORD timeout) {
    if (timeout != h->timeout) {
    	COMMTIMEOUTS to;

        if (!GetCommTimeouts(h->comport, &to)) {
            return gbser_ERROR;
        }
    
    	to.ReadIntervalTimeout         = timeout;
    	to.ReadTotalTimeoutMultiplier  = 0;
    	to.ReadTotalTimeoutConstant    = timeout;
    	to.WriteTotalTimeoutMultiplier = 0;
    	to.WriteTotalTimeoutConstant   = 0;
	
    	if (!SetCommTimeouts(h->comport, &to)) {
    	    return gbser_ERROR;
    	} else {
    	    h->timeout = timeout;
    	    return gbser_OK;
    	}
    } else {
	    return gbser_OK;
    }
}

/* This isn't part of the above abstraction; it's just a helper for 
 * the other serial modules in the tree.
 *
 * Windows does a weird thing with serial ports.  
 * COM ports 1 - 9 are "COM1:" through "COM9:"
 * The one after that is \\.\\com10 - this function tries to plaster over
 * that.
 * 
 * Worse still, Win98 and ME fail the open if you rename com1 to be \\.\\com1:
 *
 * It returns a pointer to a staticly allocated buffer and is therefore not
 * thread safe.   The buffer pointed to remains valid only until the next
 * call to this function.
 */

const char *
fix_win_serial_name_r(const char *comname, char *obuf, size_t len) 
{
	if (!gbser_is_serial(comname) ||
		((strlen(comname) == 5) && (comname[4] == ':')) || 
		((strlen(comname) == 4) && (case_ignore_strncmp(comname, "com", 3) == 0))
		) {
		strncpy(obuf, comname, len);
	} else {
	    size_t l;
		snprintf(obuf, len, DEV_PREFIX "%s", comname);
		l = strlen(obuf);
		if (obuf[l - 1] == ':') {
			obuf[l - 1] = '\0';
		}
	}

	return obuf;
}

static char gb_com_buffer[100];

const char *fix_win_serial_name(const char *comname) 
{
	return fix_win_serial_name_r(comname, gb_com_buffer, sizeof(gb_com_buffer));
}

/* Open a serial port. |port_name| is the (platform specific) name
 * of the serial device to open. Under WIN32 familiar DOS port names
 * ('com1:') are translated into the equivalent name required by
 * WIN32
 */
void *gbser_init(const char *port_name) 
{
	HANDLE comport;
	gbser_handle* h = xcalloc(1, sizeof(*h));
	const char *xname = fix_win_serial_name(port_name);

	gbser__db(2, "Translated port name: \"%s\"\n", xname);

	h->magic = MYMAGIC;

	comport = CreateFile(xname, GENERIC_READ | GENERIC_WRITE, 
            			 0, NULL, OPEN_EXISTING, 0, NULL);

	if (comport == INVALID_HANDLE_VALUE) {
	    goto failed;
	}
	
	h->comport = comport;
	h->timeout = 1;
	if (gbser_set_port(h, 4800, 8, 0, 1) || set_rx_timeout(h, 0)) {
	    goto failed;
	}
	
	return h;
	
failed:
	if (comport) { 
		CloseHandle(h->comport); 
	}
	xfree(h);
    
	return NULL;
}

/* Close a serial port
 */
void gbser_deinit(void *handle) {
	gbser_handle *h = gbser__get_handle(handle);

	CloseHandle(h->comport);

	xfree(h);
}

int gbser_set_port(void *handle, unsigned speed, unsigned bits, unsigned parity, unsigned stop) {
	gbser_handle *h = gbser__get_handle(handle);
	DCB tio;

    if (bits < 5 || bits > 8) {
        fatal("Unsupported bits setting: %d\n", bits);
    }

    if (parity > 2) {
        fatal("Unsupported parity setting: %d\n", parity);
    }
    
    if (stop < 1 || stop > 2) {
        fatal("Unsupported stop setting: %d\n", stop);
    }
	
	tio.DCBlength = sizeof(DCB);
	GetCommState(h->comport, &tio);
	
	tio.BaudRate            = mkspeed(speed);
	tio.fBinary             = TRUE;
	tio.fParity             = TRUE;
	tio.fOutxCtsFlow        = FALSE;
	tio.fOutxDsrFlow        = FALSE;
	tio.fDtrControl         = DTR_CONTROL_ENABLE;
	tio.fDsrSensitivity     = FALSE;
	tio.fTXContinueOnXoff   = TRUE;
	tio.fOutX               = FALSE;
	tio.fInX                = FALSE;
	tio.fErrorChar          = FALSE;
	tio.fNull               = FALSE;
	tio.fRtsControl         = RTS_CONTROL_ENABLE;
	tio.fAbortOnError       = FALSE;
	tio.ByteSize            = bits;
	tio.Parity              = parity == 0 ? NOPARITY : 
								  (parity == 1 ? ODDPARITY : EVENPARITY);
	tio.StopBits            = stop   == 1 ? ONESTOPBIT : TWOSTOPBITS;

	if (!SetCommState(h->comport, &tio)) {
		return gbser_ERROR;
	}
	return gbser_OK;
}

unsigned gbser__read_buffer(void *handle, void **buf, unsigned *len) {
	gbser_handle *h = gbser__get_handle(handle);
    unsigned count = *len;
    unsigned char *cp = *buf;
    if (count > h->inbuf_used) {
        count = h->inbuf_used;
    }
    
    memcpy(cp, h->inbuf, count);
    memmove(h->inbuf, h->inbuf + count, 
                      h->inbuf_used - count);
    h->inbuf_used -= count;
    *len -= count;
    cp   += count;
    *buf = (void *) cp;
    return count;
}

/* Return when the input buffer contains at least |want| bytes or |*ms|
 * milliseconds have elapsed. |ms| may be NULL or |*ms| may be zero to
 * poll the port for available bytes and return immediately. |*ms| will
 * be updated to indicate the remaining time on exit.
 * Returns the number of bytes available (>=0) or an error code (<0).
 */
int gbser__fill_buffer(void *handle, unsigned want, unsigned *ms) {
    int rc;
	gbser_handle *h = gbser__get_handle(handle);
    
    if (want > BUFSIZE) {
        want = BUFSIZE;
    }

    /* Already got enough bytes? */
    if (h->inbuf_used >= want) {
        return h->inbuf_used;
    }
    
    if (NULL == ms || 0 == *ms) {
    	DWORD err, nread;
    	COMSTAT stat;
    	ClearCommError(h->comport, &err, &stat);
    	if (stat.cbInQue > 0) {
    	    DWORD count = want - h->inbuf_used;
    	    if (count > stat.cbInQue) {
    	        count = stat.cbInQue;
    	    }
    	    if (rc = set_rx_timeout(h, 1), rc) {
    	        return rc;
    	    }
    	    if (!ReadFile(h->comport, h->inbuf + h->inbuf_used, 
    	                               count, &nread, NULL)) {
    	        err = GetLastError();
    	        if (err != ERROR_COUNTER_TIMEOUT && err != ERROR_TIMEOUT) {
    	            return gbser_ERROR;
    	        }
    	    }
    	    h->inbuf_used += nread;
    	}
    } else {
        hp_time tv;
        double time_left;
		DWORD err, nread;
        get_time(&tv);
        if (rc = set_rx_timeout(h, *ms), rc) {
            return rc;
        }
	    if (!ReadFile(h->comport, h->inbuf + h->inbuf_used, 
	                               want - h->inbuf_used, 
	                               &nread, NULL)) {
	        err = GetLastError();
	        if (err != ERROR_COUNTER_TIMEOUT && err != ERROR_TIMEOUT) {
	            return gbser_ERROR;
	        }
	    }
	    h->inbuf_used += nread;
        time_left = *ms - elapsed(&tv);
        *ms = time_left < 0 ? 0 : (unsigned) time_left;
    }

    return h->inbuf_used;
}

/* Discard any pending input on the serial port.
 */                    
int gbser_flush(void *handle) {
	gbser_handle *h = gbser__get_handle(handle);
	h->inbuf_used = 0;
    if (!PurgeComm(h->comport, PURGE_RXCLEAR)) {
        return gbser_ERROR;
    }
    return gbser_OK;
}

/* Write |len| bytes from |buf| to the serial port.
 */
int gbser_write(void *handle, const void *buf, unsigned len) {
	gbser_handle *h = gbser__get_handle(handle);
    DWORD nwritten;
    const char *bp = buf;
    /* Not sure we need to spin here - but this'll work even if we don't */
    while (len > 0) {
        if (!WriteFile(h->comport, bp, len, &nwritten, NULL)) {
            return gbser_ERROR;
        }
        len -= nwritten;
        bp  += nwritten;
    }
    return gbser_OK;
}

/* Return true if a port name seems to refer to a serial port.
 * On Windows this tests the filename (against the regex
 * /^(\\\\\.\\\\)?com\d+:?$/i). On Posix it returns the value of
 * isatty()
 */

int gbser_is_serial(const char *port_name) {
	const char *pfx = DEV_PREFIX;
	size_t pfx_l = strlen(pfx);
	const char *com = "COM";
	size_t com_l = strlen(com);
	unsigned digits;

	if (NULL == port_name) {
		return 0;
	}

	/* Skip any prefix */
	if (memcmp(port_name, pfx, pfx_l) == 0) {
		port_name += pfx_l;
	}

	if (case_ignore_strncmp(port_name, com, com_l) != 0) {
		return 0;
	}

	port_name += com_l;
	for (digits = 0; isdigit(*port_name); port_name++, digits++) {
		/* do nothing */
	}

	if (digits == 0) {
		return 0;
	}

	if (*port_name == ':') {
		port_name++;
	}

	if (*port_name != '\0') {
		return 0;
	}

	/* Success! */
	return 1;
}

/* Read from the serial port until the specified |eol| character is
 * found. Any character matching |discard| will be discarded. To
 * read lines terminated by 0x0A0x0D discarding linefeeds use
 * gbser_read_line(h, buf, len, 1000, 0x0D, 0x0A);
 */
int gbser_read_line(void *handle, void *buf, 
                    unsigned len, unsigned ms,
                    int eol, int discard) {
    char *bp = buf;
    unsigned pos = 0;
    hp_time tv;
    get_time(&tv);
    bp[pos] = '\0';
    for (;;) {
        signed time_left = ms - elapsed(&tv);
        int c;
        if (time_left <= 0) {
            return gbser_TIMEOUT;
        }
        c = gbser_readc_wait(handle, time_left);
        if (c == gbser_ERROR) {
            return c;
        } else if (c == eol) {
            return gbser_OK;
        }
        if (c != gbser_NOTHING && c != discard && pos < len - 1) {
            bp[pos++] = c;
            bp[pos]   = '\0';
        }
    }
}
