/*
 * Apple // emulator for *ix
 *
 * This software package is subject to the GNU General Public License
 * version 3 or later (your choice) as published by the Free Software
 * Foundation.
 *
 * Copyright 2014, 2015 Aaron Culliney
 *
 */

#import "EmulatorDiskController.h"
#import "EmulatorPrefsController.h"
#import "common.h"

#define READONLY_CHOICE_INDEX 0
#define NO_DISK_INSERTED @"(No Disk Inserted)"
#define DSK_PROPERTIES @".dsk 143360 bytes"
#define NIB_PROPERTIES @".nib 232960 bytes"

#define kApple2DisksURL @"kApple2DisksURL"

@interface EmulatorDiskController ()

@property (assign) IBOutlet NSWindow *disksWindow;
@property (assign) IBOutlet NSTextField *diskInA;
@property (assign) IBOutlet NSTextField *diskInB;
@property (assign) IBOutlet NSTextField *diskAProperties;
@property (assign) IBOutlet NSTextField *diskBProperties;
@property (assign) IBOutlet NSMatrix *diskAProtection;
@property (assign) IBOutlet NSMatrix *diskBProtection;
@property (assign) IBOutlet NSButton *chooseDiskA;
@property (assign) IBOutlet NSButton *chooseDiskB;
@property (assign) IBOutlet NSButton *startupLoadDiskA;
@property (assign) IBOutlet NSButton *startupLoadDiskB;
@property (assign) IBOutlet NSButton *okButton;

- (void)loadPrefsForDomain:(const char *)domain;

@end

static EmulatorDiskController *diskInstance = nil;

static void prefsChangeCallback(const char *domain)
{
    (void)domain;
    assert(diskInstance);
    [diskInstance loadPrefsForDomain:domain];
}

@implementation EmulatorDiskController

- (void)awakeFromNib
{
#if CRASH_APP_ON_LOAD_BECAUSE_YAY_GJ_APPLE
    glGetError();
#endif
    assert(!diskInstance);
    diskInstance = self;

    prefs_registerListener(PREF_DOMAIN_VM, prefsChangeCallback);

    [self.diskInA setStringValue:NO_DISK_INSERTED];
    [self.diskAProperties setStringValue:@""];
    [self.diskInB setStringValue:NO_DISK_INSERTED];
    [self.diskBProperties setStringValue:@""];
    [self.chooseDiskA setKeyEquivalent:@"\r"];
    [self.chooseDiskA setBezelStyle:NSRoundedBezelStyle];
    [self.startupLoadDiskA setState:NSOffState];
    [self.startupLoadDiskB setState:NSOffState];
}

