/***************************************************************************
 * CVSID: $Id$
 *
 * hf-storage.c : storage device support
 *
 * Copyright (C) 2006 Jean-Yves Lefort <jylefort@FreeBSD.org>
 *
 * 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., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 **************************************************************************/

#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif

#include <string.h>
#include <sys/types.h>
#include <libdisk.h>
#include <sys/disklabel.h>

#include "../logger.h"
#include "../osspec.h"

#include "hf-storage.h"
#include "hf-block.h"
#include "hf-devtree.h"
#include "hf-volume.h"
#include "hf-util.h"

typedef struct
{
  char *name;
  char *conf;
} Disk;

/* Disk_Names() has a memory leak, hence this */
static char **
hf_storage_get_disk_names (GError **err)
{
  char *list;
  char **names;

  list = hf_get_string_sysctl(err, "kern.disks");
  if (! list)
    return NULL;

  names = g_strsplit(list, " ", 0);
  g_free(list);

  return names;
}

static gboolean
hf_storage_disk_exists (const char *diskname)
{
  char **disks;
  gboolean exists;

  g_return_val_if_fail(diskname != NULL, FALSE);

  disks = hf_storage_get_disk_names(NULL);
  if (! disks)
    return FALSE;

  exists = hf_strv_find(disks, diskname) != -1;
  g_strfreev(disks);

  return exists;
}

static gboolean
hf_storage_chunk_has_partitions (const struct chunk *chunk)
{
  return chunk
    && ((chunk->type != whole && chunk->type != unused)
	|| hf_storage_chunk_has_partitions(chunk->part)
	|| hf_storage_chunk_has_partitions(chunk->next));
}

static gboolean
hf_storage_chunk_is_swap (const struct chunk *chunk)
{
  g_return_val_if_fail(chunk != NULL, FALSE);

  return (chunk->type == part && chunk->subtype == FS_SWAP)
    || (chunk->type == mbr
	&& (chunk->subtype == 0x18		/* AST Windows swapfile */
	    || chunk->subtype == 0x42		/* SFS or Linux swap */
	    || chunk->subtype == 0x82		/* Linux swap or Solaris x86 */
	    || chunk->subtype == 0xB8));	/* BSDI BSD/386 swap */
}

static void
hf_storage_device_probe_chunk (HalDevice *parent,
			       HalDevice *storage_device,
			       const struct chunk *chunk)
{
  HalDevice *device = NULL;

  g_return_if_fail(HAL_IS_DEVICE(parent));
  g_return_if_fail(HAL_IS_DEVICE(storage_device));

  if (! chunk)
    return;

  if (chunk->type != whole && chunk->type != unused)
    {
      char *special;

      special = g_strdup_printf("/dev/%s", chunk->name);
      device = hal_device_store_match_key_value_string(hald_get_gdl(), "block.device", special);
      g_free(special);

      if (! device)
	device = hf_volume_device_add(parent,
				      storage_device,
				      hf_storage_chunk_has_partitions(chunk->part),
				      hf_storage_chunk_is_swap(chunk),
				      chunk->name,
				      chunk->offset * chunk->disk->sector_size,
				      chunk->size * chunk->disk->sector_size);
    }

  if (! device || ! hal_device_property_get_bool(device, "info.ignore"))
    hf_storage_device_probe_chunk(device ? device : parent, storage_device, chunk->part);
  hf_storage_device_probe_chunk(parent, storage_device, chunk->next);
}

static void
hf_storage_device_probe_partitions (HalDevice *device)
{
  char *diskname;

  g_return_if_fail(HAL_IS_DEVICE(device));

  diskname = hf_devtree_device_get_name(device);
  if (! diskname)
    return;

  if (hf_storage_disk_exists(diskname))	/* otherwise Open_Disk() crashes */
    {
      struct disk *disk;

      disk = Open_Disk(diskname);
      if (disk)
	{
	  hf_storage_device_probe_chunk(device, device, disk->chunks);
	  Free_Disk(disk);
	}
    }
  g_free(diskname);
}

