/* LIBUSB-WIN32, Generic Windows USB Driver
 * Copyright (C) 2002-2004 Stephan Meyer, <ste_meyer@web.de>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */


#include <windows.h>
#include <setupapi.h>
#include <stdio.h>
#include <ctype.h>

#define LIBUSB_DRIVER_NAME_NT "libusb0"
#define LIBUSB_DRIVER_NAME_9X "libusb0.sys"
#define REGISTRY_BUF_SIZE 512


void usb_insert_composite_filter(void);
void usb_restart_host_controllers(void);
void usb_start_host_controllers(void);
void usb_stop_host_controllers(void);

static int usb_is_nt();
static void usb_debug_error(const char *s, ...);
static int usb_get_property(DWORD which, HDEVINFO dev_info, 
		     SP_DEVINFO_DATA *dev_info_data,
		     char *buf, int size);
static int usb_set_property(DWORD which, HDEVINFO dev_info, 
		     SP_DEVINFO_DATA *dev_info_data, 
		     const char *buf);

static int usb_set_device_state(DWORD state, HDEVINFO dev_info, 
			 SP_DEVINFO_DATA *dev_info_data);

static int usb_insert_filter(HDEVINFO dev_info,
		      SP_DEVINFO_DATA *dev_info_data,
		      const char *filter_name);
static int usb_is_service_libusb(HDEVINFO dev_info, 
			     SP_DEVINFO_DATA *dev_info_data);
static int usb_is_composite_device(HDEVINFO dev_info, 
			       SP_DEVINFO_DATA *dev_info_data);


static int usb_is_nt()
{
  return GetVersion() < 0x80000000 ? 1 : 0;
}

static void usb_debug_error(const char *s, ...)
{
  char tmp[512];
  va_list args;
  
  memset(tmp, 0, sizeof(tmp));

  int offset = sprintf(tmp, "LIBUSB: error: ");
  va_start(args, s);
  vsnprintf(tmp + offset, sizeof(tmp) - offset - 1, s, args);
  va_end(args);
  printf("%s\n",tmp);

  OutputDebugString(tmp);
}

static int usb_get_property(DWORD which, HDEVINFO dev_info, 
		     SP_DEVINFO_DATA *dev_info_data,
		     char *buf, int size)
{
  DWORD reg_type;
  DWORD key_type;
  DWORD length = size;
  char *p = NULL;
  char *val_name = NULL;
  HKEY reg_key = NULL;
  
  memset(buf, 0, size);

  switch(which)
    {
    case SPDRP_LOWERFILTERS:
      val_name = "LowerFilters";
      key_type = DIREG_DEV; 
      break;
    case SPDRP_SERVICE:
      val_name = "NTMPDriver";
      key_type = DIREG_DRV;
      break;
    case SPDRP_CLASS:
      val_name = "Class";
      key_type = DIREG_DEV;    
      break;
    case SPDRP_HARDWAREID:
      val_name = "HardwareID";
      key_type = DIREG_DEV;    
      break;
    default:
      return 0;
    }

  if(usb_is_nt())
    {
      if(!SetupDiGetDeviceRegistryProperty(dev_info, dev_info_data, which,  
					   &reg_type, (BYTE *)buf, 
					   size, &length))
	{
	  return 0;
	}

      if(length <= 2)
	{
	  return 0;
	}
    }
  else /* Win9x */
    {
      reg_key = SetupDiOpenDevRegKey(dev_info, dev_info_data, 
				     DICS_FLAG_GLOBAL, 
				     0, key_type, KEY_ALL_ACCESS);
      
      if(reg_key == INVALID_HANDLE_VALUE)
	{
	  usb_debug_error("usb_get_property(): reading "
			  "registry key failed");
	  return 0;
	}
      
      if(RegQueryValueEx(reg_key, val_name, NULL, &reg_type, 
			 (BYTE *)buf, &length) != ERROR_SUCCESS
	 || length <= 2)
	{
	  RegCloseKey(reg_key);
	  return 0;
	}
    }

  p = buf;
  
  if((REG_MULTI_SZ == reg_type))
    {
      while(*p || *(p + 1))
	{
	  if(*p == 0)
	    {
	      *p = ',';
	    }
	  p++;
	}
    }
  
  p = buf;
  
  while(*p)
    {
      *p = tolower(*p);
      p++;
    }
  
  if(reg_key)
    {
      RegCloseKey(reg_key);
    }
  return 1;
}

