#!/usr/bin/python
# Copyright 2015, 2017 Cumulus Networks, Inc.
#
# dbg-acl - Used to set and read ACL hw stats
#
# See usage (dbg-acl -h) for instructions
#
import argcomplete
import argparse
import subprocess
import os
import re
import sys
from datetime import datetime
import xml.etree.ElementTree as ET
try:
    import bcmshell
    bcmShellAvailable = True
except ImportError:
    bcmShellAvailable = False

########################################################################
# Exceptions
########################################################################
class UnexpectedFpOutput(RuntimeError):
    pass
class UnexpectedXmlContent(RuntimeError):
    pass
class ParseError(RuntimeError):
    pass

########################################################################
# Definitions and Globals
########################################################################
# We store the last read data in xml format and use it for checking if
# anything changed on the next run
gOldFpFile = '/var/run/cumulus/dbg_acl_data.xml'

# version stored in the xml file. if the major version doesn't match
# data from the file is discarded
gClDbgAclVer = '1.0.0'

gStageMap = {'Ingress' : 'ifp',
             'Egress' : 'efp',
             'Lookup' : 'vfp'}

# Stats are displayed in this order
ACL_STAT_TYPE_PKTS=1
ACL_STAT_TYPE_BYTES=2
ACL_STAT_TYPE_GREEN_PKTS=3
ACL_STAT_TYPE_GREEN_BYTES=4
ACL_STAT_TYPE_NOT_GREEN_PKTS=5
ACL_STAT_TYPE_NOT_GREEN_BYTES=6

# BCM string to id
gStatMapStr = {'Packets' : ACL_STAT_TYPE_PKTS,
               'Bytes' : ACL_STAT_TYPE_BYTES,
               'GreenPackets' : ACL_STAT_TYPE_GREEN_PKTS,
               'GreenBytes' : ACL_STAT_TYPE_GREEN_BYTES,
               'NotGreenPackets' : ACL_STAT_TYPE_NOT_GREEN_PKTS,
               'NotGreenBytes' : ACL_STAT_TYPE_NOT_GREEN_BYTES}

# Stat id to display string
gStatMapId = {ACL_STAT_TYPE_PKTS: 'pkts',
              ACL_STAT_TYPE_BYTES: 'bytes',
              ACL_STAT_TYPE_GREEN_PKTS : 'g-pkts',
              ACL_STAT_TYPE_GREEN_BYTES : 'g-bytes',
              ACL_STAT_TYPE_NOT_GREEN_PKTS : 'ng-pkts',
              ACL_STAT_TYPE_NOT_GREEN_BYTES : 'ng-bytes'}

# table ids support for each of the xTables
ACL_TBL_ID_FILTER=1
ACL_TBL_ID_MANGLE=2

# table name to id mapping
gTblMapName = {'filter': ACL_TBL_ID_FILTER,
               'mangle': ACL_TBL_ID_MANGLE}

# table id to name mapping
gTblMapId = {ACL_TBL_ID_FILTER: 'filter',
             ACL_TBL_ID_MANGLE: 'mangle'}

# FP entries from the last run of this app - (unit, eid):FpEntry
gOldFp = {}

# Current FP entries - (unit, eid):FpEntry
gCurrFp = {}

# List of relevant units
gUnits = []

# Copy of the iptable as displayed by cl-acltool - (ACL_TBL_ID_XXX, kidx): desc
gIpTable = {}

# Copy of the ip6table as displayed by cl-acltool - (ACL_TBL_ID_XXX, kidx): desc
gIp6Table = {}

# Copy of the ebtable as displayed by cl-acltool - (ACL_TBL_ID_XXX, kidx): desc
gEbTable = {}

# Per unit FP reprogram sequence number (curr - read from switchd; old - read 
# from the cached xm info)
gCurrFpProgramSeq = {}
gOldFpProgramSeq = {}

DATE_FMT = '%Y-%m-%d %H:%M:%S'

