'''
Name: avi_attribute_qc.py

Overview: Provides quality control routines to validate AVI attribute content and
          relationships.

Description:
    This is the Python replacement for the legacy IAS system. It is more or
    less a direct translation of the VBA code used by IAS to evaluate AVI
    attributes.

    The attribute audit creates two tables, the first is an informational
    table that contains information on the audit run while the other table
    contains any errors or warnings that were generated. The information table
    conatins a single field that contains messages such as date of QC run. The
    other audit table consists of three items: ID, TYPE, and MESSAGE. The ID links
    the message to the POLY_NUM of the feature that was audited. The TYPE field
    contains the type of message, either WARNING or ERROR. The MESSAGE field contains
    the actual audit message.

    The informational and audit tables will be created in the same FGDB as the
    feature class being audited. The name of the informational table is the same
    as the feature class but with an _INFO added to end. The audit table is also
    the same as the feature class but with an _ATT_LOG appended to end. If the
    feature class is contained in a feature dataset then these tables cannot be
    generated within the feature dataset and will be located at the root level
    in the FGDB.
    
    Because an individual feature can have multiple messages associated with it
    it is not appropriate to use a join from the feature class to the audit table.
    Rather, a relate should be performed so that all audit messages can be displayed.

    The easiest way of viewing audit messages is to run the audit report tool.
    This will generate a text file containing the ID (POLY_NUM) and any associated
    errors or warnings.
    
Notes:
    Uses the AVI geodatabase data model for accessing attributes.

Author: Doug Crane
        Dec, 2009

Modifications:
    May 9, 2011 Added check for Existing Data and TPR. If a forested stand
    is less than 2 m then TPR cannot be calculated using formulas and should
    therefore be interpeted. Error will occur if Existing Data is not already
    populated with another value and height is less than 2 m.

    Jan 5, 2017 Added error for missing Interpreter Initials and warnings for
    no understory when overstory Density ws A or B.

    Jul 9, 2019 Added modifier and other changes to reflect specification updates for 2019
'''

import pdb
import os
import sys
import datetime
import math

import arcpy

from srd.srd_exception import *
import srd.srd_logging as srd_logging
import srd.srd_misc as srd_misc
import srd.srd_audit_log as srd_audit_log
import srd.srd_featureclass_info as srd_featureclass_info
import srd.srd_recordset as srd_recordset

from avi_record import *

import avi_tpr
import avi_schema

__author__ = 'Doug Crane'
__version__ = '1.0'

__all__ = ['AVI_AttributeAudit']
        
# ----------------------------------------------------------
class AVI_AttributeAudit(object):
    '''
    Provides methods for creating AVI geodatabase feature class
    '''

    # Case sensitive domain lists for AVI fields.
    domainSpeciesList = ["Sw","Sb","Se","P","Pl","Pj","Pa","Pf","Fb","Fd","Fa","Lt","La","Lw","Aw","Bw","Pb","A",""]
    domainMoistureRegimeList = ['d','m','w','a','']
    domainTPRList = ['G','M','F','U','']
    domainCrownClosureList = ['A','B','C','D','']
    domainStandStructureList = ['M','H','C','']
    domainDataSourceList = ['F','P','V','C','S','A','L','I','']
    domainNonForestVegList = ['SC','SO','HG','HF','BR','']
    domainNatNonVegList = ['NWI','NWL','NWR','NWF','NMB','NMC','NMR','NMS','']
    domainAnthVegList = ['CA','CP','CPR','CIP','CIW','CIH','']
    domainAnthNonVegList = ['ASC','ASR','AIH','AIE','AIG','AIF','AIM','AII','']
    domainModifierList = ['BV','SF','SL','BK','FS','UP','MO','CC','BU','WF','CL','DI','IK','UK','WE','DT','BT','SN','ST','SI','SC','PL','TH','GR','IR','RT','']

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def version(self):
        '''
        Current version of software.
        '''
        return 'Jan 12, 2022'
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __init__(self, featureClass):
        '''
        Parameters:
        '''
        
        # Set basic geoprocessing environment.
        arcpy.env.overwriteOutput = True
        arcpy.gp.logHistory = False

        # Count maintained for errors and warnings,.
        self.totalErrorCount = 0
        self.totalWarningCount = 0
        
        # Information regarding dataset being audited. Must
        # set when audit method called.
        self.aviFCPath = featureClass

        utilFCObj = srd_featureclass_info.SRD_FeatureClassInfo(featureClass)

        self.aviFCName = os.path.basename(featureClass)
        self.fgdbPath = utilFCObj.getFGDBPath()
        if not self.fgdbPath:
            raise SRD_Exception('Cannot obtain path to FGDB containing feature class')

        self.fgdbDir = utilFCObj.getParentDir()
        self.fgdbName = os.path.basename(self.fgdbPath)

        name,ext = os.path.splitext(self.fgdbName)
        logFileName = '%s.log' % name
        self.logFilePath = os.path.join(self.fgdbDir, logFileName)
        self._logChn = srd_logging.SRD_Log(self.logFilePath)

        self.auditTableName = '%s_att_log' % self.aviFCName
        self.auditTablePath = os.path.join(self.fgdbPath, self.auditTableName)

        self.infoTableName = '%s_info' % self.aviFCName
        self.infoTablePath = os.path.join(self.fgdbPath, self.infoTableName)
                    
        self.idFieldName = ''

        # Build list of acceptable domain values for use in error messages.
        self.validSpecies = self._buildDomainString(self.domainSpeciesList)
        self.validMoistureRegime = self._buildDomainString(self.domainMoistureRegimeList)
        self.validTPR = self._buildDomainString(self.domainTPRList)
        self.validCrownClosure = self._buildDomainString(self.domainCrownClosureList)
        self.validStandStructure = self._buildDomainString(self.domainStandStructureList)
        self.validDataSource = self._buildDomainString(self.domainDataSourceList)
        self.validNonForestVeg = self._buildDomainString(self.domainNonForestVegList)
        self.validNatNonVeg = self._buildDomainString(self.domainNatNonVegList)
        self.validAnthVeg = self._buildDomainString(self.domainAnthVegList)
        self.validAnthNonVeg = self._buildDomainString(self.domainAnthNonVegList)
        self.validModifier = self._buildDomainString(self.domainModifierList)
        
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _buildDomainString(self, domainList):
        '''
        Builds a string containing all the domain values. Used
        to clarify error messages.

        Parameters:
            domainList - list of domain values to use.
        '''

        domainString = ''
        for domainValue in domainList:
            if domainString:
                domainString += ' %s' % domainValue
            else:
                domainString = domainValue

        return domainString
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __del__(self):
        '''
        Remove reference to geoprocessing object.
        '''
        self._logChn.closeLog()
        
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def validateSpecies(self, aviRecord, auditLogObj, layer):
        '''
        Validate basic species data including relationships.

        Parameters:
            aviRecord - either an understorey or overstorey record that has
                        been extracted to a common structure where SP1
                        can mean either SP1 or USP1. The layer parameter
                        determines which layer it came from.

            auditLogObj - current audit log where messages will be written.

            layer - either OVERSTOREY or UNDERSTOREY
        '''

        sp1 = aviRecord['sp1']
        sp1_per = aviRecord['sp1_per']
        sp2 = aviRecord['sp2']
        sp2_per = aviRecord['sp2_per']
        sp3 = aviRecord['sp3']
        sp3_per = aviRecord['sp3_per']
        sp4 = aviRecord['sp4']
        sp4_per = aviRecord['sp4_per']
        sp5 = aviRecord['sp5']
        sp5_per = aviRecord['sp5_per']

        totPct = sp1_per + sp2_per + sp3_per + sp4_per + sp5_per

        errList = []
        warnList = []
        msg = ''
        
        # Species must be in order and not seperated by blank species fields.
        if not sp1:
            if sp2 or sp3 or sp4 or sp5:
                msg = '%s 3.5.1 Species 1 cannot be blank if any of SP2 - SP5 is populated' % layer
        elif not sp2:
            if sp3 or sp4 or sp5:
                msg = '%s 3.5.1 Species 2 cannot be blank if any of SP3 - SP5 is populated' % layer
        elif not sp3:
            if sp4 or sp5:
                msg = '%s 3.5.1 Species 3 cannot be blank if any of SP4 - SP5 is populated' % layer
        elif not sp4:
            if sp5:
                msg = '%s 3.5.1 Species 4 cannot be blank if SP5 is populated' % layer

        if msg:
            errList.append(msg)

        # Validate domain.
        if sp1:
            if not sp1 in self.domainSpeciesList:
                errList.append('%s 2.5 Species 1: %s invalid, must be one of: %s' % (layer, sp1, self.validSpecies))
            if sp1_per < 1 or sp1_per > 10:
                errList.append('%s 3.5.7 Species percent 1: %s invalid, must be between 1 and 10' % (layer, sp1_per))
                
        if sp2:
            if not sp2 in self.domainSpeciesList:
                errList.append('%s 2.5 Species 2: %s invalid, must be one of: %s' % (layer, sp2, self.validSpecies))
            if sp2_per < 1 or sp2_per > 5:
                errList.append('%s 3.5.8 Species percent 2: %s invalid, must be between 1 and 5' % (layer, sp2_per))
            if sp2_per > sp1_per:
                errList.append('%s 3.5.9 Species percent 2: %s invalid, cannot be greater than percent of a previous species' % (layer, sp2_per))

        if sp3:
            if not sp3 in self.domainSpeciesList:
                errList.append('%s 2.5 Species 3: %s invalid, must be one of: %s' % (layer, sp3, self.validSpecies))
            if sp3_per < 1 or sp3_per > 5:
                errList.append('%s 3.5.8 Species percent 3: %s invalid, must be between 1 and 5' % (layer, sp3_per))
            if sp3_per > sp2_per or sp3_per > sp1_per:
                errList.append('%s 3.5.9 Species percent 3: %s invalid, cannot be greater than percent of a previous species' % (layer, sp3_per))

        if sp4:
            if not sp4 in self.domainSpeciesList:
                errList.append('%s 2.5 Species 4: %s invalid, must be one of: %s' % (layer, sp4, self.validSpecies))
            if sp4_per < 1 or sp4_per > 5:
                errList.append('%s 3.5.8 Species percent 4: %s invalid, must be between 1 and 5' % (layer, sp4_per))
            if sp4_per > sp3_per or sp4_per > sp2_per or sp4_per > sp1_per:
                errList.append('%s 3.5.9 Species percent 4: %s invalid, cannot be greater than percent of a previous species' % (layer, sp4_per))

        if sp5:
            if not sp5 in self.domainSpeciesList:
                errList.append('%s 2.5 Species 5: %s invalid, must be one of: %s' % (layer, sp5, self.validSpecies))
            if sp5_per < 1 or sp5_per > 5:
                errList.append('%s 3.5.8 Species percent 5: %s invalid, must be between 1 and 5' % (layer, sp5_per))
            if sp5_per > sp4_per or sp5_per > sp3_per or sp5_per > sp2_per or sp5_per > sp1_per:
                errList.append('%s 3.5.9 Species percent 5: %s invalid, cannot be greater than percent of a previous species' % (layer, sp5_per))

        # If any species present then validate total percentage.
        if sp1 or sp2 or sp3 or sp4 or sp5:
            if totPct <> 10:
                errList.append('%s 3.5.2 Total species percent: %s invalid, does not add up to 10' % (layer, totPct))

        # If a species percent is present the there should be an associated species.
        if sp1_per:
            if not sp1:
                errList.append('%s 3.5.3 Species percent 1: %s present but missing a species call' % (layer, sp1_per))

        if sp2_per:
            if not sp2:
                errList.append('%s 3.5.3 Species percent 2: %s present but missing a species call' % (layer, sp2_per))

        if sp3_per:
            if not sp3:
                errList.append('%s 3.5.3 Species percent 3: %s present but missing a species call' % (layer, sp3_per))

        if sp4_per:
            if not sp4:
                errList.append('%s 3.5.3 Species percent 4: %s present but missing a species call' % (layer, sp4_per))

        if sp5_per:
            if not sp5:
                errList.append('%s 3.5.3 Species percent 5: %s present but missing a species call' % (layer, sp5_per))

        # Make sure no species are duplicated:
        spList = [sp1, ]

        if sp2:
            if sp2 == sp1:
                errList.append('%s 3.5.4 Species 2: %s invalid, already exists, cannot be duplicated' % (layer, sp2))
            else:
                spList.append(sp2)
                
        if sp3:
            if sp3 in spList:
                errList.append('%s 3.5.4 Species 3: %s invalid, already exists, cannot be duplicated' % (layer, sp3))
            else:
                spList.append(sp3)

        if sp4:
            if sp4 in spList:
                errList.append('%s 3.5.4 Species 4: %s invalid, already exists, cannot be duplicated' % (layer, sp4))
            else:
                spList.append(sp4)

        if sp5:
            if sp5 in spList:
                errList.append('%s 3.5.4 Species 5: %s invalid, already exists, cannot be duplicated' % (layer, sp5))
            else:
                spList.append(sp5)

        # Certain combinations of species are not allowed.
        if 'A' in spList:
            if 'Aw' in spList or 'Pb' in spList:
                errList.append('%s 3.5.5 Cannot have a species: A if one of the other species is Aw or Pb' % layer)

        if 'P' in spList:
            if 'Pl' in spList or 'Pj' in spList or 'Pa' in spList or 'Pf' in spList:
                errList.append('%s 3.5.6 Cannot have a species: P if one of the other species is Pl, Pj, Pa, or Pf' % layer)
                               
        # Add all validation messages to our audit log.            
        rec_id = aviRecord['oid']
        auditLogObj.addAuditRecords(rec_id, errList, 'ERROR')

        self.totalErrorCount += len(errList)
        self.totalWarningCount += len(warnList)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def validateDataSource(self, aviRecord, auditLogObj, layer):
        '''
        Validates the data source.
        
        Parameters:
            aviRecord - either an understorey or overstorey record that has
                        been extracted to a common structure where SP1
                        can mean either SP1 or USP1. The layer parameter
                        determines which layer it came from.

            auditLogObj - current audit log where messages will be written.

            layer - either OVERSTOREY or UNDERSTOREY
        '''

        nat_non = aviRecord['nat_non']
        anth_veg = aviRecord['anth_veg']
        anth_non = aviRecord['anth_non']
        data = aviRecord['data']
        data_yr = aviRecord['data_yr']
        ht = aviRecord['height']
        sp1 = aviRecord['sp1']

        errList = []
        warnList = []

        if data:
            if not data in self.domainDataSourceList:
                errList.append('%s 2.18 Data Source: %s invalid, must be one of %s' % (layer, data, self.validDataSource))

            if data == 'I':
                if anth_veg:
                    errList.append('%s 9.1.1 Data Source I cannot be used in conjunction with an Anthropogenic Vegetated stand' % layer)
                if anth_non:
                    errList.append('%s 9.1.2 Data Source I cannot be used in conjunction with an Anthropogenic Non-vegetated stand' % layer)
                if nat_non:
                    errList.append('%s 9.1.3 Data Source I cannot be used in conjunction with an Naturally Non-vegetated stand' % layer)
                if data_yr:
                    errList.append('%s 9.1.4 When Data Source is I then no Data Source Year is allowed' % layer)
        else:
            if data_yr:
                errList.append('%s 9.1.5 Data Source is missing but Data Source Year: %s is populated' % (layer, data_yr))