static int usb_set_property(DWORD which, HDEVINFO dev_info, 
			    SP_DEVINFO_DATA *dev_info_data, const char *buf)
{
  char *val_name = NULL;
  HKEY reg_key;
  char tmp[REGISTRY_BUF_SIZE];
  DWORD reg_type;

  DWORD size = 0;

  switch(which)
    {
    case SPDRP_LOWERFILTERS:
      reg_type = usb_is_nt() ? REG_MULTI_SZ : REG_SZ;
      val_name = "LowerFilters";
      break;
    default:
      return 0;
    }

  memset(tmp, 0, sizeof(tmp));
  
  if(buf)
    {
      strncpy(tmp, buf, sizeof(tmp));
      size = strlen(tmp);
      size += reg_type == REG_MULTI_SZ ? 2 : 1;
    }

  if(usb_is_nt())
    {
      if(size > 3)
	{
	  if(!SetupDiSetDeviceRegistryProperty(dev_info, dev_info_data,
					       which, (BYTE *)tmp, size))
	    {
	      usb_debug_error("usb_set_property(): setting "
			      "property '%s' failed", val_name);
	      return 0;
	    }
	}
      else
	{
	  if(!SetupDiSetDeviceRegistryProperty(dev_info, dev_info_data,
					       which, NULL, 0))
	    {
	      usb_debug_error("usb_set_property(): deleting "
			      "property '%s' failed", val_name);
	      return 0;
	    }
	}
    }
  else
    {
      reg_key = SetupDiOpenDevRegKey(dev_info, dev_info_data, 
				     DICS_FLAG_GLOBAL, 
				     0, DIREG_DEV, KEY_ALL_ACCESS);
      
      if(reg_key == INVALID_HANDLE_VALUE)
	{
	  usb_debug_error("usb_set_property(): reading "
			  "registry key failed");
	  return 0;
	}
      
      if(size > 3)
	{
	  if(RegSetValueEx(reg_key, val_name, 0, reg_type, (BYTE *)buf, 
			   size) != ERROR_SUCCESS)
	    {
	      usb_debug_error("usb_set_property(): setting "
			      "property '%s' failed", val_name);
	      RegCloseKey(reg_key);
	      return 0;
	    }
	}
      else
	{
	  if(RegDeleteValue(reg_key, val_name) != ERROR_SUCCESS)
	    {
	      usb_debug_error("usb_set_property(): deleting "
			      "property '%s' failed", val_name);
	      RegCloseKey(reg_key);
	      return 0;
	    }
	}
      RegCloseKey(reg_key);
    }

  return 1;
}

static int usb_set_device_state(DWORD state, HDEVINFO dev_info, 
				SP_DEVINFO_DATA *dev_info_data)
{
  SP_PROPCHANGE_PARAMS params;
  
  memset(&params, 0, sizeof(SP_PROPCHANGE_PARAMS));
  
  params.ClassInstallHeader.cbSize = sizeof(SP_CLASSINSTALL_HEADER);
  params.ClassInstallHeader.InstallFunction = DIF_PROPERTYCHANGE;
  params.StateChange = state;
  params.Scope = DICS_FLAG_GLOBAL;
  params.HwProfile = 0;
  
  if(!SetupDiSetClassInstallParams(dev_info, dev_info_data,
				   (PSP_CLASSINSTALL_HEADER) &params,
				   sizeof(SP_PROPCHANGE_PARAMS)))
    {
      usb_debug_error("usb_set_device_state(): setting class "
		      "install parameters failed");
      return 0;
    }
	  
  if(!SetupDiCallClassInstaller(DIF_PROPERTYCHANGE, dev_info, 
				dev_info_data))
    {
      usb_debug_error("usb_set_device_state(): calling class "
		      "installer failed");
      return 0;
    }

  return 1;
}

static int usb_insert_filter(HDEVINFO dev_info, 
			     SP_DEVINFO_DATA *dev_info_data, 
			     const char *filter_name)
{
  return usb_set_property(SPDRP_LOWERFILTERS, dev_info, 
			  dev_info_data, filter_name);
}

static int usb_is_service_libusb(HDEVINFO dev_info, 
			  SP_DEVINFO_DATA *dev_info_data)
{
  char service_name[REGISTRY_BUF_SIZE];


  if(!usb_get_property(SPDRP_SERVICE, dev_info, dev_info_data,
		       service_name, sizeof(service_name)))
    {
      return 0;
    }
 
   
  if(strstr(service_name, LIBUSB_DRIVER_NAME_NT))
    {
      return 1;
    }

  return 0;
}

