//  BMP
//  Copyright (C) 2005-2007 BMP development.
//
//  This program is free software; you can redistribute it and/or modify
//  it under the terms of the GNU General Public License Version 2
//  as published by the Free Software Foundation.
//
//  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.
//
//  --
//
//  The BMPx project hereby grants permission for non-GPL compatible GStreamer
//  plugins to be used and distributed together with GStreamer and BMPx. This
//  permission is above and beyond the permissions granted by the GPL license
//  BMPx is covered by.

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

#include <iostream>
#include <cstring>
#include <cstdio>
#include <string>

#include <gtkmm.h>
#include <glibmm.h>
#include <glibmm/i18n.h>

#include <dbus/dbus.h>
#include <dbus/dbus-glib.h>
#include <dbus/dbus-glib-lowlevel.h>

#include <libhal.h>
#include <libhal-storage.h>

#include <boost/format.hpp>
#include <boost/algorithm/string.hpp>

#include "core.hh"

#include "debug.hh"
#include "paths.hh"
#include "database.hh"
#include "uri.hh"
#include "hal++.hh"

#include "bmp/types/types-library.hh"

#include "hal.hh"

using namespace Glib;
using namespace Gtk;
using namespace Hal;
using namespace std;
using namespace Bmp::DB;

namespace
{

  const char* HALBus[] =
  {
    "<Unknown>",
    "IDE",
    "SCSI",
    "USB",
    "IEEE1394",
    "CCW"
  };

  struct HALDriveType
  {
    const char  * name;
    const char  * icon_default;
    const char  * icon_usb;
    const char  * icon_1394;
  };

  const HALDriveType drive_type_data[] =
  {
    { N_("Removable Disk"),       "device-removable.png",   "device-removable-usb.png", "device-removable-1394.png"   },
    { N_("Disk"),                 "device-harddrive.png",   "device-harddrive-usb.png", "device-harddrive-1394.png"   },
    { N_("CD-ROM"),               "device-cdrom.png",        NULL,                       NULL                         },
    { N_("Floppy"),               "device-floppy.png",       NULL,                       NULL                         }, 
    { N_("Tape"),                 "device-removable.png",    NULL,                       NULL                         },  
    { N_("CF Card"),              "device-cf-card.png",      NULL,                       NULL                         }, 
    { N_("MemoryStick (TM)"),     "device-ms.png",           NULL,                       NULL                         },  
    { N_("SmartMedia Card"),      "device-sm-card.png",      NULL,                       NULL                         },  
    { N_("SD/MMC Card"),          "device-sdmmc-card.png",   NULL,                       NULL                         },  
    { N_("Camera"),               "device-camera.png",       NULL,                       NULL                         },  
    { N_("Portable Player"),      "device-ipod.png",         NULL,                       NULL                         },   
    { N_("ZIP Drive"),            "device-floppy.png",       NULL,                       NULL                         },    
    { N_("JAZ Drive"),            "device-floppy.png",       NULL,                       NULL                         },     
    { N_("FlashKey"),             "device-removable.png",    NULL,                       NULL                         },     
  };
}

namespace Bmp
{
      AttributeInfo
      volume_attributes[] = 
      {
        {
          N_("Volume UDI"),
          "volume_udi",
          DB::VALUE_TYPE_STRING,
          true	
        },
        {
          N_("Device UDI"),
          "device_udi",
          DB::VALUE_TYPE_STRING,
      	  true	
        },
        {
          N_("Mount Path"),
          "mount_point",
          DB::VALUE_TYPE_STRING,
      	  true	
        },
        {
          N_("Device Serial"),
          "drive_serial",
          DB::VALUE_TYPE_STRING,
      	  true	
        },
        {
          N_("Volume Name"),
          "label",
          DB::VALUE_TYPE_STRING,
      	  true	
        },
        {
          N_("Device File"),
          "device_node",
          DB::VALUE_TYPE_STRING,
      	  true	
        },
        {
          N_("Drive Bus"),
          "drive_bus",
          DB::VALUE_TYPE_INT,
      	  true	
        },
        {
          N_("Drive Type"),
          "drive_type",
          DB::VALUE_TYPE_INT,
      	  true	
        },
        {
          N_("Volume Size"),
          "size",
          DB::VALUE_TYPE_INT,
       	  true	
        },
        {
          N_("Drive Size"),
          "drive_size",
          DB::VALUE_TYPE_INT,
      	  true	
        },
        {
          N_("Drive Size"),
          "mount_time",
          DB::VALUE_TYPE_INT,
      	  true	
        },
      };

