/**
    @file   ConnectionCOM.cpp
    @author Lime Microsystems (www.limemicro.com)
    @brief  Implementation of communications through COM port
*/

#include "ConnectionCOM.h"

#include "string.h"
#ifdef __unix__
#include <fstream>
#include <errno.h>
#include <unistd.h>
#include <termios.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <iostream>
#include <stdio.h>
#endif // LINUX

const int COM_RETRY_INTERVAL = 20; //ms
const int COM_TOTAL_TIMEOUT = 300; //ms

/** @brief Initializes com port connection
*/
ConnectionCOM::ConnectionCOM()
{
    currentDeviceIndex = -1;
	connected = false;
	comPortList.clear();
	m_deviceNames.clear();
	m_connectionType = COM_PORT;
#ifndef __unix__
	hComm = INVALID_HANDLE_VALUE;
#else
    hComm = -1;
#endif
	comBaudrate = 9600;
}

/** @brief When object is destroyed it closes it's opened COM port
*/
ConnectionCOM::~ConnectionCOM()
{
	Close();
}

/** @brief Opens connection to first found chip
    @return 0-success
*/
IConnection::DeviceStatus ConnectionCOM::Open()
{
	comPortList.clear();
	if(comPortList.size() == 0)
		FindAllComPorts();

	m_deviceNames.clear();
	if(m_deviceNames.size() == 0)
		RefreshDeviceList();

	for(unsigned int i=0; i<m_deviceNames.size(); i++)
	{
		if( Open(i) == SUCCESS)
		{
			currentDeviceIndex = i;
			return SUCCESS;
		}
	}
	return FAILURE;
}

/** @brief Opens connection to selected chip
    @param index chip index in device list
    @return 0-success
*/
IConnection::DeviceStatus ConnectionCOM::Open(unsigned index)
{
    unsigned int toOpen = index;
	Close();
	if(toOpen < m_deviceNames.size() && m_deviceNames.size() > 0 )
	{
		comPortName = m_deviceNames[toOpen];
		IConnection::DeviceStatus status = Open(comPortName.c_str(), comBaudrate);
		if( status == SUCCESS )
			currentDeviceIndex = toOpen;
        return status;
	}
	return FAILURE;
}

/** @brief Closes connection to chip
*/
void ConnectionCOM::Close()
{
    connected = false;
    currentDeviceIndex = -1;
#ifndef __unix__
	if (hComm != INVALID_HANDLE_VALUE)
	{
		SetCommTimeouts(hComm, &m_ctmoOld);
		CloseHandle(hComm);
	}
	hComm = INVALID_HANDLE_VALUE;
#else
    if( hComm >= 0)
    {
        close(hComm);
    }
    hComm = -1;
#endif
}

/** @brief Returns whether chip is connected
    @return chip is connected
*/
bool ConnectionCOM::IsOpen()
{
#ifndef __unix__
	if (hComm != INVALID_HANDLE_VALUE && TestConnectivity() )
		return true;
#else
    if( hComm != -1 && TestConnectivity() )
        return true;
#endif
    return false;
}

int ConnectionCOM::GetOpenedIndex()
{
    return currentDeviceIndex;
}

/** @brief Sends data through COM port
    @param buffer data buffer to send
    @param length size of data buffer
    @param timeout_ms timeout limit for operation in milliseconds
    @return Number of bytes sent
*/
int ConnectionCOM::Write(const unsigned char *buffer, int length, int timeout_ms)
{
    if(timeout_ms == 0)
    {
        timeout_ms = COM_TOTAL_TIMEOUT;
    }
    int retryCount = 0;
    const int maxRetries = (timeout_ms/COM_RETRY_INTERVAL) > 1 ? (timeout_ms/COM_RETRY_INTERVAL) : 1;
    bool status = false;
#ifndef __unix__
	unsigned long bytesWriten = 0;
	m_osWOverlap.InternalHigh = 0;

	for(int i = 0; i<maxRetries && status == false; ++i)
    {
        if (!WriteFile(hComm, buffer, length , &bytesWriten, NULL))
        {
            status = false;
        }
        else
            status = true;
        ++retryCount;
    }
#else
    long bytesWriten = 0;
    for(int i = 0; i<maxRetries && bytesWriten == 0; ++i)
    {
        bytesWriten = write(hComm, buffer, length);
        if(bytesWriten <= 0)
        {
//            if(bytesWriten < 0)
//                MessageLog::getInstance()->write("COM PORT: error writing data\n", LOG_ERROR);
//            if(bytesWriten == 0)
//                MessageLog::getInstance()->write("COM PORT: data bytes sent 0\n", LOG_WARNING);
            status = false;
        }
        else
            status = true;
        ++retryCount;
    }
#endif
    if(bytesWriten == length)
        status = true;

	return bytesWriten;
}

