#if defined(__APPLE__)

#include <sys/param.h>
#include <stdint.h>

#include <CoreFoundation/CoreFoundation.h>
#include <IOKit/IOKitLib.h>
#include <IOKit/serial/IOSerialKeys.h>
#include <IOKit/IOBSD.h>

#include <iostream>
#include <string>
#include <vector>

#include "serial/serial.h"

using serial::PortInfo;
using std::string;
using std::vector;

#define HARDWARE_ID_STRING_LENGTH 128

string cfstring_to_string( CFStringRef cfstring );
string get_device_path( io_object_t& serial_port );
string get_class_name( io_object_t& obj );
io_registry_entry_t get_parent_iousb_device( io_object_t& serial_port );
string get_string_property( io_object_t& device, const char* property );
uint16_t get_int_property( io_object_t& device, const char* property );
string rtrim(const string& str);

string
cfstring_to_string( CFStringRef cfstring )
{
    char cstring[MAXPATHLEN];
    string result;

    if( cfstring )
    {
        Boolean success = CFStringGetCString( cfstring,
            cstring,
            sizeof(cstring),
            kCFStringEncodingASCII );

        if( success )
            result = cstring;
    }

    return result;
}

string
get_device_path( io_object_t& serial_port )
{
    CFTypeRef dialin_path;
    string device_path;

    dialin_path = IORegistryEntryCreateCFProperty( serial_port,
        CFSTR(kIODialinDeviceKey),
        kCFAllocatorDefault,
        0 );

    if (dialin_path)
    {
        if( CFGetTypeID(dialin_path) == CFStringGetTypeID() )
            device_path = cfstring_to_string( static_cast<CFStringRef>(dialin_path) );

        CFRelease(dialin_path);
    }

    return device_path;
}

string
get_class_name( io_object_t& obj )
{
    string result;
    io_name_t class_name;
    kern_return_t kern_result;

    kern_result = IOObjectGetClass( obj, class_name );

    if( kern_result == KERN_SUCCESS )
        result = class_name;

    return result;
}

io_registry_entry_t
get_parent_iousb_device( io_object_t& serial_port )
{
    io_object_t device = serial_port;
    io_registry_entry_t parent = 0;
    io_registry_entry_t result = 0;
    kern_return_t kern_result = KERN_FAILURE;
    string name = get_class_name(device);

    // Walk the IO Registry tree looking for this devices parent IOUSBDevice.
    while( name != "IOUSBDevice" )
    {
        kern_result = IORegistryEntryGetParentEntry( device,
        kIOServicePlane,
        &parent );

        if(kern_result != KERN_SUCCESS)
        {
            result = 0;
            break;
        }

        device = parent;

        name = get_class_name(device);
    }

    if(kern_result == KERN_SUCCESS)
        result = device;

    return result;
}

string
get_string_property( io_object_t& device, const char* property )
{
    string property_name;

    if( device )
    {
        CFStringRef property_as_cfstring = CFStringCreateWithCString (
            kCFAllocatorDefault,
            property,
            kCFStringEncodingASCII );

        CFTypeRef name_as_cfstring = IORegistryEntryCreateCFProperty(
            device,
            property_as_cfstring,
            kCFAllocatorDefault,
            0 );

        if( name_as_cfstring )
        {
            if( CFGetTypeID(name_as_cfstring) == CFStringGetTypeID() )
                property_name = cfstring_to_string( static_cast<CFStringRef>(name_as_cfstring) );

            CFRelease(name_as_cfstring);
        }

        if(property_as_cfstring)
            CFRelease(property_as_cfstring);
    }

    return property_name;
}

uint16_t
get_int_property( io_object_t& device, const char* property )
{
    uint16_t result = 0;

    if( device )
    {
        CFStringRef property_as_cfstring = CFStringCreateWithCString (
            kCFAllocatorDefault,
            property,
            kCFStringEncodingASCII );

        CFTypeRef number = IORegistryEntryCreateCFProperty( device,
            property_as_cfstring,
            kCFAllocatorDefault,
            0 );

        if(property_as_cfstring)
            CFRelease(property_as_cfstring);

        if( number )
        {
            if( CFGetTypeID(number) == CFNumberGetTypeID() )
            {
                bool success = CFNumberGetValue( static_cast<CFNumberRef>(number),
                    kCFNumberSInt16Type,
                    &result );

                if( !success )
                    result = 0;
            }

            CFRelease(number);
        }

    }

    return result;
}

string rtrim(const string& str)
{
    string result = str;

    string whitespace = " \t\f\v\n\r";

    std::size_t found = result.find_last_not_of(whitespace);

    if (found != std::string::npos)
        result.erase(found+1);
    else
        result.clear();

    return result;
}

vector<PortInfo>
serial::list_ports(void)
{
    vector<PortInfo> devices_found;
    CFMutableDictionaryRef classes_to_match;
    io_iterator_t serial_port_iterator;
    io_object_t serial_port;
    mach_port_t master_port;
    kern_return_t kern_result;

    kern_result = IOMasterPort(MACH_PORT_NULL, &master_port);

    if(kern_result != KERN_SUCCESS)
        return devices_found;

    classes_to_match = IOServiceMatching(kIOSerialBSDServiceValue);

    if (classes_to_match == NULL)
        return devices_found;

    CFDictionarySetValue( classes_to_match,
        CFSTR(kIOSerialBSDTypeKey),
        CFSTR(kIOSerialBSDAllTypes) );

    kern_result = IOServiceGetMatchingServices(master_port, classes_to_match, &serial_port_iterator);

    if (KERN_SUCCESS != kern_result)
        return devices_found;

    while ( (serial_port = IOIteratorNext(serial_port_iterator)) )
    {
        string device_path = get_device_path( serial_port );
        io_registry_entry_t parent = get_parent_iousb_device( serial_port );
        IOObjectRelease(serial_port);

        if( device_path.empty() )
            continue;

        PortInfo port_info;
        port_info.port = device_path;
        port_info.description = "n/a";
        port_info.hardware_id = "n/a";

        string device_name = rtrim( get_string_property( parent, "USB Product Name" ) );
        string vendor_name = rtrim( get_string_property( parent, "USB Vendor Name") );
        string description = rtrim( vendor_name + " " + device_name );
        if( !description.empty() )
            port_info.description = description;

        string serial_number = rtrim(get_string_property( parent, "USB Serial Number" ) );
        uint16_t vendor_id = get_int_property( parent, "idVendor" );
        uint16_t product_id = get_int_property( parent, "idProduct" );

        if( vendor_id && product_id )
        {
            char cstring[HARDWARE_ID_STRING_LENGTH];

            if(serial_number.empty())
                serial_number = "None";

            int ret = snprintf( cstring, HARDWARE_ID_STRING_LENGTH, "USB VID:PID=%04x:%04x SNR=%s",
                vendor_id,
                product_id,
                serial_number.c_str() );

            if( (ret >= 0) && (ret < HARDWARE_ID_STRING_LENGTH) )
                port_info.hardware_id = cstring;
        }

        devices_found.push_back(port_info);
    }

    IOObjectRelease(serial_port_iterator);
    return devices_found;
}

#endif // defined(__APPLE__)