########################################################################
# FP entry
########################################################################
class FpEntry:
    def __init__(self, unit, eid, gid, stat_id, stage):
        # key (unit, eid)
        self.unit = unit
        self.eid = eid

        # Data
        self.gid = gid
        self.stage = stage
        self.stat_id = stat_id

        # switchd info
        self.rule_idx = 0
        self.kidx = -1
        self.kchain = "-"
        self.tblname = "-"
        self.tbltype = "-" # filter or mangle
        self.rule_desc = "unknown"

        # Stats - number of stats associated with an FP entry is variable
        # type : cnt
        #
        # {
        #     (ACL_STAT_TYPE_PKTS): 567879
        #     (ACL_STAT_TYPE_BYTES): 78567879
        # }
        self.stat_list = {}

    def add_stat(self, type, cnt):
        if type not in gStatMapId:
            raise RuntimeError('Unknown stat type: %d\n' % type)
            
        self.stat_list[type] = cnt

    def add_switchd_info(self, rule_idx, kidx, tblname, kchain, tbltype, rule_desc):
        self.rule_idx = rule_idx
        self.kidx = kidx
        self.tblname = tblname
        self.kchain = kchain
        self.tbltype = tbltype
        self.rule_desc = rule_desc

    def dump_stat(self, verbose):
        str = 'eid=%d gid=%d stage=%s' % (self.eid, self.gid, self.stage)
        for type in sorted(self.stat_list):
            str = str + ' %s=%d' % (gStatMapId[type], self.stat_list[type])
        sys.stdout.write('%s\n' % str)
        if verbose:
            sys.stdout.write(' [rule_idx=%d kidx=%d tbl=%s type=%s chain=%s\n' %\
                     (self.rule_idx, self.kidx, self.tblname,\
                      self.tbltype, self.kchain))
            sys.stdout.write('  %s]\n' % (self.rule_desc))

    def is_nz(self):
        for type in self.stat_list:
            if self.stat_list[type]:
                return True
        return False

    def __eq__(self, other):
        if not isinstance(other, FpEntry):
            return  NotImplemented

        if other.eid != self.eid or \
            other.stat_id != self.stat_id or \
            other.stage != self.stage or\
            other.unit != self.unit or\
            len(other.stat_list) != len(self.stat_list):
            return False

        for type in self.stat_list:
            cnt = self.stat_list[type]
            if type not in other.stat_list or other.stat_list[type] != cnt:
                return False

        return True

    def __ne__(self, other):
        rc = self.__eq__(other)
        if rc is NotImplemented:
            return rc
        return not rc
        
########################################################################
# Helper functions
########################################################################
# parses the output of "fp show" and "fp stat get" to build the fp 
# entry database
def parse_fp_show_per_unit(unit, clearStat):
    global gCurrFp

    fpStr = bcmshell.bcmshell().run('%d: fp show' % unit)

    # string patterns that we will be looking for
    # sample - 'EID 0x00000003: gid=0x2,'
    entryPat=re.compile('EID 0x(?P<eid>[0-9a-fA-F]+): gid=0x(?P<gid>[0-9a-fA-F]+),')
    # sample - ' StageIngress'
    stagePat=re.compile('^[ \t]*Stage(?P<stage>[a-zA-Z]+)')
    # sample - 'statistics={stat id 700  slice = 2 idx=23 entries=1}{Bytes}{Packets}'
    statPat=re.compile('^[ \t]*statistics={stat id (?P<sid>\d+).* entries=(?P<ecnt>\d+).*}')
    # sample - 'The value is: 0x0463a0'
    statValPat = re.compile('The value is: 0x(?P<val>[0-9a-fA-F]+)')

    # Init values that we will be collecting per FP enty
    eid=0
    gid=0
    stage='Unk'
    sid=0
    ecnt=0
    for line in fpStr.splitlines():
        obj = entryPat.search(line)
        if obj:
            eid = int(obj.group('eid'), 16)
            gid = int(obj.group('gid'), 16)
            # any time a new entry is encountered reset the other info
            stage='Unk'
            sid=0
            ecnt=0
            continue

        obj = stagePat.search(line)
        if obj:
            stage = obj.group('stage')
            if stage in gStageMap:
                stage = gStageMap[stage]
            else:
                stage='Unk'
            continue

        obj = statPat.search(line)
        if obj:
            if not gid:
                continue
            sid = int(obj.group('sid'))
            ecnt = int(obj.group('ecnt'))
            #sys.stdout.write('%s eid=0x%x gid=%d sid=%d ecnt=%d\n' % (stage, eid, gid, sid, ecnt))
            # we have locate stats for an entry - create it
            if (unit, eid) in gCurrFp:
                raise UnexpectedFpOutput(\
                    'Duplicate eid %d:%d detected in the display' % (unit, eid))
                
            if not clearStat:
                gCurrFp[unit, eid]=FpEntry(unit, eid, gid, sid, stage)

            if not ecnt:
                raise UnexpectedFpOutput(\
                'Stat is empty - eid %d, sid %d' % (eid, sid))
            # Read stats (one at a time)
            firstWord=True
            for word in line.split('}'):
                if firstWord:
                    firstWord = False
                    continue

                typeStr=word[1:]
                if typeStr not in gStatMapStr:
                    continue

                if clearStat:
                    statStr = bcmshell.bcmshell().run(\
                        '%d: fp stat set si=%d type=%s val=0' % (unit, sid, typeStr))
                else:
                    statStr = bcmshell.bcmshell().run(\
                        '%d: fp stat get si=%d type=%s' % (unit, sid, typeStr))
                    obj = statValPat.search(statStr)
                    if obj:
                        val = int(obj.group('val'), 16)
                    else:
                        val = 0
                    #sys.stdout.write('  %d: %d %s\n' % (sid, val, typeStr))
                    gCurrFp[unit, eid].add_stat(gStatMapStr[typeStr], val)
                    
            # any time a stat id is encountered reset the entry
            eid = gid = 0

