#! /usr/bin/env python

import os.path
import sys

import isi.hw.check.lib.args as argslib
import isi.hw.check.lib.vpd as vpdlib

# Starting backwards from Reserved Manufacturing Scratch Data: Version 1:
# 0xfff0  XX XX XX XX  Manufacturing Scratch Data ...       16B Total
# 0xffec  SS SS VV VV  Header: 2B Data Size, 2B Version  \__ 8B Header
# 0xffe8  CC CC CC CC  4B CRC Protection (Data)          /
# 0xffe4  CC CC CC CC  4B CRC Protection (Header)            4B Header CRC
# 0xffe0  DD DD DD DD  Data ...

EEPROM_FILE_VERSION = 1
EEPROM_FILE_DATA_ALIGNMENT = 0x40

# get spec using get_eeprom_file_spec(vpd, version), which sets header_offset
EEPROM_FILE_SPEC = {
    1: { 'header_size': 12, # Including 4B Header CRC at (8,12)
         'header_range_crc': (0,8), # Not incl Header CRC
         'header_loc_version': (0,2),
         'header_loc_data_size': (2,4),
         'header_loc_data_crc': (4,8),
         'header_loc_hdr_crc': (8,12),
         'data_offset': 12,
         },
    }

_vpd = None
_debug = False
_verbose = False

def verbose(msg):
    if _verbose:
        print msg

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

def get_eeprom_file_spec(version):
    spec = EEPROM_FILE_SPEC.get(version)
    if spec:
        header_size = spec.get('header_size',0)
        header_offset = _vpd.eeprom.scratchpad_offset - header_size
        spec.update({
            'header_offset': header_offset,
            })
    return spec

def reverse_slice_lookup(data, start, end, step=1):
    # d[::-1] reverses d, as opposed to d.reverse() which reverse in-place
    # Determine sequence slice in "reverse order", but return the data in
    # actual order -- so two reverses are necessary.
    return data[::-1][start:end:step][::-1]

def eeprom_data_lookup(data, spec, loc_key):
    if loc_key in spec:
        start, end = spec[loc_key]
        return reverse_slice_lookup(data, start, end)
    return None

def eeprom_word_lookup(data, spec, loc_key):
    lookup = eeprom_data_lookup(data, spec, loc_key)
    if lookup:
        return _vpd.eeprom.byte_list_to_word(lookup, length=len(lookup))
    return None

def crc(data):
    return _vpd.eeprom.crc.calc(data, len(data))

def get_vpd_info():
    err, data = _vpd.eeprom.get_header_data()
    if err:
        error("Error reading EEPROM header data:", err)
        return
    err, hdr = _vpd.eeprom.header_data_bytes_to_dict(data)
    if err:
        error("Error converting EEPROM header data:", err)
        return
    eeprom_size = hdr.get('total_size', 0)
    vpd_end = hdr.get('page1_offset', 0) + hdr.get('page_size', 0)
    if not eeprom_size or not vpd_end:
        error("Unable to determine EEPROM VPD data usage")
        return
    elif eeprom_size != _vpd.eeprom.size:
        error("EEPROM Size Mismatch: Header reported %d, VPD class "
              "reported %d" % (eeprom_size, _vpd.eeprom.size))
        return
    vpd_info = {
        'eeprom_size': eeprom_size,
        'vpd_end': vpd_end
        }
    return vpd_info

