/*
   +----------------------------------------------------------------------+
   | PHP Version 7                                                        |
   +----------------------------------------------------------------------+
   | Copyright (c) 2009 Melanie Rhianna Lewis                             |
   +----------------------------------------------------------------------+
   | This source file is subject to version 3.0 of the PHP license,       |
   | that is bundled with this package in the file LICENSE, and is        |
   | available through the world-wide-web at the following url:           |
   | http://www.php.net/license/3_01.txt.                                 |
   | If you did not receive a copy of the PHP license and are unable to   |
   | obtain it through the world-wide-web, please send a note to          |
   | license@php.net so we can mail you a copy immediately.               |
   +----------------------------------------------------------------------+
   | Author: Melanie Rhianna Lewis <cyberspice@php.net>                   |
   +----------------------------------------------------------------------+
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "php_dio_common.h"

#ifndef ZEND_WIN32
#error ZEND_WIN32 not defined!
#endif

/* {{{ dio_last_error_php_error
 * Generates a PHP error message based upon the last Windows error.
 */
static void dio_last_error_php_error(int level, char * message) {
	LPVOID msgbuf;
	DWORD  msgbuflen;
	char * errmsg;
	DWORD  err;

#ifdef UNICODE
	DWORD  errmsglen;
#endif

	err = GetLastError();
	msgbuflen = FormatMessage(
		FORMAT_MESSAGE_ALLOCATE_BUFFER|
		FORMAT_MESSAGE_FROM_SYSTEM|
		FORMAT_MESSAGE_IGNORE_INSERTS,
		NULL,
		err,
		MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
		(LPTSTR)&msgbuf,
		0,
		NULL);

#ifdef UNICODE

	/* Get the length of the converted message */
	errmsglen = WideCharToMultibyte(
		CP_ACP,
		0
		(LPCWSTR)msgbuf,
		-1,
		(LPSTR)errmsg,
		0,
		NULL,
		NULL);

	/* Allocate a buffer */
	errmsg = emalloc(errmsglen);
	if (!errmsg) {
		php_error_docref(NULL, E_ERROR, "Out of memory in dio_last_error_php_error()!");
		LocalFree(msgbuf);
		return;
	}

	/* Convert the message */
	errmsglen = WideCharToMultibyte(
		CP_ACP,
		0
		(LPCWSTR)msgbuf,
		-1,
		(LPSTR)errmsg,
		errmsglen,
		NULL,
		NULL);

#else
	errmsg = (char *)msgbuf;
#endif

	php_error_docref(NULL, E_WARNING, "%s[ERROR %d] %s", message, err, errmsg);

	LocalFree(msgbuf);
#ifdef UNICODE
	efree(errmsg);
#endif
}

/* {{{ dio_data_rate_to_define
 * Converts a numeric data rate to a termios define
 */
static int dio_data_rate_to_define(long rate, DWORD *def) {
	switch (rate) {
		case 75:
		case 110:
		case 134:
		case 150:
		case 300:
		case 600:
		case 1200:
		case 1800:
		case 2400:
		case 4800:
		case 7200:
		case 9600:
		case 14400:
		case 19200:
		case 38400:
		case 57600:
		case 115200:
		case 56000:
		case 128000:
		case 256000:
			break;
		default:
			return 0;
	}

	*def = (DWORD)rate;
	return 1;
}
/* }}} */


/* {{{ dio_data_bits_to_define
 * Converts a number of data bits to a termios define
 */
static int dio_data_bits_to_define(int data_bits, DWORD *def) {
	switch (data_bits) {
		case 8:
		case 7:
		case 6:
		case 5:
		case 4:
			break;
		default:
			return 0;
	}

	*def = (DWORD)data_bits;
	return 1;
}
/* }}} */

/* {{{ dio_stop_bits_to_define
 * Converts a number of stop bits to a termios define
 */
