/* usb.c - USB Hub Support.  */
/*
 *  GRUB  --  GRand Unified Bootloader
 *  Copyright (C) 2008  Free Software Foundation, Inc.
 *
 *  GRUB 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 3 of the License, or
 *  (at your option) any later version.
 *
 *  GRUB 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 GRUB.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <grub/dl.h>
#include <grub/mm.h>
#include <grub/usb.h>
#include <grub/misc.h>

/* USB Supports 127 devices, with device 0 as special case.  */
static struct grub_usb_device *grub_usb_devs[128];

/* Add a device that currently has device number 0 and resides on
   CONTROLLER, the Hub reported that the device speed is SPEED.  */
static grub_usb_device_t
grub_usb_hub_add_dev (grub_usb_controller_t controller, grub_usb_speed_t speed)
{
  grub_usb_device_t dev;
  int i;

  dev = grub_malloc (sizeof (struct grub_usb_device));
  if (! dev)
    return NULL;

  dev->controller = *controller;
  dev->addr = 0;
  dev->initialized = 0;
  dev->speed = speed;

  grub_usb_device_initialize (dev);

  /* Assign a new address to the device.  */
  for (i = 1; i < 128; i++)
    {
      if (! grub_usb_devs[i])
	break;
    }
  if (grub_usb_devs[i])
    {
      grub_error (GRUB_ERR_IO, "Can't assign address to USB device");
      return NULL;
    }

  grub_usb_control_msg (dev,
			(GRUB_USB_REQTYPE_OUT
			 | GRUB_USB_REQTYPE_STANDARD
			 | GRUB_USB_REQTYPE_TARGET_DEV),
			GRUB_USB_REQ_SET_ADDRESS,
			i, 0, 0, NULL);
  dev->addr = i;
  dev->initialized = 1;
  grub_usb_devs[i] = dev;

  return dev;
}


static grub_err_t
grub_usb_add_hub (grub_usb_device_t dev)
{
  struct grub_usb_usb_hubdesc hubdesc;
  grub_err_t err;
  int i;

  grub_usb_control_msg (dev, (GRUB_USB_REQTYPE_IN
			      | GRUB_USB_REQTYPE_CLASS
			      | GRUB_USB_REQTYPE_TARGET_DEV),
			GRUB_USB_REQ_GET_DESCRIPTOR,
			(GRUB_USB_DESCRIPTOR_HUB << 8) | 0,
			0, sizeof (hubdesc), (char *) &hubdesc);

  /* Iterate over the Hub ports.  */
  for (i = 1; i <= hubdesc.portcnt; i++)
    {
      grub_uint32_t status;

      /* Get the port status.  */
      err = grub_usb_control_msg (dev, (GRUB_USB_REQTYPE_IN
					| GRUB_USB_REQTYPE_CLASS
					| GRUB_USB_REQTYPE_TARGET_OTHER),
				  GRUB_USB_REQ_HUB_GET_PORT_STATUS,
				  0, i, sizeof (status), (char *) &status);

      /* Just ignore the device if the Hub does not report the
	 status.  */
      if (err)
	continue;

      /* If connected, reset and enable the port.  */
      if (status & GRUB_USB_HUB_STATUS_CONNECTED)
	{
	  grub_usb_speed_t speed;

	  /* Determine the device speed.  */
	  if (status & GRUB_USB_HUB_STATUS_LOWSPEED)
	    speed = GRUB_USB_SPEED_LOW;
	  else
	    {
	      if (status & GRUB_USB_HUB_STATUS_HIGHSPEED)
		speed = GRUB_USB_SPEED_HIGH;
	      else
		speed = GRUB_USB_SPEED_FULL;
	    }

	  /* A device is actually connected to this port, not enable
	     the port.  XXX: Why 0x03?  According to some docs it
	     should be 0x0.  Check the specification!  */
	  err = grub_usb_control_msg (dev, (GRUB_USB_REQTYPE_OUT
					    | GRUB_USB_REQTYPE_CLASS
					    | GRUB_USB_REQTYPE_TARGET_OTHER),
				      0x3, 0x4, i, 0, 0);

	  /* If the Hub does not cooperate for this port, just skip
	     the port.  */
	  if (err)
	    continue;

	  /* Add the device and assign a device address to it.  */
	  grub_usb_hub_add_dev (&dev->controller, speed);
	}
    }

  return GRUB_ERR_NONE;
}

grub_usb_err_t
grub_usb_root_hub (grub_usb_controller_t controller)
{
  grub_err_t err;
  int ports;
  int i;

  /* Query the number of ports the root Hub has.  */
  ports = controller->dev->hubports (controller);

  for (i = 0; i < ports; i++)
    {
      grub_usb_speed_t speed = controller->dev->detect_dev (controller, i);

      if (speed != GRUB_USB_SPEED_NONE)
	{
	  grub_usb_device_t dev;

	  /* Enable the port.  */
	  err = controller->dev->portstatus (controller, i, 1);
	  if (err)
	    continue;

	  /* Enable the port and create a device.  */
	  dev = grub_usb_hub_add_dev (controller, speed);
	  if (! dev)
	    continue;

	  /* If the device is a Hub, scan it for more devices.  */
	  if (dev->descdev.class == 0x09)
	    grub_usb_add_hub (dev);
	}
    }

  return GRUB_USB_ERR_NONE;
}

int
grub_usb_iterate (int (*hook) (grub_usb_device_t dev))
{
  int i;

  for (i = 0; i < 128; i++)
    {
      if (grub_usb_devs[i])
	{
	  if (hook (grub_usb_devs[i]))
	      return 1;
	}
    }

  return 0;
}
