#! /usr/bin/env python

import sys
import time

import isi.hw.hal as hal

import isi.hw.check.lib.args as argslib
import isi.hw.check.lib.eeprom as eeprom
import isi.hw.check.lib.filenames as files

_vpdfile = files.node_vpd_data_file()
_debug = False
_verbose = True
_force = False
_single = False

def verbose(msg):
    if _verbose:
        print msg

def error(msg, err=None):
    print msg
    if err:
        print err

def opt2int(opt, base=16):
    r, x = 0, None
    try:
        x = int(opt)
    except ValueError:
        try:
            x = int(opt, base)
        except ValueError:
            r = 1
            error('Unable to convert arg "%s" to int' % opt)
    return r, x

def init(vpd, options):
    # optionally have a flag to force no prompt;
    verbose("Initializing EEPROM data...")
    err = vpd.eeprom_init(force=_force)
    if err:
        error("Unable to initialize EEPROM VPD data:", err)
    r = err and 1 or 0
    return r

def initrev(vpd, options):
    # optionally have a flag to force no prompt;
    r, version = opt2int(options['version'], base=10)
    if not r:
        verbose("Initializing EEPROM data for rev %d..." % version)
        err = vpd.eeprom_init(rev=version, force=_force)
        if err:
            error("Unable to initialize EEPROM VPD data:", err)
        r = err and 1 or 0
    return r

def check(vpd, options):
    verbose("Checking EEPROM data...")
    # we can pass specific checks to this func w/ keyword checks=[...]
    err, temp = vpd.eeprom.check_data()
    if err:
        error("EEPROM header/data invalid: Consider re-init?:", err)
    else:
        verbose("EEPROM header and data appear valid!")
    r = err and 1 or 0
    return r

# Consider: making _vpdfile an option.
# This allows syncing EEPROM vpd data with a user-specified non-default
# file, e.g., a /tmp/ located file, for integration with isi_dongle_sync,
# for single call with isi_dongle_sync "pull" :
#    {[dongle], [eeprom]} --> {temp} --> {<legacy files>, <vpd file>}
# This requires careful consideration and integration of the current
# coding paradigm in (cmds_list,cmds_desc) methods, 'func', and the
# generic calls:
# argfunc = cmds_desc[cmd]['argfunc'] --> func_options
# f = cmds_desc[cmd]['func'] = sync(), result = f(vpd, func_options)

def sync(vpd, options):
    verbose("Syncing EEPROM data with local files...")
    err = vpd.update('sync')
    if err:
        error("EEPROM sync with local files failed:", err)
    r = err and 1 or 0
    return r

def push(vpd, options):
    verbose("Pushing VPD data to EEPROM from local files...")
    err = vpd.update('push')
    if err:
        error("EEPROM push with local files failed:", err)
    r = err and 1 or 0
    return r

def pull(vpd, options):
    verbose("Pulling VPD data from EEPROM to local files...")
    err = vpd.update('pull')
    if err:
        error("EEPROM pull with local files failed:", err)
    r = err and 1 or 0
    return r

def dump(vpd, options):
    verbose("Reading data from EEPROM...")
    err, lines = vpd.eeprom.dump_data(options={'single':_single})
    if err:
        error("Error reading EEPROM data:", err)
    else:
        verbose('EEPROM data dump:')
        for line in lines:
            print line # Non-verbose
    r = err and 1 or 0
    return r

# Optional args: offset, length
def dumpraw(vpd, options):
    verbose("Reading (raw) data from EEPROM...")
    try:
        opt = options['off']
        r, offset = opt2int(opt)
        if r:
            return r
    except KeyError:
        offset = 0
    try:
        opt = options['len']
        r, length = opt2int(opt)
        if r:
            return r
    except KeyError:
        length = None # Default to entire EEPROM
    err, lines = vpd.eeprom.dump_raw(offset, length, block_protect=False)
    if err:
        error("Error reading EEPROM (raw) data:", err)
    else:
        verbose('EEPROM (raw) data dump:')
        for line in lines:
            print line # Non-verbose
    r = err and 1 or 0
    return r

