/*
 * Copyright (C) 2015 by Multi-Tech Systems
 *
 * This file is part of libmts-io.
 *
 * libmts-io is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * libmts-io 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with libmts-io.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

/*! 
 \file MTS_IO_SerialConnection.cpp
 \brief A brief description 
 \date Jan 15, 2013
 \author sgodinez

 A more elaborate description
*/

#include <mts/MTS_IO_SerialConnection.h>
#include <mts/MTS_Text.h>
#include <mts/MTS_Logger.h>
#include <unistd.h>

#ifdef WIN32

#else
#include <fcntl.h>   /* File control definitions */
#include <errno.h>   /* Error number definitions */
#include <termios.h> /* POSIX terminal control definitions */
#include <string.h>  /* String function definitions */
#endif

using namespace MTS::IO;

SerialConnection::Builder::Builder(const std::string& sPortName)
: m_sPortName(sPortName)
, m_iBaudRate(9600)
, m_eType(RS232)
, m_eParity(OFF)
, m_eDataBits(B8)
, m_eStopBits(B1)
, m_bBuilt(false)
{

}

SerialConnection::Builder& SerialConnection::Builder::type(const ETYPE& eType) {
    m_eType = eType;
    return *this;
}

SerialConnection::Builder& SerialConnection::Builder::rs232() {
    m_eType = RS232;
    return *this;
}

SerialConnection::Builder& SerialConnection::Builder::rs422() {
    m_eType = RS422;
    return *this;
}

SerialConnection::Builder& SerialConnection::Builder::rs485() {
    m_eType = RS485;
    return *this;
}

SerialConnection::Builder& SerialConnection::Builder::parity(const EPARITY& eParity) {
    m_eParity = eParity;
    return *this;
}

SerialConnection::Builder& SerialConnection::Builder::parityOff() {
    m_eParity = OFF;
    return *this;
}

SerialConnection::Builder& SerialConnection::Builder::parityOdd() {
    m_eParity = ODD;
    return *this;
}

SerialConnection::Builder& SerialConnection::Builder::parityEven() {
    m_eParity = EVEN;
    return *this;
}

SerialConnection::Builder& SerialConnection::Builder::dataBits(const EDATABITS& eDataBits) {
    m_eDataBits = eDataBits;
    return *this;
}

SerialConnection::Builder& SerialConnection::Builder::fiveDataBits() {
    m_eDataBits = B5;
    return *this;
}

SerialConnection::Builder& SerialConnection::Builder::sixDataBits() {
    m_eDataBits = B6;
    return *this;
}

SerialConnection::Builder& SerialConnection::Builder::sevenDataBits() {
    m_eDataBits = B7;
    return *this;
}

SerialConnection::Builder& SerialConnection::Builder::eightDataBits() {
    m_eDataBits = B8;
    return *this;
}

SerialConnection::Builder& SerialConnection::Builder::stopBits(const ESTOPBITS& eStopBits) {
    m_eStopBits = eStopBits;
    return *this;
}

SerialConnection::Builder& SerialConnection::Builder::oneStopBit() {
    m_eStopBits = B1;
    return *this;
}

SerialConnection::Builder& SerialConnection::Builder::twoStopBits() {
    m_eStopBits = B2;
    return *this;
}

SerialConnection::Builder& SerialConnection::Builder::baudRate(uint32_t iBaud) {
    m_iBaudRate = iBaud;
    return *this;
}

SerialConnection::Builder& SerialConnection::Builder::useLockFile(bool bEnabled) {
    m_bLockFile = bEnabled;
    return *this;
}

SerialConnection::Builder& SerialConnection::Builder::build() {
    m_bBuilt = true;
    return *this;
}

const std::string& SerialConnection::Builder::getPortName() const {
    return m_sPortName;
}

const uint32_t& SerialConnection::Builder::getBaudRate() const {
    return m_iBaudRate;
}

SerialConnection::ETYPE SerialConnection::Builder::getType() const {
    return m_eType;
}

SerialConnection::EPARITY SerialConnection::Builder::getParity() const {
    return m_eParity;
}

SerialConnection::EDATABITS SerialConnection::Builder::getDataBits() const {
    return m_eDataBits;
}

SerialConnection::ESTOPBITS SerialConnection::Builder::getStopBits() const {
    return m_eStopBits;
}

bool SerialConnection::Builder::isUsingLockFile() const {
    return m_bLockFile;
}

std::string SerialConnection::Builder::getTypeAsString() const {
    std::string sType;
    switch(m_eType) {
        case RS232:
            sType = "RS232";
            break;
        case RS422:
            sType = "RS422";
            break;
        case RS485:
            sType = "RS485";
            break;
        default:
            break;
    }
    return sType;
}

SerialConnection::Builder& SerialConnection::Builder::flowControl(const SerialConnection::FLOW_CONTROL& eFlowControl) {
	m_eFlowControl = eFlowControl;
	return *this;
}

