#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <errno.h>

#include <Ecore.h>
#include <Eeze.h>
#include <Eeze_Disk.h>

#include "eeze_udev_private.h"
#include "eeze_disk_private.h"

#define EEZE_MOUNT_DEFAULT_OPTS "noexec,nosuid,utf8"

EAPI int EEZE_EVENT_DISK_MOUNT = 0;
EAPI int EEZE_EVENT_DISK_UNMOUNT = 0;
EAPI int EEZE_EVENT_DISK_EJECT = 0;
EAPI int EEZE_EVENT_DISK_ERROR = 0;
static Ecore_Event_Handler *_mount_handler = NULL;
Eina_List *eeze_events = NULL;

/*
 *
 * PRIVATE
 *
 */

static void
_eeze_disk_mount_error_free(void *data EINA_UNUSED, Eeze_Event_Disk_Error *de)
{
   if (!de)
     return;

   eina_stringshare_del(de->message);
   free(de);
}

static void
_eeze_disk_mount_error_handler(Eeze_Disk *disk, const char *error)
{
   Eeze_Event_Disk_Error *de;

   ERR("%s", error);
   if (!(de = calloc(1, sizeof(Eeze_Event_Disk_Error))))
     return;

   de->disk = disk;
   de->message = eina_stringshare_add(error);
   /* FIXME: placeholder since currently there are only mount-type errors */
   ecore_event_add(EEZE_EVENT_DISK_ERROR, de, (Ecore_End_Cb)_eeze_disk_mount_error_free, NULL);
}

static Eina_Bool
_eeze_disk_mount_result_handler(void *data EINA_UNUSED, int type EINA_UNUSED, Ecore_Exe_Event_Del *ev)
{
   Eeze_Disk *disk;
   Eina_List *l;
   Eeze_Event_Disk_Mount *e;

   if ((!ev) || (!ev->exe))
     return ECORE_CALLBACK_RENEW;
   disk = ecore_exe_data_get(ev->exe);

   if ((!disk) || (!eeze_events) || (!(l = eina_list_data_find_list(eeze_events, disk))))
     return ECORE_CALLBACK_RENEW;

   eeze_events = eina_list_remove_list(eeze_events, l);
   if (!disk->mounter) /* killed */
     {
        disk->mount_status = EEZE_DISK_NULL;
        return ECORE_CALLBACK_RENEW;
     }
   if (disk->mount_status == EEZE_DISK_MOUNTING)
     {
        disk->mounter = NULL;
        if (!ev->exit_code)
          {
              disk->mounted = EINA_TRUE;
              e = malloc(sizeof(Eeze_Event_Disk_Mount));
              EINA_SAFETY_ON_NULL_RETURN_VAL(e, ECORE_CALLBACK_RENEW);
              e->disk = disk;
              ecore_event_add(EEZE_EVENT_DISK_MOUNT, e, NULL, NULL);
          }
        else if (ev->exit_code & 2)
           _eeze_disk_mount_error_handler(disk, "system error (out of memory, cannot fork, no more loop devices)");
        else if (ev->exit_code & 4)
           _eeze_disk_mount_error_handler(disk, "internal mount bug");
        else if (ev->exit_code & 8)
           _eeze_disk_mount_error_handler(disk, "user interrupt");
        else if (ev->exit_code & 16)
           _eeze_disk_mount_error_handler(disk, "problems writing or locking /etc/mtab");
        else if (ev->exit_code & 32)
           _eeze_disk_mount_error_handler(disk, "mount failure");
        else if (ev->exit_code & 64)
           _eeze_disk_mount_error_handler(disk, "some mount succeeded");
        else
           _eeze_disk_mount_error_handler(disk, "incorrect invocation or permissions");
     }
   else if (disk->mount_status == EEZE_DISK_UNMOUNTING)
     switch (ev->exit_code)
       {
        case 0:
          e = malloc(sizeof(Eeze_Event_Disk_Unmount));
          EINA_SAFETY_ON_NULL_RETURN_VAL(e, ECORE_CALLBACK_RENEW);
          e->disk = disk;
          disk->mounter = NULL;
          disk->mounted = EINA_FALSE;
          ecore_event_add(EEZE_EVENT_DISK_UNMOUNT, e, NULL, NULL);
          break;

        default:
          if (disk->mount_fail_count++ < 3)
            {
               INF("Could not unmount disk, retrying");
               disk->mounter = ecore_exe_run(eina_strbuf_string_get(disk->unmount_cmd), disk);
               eeze_events = eina_list_append(eeze_events, disk);
            }
          else
            {
               disk->mount_fail_count = 0;
               _eeze_disk_mount_error_handler(disk, "Maximum number of mount-related failures reached");
            }
          return ECORE_CALLBACK_RENEW;
       }
     else
       switch (ev->exit_code)
         {
          case 0:
            e = malloc(sizeof(Eeze_Event_Disk_Eject));
            EINA_SAFETY_ON_NULL_RETURN_VAL(e, ECORE_CALLBACK_RENEW);
            e->disk = disk;
            disk->mounter = NULL;
            if (disk->mount_status & EEZE_DISK_UNMOUNTING)
              {
                 disk->mount_status |= EEZE_DISK_UNMOUNTING;
                 disk->mounted = EINA_FALSE;
                 ecore_event_add(EEZE_EVENT_DISK_UNMOUNT, e, NULL, NULL);
                 eeze_disk_eject(disk);
              }
            else
              ecore_event_add(EEZE_EVENT_DISK_EJECT, e, NULL, NULL);
            break;

          default:
            if (disk->mount_fail_count++ < 3)
              {
                 INF("Could not eject disk, retrying");
                 if (disk->mount_status & EEZE_DISK_UNMOUNTING)
                   disk->mounter = ecore_exe_run(eina_strbuf_string_get(disk->unmount_cmd), disk);
                 else
                   disk->mounter = ecore_exe_run(eina_strbuf_string_get(disk->eject_cmd), disk);
                 eeze_events = eina_list_append(eeze_events, disk);
              }
           else
             {
                disk->mount_fail_count = 0;
                _eeze_disk_mount_error_handler(disk, "Maximum number of mount-related failures reached");
             }
            return ECORE_CALLBACK_RENEW;
         }
   return ECORE_CALLBACK_RENEW;
}