static gboolean
hf_storage_device_has_partitions (HalDevice *device)
{
  gboolean has = FALSE;
  char *diskname;

  g_return_val_if_fail(HAL_IS_DEVICE(device), FALSE);

  diskname = hf_devtree_device_get_name(device);
  if (! diskname)
    return FALSE;

  if (hf_storage_disk_exists(diskname))	/* otherwise Open_Disk() crashes */
    {
      struct disk *disk;

      disk = Open_Disk(diskname);
      if (disk)
	{
	  if (hf_storage_chunk_has_partitions(disk->chunks))
	    has = TRUE;

	  Free_Disk(disk);
	}
    }
  g_free(diskname);

  return has;
}

static HalDevice *
hf_storage_device_new (HalDevice *parent, const char *diskname)
{
  HalDevice *device;

  g_return_val_if_fail(diskname != NULL, NULL);

  device = hf_device_new(parent);

  hf_devtree_device_set_name(device, diskname);

  hf_block_device_enable(device, diskname);
  hf_storage_device_enable(device);
  hf_block_device_complete(device, device, FALSE);

  return device;
}

void
hf_storage_device_probe (HalDevice *device, gboolean only_media)
{
  g_return_if_fail(HAL_IS_DEVICE(device));

  if (hf_runner_run_sync(device, 0, "hald-probe-storage",
			 "HF_HAS_CHILDREN", HF_BOOL_TO_STRING(hf_storage_device_has_partitions(device)),
			 "HF_ONLY_CHECK_FOR_MEDIA", HF_BOOL_TO_STRING(only_media),
			 NULL) == 2)
    {				/* add a child volume */
      char *devname;

      devname = hf_devtree_device_get_name(device);
      g_assert(devname != NULL);

      /*
       * We do not need to check if the device already exists:
       *
       *   - if we're called from osspec_device_rescan() or
       *     osspec_device_reprobe(), the child device has been removed.
       *   - otherwise, the storage device is new and has no child.
       */

      hf_volume_device_add(device, device, FALSE, FALSE, devname, 0, 0);
      g_free(devname);
    }
  else				/* probe partitions */
    hf_storage_device_probe_partitions(device);
}

static void
hf_storage_probe (void)
{
  GError *err = NULL;
  char **disks;

  /* add disks which have not been handled by hf-ata or hf-scsi */

  disks = hf_storage_get_disk_names(&err);
  if (disks)
    {
      int i;

      for (i = 0; disks[i]; i++)
	{
	  HalDevice *device;

	  device = hf_devtree_find_from_name(hald_get_gdl(), disks[i]);
	  if (! device)
	    {
	      HalDevice *parent;

	      /* device not found, add a generic storage device */

	      parent = hf_devtree_find_parent_from_name(hald_get_gdl(), disks[i]);
	      if (! parent || ! hal_device_property_get_bool(parent, "info.ignore"))
		{
		  device = hf_storage_device_new(parent, disks[i]);
		  hf_storage_device_add(device);
		}
	    }
	}

      g_strfreev(disks);
    }
  else
    {
      HAL_WARNING(("unable to get disk list: %s", err->message));
      g_error_free(err);
    }
}

static GSList *
hf_storage_parse_conftxt (const char *conftxt)
{
  GSList *disks = NULL;
  char **lines;
  Disk *disk = NULL;
  GString *disk_conf = NULL;
  int i;

  if (! conftxt)
    return NULL;

  lines = g_strsplit(conftxt, "\n", 0);
  for (i = 0; lines[i]; i++)
    {
      char **fields;

      if (! *lines[i])
	continue;

      fields = g_strsplit(lines[i], " ", 0);
      if (g_strv_length(fields) >= 3 && ! strcmp(fields[1], "DISK"))
	{
	  if (disk)
	    disk->conf = g_string_free(disk_conf, FALSE);

	  disk = g_new(Disk, 1);
	  disk->name = g_strdup(fields[2]);
	  disk_conf = g_string_new(NULL);
	  disks = g_slist_prepend(disks, disk);
	}
      g_strfreev(fields);

      if (disk)
	{
	  if (*disk_conf->str)
	    g_string_append_c(disk_conf, '\n');
	  g_string_append(disk_conf, lines[i]);
	}
    }
  g_strfreev(lines);

  if (disk)
    disk->conf = g_string_free(disk_conf, FALSE);

  return disks;
}