def get_cl_acltool_rule_from_kidx(tbl_name, tbltype, kidx):
    if tbl_name == 'iptables':
        tbl = gIpTable
    elif tbl_name == 'ip6tables':
        tbl = gIp6Table
    else:
        tbl = gEbTable
    tbl_id = gTblMapName[tbltype]

    if kidx == -1:
        return 'internal'

    return tbl[tbl_id, kidx] if (tbl_id, kidx) in tbl else 'unknown'

def cl_acltool_info_setup_per_tbl(tbl_name):
    global gIpTable
    global gIp6Table
    global gEbTable

    if tbl_name == 'ip':
        tbl = gIpTable
    elif tbl_name == 'ip6':
        tbl = gIp6Table
    else:
        tbl = gEbTable

    aclStr = '' 
    try: 
        aclStr = subprocess.check_output(['/usr/cumulus/bin/cl-acltool', '-L',
            tbl_name])
    except subprocess.CalledProcessError, e:
        sys.stderr.write('cl-acltool -L %s failed - %s\n' % (tbl_name, e))
        pass

    # XXX - The rule searching here is not the best
    # sample - 'TABLE filter :'
    tablePat = re.compile('TABLE (?P<type>[\w]+) :')
    if tbl_name == 'eb':
        # sample - 'Bridge chain: INPUT, entries: 16, policy: ACCEPT'
        chainPat = re.compile('Bridge chain: (?P<chain>[\w]+),')
        # sample - '-d BGA -i swp+ -j setclass --class 7 , pcnt = 0 -- bcnt = 0'
        rulePat = re.compile('pcnt')
    else:
        # sample - 'Chain INPUT (policy ACCEPT 1007 packets, 378K bytes)'
        chainPat = re.compile('Chain (?P<chain>[\w]+) ')
        # sample - '0     0 DROP       all  --  swp+   any     240.0.0.0/5          anywhere'
        rulePat = re.compile('^[ \t]*(?P<rule_idx>[\d]+)')
        # sample -  'pkts bytes target     prot opt in     out     source               destination'
        ruleHeadPat = re.compile(' pkts bytes')
    bypass_table = 1
    chain = None
    rule_idx = 0
    old_type = '-'
    tbl_id = 0
    for line in aclStr.splitlines():
        obj = tablePat.search(line)
        if obj:
            # New table
            type = obj.group('type')
            # First chain for this table is yet to be detected
            chain = None
            if type in gTblMapName and (type == 'filter' or type == 'mangle'):
                bypass_table = 0
                if gTblMapName[type] != tbl_id:
                    # any time the table type changes re-init the idx
                    rule_idx = 0
                    tbl_id = gTblMapName[type]
            else:
                bypass_table = 1
            continue
            
        if bypass_table:
            # we are not interested in this table - skip all info till the 
            # next table is encountered
            continue

        obj = chainPat.search(line)
        if obj:
            # when a new chain is encountered add an implicit-policy at the end
            # of the prev chain for ip and ip6 (for ebtables switchd skips
            # allocation of kernel index for policy entries - XXX - why
            # this diff in handling?)
            if chain and (tbl_name == 'ip' or tbl_name == 'ip6'):
                tbl[tbl_id, rule_idx] = 'policy-entry (implicit-permit)'
                rule_idx += 1
            chain = obj.group('chain')
            continue

        obj = rulePat.search(line)
        if obj:
            tbl[tbl_id, rule_idx] = line
            rule_idx += 1