##            if sp1 and ht:
##                if ht < 2:
##                    errList.append('%s Data Source must be I if forested and tree height is less than 2 m (%s) [stands less than 2 cannot be calculated using TPR formula]' % (layer, ht))
                    
        if data_yr:
            curYr = datetime.date.today().year
            if data_yr < 1940 or data_yr > curYr:
                errList.append('%s 9.1.6 Data Source Year: %s invalid, must be between 1940 and %s' % (layer, data_yr, curYr))
                                
        # Add all validation messages to our audit log.            
        auditLogObj.addAuditRecords(aviRecord['oid'], errList, 'ERROR')
        auditLogObj.addAuditRecords(aviRecord['oid'], warnList, 'WARNING')

        self.totalErrorCount += len(errList)
        self.totalWarningCount += len(warnList)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def validateModifiers(self, aviRecord, auditLogObj, layer):
        '''
        Validate basic modifier data including relationships.

        Parameters:
            aviRecord - either an understorey or overstorey record that has
                        been extracted to a common structure where SP1
                        can mean either SP1 or USP1. The layer parameter
                        determines which layer it came from.

            auditLogObj - current audit log where messages will be written.

            layer - either OVERSTOREY or UNDERSTOREY
        '''

        mod1 = aviRecord['mod1']
        mod1_ext = aviRecord['mod1_ext']
        mod1_yr = aviRecord['mod1_yr']
        mod2 = aviRecord['mod2']
        mod2_ext = aviRecord['mod2_ext']
        mod2_yr = aviRecord['mod2_yr']
        anth_veg = aviRecord['anth_veg']
        nfl = aviRecord['nfl']
        sp1 = aviRecord['sp1']
        sp2 = aviRecord['sp2']
        sp3 = aviRecord['sp3']
        sp4 = aviRecord['sp4']
        sp5 = aviRecord['sp5']
        tpr = aviRecord['tpr']
        origin = aviRecord['origin']
        moist_reg = aviRecord['moist_reg']
        density = aviRecord['density']
        photo_yr = aviRecord['photo_yr']
        data = aviRecord['data']
        aris = aviRecord['aris']
        errList = []
        warnList = []
        msg = ''

        curYr = datetime.date.today().year
        
        if mod1:
            if not mod1 in self.domainModifierList:
                errList.append('%s 2.17 Modifier 1: %s invalid, must be one of: %s' % (layer, mod1, self.validModifier))
            ## Modified Jan 2022 to add FS and RT to list not requiring extent
            if mod1 not in ('CL', 'FS', 'RT'):
                if not mod1_ext:
                    errList.append('%s Modifier 1 Extent missing' % layer)

        if mod1_ext:
            if mod1_ext < 1 or mod1_ext > 5:
                errList.append('%s 10.1.1 Modifier 1 Extent: %s invalid, must be between 1 - 5' % (layer, mod1_ext))
            if mod1 in ('CL', 'FS', 'RT'):
                errList.append('%s 10.1.2 Modifier 1 Extent: %s invalid, no extent is allowed with a CL, FS, RT modifier' % (layer, mod1_ext))
            if not mod1:
                errList.append('%s 10.1.3 Modifier 1 Extent: %s present without a modifier' % (layer, mod1_ext))

        if mod1_yr:
            if mod1_yr < 1600 or mod1_yr > curYr:
                msg = '%s 10.2.1 Modifier 1 Year: %s invalid, must be between 1600 and %s' % (layer, mod1_yr, curYr)
                errList.append(msg)
            if not mod1:
                errList.append('%s 10.2.2 Modifier 1 Year: %s present without a modifier' % (layer, mod1_yr))
            if mod1 in ('CL', 'FS', 'RT'):
                errList.append('%s 10.1.2.2 Modifier 1 Year: %s invalid, no modifier year is allowed with a CL, FS, RT modifier' % (layer, mod1_ext))

        if mod2:
            if not mod2 in self.domainModifierList:
                errList.append('%s 2.17 Modifier 2: %s invalid, must be one of: %s' % (layer, mod2, self.validModifier))
            if mod2 not in ('CL', 'FS', 'RT'):
                if not mod2_ext:
                    errList.append('%s Modifier 2 Extent missing' % layer)
            if mod2 in ('MO', 'RT', 'FS', 'BK'):
                errList.append('%s 10.1.1.2 Modifier 2 cannot contain MO, RT, FS, or BK (%s)' % (layer, mod2))
                            
        if mod2_ext:
            if mod2_ext < 1 or mod2_ext > 5:
                errList.append('%s 10.1.1 Modifier 2 Extent: %s invalid, must be between 1 - 5' % (layer, mod2_ext))
            if mod2 in ('CL', 'FS', 'RT'):
                errList.append('%s 10.1.2 Modifier 2 Extent: %s invalid, no extent is allowed with a CL, FS, RT modifier' % (layer, mod2_ext))
            if not mod2:
                errList.append('%s 10.1.3 Modifier 2 Extent: %s present without a modifier' % (layer, mod2_ext))

        if mod2_yr:
            if mod2_yr < 1600 or mod2_yr > curYr:
                msg = '%s 10.2.1 Modifier 2 Year: %s invalid, must be between 1600 and %s' % (layer, mod2_yr, curYr)
                errList.append(msg)
            if not mod2:
                errList.append('%s 10.2.2 Modifier 2 Year: %s present without a modifier' % (layer, mod2_yr))
            if mod2 in ('CL', 'FS', 'RT'):
                errList.append('%s 10.1.2.2 Modifier 2 Year: %s invalid, no modifier year is allowed with a CL, FS, RT modifier' % (layer, mod1_ext))

        # Ensure that MOD1 and MOD2 are not duplicated.
        noDupModifierList = ['BV','SF','SL','BK','WF','CL','DI','IK','UK','WE','DT','BT','SN','ST','GR','IR']
        if mod1 and mod2:
            if mod1 == mod2:
                # Most modifiers cannot be duplicated.
                if mod1 in noDupModifierList:
                    errList.append('%s 10.3.1 Modifier 1: %s and Modifier 2: %s cannot be the same' % (layer, mod1, mod2))
                else:
                    # In some cases can be the same if years are different.
                    if mod1_yr == mod2_yr:
                        errList.append('%s 10.3.2 Modifier 1: %s and Modifier 2: %s cannot be the same unless modifier years are different' % (layer, mod1, mod2))
                    else:
                        warnList.append('%s 10.3.3 (Modifier 1: %s Year: %s) and (Modifier 2: %s Year: %s) are the same with only difference in modifier years' % (layer, mod1, mod1_yr, mod2, mod2_yr))

        # Must have a Mod1 if Mod2 present.
        if mod2 and not mod1:
            errList.append('%s 10.3.4 Modifier 2 cannot have a value if Modifier 1 does not already have a value. (mod1: %s mod2: %s)' % (layer, mod1, mod2))

        if mod1 and mod2 == 'CC':
            if mod1 != 'CC':
                errList.append('%s 10.3.5 Where a layer has multiple modifiers and one of them is CC then the CC must be defined as modifier 1 regardless of origins.)' % layer)
            
        # Most recent modifier must be second unless CC.
        if mod1_yr and mod2_yr:
            if mod1_yr > mod2_yr:
                if mod1 != 'CC':
                    errList.append('%s 10.3.6 Where first modifier is not CC then the most recent Stand Modifier must be second modifier. (mod1 yr: %s mod2 yr: %s)' % (layer, mod1_yr, mod2_yr))
                else:
                    if mod1 == 'CC' and mod2 == 'CC':
                        errList.append('%s 10.3.7 Where CC is contained in both modifier 1 and 2, the most recent CC must be the second modifier. (mod1 yr: %s mod2 yr: %s)' % (layer, mod1_yr, mod2_yr))

        yrDif = 0
        if photo_yr:
            if mod1_yr:
                yrDif = photo_yr - mod1_yr
                if mod1_yr > photo_yr and data != 'S':
                    errList.append('%s 10.3.8 Modifier year: %s cannot be greater than the Photo year: %s unless DATA = S' % (layer, mod1_yr, photo_yr))
    
        if yrDif > 0:
            if mod1 == 'CC' and yrDif < 5 and mod1_yr > 0:

                # Removed July 2019
##                if mod1_ext == 5 and sp1 != '':
##                    errList.append('%s When modifier 1 is CC, extent is 5, and year of cut is less than 5 years old (using photo year), then you cannot have any tree species associated with the stand' % layer)

                if mod1_ext == 4 and not density in ['A', ''] :
                    warnList.append('%s 10.3.9 When modifier 1 is CC, extent is 4, and year of cut is less than 5 years old (using photo year), then you cannot have a forested stand with a crown closure greater than A. (crown closure: %s)' % (layer, density))

                if mod1_ext == 3 and not density in ['A', 'B', ''] :
                    warnList.append('%s 10.3.10 When modifier 1 is CC, extent is 3, and year of cut is less than 5 years old (using photo year), then you cannot have a forested stand with a crown closure greater than B. (crown closure: %s)' % (layer, density))

                if mod1_ext == 2 and not density in ['A', 'B', 'C', ''] :
                    warnList.append('%s 10.3.11 When modifier 1 is CC, extent is 2, and year of cut is less than 5 years old (using photo year), then you cannot have a forested stand with a crown closure greater than C. (crown closure: %s)' % (layer, density))

            # Added for July 2019 changes
            if mod1 == 'BU' and yrDif > 30:
                errList.append('%s 10.3.12 When a burn origin is >= 30 years (using photo year) then modifier 1 BU should not be used. Burn is reflected in origin. Calculated Age: %s' % (layer, yrDif))
                
                
        # Changed to error July 2019
        if mod1 == 'CC' and not mod1_yr:
            errList.append('%s 10.3.13 Year of cutblock missing for modifier 1; must have a modifier year' % layer)

        if mod1 == 'CC' and  mod1_yr > 0 and mod1_yr < 1950:
            if mod1_yr < 1930:
                errList.append('%s 10.3.14 Year of cutblock is prior to 1930 for modifier 1. (mod yr: %s)' % (layer, mod1_yr))
            else:
                warnList.append('%s 10.3.15 Year of cutblock is prior to 1950 for modifier 1. (mod yr: %s)' % (layer, mod1_yr))

        # Added for July 2019 changes