def get_eeprom_info():
    eeprom_info = {
        'version': None,
        'header_size': None,
        'header_offset': None,
        'header_crc': None,
        'data_size': None,
        'data_offset': None,
        'data_crc': None,
        'raw': None,
        'data': None,
        'valid': False,
        }
    vpd_info = get_vpd_info()
    if not vpd_info:
        error("Unable to determine EEPROM VPD Info")
        return
    # Assume header is version 1 until known otherwise from EEPROM read
    # For absolute simplicity, version data is NOT CRC protected:
    # Complexity if future header versions have different sizes or layouts
    version = 1
    spec = get_eeprom_file_spec(version)
    if not spec:
        error("No EEPROM File Spec defined for version %s" % version)
        return
    header_size, header_offset = spec['header_size'], spec['header_offset']
    err, hdr = _vpd.eeprom.read_raw(header_offset, header_size)
    if err:
        error("EEPROM Read Error: Header: %s" % err)
        return
    # Check actual version
    version = eeprom_word_lookup(hdr, spec, 'header_loc_version')
    if version == 1:
        eeprom_info['version'] = version
        eeprom_info['header_size'] = header_size
        eeprom_info['header_offset'] = header_offset
        # No need to re-read header data, matches assumed version=1
        # VERIFY HEADER CRC
        hdr_crc = eeprom_word_lookup(hdr, spec, 'header_loc_hdr_crc')
        eeprom_info['header_crc'] = hdr_crc
        calc_crc = crc(eeprom_data_lookup(hdr, spec, 'header_range_crc'))
        if calc_crc != hdr_crc:
            error("EEPROM Read CRC Mismatch: Header: "
                  "0x%08x vs 0x%08x" % (hdr_crc, calc_crc))
            return
        data_size = eeprom_word_lookup(hdr, spec, 'header_loc_data_size')
        data_offset = header_offset - data_size
        eeprom_info['data_size'] = data_size
        eeprom_info['data_offset'] = data_offset
        if data_offset < vpd_info['vpd_end']:
            error("EEPROM CTO Data Size too large: overlaps with "
                  "EEPROM VPD Data (0x%04x < 0x%04x)" %
                  (data_offset, vpd_info['vpd_end']))
            return
        err, raw = _vpd.eeprom.read_raw(data_offset, data_size)
        if err:
            error("EEPROM Read Error: Data: %s" % err)
            return
        data_crc = eeprom_word_lookup(hdr, spec, 'header_loc_data_crc')
        eeprom_info['data_crc'] = data_crc
        calc_crc = crc(raw)
        if calc_crc != data_crc:
            error("EEPROM Read CRC Mismatch: Data: "
                  "0x%08x vs 0x%08x" % (data_crc, calc_crc))
            return
        # AFTER calculating data CRC from raw, strip off any trailing
        # null bytes which may have been added during write for padding
        # (This is only safe for normal ASCII text files!)
        while raw and raw[-1] == 0:
            raw.pop()
        eeprom_info['raw'] = raw
        eeprom_info['data'] = _vpd.eeprom.data_bytes_to_str(raw)
        eeprom_info['valid'] = True
    else:
        error("Unknown Header Version %s" % version)
        return
    return eeprom_info

def get_eeprom_data():
    info = get_eeprom_info()
    if info and info.get('valid'):
        return info.get('data')
    return

def check(options):
    verbose("Checking EEPROM data...")
    # If we can successfully get_eeprom_info(), we're good to go
    # Otherwise, all errors are internally reported already...
    info = get_eeprom_info()
    if info and info.get('valid'):
        verbose("EEPROM header and data appear valid!")
        return 0
    error("EEPROM header/data invalid: Consider re-init?")
    return 1

def dumpraw(options):
    verbose("Reading data from EEPROM...")
    info = get_eeprom_info()
    # XXX Optionally in the future, we could dump what we have "so far",
    # XXX for info.get('valid')==False case ?
    if info and info.get('valid'):
        data_size = info.get('data_size', 0)
        data_offset = info.get('data_offset', 0)
        header_size = info.get('header_size', 0)
        total_size = data_size + header_size
        err, lines = _vpd.eeprom.dump_raw(data_offset, total_size)
        print 'EEPROM data dump:'
        print 'Header Version: %s'     % info.get('version')
        print 'Header Offset : 0x%04x' % info.get('header_offset', 0)
        print 'Data   Offset : 0x%04x' % data_offset
        print 'Data   Size   : 0x%04x = %d' % (data_size, data_size)
        print 'Header CRC    : 0x%08x' % info.get('header_crc', 0)
        print 'Data   CRC    : 0x%08x' % info.get('data_crc', 0)
        if err:
            error("Error reading EEPROM data:", err)
            return 1
        for line in lines:
            print line
        return 0
    error("Error reading EEPROM")
    return 1

def dump(options):
    verbose("Reading data from EEPROM...")
    data = get_eeprom_data()
    if data is not None:
        verbose('EEPROM data contents:')
        for line in data.split('\n'):
            print line
        return 0
    error("Error reading EEPROM")
    return 1

