#if defined(_WIN32)

/* Copyright 2012 William Woodall and John Harrison */

#include <sstream>

#include "serial/impl/win.h"

using std::string;
using std::wstring;
using std::stringstream;
using std::invalid_argument;
using serial::Serial;
using serial::Timeout;
using serial::bytesize_t;
using serial::parity_t;
using serial::stopbits_t;
using serial::flowcontrol_t;
using serial::SerialException;
using serial::PortNotOpenedException;
using serial::IOException;

inline wstring
_prefix_port_if_needed(const wstring &input)
{
  static wstring windows_com_port_prefix = L"\\\\.\\";
  if (input.compare(0, windows_com_port_prefix.size(), windows_com_port_prefix) != 0)
  {
    return windows_com_port_prefix + input;
  }
  return input;
}

Serial::SerialImpl::SerialImpl (const string &port, unsigned long baudrate,
                                bytesize_t bytesize,
                                parity_t parity, stopbits_t stopbits,
                                flowcontrol_t flowcontrol)
  : port_ (port.begin(), port.end()), fd_ (INVALID_HANDLE_VALUE), is_open_ (false),
    baudrate_ (baudrate), parity_ (parity),
    bytesize_ (bytesize), stopbits_ (stopbits), flowcontrol_ (flowcontrol)
{
  if (port_.empty () == false)
    open ();
  read_mutex = CreateMutex(NULL, false, NULL);
  write_mutex = CreateMutex(NULL, false, NULL);
}

Serial::SerialImpl::~SerialImpl ()
{
  this->close();
  CloseHandle(read_mutex);
  CloseHandle(write_mutex);
}

void
Serial::SerialImpl::open ()
{
  if (port_.empty ()) {
    throw invalid_argument ("Empty port is invalid.");
  }
  if (is_open_ == true) {
    throw SerialException ("Serial port already open.");
  }

  // See: https://github.com/wjwwood/serial/issues/84
  wstring port_with_prefix = _prefix_port_if_needed(port_);
  LPCWSTR lp_port = port_with_prefix.c_str();
  fd_ = CreateFileW(lp_port,
                    GENERIC_READ | GENERIC_WRITE,
                    0,
                    0,
                    OPEN_EXISTING,
                    FILE_ATTRIBUTE_NORMAL,
                    0);

  if (fd_ == INVALID_HANDLE_VALUE) {
    DWORD create_file_err = GetLastError();
	stringstream ss;
    switch (create_file_err) {
    case ERROR_FILE_NOT_FOUND:
      // Use this->getPort to convert to a std::string
      ss << "Specified port, " << this->getPort() << ", does not exist.";
      THROW (IOException, ss.str().c_str());
    default:
      ss << "Unknown error opening the serial port: " << create_file_err;
      THROW (IOException, ss.str().c_str());
    }
  }

  reconfigurePort();
  is_open_ = true;
}