##        if mod1 == 'CC' and mod1_ext in (4, 5):
##            if density and density.upper() in ('C', 'D'):
##                errList.append('%s When MOD1 is CC and MOD1_EXT is 4 or 5 then Density cannot be C or D (density: %s)' % (layer, density))

        # Added for July 2019 changes
        if density in ('C', 'D') and mod1 in ('CC', 'BU') and mod1_ext == 5:
            if origin and mod1_yr and origin < mod1_yr:
                errList.append('%s 10.3.16 When MOD1 is CC or BU and MOD1_EXT is 5 then Origin cannot be less than MOD1_YR (origin: %s mod1_yr: %s)' % (layer, origin, mod1_yr))

        # Added Nov 2019
        if mod1 == 'CC' and mod1_ext:
            if mod1_ext == 5:
                if sp1 and mod2 != 'BU':
                    if mod1_yr and origin != mod1_yr:
                        errList.append('%s 10.3.17 For treed stands, when MOD1 is CC and MOD2 <> BU and MOD1_EXT is 5, then Origin must be same as MOD1_YR (origin: %s mod1_yr: %s)' % (layer, origin, mod1_yr))

            else:
                # Extent less than 5
                if mod1_yr and origin >= mod1_yr:
                    errList.append('%s 10.3.18 When MOD1 is CC and MOD1_EXT < 5, then Origin must be less than MOD1_YR (origin: %s mod1_yr: %s)' % (layer, origin, mod1_yr))
                    
                
        # Added for July 2019 changes
##        if density in ('A', 'B', 'C', 'D') and mod1 == 'CC' and mod1_ext == 5:
##            if mod1_yr and mod2_yr and mod2 == 'BU' and mod2_ext == 5 and mod2_yr > mod1_yr:
##                if origin != mod2_yr:
##                    errList.append('%s When MOD2_YR > MOD1_YR and MOD2 = BU with MOD2_EXT = 5 and Origin not equal to MOD2_YR then Density cannot be A, B, C, or D (origin: %s mod2_yr: %s density: %s )' % (layer, origin, mod1_yr, density))
##            else:
##                if origin and mod1_yr and origin != mod1_yr:
##                    errList.append('%s When MOD1 is CC with MOD1_EXT = 5 and Origin not equal to MOD1_YR then Density cannot be A, B, C, or D (origin: %s mod1_yr: %s density: %s )' % (layer, origin, mod1_yr, density))

        # Modified Sep 2021 for specification update
        if sp1 and mod1 == 'CC' and mod1_ext == 5 and mod1_yr and origin:
            if mod2 and mod2_yr and mod2_ext and mod2 == 'BU' and mod2_ext < 5:
                if origin != mod1_yr:
                    errList.append('%s 10.3.19 When MOD1 is CC with MOD1_EXT = 5 and MOD2 is BU with MOD2_EXT < 5 then origin must be the same as MOD1_YR (origin: %s mod1_yr: %s mod2_yr %s) )' % (layer, origin, mod1_yr, mod2_yr))

## Removed Jan 2022
##            else:
##                if origin != mod1_yr:
##                    errList.append('%s 10.3.20 When MOD1 is CC with MOD1_EXT = 5 then origin must be the same as MOD1_YR (origin: %s mod1_yr: %s) )' % (layer, origin, mod1_yr))                

        # Changed to error July 2019
        if mod1 == 'BU' and not mod1_yr:
            errList.append('%s 10.3.21 Year of burn missing for modifier 1; must have a modifier year' % layer)

        if mod1 == 'MO' and not mod1_yr:
            errList.append('%s 10.3.21 Year of MOF block missing for modifier 1; must have a modifier year' % layer)

        # Added for July 2019 changes
        if origin and mod1 == 'CC' and mod1_ext == 5 and mod2 == 'BU' and mod2_ext == 5:
            if mod1_yr and mod2_yr:
                if mod1_yr < mod2_yr and origin != mod2_yr:
                    errList.append('%s 10.3.22 If modifier extent is 5 for both CC and BU modifiers and where modifier year for BU is more recent then CC then Origin must match that of BU (origin: %s mod2_yr: %s ' % (layer, origin, mod2_yr))
                    
        # Added for July 2019 changes
        if origin and origin >= 1970 and layer == 'OVERSTOREY':
            if not mod1:
                warnList.append('%s 10.3.23 Stands with origin >= 1970 have most likely been burned or harvested so should have a modifier, extent and modifier year (origin: %s)' % (layer, origin))
                
        # Added for July 2019 changes
        # Modified Oct 2021 to remove treed requsite
        if aris and layer == 'OVERSTOREY':
            if mod1 not in ('CC', 'RT') and mod2 not in ('CC', 'RT'):
                errList.append('%s 10.3.24 When ARIS field is populated then should have a CC or RT modifier and modifier year' % layer)

        # Added for July 2019 changes
        # Removed Nov 2019
##        if mod1 == 'SN' and mod1_ext > 3:
##            if density in ('C', 'D'):
##                errList.append('%s When MOD1 is SN and the MOD1_EXT is greater than 3 then Density cannot be C or D' % layer)

        # Added for July 2019 changes
        # REMOVED at acceptance testing
##        if density in ('A', 'B'):
##            if mod1_ext and mod1_ext > 4:
##                errList.append('%s When Density is A or B then MOD1_EXT must be less than 5 (MOD1: %s)' % (layer,  mod1))
##
##            if mod2_ext and mod2_ext > 4:
##                errList.append('%s When Density is A or B then MOD2_EXT must be less than 5 (MOD2: %s)' % (layer, mod2))
                
        # Added for July 2019 changes
        if mod1 == 'BK':

            if tpr and tpr.upper() in ('F', 'U'):
               warnList.append('%s 10.3.25 A modifier of BK is questionable in association with a of TPR U or F' % layer)
## Removed Jan 2022 as SN does not need year           
##            if mod2 == 'SN' and mod1_yr and mod2_yr:
##                if mod2_yr < mod1_yr:
##                    errList.append('%s 10.3.26 SN MOD2_YR cannot be less than BK MOD2_YR (mod1_yr: %s mod2_yr: %s)' % (layer, mod1_yr, mod2_yr))
                    
            # Modifier 2 must be SN for BK
            if mod2 != 'SN':
                errList.append('%s 10.3.27 When BK is used as a modifier then MOD2 must be SN' % layer)
##            else:
##                if mod2_ext > 2:
##                    if density in ('C', 'D'):
##                        errList.append('%s 10.3.28 When MOD1 is BK and MOD2 is SN with MOD2_EXT greater than 2 then density cannot be C or D' % layer)

            if sp1:
                # Check pine existance based on modifier extent
                pineList = ('P','PL','PJ','PA','PF')
                spList = (sp1,sp2,sp3,sp4,sp5)
                pineFnd = False
                for sp in spList:
                    if sp and sp.upper() in pineList:
                        pineFnd = True

                if mod1_ext:
                    if mod1_ext == 5:    
                        if pineFnd:
                            errList.append('%s 10.3.29 When modifier is BK and modifier extent is 5 then no pine is allowed as a species' % layer)

                    if mod1_ext < 5:
                        if not pineFnd:
                            errList.append('%s 10.3.30 When modifier is BK and modifier extent is less than 5 then one of the species must be pine' % layer)

            else:
                warnList.append(' %s 10.3.31 When BK is used as a modifier then layer should be treed' % layer)

        if mod2 == 'BK':
            errList.append('%s 10.3.32 The modifier BK must be assigned to MOD1 not MOD2' % layer)
            
        if mod1 in ('CC', 'BU') and mod1_ext > 3 and mod1_yr and sp1:
            if origin != mod1_yr:
                if mod1_ext == 5:
                    # If origin is in same decade as CC or BU then just flag it as warning.
                    # Also, may have some residual trees so allow an A density 
                    isError = True
                    if origin and mod1_yr:
                        if origin == mod1_yr:
                            isError = False
                        else:
                            isError = True
                                
                    if isError and density != 'A':
                        errList.append('%s 10.3.33 Treed layer has a CC or BU modifier1 with an associated year and extent = 5 but the origin of the stand does not match the modifier year. (mod_yr: %s origin: %s)' % (layer, mod1_yr, origin))
                    else:
                        warnList.append('%s 10.3.34 Treed layer has a CC or BU modifier1 with an associated year but the origin of the stand does not match the modifier year. (mod_yr: %s origin: %s)' % (layer, mod1_yr, origin))                        
                else:
                    warnList.append('%s 10.3.35 Treed layer has a CC or BU modifier1 with an associated year but the origin of the stand does not match the modifier year. (mod_yr: %s origin: %s)' % (layer, mod1_yr, origin))

        yrDif = 0
        if photo_yr:
            if mod2_yr:
                yrDif = photo_yr - mod2_yr
                if mod2_yr > photo_yr and data != 'S':
                    errList.append('%s 10.3.8 Modifier 2 year: %s cannot be greater than the Photo year: %s unless DATA = S' % (layer, mod2_yr, photo_yr))
                    
        if yrDif > 0:
            if mod2 == 'CC' and yrDif < 5 and mod2_yr > 0:

                # Removed July 2019
##                if mod2_ext == 5 and sp1 != '':
##                    errList.append('%s When modifier 2 is CC, extent is 5, and year of cut is less than 5 years old (using photo year), then you cannot have any tree species associated with the stand' % layer)

                if mod2_ext == 4 and not density in ['A', ''] :
                    warnList.append('%s 10.3.9 When modifier 2 is CC, extent is 4, and year of cut is less than 5 years old (using photo year), then you cannot have a forested stand with a crown closure greater than A. (crown closure: %s)' % (layer, density))

                if mod2_ext == 3 and not density in ['A', 'B', ''] :
                    warnList.append('%s 10.3.10 When modifier 2 is CC, extent is 3, and year of cut is less than 5 years old (using photo year), then you cannot have a forested stand with a crown closure greater than B. (crown closure: %s)' % (layer, density))

                if mod2_ext == 2 and not density in ['A', 'B', 'C', ''] :
                    warnList.append('%s 10.3.11 When modifier 2 is CC, extent is 2, and year of cut is less than 5 years old (using photo year), then you cannot have a forested stand with a crown closure greater than C. (crown closure: %s)' % (layer, density))

            # Added for July 2019 changes
            if mod2 == 'BU' and yrDif > 30:
                errList.append('%s 10.3.12 When a burn orign is > or = 30 years (using photo year) then modifier 2 BU should not be used. Burn is reflected in origin. Calculated Age: %s ' % (layer, yrDif))

        # Changed to error July 2019 
        if mod2 == 'CC' and not mod2_yr:
            errList.append('%s 10.3.13 Year of cutblock missing for modifier 2; must have a modifier year' % layer)

        if mod2 == 'CC' and  mod2_yr > 0 and mod2_yr < 1950:
            if mod2_yr < 1930:
                errList.append('%s 10.3.14 Year of cutblock is prior to 1930 for modifier 1. (mod yr: %s)' % (layer, mod2_yr))
            else:
                warnList.append('%s 10.3.15 Year of cutblock is prior to 1950 for modifier 1. (mod yr: %s)' % (layer, mod2_yr))

        # Added for July 2019 changes
        # Removed Nov 2019
##        if mod2 == 'CC' and mod2_ext  in (4, 5):
##            if density and density.upper() in ('C', 'D'):
##                errList.append('%s When MOD2 is CC and MOD2_EXT is 4 or 5 then Density cannot be C or D (density: %s)' % (layer, density))

        # Added for July 2019 changes
        if mod2 in ('CC', 'BU') and mod2_ext == 5:
            if origin and mod2_yr:
                if origin < mod2_yr:
                    errList.append('%s 10.3.16 When MOD2 is CC or BU and MOD2_EXT is 5 then Origin cannot be less than MOD1_YR (origin: %s mod2_yr: %s)' % (layer, origin, mod2_yr))
        
        # Changed to error July 2019 
        if mod2 == 'BU' and not mod2_yr:
            errList.append('%s 10.3.21 Year of burn missing for modifier 2; must have a modifier year' % layer)

        if mod2 == 'MO' and not mod2_yr:
            errList.append('%s 10.3.21 Year of MOF block missing for modifier 2; must have a modifier year' % layer)

        # Removed Nov
        # Added for July 2019 changes