def write(options):
    version = EEPROM_FILE_VERSION
    filename = options.get('file')
    if not filename:
        error("No file specified for write")
        return 1
    if not os.path.exists(filename):
        error("Specified file %s does not exist")
        return 1
    vpd_info = get_vpd_info()
    if not vpd_info:
        error("Unable to determine EEPROM VPD Info")
        return 1
    spec = get_eeprom_file_spec(version)
    if not spec:
        error("No EEPROM File Spec defined for version %s" % version)
        return 1
    verbose("Writing %s data to EEPROM" % filename)
    with open(filename, 'r') as f:
        fdata = f.read()
    data = _vpd.eeprom.data_str_to_bytes(fdata)
    if not data:
        error("No data available from file %s" % filename)
        return 1
    header_size, header_offset = spec['header_size'], spec['header_offset']
    data_offset = data_crc = write_data = None
    if version == 1:
        # Pad data with null chars for data alignment
        offset = header_offset - len(data)
        data += [0] * (offset % EEPROM_FILE_DATA_ALIGNMENT)
        data_size = len(data)
        data_offset = header_offset - data_size
        if data_offset < vpd_info['vpd_end']:
            error("EEPROM CTO Data Size too large: overlaps with "
                  "EEPROM VPD Data (0x%04x < 0x%04x)" %
                  (data_offset, vpd_info['vpd_end']))
            return 1
        bytes = lambda data, size: _vpd.eeprom.word_to_byte_list(data, size)
        data_crc = crc(data)
        hdr = bytes(data_crc, 4) + bytes(data_size, 2) + bytes(version, 2)
        write_data = data + bytes(crc(hdr), 4) + hdr
    else:
        error("Unknown EEPROM File version %s" % version)
        return 1
    err = _vpd.eeprom.write_raw(data_offset, write_data)
    if err:
        error("EEPROM Write Error: %s" % err)
        return 1
    # Read back and confirm CRC
    verbose("Confirming written EEPROM data...")
    info = get_eeprom_info()
    valid = info and info.get('valid')
    check_crc = info and info.get('data_crc')
    if not info or not valid or not data_crc == check_crc:
        error("EEPROM Write Error: CRC Confirmation Failed")
        return 1
    return 0

def read(options):
    filename = options.get('file')
    if not filename:
        error("No file specified for read")
        return 1
    data = get_eeprom_data()
    if data is None:
        error("Error reading EEPROM")
        return 1
    verbose("Writing EEPROM data to %s" % filename)
    with open(filename, 'w') as f:
        f.write(data)
    return 0

def generic_get_args(usercmds, cmdargs):
    numarg = len(cmdargs)
    userargs = usercmds[:numarg]
    usercmds = usercmds[numarg:]
    valid = len(userargs) == numarg
    options = valid and dict(zip(cmdargs,userargs)) or {}
    return valid, options, usercmds

opts_desc = [
    ('-d, --debug',    'Debug mode'),
    ('-v, --verbose',  'Verbose mode'),
    ('-h, --help',     'Show this text'),
    ]

cmds_list = [
    ('check',
    { 'func': check,
      'title': 'EEPROM Data Check (validate header, data)',
      }),
    ('dumpraw',
    { 'func': dumpraw,
      'title': 'Dump EEPROM - display EEPROM data, raw',
      }),
    ('dump',
    { 'func': dump,
      'title': 'Dump EEPROM - display EEPROM data, formatted',
      }),
    ('write',
    { 'func': write,
      'title': 'Write EEPROM - write file data to EEPROM',
      'args': ['file'],
      }),
    ('read',
    { 'func': read,
      'title': 'Read EEPROM - write EEPROM data to file',
      'args': ['file'],
      }),
    ]

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 _vpd, _debug, _verbose

    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['verbose']:
        _verbose = True

    vpdoptions = {
        'verbose': _verbose,
        'debug': _debug,
        }

    actualcmds = filter(lambda c: c in commands, usercmds)
    if len(actualcmds) == 0:
        raise argslib.Error('No valid command specified')

    try:
        _vpd = vpdlib.get_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 = True
            usercmd = usercmds.pop(0)
            cmd = cmds_desc.get(usercmd, {})
            cmdfunc, cmdargs = [cmd.get(key) for key in ('func', 'args')]
            cmdoptions = {}
            if not cmdfunc:
                error("Unsupported command '%s'" % usercmd)
                valid, show_usage = False, True
            if valid and cmdargs:
                argfunc = cmd.get('argfunc', generic_get_args)
                valid, cmdoptions, usercmds = argfunc(usercmds, cmdargs)
                if not valid:
                    error("Insufficient or Invalid args for command '%s'" %
                          usercmd)
                    show_usage = True
            if valid:
                try:
                    if cmdfunc(cmdoptions):
                        valid = False
                except IOError, e:
                    error("IOError during '%s':" % usercmd, e)
                    valid = False
            if not valid:
                result = 1
    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 CTO Receipt Tool.
Provides Check, Dump, Write, Read commands.""" %
    (argslib.appname(), disp_opts, disp_cmds))
    
    options = {
        'debug':
        {'opts'    : ['d', 'debug'],
         'default' : False,
         'func'    : argslib.true_func(),
         },
        'verbose':
        {'opts'    : ['v', 'verbose'],
         'default' : False,
         'func'    : argslib.true_func(),
         },
        'help':
        {'opts'    : ['h', 'help'],
         'func'    : argslib.help_func(usage),
         },
        }

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