static int dio_stop_bits_to_define(int stop_bits, DWORD *def) {
	DWORD val;

	switch (stop_bits) {
		case 1:
			val = ONESTOPBIT;
			break;
		case 2:
			val = TWOSTOPBITS;
			break;
		case 3:
			val = ONE5STOPBITS;
			break;
		default:
			return 0;
	}

	*def = val;
	return 1;
}
/* }}} */

/* {{{ dio_parity_to_define
 * Converts a parity type to a termios define
 */
static int dio_parity_to_define(int parity, DWORD *def) {
	DWORD val;

	switch (parity) {
		case 0:
			val = NOPARITY;
			break;
		case 1:
			val = ODDPARITY;
			break;
		case 2:
			val = EVENPARITY;
			break;
		default:
			return 0;
	}

	*def = val;
	return 1;
}
/* }}} */

/* {{{ dio_create_stream_data
 * Creates an initialised stream data structure.  Free with efree().
 */
php_dio_stream_data * dio_create_stream_data(void) {
	php_dio_win32_stream_data * data = emalloc(sizeof(php_dio_win32_stream_data));
	memset(data, 0, sizeof(php_dio_win32_stream_data));
	dio_init_stream_data(&(data->common));
	data->handle = INVALID_HANDLE_VALUE;
	data->desired_access = 0;
	data->creation_disposition = 0;
	data->olddcb.DCBlength = sizeof(DCB);

	return (php_dio_stream_data *)data;
}
/* }}} */

/* {{{ dio_common_write
 * Writes count chars from the buffer to the stream described by the stream data.
 */
size_t dio_common_write(php_dio_stream_data *data, const char *buf, size_t count) {
	php_dio_win32_stream_data *wdata = (php_dio_win32_stream_data*)data;
	DWORD total = 0;

	if (WriteFile(wdata->handle, buf, (DWORD)count, &total, NULL)) {
		return (size_t)total;
	}

	return 0;
}
/* }}} */

/* {{{ dio_buffer_read
 * Reads any available chars from the canonical buffer.
 */
static size_t dio_buffer_read(php_dio_win32_stream_data *wdata, const char *buf, size_t count) {
	php_dio_win32_canon_data *canon_data = wdata->canon_data;
	size_t total = 0;

	/* Read always follows write.  I.e. if read ptr > write ptr buffer has
	   wrapped and so we need to copy two blocks of data. */
	if (canon_data->read_pos > canon_data->write_pos) {

		/* Check we actually need to copy both blocks */
		if ((canon_data->size - canon_data->read_pos) > count) {

			/* No we don't.  Just copy as much as we were asked for. */
			memcpy((char*)buf, 
				   &(canon_data->buf[canon_data->read_pos]), 
				   count);
			/* Update the read pointer. */
			canon_data->read_pos += count;

			/* Return the amount read. */
			return count;
		} else {

			/* We need to copy both blocks so copy data up to the end of 
			   the buffer. */
			total = canon_data->size - canon_data->read_pos;
			memcpy((char*)buf, 
				   &(canon_data->buf[canon_data->read_pos]), 
				   total);
			canon_data->read_pos = 0;
			count -= total;

			/* Now copy the data from the start of the buffer either up
			   count or the number of bytes in the buffer. */

			if (canon_data->write_pos > count) {
				memcpy((char*)buf, canon_data->buf, count);
				canon_data->read_pos = count;
				total += count;

				return total;
			} else {
				memcpy((char*)buf, canon_data->buf, canon_data->write_pos);
				canon_data->read_pos = canon_data->write_pos;
				total += canon_data->write_pos;

				return total;
			}
		}

	/* Else if write follows read.  This is a simpler case.  We just copy 
	   either all the data buffered or count, which ever is smaller. */
	} else if (canon_data->write_pos > canon_data->read_pos) {
		if ((canon_data->write_pos - canon_data->read_pos) > count) {
			memcpy((char*)buf, 
				   &(canon_data->buf[canon_data->read_pos]), 
				   count);
			canon_data->read_pos += count;

			return count;
		} else {
			total = canon_data->write_pos - canon_data->read_pos;
			memcpy((char*)buf, 
				   &(canon_data->buf[canon_data->read_pos]), 
				   total);
			canon_data->read_pos += total;

			return total;
		}
	}

	/* Else we need to read more data from the data port. */
	return 0;
}