/*
 *
 * INVISIBLE
 *
 */

Eina_Bool
eeze_mount_init(void)
{
   EEZE_EVENT_DISK_MOUNT = ecore_event_type_new();
   EEZE_EVENT_DISK_UNMOUNT = ecore_event_type_new();
   EEZE_EVENT_DISK_EJECT = ecore_event_type_new();
   EEZE_EVENT_DISK_ERROR = ecore_event_type_new();
   _mount_handler = ecore_event_handler_add(ECORE_EXE_EVENT_DEL,
                                           (Ecore_Event_Handler_Cb)_eeze_disk_mount_result_handler, NULL);
   return eeze_libmount_init();
}

void
eeze_mount_shutdown(void)
{
   ecore_event_type_flush(EEZE_EVENT_DISK_MOUNT,
                          EEZE_EVENT_DISK_UNMOUNT,
                          EEZE_EVENT_DISK_EJECT,
                          EEZE_EVENT_DISK_ERROR);
   eeze_libmount_shutdown();
   ecore_event_handler_del(_mount_handler);
   _mount_handler = NULL;
}

/*
 *
 * API
 *
 */

EAPI Eina_Bool
eeze_disk_mounted_get(Eeze_Disk *disk)
{
   EINA_SAFETY_ON_NULL_RETURN_VAL(disk, EINA_FALSE);

   return eeze_disk_libmount_mounted_get(disk);
}

EAPI Eina_Bool
eeze_disk_mountopts_set(Eeze_Disk *disk, unsigned long opts)
{
   EINA_SAFETY_ON_NULL_RETURN_VAL(disk, EINA_FALSE);
   if (opts != disk->mount_opts)
     disk->mount_cmd_changed = EINA_TRUE;
   disk->mount_opts = opts;
   if (opts & EEZE_DISK_MOUNTOPT_UID)
     disk->uid = getuid();
   return EINA_TRUE;
}

EAPI unsigned long
eeze_disk_mountopts_get(Eeze_Disk *disk)
{
   EINA_SAFETY_ON_NULL_RETURN_VAL(disk, 0);
   return disk->mount_opts;
}

EAPI Eina_Bool
eeze_disk_mount_wrapper_set(Eeze_Disk *disk, const char *wrapper)
{
   EINA_SAFETY_ON_NULL_RETURN_VAL(disk, EINA_FALSE);
   if (wrapper) EINA_SAFETY_ON_TRUE_RETURN_VAL(!*wrapper, EINA_FALSE);
   else
     {
        eina_stringshare_del(disk->mount_wrapper);
        disk->mount_wrapper = NULL;
        return EINA_TRUE;
     }
   eina_stringshare_replace(&disk->mount_wrapper, wrapper);
   return EINA_TRUE;
}

EAPI const char *
eeze_disk_mount_wrapper_get(Eeze_Disk *disk)
{
   EINA_SAFETY_ON_NULL_RETURN_VAL(disk, NULL);
   return disk->mount_wrapper;
}