      enum VolumeAttribute
      {
        VATTR_VOLUME_UDI,
        VATTR_DEVICE_UDI,
        VATTR_MOUNT_PATH,
        VATTR_DEVICE_SERIAL,
        VATTR_VOLUME_NAME,
        VATTR_DEVICE_FILE,
        VATTR_DRIVE_BUS,         
        VATTR_DRIVE_TYPE,         
        VATTR_SIZE,         
        VATTR_DRIVE_SIZE,         
        VATTR_MOUNT_TIME,         
      };

      HAL::Volume::Volume ( Hal::RefPtr<Hal::Context> const& context,
                            Hal::RefPtr<Hal::Volume>  const& volume) throw ()
      {
        Hal::RefPtr<Hal::Drive> drive = Hal::Drive::create_from_udi (context, volume->get_storage_device_udi());

        volume_udi      = volume->get_udi();
        device_udi      = volume->get_storage_device_udi(); 

        size            = volume->get_size ();
        label           = volume->get_label(); 

        mount_time      = time( NULL ); 
        mount_point     = volume->get_mount_point(); 

        drive_bus       = drive->get_bus();
        drive_type      = drive->get_type();

#ifdef HAVE_HAL_058
        drive_size      = drive->get_size(); 
#else
        drive_size      = 0; 
#endif //HAVE_HAL_058

        drive_serial    = drive->get_serial(); 
        device_file     = volume->get_device_file(); 
      }


      HAL::Volume::Volume (Bmp::DB::Row const& row)
      {
        Row::const_iterator i;
        Row::const_iterator end = row.end();

        i = row.find (volume_attributes[VATTR_VOLUME_UDI].id);
        volume_udi = boost::get <string> (i->second).c_str();

        i = row.find (volume_attributes[VATTR_DEVICE_UDI].id);
        device_udi = boost::get <string> (i->second).c_str();

        i = row.find (volume_attributes[VATTR_DRIVE_BUS].id);
        drive_bus = Hal::DriveBus(boost::get <guint64> (i->second));

        i = row.find (volume_attributes[VATTR_DRIVE_TYPE].id);
        drive_type = Hal::DriveType (boost::get <guint64> (i->second));

        i = row.find (volume_attributes[VATTR_SIZE].id);
        size = boost::get <guint64> (i->second);

        i = row.find (volume_attributes[VATTR_DRIVE_SIZE].id);
        drive_size = boost::get <guint64> (i->second);

        i = row.find (volume_attributes[VATTR_MOUNT_TIME].id);
        mount_time = boost::get <guint64> (i->second);

        i = row.find (volume_attributes[VATTR_MOUNT_PATH].id);
        if (i != end)
          mount_point = boost::get <string> (i->second).c_str();

        i = row.find (volume_attributes[VATTR_DEVICE_SERIAL].id);
        if (i != end)
          drive_serial = boost::get <string> (i->second).c_str();

        i = row.find (volume_attributes[VATTR_VOLUME_NAME].id);
        if (i != end)
          label = boost::get <string> (i->second).c_str();

        i = row.find (volume_attributes[VATTR_DEVICE_FILE].id);
        if (i != end)
          device_file = boost::get <string> (i->second).c_str();
      }

      ///////////////////////////////////////////////////////

      void
      HAL::register_update_volume (Volume const& volume)
      {
        static boost::format insert_volume_f
        ("INSERT INTO hal_volumes (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) VALUES ('%s', '%s', '%s', '%s', '%s', '%s', '%llu', '%llu', '%llu', '%llu', '%llu');");

        m_hal_db->exec_sql ((insert_volume_f
          % volume_attributes[VATTR_VOLUME_UDI].id
          % volume_attributes[VATTR_DEVICE_UDI].id
          % volume_attributes[VATTR_MOUNT_PATH].id
          % volume_attributes[VATTR_DEVICE_SERIAL].id
          % volume_attributes[VATTR_VOLUME_NAME].id
          % volume_attributes[VATTR_DEVICE_FILE].id
          % volume_attributes[VATTR_DRIVE_BUS].id
          % volume_attributes[VATTR_DRIVE_TYPE].id
          % volume_attributes[VATTR_SIZE].id
          % volume_attributes[VATTR_DRIVE_SIZE].id
          % volume_attributes[VATTR_MOUNT_TIME].id
          % volume.volume_udi
          % volume.device_udi
          % volume.mount_point
          % volume.drive_serial
          % volume.label
          % volume.device_file
          % guint64(volume.drive_bus)
          % guint64(volume.drive_type)
          % guint64(volume.size)
          % guint64(volume.drive_size)
          % guint64(volume.mount_time)).str());
      }