- (void)loadPrefsForDomain:(const char *)domain
{
    assert(strcmp(domain, PREF_DOMAIN_VM) == 0);
    (void)domain;

    {
        NSString *startupDiskA = nil;
        char *pathA = NULL;
        startupDiskA = prefs_copyStringValue(PREF_DOMAIN_VM, PREF_DISK_PATH_A, &pathA) ? [NSString stringWithUTF8String:pathA] : nil;
        FREE(pathA);

        bool bVal = false;
        BOOL readOnlyA = prefs_parseBoolValue(PREF_DOMAIN_VM, PREF_DISK_PATH_A_RO, &bVal) ? bVal : true;
        if (startupDiskA && [startupDiskA length])
        {
            const char *path = [startupDiskA UTF8String];
            int fdA = -1;
            TEMP_FAILURE_RETRY(fdA = open(path, readOnlyA ? O_RDONLY : O_RDWR));
            if (fdA == -1) {
                LOG("OOPS, open failed for path %s (%s)", path, strerror(errno));
            }
            const char *err = disk6_insert(fdA, 0, path, readOnlyA);
            if (fdA >= 0) {
                TEMP_FAILURE_RETRY(close(fdA));
            }
            if (!err)
            {
                [self.diskInA setStringValue:[[startupDiskA pathComponents] lastObject]];
                [self.startupLoadDiskA setState:NSOnState];
                [self.diskAProtection setState:(readOnlyA ? NSOnState : NSOffState) atRow:0 column:0];
                [self.diskAProtection setState:(!readOnlyA ? NSOnState : NSOffState) atRow:0 column:1];
            }
        }
    }
    
    {
        NSString *startupDiskB = nil;
        char *pathB = NULL;
        startupDiskB = prefs_copyStringValue(PREF_DOMAIN_VM, PREF_DISK_PATH_B, &pathB) ? [NSString stringWithUTF8String:pathB] : nil;
        FREE(pathB);

        bool bVal = false;
        BOOL readOnlyB = prefs_parseBoolValue(PREF_DOMAIN_VM, PREF_DISK_PATH_B_RO, &bVal) ? bVal : true;
        if (startupDiskB && [startupDiskB length])
        {
            const char *path = [startupDiskB UTF8String];
            int fdB = -1;
            TEMP_FAILURE_RETRY(fdB = open(path, readOnlyB ? O_RDONLY : O_RDWR));
            if (fdB == -1) {
                LOG("OOPS, open failed for path %s (%s)", path, strerror(errno));
            }
            const char *err = disk6_insert(fdB, 1, path, readOnlyB);
            if (fdB >= 0) {
                TEMP_FAILURE_RETRY(close(fdB));
            }
            if (!err)
            {
                [self.diskInB setStringValue:[[startupDiskB pathComponents] lastObject]];
                [self.startupLoadDiskB setState:NSOnState];
                [self.diskBProtection setState:(readOnlyB ? NSOnState : NSOffState) atRow:0 column:0];
                [self.diskBProtection setState:(!readOnlyB ? NSOnState : NSOffState) atRow:0 column:1];
            }
        }
    }
}

- (void)_savePrefs
{
    if (([self.startupLoadDiskA state] == NSOnState) && (disk6.disk[0].fd >= 0))
    {
        prefs_setStringValue(PREF_DOMAIN_VM, PREF_DISK_PATH_A, disk6.disk[0].file_name);
        NSButtonCell *readOnlyChoice = (NSButtonCell *)[[[self diskAProtection] cells] firstObject];
        prefs_setBoolValue(PREF_DOMAIN_VM, PREF_DISK_PATH_A_RO, ([readOnlyChoice state] == NSOnState));
    }
    else
    {
        prefs_setStringValue(PREF_DOMAIN_VM, PREF_DISK_PATH_A, "");
        prefs_setBoolValue(PREF_DOMAIN_VM, PREF_DISK_PATH_A_RO, true);
    }
    
    if (([self.startupLoadDiskB state] == NSOnState) && (disk6.disk[1].fd >= 0))
    {
        prefs_setStringValue(PREF_DOMAIN_VM, PREF_DISK_PATH_B, disk6.disk[1].file_name);
        NSButtonCell *readOnlyChoice = (NSButtonCell *)[[[self diskBProtection] cells] firstObject];
        prefs_setBoolValue(PREF_DOMAIN_VM, PREF_DISK_PATH_B_RO, ([readOnlyChoice state] == NSOnState) );
    }
    else
    {
        prefs_setStringValue(PREF_DOMAIN_VM, PREF_DISK_PATH_B, "");
        prefs_setBoolValue(PREF_DOMAIN_VM, PREF_DISK_PATH_B_RO, true);
    }

    //prefs_sync(PREF_DOMAIN_VM); -- do not sync here since that will trigger reload of startup disks
    prefs_save();
}

- (void)_protectionChangedForDrive:(int)drive
{
    if (disk6.disk[drive].fd < 0)
    {
        return;
    }
    // HACK NOTE : dispatch so that state of outlet property is set properly
    dispatch_async(dispatch_get_main_queue(), ^{
        NSButtonCell *readOnlyChoice = (NSButtonCell *)[[(drive == 0 ? [self diskAProtection] : [self diskBProtection]) cells] firstObject];
        NSString *path = [NSString stringWithUTF8String:disk6.disk[drive].file_name];
        [self _insertDisketteInDrive:drive path:path type:[EmulatorDiskController extensionForPath:path] readOnly:([readOnlyChoice state] == NSOnState)];
        [self _savePrefs];
    });
}