def dumphdr(vpd, options):
    verbose("Reading header data from EEPROM...")
    raw = _debug
    err, lines = vpd.eeprom.dump_header(raw=raw)
    if err:
        error("Error reading EEPROM header:", err)
    else:
        verbose('EEPROM header dump:')
        if raw:
            err, temp = vpd.eeprom.check_data()
            if err:
                verbose('NOTE: HEADER IS NOT VALID! Debug mode, forcing dump.')
                verbose('%s' % err)
                err = None
        if isinstance(lines, list ):
            for line in lines:
                print line # Non-verbose
    r = err and 1 or 0
    return r

def _read(vpd, keys):
    verbose("Reading data from EEPROM: keys=%s" % keys)
    if not len(keys):
        return 0 # No data to read
    err, data = vpd.eeprom.read_data()
    if err:
        error("Failure EEPROM Read (data):", err)
    else:
        read = {}
        for key in keys:
            try:
                value = data[key]
            except KeyError:
                err = "Failure EEPROM Read: one or more keys not found"
                value = None
            read[key] = value
        width = max(map(lambda key:len(key), read.keys()))
        errval = "Error: key not found"
        for key in keys:
            found = key in read and not read[key] is None
            value = not found and errval or read[key]
            if _single:
                value = found and len(value) and value[0] or ''
            print "%-*s = %s" % (width, key, value) # Non-verbose
        if err:
            error(err)
    r = err and 1 or 0
    return r

# Requires arg: key
# This requires reading all data with vpd.eeprom.read_data,
# then parsing results... unless we add a new method to eeprom?
def read(vpd, options):
    keys = [ options['key'] ]
    r = _read(vpd, keys)
    return r

# Requires args: <count> + keys
# This requires reading all data with vpd.eeprom.read_data,
# then parsing results... unless we add a new method to eeprom?
def read_n(vpd, options):
    r, count = opt2int(options['len'], base=10)
    if r:
        return r
    # keys should be a list of keys per len_keys_get_args
    keys = options['[keys]']
    if None in [count,keys]:
        return 1
    r = _read(vpd, keys)
    return r

def _delete(vpd, keys):
    verbose("Deleting data from EEPROM: keys=%s" % keys)
    if not len(keys):
        return 0 # No data to delete
    err = vpd.delete(keys)
    if err:
        error("Failure EEPROM Delete:", err)
    r = err and 1 or 0
    return r

# Requires arg: key
def delete(vpd, options):
    keys = [ options['key'] ]
    r = _delete(vpd, keys)
    return r

# Requires args: <count> + keys
def delete_n(vpd, options):
    r, count = opt2int(options['len'], base=10)
    if r:
        return r
    # keys should be a list of keys per len_keys_get_args
    keys = options['[keys]']
    if None in [count,keys]:
        return 1
    r = _delete(vpd, keys)
    return r

def _write(vpd, data):
    verbose("Writing data to EEPROM: %s" % data)
    if not len(data):
        return 0 # No data to write
    err, temp_info = vpd.sync_vpd(new_vpd_data=data, priority='eeprom_only')
    if err:
        error("Failure VPD Update (data=%s):" % data, err)
    else:
        err = vpd.eeprom_update()
        if err:
            error("Failure EEPROM Write (data=%s):" % data, err)
    r = err and 1 or 0
    return r

# Requires arg: key:value dict pair
def write(vpd, options):
    userdata = { options['key'] : options['value'] }
    r = _write(vpd, userdata)
    return r

# Requires args: <count> + key:value dict pairs
def write_n(vpd, options):
    r, count = opt2int(options['len'], base=10)
    if r:
        return r
    # userdata should be a dict of key:value pairs per len_key_value_get_args
    userdata = options['[key value]']
    if None in [count,userdata]:
        return 1
    r = _write(vpd, userdata)
    return r