/** @brief Reads data from COM port
    @param buffer pointer to data buffer for receiving
    @param length number of bytes to read
    @param timeout_ms timeout limit for operation in milliseconds
    @return Number of bytes received
*/
int ConnectionCOM::Read(unsigned char *buffer, int length, int timeout_ms)
{
    if(timeout_ms == 0)
    {
        timeout_ms = COM_TOTAL_TIMEOUT;
    }
    int retryCount = 0;
    const int maxRetries = (timeout_ms/COM_RETRY_INTERVAL) > 1 ? (timeout_ms/COM_RETRY_INTERVAL) : 1;
    bool status = false;

    memset(buffer, 0, length);
	long bytesReaded = 0;
	unsigned long totalBytesReaded = 0;
	char cRawData[COM_BUFFER_LENGTH];
	unsigned long bytesToRead = length;

	memset(cRawData, '\0', sizeof(cRawData[0])*COM_BUFFER_LENGTH);

	for(int i=0; i<maxRetries && status == false; ++i)
    {
        memset(cRawData, '\0', sizeof(cRawData[0])*COM_BUFFER_LENGTH);
#ifndef __unix__

        DWORD bytesReceived = 0;
        if ( !ReadFile(hComm, cRawData, bytesToRead, &bytesReceived, NULL) )
        {
            status = false;
        }

        bytesReaded = bytesReceived;
#else
        bytesReaded = read(hComm, cRawData, bytesToRead);
        if(bytesReaded <= 0)
        {
//            if(bytesReaded < 0)
//                MessageLog::getInstance()->write("COM PORT: error reading data\n", LOG_ERROR);
//            if(bytesReaded == 0)
//                MessageLog::getInstance()->write("COM PORT: reading 0 bytes\n", LOG_WARNING);
            status = false;
        }
#endif
        retryCount++;

        for(int j=0; j<bytesReaded; ++j)
        {
            buffer[totalBytesReaded+j] = cRawData[j];
        }

        totalBytesReaded += bytesReaded;
        if(totalBytesReaded == bytesToRead)
            status = true;
    }

//    ss << " read(" << totalBytesReaded << "): ";
//    for(unsigned int i=0; i<64; ++i)
//        ss << int2hex(buffer[i], 1) << " ";
//    ss <<  " - retries: " << retryCount-1 << endl;
    //MessageLog::getInstance()->write(ss.str(), LOG_DATA);

//	if(retryCount == maxRetries)
//        MessageLog::getInstance()->write("COM PORT: read data timeout\n", LOG_WARNING);
//
//    if(totalBytesReaded > length)
//        MessageLog::getInstance()->write("COM PORT: read data corrupted, received length > requested length\n", LOG_ERROR);
	return totalBytesReaded;
}