void
Serial::SerialImpl::reconfigurePort ()
{
  if (fd_ == INVALID_HANDLE_VALUE) {
    // Can only operate on a valid file descriptor
    THROW (IOException, "Invalid file descriptor, is the serial port open?");
  }

  DCB dcbSerialParams = {0};

  dcbSerialParams.DCBlength=sizeof(dcbSerialParams);

  if (!GetCommState(fd_, &dcbSerialParams)) {
    //error getting state
    THROW (IOException, "Error getting the serial port state.");
  }

  // setup baud rate
  switch (baudrate_) {
#ifdef CBR_0
  case 0: dcbSerialParams.BaudRate = CBR_0; break;
#endif
#ifdef CBR_50
  case 50: dcbSerialParams.BaudRate = CBR_50; break;
#endif
#ifdef CBR_75
  case 75: dcbSerialParams.BaudRate = CBR_75; break;
#endif
#ifdef CBR_110
  case 110: dcbSerialParams.BaudRate = CBR_110; break;
#endif
#ifdef CBR_134
  case 134: dcbSerialParams.BaudRate = CBR_134; break;
#endif
#ifdef CBR_150
  case 150: dcbSerialParams.BaudRate = CBR_150; break;
#endif
#ifdef CBR_200
  case 200: dcbSerialParams.BaudRate = CBR_200; break;
#endif
#ifdef CBR_300
  case 300: dcbSerialParams.BaudRate = CBR_300; break;
#endif
#ifdef CBR_600
  case 600: dcbSerialParams.BaudRate = CBR_600; break;
#endif
#ifdef CBR_1200
  case 1200: dcbSerialParams.BaudRate = CBR_1200; break;
#endif
#ifdef CBR_1800
  case 1800: dcbSerialParams.BaudRate = CBR_1800; break;
#endif
#ifdef CBR_2400
  case 2400: dcbSerialParams.BaudRate = CBR_2400; break;
#endif
#ifdef CBR_4800
  case 4800: dcbSerialParams.BaudRate = CBR_4800; break;
#endif
#ifdef CBR_7200
  case 7200: dcbSerialParams.BaudRate = CBR_7200; break;
#endif
#ifdef CBR_9600
  case 9600: dcbSerialParams.BaudRate = CBR_9600; break;
#endif
#ifdef CBR_14400
  case 14400: dcbSerialParams.BaudRate = CBR_14400; break;
#endif
#ifdef CBR_19200
  case 19200: dcbSerialParams.BaudRate = CBR_19200; break;
#endif
#ifdef CBR_28800
  case 28800: dcbSerialParams.BaudRate = CBR_28800; break;
#endif
#ifdef CBR_57600
  case 57600: dcbSerialParams.BaudRate = CBR_57600; break;
#endif
#ifdef CBR_76800
  case 76800: dcbSerialParams.BaudRate = CBR_76800; break;
#endif
#ifdef CBR_38400
  case 38400: dcbSerialParams.BaudRate = CBR_38400; break;
#endif
#ifdef CBR_115200
  case 115200: dcbSerialParams.BaudRate = CBR_115200; break;
#endif
#ifdef CBR_128000
  case 128000: dcbSerialParams.BaudRate = CBR_128000; break;
#endif
#ifdef CBR_153600
  case 153600: dcbSerialParams.BaudRate = CBR_153600; break;
#endif
#ifdef CBR_230400
  case 230400: dcbSerialParams.BaudRate = CBR_230400; break;
#endif
#ifdef CBR_256000
  case 256000: dcbSerialParams.BaudRate = CBR_256000; break;
#endif
#ifdef CBR_460800
  case 460800: dcbSerialParams.BaudRate = CBR_460800; break;
#endif
#ifdef CBR_921600
  case 921600: dcbSerialParams.BaudRate = CBR_921600; break;
#endif
  default:
    // Try to blindly assign it
    dcbSerialParams.BaudRate = baudrate_;
  }

  // setup char len
  if (bytesize_ == eightbits)
    dcbSerialParams.ByteSize = 8;
  else if (bytesize_ == sevenbits)
    dcbSerialParams.ByteSize = 7;
  else if (bytesize_ == sixbits)
    dcbSerialParams.ByteSize = 6;
  else if (bytesize_ == fivebits)
    dcbSerialParams.ByteSize = 5;
  else
    throw invalid_argument ("invalid char len");

  // setup stopbits
  if (stopbits_ == stopbits_one)
    dcbSerialParams.StopBits = ONESTOPBIT;
  else if (stopbits_ == stopbits_one_point_five)
    dcbSerialParams.StopBits = ONE5STOPBITS;
  else if (stopbits_ == stopbits_two)
    dcbSerialParams.StopBits = TWOSTOPBITS;
  else
    throw invalid_argument ("invalid stop bit");

  // setup parity
  if (parity_ == parity_none) {
    dcbSerialParams.Parity = NOPARITY;
  } else if (parity_ == parity_even) {
    dcbSerialParams.Parity = EVENPARITY;
  } else if (parity_ == parity_odd) {
    dcbSerialParams.Parity = ODDPARITY;
  } else if (parity_ == parity_mark) {
    dcbSerialParams.Parity = MARKPARITY;
  } else if (parity_ == parity_space) {
    dcbSerialParams.Parity = SPACEPARITY;
  } else {
    throw invalid_argument ("invalid parity");
  }

  // setup flowcontrol
  if (flowcontrol_ == flowcontrol_none) {
    dcbSerialParams.fOutxCtsFlow = false;
    dcbSerialParams.fRtsControl = RTS_CONTROL_DISABLE;
    dcbSerialParams.fOutX = false;
    dcbSerialParams.fInX = false;
  }
  if (flowcontrol_ == flowcontrol_software) {
    dcbSerialParams.fOutxCtsFlow = false;
    dcbSerialParams.fRtsControl = RTS_CONTROL_DISABLE;
    dcbSerialParams.fOutX = true;
    dcbSerialParams.fInX = true;
  }
  if (flowcontrol_ == flowcontrol_hardware) {
    dcbSerialParams.fOutxCtsFlow = true;
    dcbSerialParams.fRtsControl = RTS_CONTROL_HANDSHAKE;
    dcbSerialParams.fOutX = false;
    dcbSerialParams.fInX = false;
  }

  // activate settings
  if (!SetCommState(fd_, &dcbSerialParams)){
    CloseHandle(fd_);
    THROW (IOException, "Error setting serial port settings.");
  }

  // Setup timeouts
  COMMTIMEOUTS timeouts = {0};
  timeouts.ReadIntervalTimeout = timeout_.inter_byte_timeout;
  timeouts.ReadTotalTimeoutConstant = timeout_.read_timeout_constant;
  timeouts.ReadTotalTimeoutMultiplier = timeout_.read_timeout_multiplier;
  timeouts.WriteTotalTimeoutConstant = timeout_.write_timeout_constant;
  timeouts.WriteTotalTimeoutMultiplier = timeout_.write_timeout_multiplier;
  if (!SetCommTimeouts(fd_, &timeouts)) {
    THROW (IOException, "Error setting timeouts.");
  }
}