# Requires arg: offset, length, list
def writeraw(vpd, options):
    r, offset = opt2int(options['off'])
    if r:
        return r
    r, length = opt2int(options['len'])
    if r:
        return r

    # data should already be a list of ints per off_len_data_get_args
    data = options['data']
    if None in [offset,length,data]:
        return 1
    verbose("Writing data %s to EEPROM at offset 0x%x..." % (data,offset))
    err = vpd.eeprom.write_raw(offset, data)
    if err:
        error("Failure EEPROM Write (data=%s):" % data, err)
    r = err and 1 or 0
    return r

def len_keys_get_args(usercmds, cmdargs):
    # Last opt '[keys]' is variable len, >= 1, of keys
    valid, options = True, {}
    numarg = len(cmdargs) - 1
    userargs = usercmds[:numarg]
    usercmds = usercmds[numarg:]
    if len(userargs) != numarg:
        valid = False
    if valid:
        options = dict(zip(cmdargs,userargs))
        r, datalen = opt2int(options['len'], 10)
        if r:
            valid = False
        elif datalen is None:
            valid = False
    if valid:
        dataargs = usercmds[:datalen]
        usercmds = usercmds[datalen:]
        if len(dataargs) != datalen:
            valid = False
    if valid:
        data = dataargs
        options['[keys]'] = data
    return valid, options, usercmds

def len_key_value_get_args(usercmds, cmdargs):
    # Last opt '[key value]' is variable len, >= 1, of key:value pairs
    valid, options = True, {}
    numarg = len(cmdargs) - 1
    userargs = usercmds[:numarg]
    usercmds = usercmds[numarg:]
    if len(userargs) != numarg:
        valid = False
    if valid:
        options = dict(zip(cmdargs,userargs))
        r, datalen = opt2int(options['len'], 10)
        if r:
            valid = False
        elif datalen is None:
            valid = False
    if valid:
        datalen *= 2 # key,value
        dataargs = usercmds[:datalen]
        usercmds = usercmds[datalen:]
        if len(dataargs) != datalen:
            valid = False
    if valid:
        data = [[dataargs[x] for x in range(y,datalen,2)] for y in range(2)]
        data = dict(zip(*data))
        options['[key value]'] = data
    return valid, options, usercmds

def off_len_data_get_args(usercmds, cmdargs):
    # Last opt 'data' is variable len, >= 1
    valid, options = True, {}
    numarg = len(cmdargs) - 1
    userargs = usercmds[:numarg]
    usercmds = usercmds[numarg:]
    if len(userargs) != numarg:
        valid = False
    if valid:
        options = dict(zip(cmdargs,userargs))
        r, datalen = opt2int(options['len'])
        if r:
            valid = False
        elif datalen is None:
            valid = False
    if valid:
        dataargs = usercmds[:datalen]
        usercmds = usercmds[datalen:]
        if len(dataargs) != datalen:
            valid = False
    if valid:
        data = []
        for arg in dataargs:
            r, d = opt2int(arg)
            if r:
                valid = False
                break
            data.append(d)
    if valid:
        invalid = filter(lambda x: not isinstance(x,int), data)
        if len(invalid):
            valid = False
    if valid:
        options['data'] = data
    return valid, options, usercmds

def generic_get_args(usercmds, cmdargs):
    options = None
    valid = True
    numarg = len(cmdargs)
    userargs = usercmds[:numarg]
    usercmds = usercmds[numarg:]
    if len(userargs) != numarg:
        valid = False
    else:
        options = dict(zip(cmdargs,userargs))
    return valid, options, usercmds

def dumppbb():
    print 'Power Bridge Board EEPROM data dump:'
    result = 0
    bus = hal.getPowerSupplyBus()
    # XXX Bug 82926: Use hal usage for addr, size
    addr = 0xa1
    size = 256
    pbb_eeprom = eeprom.Eeprom8Bit(bus=bus, addr=addr, size=size)
    err, info = pbb_eeprom.data_dump()
    if err:
        return 1
    for (key_width, name, value) in info:
        print '%-*s : %s' % (key_width, name, value)
    return result