/** @brief Searches for available COM ports and adds them to list
*/
void ConnectionCOM::FindAllComPorts()
{
    Close();
	comPortList.clear();
#ifndef __unix__
	HKEY hSERIALCOMM;
	if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, TEXT("HARDWARE\\DEVICEMAP\\SERIALCOMM"),	0, KEY_QUERY_VALUE, &hSERIALCOMM) == ERROR_SUCCESS)
	{
		// Get the max value name and max value lengths
		DWORD dwMaxValueNameLen;
		DWORD dwMaxValueLen;
		DWORD dwQueryInfo = RegQueryInfoKey(hSERIALCOMM, NULL, NULL, NULL, NULL, NULL, NULL, NULL, &dwMaxValueNameLen, &dwMaxValueLen, NULL, NULL);
		if (dwQueryInfo == ERROR_SUCCESS)
		{
			DWORD dwMaxValueNameSizeInChars = dwMaxValueNameLen + 1;
			// Include space for the NULL terminator
			DWORD dwMaxValueNameSizeInBytes = dwMaxValueNameSizeInChars*sizeof(TCHAR);
			DWORD dwMaxValueDataSizeInChars = dwMaxValueLen / sizeof(TCHAR) + 1;
			// Include space for the NULL terminator
			DWORD dwMaxValueDataSizeInBytes = dwMaxValueDataSizeInChars*sizeof(TCHAR);

			// Allocate some space for the value name and value data
			TCHAR* szValueName = new TCHAR[dwMaxValueNameSizeInChars];
			TCHAR* byValue = new TCHAR[dwMaxValueDataSizeInBytes];
			if (szValueName && byValue)
			{
				// Enumerate all the values underneath HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP\SERIALCOMM
				DWORD dwIndex = 0;
				DWORD dwType;
				DWORD dwValueNameSize = dwMaxValueNameSizeInChars;
				DWORD dwDataSize = dwMaxValueDataSizeInBytes;
				memset(szValueName, 0, dwMaxValueNameSizeInBytes);
				memset(byValue, 0, dwMaxValueDataSizeInBytes);
				LONG nEnum = RegEnumValue(hSERIALCOMM, dwIndex, szValueName, &dwValueNameSize, NULL, &dwType, (LPBYTE)byValue, &dwDataSize);
				while (nEnum == ERROR_SUCCESS)
				{
					// If the value is of the correct type, then add it to the array
					if (dwType == REG_SZ)
					{
					    char portname[512];
						TCHAR* szPort = byValue;
						int nUserNameLenUnicode = lstrlen( szPort ); // Convert all UNICODE characters
						int nUserNameLen = WideCharToMultiByte( CP_ACP, // ANSI Code Page
                        0, // No special handling of unmapped chars
                        (LPCWSTR)szPort, // wide-character string to be converted
                        nUserNameLenUnicode,
                        NULL, 0, // No output buffer since we are calculating length
                        NULL, NULL ); // Unrepresented char replacement - Use Default
                        TCHAR* pszUserName = new TCHAR[ nUserNameLen ]; // nUserNameLen includes the NULL character
                        WideCharToMultiByte( CP_ACP, // ANSI Code Page
                        0, // No special handling of unmapped chars
						(LPCWSTR)szPort, // wide-character string to be converted
                        nUserNameLenUnicode,
                        portname,
                        nUserNameLen,
                        NULL, NULL ); // Unrepresented char replacement - Use Default
						portname[nUserNameLen] = 0;
#ifdef UNICODE
                        comPortList.push_back(portname);
#else
                        comPortList.push_back(szPort);
#endif
					}
					// Prepare for the next time around
					dwValueNameSize = dwMaxValueNameSizeInChars;
					dwDataSize = dwMaxValueDataSizeInBytes;
					memset(szValueName, 0, dwMaxValueNameSizeInBytes);
					memset(byValue, 0, dwMaxValueDataSizeInBytes);
					++dwIndex;
					nEnum = RegEnumValue(hSERIALCOMM, dwIndex, szValueName,	&dwValueNameSize, NULL, &dwType, (LPBYTE)byValue, &dwDataSize);
				}
			}
			delete szValueName;
			delete byValue;
		}
		// Close the registry key now that we are finished with it
		RegCloseKey(hSERIALCOMM);

		if (dwQueryInfo != ERROR_SUCCESS)
			SetLastError(dwQueryInfo);
	}
#else
    char tempBuffer[256];
    string result = "";