def dump_seq():
    sys.stdout.write('Old\n')
    for unit in gOldFpProgramSeq:
        sys.stdout.write('unit %d seq %d\n' % (unit, gOldFpProgramSeq[unit]))
        
    sys.stdout.write('New\n')
    for unit in gCurrFpProgramSeq:
        sys.stdout.write('unit %d seq %d\n' % (unit, gCurrFpProgramSeq[unit]))

def dump_cl_acltool_tbl_info(tbl_name, tbl):
    sys.stdout.write('%s:\n' % tbl_name)
    for tbl_id, rule in sorted(tbl):
        sys.stdout.write('[%s,%d]: %s\n' % (gTblMapId[tbl_id], rule, tbl[tbl_id, rule]))

def dump_cl_acltool_info():
    dump_cl_acltool_tbl_info('ip', gIpTable)
    dump_cl_acltool_tbl_info('ip6', gIp6Table)
    dump_cl_acltool_tbl_info('eb', gEbTable)

def cl_acltool_info_setup():
    cl_acltool_info_setup_per_tbl('ip')
    cl_acltool_info_setup_per_tbl('ip6')
    cl_acltool_info_setup_per_tbl('eb')

    #dump_cl_acltool_info()

def switchd_dbg_acl_info_get():
    # check if the debug dir exists
    aclStr = ''
    debug_dir = '/cumulus/switchd/debug'
    debug_file = debug_dir + '/acl'
    try:
        if not os.path.exists('/cumulus/switchd/debug'):
            with open('/cumulus/switchd/ctrl/debug', 'w') as fd:
                fd.write('1\n')
    except Exception as e:
        sys.stderr.write('switchd debug could not be enabled - %s\n' % e)
        pass

    if os.path.isfile(debug_file):
        try: 
            aclStr = subprocess.check_output(['/bin/cat', debug_file])
        except subprocess.CalledProcessError, e:
            sys.stderr.write('switchd acl info read failed - %s\n' % e)
            pass
        
    return aclStr

def get_fp_seq_from_hal():
    global gCurrFpProgramSeq

    halAclStr = switchd_dbg_acl_info_get()
    # string patterns that we will be looking for
    # Sample - '  Rule_index: 0'
    rulePat = re.compile('^[ \t]*Rule_index: (?P<rule_idx>[\d]+)')
    # Sample - 'Backend-info: unit 0 fp-reprograms 6'
    beInfoPat = re.compile('^[ \t]*Backend-info: unit (?P<unit>[\d]+) fp-reprograms (?P<cnt>[\d]+)')
    for line in halAclStr.splitlines():
        obj = beInfoPat.search(line)
        if obj:
            unit = int(obj.group('unit'))
            cnt = int(obj.group('cnt'))
            gCurrFpProgramSeq[unit] = cnt

        obj = rulePat.search(line)
        if obj:
            # stop searching for summary info (printed before the rules)
            return