/* {{{ dio_com_read
 * Read chars from the data port.
 */
static size_t dio_com_read(php_dio_stream_data *data, const char *buf, size_t count) {
	php_dio_win32_stream_data *wdata = (php_dio_win32_stream_data*)data;
	DWORD err, total = 0;

	if (ReadFile(wdata->handle, (void*)buf, (DWORD)count, &total, NULL)) {

		if (total) {
			return (size_t)total;
		}

		data->end_of_file = 1;
	}

	if (!data->end_of_file) {
		err = GetLastError();

		if (ERROR_HANDLE_EOF == err) {
			data->end_of_file = 1;
		}
	}

	return 0;
}

/* {{{ dio_canonical_read
 * Reads chars from the input stream until the internal buffer is full or a new
 * line is reached.
 */
static size_t dio_canonical_read(php_dio_win32_stream_data *wdata, const char *buf, size_t count) {
	php_dio_win32_canon_data *canon_data = wdata->canon_data;
	size_t total = 0;
	char ch;

	/* See if there's any buffered data and copy it. */
	total = dio_buffer_read(wdata, buf, count);
	if (total) {
		return total;
	}

	/* Need to read more data from the data port.  Buffer should be empty(er)
	   by now. */
	do {
		/* Is the buffer full? */
		if (((canon_data->write_pos + 1) % canon_data->size) == 
			canon_data->read_pos) {
			break;
		}

		/* Read a byte from the input checking for EOF. */
		if (!dio_com_read((php_dio_stream_data*)wdata, &ch, 1)) {
			break;
		}

		/* Translate CR to newlines (same as ICRNL in POSIX) */
		ch = (ch != '\r') ? ch : '\n';

		/* We read a character!  So buffer it. */
		canon_data->buf[canon_data->write_pos++] = ch;
		if (canon_data->write_pos >= canon_data->size) {
			canon_data->write_pos = 0;
		}

		/* End of line/input (^D)? */
	} while ((ch != '\n') && (ch != 0x04));

	return dio_buffer_read(wdata, buf, count);
}
/* }}} */

/* {{{ dio_common_read
 * Reads count chars to the buffer to the stream described by the stream data.
 */
size_t dio_common_read(php_dio_stream_data *data, const char *buf, size_t count) {

	/* You ask for no bytes you'll get none :-) */
	if (!count) {
		return 0;
	}

	if (data->canonical) {
		return dio_canonical_read((php_dio_win32_stream_data*)data, buf, count);
	} else {
		return dio_com_read(data, buf, count);
	}
}
/* }}} */

/* {{{ php_dio_stream_data
 * Closes the php_stream.
 */
int dio_common_close(php_dio_stream_data *data) {
	php_dio_win32_stream_data *wdata = (php_dio_win32_stream_data*)data;

	if (data->canonical) {
		efree(wdata->canon_data);
	}

	if (!CloseHandle(wdata->handle)) {
		return 0;
	}

	return 1;
}
/* }}} */

/* {{{ dio_common_set_option
 * Sets/gets stream options
 */
