/*
 * avrdude - A Downloader/Uploader for AVR device programmers
 * Copyright (C) 2003, 2004  Martin J. Thomas  <mthomas@rhrk.uni-kl.de>
 * Copyright (C) 2006  Joerg Wunsch <j@uriah.heep.sax.de>
 *
 * 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, see <http://www.gnu.org/licenses/>.
 */

/* $Id$ */

/*
 * Native Win32 serial interface for avrdude.
 */

#include "ac_cfg.h"

#if defined(WIN32NATIVE)

#ifdef HAVE_LIBWS2_32
/* winsock2.h must be included before windows.h from avrdude.h... */
#  include <winsock2.h>
#endif

#include <windows.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>   /* for isprint */
#include <errno.h>   /* ENOTTY */

#include "avrdude.h"
#include "libavrdude.h"
#include "windows/utf8.h"

long serial_recv_timeout = 5000; /* ms */

#define W32SERBUFSIZE 1024


// Get last error message string in UTF-8
// Always return a valid null-terminated string
// The returned string should be freed by the caller
char* last_error_string(int wsa)
{
	LPWSTR wbuffer = NULL;

	(void)wsa;

	DWORD wbuffer_len = FormatMessageW(
		FORMAT_MESSAGE_ALLOCATE_BUFFER |
		FORMAT_MESSAGE_FROM_SYSTEM |
		FORMAT_MESSAGE_IGNORE_INSERTS,
		NULL,
#ifdef HAVE_LIBWS2_32
		wsa ? WSAGetLastError() : GetLastError(),
#else
		GetLastError(),
#endif
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		(LPWSTR)&wbuffer,
		0,
		NULL);

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

	char *res = wstr_to_utf8(wbuffer, wbuffer_len);

	LocalFree(wbuffer);

	if (res == NULL) {
		// If we get here, conversion to UTF-8 failed
		res = strdup("(could not get error message)");
		if (res == NULL) {
			avrdude_oom("last_error_string(): out of memory\n");
		}
	}

	return res;
}


struct baud_mapping {
  long baud;
  DWORD speed;
};

static unsigned char serial_over_ethernet = 0;

/* HANDLE hComPort=INVALID_HANDLE_VALUE; */

static struct baud_mapping baud_lookup_table [] = {
  { 1200,   CBR_1200 },
  { 2400,   CBR_2400 },
  { 4800,   CBR_4800 },
  { 9600,   CBR_9600 },
  { 19200,  CBR_19200 },
  { 38400,  CBR_38400 },
  { 57600,  CBR_57600 },
  { 115200, CBR_115200 },
  { 0,      0 }                 /* Terminator. */
};

static DWORD serial_baud_lookup(long baud)
{
  struct baud_mapping *map = baud_lookup_table;

  while (map->baud) {
    if (map->baud == baud)
      return map->speed;
    map++;
  }

  /*
   * If a non-standard BAUD rate is used, issue
   * a warning (if we are verbose) and return the raw rate
   */
  avrdude_message(MSG_NOTICE, "%s: serial_baud_lookup(): Using non-standard baud rate: %ld",
              progname, baud);

  return baud;
}


static BOOL serial_w32SetTimeOut(HANDLE hComPort, DWORD timeout) // in ms
{
	COMMTIMEOUTS ctmo;
	ZeroMemory (&ctmo, sizeof(COMMTIMEOUTS));
	// Don't use the two other two values, set them to zero. This way a constant timeout is used
	// for the following reads.
	// ctmo.ReadIntervalTimeout = timeout;
	// ctmo.ReadTotalTimeoutMultiplier = timeout;
	ctmo.ReadTotalTimeoutConstant = timeout;
	ctmo.WriteTotalTimeoutConstant = timeout;

	return SetCommTimeouts(hComPort, &ctmo);
}

static int ser_setspeed(union filedescriptor *fd, long baud)
{
	if (serial_over_ethernet) {
		return -ENOTTY;
	} else {
		DCB dcb;
		HANDLE hComPort = (HANDLE)fd->pfd;

		ZeroMemory (&dcb, sizeof(DCB));
		dcb.DCBlength = sizeof(DCB);
		dcb.BaudRate = serial_baud_lookup (baud);
		dcb.fBinary = 1;
		dcb.fDtrControl = DTR_CONTROL_DISABLE;
		dcb.fRtsControl = RTS_CONTROL_DISABLE;
		dcb.ByteSize = 8;
		dcb.Parity = NOPARITY;
		dcb.StopBits = ONESTOPBIT;

		if (!SetCommState(hComPort, &dcb))
			return -1;

		return 0;
	}
}