EAPI Eina_Bool
eeze_disk_mount(Eeze_Disk *disk)
{
   struct stat st;
   EINA_SAFETY_ON_NULL_RETURN_VAL(disk, EINA_FALSE);

   if (!disk->mount_point)
     return EINA_FALSE;
   if (eeze_disk_libmount_mounted_get(disk))
     return EINA_FALSE;

   if (!disk->mount_cmd)
     disk->mount_cmd = eina_strbuf_new();

   if (disk->mount_cmd_changed)
     {
        const char *dev, *str;
        eina_strbuf_string_free(disk->mount_cmd);
        dev = eeze_disk_uuid_get(disk);
        if (dev) str = "UUID=";
        else
          {
             dev = eeze_disk_devpath_get(disk);
             str = NULL;
          }

        if (!disk->mount_point)
          {
             const char *mp;
             /* here we attempt to guess the mount point using libmount */
             mp = eeze_disk_libmount_mp_lookup_by_uuid(disk->cache.uuid);
             if (!mp)
               {
                  const char *devpath;

                  devpath = eeze_disk_devpath_get(disk);
                  if (devpath)
                    {
                       mp = eeze_disk_libmount_mp_lookup_by_devpath(devpath);
                       eina_stringshare_del(devpath);
                    }
               }
             if (!eeze_disk_mount_point_set(disk, mp))
               /* sometimes we fail */
               return EINA_FALSE;
          }

        if ((!disk->mount_point) || (!disk->mount_point[0])) return EINA_FALSE;
        if (disk->mount_wrapper)
          eina_strbuf_append_printf(disk->mount_cmd, "%s ", disk->mount_wrapper);
        if (disk->mount_opts == EEZE_DISK_MOUNTOPT_DEFAULTS)
          eina_strbuf_append_printf(disk->mount_cmd, EEZE_MOUNT_BIN" -o "EEZE_MOUNT_DEFAULT_OPTS" %s%s %s", str ? str : "", dev, disk->mount_point);
        else if (!disk->mount_opts)
          eina_strbuf_append_printf(disk->mount_cmd, EEZE_MOUNT_BIN" %s%s %s", str ? str : "", dev, disk->mount_point);
        else
          {
             eina_strbuf_append(disk->mount_cmd, EEZE_MOUNT_BIN" -o ");
             /* trailing commas are okay */
             if (disk->mount_opts & EEZE_DISK_MOUNTOPT_LOOP)
               eina_strbuf_append(disk->mount_cmd, "loop,");
             if (disk->mount_opts & EEZE_DISK_MOUNTOPT_UTF8)
               {
                  const char *fstype;
                  eina_strbuf_append(disk->mount_cmd, "utf8,");
                  fstype = eeze_disk_fstype_get(disk);
                  if (fstype && (!strcmp(fstype, "jfs")))
                    eina_strbuf_append(disk->mount_cmd, "iocharset=utf8,");
               }
             if (disk->mount_opts & EEZE_DISK_MOUNTOPT_NOEXEC)
               eina_strbuf_append(disk->mount_cmd, "noexec,");
             if (disk->mount_opts & EEZE_DISK_MOUNTOPT_NODEV)
               eina_strbuf_append(disk->mount_cmd, "nodev,");
             if (disk->mount_opts & EEZE_DISK_MOUNTOPT_NOSUID)
               eina_strbuf_append(disk->mount_cmd, "nosuid,");
             if (disk->mount_opts & EEZE_DISK_MOUNTOPT_REMOUNT)
               eina_strbuf_append(disk->mount_cmd, "remount,");
             if (disk->mount_opts & EEZE_DISK_MOUNTOPT_UID)
               eina_strbuf_append_printf(disk->mount_cmd, "uid=%i,", (int)disk->uid);
             eina_strbuf_append_printf(disk->mount_cmd, " %s%s %s", str ? str : "", dev, disk->mount_point);
          }
        disk->mount_cmd_changed = EINA_FALSE;
     }

   if (stat(disk->mount_point, &st))
     {
        INF("Creating not-existing mount point directory '%s'", disk->mount_point);
        if (mkdir(disk->mount_point, S_IROTH | S_IWOTH | S_IXOTH))
          ERR("Could not create directory: %s; hopefully this is handled by your mounter!", strerror(errno));
     }
   else if (!S_ISDIR(st.st_mode))
     {
        ERR("%s is not a directory!", disk->mount_point);
        return EINA_FALSE;
     }
   INF("Mounting: %s", eina_strbuf_string_get(disk->mount_cmd));
   disk->mounter = ecore_exe_run(eina_strbuf_string_get(disk->mount_cmd), disk);
   if (!disk->mounter)
     return EINA_FALSE;
   eeze_events = eina_list_append(eeze_events, disk);
   disk->mount_status = EEZE_DISK_MOUNTING;

   return EINA_TRUE;
}