# parses switchd acl debug info to map (unit, eid) to rule idx
def update_fp_with_hal_info():
    global gCurrFpProgramSeq
    global gCurrFp
    
    # setup index to desc mapping for each of the filter tables
    cl_acltool_info_setup()

    halAclStr = switchd_dbg_acl_info_get()
    # string patterns that we will be looking for
    # Sample - ' Name: filter'
    tblTypePat = re.compile('^[ \t]*Name: (?P<tbltype>[\w]+)')
    # Sample - '  Rule_index: 0'
    rulePat = re.compile('^[ \t]*Rule_index: (?P<rule_idx>[\d]+)')
    # Sample - '    Kernel Id Index: 0'
    kidxPat = re.compile('^[ \t]*Kernel Id Index: (?P<kidx>[\d]+)')
    # Sample - '    Kernel Table Type: ip6tables'
    tblnamePat = re.compile('^[ \t]*Kernel Table Type: (?P<tblname>[\w]+)')
    # Sample - '    Kernel Chain Type: INPUT'
    kchainPat = re.compile('^[ \t]*Kernel Chain Type: (?P<kchain>[\w]+)')
    # Sample - '    Backend: unit 0, eid 4, stat_id 1'
    bePat = re.compile('^[ \t]*Backend: unit (?P<unit>[\d]+), eid (?P<eid>[\d]+)')
    # Sample - 'Backend-info: unit 0 fp-reprograms 6'
    beInfoPat = re.compile('^[ \t]*Backend-info: unit (?P<unit>[\d]+) fp-reprograms (?P<cnt>[\d]+)')

    # Init values that we will be collecting per acl rule
    rule_idx=-1
    kidx=-1
    tblname='-'
    kchain='-'
    tbltype='-'
    unit=0
    eid=0
    summarySearch = True
    for line in halAclStr.splitlines():
        if summarySearch:
            obj = beInfoPat.search(line)
            if obj:
                unit = int(obj.group('unit'))
                cnt = int(obj.group('cnt'))
                gCurrFpProgramSeq[unit] = cnt

        obj = tblTypePat.search(line)
        if obj:
            tbltype = obj.group('tbltype')
            continue

        obj = rulePat.search(line)
        if obj:
            rule_idx = int(obj.group('rule_idx'))
            #any time a new rule is encountered reset other params
            kidx=-1
            tblname='-'
            kchain='-'
            unit=0
            eid=0
            # also stop searching for summary info (printed before the rules)
            summarySearch = False
            continue

        obj = kidxPat.search(line)
        if obj:
            kidx = int(obj.group('kidx'))
            continue
    
        obj = tblnamePat.search(line)
        if obj:
            tblname = obj.group('tblname')
            continue
    
        obj = kchainPat.search(line)
        if obj:
            kchain = obj.group('kchain')
            continue

        obj = bePat.search(line)
        if obj:
            unit = int(obj.group('unit'))
            eid = int(obj.group('eid'))
            # when a backend (unit, eid) entry is located update fpDb with the
            # hal and kernel indices
            if (unit, eid) in gCurrFp:
                rule_desc =  get_cl_acltool_rule_from_kidx(tblname, tbltype, kidx)
                gCurrFp[unit, eid].add_switchd_info(rule_idx, kidx, tblname, kchain, tbltype, rule_desc)
    
def parse_fp_show(clearStat=False, verbose=False):
    for unit in gUnits:
        parse_fp_show_per_unit(unit, clearStat)

    if verbose:
        update_fp_with_hal_info()
    else:
        # if not verbose we dont parse switchd debug/acl info; so just do the 
        # minimum needed to get the seq number
        get_fp_seq_from_hal()
    #dump_seq()

#############################################################################
# FP data is stored as a XML file to allow adding/removing info in the
# future.
# Here is the current format of the file -
#       <cl_dbg_acl_data version='1.0.0'>
#         <switchd pid='2375' />
#         <timestamp date=str(datatime) />
#         <units>
#           <unit id='0' fp_program_seq='6'>
#           <unit id='1' fp_program_seq='6'>
#         <units>
#         <fp_entries>
#           <fp_entry unit='0' eid='100' gid='5' stat_id='678' stage='ifp'>
#             <stats>
#               <stat type='1' val='100' />
#               <stat type='2' val='70000' />
#             </stats>
#           </fp_entry>
#           <fp_entry unit='0' eid='101'>
#             <stats>
#               <stat type='3' val='100' />
#               <stat type='4' val='70000' />
#               <stat type='5' val='200' />
#               <stat type='6' val='140000' />
#             </stats>
#           </fp_entry>
#         </fp_entries>
#       </cl_dbg_acl_data>
#############################################################################
def gen_fp_entry_xml_str(fp_entries, entry):
    fpAttr = { 'unit' : str(entry.unit), 'eid' : str(entry.eid) }
    fpAttr['gid'] = str(entry.gid)
    fpAttr['stat_id'] = str(entry.stat_id)
    fpAttr['stage'] = entry.stage
    fp_entry = ET.SubElement(fp_entries, 'fp_entry', fpAttr)
    stats = ET.SubElement(fp_entry, 'stats')
    for type in entry.stat_list:
        ET.SubElement(stats, 'stat', {'type': str(type), 'val' : str(entry.stat_list[type])})
        