#warning Currently searching only for ACM connections
    system( "ls /dev | grep ttyACM > /tmp/foundSerialPorts.txt");

    fstream fin;
    fin.open("/tmp/foundSerialPorts.txt", ios::in);
    while(!fin.eof())
    {
        fin.getline(tempBuffer, 256);
        result = "/dev/";
        result.append(tempBuffer);
        if( strlen(tempBuffer) > 3 ) //longer than tty
            comPortList.push_back(result);
    }
    fin.close();
#endif
}

/** @brief Opens COM port
    @param comName COM port name
    @param baudrate COM port baudrate
    @return 0 on success
*/
IConnection::DeviceStatus ConnectionCOM::Open(const char *comName, int baudrate)
{
	Close();
	if (strlen(comName) == 0)
		return FAILURE;

	DeviceStatus errorCode = SUCCESS;

#ifndef __unix__
	// Initialize Overlap structures
	m_osROverlap.Internal = 0;
	m_osROverlap.InternalHigh = 0;
	m_osROverlap.Offset = 0;
	m_osROverlap.OffsetHigh = 0;
	m_osROverlap.hEvent = CreateEvent(NULL, false, false, NULL);

	m_osWOverlap.Internal = 0;
	m_osWOverlap.InternalHigh = 0;
	m_osWOverlap.Offset = 0;
	m_osWOverlap.OffsetHigh = 0;
	m_osWOverlap.hEvent = CreateEvent(NULL, false, false, NULL);

	// Initialize DSB structure
	memset(&m_dcbCommPort, 0, sizeof(m_dcbCommPort));

	m_dcbCommPort.BaudRate = comBaudrate;
	m_dcbCommPort.fBinary = 1;
	m_dcbCommPort.fParity = 0;
	m_dcbCommPort.fOutxCtsFlow = 0;
	m_dcbCommPort.fOutxDsrFlow = 0;
	m_dcbCommPort.fDtrControl = 0;
	m_dcbCommPort.fDsrSensitivity = 0;
	m_dcbCommPort.fTXContinueOnXoff = 0;
	m_dcbCommPort.fOutX = 0;
	m_dcbCommPort.fInX = 0;
	m_dcbCommPort.fErrorChar = 0;
	m_dcbCommPort.fNull = 0;
	m_dcbCommPort.fRtsControl = 0;
	m_dcbCommPort.fAbortOnError = 0;
	m_dcbCommPort.fDummy2 = 0;
	// m_dcbCommPort.wReserved = 0;
	m_dcbCommPort.XonLim = 512;
	m_dcbCommPort.XoffLim = 512;
	m_dcbCommPort.ByteSize = 8;
	m_dcbCommPort.Parity = 0;
	m_dcbCommPort.StopBits = 0;
	//m_dcbCommPort.StopBits = 1;
	m_dcbCommPort.XonChar = 17;
	m_dcbCommPort.XoffChar = 19;
	m_dcbCommPort.ErrorChar = 0;
	m_dcbCommPort.EofChar = 26;
	m_dcbCommPort.EvtChar = 0;
	m_dcbCommPort.wReserved1 = 0;
	m_dcbCommPort.DCBlength = sizeof(DCB);

	// Initialize Timeout's
	m_ctmoNew.ReadIntervalTimeout = 50;
	m_ctmoNew.ReadTotalTimeoutMultiplier = 0;
	m_ctmoNew.ReadTotalTimeoutConstant = 100; // 1;
	m_ctmoNew.WriteTotalTimeoutMultiplier = 0;
	m_ctmoNew.WriteTotalTimeoutConstant = 100;

	// Open COM port
	string stmp;
	stmp = "\\\\.\\";
	stmp.append(comName);
	hComm = CreateFileA(stmp.c_str(), GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);

	if (hComm != INVALID_HANDLE_VALUE)
	{
		// Set Events
		if (!SetCommMask(hComm, 0))
			errorCode = FAILURE;

		// Set Timeouts
		GetCommTimeouts(hComm, &m_ctmoOld);
		if (!SetCommTimeouts(hComm, &m_ctmoNew))
			errorCode = FAILURE;

		// Set DCB
		if (!SetCommState(hComm, &m_dcbCommPort))
			errorCode = FAILURE;
	}
	else
	{
		errorCode = FAILURE;
	};

	// Check the results
	if (errorCode != 0)
	{
		//unsigned long err = GetLastError();
		CloseHandle(hComm);
		hComm = INVALID_HANDLE_VALUE;
		return errorCode;
	}
	else
	{
		PurgeComm(hComm, PURGE_TXCLEAR | PURGE_RXCLEAR);
		return SUCCESS;
	}
#else
    hComm = open(comName, O_RDWR | O_NOCTTY | O_SYNC);
    if(hComm < 0)
    {
//        printf("%s",strerror(errno));
//        MessageLog::getInstance()->write("Connection manager: failed opening COM port\n", LOG_ERROR);
        return FAILURE;
    }

    struct termios tty;
    memset(&tty, 0, sizeof(tty));
    if( tcgetattr(hComm, &tty) != 0)
    {
//        MessageLog::getInstance()->write("Connection Manager: error from tcgetattr\n", LOG_ERROR);
        return FAILURE;
    }
    int speed = B9600;
    cfsetospeed(&tty, speed);
    cfsetispeed(&tty, speed);

    tty.c_cflag = (tty.c_cflag & ~CSIZE) | CS8;
    tty.c_iflag &= ~IGNBRK;
    tty.c_lflag = 0;
    tty.c_oflag = 0;
    tty.c_cc[VMIN] = 0; // read non blocking
    tty.c_cc[VTIME] = 5; // 0.5 seconds read timeout

    tty.c_iflag &= ~(IXON | IXOFF | IXANY);
    tty.c_cflag |= (CLOCAL | CREAD);

    if(tcsetattr(hComm, TCSANOW, &tty) != 0)
    {
//        MessageLog::getInstance()->write("Connection manager: error from tcsetattr\n", LOG_ERROR);
        return FAILURE;
    }
#endif
    return SUCCESS;
}