static Disk *
hf_storage_find_disk (const GSList *disks, const char *name)
{
  const GSList *l;

  HF_LIST_FOREACH(l, disks)
    {
      Disk *disk = l->data;

      if (! strcmp(disk->name, name))
	return disk;
    }

  return NULL;
}

static void
hf_storage_disk_free (Disk *disk)
{
  g_return_if_fail(disk != NULL);

  g_free(disk->name);
  g_free(disk->conf);
  g_free(disk);
}

static void
hf_storage_device_rescan_real (HalDevice *device)
{
  g_return_if_fail(HAL_IS_DEVICE(device));

  /* remove all children */

  hf_device_remove_children(device);

  /* rescan */

  hf_storage_device_probe(device, TRUE);
}

static gboolean
hf_storage_conftxt_timeout_cb (gpointer data)
{
  static GSList *disks = NULL;
  static gboolean first = TRUE;
  char *conftxt;
  GSList *new_disks;

  if (hf_is_waiting)
    return TRUE;

  conftxt = hf_get_string_sysctl(NULL, "kern.geom.conftxt");
  new_disks = hf_storage_parse_conftxt(conftxt);
  g_free(conftxt);

  /* if this is not the initial check, handle changes */

  if (first)
    first = FALSE;
  else
    {
      GSList *l;

      /* check for new disks */

      HF_LIST_FOREACH(l, new_disks)
	{
	  Disk *disk = l->data;

	  if (! hf_storage_find_disk(disks, disk->name))
	    {
	      osspec_probe();	/* catch new disk(s) */
	      break;
	    }
	}

      /* check for disks which have changed or have been removed */

      HF_LIST_FOREACH(l, disks)
	{
	  Disk *disk = l->data;
	  Disk *new_disk;
	  HalDevice *device;

	  new_disk = hf_storage_find_disk(new_disks, disk->name);
	  if (new_disk)
	    {
	      if (strcmp(disk->conf, new_disk->conf))
		{
		  /* disk changed */
		  device = hf_devtree_find_from_name(hald_get_gdl(), disk->name);
		  if (device)
		    {
		      g_assert(hal_device_has_capability(device, "storage"));
		      hf_storage_device_rescan_real(device);
		    }
		}
	    }
	  else
	    {
	      /* disk removed */
	      device = hf_devtree_find_from_name(hald_get_gdl(), disk->name);
	      if (device)
		{
		  g_assert(hal_device_has_capability(device, "storage"));
		  hf_device_remove_tree(device);
		}
	    }
	}
    }

  g_slist_foreach(disks, (GFunc) hf_storage_disk_free, NULL);
  g_slist_free(disks);
  disks = new_disks;

  return TRUE;
}

static void
hf_storage_init (void)
{
  g_timeout_add(3000, hf_storage_conftxt_timeout_cb, NULL);
}

void
hf_storage_device_enable (HalDevice *device)
{
  g_return_if_fail(HAL_IS_DEVICE(device));
  g_return_if_fail(devname != NULL);

  hal_device_property_set_string(device, "storage.bus", "platform");
  hal_device_property_set_string(device, "storage.drive_type", "disk");

  hal_device_property_set_bool(device, "storage.removable", FALSE);
  hal_device_property_set_bool(device, "storage.requires_eject", FALSE);
  hal_device_property_set_bool(device, "storage.hotpluggable", FALSE);
  hal_device_property_set_bool(device, "storage.media_check_enabled", FALSE);
  hal_device_property_set_bool(device, "storage.automount_enabled_hint", TRUE);
  hal_device_property_set_bool(device, "storage.no_partitions_hint", FALSE);

  hal_device_property_set_string(device, "storage.physical_device", NULL);
  hal_device_property_set_string(device, "storage.model", NULL);
  hal_device_property_set_string(device, "storage.vendor", NULL);

  hal_device_add_capability(device, "storage");
  hal_device_property_set_string(device, "info.category", "storage");
}

