/**
 * HBDriveDetector.m
 * 8/17/2007
 * 
 * This file is part of the HandBrake source code.
 * Homepage: <http://handbrake.fr/>.
 * It may be used under the terms of the GNU General Public License.
 */

#include <IOKit/IOKitLib.h>
#include <IOKit/storage/IOMedia.h>
#include <IOKit/storage/IODVDMedia.h>

#import "HBDVDDetector.h"


@interface HBDVDDetector (Private)

- (NSString *)bsdNameForPath;
- (BOOL)pathHasVideoTS;
- (BOOL)deviceIsDVD;
- (io_service_t)getIOKitServiceForBSDName;
- (BOOL)isDVDService: (io_service_t)service;
- (BOOL)isWholeMediaService: (io_service_t)service;

@end


@implementation HBDVDDetector

+ (HBDVDDetector *)detectorForPath: (NSString *)aPath
{
    return [[[self alloc] initWithPath:aPath] autorelease];
}


- (HBDVDDetector *)initWithPath: (NSString *)aPath
{
    NSAssert(aPath, @"nil string passed to drive detector.");
	if( self = [super init] )	
	{
        path = [aPath retain];
        bsdName = nil;
	}
    return self;
}


- (void)dealloc
{
    [path release];
    path = nil;
    [bsdName release];
    bsdName = nil;
    [super dealloc];
}


- (BOOL)isVideoDVD
{
    if( !bsdName )
    {
        bsdName = [[self bsdNameForPath] retain];
    }
    return ( [self pathHasVideoTS] && [self deviceIsDVD] );
}


- (NSString *)devicePath
{
    if( !bsdName )
    {
        bsdName = [[self bsdNameForPath] retain];
    }
    return [NSString stringWithFormat:@"/dev/%@", bsdName];
}

@end


@implementation HBDVDDetector (Private)

- (NSString *)bsdNameForPath
{
    OSStatus err;
    FSRef ref;
    err = FSPathMakeRef( (const UInt8 *) [path fileSystemRepresentation],
                         &ref, NULL );	
    if( err != noErr )
    {
        return nil;
    }

    // Get the volume reference number.
    FSCatalogInfo catalogInfo;
    err = FSGetCatalogInfo( &ref, kFSCatInfoVolume, &catalogInfo, NULL, NULL,
                            NULL);
    if( err != noErr )
    {
        return nil;
    }
    FSVolumeRefNum volRefNum = catalogInfo.volume;

    // Now let's get the device name
    GetVolParmsInfoBuffer volumeParms;
    err = FSGetVolumeParms ( volRefNum, &volumeParms, sizeof( volumeParms ) );

    if( err != noErr )
    {
        return nil;
    }

    // A version 4 GetVolParmsInfoBuffer contains the BSD node name in the vMDeviceID field.
    // It is actually a char * value. This is mentioned in the header CoreServices/CarbonCore/Files.h.
    if( volumeParms.vMVersion < 4 )
    {
        return nil;
    }

    // vMDeviceID might be zero as is reported with experimental ZFS (zfs-119) support in Leopard.
    if( !volumeParms.vMDeviceID )
    {
        return nil;
    }

    return [NSString stringWithUTF8String:(const char *)volumeParms.vMDeviceID];
}


- (BOOL)pathHasVideoTS
{
    // Check one level under the path
    if( [[NSFileManager defaultManager] fileExistsAtPath:
        [path stringByAppendingPathComponent:@"VIDEO_TS"]] )
    {
        return YES;
    }

    // Now check above the path
    return [[path pathComponents] containsObject:@"VIDEO_TS"];
}


- (BOOL)deviceIsDVD
{
    io_service_t service = [self getIOKitServiceForBSDName];
    if( service == IO_OBJECT_NULL )
    {
        return NO;
    }
    BOOL result = [self isDVDService:service];
    IOObjectRelease(service);
    return result;
}


- (io_service_t)getIOKitServiceForBSDName
{
    CFMutableDictionaryRef  matchingDict;
    matchingDict = IOBSDNameMatching( kIOMasterPortDefault, 0, [bsdName UTF8String] );
    if( matchingDict == NULL )
    {
        return IO_OBJECT_NULL;
    }
	
    // Fetch the object with the matching BSD node name. There should only be
    // one match, so IOServiceGetMatchingService is used instead of
    // IOServiceGetMatchingServices to simplify the code.
    return IOServiceGetMatchingService( kIOMasterPortDefault, matchingDict );    
}


- (BOOL)isDVDService: (io_service_t)service
{
    // Find the IOMedia object that represents the entire (whole) media that the
    // volume is on. 
    //
    // If the volume is on partitioned media, the whole media object will be a
    // parent of the volume's media object. If the media is not partitioned, the
    // volume's media object will be the whole media object.
    // 
    // The whole media object is indicated in the IORegistry by the presence of
    // a property with the key "Whole" and value "Yes".

    // Create an iterator across all parents of the service object passed in.
    kern_return_t  kernResult;
    io_iterator_t  iter;
    kernResult = IORegistryEntryCreateIterator( service,
                                                kIOServicePlane,
                                                kIORegistryIterateRecursively | kIORegistryIterateParents,
                                                &iter );
    if( kernResult != KERN_SUCCESS )
    {
        return NO;
    }
    if( iter == IO_OBJECT_NULL )
    {
        return NO;
    }


    // A reference on the initial service object is released in the do-while loop below,
    // so add a reference to balance.
    IOObjectRetain( service );

    BOOL isDVD = NO;
    do
    {
        isDVD = ( [self isWholeMediaService:service] &&
                  IOObjectConformsTo(service, kIODVDMediaClass) );
        IOObjectRelease(service);
    } while( !isDVD && (service = IOIteratorNext(iter)) );
    IOObjectRelease( iter );

    return isDVD;
}


- (BOOL)isWholeMediaService: (io_service_t)service
{
    //
    // Determine if the object passed in represents an IOMedia (or subclass) object.
    // If it does, test the "Whole" property.
    //
    
    Boolean isWholeMedia = NO;
    
    if( IOObjectConformsTo(service, kIOMediaClass) )
    {
        CFTypeRef wholeMedia;
        wholeMedia = IORegistryEntryCreateCFProperty( service, 
                                                      CFSTR(kIOMediaWholeKey), 
                                                      kCFAllocatorDefault, 
                                                      0);
        if( !wholeMedia )
        {
            return NO;
        }
        isWholeMedia = CFBooleanGetValue( (CFBooleanRef)wholeMedia );
        CFRelease(wholeMedia);
    }

    return isWholeMedia;
}


@end