#!/usr/bin/env python
import getopt
import re
import os
import sys

import isi.app.lib.procs as procs

import isi.hw.check.lib.biosver as biosver

import isi.hw.hal as hal
import isi.hw.version as hwver

PROGNAME = os.path.basename(sys.argv[0])

# Fields that we ignore for manufacturing
EXCLUDE_FIELDS = [
    # Settings that appear to change every time we boot, or that
    # we just plain want to ignore
    "rsvd_0_0",
    "cmosRegA",
    "cmosDaylightSavings",
    "cmosRegB",
    "cmosRegC",
    "rsvd_D_0",
    "cmosValidRamBit",
    "rsvd_E_0",
    "cmosTimeFailedBit",
    "cmosDiskFailedBit",
    "rsvd_E_4",
    "cmosBadConfigBit",
    "cmosBadChecksumBit",
    "cmosBadPowerbit",
    "cmosShutdown",
    "cmosChecksumHi",
    "cmosChecksumLo",
    "cmosExtMemCnt",
    "cmosAbove4GBSize",
    "cmosCRC",
    "cmosIpmiBMCPassword", # Page 1 (Supermicro v1.3b)
    "cmosDDRCSR1",         # (not in Supermicro v1.3b...)
    "cmosDRAMISCTL0",      # (not in Supermicro v1.3b...)
    "cmosPmWbr41",         # (not in Supermicro v1.3b...)
    "cmosPmWbr42",         # (not in Supermicro v1.3b...)
    "cmosPmADTr41",        # (not in Supermicro v1.3b...)
    "cmosPmADTr42",        # (not in Supermicro v1.3b...)
    "cmosPmADT1r41",       # (not in Supermicro v1.3b...)
    "cmosPmADT1r42",       # (not in Supermicro v1.3b...)
    "cmosUserPassword",
    "cmosPostVgaMode",     # Page 4 (Supermicro v1.3b)

    # The following seem to be involved in the boot order settings,
    # which we're just ignoring for the moment.    
    "cmosTOUD0",           # (not in Supermicro v1.3b...)
    "cmosTOUD1",           # (not in Supermicro v1.3b...)
    "cmosRCVENDLYA0",      # (not in Supermicro v1.3b...)
    "cmosRCVENDLYA1",      # (not in Supermicro v1.3b...)
    "cmosRCVENDLYA2",      # (not in Supermicro v1.3b...)
    "cmosRCVENDLYA3",      # (not in Supermicro v1.3b...)
    "cmosRCVENDLYB0",      # (not in Supermicro v1.3b...)
    "cmosRCVENDLYB1",      # (not in Supermicro v1.3b...)
    "cmosRCVENDLYB2",      # (not in Supermicro v1.3b...)
    "cmosRCVENDLYB3",      # (not in Supermicro v1.3b...)
    "cmosBCVOrder0",       # (not in Supermicro v1.3b...)
    "cmosBCVOrder1",       # (not in Supermicro v1.3b...)
    "cmosBCVOrder2",       # (not in Supermicro v1.3b...)
    "cmosBCVOrder3",       # (not in Supermicro v1.3b...)
    "cmosBCVOrder4",       # (not in Supermicro v1.3b...)
    "cmosBCVOrder5",       # (not in Supermicro v1.3b...)
    "cmosBCVOrder6",       # (not in Supermicro v1.3b...)
    "cmosBCVOrder7",       # (not in Supermicro v1.3b...)
    "cmosBFDOrder0",       # (not in Supermicro v1.3b...)
    "cmosBFDOrder1",       # (not in Supermicro v1.3b...)
    "cmosBFDOrder2",       # (not in Supermicro v1.3b...)
    "cmosBFDOrder3",       # (not in Supermicro v1.3b...)
    "cmosBFDOrder4",       # (not in Supermicro v1.3b...)
    "cmosBFDOrder5",       # (not in Supermicro v1.3b...)
    "cmosBFDOrder6",       # (not in Supermicro v1.3b...)
    "cmosBFDOrder7",       # (not in Supermicro v1.3b...)
    "cmosIPLOrder0",
    "cmosIPLOrder1",
    "cmosIPLOrder2",
    "cmosIPLOrder3",
    "cmosIPLOrder4",
    "cmosIPLOrder5",
    "cmosIPLOrder6",
    "cmosIPLOrder7",
    "cmosLan1BootStrap",   # (not in Supermicro v1.3b...)
    "cmosLan2BootStrap",   # (not in Supermicro v1.3b...)

    # PCI device-dependent stuff (different on GigE vs. IB)
    "cmosPciDev1Master",   # Page 1 (Supermicro v1.3b)
    "cmosPciDev1Latency",  # Page 1 (Supermicro v1.3b)
    "cmosPciDev2Master",   # Page 1 (Supermicro v1.3b)
    "cmosPciDev2Latency",  # Page 1 (Supermicro v1.3b)
    "cmosPciDev3Master",   # Page 1 (Supermicro v1.3b)
    "cmosPciDev3Latency",  # Page 1 (Supermicro v1.3b)
    "cmosPciDev4Master",   # Page 1 (Supermicro v1.3b)
    "cmosPciDev4Latency",  # Page 1 (Supermicro v1.3b)
    "cmosPciDev5Master",   # Page 1 (Supermicro v1.3b)
    "cmosPciDev5Latency",  # Page 1 (Supermicro v1.3b)
    "cmosPciDev6Master",   # (not in Supermicro v1.3b...)
    "cmosPciDev6Latency",  # (not in Supermicro v1.3b...)
    "cmosPciDev7Master",   # (not in Supermicro v1.3b...)
    "cmosPciDev7Latency",  # (not in Supermicro v1.3b...)

    # Settings I don't totally understand, but which seem to differ
    # between gigE and IB nodes, and thus I'm guessing are maybe
    # dependent on underlying hardware...
    "cmosCommandByte1",    # Page 1 (Supermicro v1.3b)
    "cmosCommandByte2",    # Page 1 (Supermicro v1.3b)
    "cmosCommandByte3",    # Page 1 (Supermicro v1.3b)
    "cmosCommandByte4",    # Page 1 (Supermicro v1.3b)
    "cmosCommandByte5",    # Page 1 (Supermicro v1.3b)
    "cmosCommandByte6",    # (not in Supermicro v1.3b...)
    "cmosCommandByte7",    # (not in Supermicro v1.3b...)
    "cmosBspBistLSB",      # (not in Supermicro v1.3b...)
    "cmosBspBistLSBH",     # (not in Supermicro v1.3b...)
    "cmosBspBistMSBL",     # (not in Supermicro v1.3b...)
    "cmosBspBistMSB",      # (not in Supermicro v1.3b...)
    ## These are "ported" Supermicro versions of ignored bioscmos_info fields
    "cmosBspBistLow0",
    "cmosBspBistLow8",
    "cmosBspBistHigh0",
    "cmosBspBistHigh8",
    "cmosSecurNvIch1",     # (not in Supermicro v1.3b...)
    "cmosSecurNvIch2",     # (not in Supermicro v1.3b...)
    "cmosSecurNvIch3",     # (not in Supermicro v1.3b...)
    "cmosSecurNvIch4",     # (not in Supermicro v1.3b...)
    "cmosSecurNvIch5",     # (not in Supermicro v1.3b...)
    "cmosSecurNvIch6",     # (not in Supermicro v1.3b...)
    "cmosSecurNvIch7",     # (not in Supermicro v1.3b...)
    "cmosSecurNvIch8",     # (not in Supermicro v1.3b...)
    "cmosSecurNvIch9",     # (not in Supermicro v1.3b...)
    "cmosSecurNvIchA",     # (not in Supermicro v1.3b...)
    "cmosSecurNvIchB",     # (not in Supermicro v1.3b...)
    "cmosEvtLogCfg",
    "cmosboot2nd",         # (not in Supermicro v1.3b...)

    # This one differs between software builds, apparently...
    'cmosSBF',
    ]
    
