// Copyright 1998-2000 Omni Development, Inc.  All rights reserved.
//
// This software may only be used and reproduced according to the
// terms in the file OmniSourceLicense.html, which should be
// distributed with this project and can also be found at
// http://www.omnigroup.com/DeveloperResources/OmniSourceLicense.html.
//
// $Header: /Network/Source/CVS/OmniGroup/./Applications/OmniCD/OmniCDAudio/OmniCDAudio_main.m,v 1.8 2000/09/12 19:08:49 bungi Exp $

#import <Foundation/Foundation.h>

#import <OmniBase/OBPostLoader.h>
#import <OmniSCSI/OSCSITarget.h>
#import <OmniSCSI/OSCSICDProtocols.h>

//
// This is a very lame filesystem tool.  The basic approach is that Workspace
// will invoke this when it detects new media is inserted.  First it will
// call each file system in turn with '-p sdN removable readonly' (in the case
// we're interested in).  Exit code 255 says that we are able to service
// the device.  Exit code 254 says that we aren't.  Then Workspace invokes
// us again with '-m sdN /path removable readonly' asking us to mount the file system.
// In this case, exit code 253 says that we did.
//
// It is currently unclear to me how it determines the path to use.  In the
// case of the cd9660.fs, it writes a cd9660.label file.  We currently don't
// really want to mount the device as a real filesystem.
//
// Later, we could implement a VFS module in a loadable kernel server that
// would allow you to copy the CDDA data off the disc.  If proper integration
// was made with CDDB or some other CD info database, we could even name
// the files based on the title.
//


static int scsi_deviceIndex(const char *sd);

static BOOL cdaudio_probe(int sd);
static BOOL cdaudio_mount(int sd, const char *mountPoint);
static BOOL cdaudio_unmount(int sd, const char *mountPoint);

static const char *pgm = NULL;

static void usage()
{
    fprintf(stderr, "usage: %s -p sdN [removable] [readonly]\n", pgm);
    fprintf(stderr, "       %s -m sdN /cdaudio [removable] [readonly]\n", pgm);
    fprintf(stderr, "       %s -u sd3 /cdaudio\n", pgm);
    exit(1);
}

int main (int argc, const char *argv[])
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    int argi;

    // Initialize the various Omni frameworks
    [OBPostLoader processClasses];

#ifdef DEBUG
    fprintf(stderr, "invoked with:");
    for (argi = 0; argi < argc; argi++) {
        fprintf(stderr, " %s", argv[argi]);
    }
    fprintf(stderr, "\n");
#endif
    
    pgm = argv[0];
    if (argc < 2)
        usage();

    if (!strcmp(argv[1], "-p")) {
        if (argc < 3)
            usage();
        if (cdaudio_probe(scsi_deviceIndex(argv[2])))
            exit(255);
        else
            exit(254);
    } else if (!strcmp(argv[1], "-m")) {
        if (argc < 4)
            usage();
        if (cdaudio_mount(scsi_deviceIndex(argv[2]), argv[3]))
            exit(253);
        else
#warning I do not know what this exit code should really be
            exit(1);
    } else if (!strcmp(argv[1], "-u")) {
        if (argc < 4)
            usage();
        if (cdaudio_unmount(scsi_deviceIndex(argv[2]), argv[3]))
            exit(253);
        else
#warning I do not know what this exit code should really be
            exit(1);
    } else
        usage();
    
    [pool release];
    exit(0);       // insure the process exit status is 0
    return 0;      // ...and make main fit the ANSI spec.
}


static int scsi_deviceIndex(const char *sd)
{
    const char *num = sd;
    int index = -1;
    
    if (!sd) {
        fprintf(stderr, "scsi_deviceIndex: device name is NULL\n");
        exit(1);
    }

    if (*num && *num == 's') {
        num++;
        if (*num && *num == 'd') {
            num++;
            index = atoi(num);
        }
    }

    if (index < 0) {
        fprintf(stderr, "scsi_deviceIndex: sd = '%s' is not a legal format.  Must be 'sd' followed by an non-negative integer.", sd);
        exit(1);
    }

    return index;
}

static BOOL cdaudio_probe(int sd)
{

#ifdef DEBUG
    fprintf(stderr, "%s: probing /dev/rsd%dh\n", pgm, sd);
#endif
    
    // This isn't really the most efficient way to do this, but it is easy
    NS_DURING {
        OSCSITarget                 *target;
        OSCSIUnit                   *unit;
        id <OSCSICDRomAudioControl>  audioUnit;

        target = [[OSCSITarget alloc] initWithBusNumber: OSCSI_INVALID_BUS_NUMBER
                                           targetNumber: OSCSI_INVALID_TARGET_NUMBER
                                          rawDeviceName: [NSString stringWithFormat: @"/dev/rsd%dh", sd]];

        unit = [target unitWithUnitNumber: 0];
        
        // The device must support playing audio discs
        if (![unit conformsToProtocol: @protocol(OSCSICDRomAudioControl)]){
            NSLog(@"Device doesn't implement OSCSICDRomAudioControl.");
            NS_VALUERETURN(NO, BOOL);
        }

        audioUnit = (id <OSCSICDRomAudioControl>) unit;
        [audioUnit updateStatus];

        // Read the table of contents to make sure it is a CDDA disc
#warning Actually this is not correct.  Data CDs have a TOC as well, it just happens that their tracks are data.  But since the cd9660 file system is currently getting probed before we are, it will filter out any discs that have a data track.  This is somewhat bad since the current approach does not handle mixed-mode discs (ie, those with both data and audio).
        if (![audioUnit tableOfContents]) {
            NSLog(@"No table of contents");
            NS_VALUERETURN(NO, BOOL);
        }
        
    } NS_HANDLER {
        NSLog(@"Error while probing: %@", localException);
        return NO;
    } NS_ENDHANDLER;

    return YES;
}