      void
      HAL::volumes (VVolumes & volumes) const
      {
        RowV rows;
        m_hal_db->get ("hal_volumes", rows);

        for (RowV::const_iterator i = rows.begin(); i != rows.end(); ++i)
        {
          Volume volume (*i);
          volumes.push_back (volume);
        }
      }

      HAL::HAL ()
      {
        m_initialized = hal_init ();
        if (!m_initialized)
        {
          MessageDialog dialog (_("HAL could not be accessed from BMP. This is a fatal error. BMP can not continue "
                                  "to run without HAL accessible."), false, MESSAGE_ERROR, BUTTONS_OK, true);
          dialog.run();
          dialog.hide();
          throw NotInitializedError();
        }
      }

      void
      HAL::cdrom_policy (Hal::RefPtr<Hal::Volume> const& volume)
      {
        Hal::RefPtr<Hal::Drive> drive = Hal::Drive::create_from_udi (m_context, volume->get_storage_device_udi ());

        if (drive->property_exists  ("info.locked") &&
            drive->get_property <bool> ("info.locked"))
        {
          debug ("hal", "drive with udi %s is locked through hal; skipping policy", volume->get_udi().c_str());
          return;
        }

        Hal::DriveType drive_type; 

        try{
          drive_type = drive->get_type ();
          debug ("hal", "Drive type is %d", int (drive_type));
          }
        catch (...)
          {
            debug ("hal", "cannot get drive_type for %s", volume->get_storage_device_udi().c_str());
            return;
          }

        if (drive_type == Hal::DRIVE_TYPE_CDROM)
        {
            Hal::DiscProperties props; 

            try{
              props = volume->get_disc_properties();
              debug ("hal", "Disc properties are %d", int (props)); 
              }
            catch (...)
              {
                debug ("hal", "Couldn't get disc properties for udi '%s'", volume->get_udi().c_str());
                return;
              }

            std::string device_udi (volume->get_storage_device_udi());

            if (props & Hal::DISC_HAS_AUDIO)
            {
              if (!(props & Hal::DISC_HAS_DATA))
              {
                debug ("hal", "CD is CDDA and has no data: %s at %s", volume->get_udi().c_str(), device_udi.c_str());
              }
              else
              {
                debug ("hal", "CD is CDDA/Mixed-Mode: %s at %s", volume->get_udi().c_str(), device_udi.c_str());
              }
              signal_cdda_inserted_.emit (volume->get_udi(), volume->get_device_file());
            }
            else
            if (props & Hal::DISC_HAS_DATA)
            {
              debug ("hal", "CD/DVD is data-only medium");
            }
            else
            if (props & Hal::DISC_IS_BLANK)
            {
              debug ("hal", "CD is a blank medium");
            }
        }
      }

      void
      HAL::process_volume (std::string const &udi)
      {
        debug ("hal", "Processing volume with UDI '%s'", udi.c_str());

        Hal::RefPtr<Hal::Volume> volume = Hal::Volume::create_from_udi (m_context, udi);

        if (volume->get_device_file().empty())
        {
          debug ("hal", "Cannot get device file: %s", udi.c_str()); 
          return;
        }

        if (volume->is_disc())
        {
          cdrom_policy (volume);
          return;
        }
      }

      Glib::ustring
      HAL::get_volume_type (Volume const& volume) const
      {
        return Glib::ustring (drive_type_data[guint(volume.drive_type)].name);
      }
 
      Glib::ustring
      HAL::get_volume_drive_bus (Volume const& volume) const
      {
        return Glib::ustring (HALBus[volume.drive_bus]);
      }

      bool
      HAL::volume_is_mounted  (VolumeKey const& volume_key) const
      {
        try{
            return (m_volumes_mounted.find (volume_key)->second);
          }
        catch (...)
          {
            throw InvalidVolumeSpecifiedError (volume_key.first);
          }
      }

      bool
      HAL::volume_is_mounted  (Volume const& volume) const
      {
        try{
            Hal::RefPtr<Hal::Volume> volume_hal_instance = Hal::Volume::create_from_udi (m_context, volume.volume_udi);
            return volume_hal_instance->is_mounted();
          }
        catch (...)
          {
            throw InvalidVolumeSpecifiedError (volume.volume_udi.c_str());
          }
      }