class Args:
    usage = \
"""Usage: %s [OPTIONS]
OPTIONS:
  -m <MAPFILE>           Use given mapfile instead of default mapfile
  -g                     Generate python code for mapfile
  -p                     Generate python code for bios detail
  -x                     Exclude fields we ignore for manufacturing

  -h, --help             Show this text
""" % PROGNAME

    class Error(Exception):
        # Unix programs generally use 2 for command line syntax errors
        def __init__(self, message, exitCode = 2):
            self.message = message
            self.exitCode = exitCode

    def __init__(self, args):
        self.mapfile = None
        self.do_python_map = False
        self.do_python_detail = False
        self.exclude_for_mfg = False
        try:
            (opts, non) = getopt.getopt(args, 'hm:gpx', ['help'])
        except getopt.GetoptError, e:
            raise Args.Error(str(e))

        for (opt, opt_param) in opts:
            if opt == '-h' or opt == '--help':
                raise Args.Error('', 0)
            elif opt == '-g':
                self.do_python_map = True
            elif opt == '-p':
                self.do_python_detail = True
            elif opt == '-m':
                self.mapfile = opt_param
            elif opt == '-x':
                self.exclude_for_mfg = True
            else:
                raise Args.Error('unsupported option')

        if len(non) > 0:
            raise Args.Error('too many arguments')