SerialConnection::FLOW_CONTROL SerialConnection::Builder::getFlowControl() const {
	return m_eFlowControl;
}

bool SerialConnection::Builder::isBuilt() const {
    return m_bBuilt;
}

SerialConnection::SerialConnection(const Builder& oBuilder)
: Connection(oBuilder.getTypeAsString())
, m_sPortName(oBuilder.getPortName())
, m_iBaudRate(oBuilder.getBaudRate())
, m_eType(oBuilder.getType())
, m_eParity(oBuilder.getParity())
, m_eDataBits(oBuilder.getDataBits())
, m_eStopBits(oBuilder.getStopBits())
, m_eFlowControl(oBuilder.getFlowControl())
{
    if(!oBuilder.isBuilt()) {
        //Throw Exception?
    }
#ifdef WIN32
    m_iHandle = INVALID_HANDLE_VALUE;
#else
    m_iHandle = -1;
#endif
    m_apHandleLock.reset(new Lock());
    if(oBuilder.isUsingLockFile()) {
        std::vector<std::string> vParts = MTS::Text::split(m_sPortName, "/");
        m_apLockFile.reset(new LockFile("/var/lock/LCK.." + vParts[vParts.size()-1]));
    }
}

SerialConnection::~SerialConnection() {
    doClose();
    m_apHandleLock.reset();
}

const std::string& SerialConnection::getPortName() const {
    return m_sPortName;
}

uint32_t SerialConnection::getBaudRate() const {
    return m_iBaudRate;
}

int SerialConnection::getFileDescriptor() {
    m_apHandleLock->lock();
    int h = m_iHandle;
    m_apHandleLock->unlock();
    return h;
}

bool SerialConnection::doOpen(const int32_t& timeoutMillis) {
    m_apHandleLock->lock();

    if (m_iHandle != -1) {
        m_apHandleLock->unlock();
        return true;
    }
    if(!m_apLockFile.isNull() && !m_apLockFile->lock(timeoutMillis)) {
        printError("SERIAL| Failed to obtain file lock");
        m_apHandleLock->unlock();
        return false;
    }

#ifdef WIN32

#else
    m_iHandle = ::open(m_sPortName.c_str(), O_RDWR | O_NOCTTY );
    if (m_iHandle == -1) {
        printWarning("SERIAL| Failed to open port [%s] [%d]%s", m_sPortName.c_str(), errno,
                     errno == 13 ? " (Permission Denied)" : "");
        if(!m_apLockFile.isNull()) {
            m_apLockFile->unlock();
        }
        m_apHandleLock->unlock();
        return false;
    }

    int result = tcgetattr(m_iHandle, &m_stPrevOptions);
    if (result == -1) {
        printWarning("SERIAL| Failed to get attributes on port [%s] [%d]%s", m_sPortName.c_str(), errno,
                     errno == 13 ? " (Permission Denied)" : "");
    }

    termios options = m_stPrevOptions;

    //Set to Raw Mode
    cfmakeraw(&options);

    //Set the Baud Rate
    speed_t speed = B9600;
    switch (m_iBaudRate) {
        case 300:
            speed = B300;
            break;
        case 600:
            speed = B600;
            break;
        case 1200:
            speed = B1200;
            break;
        case 2400:
            speed = B2400;
            break;
        case 4800:
            speed = B4800;
            break;
        case 9600:
            speed = B9600;
            break;
        case 19200:
            speed = B19200;
            break;
        case 38400:
            speed = B38400;
            break;
        case 57600:
            speed = B57600;
            break;
        case 115200:
            speed = B115200;
            break;
        case 230400:
            speed = B230400;
            break;
        case 460800:
            speed = B460800;
            break;
        case 921600:
            speed = B921600;
            break;
        default:
            printWarning("SERIAL| Baud Rate [%d] not supported. Defaulting to 9600.", m_iBaudRate);
            m_iBaudRate = 9600;
            speed = B9600;
            break;
    }

    result = cfsetispeed(&options, speed);
    if (result == -1) {
        printWarning("SERIAL| Failed to set input speed of [%d] on port [%s] [%d]", m_iBaudRate, m_sPortName.c_str(), errno);
        goto CLEANUP;
    }
    result = cfsetospeed(&options, speed);
    if (result == -1) {
        printWarning("SERIAL| Failed to set output speed of [%d] on port [%s] [%d]", m_iBaudRate, m_sPortName.c_str(), errno);
        goto CLEANUP;
    }

    //Set Parity, Stop Bits, and Data Length
    switch(m_eParity) {
        case ODD:
            options.c_cflag |= ( PARODD | PARENB );
            break;

        case EVEN:
            options.c_cflag |= PARENB;
            break;

        case OFF:
        default:
            options.c_cflag &= ~PARENB;
            break;
    }

    options.c_cflag &= ~CSIZE;
    switch(m_eDataBits) {
        case B5:
            options.c_cflag |= CS5;
            break;

        case B6:
            options.c_cflag |= CS6;
            break;

        case B7:
            options.c_cflag |= CS7;
            break;

        case B8:
        default:
            options.c_cflag |= CS8;
            break;
    }

    switch(m_eStopBits) {
        case B2:
            options.c_cflag &= CSTOPB;
            break;

        case B1:
        default:
            options.c_cflag &= ~CSTOPB;
		break;
	}

	switch (m_eFlowControl) {
		case RTS_CTS:
			options.c_cflag &= CRTSCTS;
			break;
		case NONE:
		default:
			options.c_cflag &= ~(IXON | IXOFF | IXANY);
            break;
    }

	//Set Control Modes
    options.c_cc[VMIN] = 0;
    options.c_cc[VTIME] = 2; // tenths of seconds

    result = tcsetattr(m_iHandle, TCSANOW, &options);
    if (result == -1) {
        printWarning("SERIAL| Failed to set configurations on port [%s] [%d]%s", m_sPortName.c_str(), errno,
                     errno == 13 ? " (Permission Denied)" : "");
        goto CLEANUP;
    }
    result = tcflush(m_iHandle, TCIOFLUSH);
    if (result == -1) {
        printWarning("SERIAL| Failed to flush port [%s] [%d]", m_sPortName.c_str(), errno);
        goto CLEANUP;
    }
#endif
    m_apHandleLock->unlock();
    printInfo("SERIAL| %s connection opened on %s @ %d", getName().c_str(), m_sPortName.c_str(), m_iBaudRate);
    return true;
CLEANUP:
    cleanup();
    m_apHandleLock->unlock();
    return false;
}