int dio_common_set_option(php_dio_stream_data *data, int option, int value, void *ptrparam) {
	COMMTIMEOUTS cto = { 0, 0, 0, 0, 0 };
	int old_is_blocking = 0;

	/* Can't do timeouts or non blocking with raw windows streams :-( */
	if (DIO_STREAM_TYPE_SERIAL == data->stream_type) {
		switch (option) {
			case PHP_STREAM_OPTION_BLOCKING:
				old_is_blocking   = data->is_blocking;
				data->is_blocking = value ? 1 : 0;

				/* Only change values if we need to change them. */
				if (data->is_blocking != old_is_blocking) {
					/* If we're not blocking but don't have a timeout
					   set to return immediately */
					if (!data->is_blocking && !data->has_timeout) {
						cto.ReadIntervalTimeout = MAXDWORD;
					}

					/* If we have a timeout ignore the blocking and set
					   the total time in which to read the data */
					if (data->has_timeout) {
						cto.ReadIntervalTimeout = MAXDWORD;
						cto.ReadTotalTimeoutMultiplier  = MAXDWORD;
						cto.ReadTotalTimeoutConstant = (data->timeout_usec / 1000) +
							(data->timeout_sec * 1000);
					}

					if (!SetCommTimeouts(((php_dio_win32_stream_data*)data)->handle, &cto)) {
						return PHP_STREAM_OPTION_RETURN_ERR;
					}
				}
				return old_is_blocking ? PHP_STREAM_OPTION_RETURN_OK : PHP_STREAM_OPTION_RETURN_ERR;

			case PHP_STREAM_OPTION_READ_TIMEOUT:
				if (ptrparam) {
					/* struct timeval is supported with PHP_WIN32 defined. */
					struct timeval *tv = (struct timeval*)ptrparam;

					/* A timeout of zero seconds and zero microseconds disables
					   any existing timeout. */
					if (tv->tv_sec || tv->tv_usec) {
						data->timeout_sec = tv->tv_sec;
						data->timeout_usec = tv->tv_usec;
						data->has_timeout = -1;

						cto.ReadIntervalTimeout = MAXDWORD;
						cto.ReadTotalTimeoutMultiplier  = MAXDWORD;
						cto.ReadTotalTimeoutConstant = (data->timeout_usec / 1000) +
							(data->timeout_sec * 1000);
					} else {
						data->timeout_sec = 0;
						data->timeout_usec = 0;
						data->has_timeout = 0;
						data->timed_out = 0;

						/* If we're not blocking but don't have a timeout
						   set to return immediately */
						if (!data->is_blocking) {
							cto.ReadIntervalTimeout = MAXDWORD;
						}
					}

					if (!SetCommTimeouts(((php_dio_win32_stream_data*)data)->handle, &cto)) {
						return PHP_STREAM_OPTION_RETURN_ERR;
					} else {
						return PHP_STREAM_OPTION_RETURN_OK;
					}
				} else {
					return PHP_STREAM_OPTION_RETURN_ERR;
				}

			default:
				break;
		}
	}

	return 1;
}
/* }}} */

/* {{{ dio_raw_open_stream
 * Opens the underlying stream.
 */