def is_question_format():
    question_format_versions = ['2.0a', '2.0a1']
    ver = biosver.get_biosver()
    return ver in question_format_versions

def read_old_mapfile(mapfile):
    cmd = (("/usr/bin/awk '$0 ~ /[0-9a-f][0-9a-f][0-9a-f][0-9a-f][ \t]+[0-9]/ " +
            "{print $2,$3,$4,$NF}' %s") % mapfile)
    (error, output) = procs.get_cmd_output(cmd)
    if error:
        print "ERROR: unable to read mapfile '%s'" % mapfile
        output = []
    return output

def read_new_mapfile(mapfile):
    cmd = (("/usr/bin/egrep \"Yes|No\" %s | /usr/bin/awk " +
            "'{print $3,$4,$1}'") % mapfile)
    (error, output) = procs.get_cmd_output(cmd)
    if error:
        print "ERROR: unable to read mapfile '%s'" % mapfile
        output = []
    return output

def main():
    result = 0
    args = Args(sys.argv[1:])

    # read our mapfile
    if args.mapfile != None:
        if is_question_format():
            output = read_new_mapfile(args.mapfile)
        else:
            output = read_old_mapfile(args.mapfile)
        #output == [] on error
    else:
        output = biosver.get_biosmap(biosver.get_biosver())
        #output == None on error

    if output == None or len(output) == 0:
        print "Failed to find appropriate biosmap for field names"
        return 1

    if args.do_python_map:
        # map python-dump mode
        if args.mapfile != None:
            filename = os.path.basename(args.mapfile).upper()
        else:
            filename = 'LIVE_BIOS'
        print 'BIOS_MAP = [ #%s' % filename
        for line in output:
            print "    '%s'," % line
        print ']'

    if not args.do_python_map or args.do_python_detail:
        # Extract properly parsed fields from output.
        fields = None
        if is_question_format():
            fields = new_mapfile_output_to_regions(output)
        else:
            fields = old_mapfile_output_to_regions(output)
        if fields is None:
            return 1

        # normal mode
        if (args.exclude_for_mfg):
            fields = filter_excluded_fields(fields)
        result = detail_biosdump(fields, args.do_python_detail)

    return result

def old_mapfile_output_to_regions(output):
    # convert filtered mapfile lines to handy tuples
    # Two possible mapfile field value layouts:
    # - Legacy used: (offset,width,name)
    # - bug 36633 corrected this to: (page,offset,width,name)
    # We support either one, depending on what is reported in output
    # However, only the post-bug 36633 map data with page=0,1 is
    # "correct" -- legacy (Rainer, Tyan, etc) data has the "wrap" bug
    # until we decide to revisit and update those map data files.
    fields = []

    for line in output:
        data = line.split()
        count = len(data)
        if count == 3:
            page, (offset, width, name) = 0, tuple(data)
        elif count == 4:
            (page, offset, width, name) = tuple(data)
        else:
            print ('ERROR: invalid biosmap data (%d fields, expd 3 or 4)' % count)
            return None
        fields.append((name, int(offset, 16), int(width, 16), int(page)))

    return fields

# Helper function for new_mapfile_output_to_reginos.  Takes in hex format
# two strings representing the byte aligned offset and mask to access flags
# in that location in BIOS.  It then returns a bit offset and bit size for
# these flags to be given to detail_biosdump.
def reg_mask_to_offset_width(reg, mask):
    mobo = hal.getMotherboardType()
    if mobo == hwver.MOBO_SUPERX8DT6_F:
        offset = ( int(reg, 16) & 0xff ) * 8
    else:
        offset = int(reg, 16)
    int_mask = int(mask, 16)
    width = 0
    i = 0
    
    # Handle masks with no high bits.
    if int_mask > 0:
        # Find the first high bit.
        while i <= 16:
            if (int_mask & (1 << i)):
                break
            else:
                offset += 1
                i += 1
        
        # Find the last high bit.
        while i <= 16:
            if (int_mask & (1 << i)):
                width += 1
                i += 1
            else:
                break
    
    return offset, width