##        if mod2 == 'SN' and mod2_ext > 3:
##            if density in ('C', 'D'):
##                errList.append('%s When MOD2 is SN and the MOD2_EXT is greater than 3 then Density cannot be C or D' % layer)


        if mod2 in ('CC', 'BU') and mod2_ext > 3 and mod2_yr and sp1:
            if origin != mod2_yr:
                if mod2_ext == 5:
                    # If origin is in same decade as CC or BU then just
                    # flag it as warning.
                    isError = True
                    if origin and mod2_yr:
                        if origin == mod2_yr:
                            isError = False
                        else:
                            isError = True
                        
                    if isError and density != 'A':                        
                        errList.append('%s 10.3.33 Treed layer has a CC or BU modifier2 with an associated year and extent = 5 but the origin of the stand does not match the modifier year. (mod_yr: %s origin: %s)' % (layer, mod2_yr, origin))
                    else:
                        warnList.append('%s 10.3.34 Treed layer has a CC or BU modifier2 with an associated year but the origin of the stand does not match the modifier year. (mod_yr: %s origin: %s)' % (layer, mod2_yr, origin))
                else:
                    warnList.append('%s 10.3.35 Treed layer has a CC or BU modifier2 with an associated year but the origin of the stand does not match the modifier year. (mod_yr: %s origin: %s)' % (layer, mod2_yr, origin))

        if (mod1 == 'CC' and mod2 == 'CL') or (mod1 == 'CL' and mod2 == 'CC'):
            errList.append('%s 10.3.36 A CC modifier cannot be found in conjunction with a CL modifier' % layer)
                    
        if moist_reg == 'a' and (mod1 == 'CC' or mod2 == 'CC'):
            warnList.append('%s 10.3.37 A CC modifier was found in association with an aquatic moisture regime' % layer)

        # Added Oct 2021
        modYr = 0
        if mod1 == 'CC' and mod1_yr:
            modYr = mod1_yr
            
        if mod2 == 'CC' and mod2_yr:
            modYr = mod2_yr

        if modYr and modYr > 1990 and not aris:
            errList.append('%s 10.3.39 Areas harvested after 1990 must have the ARIS field populated' % layer)

        if (mod1 == 'RT' or mod2 == 'RT') and not aris:
            errList.append('%s 10.3.40 A RT modifier requires the ARIS field to be populated' % layer)
                
        # Add all validation messages to our audit log.            
        auditLogObj.addAuditRecords(aviRecord['oid'], errList, 'ERROR')
        auditLogObj.addAuditRecords(aviRecord['oid'], warnList, 'WARNING')

        self.totalErrorCount += len(errList)
        self.totalWarningCount += len(warnList)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def validateForestedLayer(self, aviRecord, auditLogObj, layer):
        '''
        Validate a layer that is classified as forested.

        Parameters:
            aviRecord - either an understorey or overstorey record that has
                        been extracted to a common structure where SP1
                        can mean either SP1 or USP1. The layer parameter
                        determines which layer it came from.

            auditLogObj - current audit log where messages will be written.

            layer - either OVERSTOREY or UNDERSTOREY
        '''

        moist_reg = aviRecord['moist_reg']
        sp1 = aviRecord['sp1']
        tpr = aviRecord['tpr']

        # List of required fields for Forested layer.
        requiredFieldList = ['sp1',
                             'sp1_per',
                             'moist_reg',
                             'height',
                             'density',
                             'origin', 
                             'tpr'
                             ]

        excludeFieldList = ['nat_non',
                            'nfl',
                            'nfl_per', 
                            'anth_veg',
                            'anth_non'
                            ]
                            
        # Species must be in order and not seperated by blank species fields.
        errList = []
        warnList = []

        # Verify existance of required fields.
        for fieldName in requiredFieldList:
            val = aviRecord[fieldName]
            if not val:
                if fieldName.lower() != 'tpr':
                    errList.append('%s 3.1 Forested layer required field: %s missing value' % (layer, fieldName))

        # Verify absence of excluded fields.
        for fieldName in excludeFieldList:
            val = aviRecord[fieldName]
            if val:
                errList.append('%s 3.2 Field: %s cannot be present in Forested layer' % (layer, fieldName))

        # Validate relationship between species and other attributes.
        if sp1 in ['Sb', 'Lt']:
            if moist_reg == 'm' or moist_reg == 'd':
                warnList.append('%s 3.3.1 A mesic or dry moisture Regime: %s found in conjunction with a %s species 1' % (layer, moist_reg, sp1))

        if sp1 in ['Pl', 'Pj', 'Pa', 'Pf', 'P', 'A', 'Aw']:
            if moist_reg == 'w':
                warnList.append('%s 3.3.2 A wet moisture Regime: %s found in conjunction with a %s species 1' % (layer, moist_reg, sp1))

        if sp1 in ['Sw', 'Se', 'Pl', 'Pj', 'P', 'A', 'Aw', 'Pb', 'Bw', 'Fd']:
            if tpr == 'U':
                warnList.append('%s 3.4.1 TPR: %s found in conjunction with a %s species 1' % (layer, tpr, sp1))

        if moist_reg == 'a':
            errList.append('%s 3.3.3 An aquatic moisture regime of "a" cannot be associaed with a Forested layer' % layer)
            
        # Add all validation messages to our audit log.            
        auditLogObj.addAuditRecords(aviRecord['oid'], errList, 'ERROR')
        auditLogObj.addAuditRecords(aviRecord['oid'], warnList, 'WARNING')

        self.totalErrorCount += len(errList)
        self.totalWarningCount += len(warnList)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def validateNonForestVegLayer(self, aviRecord, auditLogObj, layer):
        '''
        Validates a Non-forest Vegetated (nfl) layer.
        
        Parameters:
            aviRecord - either an understorey or overstorey record that has
                        been extracted to a common structure where SP1
                        can mean either SP1 or USP1. The layer parameter
                        determines which layer it came from.

            auditLogObj - current audit log where messages will be written.

            layer - either OVERSTOREY or UNDERSTOREY

        '''

        height = aviRecord['height']
        nfl = aviRecord['nfl']
        nfl_per = aviRecord['nfl_per']
        anth_veg = aviRecord['anth_veg']
        data = aviRecord['data']

        # List of required fields for Forested layer.

        requiredFieldList = ['nfl',
                            'moist_reg',
                            'tpr'
                            ]

        excludeFieldList = ['sp1',
                            'sp1_per',
                            'sp2',
                            'sp2_per',
                            'sp3',
                            'sp3_per',
                            'sp4',
                            'sp4_per',
                            'sp5',
                            'sp5_per',
                            'density',
                            'origin', 
                            'nat_non',
                            'anth_non'
                             ]
        
        errList = []
        warnList = []

        # Verify existance of required fields.
        for fieldName in requiredFieldList:
            val = aviRecord[fieldName]
            if not val:
                errList.append('%s 4.1 Non-forest Vegetated layer required field: %s missing' % (layer, fieldName))

        # Verify absence of excluded fields.
        for fieldName in excludeFieldList:
            val = aviRecord[fieldName]
            if val:
                errList.append('%s 4.2 Field: %s cannot be present in Non-forest Vegetated layer' % (layer, fieldName))

        if nfl in ['SC', 'SO']:
           if height:
                if height < 1 or height > 10:
                    errList.append('%s 4.3.2 Non-forest Vegetated Height for SO or SC: %s invalid, must be between 1 - 10' % (layer, height))
           else:
               errList.append('%s 4.3.1 Non-forest Vegetated Height for SO or SC is missing' % layer)

           if nfl_per:
                if nfl_per < 1 or nfl_per > 10:
                    errList.append('%s 4.4.2 Non-forest Vegetated Percent: %s invalid must be between 1 - 10' % (layer, nfl_per))
           else:
               errList.append('%s 4.4.1 Non-forest Vegetated Percent is missing, required when SO or SC' % layer)
                
        else:
            if nfl_per > 0:
                errList.append('%s 4.4.3 Non-forest Vegetated Percent can only be associated with SC or SO. (NFL: %s NFL Percent: %s)' % (layer, nfl, nfl_per))

            if height > 0:
                errList.append('%s 4.3.3 Height cannot exist with Non-forest Vegetated types other than SC or SO. (NFL: %s Height: %s)' % (layer, nfl, height))
                
        if anth_veg:
            if anth_veg != 'CPR':
                errList.append('%s 4.5.1 Non-forest Vegetated can only be associated with an Anthropogenic Vegetated of CPR, was found with: %s' % (layer, anth_veg))

        if data == 'I':
            errList.append('%s 4.6.1 Non-forest Vegetated cannot have a Data Source code of I' % layer)
                
        # Add all validation messages to our audit log.            
        auditLogObj.addAuditRecords(aviRecord['oid'], errList, 'ERROR')
        auditLogObj.addAuditRecords(aviRecord['oid'], warnList, 'WARNING')

        self.totalErrorCount += len(errList)
        self.totalWarningCount += len(warnList)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def validateNatNonVegLayer(self, aviRecord, auditLogObj, layer):
        '''

        Validates a Natually Non-vegetated (nat_non) layer.
        
        Parameters:
            aviRecord - either an understorey or overstorey record that has
                        been extracted to a common structure where SP1
                        can mean either SP1 or USP1. The layer parameter
                        determines which layer it came from.

            auditLogObj - current audit log where messages will be written.

            layer - either OVERSTOREY or UNDERSTOREY

        '''

        moist_reg = aviRecord['moist_reg']
        tpr = aviRecord['tpr']
        nat_non = aviRecord['nat_non']
        mod1 = aviRecord['mod1']
        mod2 = aviRecord['mod2']
        data = aviRecord['data']

        # List of required fields for Forested layer.

        requiredFieldList = ['nat_non']

        excludeFieldList = ['sp1',
                            'sp1_per',
                            'sp2',
                            'sp2_per',
                            'sp3',
                            'sp3_per',
                            'sp4',
                            'sp4_per',
                            'sp5',
                            'sp5_per',
                            'density',
                            'height',
                            'origin',
                            'nfl',
                            'anth_veg',
                            'anth_non'
                             ]
        
        errList = []
        warnList = []

        # Verify existance of required fields.
        for fieldName in requiredFieldList:
            val = aviRecord[fieldName]
            if not val:
                errList.append('%s 5.1 Naturally Non-vegetated layer required field: %s missing' % (layer, fieldName))

        # Verify absence of excluded fields.
        for fieldName in excludeFieldList:
            val = aviRecord[fieldName]
            if val:
                errList.append('%s 5.2 Field: %s cannot be present in Naturally Non-vegetated layer' % (layer, fieldName))

        if nat_non == 'NMB':
##            if not tpr:
##                errList.append('%s TPR required when Naturally Non-vegetated layer is NMB' % layer)
            if not moist_reg:
                errList.append('%s 5.3.1 Moisture Regime required when Naturally Non-vegetated layer is NMB' % layer)
        else:
##            if tpr:
##                errList.append('%s TPR cannot be present in Naturally Non-vegetated layer unless NMB. (TPR: %s)' % (layer, tpr))
            if moist_reg:
                errList.append('%s 5.3.2 Moisture Regime cannot be present in Naturally Non-vegetated layer unless NMB. (moisture regime: %s)' % (layer, moist_reg))

        if mod1:
            if nat_non in ['NMC', 'NMS', 'NWI', 'NMR']:
                errList.append('%s 5.4.1 Modifier cannot be present in Naturally Non-vegetated layer type NMC, NMS, NWI or NMR. (modifier 1: %s)' % (layer, mod1))
            
            if mod1 == 'SN':
                if not nat_non in ['NWL', 'NWR', 'NWF']:
                    errList.append('%s 5.4.2 Modifier restricted to SN in Naturally Non-vegetated layer of NWL, NWR or NWF. (modifier 1: %s)' % (layer, mod1))

        if mod2:
            if nat_non in ['NMC', 'NMS', 'NWI', 'NMR']:
                errList.append('%s 5.4.1 Modifier cannot be present in Naturally Non-vegetated layer type NMC, NMS, NWI or NMR. (modifier 2: %s)' % (layer, mod2))
            if nat_non in ['NWL', 'NWR', 'NWF']:
                if mod2 != 'SN':
                    errList.append('%s 5.4.2 Modifier restricted to SN in Naturally Non-vegetated layer of NWL, NWR or NWF. (modifier 2: %s)' % (layer, mod2))
                    
        # Add all validation messages to our audit log.            
        auditLogObj.addAuditRecords(aviRecord['oid'], errList, 'ERROR')
        auditLogObj.addAuditRecords(aviRecord['oid'], warnList, 'WARNING')

        self.totalErrorCount += len(errList)
        self.totalWarningCount += len(warnList)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def validateAnthVegLayer(self, aviRecord, auditLogObj, layer):
        '''

        Validates an Anthropogenic Vegetated (anth_veg) layer.
        
        Parameters:
            aviRecord - either an understorey or overstorey record that has
                        been extracted to a common structure where SP1
                        can mean either SP1 or USP1. The layer parameter
                        determines which layer it came from.

            auditLogObj - current audit log where messages will be written.

            layer - either OVERSTOREY or UNDERSTOREY

        '''

        tpr = aviRecord['tpr']
        nfl = aviRecord['nfl']
        anth_veg = aviRecord['anth_veg']
        mod1 = aviRecord['mod1']
        mod2 = aviRecord['mod2']
        data = aviRecord['data']

        # List of required fields for Forested layer.

        requiredFieldList = ['anth_veg',
                             'moist_reg']

        if anth_veg == 'CPR':
            excludeFieldList = ['sp1',
                                'sp1_per',
                                'sp2',
                                'sp2_per',
                                'sp3',
                                'sp3_per',
                                'sp4',
                                'sp4_per',
                                'sp5',
                                'sp5_per',
                                'density',
                                'origin', 
                                'nat_non',
                                'anth_non'
                                 ]
        else:
            excludeFieldList = ['sp1',
                                'sp1_per',
                                'sp2',
                                'sp2_per',
                                'sp3',
                                'sp3_per',
                                'sp4',
                                'sp4_per',
                                'sp5',
                                'sp5_per',
                                'density',
                                'height',
                                'origin', 
                                'tpr', 
                                'nat_non',
                                'anth_non'
                                 ]
        
        errList = []
        warnList = []

        # Verify existance of required fields.
        for fieldName in requiredFieldList:
            val = aviRecord[fieldName]
            if not val:
                errList.append('%s 6.1 Anthropogenic Vegetated layer required field: %s missing value' % (layer, fieldName))

        # Verify absence of excluded fields.
        for fieldName in excludeFieldList:
            val = aviRecord[fieldName]
            if val:
                errList.append('%s 6.2 Field: %s cannot be present in Anthropogenic Vegetated layer' % (layer, fieldName))

        if anth_veg == 'CPR':
            if not nfl in ['SO', 'SC']:
                errList.append('%s 6.3.1 When Anthropogenic Vegetated is CPR then must have an Non-forest Vegetated call of SO or SC. (nfl: %s)' % (layer, nfl))
##            if not tpr:
##                errList.append('%s When Anthropogenic Vegetated is CPR then must have a TPR call' % layer)
                
        if data == 'I':
            errList.append('%s 6.5.1 Anthropogenic Vegetated cannot have a Data Source code of I' % layer)

        if mod1:
            if anth_veg in ['CIP', 'CIW']:
                if not mod1 in ['SN', 'ST', 'IR', 'GR', 'CC', 'BU', '']:
                    errList.append('%s 6.4.1 Invalid Modifier 1: %s When Anthropogenic Vegetated is CIP or CIW then Modifier can only be SN, ST, IR, GR, CC, CL or BU' % (layer, mod1))
            else:
                if not mod1 in ['SN', 'ST', 'IR', 'GR', 'CC', '']:
                    errList.append('%s 6.4.2 Invalid Modifier 1: %s When Anthropogenic Vegetated is not CIP or CIW then Modifier can only be SN, ST, IR, GR, CC or CL' % (layer, mod1))
                
        if mod2:
            if anth_veg in ['CIP', 'CIW']:
                if not mod2 in ['SN', 'ST', 'IR', 'GR', 'CC', 'BU', '']:
                    errList.append('%s 6.4.1 Invalid Modifier 2: %s When Anthropogenic Vegetated is CIP or CIW then Modifier can only be SN, ST, IR, GR, CC, CL or BU' % (layer, mod2))
            else:
                if not mod2 in ['SN', 'ST', 'IR', 'GR', 'CC', '']:
                    errList.append('%s 6.4.2 Invalid Modifier 2: %s When Anthropogenic Vegetated Modifier can only be SN, ST, IR, GR, CC or CL' % (layer, mod2))
                    
        # Add all validation messages to our audit log.            
        auditLogObj.addAuditRecords(aviRecord['oid'], errList, 'ERROR')
        auditLogObj.addAuditRecords(aviRecord['oid'], warnList, 'WARNING')

        self.totalErrorCount += len(errList)
        self.totalWarningCount += len(warnList)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def validateAnthNonVegLayer(self, aviRecord, auditLogObj, layer):
        '''

        Validates an Anthropogenic Non-vegetated (anth_non) layer.
        
        Parameters:
            aviRecord - either an understorey or overstorey record that has
                        been extracted to a common structure where SP1
                        can mean either SP1 or USP1. The layer parameter
                        determines which layer it came from.

            auditLogObj - current audit log where messages will be written.

            layer - either OVERSTOREY or UNDERSTOREY

        '''

        data = aviRecord['data']

        # List of required fields for Forested layer.

        requiredFieldList = ['anth_non']

        excludeFieldList = ['sp1',
                            'sp1_per',
                            'sp2',
                            'sp2_per',
                            'sp3',
                            'sp3_per',
                            'sp4',
                            'sp4_per',
                            'sp5',
                            'sp5_per',
                            'density',
                            'height',
                            'moist_reg',
                            'origin', 
                            'tpr', 
                            'nat_non',
                            'nfl',
                            'anth_veg',
                            'mod1',
                            'mod1_ext',
                            'mod1_yr',
                            'mod2',
                            'mod2_ext',
                            'mod2_yr'                            
                             ]
        
        errList = []
        warnList = []

        # Verify existance of required fields.
        for fieldName in requiredFieldList:
            val = aviRecord[fieldName]
            if not val:
                errList.append('%s 7.1 Anthropogenic Non-vegetated layer required field: %s missing' % (layer, fieldName))

        # Verify absence of excluded fields.
        for fieldName in excludeFieldList:
            val = aviRecord[fieldName]
            if val:
                errList.append('%s 7.2 Field: %s cannot be present in Anthropogenic Non-vegetated layer' % (layer, fieldName))
                    
        # Add all validation messages to our audit log.            
        auditLogObj.addAuditRecords(aviRecord['oid'], errList, 'ERROR')
        auditLogObj.addAuditRecords(aviRecord['oid'], warnList, 'WARNING')

        self.totalErrorCount += len(errList)
        self.totalWarningCount += len(warnList)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def containsLayerData(self, aviRecObj, layer):
        '''
        Will return if any of the fields in the requested layer
        contains data values

        Parameters:
            aviRecObj - full AVI record object used to store record information.

            layer - the layer to check, either OVER or UNDER
        '''

        dataExists = False
        existingFields = []
        
        # If a field contains a non-blank or non-zero value then
        # it is assumed to contain data.
        if layer == 'OVER':
            for fieldName in aviRecObj.overstoryFieldNames:
                if aviRecObj.fieldValues[fieldName]:
                    dataExists = True
                    existingFields.append(fieldName)
        elif layer == 'UNDER':
            for fieldName in aviRecObj.understoryFieldNames:
                if aviRecObj.fieldValues[fieldName]:
                    existingFields.append(fieldName)
                    dataExists = True
        else:
            raise SRD_Exception('Invalid layer type passed to containsLayerData')

        return dataExists

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def getFieldsContainingData(self, aviRecObj, layer):
        '''
        Will return field names that are populated in the requested layer
        contains data values

        Parameters:
            aviRecObj - full AVI record object used to store record information.

            layer - the layer to check, either OVER or UNDER
        '''

        existingFields = []
        
        # If a field contains a non-blank or non-zero value then
        # it is assumed to contain data.
        if layer == 'OVER':
            for fieldName in aviRecObj.overstoryFieldNames:
                if aviRecObj.fieldValues[fieldName]:
                    existingFields.append(fieldName)
        elif layer == 'UNDER':
            for fieldName in aviRecObj.understoryFieldNames:
                if aviRecObj.fieldValues[fieldName]:
                    existingFields.append(fieldName)
        else:
            raise SRD_Exception('Invalid layer type passed to containsLayerData')

        return existingFields
    
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def validateStructure(self, aviRecObj, auditLogObj):
        '''
        Validates any structure relationships and dependencies.

        Parameters:
            aviRecObj - full AVI record object used to store record information.

            auditLogObj - current audit log where messages will be written.

        '''

        aviRecord = aviRecObj.fieldValues
        oStructure = aviRecord['struc']
        oStructureValue = aviRecord['struc_val']
        oDensity = aviRecord['density']
        oNfl = aviRecord['nfl']
        oNfl_per = aviRecord['nfl_per']
        
        uStructure = aviRecord['ustruc']
        uStructureValue = aviRecord['ustruc_val']

        sp1 = aviRecord['sp1']
        sp1_per = aviRecord['sp1_per']
        sp2 = aviRecord['sp2']
        sp2_per = aviRecord['sp2_per']

        errList = []
        warnList = []

        if oDensity:
            if oDensity.upper() in ('A', 'B'):
                if not self.containsLayerData(aviRecObj, 'UNDER'):
                    errList.append('%s 8.1.1 No understorey; when Density is either A or B then likely to have an understorey' % 'UNDERSTOREY')

        if oNfl:
            if oNfl == 'SO' and oNfl_per <= 5:
                if not self.containsLayerData(aviRecObj, 'UNDER'):
                    warnList.append('%s 8.1.2 No understorey; when NFL is SO and NFL_PER is less than or equal to 5 then likely to have an understorey' % 'UNDERSTOREY')
                
        # Validate domains.
        if oStructure:
            if not oStructure in self.domainStandStructureList:
                errList.append('%s 2.7 Stand structure: %s invalid, must be one of: %s' % ('OVERSTOREY', oStructure, self.validStandStructure))
                
        if uStructure:
            if not uStructure in self.domainStandStructureList:
                errList.append('%s 2.7 Stand structure: %s invalid, must be one of: %s' % ('UNDERSTOREY', uStructure, self.validStandStructure))

        # Multi-storied stand.
        if oStructure == 'M':
            if not self.containsLayerData(aviRecObj, 'UNDER'):
                errList.append('%s 8.2.1 Stand structure M but no data was found in understorey' % 'UNDERSTOREY')
            if oStructureValue:
                errList.append('%s 8.2.2 Invalid stand structure value: %s, when stand structure is M cannot have stand structure value' % ('OVERSTOREY', oStructureValue))
            if uStructureValue:
                errList.append('%s 8.2.2 Invalid stand structure value: %s, when stand structure is M cannot have stand structure value' % ('UNDERSTOREY', uStructureValue))
            if uStructure:
                if uStructure == 'M':
                    warnList.append('%s 8.2.3 Understorey stand structure value M not required when overstorey stand structure is M' % 'UNDERSTOREY')
                else:
                    errList.append('%s 8.2.4 Invalid stand structure value: %s, when overstorey stand structure is M understorey stand structure should be blank' % ('UNDERSTOREY', uStructure))
                
        # Horizontal stand structure checks.
        if oStructure == 'H':
            if uStructure != 'H':
                errList.append('%s 8.3.1 Stand structure: %s invalid, when overstorey stand structure is H then the understorey stand structure must also be H' % ('UNDERSTOREY', uStructure))
            if oStructureValue:
                if oStructureValue < 1 or oStructureValue > 9:
                    errList.append('%s 8.3.2 Stand structure value: %s invalid, must be between 1 and 9 when a horizontal stand' % ('OVERSTOREY', uStructureValue))

        if uStructure == 'H':
            if oStructure != 'H':
                errList.append('%s 8.3.1 Stand structure: %s invalid, when understorey stand structure is H then the overstorey stand structure must also be H' % ('OVERSTOREY', uStructure))
            if uStructureValue:
                if uStructureValue < 1 or uStructureValue > 9:
                    errList.append('%s 8.3.2 Stand structure value: %s invalid, must be between 1 and 9 when a horizontal stand' % ('UNDERSTOREY', uStructureValue))

        if oStructure == 'H' or uStructure == 'H':
            totalVal = oStructureValue + uStructureValue
            if totalVal != 10:
                errList.append('%s 8.3.3 When stand structure is H the stand structure total value must be 10 but is: %s for this stand' % ('OVERSTOREY', totalVal))

        if uStructure == 'C':
            errList.append('UNDERSTOREY 8.4.1 Complex structure: C only allowed in overstorey')

        # List of species that can be associated with a complex.
        complexSpeciesList = ['Sb', 'Lt', 'Fb', 'Sw']            
        if oStructure == 'C':
            if not sp1:
                errList.append('OVERSTOREY 8.4.2 Complex structure only allowed for forested layers')
                
            if sp1:
                if not sp1 in complexSpeciesList:
                    errList.append('%s 8.4.3 Species 1: %s invalid, when the stand structure is C, leading tree species must be either Sb, Lt, Fb, or Sw' % ('OVERSTOREY', sp1))

                if sp1 in complexSpeciesList:
                    if sp1_per < 8:
                        if sp2 in complexSpeciesList:
                            totPct = sp1_per + sp2_per
                            if totPct < 8:
                                errList.append('%s 8.4.4 When the stand structure is C, the leading tree species (1 and 2) must be either Sb, Lt, Fb, or Sw and have a minimum stand percentage of 8: sp1/sp2_per is only %s for this stand' % ('OVERSTOREY', totPct))
                        else:
                            errList.append('%s 8.4.4 When the stand structure is C, the leading tree species (1 and 2) must be either Sb, Lt, Fb, or Sw and have a minimum stand percentage of 8: sp1_per is only %s for this stand' % ('OVERSTOREY', sp1_per))