void
Serial::SerialImpl::close ()
{
  if (is_open_ == true) {
    if (fd_ != INVALID_HANDLE_VALUE) {
      int ret;
      ret = CloseHandle(fd_);
      if (ret == 0) {
        stringstream ss;
        ss << "Error while closing serial port: " << GetLastError();
        THROW (IOException, ss.str().c_str());
      } else {
        fd_ = INVALID_HANDLE_VALUE;
      }
    }
    is_open_ = false;
  }
}

bool
Serial::SerialImpl::isOpen () const
{
  return is_open_;
}

size_t
Serial::SerialImpl::available ()
{
  if (!is_open_) {
    return 0;
  }
  COMSTAT cs;
  if (!ClearCommError(fd_, NULL, &cs)) {
    stringstream ss;
    ss << "Error while checking status of the serial port: " << GetLastError();
    THROW (IOException, ss.str().c_str());
  }
  return static_cast<size_t>(cs.cbInQue);
}

bool
Serial::SerialImpl::waitReadable (uint32_t /*timeout*/)
{
  THROW (IOException, "waitReadable is not implemented on Windows.");
  return false;
}

void
Serial::SerialImpl::waitByteTimes (size_t /*count*/)
{
  THROW (IOException, "waitByteTimes is not implemented on Windows.");
}

size_t
Serial::SerialImpl::read (uint8_t *buf, size_t size)
{
  if (!is_open_) {
    throw PortNotOpenedException ("Serial::read");
  }
  DWORD bytes_read;
  if (!ReadFile(fd_, buf, static_cast<DWORD>(size), &bytes_read, NULL)) {
    stringstream ss;
    ss << "Error while reading from the serial port: " << GetLastError();
    THROW (IOException, ss.str().c_str());
  }
  return (size_t) (bytes_read);
}

size_t
Serial::SerialImpl::write (const uint8_t *data, size_t length)
{
  if (is_open_ == false) {
    throw PortNotOpenedException ("Serial::write");
  }
  DWORD bytes_written;
  if (!WriteFile(fd_, data, static_cast<DWORD>(length), &bytes_written, NULL)) {
    stringstream ss;
    ss << "Error while writing to the serial port: " << GetLastError();
    THROW (IOException, ss.str().c_str());
  }
  return (size_t) (bytes_written);
}

void
Serial::SerialImpl::setPort (const string &port)
{
  port_ = wstring(port.begin(), port.end());
}

string
Serial::SerialImpl::getPort () const
{
  return string(port_.begin(), port_.end());
}

void
Serial::SerialImpl::setTimeout (serial::Timeout &timeout)
{
  timeout_ = timeout;
  if (is_open_) {
    reconfigurePort ();
  }
}

serial::Timeout
Serial::SerialImpl::getTimeout () const
{
  return timeout_;
}

void
Serial::SerialImpl::setBaudrate (unsigned long baudrate)
{
  baudrate_ = baudrate;
  if (is_open_) {
    reconfigurePort ();
  }
}

unsigned long
Serial::SerialImpl::getBaudrate () const
{
  return baudrate_;
}

void
Serial::SerialImpl::setBytesize (serial::bytesize_t bytesize)
{
  bytesize_ = bytesize;
  if (is_open_) {
    reconfigurePort ();
  }
}

serial::bytesize_t
Serial::SerialImpl::getBytesize () const
{
  return bytesize_;
}

void
Serial::SerialImpl::setParity (serial::parity_t parity)
{
  parity_ = parity;
  if (is_open_) {
    reconfigurePort ();
  }
}

serial::parity_t
Serial::SerialImpl::getParity () const
{
  return parity_;
}

void
Serial::SerialImpl::setStopbits (serial::stopbits_t stopbits)
{
  stopbits_ = stopbits;
  if (is_open_) {
    reconfigurePort ();
  }
}

serial::stopbits_t
Serial::SerialImpl::getStopbits () const
{
  return stopbits_;
}

void
Serial::SerialImpl::setFlowcontrol (serial::flowcontrol_t flowcontrol)
{
  flowcontrol_ = flowcontrol;
  if (is_open_) {
    reconfigurePort ();
  }
}

serial::flowcontrol_t
Serial::SerialImpl::getFlowcontrol () const
{
  return flowcontrol_;
}