int dio_raw_open_stream(const char *filename, const char *mode, php_dio_stream_data *data) {
	php_dio_win32_stream_data *wdata = (php_dio_win32_stream_data*)data;
	DWORD err;

	switch(*mode) {
		case 'r':
			wdata->creation_disposition = OPEN_EXISTING;
			break;
		case 'w':
			wdata->creation_disposition = TRUNCATE_EXISTING;
			break;
		case 'a':
			wdata->creation_disposition = OPEN_ALWAYS;
			break;
		case 'x':
			wdata->creation_disposition = CREATE_NEW;
			break;
	}
	mode ++;

	if (*mode && (*mode != '+')) {
		mode++;
	}

	if (*mode && (*mode == '+')) {
		wdata->desired_access = GENERIC_READ | GENERIC_WRITE;
	} else if (OPEN_EXISTING == wdata->creation_disposition) {
		wdata->desired_access = GENERIC_READ;
	} else {
		wdata->desired_access = GENERIC_WRITE;
	}

	wdata->handle = CreateFile(filename, wdata->desired_access, 0,
			NULL, wdata->creation_disposition, FILE_ATTRIBUTE_NORMAL, NULL);
	if (INVALID_HANDLE_VALUE == wdata->handle) {
		err = GetLastError();
		switch (err) {
			case ERROR_FILE_EXISTS:
				php_error_docref(NULL, E_WARNING, "File exists!");
				return 0;

			case ERROR_FILE_NOT_FOUND:
				/* ERROR_FILE_NOT_FOUND with TRUNCATE_EXISTING means that
				 * the file doesn't exist so now try to create it. */
				if (TRUNCATE_EXISTING == wdata->creation_disposition) {
					php_error_docref(NULL, E_NOTICE, "File does not exist, creating new file!");

					wdata->handle = CreateFile(filename, wdata->desired_access, 0,
								NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
					if (INVALID_HANDLE_VALUE == wdata->handle) {
						dio_last_error_php_error(E_WARNING, "CreateFile() failed:");
						return 0;
					}
				} else {
					php_error_docref(NULL, E_WARNING, "File not found!");
					return 0;
				}
				break;

			default:
				dio_last_error_php_error(E_WARNING, "CreateFile() failed:");
				return 0;
		}
	}

	/* If canonical allocate the canonical buffer. */
	if (data->canonical) {
		wdata->canon_data = emalloc(sizeof(php_dio_win32_canon_data));
		memset(wdata->canon_data, 0, sizeof(php_dio_win32_canon_data));
		wdata->canon_data->size = DIO_WIN32_CANON_BUF_SIZE;
	}

	return 1;
}
/* }}} */

/* {{{ dio_serial_init
 * Initialises the serial port
 */
static int dio_serial_init(php_dio_stream_data *data) {
	php_dio_win32_stream_data *wdata = (php_dio_win32_stream_data*)data;
	DWORD rate_def, data_bits_def, stop_bits_def, parity_def;
	DCB dcb;

	if (!dio_data_rate_to_define(data->data_rate, &rate_def)) {
		php_error_docref(NULL, E_WARNING, "invalid data_rate value (%d)", data->data_rate);
		return 0;
	}

	if (!dio_data_bits_to_define(data->data_bits, &data_bits_def)) {
		php_error_docref(NULL, E_WARNING, "invalid data_bits value (%d)", data->data_bits);
		return 0;
	}

	if (!dio_stop_bits_to_define(data->stop_bits, &stop_bits_def)) {
		php_error_docref(NULL, E_WARNING, "invalid stop_bits value (%d)", data->stop_bits);
		return 0;
	}

	if (!dio_parity_to_define(data->parity, &parity_def)) {
		php_error_docref(NULL, E_WARNING, "invalid parity value (%d)", data->parity);
		return 0;
	}

	if (!GetCommState(wdata->handle, &(wdata->olddcb))) {
		dio_last_error_php_error(E_WARNING, "GetCommState() failed:");
		return 0;
	}

	/* Init the DCB structure */
	memset(&dcb, 0, sizeof(DCB));
	dcb.DCBlength = sizeof(DCB);

	/* Set the communication parameters */
	dcb.fBinary  = 1;
	dcb.BaudRate = rate_def;
	dcb.ByteSize = (BYTE)data_bits_def;
	dcb.StopBits = (BYTE)stop_bits_def;
	dcb.Parity   = (BYTE)parity_def;

	/* Set the control line parameters */
	dcb.fDtrControl       = DTR_CONTROL_DISABLE;
	dcb.fDsrSensitivity   = FALSE;
	dcb.fOutxDsrFlow      = FALSE;
	dcb.fTXContinueOnXoff = FALSE;
	dcb.fOutX             = FALSE;
	dcb.fInX              = FALSE;
	dcb.fErrorChar        = FALSE;
	dcb.fNull             = FALSE;
	dcb.fAbortOnError     = FALSE;

	/* Hardware flow control */
	if (data->flow_control) {
		dcb.fOutxCtsFlow = TRUE;
		dcb.fRtsControl  = RTS_CONTROL_HANDSHAKE;
	} else {
		dcb.fOutxCtsFlow = FALSE;
		dcb.fRtsControl  = RTS_CONTROL_DISABLE;
	}

	if (!SetCommState(wdata->handle, &dcb)) {
		dio_last_error_php_error(E_WARNING, "SetCommState() failed:");
		return 0;
	}

	return 1;
}
/* }}} */


/* {{{ dio_serial_uninit
 * Restores the serial settings back to their original state.
 */
int dio_serial_uninit(php_dio_stream_data *data) {
	php_dio_win32_stream_data *wdata = (php_dio_win32_stream_data*)data;

	if (!SetCommState(wdata->handle, &(wdata->olddcb))) {
		return 0;
	}

	return 1;
}
/* }}} */

/* {{{ dio_serial_flush
 * Purges the serial buffers of data.
 */
int dio_serial_purge(php_dio_stream_data *data) {
	php_dio_win32_stream_data *wdata = (php_dio_win32_stream_data*)data;
	BOOL ret;

	/* Purge the canonical buffer if required */
	if (data->canonical && ((wdata->desired_access & GENERIC_READ) == GENERIC_READ)) {
		wdata->canon_data->read_pos  = 0;
		wdata->canon_data->write_pos = 0;
	}

	/* Purge the com port */
	if ((wdata->desired_access & (GENERIC_READ|GENERIC_WRITE)) == (GENERIC_READ|GENERIC_WRITE)) {
		ret = PurgeComm(wdata->handle, PURGE_RXCLEAR|PURGE_TXCLEAR);
	} else if ((wdata->desired_access & GENERIC_WRITE) == GENERIC_WRITE) {
		ret = PurgeComm(wdata->handle, PURGE_TXCLEAR);
	} else if ((wdata->desired_access & GENERIC_READ) == GENERIC_READ) {
		ret = PurgeComm(wdata->handle, PURGE_RXCLEAR);
	}

	return ret;
}
/* }}} */

/* {{{ dio_serial_open_stream
 * Opens the underlying stream.
 */
int dio_serial_open_stream(const char *filename, const char *mode, php_dio_stream_data *data) {
	php_dio_win32_stream_data *wdata = (php_dio_win32_stream_data*)data;
	COMMTIMEOUTS cto = { 0, 0, 0, 0, 0 };

	php_error_docref(NULL, E_NOTICE, "Opening \"%s\" as a serial port (mode=\"%s\").", filename, mode);

	if (*mode != 'r') {
		php_error_docref(NULL, E_WARNING, "You must open serial ports in read or read/write mode!");
		return 0;
	}

	if (!dio_raw_open_stream(filename, mode, data)) {
		return 0;
	}

	if (!GetCommTimeouts(wdata->handle, &(wdata->oldcto))) {
		dio_last_error_php_error(E_WARNING, "GetCommTimeouts() failed (Not a comm port?):");
		CloseHandle(wdata->handle);
		return 0;
	}

	/* If we're not blocking but don't have a timeout
	   set to return immediately */
	if (!data->is_blocking && !data->has_timeout) {
		cto.ReadIntervalTimeout = MAXDWORD;
	}
	
	/* If we have a timeout ignore the blocking and set
	   the total time in which to read the data */
	if (data->has_timeout) {
		cto.ReadIntervalTimeout = MAXDWORD;
		cto.ReadTotalTimeoutMultiplier  = MAXDWORD;
		cto.ReadTotalTimeoutConstant = (data->timeout_usec / 1000) + 
			(data->timeout_sec * 1000);
	}

	if (!SetCommTimeouts(wdata->handle, &cto)) {
		dio_last_error_php_error(E_WARNING, "SetCommTimeouts() failed:");
		CloseHandle(wdata->handle);
		return 0;
	}

	if (!dio_serial_init(data)) {
		CloseHandle(wdata->handle);
		return 0;
	}

	return 1;
}
/* }}} */

/*
 * Local variables:
 * c-basic-offset: 4
 * tab-width: 4
 * End:
 * vim600: fdm=marker
 * vim: sw=4 ts=4 noet
 */