##            if sp2:
##                if not sp2 in complexSpeciesList:
##                    errList.append('%s Species 2: %s invalid, when the stand structure is C, must be either Sb, Lt, Fb, or Sw' % ('OVERSTOREY', sp2))
##
##            if sp3:
##                if not sp3 in complexSpeciesList:
##                    errList.append('%s Species 3: %s invalid, when the stand structure is C, must be either Sb, Lt, Fb, or Sw' % ('OVERSTOREY', sp3))
##
##            if sp4:
##                if not sp4 in complexSpeciesList:
##                    errList.append('%s Species 4: %s invalid, when the stand structure is C, must be either Sb, Lt, Fb, or Sw' % ('OVERSTOREY', sp4))
##
##            if sp5:
##                if not sp5 in complexSpeciesList:
##                    errList.append('%s Species 5: %s invalid, when the stand structure is C, must be either Sb, Lt, Fb, or Sw' % ('OVERSTOREY', sp5))

            if self.containsLayerData(aviRecObj, 'UNDER'):
                errList.append('%s 8.4.1 When the stand structure is C no understorey attributes are allowed' % 'UNDERSTOREY')

            if not oStructureValue:
                errList.append('%s 8.4.5 When the stand structure is C must have a height range in STRUC_VAL field' % 'OVERSTOREY')
            else:
                if oStructureValue < 1 or oStructureValue > 10:
                    errList.append('%s 8.4.6 Complex height difference: %s invalid, must be between 1 and 10' % ('OVERSTOREY', oStructureValue))

        # Add all validation messages to our audit log.            
        auditLogObj.addAuditRecords(aviRecord['oid'], errList, 'ERROR')
        auditLogObj.addAuditRecords(aviRecord['oid'], warnList, 'WARNING')

        self.totalErrorCount += len(errList)
        self.totalWarningCount += len(warnList)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def validateLayerRelationships(self, aviRecObj, auditLogObj):
        '''
        Validates any inter-layer relationships.

        Parameters:
            aviRecObj - full AVI record object used to store record information.

            auditLogObj - current audit log where messages will be written.

        '''

        aviRecord = aviRecObj.fieldValues

        moist_reg = aviRecord['moist_reg']
        density = aviRecord['density']
        height = aviRecord['height']
        sp1 = aviRecord['sp1']
        sp1_per = aviRecord['sp1_per']
        struc = aviRecord['struc']
        origin = aviRecord['origin']
        tpr = aviRecord['tpr']
        nfl = aviRecord['nfl']
        nat_non = aviRecord['nat_non']
        anth_veg = aviRecord['anth_veg']
        anth_non = aviRecord['anth_non']
        data = aviRecord['data']
        data_yr = aviRecord['data_yr']
        mod1 = aviRecord['mod1']
        mod2 = aviRecord['mod2']
        
        umoist_reg = aviRecord['umoist_reg']
        udensity = aviRecord['udensity']
        uheight = aviRecord['uheight']
        usp1 = aviRecord['usp1']
        usp1_per = aviRecord['usp1_per']
        uorigin = aviRecord['uorigin']
        utpr = aviRecord['utpr']
        unfl = aviRecord['unfl']
        unat_non = aviRecord['unat_non']
        uanth_veg = aviRecord['uanth_veg']
        uanth_non = aviRecord['uanth_non']
        udata = aviRecord['udata']
        udata_yr = aviRecord['udata_yr']
        umod1 = aviRecord['umod1']
        umod2 = aviRecord['umod2']

        errList = []
        warnList = []
        msg = ''

##        if not struc:
##            if nfl:
##                if not data:
##                    msg = 'When non-forest vegetated then data source must be I (O nfl: %s)' % nfl
##                    errList.append(msg)

        if self.containsLayerData(aviRecObj, 'UNDER'):
            if struc != 'M' and struc != 'H':
                # Obtain list of offending fields so we can display them.
                uFieldList = self.getFieldsContainingData(aviRecObj, 'UNDER')
                fieldNames = ''
                for fieldName in uFieldList:
                    fieldNames += ' ' + fieldName
                msg = '12.1.1 Understorey attributes found but overstorey stand structure is neither M nor H (Fields with data: %s)' % fieldNames
                errList.append(msg)
        else:
            if struc in ['M', 'H']:
                msg = '12.1.2 When the Stand Structure is M or H then must have understorey attributes, none were found. (Stand Structure: %s)' % struc
                errList.append(msg)
            

        # If moisture regime is "a" then understorey must contain NWL or NWF.
        # Modified Sep 2021 to match specifications
        if moist_reg == 'a' and nfl == 'HF':
            if struc == 'M':
                msg = '12.1.3 When the overstorey contains HF and the moisture regime is aquatic then there cannot be an understorey (Stand Structure: %s' % struc
                errList.append(msg)

        # Check for any duplicated stands in overstorey and understorey.
        # Does not matter if H structure.
        if nfl:
            if nfl in ['SO', 'SC']:
                if nfl == unfl:
                    msg = '12.1.4 Overstorey and understorey have the same non-forest vegetated call of: %s' % nfl
                    warnList.append(msg)
            else:
                if nfl == unfl:
                    msg = '12.1.5 Overstorey and understorey have the same non-forest vegetated call of: %s' % nfl
                    errList.append(msg)

        if nat_non:
            if nat_non == unat_non:
                msg = '12.1.6 Overstorey and understorey have the same naturally non-vegetated call of: %s' % nat_non
                errList.append(msg)

        if anth_veg:
            if anth_veg == uanth_veg:
                msg = '12.1.7 Overstorey and understorey have the same anthropogenic vegetated call of: %s' % anth_veg
                errList.append(msg)

        if anth_non:
            if anth_non == uanth_non:
                msg = '12.1.8 Overstorey and understorey have the same anthropogenic non-vegetated call of: %s' % anth_non
                errList.append(msg)

        # Rules where stand is multi-storied.
        if struc == 'M':

            # Changed for July 2019 update
            if umod1 in ['CC', 'BU'] or umod2 in  ['CC', 'BU']:
                msg = '12.1.9 CC or BU modifier should not be contained in understorey'
                errList.append(msg)
                                   
            if moist_reg and umoist_reg:
                if usp1 or uanth_veg or unfl:
                    if moist_reg != umoist_reg:
                        msg = '12.1.10 When stand structure is M then overstorey and understorey moisture regime values should be same. (O moist reg: %s U moist reg: %s)' % (moist_reg, umoist_reg)
                        errList.append(msg)

            if unfl in ['SO', 'SC']:
                if anth_veg:
                    msg = '12.1.11 When SO or SC is contained in understorey then you cannot have an anthropogenic vegetated call in the overstorey. (O anth veg: %s U nfl: %s)' % (anth_veg, unfl)
                    errList.append(msg)
                if anth_non:
                    msg = '12.1.12 When SO or SC is contained in understorey then you cannot have an anthropogenic non-vegetated call in the overstorey. (O anth non: %s U nfl: %s)' % (anth_non, unfl)
                    errList.append(msg)

            if origin and uorigin:
                if uorigin < origin:
                    if data != 'F' and udata != 'F':
                        msg = '12.1.13 Understorey origin cannot be older than overstorey origin unless stand is field checked. (O origin: %s U origin: %s)' % (origin, uorigin)
                        errList.append(msg)

            if uanth_veg:
                msg = '12.1.14 Cannot have an anthropogenic vegetated understorey: %s)' % uanth_veg
                errList.append(msg)

            if sp1 and uanth_non:
                if not uanth_non in ['ASR', 'ASC', 'AIF', '']:
                    msg = '12.1.15 Cannot have an anthropogenic non-vegetated understorey (other than ASR, ASC, or AIF) unless overstorey is treed. (U anth non-veg: %s)' % uanth_non
                    errList.append(msg)

            if sp1 and unat_non:
                msg = '12.1.16 A Naturally Non-vegetated understorey is not allowed in a multistoried stand when the overstorey is treed. (O sp1: %s U nat non-veg: %s)' % (sp1, unat_non)
                errList.append(msg)

            if not nfl:
                if unat_non not in ['NMS', 'NWF', 'NWL', 'NMC', '']:
                    msg = '12.1.17 You can only have a Naturally Non-vegetated understorey of NMS, NWF, NWL or NMC when overstorey is not Non-forest Vegetated. (O nfl: %s U nat non: %s)' % (nfl, unat_non)
                    errList.append(msg)

            if nfl:
                if usp1 and not nfl in ['SC', 'SO', ''] and not data in ['F', 'A']:
                    msg = '12.1.18 When non-forest vegetated overstorey is not SO or SC then you cannot have a forested understorey unless data source is A, F. (O nfl: %s U sp1: %s)' % (nfl, usp1)
                    errList.append(msg)

                if not uanth_veg in ['CIP', 'CIW', ''] and nfl in ['SC', 'SO'] and not data in ['F', 'A', 'S']:
                    msg = '12.1.19 When overstorey is SO or SC then you cannot have a anthropogenic vegetated understorey other than CIP or CIW unless data source is A, F, or S. (O nfl: %s U anth veg: %s)' % (nfl, uanth_veg)
                    errList.append(msg)

                if unat_non:
                    if unat_non == 'NMS':
                        if not data in ['F', 'A', '']:
                            msg = '12.1.20 When overstorey is SO or SC then you cannot have a naturally non-vegetated understorey of NMS unless data source is A or F. (O nfl: %s U nat non-veg: %s)' % (nfl, unat_non)
                            errList.append(msg)
                    else:
                        if unat_non in ['NWL', 'NWF']:
                            msg = '12.1.21 When overstorey is SO or SC then you cannot have a naturally non-vegetated understorey of NWL or NWF. (O nfl: %s U nat non-veg: %s)' % (nfl, unat_non)
                            errList.append(msg)
                        
            if nfl in ['HG', 'HF', 'BR']:
                if unfl:
                    msg = '12.1.22 When overstorey is HG, HF or BR then you cannot have a non-forest vegetated understorey. (O nfl: %s U nfl: %s)' % (nfl, unfl)
                    errList.append(msg)

            if nfl == 'HF':
                if unat_non in ['NWI', 'NWR']:
                    msg = '12.1.23 When overstorey is HF then you cannot have a naturally non-vegetated understorey of NWI or NWR. (O nfl: %s U nat non-veg: %s)' % (nfl, unat_non)
                    errList.append(msg)
                    
                if not data in ['F', 'A']:
                    if unat_non in ['NMR', 'NMS', 'NMC']:
                        msg = '12.1.24 When overstorey is HF then you cannot have a naturally non-vegetated understorey of NMR, NMS. or NMC unless data source is A or F. (O nfl: %s U nat non-veg: %s)' % (nfl, unat_non)
                        errList.append(msg)

                if unat_non in ['NWL', 'NWF'] and moist_reg != 'a':
                    msg = '12.1.25 When overstorey is HF and you have a naturally non-vegetated understorey of NWL or NWF then the overstorey moisture regime must be aquatic. (O moist reg: %s)' % moist_reg
                    errList.append(msg)

            if nfl in ['HG', 'BR'] and unat_non:                    
                if unat_non in ['NMR', 'NMS', 'NMC']:
                    if not data in ['F', 'A']:
                        msg = '12.1.26 When overstorey is HG or BR then you cannot have a naturally non-vegetated understorey of NMR, NMS. or NMC unless data source is A or F. (O nfl: %s U nat non-veg: %s)' % (nfl, unat_non)
                        errList.append(msg)
                else:
                    msg = '12.1.27 When overstorey is HG or BR then you cannot have a naturally non-vegetated understorey other than NMR, NMS. or NMC (Must Also Be Field Checked)'
                    errList.append(msg)

            if anth_non:
                msg = '12.1.28 When overstorey is Anthropogenic Non-vegetated then no understorey is allowed (O anth non: %s)' % anth_non
                errList.append(msg)

            if anth_veg:
                msg = '12.1.29 When overstorey is Anthropogenic Vegetated then no understorey is allowed (O anth veg: %s)' % anth_veg
                errList.append(msg)

            if nat_non:
                msg = '12.1.30 When overstorey is Naturally Non-vegetated then no understorey is allowed (O nat non: %s)' % nat_non
                errList.append(msg)

            if height and uheight:
                if uheight > height:
                    msg = '12.1.31 Overstorey Height must be greater than understorey Height. (O height: %s U height: %s)' % (height, uheight)
                    errList.append(msg)

                if sp1 and usp1:
                    htDif = abs(height - uheight)
                    if htDif < 3:
                        msg = '12.1.32 Overstorey Height must be at least 3 meters greater than understorey Height when both overstorey and understorey are treed. (O height: %s U height: %s)' % (height, uheight)
                        errList.append(msg)
                else:
                    if height == uheight and not unfl in ['SC', 'SO', '']:
                        msg = '12.1.33 When the overstorey Height is the same as the understorey Height then understory must be SC or SO. (O height: %s U height: %s)' % (height, uheight)
                        errList.append(msg)

            if udata:

                if udata == 'F' and data == 'F' and data_yr != udata_yr:
                    msg = '12.1.34 When both the understorey and overstorey have a Data Source value of F then the Data Source Years must be the same (O: data: %s data yr: %s U data source: %s yr: %s)' % (data, data_yr, udata, udata_yr)
                    errList.append(msg)
                
                if udata == 'A' and not data in ['A', 'I']:
                    msg = '12.1.35 When the understorey has a Data Source value of A then the overstorey Data Source value must be either A or I. (O data source: %s U data source: %s)' % (data, udata)
                    errList.append(msg)

                if udata == 'A' and data == 'A' and data_yr != udata_yr:
                    msg = '12.1.36 When the Data Source of both the understorey and overstorey is A then Data Source Year values must the same. (O data yr: %s U data yr: %s)' % (data_yr, udata_yr)
                    errList.append(msg)

                if udata in ['S', 'F'] and (data != udata or data_yr != udata_yr):
                    msg = '12.1.37 When the understorey has a Data Source value of F or S then the overstorey Data Source must be F and the data years must be the same (O: data source: %s data yr: %s U data: %s yr: %s)' % (data, data_yr, udata, udata_yr)
                    errList.append(msg)

            if data:                    
                if data == 'A' and not udata in ['A', 'S', 'F', 'I', '']:
                    msg = '12.1.38 When overstorey Data Source is A then understorey Data Source can only be A, S, F, or I. (O data: %s U data: %s)' % (data, udata)
                    errList.append(msg)

                if data in ['S', 'F']:
                    if udata:
                        if (data != udata or data_yr != udata_yr):
                            msg = '12.1.39 When the overstorey has a Data Source value of F or S then the understorey Data Source must be same and the Data Years must be the same (O: data: %s data yr: %s U data: %s yr: %s)' % (data, data_yr, udata, udata_yr)
                            errList.append(msg)
                    
        # ------------------------------------
        # End of struc = M
        
        if struc == 'H':
            if not sp1 and usp1:
                msg = '12.1.40 When a horizontal structure and only one of the horizontal components has forested data then that forested data must be found in the first horizontal component.'
                errList.append(msg)

            if not sp1 and not usp1:
                if uheight > height:
                    msg = '12.1.41 With a horizontal structure the tallest component must be first even if its horizontal percentage is less than the other component. (O height: %s Y height: %s)' % (height, uheight)
                    errList.append(msg)
            
            if sp1:
                if sp1 == usp1 and height == uheight and sp1_per == usp1_per:
                    msg = '12.1.42 With a horizontal structure, species 1 cannot be duplicated in the two components when their species percentages and heights are the same.'

                if usp1 and uheight > height:
                    msg = '12.1.44 When both components of a horizontal stand are treed then the component with the greater height must be in the first component. (O height: %s Y height: %s)' % (height, uheight)
                    errList.append(msg)
                    errList.append(msg)
                    
            if nfl:
                if nfl == unfl:
                    msg = '12.1.43 Non-forest vegetated cannot be duplicated if a horizontal structure. (O nfl: %s U nfl: %s)' % (nfl, unfl)
                    errList.append(msg)