- (IBAction)diskAProtectionChanged:(id)sender
{
    [self _protectionChangedForDrive:0];
}

- (IBAction)diskBProtectionChanged:(id)sender
{
    [self _protectionChangedForDrive:1];
}

- (BOOL)_insertDisketteInDrive:(int)drive path:(NSString *)path type:(NSString *)type readOnly:(BOOL)readOnly
{
    disk6_eject(drive);
    
    int fd = -1;
    const char *cPath = [path UTF8String];
    TEMP_FAILURE_RETRY(fd = open(cPath, readOnly ? O_RDONLY : O_RDWR));
    if (fd == -1) {
        LOG("OOPS, open failed for path %s (%s)", cPath, strerror(errno));
    }
    const char *errMsg = disk6_insert(fd, drive, cPath, readOnly);
    if (fd >= 0) {
        TEMP_FAILURE_RETRY(close(fd));
    }
    if (errMsg)
    {
        NSAlert *alert = [NSAlert alertWithError:[NSError errorWithDomain:[NSString stringWithUTF8String:errMsg] code:-1 userInfo:nil]];
        [alert beginSheetModalForWindow:[self disksWindow] completionHandler:nil];
        if (!drive)
        {
            [[self diskInA] setStringValue:NO_DISK_INSERTED];
            [[self diskAProperties] setStringValue:@""];
        }
        else
        {
            [[self diskInB] setStringValue:NO_DISK_INSERTED];
            [[self diskBProperties] setStringValue:@""];
        }
        return NO;
    }
    path = [NSString stringWithUTF8String:disk6.disk[drive].file_name];
    NSString *imageName = [[path pathComponents] lastObject];

    if (drive == 0)
    {
        [[self diskInA] setStringValue:imageName];

        bool isStartupDiskA = false;
        {
            char *pathA = NULL;
            if (prefs_copyStringValue(PREF_DOMAIN_VM, PREF_DISK_PATH_A, &pathA)) {
                isStartupDiskA = (strcmp(pathA, disk6.disk[drive].file_name) == 0);
            }
            FREE(pathA);
        }

        if (isStartupDiskA)
        {
            [self.startupLoadDiskA setState:NSOnState];
        }
    }
    else
    {
        [[self diskInB] setStringValue:imageName];

        bool isStartupDiskB = false;
        {
            char *pathB = NULL;
            if (prefs_copyStringValue(PREF_DOMAIN_VM, PREF_DISK_PATH_B, &pathB)) {
                isStartupDiskB = (strcmp(pathB, disk6.disk[drive].file_name) == 0);
            }
            FREE(pathB);
        }

        if (isStartupDiskB)
        {
            [self.startupLoadDiskB setState:NSOnState];
        }
    }
    
    if ([type isEqualToString:@"dsk"] || [type isEqualToString:@"do"] || [type isEqualToString:@"po"])
    {
        if (!drive)
        {
            [[self diskAProperties] setStringValue:DSK_PROPERTIES];
        }
        else
        {
            [[self diskBProperties] setStringValue:DSK_PROPERTIES];
        }
    }
    else
    {
        if (!drive)
        {
            [[self diskAProperties] setStringValue:NIB_PROPERTIES];
        }
        else
        {
            [[self diskBProperties] setStringValue:NIB_PROPERTIES];
        }
    }
    
    return YES;
}

+ (NSSet *)emulatorFileTypes
{
    static NSSet *set = nil;
    static dispatch_once_t onceToken = 0L;
    dispatch_once(&onceToken, ^{
        set = [[NSSet alloc] initWithObjects:@"dsk", @"nib", @"do", @"po", @"gz", nil];
    });
    return [[set retain] autorelease];
}