void SerialConnection::doClose() {
    m_apHandleLock->lock();
    cleanup();
    m_apHandleLock->unlock();
}

int SerialConnection::doRead(char* pBuffer, const uint32_t& iSize, int32_t& timeoutMillis) {
    int32_t result = -1;
#ifdef WIN32

#else

    int h = getFileDescriptor();

    if (h == -1) {
        printError("SERIAL| bad handle on port %s", m_sPortName.c_str());
        return result;
    }

    if(timeoutMillis < 0) {
        result = ::read(h, pBuffer, iSize);
    } else {
        fd_set readfs;
        struct timeval stTimeout;
        stTimeout.tv_usec = ((timeoutMillis%1000) * 1000);
        stTimeout.tv_sec  = timeoutMillis/1000;

        FD_ZERO(&readfs);
        FD_SET(h, &readfs);
        int res = select(h + 1, &readfs, NULL, NULL, &stTimeout);
        if(res == -1) {
            result = -1;
        } else if (res == 0) {
            //Timeout Occured
            //printTrace("SERIAL| read timed out on port [%s]", m_sPortName.c_str());
            result = 0;
        } else {
            if (FD_ISSET(h, &readfs)) {
                result = ::read(h, pBuffer, iSize);
            } else {
                //socket closed?
                result = -1;
            }
        }
        timeoutMillis = (stTimeout.tv_usec / 1000) + (stTimeout.tv_sec * 1000);
    }
#endif
    if (result < 0) {
       printWarning("SERIAL| failed to read from %s [%d]", m_sPortName.c_str(), errno);
   }
    return result;
}

int SerialConnection::doWrite(const char* pBuffer, const uint32_t& iSize, int32_t& timeoutMillis) {
    int32_t result = -1;
#ifdef WIN32

#else

    int h = getFileDescriptor();

    if (h == -1) {
        printError("SERIAL| bad handle on port %s", m_sPortName.c_str());
        return result;
    }

    if(timeoutMillis < 0) {
        result = ::write(h, pBuffer, iSize);
    } else {
        //No difference between timeout and no timeout
        result = ::write(h, pBuffer, iSize);
    }

#endif
    if (result < 0) {
        printWarning("SERIAL| failed to write to %s [%d]", m_sPortName.c_str(), errno);
    }
    return result;
}

void SerialConnection::cleanup() {
#ifdef WIN32
    CloseHandle(m_iHandle);
    m_iHandle = INVALID_HANDLE_VALUE;
#else
    if(m_iHandle >= 0) {
        int result = tcsetattr(m_iHandle, TCSANOW, &m_stPrevOptions);
        if (result == -1) {
            printWarning("SERIAL| Failed to revert configurations on port [%s] [%d]", m_sPortName.c_str(), errno);
        }
        ::close(m_iHandle);
        m_iHandle = -1;
    }
#endif
    if(!m_apLockFile.isNull()) {
        m_apLockFile->unlock();
	}
}

void SerialConnection::drain() {
	int result = tcdrain(m_iHandle);
	if (result == -1) {
		printWarning("SERIAL| Failed to drain port [%s] [%d]", m_sPortName.c_str(), errno);
	}
}

void SerialConnection::flush() {
	int result = tcflush(m_iHandle, TCIOFLUSH);
	if (result == -1) {
		printWarning("SERIAL| Failed to flush port [%s] [%d]", m_sPortName.c_str(), errno);
	}
}