##                if not usp1:
##                    if not data:
##                        msg = 'When non-forest vegetated overstorey and no forested understorey then data source must be I (O nfl: %s)' % nfl
##                        errList.append(msg)
##            if unfl:
##                if not sp1:
##                    if not udata:
##                        msg = 'When non-forested understorey and no forested overstorey then data source must be I (U nfl: %s)' % unfl
##                        errList.append(msg)

        # ------------------------------------
        # End of struc = H

        if density == 'D':
            if udensity:
                msg = '12.1.45 When the overstorey crown closure is D then it is unlikely that there would be an understorey. (O denstiy: %s U density: %s)' % (density, udensity)
                warnList.append(msg)

        if udata:
            msg = '12.1.46 Understorey data source questionable. (U data: %s)' % udata
            warnList.append(msg)
    
        if umod1:
            msg = '12.1.47 Understorey modifier 1 questionable. (U mod1: %s)' % umod1
            warnList.append(msg)

        if umod2:
            msg = '12.1.47 Understorey modifier 2 questionable. (U mod1: %s)' % umod2
            warnList.append(msg)
            
        # Add all validation messages to our audit log.            
        auditLogObj.addAuditRecords(aviRecord['oid'], errList, 'ERROR')
        auditLogObj.addAuditRecords(aviRecord['oid'], warnList, 'WARNING')

        self.totalErrorCount += len(errList)
        self.totalWarningCount += len(warnList)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def validateDomains(self, aviRecord, auditLogObj, layer):
        '''
        Validate any populated domains.

        Parameters:
            aviRecord - either an understorey or overstorey record that has
                        been extracted to a common structure where SP1
                        can mean either SP1 or USP1. The layer parameter
                        determines which layer it came from.

            auditLogObj - current audit log where messages will be written.

            layer - either OVERSTOREY or UNDERSTOREY
        '''

        moist_reg = aviRecord['moist_reg']
        density = aviRecord['density']
        height = aviRecord['height']
        struc = aviRecord['struc']
        origin = aviRecord['origin']
        tpr = aviRecord['tpr']
        nfl = aviRecord['nfl']
        nfl_per = aviRecord['nfl_per']
        nat_non = aviRecord['nat_non']
        anth_veg = aviRecord['anth_veg']
        anth_non = aviRecord['anth_non']

        errList = []
        warnList = []
        msg = ''

        curYr = datetime.date.today().year

        if moist_reg:
            if not moist_reg in self.domainMoistureRegimeList:
                errList.append('%s 2.2 Moisture Regime: %s invalid, must be one of: %s' % (layer, moist_reg, self.validMoistureRegime))
            
        if density:
            if not density in self.domainCrownClosureList:
                errList.append('%s 2.3 Density/Crown Closure: %s invalid, must be one of: %s' % (layer, density, self.validCrownClosure))

        if height:
            if height < 0 or height > 40:
                errList.append('%s 2.4 Height: %s invalid, must be between 1 and 40' % (layer, height))

        self.validateSpecies(aviRecord, auditLogObj, layer)

        if struc:
            if not struc in self.domainStandStructureList:
                errList.append('%s 2.7 Stand Structure: %s invalid, must be one of: %s' % (layer, struc, self.validStandStructure))

        if origin:
            if origin < 1600 or origin > curYr:
                msg = '%s 2.9 Origin: %s invalid, must be between 1600 and %s' % (layer, origin, curYr)
                errList.append(msg)

            if origin < 1700:
                warnList.append('%s Origin less than 1700' % layer)