def del_old_fp_file():
    try: 
        os.unlink(gOldFpFile)
    except subprocess.CalledProcessError, e:
        sys.stderr.write('Old file delete failed - %s\n' % e)
        pass

def get_switchd_pid():
    try: 
        out = subprocess.check_output(['/bin/pidof', 'switchd'])
        pid = int(out)
    except subprocess.CalledProcessError:
        sys.stderr.write('switchd pid get failed - %s\n' % e)
        pid = 0
    return pid

def gen_fp_xml_root():
    root = ET.Element('cl_dbg_acl_data', {'version': gClDbgAclVer})
    pid = get_switchd_pid()
    ET.SubElement(root, 'switchd', {'pid' : str(pid)})
    currDate = datetime.now()
    currDateStr = currDate.strftime(DATE_FMT)
    ET.SubElement(root, 'timestamp', {'date' : currDateStr})

    # fill in per-unit summary info
    units = ET.SubElement(root, "units")
    for unit in gCurrFpProgramSeq:
        ET.SubElement(units, 'unit', {'id': str(unit), 'fp_program_seq': str(gCurrFpProgramSeq[unit])})

    # fill in fp entries
    fp_entries = ET.SubElement(root, "fp_entries")
    for unit, eid in gCurrFp:
        gen_fp_entry_xml_str(fp_entries, gCurrFp[unit, eid])
    return root

def write_old_fp():
    root = gen_fp_xml_root()
    tree = ET.ElementTree(root)
    tree.write(gOldFpFile)

def decode_fp_xml_info(root):
    global gOldFp
    global gOldFpProgramSeq

    units = root.find('units')
    if units is not None:
        for unit_entry in units.findall('unit'):
            unit = int(unit_entry.get('id'))
            seq = int(unit_entry.get('fp_program_seq'))
            gOldFpProgramSeq[unit] = seq

    fp_entries = root.find('fp_entries')
    if fp_entries is None:
        return

    for fp_entry in fp_entries.findall('fp_entry'):
        unit = int(fp_entry.get('unit'))
        eid = int(fp_entry.get('eid'))

        if (unit, eid) in gOldFp:
            continue
        gid = int(fp_entry.get('gid'))
        stat_id = int(fp_entry.get('stat_id'))
        stage = fp_entry.get('stage')
        gOldFp[unit, eid]=FpEntry(unit, eid, gid, stat_id, stage)
        stats = fp_entry.find('stats')
        if stats is None:
            continue
        for stat in stats.findall('stat'):
            type = int(stat.get('type'))
            val = int(stat.get('val'))
            gOldFp[unit, eid].add_stat(type, val)

def get_uptime_seconds():
    try:
        for line in open('/proc/uptime'):
            (upSeconds, idleSeconds) = line.strip().split()
            (upSeconds, upMSeconds) = upSeconds.split('.')
            upSeconds = int(upSeconds)
    except IOError:
        upSeconds = 0
    return upSeconds

def read_old_fp():
    try:
        tree = ET.parse(gOldFpFile)
    except:
        sys.stderr.write('Old file doesnot exist\n')
        return

    if tree is None:
        raise UnexpectedXmlContent('Could not locate XML tree\n')

    root = tree.getroot()
    if root is None:
        raise UnexpectedXmlContent('Could not locate XML root\n')

    # check version
    verStr = root.get("version")
    if verStr is None:
        raise UnexpectedXmlContent('Could not locate file version')
    (fMajVer, fMinVer, fSubVer) = verStr.split('.')
    (myMajVer, myMinVer, mySubVer) = gClDbgAclVer.split('.')
    if int(fMajVer) > int(myMajVer):
        sys.stderr.write('Old file version is not compatible; removing it\n')
        del_old_fp_file()
        return

    # check switchd pid
    switchd_info = root.find('switchd')
    if switchd_info is None:
        raise UnexpectedXmlContent('Could not locate switchd info\n')
    oldPid = int(switchd_info.get('pid'))
    newPid = get_switchd_pid()
    if oldPid != newPid:
        sys.stderr.write('switch pid mismatch (%d/%d); removing old file \n' %\
                     (oldPid, newPid))
        del_old_fp_file()
        return

    decode_fp_xml_info(root)