void
hf_storage_device_enable_tape (HalDevice *device)
{
  g_return_if_fail(HAL_IS_DEVICE(device));

  hal_device_property_set_string(device, "storage.drive_type", "tape");
  hal_device_property_set_bool(device, "storage.removable", TRUE);
}

void
hf_storage_device_enable_cdrom (HalDevice *device)
{
  g_return_if_fail(HAL_IS_DEVICE(device));

  hal_device_add_capability(device, "storage.cdrom");
  hal_device_property_set_string(device, "info.category", "storage.cdrom");
  hal_device_property_set_string(device, "storage.drive_type", "cdrom");
  hal_device_property_set_bool(device, "storage.removable", TRUE);
  /* enable media checks */
  hal_device_property_set_bool(device, "storage.media_check_enabled", TRUE);
  /* CD-ROM discs most likely don't have a partition table */
  hal_device_property_set_bool(device, "storage.no_partitions_hint", TRUE);
  /* the linux backend sets this one */
  hal_device_property_set_bool(device, "storage.requires_eject", TRUE);

  /* some of these will be set by probe-storage */
  hal_device_property_set_bool(device, "storage.cdrom.cdr", FALSE);
  hal_device_property_set_bool(device, "storage.cdrom.cdrw", FALSE);
  hal_device_property_set_bool(device, "storage.cdrom.dvd", FALSE);
  hal_device_property_set_bool(device, "storage.cdrom.dvdr", FALSE);
  hal_device_property_set_bool(device, "storage.cdrom.dvdrw", FALSE);
  hal_device_property_set_bool(device, "storage.cdrom.dvdram", FALSE);
  hal_device_property_set_bool(device, "storage.cdrom.dvdplusr", FALSE);
  hal_device_property_set_bool(device, "storage.cdrom.dvdplusrw", FALSE);
  hal_device_property_set_bool(device, "storage.cdrom.dvdplusrdl", FALSE);
  hal_device_property_set_bool(device, "storage.cdrom.dvdplusrwdl", FALSE);
  hal_device_property_set_bool(device, "storage.cdrom.bd", FALSE);
  hal_device_property_set_bool(device, "storage.cdrom.bdr", FALSE);
  hal_device_property_set_bool(device, "storage.cdrom.bdre", FALSE);
  hal_device_property_set_bool(device, "storage.cdrom.hddvd", FALSE);
  hal_device_property_set_bool(device, "storage.cdrom.hddvdr", FALSE);
  hal_device_property_set_bool(device, "storage.cdrom.hddvdrw", FALSE);
  hal_device_property_set_bool(device, "storage.cdrom.support_media_changed", FALSE);
  hal_device_property_set_int(device, "storage.cdrom.read_speed", 0);
  hal_device_property_set_int(device, "storage.cdrom.write_speed", 0);
}

/* preprobe, probe and add */
void
hf_storage_device_add (HalDevice *device)
{
  g_return_if_fail(HAL_IS_DEVICE(device));

  if (hf_device_preprobe(device))
    {
      hf_storage_device_probe(device, FALSE);
      hf_device_add(device);
    }
}

static gboolean
hf_storage_device_rescan (HalDevice *device)
{
  if (hal_device_has_capability(device, "storage"))
    {
      hf_storage_device_rescan_real(device);
      return TRUE;
    }
  else
    return FALSE;
}

HFHandler hf_storage_handler = {
  .init =		hf_storage_init,
  .probe =		hf_storage_probe,
  .device_rescan =	hf_storage_device_rescan
};