/** @brief Checks if chip is connected to currently open port
    @return chip is connected
*/
bool ConnectionCOM::TestConnectivity()
{
//currently set to always return true to show all com ports
    return true;
#ifndef __unix__
	if (hComm != INVALID_HANDLE_VALUE)
#else
    if( hComm >= 0)
#endif
	{
//	    unsigned char out[64];
//	    unsigned char in[64];
//	    memset(in, 0, 64);
//	    out[0] = CMD_GET_INFO;
//        SendData(out, 64);
//        ReadData(in, 64);
//        if(in[0] == CMD_GET_INFO && in[1] == 0x01)
//            return true;
//        else
//            return false;
	}
	return false;
}

/** @brief Finds all chips connected to com ports
    @return number of devices found
*/
int ConnectionCOM::RefreshDeviceList()
{
    int wasOpen = -1;
    string wasOpenName = "";
    if(IsOpen())
    {
        wasOpen = GetOpenedIndex();
        wasOpenName = comPortName;
    }
    Close();
    comPortList.clear();
	if(comPortList.size() == 0)
		FindAllComPorts();

	m_deviceNames.clear();
	string comName;
	for(unsigned int i=0; i<comPortList.size(); i++)
	{
	    comName = comPortList[i];
#ifndef __unix__
		if( Open(comName.c_str(), comBaudrate) == SUCCESS)
		{
			if( TestConnectivity() ) //if responds add it to device list
				m_deviceNames.push_back(comName);
		}
		Close();
#else
        m_deviceNames.push_back(comName);
#endif
	}
	if(wasOpen != -1)
    {
        for(unsigned i=0; i<m_deviceNames.size(); ++i)
            if(m_deviceNames[i] == wasOpenName)
            {
                Open(i);
                break;
            }
    }
	return m_deviceNames.size();
}

/** @brief Returns found devices names
    @return vector of device names
*/
vector<string> ConnectionCOM::GetDeviceNames()
{
    return m_deviceNames;
}

/** @brief Purges communication buffers
*/
void ConnectionCOM::ClearComm()
{
#ifndef __unix__
    PurgeComm(hComm, PURGE_TXCLEAR|PURGE_RXCLEAR);
#endif
}