+ (NSString *)extensionForPath:(NSString *)path
{
    NSString *extension0 = [path pathExtension];
    NSString *extension1 = [[path stringByDeletingPathExtension] pathExtension];
    if ([extension0 isEqualToString:@"gz"])
    {
        extension0 = extension1;
    }
    return [extension0 lowercaseString];
}

+ (void)chooseDiskForWindow:(NSWindow *)window completionHandler:(DiskCompletionHandler)handler
{
    NSOpenPanel *openPanel = [[NSOpenPanel openPanel] retain];
    [openPanel setTitle:@"Choose a disk image"];
    NSURL *url = [[NSUserDefaults standardUserDefaults] URLForKey:kApple2DisksURL];
    if (!url)
    {
        url = [[NSBundle mainBundle] URLForResource:@"images" withExtension:nil];
    }
    [openPanel setDirectoryURL:url];
    [openPanel setShowsResizeIndicator:YES];
    [openPanel setShowsHiddenFiles:NO];
    [openPanel setCanChooseFiles:YES];
    [openPanel setCanChooseDirectories:NO];
    [openPanel setCanCreateDirectories:NO];
    [openPanel setAllowsMultipleSelection:NO];
    
    // NOTE : Doesn't appear to be a way to specify ".dsk.gz" ... so we may inadvertently allow files of ".foo.gz" here
    NSSet *fileTypes = [EmulatorDiskController emulatorFileTypes];
    [openPanel setAllowedFileTypes:[fileTypes allObjects]];
    handler = Block_copy(handler);
    [openPanel beginSheetModalForWindow:window completionHandler:^(NSInteger result) {
        handler(openPanel, result);
        Block_release(handler);
        [openPanel autorelease];
    }];
}

- (void)_chooseDisk:(int)drive readOnly:(BOOL)readOnly
{
    [EmulatorDiskController chooseDiskForWindow:[self disksWindow] completionHandler:^(NSOpenPanel *openPanel, NSInteger result) {
        if (result == NSOKButton)
        {
            NSURL *selection = [[openPanel URLs] firstObject];
            NSString *path = [[selection path] stringByResolvingSymlinksInPath];
            NSString *extension = [EmulatorDiskController extensionForPath:path];
            NSSet *fileTypes = [EmulatorDiskController emulatorFileTypes];

            NSString *directory = [path stringByDeletingLastPathComponent];
            NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
            [defaults setURL:[NSURL URLWithString:directory] forKey:kApple2DisksURL];

            if (![fileTypes containsObject:extension])
            {
                NSAlert *alert = [NSAlert alertWithError:[NSError errorWithDomain:@"Disk image must have file extension of .dsk, .do, .po, or .nib only" code:-1 userInfo:nil]];
                [alert beginSheetModalForWindow:[self disksWindow] completionHandler:nil];
                return;
            }

            [self.chooseDiskA setKeyEquivalent:@""];
            [self.okButton setKeyEquivalent:@"\r"];

            [(drive == 0) ? self.startupLoadDiskA : self.startupLoadDiskB setState:NSOffState];
            [self _insertDisketteInDrive:drive path:path type:extension readOnly:readOnly];
        }
    }];
}

- (IBAction)chooseDriveA:(id)sender
{
    NSButtonCell *readOnlyChoice = (NSButtonCell *)[[[self diskAProtection] cells] firstObject];
    [self _chooseDisk:0 readOnly:([readOnlyChoice state] == NSOnState)];
}

- (IBAction)chooseDriveB:(id)sender
{
    NSButtonCell *readOnlyChoice = (NSButtonCell *)[[[self diskBProtection] cells] firstObject];
    [self _chooseDisk:1 readOnly:([readOnlyChoice state] == NSOnState)];
}

- (IBAction)startupDiskAChoiceChanged:(id)sender
{
    [self _savePrefs];
}

- (IBAction)startupDiskBChoiceChanged:(id)sender
{
    [self _savePrefs];
}

- (IBAction)disksOK:(id)sender
{
    [self.chooseDiskA setKeyEquivalent:@"\r"];
    [self.okButton setKeyEquivalent:@""];

    [[self disksWindow] close];
}

@end
