/*!
  \file
  \brief COM ポート一覧の取得

  \author Satofumi KAMIMURA

  $Id: FindComPorts.cpp 1729 2010-03-03 12:44:01Z satofumi $
*/

#include "FindComPorts.h"
#include "DetectOS.h"
#ifdef WINDOWS_OS
#include <windows.h>
#include <setupapi.h>
#include <deque>
#include <algorithm>

#if defined(MSC)
#pragma comment(lib, "setupapi.lib")
#endif

#else
#include <deque>
#include <algorithm>
#include <cstring>
#include <dirent.h>
#include <sys/stat.h>
#endif

using namespace qrk;
using namespace std;


struct FindComPorts::pImpl
{
  vector<string> base_names_;
  vector<string> driver_names_;


  pImpl(void)
  {
    // Windows はレジストリ情報だけから COM 一覧の探索を行う
    // よって、以降の設定は Linux, MacOS のみで使われる
#if defined(LINUX_OS)
    base_names_.push_back("/dev/ttyUSB");
    base_names_.push_back("/dev/usb/ttyUSB");
#elif defined(MAC_OS)
    base_names_.push_back("/dev/tty.usbmodem");
#endif
  }


  void addBaseName(const char* base_name)
  {
    base_names_.insert(base_names_.begin(), base_name);
  }


  void addDriverName(const char* driver_name)
  {
    driver_names_.push_back(driver_name);
  }


  void orderByDriver(deque<string>& ports, deque<string>& drivers,
                     const char* driver_name,
                     bool all_ports)
  {
    if (ports.empty()) {
      return;
    }

    vector<string>::const_iterator it =
      std::find(driver_names_.begin(), driver_names_.end(), driver_name);
    if (it != driver_names_.end()) {
      ports.push_front(ports.back());
      ports.pop_back();

      drivers.push_front(drivers.back());
      drivers.pop_back();

    } else {
      if (! all_ports) {
        ports.pop_back();
        drivers.pop_back();
      }
    }
  }


  void addFoundPorts(vector<string>& found_ports,
		     const string& port)
  {
    for (vector<string>::iterator it = found_ports.begin();
	 it != found_ports.end(); ++it) {

	// !!! データ構造を map にすべきかも
	// !!! 検索のアルゴリズムを用いるべき

      if (! port.compare(*it)) {
	return;
      }
    }
    found_ports.push_back(port);
  }
};


FindComPorts::FindComPorts(void) : pimpl(new pImpl)
{
}


FindComPorts::~FindComPorts(void)
{
}


void FindComPorts::clearBaseNames(void)
{
  pimpl->base_names_.clear();
}


void FindComPorts::addBaseName(const char* base_name)
{
  pimpl->addBaseName(base_name);
}


vector<string> FindComPorts::baseNames(void)
{
  return pimpl->base_names_;
}


void FindComPorts::addDriverName(const char* driver_name)
{
  pimpl->addDriverName(driver_name);
}


#ifdef WINDOWS_OS
// Windows の場合
size_t FindComPorts::find(std::vector<std::string>& ports,
                          std::vector<std::string>& driver_names,
                          bool all_ports)
{
  // デバイスマネージャの一覧から COM デバイスを探す
  deque<string> found_ports;
  deque<string> found_drivers;

  //4D36E978-E325-11CE-BFC1-08002BE10318
  GUID GUID_DEVINTERFACE_COM_DEVICE =
    {0x4D36E978L, 0xE325, 0x11CE,
     {0xBF, 0xC1, 0x08, 0x00, 0x2B, 0xE1, 0x03, 0x18 }
    };

  HDEVINFO hdi = SetupDiGetClassDevs(&GUID_DEVINTERFACE_COM_DEVICE, 0, 0,
                                     DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);
  if (hdi == INVALID_HANDLE_VALUE) {
    return 0;
  }

  SP_DEVINFO_DATA sDevInfo;
  sDevInfo.cbSize = sizeof(SP_DEVINFO_DATA);
  for (DWORD i = 0; SetupDiEnumDeviceInfo(hdi, i, &sDevInfo); ++i){

    enum { BufferSize = 256 };
    char buffer[BufferSize + 1];
    DWORD dwRegType;
    DWORD dwSize;

    // フレンドリーネームを取得して COM 番号を取り出す
    SetupDiGetDeviceRegistryPropertyA(hdi, &sDevInfo, SPDRP_FRIENDLYNAME,
                                      &dwRegType, (BYTE*)buffer, BufferSize,
                                      &dwSize);
    enum { ComNameLengthMax = 7 };
    size_t n = strlen(buffer);
    if (n < ComNameLengthMax) {
      // COM 名が短過ぎた場合、処理しない
      // 問題がある場合は、修正する
      continue;
    }

    // (COMx) の最後の括弧を探す
    for (int j = n - 1; j > 0; --j) {
      if (buffer[j] == ')') {
        buffer[j] = '\0';
        break;
      }
    }
    char* p = strstr(&buffer[n - ComNameLengthMax], "COM");
    if (! p) {
      continue;
    }
    found_ports.push_back(p);

    // デバイス名を取得し、IsUsbCom の条件に一致したら、先頭に配置する
    SetupDiGetDeviceRegistryPropertyA(hdi, &sDevInfo, SPDRP_DEVICEDESC,
                                      &dwRegType, (BYTE*)buffer, BufferSize,
                                      &dwSize);
    found_drivers.push_back(buffer);
    pimpl->orderByDriver(found_ports, found_drivers, buffer, all_ports);
  }
  SetupDiDestroyDeviceInfoList(hdi);

  ports.insert(ports.end(), found_ports.begin(), found_ports.end());
  driver_names.insert(driver_names.end(),
                      found_drivers.begin(), found_drivers.end());

  return ports.size();
}


#else

namespace
{
  void searchFiles(vector<string>& ports, const string& dir_name)
  {
    DIR* dp = opendir(dir_name.c_str());
    if (! dp) {
      return;
    }

    struct dirent* dir;
    while ((dir = readdir(dp))) {
      struct stat state;
      stat(dir->d_name, &state);
      if (S_ISDIR(state.st_mode)) {
        string file = dir_name + dir->d_name;
        ports.push_back(file);
      }
    }
  }
}


// Linux, Mac の場合
size_t FindComPorts::find(std::vector<std::string>& ports,
                          std::vector<std::string>& driver_names,
                          bool all_ports)
{
  static_cast<void>(all_ports);
  static_cast<void>(driver_names);

  vector<string> found_ports;

  // 登録ベース名毎に、ファイル名がないかの探索を行う
  for (vector<string>::iterator it = pimpl->base_names_.begin();
       it != pimpl->base_names_.end(); ++it) {

    // ディレクトリ名とファイル名との切り分け
    const char* path = it->c_str();
    const char* last_slash = strrchr(path, '/') + 1;

    string dir_name = it->substr(0, last_slash - path);
    vector<string> ports;
    searchFiles(ports, dir_name);

    size_t n = it->size();
    for (vector<string>::iterator fit = ports.begin();
	 fit != ports.end(); ++fit) {
      if (! fit->compare(0, n, *it)) {
	// マッチしたら、登録を行う
	pimpl->addFoundPorts(found_ports, *fit);
      }
    }
  }

  for (vector<string>::iterator it = found_ports.begin();
       it != found_ports.end(); ++it) {
    ports.push_back(*it);
    driver_names.push_back("");
  }

  return found_ports.size();
}
#endif


size_t FindComPorts::find(vector<string>& ports, bool all_ports)
{
  vector<string> driver_names_dummy;
  return find(ports, driver_names_dummy, all_ports);
}