##        if tpr:
##            if not tpr in self.domainTPRList:
##                errList.append('%s TPR: %s invalid, must be one of: %s' % (layer, tpr, self.validTPR))

        if nfl:
            if not nfl in self.domainNonForestVegList:
                errList.append('%s 2.12 Non-forest Vegetated: %s invalid, must be one of: %s' % (layer, nfl, self.validNonForestVeg))

        if nfl_per:
            if nfl_per < 1 or nfl_per > 10:
                errList.append('%s 4,4.2 Non-forest Vegetated Percent: %s invalid must be between 1 - 10' % (layer, nfl_per))

        if nat_non:
            if not nat_non in self.domainNatNonVegList:
                errList.append('%s 2.13 Naturally Non-vegetated: %s invalid, must be one of: %s' % (layer, nat_non, self.validNatNonVeg))

        if anth_veg:
            if not anth_veg in self.domainAnthVegList:
                errList.append('%s 2.15 Anthropogenic Vegetated: %s invalid, must be one of: %s' % (layer, anth_veg, self.validAnthVeg))

        if anth_non:
            if not anth_non in self.domainAnthNonVegList:
                errList.append('%s 2.16 Anthropogenic Non-vegetated: %s invalid, must be one of: %s' % (layer, anth_non, self.validAnthNonVeg))

        self.validateModifiers(aviRecord, auditLogObj, layer)

        self.validateDataSource(aviRecord, auditLogObj, layer)
        
        # Add all validation messages to our audit log.            
        auditLogObj.addAuditRecords(aviRecord['oid'], errList, 'ERROR')
        auditLogObj.addAuditRecords(aviRecord['oid'], warnList, 'WARNING')

        self.totalErrorCount += len(errList)
        self.totalWarningCount += len(warnList)
            
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def validateLayer(self, layerRecord, auditLogObj, layer):
        '''
        Performs general validation on either overstorey or understorey.

        Parameters
            layerRecord - record for layer to validate. This is a
                          dicitonary where the attributes have been
                          transposed to what would normally be the
                          overstorey. This allows us to call generic routines
                          that use overstorey field names to obtain
                          attribute values.

            auditLogObj - audit log where messages will be written.

            layer - identifies which layer is been validated, either
                    OVERSTOREY or UNDERSTOREY. This will be added to any
                    messages so user can locate correct field with error.
                    
        '''

        errList = []
        warnList = []
        
        # Determine what type of data that we are dealing with.
        # Certain assumptions are made regarding record type such
        # if it contains a SP entry then it is a forested stand.

        if layerRecord['sp1'] or layerRecord['sp2'] or layerRecord['sp3'] or layerRecord['sp4'] or layerRecord['sp5']:
            forested = True
        else:
            forested = False

        if layerRecord['nfl']:
            non_forest_veg = True
        else:
            non_forest_veg = False

        if layerRecord['nat_non']:
            nat_non_veg = True
        else:
            nat_non_veg = False

        if layerRecord['anth_veg']:
            anth_veg = True
        else:
            anth_veg = False

        if layerRecord['anth_non']:
            anth_non = True
        else:
            anth_non = False

        # A record cannot be defined as more than 1 type.
        typeList = [forested, non_forest_veg, nat_non_veg, anth_veg, anth_non]
        typeDesList = ['forested', 'non-forest-veg', 'nat-non-veg', 'anth-veg', 'anth-non-veg']

        for i in range(0, len(typeList) - 1):
            for j in range(i+1, len(typeList) - 1):
                if typeList[i] and typeList[j]:
                    if not ((layerRecord['nfl'] == 'SO' or layerRecord['nfl'] == 'SC') and layerRecord['anth_veg'] == 'CPR'):
                        errList.append('%s 1.2.1 Layer contains core data for both: %s and %s, cannot have data for more than 1 type in same layer' % (layer, typeDesList[i], typeDesList[j]))

        # If record not defined as any type then also an error.
        if not forested and not non_forest_veg and not nat_non_veg and not anth_veg and not anth_non:
            msg = '%s 1.2.2 No core data found for forested, non-forest veg, nat-non-veg, anth-veg, or anth-non-veg, must contain data for at least one of these types' % layer
            errList.append(msg)

        # First step is to validate the domains.
        self.validateDomains(layerRecord, auditLogObj, layer)

        # Interpreter Initials are required for all types
        if layer == 'OVERSTOREY':
            interp_initials = layerRecord['initials']
            if interp_initials:
                interp_initials = interp_initials.strip()
                if len(interp_initials) < 2:
                    errList.append('1.1.2 Interpreter Initials: %s less than 2 characters' % interp_initials)
                elif interp_initials.upper() != interp_initials:
                    errList.append('1.1.3 Interpreter Initials: %s must be uppercase' % interp_initials)
                elif not interp_initials.isalpha():
                    errList.append('1.1.4 Interpreter Initials: %s must only contain letters A-Z' % interp_initials)
            else:
                errList.append('1.1.1 Interpreter Initials missing, must be entered')
                
        # Perform checks for forested stand.
        if forested:
            self.validateForestedLayer(layerRecord, auditLogObj, layer)
        elif non_forest_veg:
            self.validateNonForestVegLayer(layerRecord, auditLogObj, layer)
        elif nat_non_veg:
            self.validateNatNonVegLayer(layerRecord, auditLogObj, layer)
        elif anth_veg:
            self.validateAnthVegLayer(layerRecord, auditLogObj, layer)
        elif anth_non:
            self.validateAnthNonVegLayer(layerRecord, auditLogObj, layer)

        auditLogObj.addAuditRecords(layerRecord['oid'], errList, 'ERROR')
        auditLogObj.addAuditRecords(layerRecord['oid'], warnList, 'WARNING')

        self.totalErrorCount += len(errList)
        self.totalWarningCount += len(warnList)
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def validStructure(self):
        '''
        Validate the basic field structure to see if it can be processed.
        '''
        
        # Make sure the structure of source data is acceptable.
        fcFieldNameList = [field.name.lower() for field in arcpy.ListFields(self.aviFCPath)]
        schemaObj = avi_schema.AVI_Schema()
        fieldTypeDict = schemaObj.fieldTypeDict

        # Verify that all required fields are present.
        reqFieldList = fieldTypeDict.keys()
        msgList = []
        for reqFieldName in reqFieldList:
            if not reqFieldName in fcFieldNameList:
                msgList.append('Required Field: %s missing' % reqFieldName)

        if msgList:
            self._logChn.logError('\n***\n*** Required data missing. cannot audit attributes\n***')
            for msg in msgList:
                self._logChn.logError(msg)
            return False
        
        # Verify that all required fields are defined properly.
        msgList = []
        fcFieldList = arcpy.ListFields(self.aviFCPath)
        for fcField in fcFieldList:
            fcFieldName = fcField.name.lower()
            fcFieldType = fcField.type.lower()
            fcFieldLen = fcField.length
            if fcFieldName in fieldTypeDict:
                typeList = fieldTypeDict[fcFieldName]
                typeOK = False
                reqLen = 0
                for fieldType in typeList:
                    reqLen = fieldType[1]
                    if fieldType[0] == fcFieldType:
                        typeOK = True
                        break;
                if typeOK:
                    if reqLen:
                        if fcFieldLen < reqLen:
                            msgList.append('Field: %s has invalid length, must be: %s is: %s' % (fcFieldName, reqLen, fcFieldLen))
                else:
                    msgList.append('Field: %s does not have valid field type, is defined as: %s' % (fcFieldName, fcFieldType))

        if msgList:
            self._logChn.logError('\n***\n*** Required data missing. cannot audit attributes\n***')
            for msg in msgList:
                self._logChn.logError(msg)
            return False

        return True
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def auditAttributes(self):
        '''
        Performs full audit on all attributes in associated feature class. This is
        the main driver for the attribute audit. Each AVI feature class record
        is read and processed though the validation routines.
        '''


        # Verify field structure.
        if not self.validStructure():
            return False
        
        aviRec = AVI_Record(self.aviFCPath)

        fieldList = aviRec.allFieldNames[:]

        oidFieldName = arcpy.Describe(self.aviFCPath).OIDFieldName
        fieldList.append(oidFieldName)
        
        auditLogObj = srd_audit_log.SRD_AuditLog(self.auditTablePath , self.infoTablePath )
        auditLogObj.deleteAuditTable()
        auditLogObj.deleteInformationTable()

        rsObj = srd_recordset.SRD_Recordset(self.aviFCPath)
        
        selCnt = int(arcpy.GetCount_management(self.aviFCPath).getOutput(0))
        arcpy.SetProgressor('step', 'Validating attributes...', 0, selCnt, 1)

        tprObj = avi_tpr.AVI_TPR()
        
        errCnt = 0
        polyNumDict = {}
        with arcpy.da.SearchCursor(self.aviFCPath, fieldList) as cursor:
            for row in cursor:

                # Get a dictionary or values to enable easy access.
                aviRec.fieldValues = rsObj.getRecord(row, fieldList, replaceNulls=True, stripBlanks=True)
                aviRec.fieldValues['oid'] = aviRec.fieldValues[oidFieldName]
##                aviRec.fieldValues['photo_yr'] = aviRec.fieldValues['photo_yr']
##                aviRec.fieldValues['aris'] = aviRec.fieldValues['aris']
                
                # Determine if it is a multi-storied stand so that all
                # attributes can be verified.
                struc = aviRec.fieldValues['struc']
                if struc:
                    if struc.upper() == 'M' or struc.upper() == 'H':
                        multiStoried = True
                    else:
                        multiStoried = False
                else:
                    multiStoried = False
                                
                # Validate the overstorey record.
                oLayerRec = aviRec.returnLayerRecord('OVER', aviRec.fieldValues)
                self.validateLayer(oLayerRec, auditLogObj, 'OVERSTOREY')
                self.validateStructure(aviRec, auditLogObj)

                errList = tprObj.validateTPR_Record(aviRec)
                if errList:
                    self.totalErrorCount += len(errList)
                    auditLogObj.addAuditRecords(aviRec.fieldValues['oid'], errList, 'ERROR')
                    
                if multiStoried:
                    uLayerRec = aviRec.returnLayerRecord('UNDER', aviRec.fieldValues)
                    self.validateLayer(uLayerRec, auditLogObj, 'UNDERSTOREY')

                self.validateLayerRelationships(aviRec, auditLogObj)
                        
                arcpy.SetProgressorPosition()

        auditLogObj.writeAuditRecords()
        
        arcpy.ResetProgressor()

        # Add basic audit information for report purposes.
        infoList = []
        infoList.append('AVI Attribute Audit Report')
        infoList.append('QC Software Version: %s' % self.version())
        infoList.append('AVI Feature Class: %s' % self.aviFCPath)
        infoList.append('Date: %s' % datetime.date.today())
        infoList.append('Features Processed: %s' % selCnt)
        infoList.append('Total Errors: %s' % self.totalErrorCount)
        infoList.append('Total Warnings: %s' % self.totalWarningCount)
        infoList.append('Audit Run By: %s' % os.environ.get('USERNAME'))
        auditLogObj.addInformationMessages(infoList)
        auditLogObj.writeInformationMessages()

        self._logChn.logMsg('\n *** Errors: %s Warnings: %s' % (self.totalErrorCount, self.totalWarningCount))

        # Delete and pre-existing summaries.
        errorSummaryPath = os.path.join(self.fgdbPath, 'error_summary')
        if arcpy.Exists(errorSummaryPath):
            arcpy.Delete_management(errorSummaryPath)
        warningSummaryPath = os.path.join(self.fgdbPath, 'warning_summary')
        if arcpy.Exists(warningSummaryPath):
            arcpy.Delete_management(warningSummaryPath)

        self.buildSummaryTables(self.auditTablePath, errorSummaryPath, warningSummaryPath)
        self._logChn.logMsg('\n *** Any Errors or Warnings have been logged to: %s\n***' % self.auditTablePath)
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def buildSummaryTables(self, srcTable, errorSummaryPath, warningSummaryPath):
        '''
        Build the Error and Warning summary tables. This is used to
        replace the frequency command so that we can eliminate the need
        for an Advanced License to run this tool.

        Parameters:
            srcTable - path to table containing the error and warnings.

            errorSummaryPath - path to the error summary table to create.

            warningSummaryPath - path to the warning summary table to create.
        '''

        if not arcpy.Exists(srcTable):
            self._logChn.logWarning('\n***\n*** No error-warning log file found, cannot create summary tables\n***')
            return False
                                    
        # Build dictionaries based on errors and warnings to accumulate counts of each message type.
        warnDict = {}
        errorDict = {}
        with arcpy.da.SearchCursor(srcTable, ['TYPE', 'MESSAGE']) as cursor:
            for row in cursor:
                msgType = row[0]
                msg = row[1]
                if msgType.upper() == 'ERROR':
                    if msg in errorDict:
                        cnt = errorDict[msg]
                    else:
                        cnt = 0
                    cnt += 1
                    errorDict[msg] = cnt
                elif msgType.upper() == 'WARNING':
                    if msg in warnDict:
                        cnt = warnDict[msg]
                    else:
                        cnt = 0
                    cnt += 1
                    warnDict[msg] = cnt
        
        # Populate the error summary table
        fgdbPath = os.path.dirname(errorSummaryPath)
        errTblName = os.path.basename(errorSummaryPath)
        if arcpy.Exists(errorSummaryPath):
            arcpy.Delete_management(errorSummaryPath)
        arcpy.CreateTable_management(fgdbPath, errTblName)
        arcpy.AddField_management(errorSummaryPath, 'COUNT', 'LONG')
        arcpy.AddField_management(errorSummaryPath, 'MESSAGE', 'TEXT', '', '', 250)
        outChn = arcpy.da.InsertCursor(errorSummaryPath, ('COUNT', 'MESSAGE'))
        msgList = errorDict.keys()
        msgList.sort()
        for msg in msgList:
            cnt = errorDict[msg]
            outChn.insertRow([cnt, msg])

        # Populate the error summary table
        fgdbPath = os.path.dirname(warningSummaryPath)
        warnTblName = os.path.basename(warningSummaryPath)
        if arcpy.Exists(warningSummaryPath):
            arcpy.Delete_management(warningSummaryPath)
        arcpy.CreateTable_management(fgdbPath, warnTblName)
        arcpy.AddField_management(warningSummaryPath, 'COUNT', 'LONG')
        arcpy.AddField_management(warningSummaryPath, 'MESSAGE', 'TEXT', '', '', 250)
        outChn = arcpy.da.InsertCursor(warningSummaryPath, ('COUNT', 'MESSAGE'))
        msgList = warnDict.keys()
        msgList.sort()
        for msg in msgList:
            cnt = warnDict[msg]
            outChn.insertRow([cnt, msg])
            
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def writeAuditReport(self, reportPath, errorsOnly = False, dumpAttributes = False):
        '''
        Generates an audit report using both the informational and
        audit records.

        Parameters:

            reportPath - path to report file to generate.

            errorsOnly - set to True if you only want errors reported.
                         Default value is false.
        '''

        if dumpAttributes:
            aviRecObj = AVI_Record(self.aviFCPath)
            attDumpDict = aviRecObj.buildAttributeDumpDictByOID()
        else:
            attDumpDict = {}
            
        auditLogObj = srd_audit_log.SRD_AuditLog(self.auditTablePath , self.infoTablePath )
        auditLogObj.writeAuditReport(reportPath, errorsOnly, attDumpDict, 'OID')

##--------------------------------------------------------------------
def test1():
    xlsPath = r'D:\pythonscripts\avi_class\test_data\AVI_ATTRIBUTE_TEST.xlsx\TEST_REC$'
    outTbl = r'D:\pythonscripts\avi_class\test_data\JULY_2019.gdb\ATT_TEST'

    arcpy.env.workspace = r'D:\pythonscripts\avi_class\test_data\JULY_2019.gdb'

    arcpy.DeleteRows_management(outTbl)

    arcpy.Append_management(xlsPath, outTbl, 'NO_TEST')
    
    auditObj = AVI_AttributeAudit(outTbl)
    auditObj.auditAttributes()
    
# ======================================================================
if __name__ == '__main__':

    fcPathArg = r'D:\testData\AVI\ATT_AUDIT.gdb\avi_a_20120606'
    fcPathArg = r'D:\test Data\AVI\AUDIT_TESTS.gdb\U_ANTH_VEG'

    fcPathArg = r'D:\pythonscripts\avi_class\test_data\JULY_2019.gdb\AVI_TEMPLATE_TBL_1'
    fcPathArg = r'D:\pythonscripts\avi_class\test_data\L11_Twp.gdb\L11_AVI'
    
    auditObj = AVI_AttributeAudit(fcPathArg)
    auditObj.auditAttributes()

##    test1()
    
##    aviRecObj = AVI_Record(fcPathArg)
##    attDumpDict = aviRecObj.buildAttributeDumpDict()
##
##    reportPath = r'D:\testData\AVI\avi_err.log'
##    auditObj.writeAuditReport(reportPath, False, True)
