/* This file is part of Clementine.
   Copyright 2010, David Sansome <me@davidsansome.com>

   Clementine 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.

   Clementine 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 Clementine.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "macdevicelister.h"

#include "mtpconnection.h"

#include <CoreFoundation/CFRunLoop.h>
#include <DiskArbitration/DiskArbitration.h>
#include <IOKit/kext/KextManager.h>
#include <IOKit/IOCFPlugin.h>
#include <IOKit/usb/IOUSBLib.h>
#include <IOKit/storage/IOMedia.h>

#import <AppKit/NSWorkspace.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSNotification.h>
#import <Foundation/NSPathUtilities.h>
#import <Foundation/NSString.h>
#import <Foundation/NSURL.h>

#include <boost/scope_exit.hpp>

#include <libmtp.h>

#include <QtDebug>

#include <QMutex>
#include <QString>
#include <QStringList>

#ifndef kUSBSerialNumberString
#define kUSBSerialNumberString "USB Serial Number"
#endif

#ifndef kUSBVendorString
#define kUSBVendorString "USB Vendor Name"
#endif

#ifndef kUSBProductString
#define kUSBProductString "USB Product Name"
#endif

// Helpful MTP & USB links:
// Apple USB device interface guide:
// http://developer.apple.com/mac/library/documentation/DeviceDrivers/Conceptual/USBBook/USBDeviceInterfaces/USBDevInterfaces.html
// Example Apple code for requesting a USB device descriptor:
// http://www.opensource.apple.com/source/IOUSBFamily/IOUSBFamily-208.4.5/USBProber/BusProbeClass.m
// Libmtp's detection code:
// http://libmtp.cvs.sourceforge.net/viewvc/libmtp/libmtp/src/libusb-glue.c?view=markup
// Libusb's Mac code:
// http://www.libusb.org/browser/libusb/libusb/os/darwin_usb.c
// Microsoft OS Descriptors:
// http://www.microsoft.com/whdc/connect/usb/os_desc.mspx
// Symbian docs for implementing the device side:
// http://developer.symbian.org/main/documentation/reference/s3/pdk/GUID-3FF0F248-EDF0-5348-BC43-869CE1B5B415.html
// Libgphoto2 MTP detection code:
// http://www.sfr-fresh.com/unix/privat/libgphoto2-2.4.10.1.tar.gz:a/libgphoto2-2.4.10.1/libgphoto2_port/usb/check-mtp-device.c

QSet<MacDeviceLister::MTPDevice> MacDeviceLister::sMTPDeviceList;

uint qHash(const MacDeviceLister::MTPDevice& d) {
  return qHash(d.vendor_id) ^ qHash(d.product_id);
}

MacDeviceLister::MacDeviceLister() {
}

MacDeviceLister::~MacDeviceLister() {
  CFRelease(loop_session_);
}

void MacDeviceLister::Init() {
  [[NSAutoreleasePool alloc] init];

  // Populate MTP Device list.
  if (sMTPDeviceList.empty()) {
    LIBMTP_device_entry_t* devices = NULL;
    int num = 0;
    if (LIBMTP_Get_Supported_Devices_List(&devices, &num) != 0) {
      qWarning() << "Failed to get MTP device list";
    } else {
      for (int i = 0; i < num; ++i) {
        LIBMTP_device_entry_t device = devices[i];
        MTPDevice d;
        d.vendor = QString::fromAscii(device.vendor);
        d.vendor_id = device.vendor_id;
        d.product = QString::fromAscii(device.product);
        d.product_id = device.product_id;
        d.quirks = device.device_flags;
        sMTPDeviceList << d;
      }
    }

    MTPDevice d;
    d.vendor = "SanDisk";
    d.vendor_id = 0x781;
    d.product = "Sansa Clip+";
    d.product_id = 0x74d0;

    d.quirks = 0x2 | 0x4 | 0x40 | 0x4000;
    sMTPDeviceList << d;
  }

  run_loop_ = CFRunLoopGetCurrent();

  // Register for disk mounts/unmounts.
  loop_session_ = DASessionCreate(kCFAllocatorDefault);
  DARegisterDiskAppearedCallback(
      loop_session_, kDADiskDescriptionMatchVolumeMountable, &DiskAddedCallback, reinterpret_cast<void*>(this));
  DARegisterDiskDisappearedCallback(
      loop_session_, NULL, &DiskRemovedCallback, reinterpret_cast<void*>(this));
  DASessionScheduleWithRunLoop(loop_session_, run_loop_, kCFRunLoopDefaultMode);

  // Register for USB device connection/disconnection.
  IONotificationPortRef notification_port = IONotificationPortCreate(kIOMasterPortDefault);
  CFMutableDictionaryRef matching_dict = IOServiceMatching(kIOUSBDeviceClassName);
  // IOServiceAddMatchingNotification decreases reference count.
  CFRetain(matching_dict);
  io_iterator_t it;
  kern_return_t err = IOServiceAddMatchingNotification(
      notification_port,
      kIOFirstMatchNotification,
      matching_dict,
      &USBDeviceAddedCallback,
      reinterpret_cast<void*>(this),
      &it);
  if (err == KERN_SUCCESS) {
    USBDeviceAddedCallback(this, it);
  } else {
    qWarning() << "Could not add notification on USB device connection";
  }

  err = IOServiceAddMatchingNotification(
      notification_port,
      kIOTerminatedNotification,
      matching_dict,
      &USBDeviceRemovedCallback,
      reinterpret_cast<void*>(this),
      &it);
  if (err == KERN_SUCCESS) {
    USBDeviceRemovedCallback(this, it);
  } else {
    qWarning() << "Could not add notification USB device removal";
  }

  CFRunLoopSourceRef io_source = IONotificationPortGetRunLoopSource(notification_port);
  CFRunLoopAddSource(run_loop_, io_source, kCFRunLoopDefaultMode);

  CFRunLoopRun();
}

void MacDeviceLister::ShutDown() {
  CFRunLoopStop(run_loop_);
}


// IOKit helpers.
namespace {

CFTypeRef GetUSBRegistryEntry(io_object_t device, CFStringRef key) {
  io_iterator_t it;
  if (IORegistryEntryGetParentIterator(device, kIOServicePlane, &it) == KERN_SUCCESS) {
    io_object_t next;
    while ((next = IOIteratorNext(it))) {
      CFTypeRef registry_entry = (CFStringRef)IORegistryEntryCreateCFProperty(
          next, key, kCFAllocatorDefault, 0);
      if (registry_entry) {
        IOObjectRelease(next);
        IOObjectRelease(it);
        return registry_entry;
      }

      CFTypeRef ret = GetUSBRegistryEntry(next, key);
      if (ret) {
        IOObjectRelease(next);
        IOObjectRelease(it);
        return ret;
      }

      IOObjectRelease(next);
    }
  }

  IOObjectRelease(it);
  return NULL;
}

QString GetUSBRegistryEntryString(io_object_t device, CFStringRef key) {
  CFStringRef registry_string = (CFStringRef)GetUSBRegistryEntry(device, key);
  if (registry_string) {
    QString ret = QString::fromUtf8([(NSString*)registry_string UTF8String]);
    CFRelease(registry_string);
    return ret;
  }

  return NULL;
}

quint64 GetUSBRegistryEntryInt64(io_object_t device, CFStringRef key) {
  CFNumberRef registry_num = (CFNumberRef)GetUSBRegistryEntry(device, key);
  if (registry_num) {
    qint64 ret = -1;
    Boolean result = CFNumberGetValue(registry_num, kCFNumberLongLongType, &ret);
    if (!result || ret < 0) {
      return 0;
    } else {
      return ret;
    }
  }

  return 0;
}

NSObject* GetPropertyForDevice(io_object_t device, CFStringRef key) {
  CFMutableDictionaryRef properties;
  kern_return_t ret = IORegistryEntryCreateCFProperties(
      device, &properties, kCFAllocatorDefault, 0);

  if (ret != KERN_SUCCESS) {
    return nil;
  }

  NSDictionary* dict = (NSDictionary*)properties;
  NSObject* prop = [dict objectForKey:(NSString*)key];
  if (prop) {
    return prop;
  }

  io_object_t parent;
  ret = IORegistryEntryGetParentEntry(device, kIOServicePlane, &parent);
  if (ret == KERN_SUCCESS) {
    return GetPropertyForDevice(parent, key);
  }

  return nil;
}

QString GetIconForDevice(io_object_t device) {
  NSDictionary* media_icon = (NSDictionary*)GetPropertyForDevice(device, CFSTR("IOMediaIcon"));
  if (media_icon) {
    NSString* bundle = (NSString*)[media_icon objectForKey:@"CFBundleIdentifier"];
    NSString* file = (NSString*)[media_icon objectForKey:@"IOBundleResourceFile"];

    NSURL* bundle_url = (NSURL*)KextManagerCreateURLForBundleIdentifier(
        kCFAllocatorDefault, (CFStringRef)bundle);

    QString path = QString::fromUtf8([[bundle_url path] UTF8String]);
    path += "/Contents/Resources/";
    path += [file UTF8String];
    return path;
  }

  return QString();
}

QString GetSerialForDevice(io_object_t device) {
  QString serial = GetUSBRegistryEntryString(device, CFSTR(kUSBSerialNumberString));
  if (!serial.isEmpty()) {
    return "USB/" + serial;
  }
  return QString();
}

QString GetSerialForMTPDevice(io_object_t device) {
  return QString(
      QString("MTP/") + [(NSString*)GetPropertyForDevice(device, CFSTR(kUSBSerialNumberString)) UTF8String]);
}

QString FindDeviceProperty(const QString& bsd_name, CFStringRef property) {
  DASessionRef session = DASessionCreate(kCFAllocatorDefault);
  DADiskRef disk = DADiskCreateFromBSDName(
      kCFAllocatorDefault, session, bsd_name.toAscii().constData());

  io_object_t device = DADiskCopyIOMedia(disk);
  QString ret = GetUSBRegistryEntryString(device, property);
  IOObjectRelease(device);

  CFRelease(disk);
  CFRelease(session);

  return ret;
}

}

int MacDeviceLister::GetFreeSpace(const QUrl& url) {
  QMutexLocker l(&libmtp_mutex_);
  MtpConnection connection(url);
  if (!connection.is_valid()) {
    qWarning() << "Error connecting to MTP device, couldn't get device free space";
    return -1;
  }
  return connection.device()->storage->FreeSpaceInBytes;
}

int MacDeviceLister::GetCapacity(const QUrl& url) {
  QMutexLocker l(&libmtp_mutex_);
  MtpConnection connection(url);
  if (!connection.is_valid()) {
    qWarning() << "Error connecting to MTP device, couldn't get device capacity";
    return -1;
  }
  return connection.device()->storage->MaxCapacity;
}

void MacDeviceLister::DiskAddedCallback(DADiskRef disk, void* context) {
  MacDeviceLister* me = reinterpret_cast<MacDeviceLister*>(context);

  NSDictionary* properties = (NSDictionary*)DADiskCopyDescription(disk);
  NSURL* volume_path = 
      [[properties objectForKey:(NSString*)kDADiskDescriptionVolumePathKey] copy];

  if (volume_path) {
    io_object_t device = DADiskCopyIOMedia(disk);
    CFStringRef class_name = IOObjectCopyClass(device);
    if (class_name && CFStringCompare(class_name, CFSTR(kIOMediaClass), 0) == kCFCompareEqualTo) {
      QString vendor = GetUSBRegistryEntryString(device, CFSTR(kUSBVendorString));
      QString product = GetUSBRegistryEntryString(device, CFSTR(kUSBProductString));

      CFMutableDictionaryRef properties;
      kern_return_t ret = IORegistryEntryCreateCFProperties(
          device, &properties, kCFAllocatorDefault, 0);

      if (ret == KERN_SUCCESS) {
        NSDictionary* dict = (NSDictionary*)properties;
        if ([[dict objectForKey:@"Removable"] intValue] == 1) {
          QString serial = GetSerialForDevice(device);
          if (!serial.isEmpty()) {
            me->current_devices_[serial] = QString(DADiskGetBSDName(disk));
            emit me->DeviceAdded(serial);
          }
        }
      }
    }

    if (class_name)
      CFRelease(class_name);
    IOObjectRelease(device);
  }
}

void MacDeviceLister::DiskRemovedCallback(DADiskRef disk, void* context) {
  MacDeviceLister* me = reinterpret_cast<MacDeviceLister*>(context);
  // We cannot access the USB tree when the disk is removed but we still get
  // the BSD disk name.
  for (QMap<QString, QString>::iterator it = me->current_devices_.begin();
       it != me->current_devices_.end(); ++it) {
    if (it.value() == QString::fromLocal8Bit(DADiskGetBSDName(disk))) {
      emit me->DeviceRemoved(it.key());
      me->current_devices_.erase(it);
      break;
    }
  }
}

bool DeviceRequest(IOUSBDeviceInterface** dev,
                   quint8 direction,
                   quint8 type,
                   quint8 recipient,
                   quint8 request_code,
                   quint16 value,
                   quint16 index,
                   quint16 length,
                   QByteArray* data) {
  IOUSBDevRequest req;
  req.bmRequestType = USBmakebmRequestType(direction, type, recipient);
  req.bRequest = request_code;
  req.wValue = value;
  req.wIndex = index;
  req.wLength = length;
  data->resize(256);
  req.pData = data->data();
  kern_return_t err = (*dev)->DeviceRequest(dev, &req);
  if (err != kIOReturnSuccess) {
    return false;
  }
  data->resize(req.wLenDone);
  return true;
}

int GetBusNumber(io_object_t o) {
  io_iterator_t it;
  kern_return_t err = IORegistryEntryGetParentIterator(o, kIOServicePlane, &it);
  if (err != KERN_SUCCESS) {
    return -1;
  }
  while ((o = IOIteratorNext(it))) {
    NSObject* bus = GetPropertyForDevice(o, CFSTR("USBBusNumber"));
    if (bus) {
      NSNumber* bus_num = (NSNumber*)bus;
      return [bus_num intValue];
    }
  }

  return -1;
}

void MacDeviceLister::USBDeviceAddedCallback(void* refcon, io_iterator_t it) {
  MacDeviceLister* me = reinterpret_cast<MacDeviceLister*>(refcon);

  io_object_t object;
  while ((object = IOIteratorNext(it))) {
    CFStringRef class_name = IOObjectCopyClass(object);
    BOOST_SCOPE_EXIT((class_name)(object)) {
      CFRelease(class_name);
      IOObjectRelease(object);
    } BOOST_SCOPE_EXIT_END

    if (CFStringCompare(class_name, CFSTR(kIOUSBDeviceClassName), 0) == kCFCompareEqualTo) {
      NSString* vendor = (NSString*)GetPropertyForDevice(object, CFSTR(kUSBVendorString));
      NSString* product = (NSString*)GetPropertyForDevice(object, CFSTR(kUSBProductString));
      NSNumber* vendor_id = (NSNumber*)GetPropertyForDevice(object, CFSTR(kUSBVendorID));
      NSNumber* product_id = (NSNumber*)GetPropertyForDevice(object, CFSTR(kUSBProductID));
      QString serial = GetSerialForMTPDevice(object);

      MTPDevice device;
      device.vendor = QString::fromUtf8([vendor UTF8String]);
      device.product = QString::fromUtf8([product UTF8String]);
      device.vendor_id = [vendor_id unsignedShortValue];
      device.product_id = [product_id unsignedShortValue];
      device.quirks = 0;

      device.bus = -1;
      device.address = -1;

      if (device.vendor_id == 0x5ac) {
        // I think we can safely skip Apple products.
        continue;
      }

      NSNumber* addr = (NSNumber*)GetPropertyForDevice(object, CFSTR("USB Address"));
      int bus = GetBusNumber(object);
      if (!addr || bus == -1) {
        // Failed to get bus or address number.
        continue;
      }
      device.bus = bus;
      device.address = [addr intValue];

      // First check the libmtp device list.
      QSet<MTPDevice>::const_iterator it = sMTPDeviceList.find(device);
      if (it != sMTPDeviceList.end()) {
        qDebug() << "Found in table";
        // Fill in quirks flags from libmtp.
        device.quirks = it->quirks;
        me->FoundMTPDevice(device, GetSerialForMTPDevice(object));
        continue;
      }

      IOCFPlugInInterface** plugin_interface = NULL;
      SInt32 score;
      kern_return_t err = IOCreatePlugInInterfaceForService(
          object,
          kIOUSBDeviceUserClientTypeID,
          kIOCFPlugInInterfaceID,
          &plugin_interface,
          &score);
      if (err != KERN_SUCCESS) {
        continue;
      }

      IOUSBDeviceInterface** dev = NULL;
      HRESULT result = (*plugin_interface)->QueryInterface(
          plugin_interface,
          CFUUIDGetUUIDBytes(kIOUSBDeviceInterfaceID),
          (LPVOID*)&dev);

      (*plugin_interface)->Release(plugin_interface);

      if (result || !dev) {
        continue;
      }

      err = (*dev)->USBDeviceOpen(dev);
      if (err != kIOReturnSuccess) {
        continue;
      }

      // Automatically close & release usb device at scope exit.
      BOOST_SCOPE_EXIT((dev)) {
        (*dev)->USBDeviceClose(dev);
        (*dev)->Release(dev);
      } BOOST_SCOPE_EXIT_END

      // Request the string descriptor at 0xee.
      // This is a magic string that indicates whether this device supports MTP.
      QByteArray data;
      bool ret = DeviceRequest(
          dev, kUSBIn, kUSBStandard, kUSBDevice, kUSBRqGetDescriptor,
          (kUSBStringDesc << 8) | 0xee, 0x0409, 2, &data);
      if (!ret)
        continue;

      UInt8 string_len = data[0];

      ret = DeviceRequest(
          dev, kUSBIn, kUSBStandard, kUSBDevice, kUSBRqGetDescriptor,
          (kUSBStringDesc << 8) | 0xee, 0x0409, string_len, &data);
      if (!ret)
        continue;

      // The device actually returned something. That's a good sign.
      // Because this was designed by MS, the characters are in UTF-16 (LE?).
      QString str = QString::fromUtf16(reinterpret_cast<ushort*>(data.data() + 2), (data.size() / 2) - 2);

      if (str.startsWith("MSFT100")) {
        // We got the OS descriptor!
        char vendor_code = data[16];
        ret = DeviceRequest(
            dev, kUSBIn, kUSBVendor, kUSBDevice, vendor_code, 0, 4, 256, &data);
        if (!ret || data.at(0) != 0x28)
          continue;

        if (QString::fromAscii(data.data() + 0x12, 3) != "MTP") {
          // Not quite.
          continue;
        }

        ret = DeviceRequest(
            dev, kUSBIn, kUSBVendor, kUSBDevice, vendor_code, 0, 5, 256, &data);
        if (!ret || data.at(0) != 0x28) {
          continue;
        }

        if (QString::fromAscii(data.data() + 0x12, 3) != "MTP") {
          // Not quite.
          continue;
        }
        // Hurray! We made it!
        me->FoundMTPDevice(device, serial);
      }
    }
  }
}

void MacDeviceLister::USBDeviceRemovedCallback(void* refcon, io_iterator_t it) {
  MacDeviceLister* me = reinterpret_cast<MacDeviceLister*>(refcon);
  io_object_t object;
  while ((object = IOIteratorNext(it))) {
    CFStringRef class_name = IOObjectCopyClass(object);
    BOOST_SCOPE_EXIT((class_name)(object)) {
      CFRelease(class_name);
      IOObjectRelease(object);
    } BOOST_SCOPE_EXIT_END

    if (CFStringCompare(class_name, CFSTR(kIOUSBDeviceClassName), 0) == kCFCompareEqualTo) {
      NSString* vendor = (NSString*)GetPropertyForDevice(object, CFSTR(kUSBVendorString));
      NSString* product = (NSString*)GetPropertyForDevice(object, CFSTR(kUSBProductString));
      NSNumber* vendor_id = (NSNumber*)GetPropertyForDevice(object, CFSTR(kUSBVendorID));
      NSNumber* product_id = (NSNumber*)GetPropertyForDevice(object, CFSTR(kUSBProductID));
      QString serial = GetSerialForMTPDevice(object);

      MTPDevice device;
      device.vendor = QString::fromUtf8([vendor UTF8String]);
      device.product = QString::fromUtf8([product UTF8String]);
      device.vendor_id = [vendor_id unsignedShortValue];
      device.product_id = [product_id unsignedShortValue];

      me->RemovedMTPDevice(serial);
    }
  }
}

void MacDeviceLister::RemovedMTPDevice(const QString& serial) {
  int count = mtp_devices_.remove(serial);
  if (count) {
    qDebug() << "MTP device removed:" << serial;
    emit DeviceRemoved(serial);
  }
}

void MacDeviceLister::FoundMTPDevice(const MTPDevice& device, const QString& serial) {
  qDebug() << "New MTP device detected!" << device.bus << device.address;
  mtp_devices_[serial] = device;
  QList<QUrl> urls = MakeDeviceUrls(serial);
  MTPDevice* d = &mtp_devices_[serial];
  d->capacity = GetCapacity(urls[0]);
  d->free_space = GetFreeSpace(urls[0]);
  emit DeviceAdded(serial);
}

bool IsMTPSerial(const QString& serial) {
  return serial.startsWith("MTP");
}

QString MacDeviceLister::MakeFriendlyName(const QString& serial) {
  if (IsMTPSerial(serial)) {
    const MTPDevice& device = mtp_devices_[serial];
    if (device.vendor.isEmpty()) {
      return device.product;
    } else {
      return device.vendor + " " + device.product;
    }
  }
  QString bsd_name = current_devices_[serial];
  DASessionRef session = DASessionCreate(kCFAllocatorDefault);
  DADiskRef disk = DADiskCreateFromBSDName(
      kCFAllocatorDefault, session, bsd_name.toAscii().constData());

  io_object_t device = DADiskCopyIOMedia(disk);
  QString vendor = GetUSBRegistryEntryString(device, CFSTR(kUSBVendorString));
  QString product = GetUSBRegistryEntryString(device, CFSTR(kUSBProductString));
  IOObjectRelease(device);

  CFRelease(disk);
  CFRelease(session);

  if (vendor.isEmpty()) {
    return product;
  }
  return vendor + " " + product;
}

QList<QUrl> MacDeviceLister::MakeDeviceUrls(const QString& serial) {
  if (IsMTPSerial(serial)) {
    const MTPDevice& device = mtp_devices_[serial];
    QString str;
    str.sprintf("gphoto2://usb-%d-%d/", device.bus, device.address);
    QUrl url(str);
    url.addQueryItem("vendor", device.vendor);
    url.addQueryItem("vendor_id", QString::number(device.vendor_id));
    url.addQueryItem("product", device.product);
    url.addQueryItem("product_id", QString::number(device.product_id));
    url.addQueryItem("quirks", QString::number(device.quirks));
    return QList<QUrl>() << url;
  }

  QString bsd_name = current_devices_[serial];
  DASessionRef session = DASessionCreate(kCFAllocatorDefault);
  DADiskRef disk = DADiskCreateFromBSDName(
      kCFAllocatorDefault, session, bsd_name.toAscii().constData());

  NSDictionary* properties = (NSDictionary*)DADiskCopyDescription(disk);
  NSURL* volume_path = 
      [[properties objectForKey:(NSString*)kDADiskDescriptionVolumePathKey] copy];

  QString path = [[volume_path path] UTF8String];
  QUrl ret = MakeUrlFromLocalPath(path);

  CFRelease(disk);
  CFRelease(session);

  return QList<QUrl>() << ret;
}

QStringList MacDeviceLister::DeviceUniqueIDs() {
  return current_devices_.keys() + mtp_devices_.keys();
}

QVariantList MacDeviceLister::DeviceIcons(const QString& serial) {
  if (IsMTPSerial(serial)) {
    return QVariantList();
  }
  QString bsd_name = current_devices_[serial];
  DASessionRef session = DASessionCreate(kCFAllocatorDefault);
  DADiskRef disk = DADiskCreateFromBSDName(
      kCFAllocatorDefault, session, bsd_name.toAscii().constData());

  io_object_t device = DADiskCopyIOMedia(disk);
  QString icon = GetIconForDevice(device);

  NSDictionary* properties = (NSDictionary*)DADiskCopyDescription(disk);
  NSURL* volume_path = 
      [[properties objectForKey:(NSString*)kDADiskDescriptionVolumePathKey] copy];

  QString path = QString::fromUtf8([[volume_path path] UTF8String]);

  IOObjectRelease(device);
  CFRelease(disk);
  CFRelease(session);

  QVariantList ret;
  ret << GuessIconForPath(path);
  ret << GuessIconForModel(DeviceManufacturer(serial), DeviceModel(serial));
  if (!icon.isEmpty()) {
    ret << icon;
  }
  return ret;
}

QString MacDeviceLister::DeviceManufacturer(const QString& serial){
  if (IsMTPSerial(serial)) {
    return mtp_devices_[serial].vendor;
  }
  return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBVendorString));
}

QString MacDeviceLister::DeviceModel(const QString& serial){
  if (IsMTPSerial(serial)) {
    return mtp_devices_[serial].product;
  }
  return FindDeviceProperty(current_devices_[serial], CFSTR(kUSBProductString));
}

quint64 MacDeviceLister::DeviceCapacity(const QString& serial){
  if (IsMTPSerial(serial)) {
    QList<QUrl> urls = MakeDeviceUrls(serial);
    return mtp_devices_[serial].capacity;
  }
  QString bsd_name = current_devices_[serial];
  DASessionRef session = DASessionCreate(kCFAllocatorDefault);
  DADiskRef disk = DADiskCreateFromBSDName(
      kCFAllocatorDefault, session, bsd_name.toAscii().constData());

  io_object_t device = DADiskCopyIOMedia(disk);

  NSNumber* capacity = (NSNumber*)GetPropertyForDevice(device, CFSTR("Size"));

  quint64 ret = [capacity unsignedLongLongValue];

  IOObjectRelease(device);

  CFRelease(disk);
  CFRelease(session);

  return ret;
}

quint64 MacDeviceLister::DeviceFreeSpace(const QString& serial){
  if (IsMTPSerial(serial)) {
    QList<QUrl> urls = MakeDeviceUrls(serial);
    return mtp_devices_[serial].free_space;
  }
  QString bsd_name = current_devices_[serial];
  DASessionRef session = DASessionCreate(kCFAllocatorDefault);
  DADiskRef disk = DADiskCreateFromBSDName(
      kCFAllocatorDefault, session, bsd_name.toAscii().constData());

  NSDictionary* properties = (NSDictionary*)DADiskCopyDescription(disk);
  NSURL* volume_path =
      [[properties objectForKey:(NSString*)kDADiskDescriptionVolumePathKey] copy];

  FSRef ref;
  OSStatus err = FSPathMakeRef(
      (const UInt8*)[[volume_path path] fileSystemRepresentation], &ref, NULL);
  if (err == noErr) {
    FSCatalogInfo catalog;
    err = FSGetCatalogInfo(&ref, kFSCatInfoVolume, &catalog, NULL, NULL, NULL);
    if (err == noErr) {
      FSVolumeRefNum volume_ref = catalog.volume;
      FSVolumeInfo info;
      err = FSGetVolumeInfo(
          volume_ref, 0, NULL, kFSVolInfoSizes, &info, NULL, NULL);
      UInt64 free_bytes = info.freeBytes;
      return free_bytes;
    }
  }

  return 0;
}

QVariantMap MacDeviceLister::DeviceHardwareInfo(const QString& serial){return QVariantMap();}

void MacDeviceLister::UnmountDevice(const QString& serial) {
  if (IsMTPSerial(serial)) return;

  QString bsd_name = current_devices_[serial];
  DADiskRef disk = DADiskCreateFromBSDName(
      kCFAllocatorDefault, loop_session_, bsd_name.toAscii().constData());

  DADiskUnmount(disk, kDADiskUnmountOptionDefault, &DiskUnmountCallback, this);

  CFRelease(disk);
}

void MacDeviceLister::DiskUnmountCallback(
    DADiskRef disk, DADissenterRef dissenter, void* context) {
  if (dissenter) {
    qWarning() << "Another app blocked the unmount";
  } else {
    DiskRemovedCallback(disk, context);
  }
}

void MacDeviceLister::UpdateDeviceFreeSpace(const QString& serial) {
  if (IsMTPSerial(serial)) {
    if (mtp_devices_.contains(serial)) {
      QList<QUrl> urls = MakeDeviceUrls(serial);
      MTPDevice* d = &mtp_devices_[serial];
      d->free_space = GetFreeSpace(urls[0]);
    }
  }
  emit DeviceChanged(serial);
}