void
Serial::SerialImpl::flush ()
{
  if (is_open_ == false) {
    throw PortNotOpenedException ("Serial::flush");
  }
  FlushFileBuffers (fd_);
}

void
Serial::SerialImpl::flushInput ()
{
  if (is_open_ == false) {
    throw PortNotOpenedException("Serial::flushInput");
  }
  PurgeComm(fd_, PURGE_RXCLEAR);
}

void
Serial::SerialImpl::flushOutput ()
{
  if (is_open_ == false) {
    throw PortNotOpenedException("Serial::flushOutput");
  }
  PurgeComm(fd_, PURGE_TXCLEAR);
}

void
Serial::SerialImpl::sendBreak (int /*duration*/)
{
  THROW (IOException, "sendBreak is not supported on Windows.");
}

void
Serial::SerialImpl::setBreak (bool level)
{
  if (is_open_ == false) {
    throw PortNotOpenedException ("Serial::setBreak");
  }
  if (level) {
    EscapeCommFunction (fd_, SETBREAK);
  } else {
    EscapeCommFunction (fd_, CLRBREAK);
  }
}

void
Serial::SerialImpl::setRTS (bool level)
{
  if (is_open_ == false) {
    throw PortNotOpenedException ("Serial::setRTS");
  }
  if (level) {
    EscapeCommFunction (fd_, SETRTS);
  } else {
    EscapeCommFunction (fd_, CLRRTS);
  }
}

void
Serial::SerialImpl::setDTR (bool level)
{
  if (is_open_ == false) {
    throw PortNotOpenedException ("Serial::setDTR");
  }
  if (level) {
    EscapeCommFunction (fd_, SETDTR);
  } else {
    EscapeCommFunction (fd_, CLRDTR);
  }
}

bool
Serial::SerialImpl::waitForChange ()
{
  if (is_open_ == false) {
    throw PortNotOpenedException ("Serial::waitForChange");
  }
  DWORD dwCommEvent;

  if (!SetCommMask(fd_, EV_CTS | EV_DSR | EV_RING | EV_RLSD)) {
    // Error setting communications mask
    return false;
  }

  if (!WaitCommEvent(fd_, &dwCommEvent, NULL)) {
    // An error occurred waiting for the event.
    return false;
  } else {
    // Event has occurred.
    return true;
  }
}

bool
Serial::SerialImpl::getCTS ()
{
  if (is_open_ == false) {
    throw PortNotOpenedException ("Serial::getCTS");
  }
  DWORD dwModemStatus;
  if (!GetCommModemStatus(fd_, &dwModemStatus)) {
    THROW (IOException, "Error getting the status of the CTS line.");
  }

  return (MS_CTS_ON & dwModemStatus) != 0;
}

bool
Serial::SerialImpl::getDSR ()
{
  if (is_open_ == false) {
    throw PortNotOpenedException ("Serial::getDSR");
  }
  DWORD dwModemStatus;
  if (!GetCommModemStatus(fd_, &dwModemStatus)) {
    THROW (IOException, "Error getting the status of the DSR line.");
  }

  return (MS_DSR_ON & dwModemStatus) != 0;
}

bool
Serial::SerialImpl::getRI()
{
  if (is_open_ == false) {
    throw PortNotOpenedException ("Serial::getRI");
  }
  DWORD dwModemStatus;
  if (!GetCommModemStatus(fd_, &dwModemStatus)) {
    THROW (IOException, "Error getting the status of the RI line.");
  }

  return (MS_RING_ON & dwModemStatus) != 0;
}

bool
Serial::SerialImpl::getCD()
{
  if (is_open_ == false) {
    throw PortNotOpenedException ("Serial::getCD");
  }
  DWORD dwModemStatus;
  if (!GetCommModemStatus(fd_, &dwModemStatus)) {
    // Error in GetCommModemStatus;
    THROW (IOException, "Error getting the status of the CD line.");
  }

  return (MS_RLSD_ON & dwModemStatus) != 0;
}

void
Serial::SerialImpl::readLock()
{
  if (WaitForSingleObject(read_mutex, INFINITE) != WAIT_OBJECT_0) {
    THROW (IOException, "Error claiming read mutex.");
  }
}

void
Serial::SerialImpl::readUnlock()
{
  if (!ReleaseMutex(read_mutex)) {
    THROW (IOException, "Error releasing read mutex.");
  }
}

void
Serial::SerialImpl::writeLock()
{
  if (WaitForSingleObject(write_mutex, INFINITE) != WAIT_OBJECT_0) {
    THROW (IOException, "Error claiming write mutex.");
  }
}

void
Serial::SerialImpl::writeUnlock()
{
  if (!ReleaseMutex(write_mutex)) {
    THROW (IOException, "Error releasing write mutex.");
  }
}

#endif // #if defined(_WIN32)