opts_desc = [
    ('-s, --single',   'Single value dumps (no history)'),
    ('-f, --force',    'Force prompting (no confirms)'),
    ('-d, --debug',    'Debug mode'),
    ('-q, --quiet',    'Quiet mode'),
    ('-h, --help',     'Show this text'),
    ]

cmds_list = [
    ('init',
    { 'func': init,
      'title': 'EEPROM Init to latest version (destructive)',
      }),
    ('initrev',
    { 'func': initrev,
      'title': 'EEPROM Init with given version (destructive)',
      'args' : ['version'],
      }),
    ('check',
    { 'func': check,
      'title': 'EEPROM Data Check (validate header, data)',
      }),
    ('push',
    { 'func': push,
      'title': 'Push data - to EEPROM from local VPD files',
      }),
    ('pull',
    { 'func': pull,
      'title': 'Pull data - from EEPROM to local VPD files',
      }),
    ('sync',
    { 'func': sync,
      'title': 'Sync data - EEPROM with local VPD files (uses timestamps)',
      }),
    ('dump',
    { 'func': dump,
      'title': 'Dump EEPROM - data, formatted',
      }),
    ('dumphdr',
    { 'func': dumphdr,
      'title': 'Dump EEPROM - header, formatted',
      }),
    ('dumpraw',
    { 'func': dumpraw,
      'title': 'Dump EEPROM - raw data, at given offset + length',
      'args': ['off','len'],
      'raw':True,
      }),
    ('dumpall',
    { 'func': dumpraw,
      'title': 'Dump EEPROM - raw data, ALL (long!)',
      'raw':True,
      }),
    ('read',
    { 'func': read,
      'title': 'Read EEPROM - read given key (search)',
      'args': ['key'],
      }),
    ('readn',
    { 'func': read_n,
      'title': 'Read EEPROM - read "len" keys (search)',
      'args': ['len','[keys]'],
      'argfunc': len_keys_get_args,
      }),
    ('delete',
    { 'func': delete,
      'title': 'Write EEPROM - delete given key',
      'args': ['key'],
      }),
    ('deleten',
    { 'func': delete_n,
      'title': 'Write EEPROM - delete "len" keys',
      'args': ['len','[keys]'],
      'argfunc': len_keys_get_args,
      }),
    ('write',
    { 'func': write,
      'title': 'Write EEPROM - write given key:value pair',
      'args': ['key','value'],
      }),
    ('writen',
    { 'func': write_n,
      'title': 'Write EEPROM - write "len" key:value pairs',
      'args': ['len','[key value]'],
      'argfunc': len_key_value_get_args,
      }),
    ('writeraw',
    { 'func': writeraw,
      'title': 'Write EEPROM - write raw data to given offset',
      'args': ['off','len','data'],
      'argfunc': off_len_data_get_args,
      'raw':True,
      }),
    ]

commands = [ c for (c,x) in cmds_list ]
cmds_desc = dict(cmds_list)
fmt_cmdopt = lambda c: (cmds_desc[c].has_key('args') and
                     reduce(lambda a,b:a+' '+b, cmds_desc[c]['args'])
                     or '')
fullcmdslist = map(lambda c:"%s %s" % (c, fmt_cmdopt(c)), commands)
fullcmds = dict(zip(commands, fullcmdslist))
argwidth = 1 + max(map(lambda x:len(x), fullcmdslist))
fmt_opt = lambda (o,d): "  %-*s: %s" % (argwidth,o,d)
fmt_cmd = lambda c: "  %-*s: %s" % (argwidth, fullcmds[c], cmds_desc[c]['title'])
disp_opts = reduce(lambda a,b: a+"\n"+b, map(fmt_opt, opts_desc))
disp_cmds = reduce(lambda a,b: a+"\n"+b, map(fmt_cmd, commands))