static int usb_is_composite_device(HDEVINFO dev_info, 
				   SP_DEVINFO_DATA *dev_info_data)
{
  HDEVINFO _dev_info;
  SP_DEVINFO_DATA _dev_info_data;
  int dev_index = 0;
  char parent[REGISTRY_BUF_SIZE];
  char tmp[REGISTRY_BUF_SIZE];
  
  memset(parent, 0, sizeof(parent));

  _dev_info_data.cbSize = sizeof(SP_DEVINFO_DATA);

  if(!usb_get_property(SPDRP_HARDWAREID, dev_info, dev_info_data,
		       tmp, sizeof(tmp)))
    {
      usb_debug_error("usb_is_composite_libusb(): getting hardware id failed");
      return 0;
    }

    
  if(strstr(tmp, "&mi_") || strstr(tmp, "root_hub"))
    {
      return 0;
    }

  strncpy(parent, tmp, sizeof("usb\\vid_xxxx&pid_yyyy") - 1);
  strcat(parent, "&mi_");

  _dev_info = SetupDiGetClassDevs(NULL, "USB", 0, DIGCF_ALLCLASSES);
  
  if(_dev_info == INVALID_HANDLE_VALUE)
    {
      usb_debug_error("usb_is_composite_device(): getting "
		      "device info set failed");
      
      return 0;
    }
      
  while(SetupDiEnumDeviceInfo(_dev_info, dev_index, &_dev_info_data))
    {
      if(!usb_get_property(SPDRP_HARDWAREID, _dev_info, 
				    &_dev_info_data, tmp, sizeof(tmp)))
	
	{
	  usb_debug_error("usb_is_composite_device(): getting "
			  "hardware id failed");
	  dev_index++;
	  continue;
	}
	
      if(strstr(tmp, parent)
	 && usb_is_service_libusb(_dev_info, &_dev_info_data))
	{
	  SetupDiDestroyDeviceInfoList(_dev_info);
	  return 1;
	}
      dev_index++;
    }

  SetupDiDestroyDeviceInfoList(_dev_info);
  return 0;
}

void set_host_controller_state(ULONG state)
{
  HDEVINFO dev_info;
  SP_DEVINFO_DATA dev_info_data;
  int dev_index = 0;
  char class[512];

  dev_info_data.cbSize = sizeof(SP_DEVINFO_DATA);
  dev_info = SetupDiGetClassDevs(NULL, "PCI", 0, 
				 DIGCF_ALLCLASSES | DIGCF_PRESENT);

  if(dev_info == INVALID_HANDLE_VALUE)
    {
      usb_debug_error("set_host_controller_state(): getting device info "
		      "failed");
      return;
    }

  while(SetupDiEnumDeviceInfo(dev_info, dev_index, &dev_info_data))
    {

      if(usb_get_property(SPDRP_CLASS, dev_info, &dev_info_data, class, sizeof(class)))
	//if(usb_get_property(SPDRP_SERVICE, dev_info, &dev_info_data, class, sizeof(class)))
	{
	  if(strstr(class, "usb"))
	    {
	      if(!usb_set_device_state(state, dev_info, 
				       &dev_info_data))
		{
		  usb_debug_error("set_host_controller_state: setting "
				  "device state failed");
		}
	    }
	}
      dev_index++;
    }

  SetupDiDestroyDeviceInfoList(dev_info);
}

void usb_insert_composite_filter(void)
{
  HDEVINFO dev_info;
  SP_DEVINFO_DATA dev_info_data;
  int dev_index = 0;
  char *filter_name = NULL;

  filter_name = usb_is_nt() ? 
    LIBUSB_DRIVER_NAME_NT : LIBUSB_DRIVER_NAME_9X;
  
  dev_info_data.cbSize = sizeof(SP_DEVINFO_DATA);

  dev_index = 0;
  dev_info = SetupDiGetClassDevs(NULL, "USB", 0, 
				 DIGCF_ALLCLASSES | DIGCF_PRESENT);
  
  if(dev_info == INVALID_HANDLE_VALUE)
    {
      usb_debug_error("usb_insert_composite_filter(): getting "
		      "device info set failed");
      
      return;
    }
  
  while(SetupDiEnumDeviceInfo(dev_info, dev_index, &dev_info_data))
    {
      if(usb_is_composite_device(dev_info, &dev_info_data))
	{
	  if(usb_insert_filter(dev_info, &dev_info_data, 
			       filter_name))
	    {
	      usb_set_device_state(DICS_PROPCHANGE, dev_info, 
				   &dev_info_data);
	    }
	}
      dev_index++;
    }

  SetupDiDestroyDeviceInfoList(dev_info);
}

void usb_restart_host_controllers(void)
{
  set_host_controller_state(DICS_PROPCHANGE);
}

void usb_start_host_controllers(void)
{
  set_host_controller_state(DICS_ENABLE);
}

void usb_stop_host_controllers(void)
{
  set_host_controller_state(DICS_DISABLE);
}