      HAL::Volume
      HAL::get_volume_for_uri (Glib::ustring const& uri) const
      {
        string filename (filename_from_uri (uri));
        string::size_type l1 = 0;
        Volumes::const_iterator m = m_volumes.end();

        for (Volumes::const_iterator i = m_volumes.begin (); i != m_volumes.end (); ++i)
        {
          std::string::size_type l2 (i->second.first.mount_point.length());
          if ((i->second.first.mount_point == filename.substr (0, l2)) && (l2 > l1))
          {
            m = i;
            l1 = l2;
          } 
	      }

        if (m != m_volumes.end())
          return m->second.first; 
        else
          throw NoVolumeForUriError ((boost::format ("No volume for Uri %s") % uri.c_str()).str());
      }

      std::string
      HAL::get_mount_point_for_volume (std::string const& volume_udi, std::string const& device_udi) const
      {
        VolumeKey match (volume_udi, device_udi);
        Volumes::const_iterator v_iter = m_volumes.find (match);

        if (v_iter != m_volumes.end())
          return v_iter->second.first.mount_point; 
 
        throw NoMountPathForVolumeError ((boost::format ("No mount path for given volume: device: %s, volume: %s") % device_udi % volume_udi).str());
      }

      void
      HAL::process_udi (std::string const& udi)
      {
        debug ("hal", "Processing UDI '%s'", udi.c_str());  

        Hal::RefPtr <Hal::Volume> volume_hal_instance;

        try{
          volume_hal_instance = Hal::Volume::create_from_udi (m_context, udi);
          }
        catch (...)
          {
            debug ("hal", "Error constructing Hal::Volume from UDI: %s", udi.c_str());
            return;
          }

        if (!(volume_hal_instance->get_fsusage() == Hal::VOLUME_USAGE_MOUNTABLE_FILESYSTEM))
          return;

        Volume volume (m_context, volume_hal_instance);
        VolumeKey volume_key (volume.volume_udi, volume.device_udi);

        VolumesMounted::iterator i (m_volumes_mounted.find (volume_key));
        if (i != m_volumes_mounted.end())
          m_volumes_mounted.erase (i);
        m_volumes_mounted.insert (make_pair (volume_key, volume_hal_instance->is_mounted()));

        if (volume_hal_instance->is_mounted())
          signal_volume_added_.emit (volume);
        else
          signal_volume_removed_.emit (volume);

        try {

            Volumes::iterator i (m_volumes.find (volume_key));
            if (i != m_volumes.end())
              return;

            debug ("hal", "Registering 'volume' cap device with udi '%s'", volume.volume_udi.c_str());

            m_volumes.insert (make_pair (volume_key, StoredVolume (volume, volume_hal_instance)));

            if (!volume_hal_instance->is_disc())
            {
              register_update_volume (volume);  // we don't store information for discs permanently
            }

            Hal::RefPtr<Hal::Volume> const& vol (m_volumes.find (volume_key)->second.second);

            vol->signal_condition().connect
              (sigc::mem_fun (this, &Bmp::HAL::device_condition));
            vol->signal_property_modified().connect
              (sigc::mem_fun (this, &Bmp::HAL::device_property));

            signal_volume_added_.emit (volume); 
          }
        catch (...) {}
      }