def main(options):
    global _debug, _verbose, _force, _single

    result = 0
    show_usage = False
    args = argslib.Args(options)
    non_opts = args.process(sys.argv[1:])
    usercmds = non_opts
    if len(usercmds) == 0:
        raise argslib.Error('No command specified')

    if args['debug']:
        _debug = True
    if args['quiet']:
        _verbose = False
    if args['force']:
        _force = True
    if args['single']:
        _single = True

    vpdoptions = {
        'verbose': _verbose,
        'debug': _debug,
        'unit_eeprom_test': args['unittest'],
        }

    # Initializing the vpd automatically upgrades it.  If we're going to
    # initialize the EEPROM after, might as well skip it.
    vpdoptions['skip_upgrade'] =  'init' in usercmds or 'initrev' in usercmds

    # If only using "raw" commands skip the init read of EEPROM header rev
    actualcmds = filter(lambda c: c in commands, usercmds)
    rawcases = [ cmds_desc.get(c).get('raw') for c in actualcmds ]
    rawonly = len(filter(lambda x: x!=True, rawcases)) == 0
    if rawonly:
        vpdoptions['raw'] = True
    # dumppbb() is a hidden command
    if 'dumppbb' in usercmds:
        actualcmds.append('dumppbb')
    if len(actualcmds) == 0:
        raise argslib.Error('No valid command specified')

    import isi.hw.check.lib.vpd as vpdlib
    try:
        vpd = vpdlib.VPD(options=vpdoptions)
    except Exception as e:
        error('%s: %s' % (type(e), e))
        vpd = None

    if vpd is None:
        error('Could not init EEPROM VPD object')
        result = 1
    else:

        while len(usercmds):

            valid, func_options = True, {}
            cmd = usercmds.pop(0)
            if cmd == 'dumppbb':
                result = dumppbb()
                if result:
                    error('Unable to dump Power Bridge Board EEPROM data')
                continue
            try:
                f = cmds_desc[cmd]['func']
            except KeyError:
                f = None
                error("No valid function found for cmd '%s'" % cmd)
                result, valid = 1, False
                show_usage = True
            if valid:
                try:
                    cmdargs = cmds_desc[cmd]['args']
                except KeyError:
                    cmdargs = None
                if cmdargs:
                    try:
                        argfunc = cmds_desc[cmd]['argfunc']
                    except KeyError:
                        argfunc = generic_get_args
                    valid, func_options, usercmds = argfunc(usercmds, cmdargs)
                    if not valid:
                        error("Insufficient or Invalid args for cmd '%s'" % cmd)
                        result = 1
                        show_usage = True

            if valid:
                try:
                    result = f(vpd, func_options)
                except IOError, e:
                    error("IOError during '%s':" % cmd, e)
                    result, valid = 1, False

    if show_usage:
        raise argslib.Error('Invalid command usage')

    return result

if __name__ == '__main__':
    usage = \
"""Usage: %s [OPTIONS] command [commands]

OPTIONS:
%s

COMMANDS:
%s

Isilon EEPROM VPD (Vital Product Data) Tool.
Provides Init, Check, Push, Pull, Sync, Dump, Read, Write commands.
Local VPD file: %s
Legacy VPD files listed in /etc/ifs/dongle_sync_files
Multiple commands are allowed.  Sample:
isi_fcb_vpd_tool write a 23 writen 2 foo bar keyB valB dumphdr dump read foo
""" % (argslib.appname(), disp_opts, disp_cmds, _vpdfile)
    
    options = {
        
        'single':
        {'opts'    : ['s', 'single'],
         'default' : False,
         'func'    : argslib.true_func(),
         },

        'force':
        {'opts'    : ['f', 'force'],
         'default' : False,
         'func'    : argslib.true_func(),
         },

        'debug':
        {'opts'    : ['d', 'debug'],
         'default' : False,
         'func'    : argslib.true_func(),
         },

        'quiet':
        {'opts'    : ['q', 'quiet'],
         'default' : False,
         'func'    : argslib.true_func(),
         },

        'unittest':
        {'opts'    : ['unittest'],
         'default' : False,
         'func'    : argslib.true_func(),
         },
        
        'help':
        {'opts'    : ['h', 'help'],
         'func'    : argslib.help_func(usage),
         },
        }

    sys.exit(argslib.run(lambda :main(options), usage))