EAPI Eina_Bool
eeze_disk_unmount(Eeze_Disk *disk)
{
   EINA_SAFETY_ON_NULL_RETURN_VAL(disk, EINA_FALSE);

   if (!eeze_disk_libmount_mounted_get(disk))
     return EINA_TRUE;

   if (!disk->unmount_cmd)
     disk->unmount_cmd = eina_strbuf_new();

   if (disk->unmount_cmd_changed)
     {
        eina_strbuf_string_free(disk->unmount_cmd);
        if (disk->mount_wrapper)
          eina_strbuf_append_printf(disk->unmount_cmd, "%s ", disk->mount_wrapper);
        eina_strbuf_append_printf(disk->unmount_cmd, EEZE_UNMOUNT_BIN" %s", eeze_disk_devpath_get(disk));
        disk->unmount_cmd_changed = EINA_FALSE;
     }

   INF("Unmounting: %s", eina_strbuf_string_get(disk->unmount_cmd));
   disk->mounter = ecore_exe_run(eina_strbuf_string_get(disk->unmount_cmd), disk);
   if (!disk->mounter)
     return EINA_FALSE;

   eeze_events = eina_list_append(eeze_events, disk);
   disk->mount_status = EEZE_DISK_UNMOUNTING;
   return EINA_TRUE;
}

EAPI Eina_Bool
eeze_disk_eject(Eeze_Disk *disk)
{
   EINA_SAFETY_ON_NULL_RETURN_VAL(disk, EINA_FALSE);

   if (!disk->eject_cmd)
     {
        disk->eject_cmd = eina_strbuf_new();
        if (disk->mount_wrapper)
          eina_strbuf_append_printf(disk->eject_cmd, "%s ", disk->mount_wrapper);
        eina_strbuf_append_printf(disk->eject_cmd, EEZE_EJECT_BIN" %s", eeze_disk_devpath_get(disk));
     }

   INF("Ejecting: %s", eina_strbuf_string_get(disk->eject_cmd));
   if (eeze_disk_libmount_mounted_get(disk))
     {
        Eina_Bool ret;

        ret = eeze_disk_unmount(disk);
        if (ret) disk->mount_status |= EEZE_DISK_EJECTING;
        return ret;
     }
   disk->mounter = ecore_exe_run(eina_strbuf_string_get(disk->eject_cmd), disk);
   if (!disk->mounter)
     return EINA_FALSE;

   eeze_events = eina_list_append(eeze_events, disk);
   disk->mount_status = EEZE_DISK_EJECTING;
   return EINA_TRUE;
}

EAPI void
eeze_disk_cancel(Eeze_Disk *disk)
{
   EINA_SAFETY_ON_NULL_RETURN(disk);
   if ((!disk->mount_status) || (!disk->mounter)) return;
   disk->mount_status = EEZE_DISK_NULL;
   ecore_exe_kill(disk->mounter);
   disk->mounter = NULL;
}

EAPI const char *
eeze_disk_mount_point_get(Eeze_Disk *disk)
{
   const char *mp;
   EINA_SAFETY_ON_NULL_RETURN_VAL(disk, NULL);

   if (disk->mount_point)
     return disk->mount_point;

   mp = eeze_disk_libmount_mp_lookup_by_devpath(eeze_disk_devpath_get(disk));
   if (mp)
     {
        disk->mount_point = eina_stringshare_add(mp);
        return disk->mount_point;
     }
   mp = eeze_disk_libmount_mp_lookup_by_uuid(eeze_disk_uuid_get(disk));
   if (mp)
     {
        disk->mount_point = eina_stringshare_add(mp);
        return disk->mount_point;
     }
   mp = eeze_disk_libmount_mp_lookup_by_label(eeze_disk_label_get(disk));
   if (mp)
     {
        disk->mount_point = eina_stringshare_add(mp);
        return disk->mount_point;
     }
   return NULL;
}

EAPI Eina_Bool
eeze_disk_mount_point_set(Eeze_Disk *disk, const char *mount_point)
{
   EINA_SAFETY_ON_NULL_RETURN_VAL(disk, EINA_FALSE);

   eina_stringshare_replace(&disk->mount_point, mount_point);
   disk->mount_cmd_changed = EINA_TRUE;
   disk->unmount_cmd_changed = EINA_TRUE;
   return EINA_TRUE;
}