      bool
      HAL::hal_init ()
      {
        DBusError error;
        dbus_error_init (&error);

        DBusConnection * c = dbus_bus_get (DBUS_BUS_SYSTEM, &error);

        if (dbus_error_is_set (&error))
        {
          debug ("hal", G_STRLOC ": Error conntecting to DBus.");
          dbus_error_free (&error);
          return false;
        }

        dbus_connection_setup_with_g_main (c, NULL);
        dbus_connection_set_exit_on_disconnect (c, FALSE);

        try{
          m_context = Hal::Context::create (c);
          }
        catch (...) 
          {
            debug ("hal", G_STRLOC ": Unable to create a Hal::Context");
            return false;
          }

        try{
          m_context->signal_device_added().connect
            (sigc::mem_fun (this, &Bmp::HAL::device_added));
          m_context->signal_device_removed().connect
            (sigc::mem_fun (this, &Bmp::HAL::device_removed));
          }
        catch (...)
          {
            debug ("hal", G_STRLOC ": Not able to connect device-added/device-removed signals.");
            m_context.clear();
            return false;
          }

        Hal::StrV list;
        try{
          list = m_context->find_device_by_capability ("volume");
          }
        catch (...)
          {
            debug ("hal", G_STRLOC ": Hal::Context::find_device_by_capability() failed");
            return false;
          }

        m_hal_db = new ::Bmp::DB::Database ("hal", BMP_PATH_DATA_DIR, ::Bmp::DB::DB_OPEN);

        try {

            static boost::format sql_create_table_f
              ("CREATE TABLE IF NOT EXISTS %s (%s, PRIMARY KEY ('%s','%s') ON CONFLICT REPLACE);");
            static boost::format sql_attr_f ("'%s'");
  
            std::string attrs;
            for (unsigned int n = 0; n < G_N_ELEMENTS(volume_attributes); ++n)
            {
              attrs.append((sql_attr_f % volume_attributes[n].id).str());

              switch (volume_attributes[n].type)
              {
                case VALUE_TYPE_STRING:
                    attrs.append (" TEXT ");
                    break;

                case VALUE_TYPE_INT:
                    attrs.append (" INTEGER DEFAULT 0 ");
                    break;

                default: break;
              }

              if (n < (G_N_ELEMENTS(volume_attributes)-1))
                attrs.append (", ");
            }

            m_hal_db->exec_sql ((sql_create_table_f
                                          % "hal_volumes"
                                          % attrs
                                          % volume_attributes[VATTR_DEVICE_UDI].id
                                          % volume_attributes[VATTR_VOLUME_UDI].id).str());
        }
        catch (Bmp::DB::SqlExceptionC & cxe)
        {
          abort ();
        }

        RowV rows;
        m_hal_db->get ("hal_volumes", rows);
        for (RowV::const_iterator i = rows.begin(); i != rows.end(); ++i)
        {
          Volume const& volume = *i;
          process_udi (volume.volume_udi);
        }

        for (Hal::StrV::const_iterator n = list.begin(); n != list.end(); ++n) 
        {
          process_udi (*n);
        }

        // Get all optical devices
      
        try{
          list = m_context->find_device_by_capability ("storage.cdrom");
          }
        catch (...)
          {
            debug ("hal", G_STRLOC ": Hal::Context::find_device_by_capability() failed");
            return false;
          }

        for (Hal::StrV::const_iterator n = list.begin(); n != list.end(); ++n) 
        {
            std::string product, dev; 
            try{
                Hal::RefPtr<Hal::Device> device = Hal::Device::create(m_context, *n);
                product = device->get_property<std::string>("info.product");
                dev = device->get_property<std::string>("block.device");
                debug ("hal", G_STRLOC ": Optical device '%s' at device '%s'", product.c_str(), dev.c_str());
               } 
            catch (...)
               {
                debug ("hal", G_STRLOC ": Couldn't create device for UDI '%s'", product.c_str());
                return false;
               }
        }

        return true;
      }

      HAL::~HAL ()
      {
        delete m_hal_db;
      }

      void
      HAL::device_condition  (std::string const& udi,
                              std::string const& cond_name,
                              std::string const& cond_details)
      {
        if (cond_name == "EjectPressed")
          {
            signal_ejected_.emit (udi);
            debug ("hal", "ejected: %s", udi.c_str());
          }
      }

      void
      HAL::device_added (std::string const& udi)
      {
        debug ("hal", "Device added: '%s'", udi.c_str());

        try{
            if (m_context->device_query_capability (udi, "volume"))
            {
              process_volume (udi);       
            }
          }
        catch (...)
          {
            debug ("hal", "Error in device-added signal handler!");
          }
      }

      void
      HAL::device_removed (std::string const& udi) 
      {
        signal_device_removed_.emit (udi);
        debug ("hal", "device removed: %s", udi.c_str());
      }

      void
      HAL::device_property	
          (std::string const& udi,
           std::string const& key,
           bool               is_removed,
           bool               is_added)
      {
        try{
		if (!m_context->device_query_capability (udi, "volume"))
		{
		  debug ("hal", "'%s' does not have 'volume' capability!");
		  return;
		}
	    }
	catch (Hal::Device::UnableToProbeDeviceError & cxe)
	    {
		  return;
 	    }

	try{
		Hal::RefPtr<Hal::Volume> volume = Hal::Volume::create_from_udi (m_context, udi);
		debug ("hal", "'%s' is %s", udi.c_str(), (volume->is_mounted()?"mounted":"not mounted"));
		process_udi (udi);
	   
 	   }
	catch (Hal::Device::DeviceDoesNotExistError & cxe)
	   {
		return;
	   }
      }

      HAL::SignalVolume&
      HAL::signal_volume_removed ()
      {
        return signal_volume_removed_;
      }

      HAL::SignalVolume&
      HAL::signal_volume_added ()
      {
        return signal_volume_added_;
      }

      HAL::SignalDeviceRemoved&
      HAL::signal_device_removed ()
      {
        return signal_device_removed_;
      }

      HAL::SignalCDDAInserted&
      HAL::signal_cdda_inserted ()
      {
        return signal_cdda_inserted_;
      }

      HAL::SignalEjected&
      HAL::signal_ejected ()
      {
        return signal_ejected_;
      }
}