#ifdef HAVE_LIBWS2_32
static int
net_open(const char *port, union filedescriptor *fdp)
{
	WSADATA wsaData;

	char *hstr, *pstr, *end;
	unsigned int pnum;
	int fd;
	struct sockaddr_in sockaddr;
	struct hostent *hp;

	if (WSAStartup(MAKEWORD(2, 0), &wsaData) != 0) {
		avrdude_message(MSG_INFO, "%s: net_open(): WSAStartup() failed\n", progname);
		return -1;
	}

	if ((hstr = strdup(port)) == NULL) {
		avrdude_message(MSG_INFO, "%s: net_open(): Out of memory!\n", progname);
		return -1;
	}

	if (((pstr = strchr(hstr, ':')) == NULL) || (pstr == hstr)) {
		avrdude_message(MSG_INFO, "%s: net_open(): Mangled host:port string \"%s\"\n", progname, hstr);
		free(hstr);
		return -1;
	}

	/*
	 * Terminate the host section of the description.
	 */
	*pstr++ = '\0';

	pnum = strtoul(pstr, &end, 10);

	if ((*pstr == '\0') || (*end != '\0') || (pnum == 0) || (pnum > 65535)) {
		avrdude_message(MSG_INFO, "%s: net_open(): Bad port number \"%s\"\n", progname, pstr);
		free(hstr);
		return -1;
	}

	if ((hp = gethostbyname(hstr)) == NULL) {
		avrdude_message(MSG_INFO, "%s: net_open(): unknown host \"%s\"\n", progname, hstr);
		free(hstr);
		return -1;
	}

	free(hstr);

	if ((fd = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
		const char *error = last_error_string(1);
		avrdude_message(MSG_INFO, "%s: net_open(): Cannot open socket: %s\n", progname, error);
		free(error);

		return -1;
	}

	memset(&sockaddr, 0, sizeof(struct sockaddr_in));
	sockaddr.sin_family = AF_INET;
	sockaddr.sin_port = htons(pnum);
	memcpy(&(sockaddr.sin_addr.s_addr), hp->h_addr, sizeof(struct in_addr));

	if (connect(fd, (struct sockaddr *)&sockaddr, sizeof(sockaddr))) {
		const char *error = last_error_string(1);
		avrdude_message(MSG_INFO, "%s: net_open(): Connect failed: %s\n", progname);
		free(error);
		return -1;
	}

	fdp->ifd = fd;

	serial_over_ethernet = 1;
	return 0;
}
#endif


static int ser_open(char * port, union pinfo pinfo, union filedescriptor *fdp)
{
	HANDLE hComPort=INVALID_HANDLE_VALUE;
	char *newname = 0;

	/*
	 * If the port is of the form "net:<host>:<port>", then
	 * handle it as a TCP connection to a terminal server.
	 */
	if (strncmp(port, "net:", strlen("net:")) == 0) {
#ifdef HAVE_LIBWS2_32
		return net_open(port + strlen("net:"), fdp);
#else
		avrdude_message(MSG_INFO, "%s: ser_open(): "
				"not configured for TCP connections\n",
                                progname);
		return -1;
#endif
	}

	if (strncasecmp(port, "com", strlen("com")) == 0) {

	    // prepend "\\\\.\\" to name, required for port # >= 10
	    newname = malloc(strlen("\\\\.\\") + strlen(port) + 1);

	    if (newname == 0) {
		// avrdude_message(MSG_INFO, "%s: ser_open(): out of memory\n",
		//                         progname);
		// exit(1);
			avrdude_oom("ser_open(): out of memory\n");
		}
	    strcpy(newname, "\\\\.\\");
	    strcat(newname, port);

	    port = newname;
	}

	hComPort = CreateFileA(port, GENERIC_READ | GENERIC_WRITE, 0, NULL,
		OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

	if (hComPort == INVALID_HANDLE_VALUE) {
		const char *error = last_error_string(0);
		avrdude_message(MSG_INFO, "%s: ser_open(): can't open device \"%s\": %s\n", progname, port, error);
		free((char *)error);
		return -1;
	}

	if (!SetupComm(hComPort, W32SERBUFSIZE, W32SERBUFSIZE))
	{
		CloseHandle(hComPort);
		avrdude_message(MSG_INFO, "%s: ser_open(): can't set buffers for \"%s\"\n",
				progname, port);
		return -1;
	}

        fdp->pfd = (void *)hComPort;
	if (ser_setspeed(fdp, pinfo.baud) != 0)
	{
		CloseHandle(hComPort);
		avrdude_message(MSG_INFO, "%s: ser_open(): can't set com-state for \"%s\"\n",
				progname, port);
		return -1;
	}

	if (!serial_w32SetTimeOut(hComPort,0))
	{
		CloseHandle(hComPort);
		avrdude_message(MSG_INFO, "%s: ser_open(): can't set initial timeout for \"%s\"\n",
				progname, port);
		return -1;
	}

	if (newname != 0) {
	    free(newname);
	}
	return 0;
}


static void ser_close(union filedescriptor *fd)
{
	if (serial_over_ethernet) {
#ifdef HAVE_LIBWS2_32
		closesocket(fd->ifd);
		WSACleanup();
#endif
	} else {
		HANDLE hComPort=(HANDLE)fd->pfd;
		if (hComPort != INVALID_HANDLE_VALUE)
			CloseHandle (hComPort);

		hComPort = INVALID_HANDLE_VALUE;
	}
}

static int ser_set_dtr_rts(union filedescriptor *fd, int is_on)
{
	if (serial_over_ethernet) {
		return 0;
	} else {
		HANDLE hComPort=(HANDLE)fd->pfd;

		if (is_on) {
			EscapeCommFunction(hComPort, SETDTR);
			EscapeCommFunction(hComPort, SETRTS);
		} else {
			EscapeCommFunction(hComPort, CLRDTR);
			EscapeCommFunction(hComPort, CLRRTS);
		}
		return 0;
	}
}

#ifdef HAVE_LIBWS2_32
static int net_send(union filedescriptor *fd, const unsigned char * buf, size_t buflen)
{
	int rc;
	const unsigned char *p = buf;
	size_t len = buflen;

	if (fd->ifd < 0) {
		avrdude_message(MSG_NOTICE, "%s: net_send(): connection not open\n", progname);
		return -1;
	}

	if (!len) {
		return 0;
	}

	if (verbose > 3) {
		avrdude_message(MSG_TRACE, "%s: Send: ", progname);

		while (buflen) {
			unsigned char c = *buf;
			if (isprint(c)) {
				avrdude_message(MSG_TRACE, "%c ", c);
			} else {
				avrdude_message(MSG_TRACE, ". ");
			}
			avrdude_message(MSG_TRACE, "[%02x] ", c);

			buf++;
			buflen--;
		}

		avrdude_message(MSG_TRACE, "\n");
	}

	while (len) {
		rc = send(fd->ifd, p, (len > 1024) ? 1024 : len, 0);
		if (rc < 0) {
			const char *error = last_error_string(1);
			avrdude_message(MSG_INFO, "%s: net_send(): send error: %s\n", progname, error);
			free(error);
			return -1;
		}
		p += rc;
		len -= rc;
	}

	return 0;
}
#endif


static int ser_send(union filedescriptor *fd, const unsigned char * buf, size_t buflen)
{
#ifdef HAVE_LIBWS2_32
	if (serial_over_ethernet) {
		return net_send(fd, buf, buflen);
	}
#endif

	size_t len = buflen;
	unsigned char c='\0';
	DWORD written;
        const unsigned char * b = buf;

	RETURN_IF_CANCEL();

	HANDLE hComPort=(HANDLE)fd->pfd;

	if (hComPort == INVALID_HANDLE_VALUE) {
		avrdude_message(MSG_INFO, "%s: ser_send(): port not open\n", progname);
		return -1;
	}

	if (!len)
  return 0;

	if (verbose > 3)
	{
		avrdude_message(MSG_TRACE, "%s: Send: ", progname);

		while (len) {
			c = *b;
			if (isprint(c)) {
				avrdude_message(MSG_TRACE, "%c ", c);
			}
			else {
				avrdude_message(MSG_TRACE, ". ");
			}
			avrdude_message(MSG_TRACE, "[%02x] ", c);
			b++;
			len--;
		}
      avrdude_message(MSG_INFO, "\n");
	}

	serial_w32SetTimeOut(hComPort,500);

	if (!WriteFile(hComPort, buf, (DWORD)buflen, &written, NULL)) {
		const char *error = last_error_string(0);
		avrdude_message(MSG_INFO, "%s: ser_send(): write error: %s\n", progname, error);
		free((char *)error);
		return -1;
	}

	if (written != buflen) {
		avrdude_message(MSG_INFO, "%s: ser_send(): size/send mismatch\n", progname);
		return -1;
	}

	return 0;
}


#ifdef HAVE_LIBWS2_32
static int net_recv(union filedescriptor *fd, unsigned char * buf, size_t buflen)
{
	struct timeval timeout, to2;
	fd_set rfds;
	int nfds;
	int rc;
	unsigned char *p = buf;
	size_t len = 0;

	if (fd->ifd < 0) {
		avrdude_message(MSG_INFO, "%s: net_recv(): connection not open\n", progname);
		return -1;
	}

	timeout.tv_sec  = serial_recv_timeout / 1000L;
	timeout.tv_usec = (serial_recv_timeout % 1000L) * 1000;
	to2 = timeout;

	while (len < buflen) {
reselect:
		FD_ZERO(&rfds);
		FD_SET(fd->ifd, &rfds);

		nfds = select(fd->ifd + 1, &rfds, NULL, NULL, &to2);
		if (nfds == 0) {
			if (verbose > 1) {
				avrdude_message(MSG_NOTICE, "%s: ser_recv(): programmer is not responding\n", progname);
			}
			return -1;
		} else if (nfds == -1) {
			if (WSAGetLastError() == WSAEINTR || WSAGetLastError() == WSAEINPROGRESS) {
				avrdude_message(MSG_NOTICE, "%s: ser_recv(): programmer is not responding, reselecting\n", progname);
				goto reselect;
			} else {
				const char *error = last_error_string(1);
				avrdude_message(MSG_INFO, "%s: ser_recv(): select(): %s\n", progname, error);
				free(error);
				return -1;
			}
		}

		rc = recv(fd->ifd, p, (buflen - len > 1024) ? 1024 : buflen - len, 0);
		if (rc < 0) {
			const char *error = last_error_string(1);
			avrdude_message(MSG_INFO, "%s: ser_recv(): read error: %s\n", progname, error);
			free(error);
			return -1;
		}
		p += rc;
		len += rc;
	}

	p = buf;

	if (verbose > 3) {
		avrdude_message(MSG_TRACE, "%s: Recv: ", progname);

		while (len) {
			unsigned char c = *p;
			if (isprint(c)) {
				avrdude_message(MSG_TRACE, "%c ", c);
			} else {
				avrdude_message(MSG_TRACE, ". ");
			}
			avrdude_message(MSG_TRACE, "[%02x] ", c);

			p++;
			len--;
		}
		avrdude_message(MSG_TRACE, "\n");
	}

	return 0;
}
#endif

static int ser_recv(union filedescriptor *fd, unsigned char * buf, size_t buflen)
{
#ifdef HAVE_LIBWS2_32
	if (serial_over_ethernet) {
		return net_recv(fd, buf, buflen);
	}
#endif

	unsigned char c;
	unsigned char * p = buf;
	DWORD read;

	RETURN_IF_CANCEL();

	HANDLE hComPort=(HANDLE)fd->pfd;

	if (hComPort == INVALID_HANDLE_VALUE) {
		avrdude_message(MSG_INFO, "%s: ser_read(): port not open\n", progname);
		return -1;
	}

	serial_w32SetTimeOut(hComPort, serial_recv_timeout);

	if (!ReadFile(hComPort, buf, (DWORD)buflen, &read, NULL)) {
		const char *error = last_error_string(0);
		avrdude_message(MSG_INFO, "%s: ser_recv(): read error: %s\n", progname, error);
		free((char *)error);
		return -1;
	}

	/* time out detected */
	if (read == 0) {
		avrdude_message(MSG_NOTICE2, "%s: ser_recv(): programmer is not responding\n", progname);
		return -1;
	}

	p = buf;

	if (verbose > 3)
	{
		avrdude_message(MSG_TRACE, "%s: Recv: ", progname);

		while (read) {
			c = *p;
			if (isprint(c)) {
				avrdude_message(MSG_TRACE, "%c ", c);
			}
			else {
				avrdude_message(MSG_TRACE, ". ");
			}
			avrdude_message(MSG_TRACE, "[%02x] ", c);

			p++;
			read--;
		}
		avrdude_message(MSG_INFO, "\n");
	}
  return 0;
}


static int ser_drain(union filedescriptor *fd, int display)
{
	// int rc;
	unsigned char buf[10];
	BOOL readres;
	DWORD read;

	HANDLE hComPort=(HANDLE)fd->pfd;

  	if (hComPort == INVALID_HANDLE_VALUE) {
		avrdude_message(MSG_INFO, "%s: ser_drain(): port not open\n",
              progname); 
		return -1;
	}

	serial_w32SetTimeOut(hComPort,250);
  
	if (display) {
		avrdude_message(MSG_INFO, "drain>");
	}

	while (1) {
		RETURN_IF_CANCEL();

		readres=ReadFile(hComPort, buf, 1, &read, NULL);
		if (!readres) {
			const char *error = last_error_string(0);
			avrdude_message(MSG_INFO, "%s: ser_drain(): read error: %s\n", progname, error);
			free((char *)error);
			return -1;
		}

		if (read) { // data avail
			if (display) avrdude_message(MSG_INFO, "%02x ", buf[0]);
		}
		else { // no more data
			if (display) avrdude_message(MSG_INFO, "<drain\n");
			break;
		}
	} // while
  return 0;
}

struct serial_device serial_serdev =
{
  .open = ser_open,
  .setspeed = ser_setspeed,
  .close = ser_close,
  .send = ser_send,
  .recv = ser_recv,
  .drain = ser_drain,
  .set_dtr_rts = ser_set_dtr_rts,
  .flags = SERDEV_FL_CANSETSPEED,
};

struct serial_device *serdev = &serial_serdev;

#endif /* WIN32NATIVE */