def new_mapfile_output_to_regions(output):
    fields = []

    for line in output:
        data = line.split()
        count = len(data)
        if count != 3:
            print ('ERROR: invalid biosmap data (%d fields, expd 3)' % count)
            print data
            return None
        name = data[2]
        # The "[:-1]" is to eliminate the 'h' at the end of the entry.
        offset, width = reg_mask_to_offset_width(data[0].rstrip('h'),
                                                 data[1].rstrip('h'))
        # Ignore zero width 
        if width > 0:
            fields.append((name, offset, width, 0)) # page always == 0

    return fields

# Remove excluded 
def filter_excluded_fields(fields):
    use_excludes = (EXCLUDE_FIELDS + 
                    biosver.get_excludes(biosver.get_biosver()))
    fields = filter(lambda field:not field[0] in use_excludes,
                    fields)
    return fields
    
def detail_biosdump(fields, dump_python):
    # get and parse biosdump info
    cmd = ("/usr/bin/isi_hwtools/bioscmos_dump -H | /usr/bin/awk " +
           "'{ for (i = 1; i <= NF; i++) print $i }'")
    (error, output) = procs.get_cmd_output(cmd)
    if error:
        print 'ERROR: unable to get bioscmos_dump output'
        return 1

    # convert the filtered biosdump to a list of integers
    data = []
    for line in output:
        data.append(int(line, 16))

    # process all the fields in the biosmap
    infos = []
    for field in fields:
        vals = process_field(field, data)
        if vals:
            infos.append((field, vals))

    # print our results all pretty like
    name_width = reduce(lambda x,y:max(x,y),
                        [len(field[0]) for (field, vals) in infos])
    if dump_python:
        print 'BIOS_DETAIL = {'
        for (field, vals) in infos:
            print ("    %-*s : '%s'," %
                   (name_width+2, "'%s'" % field[0], vals))
        print '}'
    else:
        hdr = ('%-*s%s  offset   bits   data' % (name_width, "name", "page"))
        print hdr
        print '-' * 78
        for (field, vals) in infos:
            (name, offset, width, page) = field
            print ('%-*s % 4s    % 4s   % 4s   %s' %
                   (name_width, name, page,
                    offset, width, vals))

    return 0
        
def process_field(field, data):
    """Reads width bits of data from bit-offset offset in data,
       which should be a list of byte-sized integers"""
    (name, offset, width, page) = field

    if page > 0:
        if page == 1:
            offset += page * 0x80 * 8 # 128 Byte / Page
        else:
            # bioscmos_dump only supports page0/page1 (BANK1/BANK2)
            return None

    bits_left = width
    data_byte = offset / 8
    data_bit = offset % 8
    bytes = []
    byte = 0
    byte_bit = 0
    
    while bits_left > 0:
        # Grab as many bits as we can from the current data byte to
        # try and fill up the current output byte
        data_bits_left = 8 - data_bit
        byte_bits_left = 8 - byte_bit
        bits_to_grab = reduce(min, [bits_left, data_bits_left, byte_bits_left])

        mask = 0x00
        for i in range(data_bit, data_bit+bits_to_grab):
            mask |= 0x01 << i
            
        byte |= ((data[data_byte] & mask) >> data_bit) << byte_bit

        byte_bit += bits_to_grab
        if byte_bit >= 8:
            bytes.append(byte)
            byte_bit = 0
            byte = 0
            
        data_bit += bits_to_grab
        if data_bit >= 8:
            data_bit = 0
            data_byte += 1

        bits_left -= bits_to_grab

    # if the last byte is not a full 8 bits wide, it won't have
    # been added to the list yet
    if byte_bit != 0:
        bytes.append(byte)

    return ' '.join(['%02x' % byte for byte in bytes])


if __name__ == '__main__':
    try:
        sys.exit(main())
    except Args.Error, e:
        if e.message:
            print >> sys.stderr, '%s: %s' % (PROGNAME, e.message)
        print >> sys.stderr, Args.usage
        sys.exit(e.exitCode)