def dump_fp_stats_per_unit(dumpUnit, verbose, all):
    firstEntry = True
    for unit, eid in sorted(gCurrFp):
        if unit != dumpUnit:
            continue
        fpEntry = gCurrFp[unit, eid]
        if (all or fpEntry.is_nz()) and\
             (not (unit, eid) in gOldFp or fpEntry != gOldFp[unit, eid]):
            if firstEntry:
                firstEntry = False
                if len(gUnits) > 1:
                    sys.stdout.write('\nUnit %d\n' % unit)
                    sys.stdout.write('====\n')
            fpEntry.dump_stat(verbose)
    if dumpUnit in gCurrFpProgramSeq and dumpUnit in gOldFpProgramSeq and\
        gCurrFpProgramSeq[dumpUnit] != gOldFpProgramSeq[dumpUnit]:
        sys.stdout.write('Unit %d FP was reprogrammed since last dump (%d/%d)\n'\
             % (unit, gOldFpProgramSeq[dumpUnit], gCurrFpProgramSeq[dumpUnit]))

# Dumps the fp entries whose stats have changed since the last dump
def dump_fp_stats(same, verbose, all):
    # Get last read stats (unless the user has asked for all stats)
    if all:
        sys.stdout.write('Showing all counters\n')
    elif same:
        sys.stdout.write('Showing all non-zero counters\n')
    else:
        read_old_fp()

    # Get current stats
    parse_fp_show(verbose=verbose)
    # Store stats for next read
    write_old_fp()
    #sys.stdout.write('\nNEW FP\n')
    for unit in gUnits:
        dump_fp_stats_per_unit(unit, verbose, all)

def xlate_fp_entry(eid):
    parse_fp_show(verbose=True)
    for unit in gUnits:
        if (unit, eid) in gCurrFp:
            fpEntry = gCurrFp[unit, eid]
            if len(gUnits) > 1:
                sys.stdout.write('\nUnit: %d\n' % unit)
            fpEntry.dump_stat(verbose=True)

def clear_fp_stats():
    sys.stdout.write('Clearing hw counters\n')
    # delete cached file if any
    del_old_fp_file()
    parse_fp_show(clearStat=True)

########################################################################
# Main entry point
########################################################################
if __name__ == '__main__':
    parser = argparse.ArgumentParser(
        description='Run ACL hw diag commands')
    parser.add_argument('-v', '--verbose',
                        required=False,
                        action='store_true',
                        help='Verbose output')
    parser.add_argument('-c', '--clearcounters',
                        required=False,
                        action='store_true',
                        help='clear hw counters')
    parser.add_argument('-s', '--showsame',
                        required=False,
                        action='store_true',
                        help='show counters whose value has not changed')
    parser.add_argument('-a', '--showall',
                        required=False,
                        action='store_true',
                        help='show all counters; zero and non-zero ')
    parser.add_argument('-x', '--xlate-eid',
                        required=False,
                        type=int,
                        help='translate FP eid to cl-acl entry')
    argcomplete.autocomplete(parser)

    try:
        args = parser.parse_args()
    except ParseError, e:
        parser.error(str(e))
  
    if (os.geteuid() != 0):
        sys.stderr.write('root privileges are needed to run dbg-acl\n')
        sys.exit(-1)

    if not bcmShellAvailable:
        sys.stderr.write(\
            'dbg-acl: bcmshell not available, unable to query hardware\n')
        sys.exit(-1)

    try:
        bcmshell.bcmshell().run('echo foo')
    except IOError as e:
        sys.stderr.write(('%s\n' % e))
        sys.exit(-1)

    # Locate the number of units
    unitStr = bcmshell.bcmshell().run('show unit')
    unitPat = re.compile('Unit (?P<uid>\d+) chip .*')
    for line in unitStr.splitlines():
        obj = unitPat.search(line)
        if obj:
            uid = int(obj.group('uid'))
            if uid in gUnits:
                sys.stderr.write('Duplicate chip unit %d detected\n' % uid)
                continue
            gUnits.append(uid)

    if args.clearcounters:
        clear_fp_stats()
        sys.exit(0)

    if args.xlate_eid:
        xlate_fp_entry(args.xlate_eid)
        sys.exit(0)

    # by default we just display changed stats
    dump_fp_stats(args.showsame, args.verbose, args.showall)