static BOOL cdaudio_mount(int sd, const char *mountPoint)
{
    NSString *path, *mountPointString, *fileName, *device, *contents;

    //
    // We'll write out a file that we will then ask Workspace to open.
    // Workspace will use the file extension to find a CD player
    // application.  This allows the user to specify what application
    // they want to use when playing CDs (although there is only
    // one currently).
    //
    // The only fly in our jam is that Workspace is single-threaded
    // (at least at this point in time).  Since it is asking us
    // to mount the device, it won't respond to our request to open
    // the cdaudioDevice file.
    //
    // So, we need to fork or otherwise do the open after this
    // program has exited.  This means that we have to return an
    // affirmative status to Workspace *before* we've done anything.
    // The only likely error is that there might be no application
    // other than TextEdit that will open the file.  In this case
    // we'll be left with a cd mounted that might not be umountable
    // (Workspace seems to turn of media removability).
    //
    // Audio discs aren't really mounted, but we'll pass along the
    // requested mount point to the application anyway.
    //
    // Workspace creates the mount point directory since BSD 4.4
    // requires that the directory be present when a mount is attempted.
    //
    // Since we don't really mount the disc (yet), we'll blow away
    // the directory.  Workspace doesn't seem to need it -- it
    // appears to find out about the mount some other way.  And,
    // if we leave the directory there, next time we load a CD
    // Workspace will append '_2', '_3' and so on to make unique
    // directories.  This causes tons of directory turds to be
    // left in /.
    //
    // Workspace (or Viewer) apparently looks at the path once
    // this tool returns.  So rather than delete it here with
    // NSFileManager, we'll delete it in the mini shell script
    // below (and we'll wait just a bit to give Workspace time
    // to do its check).
    //
    // Pretty gross, really.
    //
    // Too bad Workspace doesn't do the probe and mount operations
    // in their own thread.
    //
    
#ifdef DEBUG
    fprintf(stderr, "%s: mounting sd%d %s\n", pgm, sd, mountPoint);
#endif

    device = [NSString stringWithFormat: @"sd%d", sd];
    mountPointString = [NSString stringWithCString: mountPoint];

        
    fileName = [device stringByAppendingPathExtension: @"cdaudioDevice"];

    contents = [NSString stringWithFormat: @"%d %@", sd, mountPointString];
    
    path = [NSTemporaryDirectory() stringByAppendingPathComponent: fileName];
    [contents writeToFile: path atomically: YES];

#warning We should delete the sdN.cdaudioDevice file later.  For now it can be useful while debugging.
    {
        NSString *command;

        command = [NSString stringWithFormat: @"(/bin/sleep 1; /bin/rmdir %@; /usr/bin/open %@) &", mountPointString, path];

        [NSTask launchedTaskWithLaunchPath: @"/bin/sh"
                                 arguments: [NSArray arrayWithObjects: @"-c", command, nil]];
    }
    
    return YES;
}

//
// This doesn't currently get called since we don't really mount a filesystem
// for the CD audio disk.  But, if we did, we might do something like this.
//
static BOOL cdaudio_unmount(int sd, const char *mountPoint)
{

    fprintf(stderr, "%s: unmounting sd%d %s\n", pgm, sd, mountPoint);

    // This isn't really the most efficient way to do this, but it is easy
    NS_DURING {
        OSCSITarget                 *target;
        OSCSIUnit                   *unit;
        id <OSCSICDRomAudioControl>  audioUnit;

        target = [[OSCSITarget alloc] initWithBusNumber: OSCSI_INVALID_BUS_NUMBER
                                           targetNumber: OSCSI_INVALID_TARGET_NUMBER
                                          rawDeviceName: [NSString stringWithFormat: @"/dev/r%sh", mountPoint]];

        unit = [target unitWithUnitNumber: 0];
        
        // The device must support playing audio discs
        if (![unit conformsToProtocol: @protocol(OSCSICDRomAudioControl)]){
            NSLog(@"Device doesn't implement OSCSICDRomAudioControl.");
            NS_VALUERETURN(NO, BOOL);
        }

        audioUnit = (id <OSCSICDRomAudioControl>) unit;
        [audioUnit updateStatus];
        [(OSCSIUnit *)audioUnit eject];
    } NS_HANDLER {
        NSLog(@"Error while ejecting: %@", localException);
        return NO;
    } NS_ENDHANDLER;

    return YES;
}
