#!/usr/bin/env python

import os, time, sys, shutil

# Set to drive number (8-11) on IEC bus
IecDevice = 8

# Set to Windows drive letters to check for removable media
FlashDrives = [chr(x) for x in range(ord('e'), ord('n'))]

# Additional flags for nibread. This specifies 10 retries on errors.
OtherNibtoolFlags = '-e10'

# Fields to solicit from the user, including defaults
Config = (
    ['Manufacturer', None, 'publisher or your name'],
    ['Title', None, 'name of the disk or contents'],
    ['Year', None, 'year on disk sleeve'],
    ['Archive', 'y', 'save a copy for C64Preservation, y/n?'],
    ['Email', 'none@example.com', 'your email in case questions'],
    ['Notes', '', 'special notes or other info about disk'],
)

# Extensions for the data we create
ImageExt = ('.nib', '.g64', '.d64', '-d64.log', '-g64.log', '.log', '.txt')

ERR = sys.stderr


def FloppyCheck():
    p = os.popen('cbmctrl status %d' % IecDevice)
    status = p.read()
    if not StatusIsOk(status):
        return status
    p = os.popen('cbmctrl detect')
    detect = p.read()
    if detect.find('XP1541') == -1 and detect.find('XP1571') == -1:
        status = 'no parallel-capable drive found'
    return status

def StatusIsOk(status):
    return status[:2] == '00' or status[:2] == '73'

def WaitForFloppy():
    ret = os.system('cbmctrl change %d' % IecDevice)
    return ret

def WaitForFlash():
    while True:
        for d in FlashDrives:
            drive = '%c:/' % d
            try:
                os.stat(drive)
                return drive
            except OSError: pass
        time.sleep(1)

def NibreadFloppy(name):
    try:
        os.stat(name+'.nib')
        raise OSError('%s.nib exists, remove it first and try again' % name)
    except OSError: pass
    ret = os.system('nibread -D%d %s %s.nib > /dev/null' % (
        IecDevice, OtherNibtoolFlags, name))
    if ret:
        return ret
    ret = os.system('nibconv %s.nib %s.g64 > %s-g64.log' % (name, name, name))
    if ret:
        return ret
    ret = os.system('nibconv %s.nib %s.d64 > %s-d64.log' % (name, name, name))
    if ret:
        return ret
    return ret

def GetOption(cfgLine, argDefault=None):
    prompt, default, desc = cfgLine
    if argDefault:
        default = argDefault
    while True:
        defStr = ''
        if default:
            defStr = ', default '+default
        ret = raw_input('%s (%s%s): ' % (prompt, desc, defStr))
        ret = ret.strip()
        if ret == '':
            if default is None:
                print 'Sorry, you must enter something for this field'
                continue
            else:
                ret = default
        # boolean answers
        if default == 'y' or default == 'n':
            ret = ret.lower()
            if ret != 'y' and ret != 'n':
                print 'Sorry, response for this field must be y or n'
                continue
        break
    return ret

def GetOptions():
    print '''\nPlease enter some information about the floppy.
Spaces are ok and will be converted to "_" when used as parts of filenames.
Default values can be selected by just hitting ENTER. Items that don't have
defaults must be supplied.'''
    while True:
        # Initialize empty config if none currently known
        try:
            cfg
        except NameError:
            cfg = len(Config) * [(None, None)]
        print
        for i, line in enumerate(Config):
            cfg[i] = (line[0], GetOption(line, cfg[i][1]))
        print 'Here is what you entered:'
        print
        for line in cfg:
            print '%s: %s' % line
        print
        while True:
            ret = raw_input('Proceed? ').lower()
            if ret != 'y' and ret != 'n':
                print 'Sorry, response must be y or n'
                continue
            break
        if ret == 'n':
            continue
        break
    # Convert to a dict
    return dict(cfg)

def GetBasename(cfg):
    idx = 1
    while True:
        d = [cfg[x].replace(' ', '_') for x in ('Manufacturer', 'Title')]
        basename = '%s-%s-%d' % tuple(d+[idx])
        try:
            os.stat(basename+'.nib')
            idx += 1
        except OSError:
            break
    return basename

def WriteOptions(filename, cfg):
    fp = open(filename, 'w')
    print >>fp, 'Generated by nibbatch.py on %s\n' % time.ctime()
    for line in Config+('ImageBasename', 'FlashDrive'):
        try:
            k = line[0]
            print >>fp, '%s: %s' % (k, cfg[k])
        except KeyError: pass
    fp.close()

def CopyFilesToFlash(basename, drive):
    for ext in ImageExt:
        f = basename + ext
        shutil.copyfile(f, drive+f)

def RemoveBackupFiles(basename):
    for ext in ImageExt:
        f = basename + ext
        try:
            os.remove(basename+ext)
        except OSError:
            print >>ERR, "warning: couldn't remove %s, skipping" % (f)

def CopyLoop():
    print >>ERR, 'Batch nibbler initializing...',
    while True:
        status = FloppyCheck()
        if StatusIsOk(status):
            print >>ERR, 'ok'
            break
        else:
            print >>ERR, 'error'
            print >>ERR, 'Drive reports:', status
        raw_input('Hit ENTER to try again')

    # Main loop, get input from user and write images forever
    while True:
        print >>ERR, '\nPlease insert a floppy...',
        ret = WaitForFloppy()
        print >>ERR, 'found'
        print >>ERR, 'Waiting for flash drive...',
        drive = WaitForFlash()
        print >>ERR, 'found, drive', drive

        # Get options from the user, allowing defaults
        cfg = GetOptions()
        # Update default email to persist for the session
        for line in Config:
            if line[0] == 'Email':
                line[1] = cfg['Email']

        # Get a filename from the options and write out the info
        basename = GetBasename(cfg)
        cfg['ImageBasename'] = basename
        cfg['FlashDrive'] = drive
        WriteOptions(basename+'.txt', cfg)

        # Do the actual read
        print >>ERR, 'Starting read of floppy to file %s.nib...' % basename,
        try:
            ret = NibreadFloppy(basename)
        except OSError:
            print >>ERR, 'Error reading floppy, aborting. try again.'
            continue
        print >>ERR, 'done'

        # Copy the files to flash drive and a backup dir if user ok
        try:
            CopyFilesToFlash(basename, drive)
        except IOError:
            print >>ERR, 'Error: unable to copy files to flash but made backup ok.'
            print >>ERR, 'If you still want a copy, you need to make a manual one.'
            time.sleep(2)
            continue
        if cfg['Archive'] == 'n':
            print >>ERR, 'You requested we not keep image, removing it locally'
            RemoveBackupFiles(basename)

        os.system('sync')
        time.sleep(1)
        os.system('sync')
        print >>ERR, 'Done. You may now remove your floppy and flash drive.'
        print >>ERR, "If you're making another copy, you can leave the flash drive in."
        time.sleep(1)


if __name__ == '__main__':
    CopyLoop()
