
'''
Name: srd_fields.py

Overview: Provides methods and properties for working with fields.

Description:
    
    
Notes:

Author: Doug Crane
        May, 2012

Modifications:

'''

__author__ = 'Doug Crane'
__version__ = '1.0'

import sys
import os
import pdb

import arcpy

from srd_exception import *

import srd_logging
    
__all__ = ['SRD_FieldDefs', 'SRD_FieldDef']

# ====================================================================
class SRD_FieldDef(object):
    '''
    Provides container for defining individual field definition. This
    field definition can be used with SRD_FieldDefs to define a set of
    fields for a table or feature class. 
    '''
    
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __init__(self,
                 fieldName,
                 fieldType,
                 fieldLength=0,
                 fieldAlias='',
                 fieldPrecision=0,
                 fieldScale=0,
                 fieldDomain='',
                 fieldIsNullable=True,
                 fieldIsRequired=False,
                 fieldDefault=None,
                 fieldTag=''):
        '''
        Parameters:

            fieldName - name of physical name for field.

            fieldType - type of field as required by addField function.
                TEXT - Names or other textual qualities. 
                FLOAT - Numeric values with fractional values within a specific range. 
                DOUBLE - Numeric values with fractional values within a specific range. 
                SHORT - Numeric values without fractional values within a specific range; coded values. 
                LONG - Numeric values without fractional values within a specific range. 
                DATE - Date and/or Time. 
                BLOB - Images or other multimedia. 
                RASTER - Raster images. 
                GUID - GUID values 

            fieldLength - length of field as required for addField function.
                This option is only applicable on fields of type TEXT or BLOB.
                
            fieldAlias - alias for field name.

            fieldPrecision - Describes the number of digits that can be stored
                             in the field. All digits are counted no matter
                             what side of the decimal they are on. If the input
                             table is a personal or file geodatabase the
                             field precision value will be ignored.

            fieldScale - Sets the number of decimal places stored in a field.
                         This parameter is only used in Float and Double data
                         field types. If the input table is a personal or
                         file geodatabase the field scale value will be ignored.

            fieldDomain - domain field is to be associated with.

            fieldNullable - Can field contain a NULL value These are different
                            from zero or empty fields and are only supported
                            for fields in a geodatabase.

                            (True/False Default: True)

            fieldRequired - Specifies whether the field being created is
                            a required field for the table; only supported
                            for fields in a geodatabase. If the field is a
                            required field. This means new records must
                            contain a value for the field. Required fields
                            are permanent and can not be deleted.
                            (True/False Default: False

            fieldDefault - the default value that new records or features
                           will recieve for this field. (Must be compatable
                           with the field type)

            fieldTag - any information you wish to tag with the field.

        Properties
            Same as above:
                name - field name
                type - field type
                length - field length
                alias - field alias
                domain - field domain
                scale - field scale
                precision - field precision
                default - field default value
                isnullable - is field nullable
                isrequired - is field requied
                tag - user defined data

        Examples:

            Using named arguments to assign specific field attributes:
                fldDef = SRD_FieldDef('LNG','LONG',fieldAlias='MY_LONG',fieldDefault=0)

            Minimum field definition leaving rest of definition to defaults.
                fldDef = SRD_FieldDef('MY_NAME','TEXT', 25)
        '''

        self.name = fieldName
        self.type = fieldType
        self.length = fieldLength
        self.alias = fieldAlias
        self.domain = fieldDomain
        self.default = fieldDefault
        self.scale = fieldScale
        self.precision = fieldPrecision
        self.tag = fieldTag
        
        # Make some conversions to allow simpler use of addField
        # If property does not have relavance or a value set to an
        # empty string that will be passed to the addField tool.
        if fieldIsNullable:
            self.isNullable = 'NULLABLE'
        else:
            self.isNullable = 'NON_NULLABLE'

        if fieldIsRequired:
            self.isRequired = 'REQUIRED'
        else:
            self.isRequired = 'NON_REQUIRED'
            
        # Must have a field length if either TEXT or BLOB
        if self.type.upper() in ('TEXT', 'BLOB'):
            if self.length <= 0:
                raise SRD_Exception('Field: %s is missing length, this is require for all TEXT and BLOB fields' % self.name)
        else:
            self.length = ''

        if not self.scale:
            self.scale = ''

        if not self.precision:
            self.precision = ''

# ====================================================================
class SRD_FieldDefs(object):
    '''
    This class allows a series of fields to be defined for a given
    table or feature class so that the fields can be added in bulk.
    If contains a fieldDefList which holds the definitions for the
    fields you wish to add to the recordset. This list is built using
    the addField or addFieldDef methods. 
    '''

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __init__(self, recordsetPath):
        
        self.recordsetPath = recordsetPath

        # Set of field definitions to process.
        self._fieldDefList = []
        
        self._logChn = srd_logging.SRD_Log()

    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # Used if require full Field Definition
    def addFieldDef(self, fieldDef):
        self._fieldDefList.append(fieldDef)
        
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    # Very basic Field definition used in most cases. If you
    # need more control the use addFieldDef method.
    def addField(self, fieldName, fieldType, fieldLen=0):
        fieldDef = SRD_FieldDef(fieldName, fieldType, fieldLen)
        self._fieldDefList.append(fieldDef)
        
    #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def __str__(self):
        return str(self._fieldDefList)

    def __repr__(self):
        return str(self._fieldDefList)

    def __getitem__(self, key):
        return Layers(self._fieldDefList[key])

    def __len__(self):
        return len(self._fieldDefList)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def clearFieldList(self):
        '''
        Clears the list of field definitions associated with this instance.
        '''
        self._fieldDefList = []
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def setFieldList(self, fieldList):
        '''
        Sets the field definition list for fields to be added.
        '''
        self._fieldDefList = fieldList[:]

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def addFields(self, useAlias=True, verbose=True):
        '''
        Adds fields to a table or feature class using information contained
        in field list. Will only add field if field does not already exist.
        A list of fields is built using one of the available methods such
        as addField or addFieldDef. Calling this method will process each
        field definition in this list and try and add it to the table or
        feature class that is associated with this instance.

        Parameters:

            useAlias - optional parameter indicating if you wish to
                       assign the field alias. In some cases you may
                       wish just to see the actual field name in which
                       case pass False for this parameter. The default
                       is true.

            verbose - (True/False Default: True) Determines if any messages
                      will be displayed when adding fields.

        Examples:

            Examples of ways to add fields using field definitions:
            
            fldDefsObj = SRD_FieldDefs(outFC)

            # Use named arguments to simplify field def construction.
            fldDef = SRD_FieldDef('LNG','LONG',fieldAlias='MY_LONG',fieldDefault=0)
            fldDefsObj.addFieldDef(fldDef)

            # Use minimum number of arguments leaving rest as defaults.
            fldDefsObj.addFieldDef(SRD_FieldDef('MY_NAME','TEXT', 25))
            
            # Use very simple method to add a field
            fldDefsObj.addField('MY_DATE','DATE')

            # Add the fields without showing messages.
            fldDefsObj.addFields(verbose=False)
            
        '''

        if verbose: self._logChn.logMsg('\n***\n*** Adding fields to %s' % self.recordsetPath)

        try:
            dataObj = 'data_obj'
            arcpy.MakeTableView_management(self.recordsetPath, dataObj)
            delDataObj = True
        except:
            dataObj = self.recordsetPath
            delDataObj = False
        
        # Add the set of fields contained in the instance.
        for fldDef in self._fieldDefList:

            # Only add field if it does not exist.
            if not arcpy.ListFields(dataObj, fldDef.name):
                if verbose: self._logChn.logMsg('Adding %s' % fldDef.name)
                arcpy.AddField_management(dataObj, fldDef.name, fldDef.type, fldDef.precision, fldDef.scale, fldDef.length, fldDef.alias, fldDef.isNullable, fldDef.isRequired, fldDef.domain)

                # Assign the default value if one was provided.
                if fldDef.default is not None:
                    arcpy.AssignDefaultToField_management(dataObj, fldDef.name, fldDef.default)
                    
            else:
                if verbose: self._logChn.logMsg('Field %s already exists, will not add' % fldDef.name)

        if delDataObj:
            arcpy.Delete_management(dataObj)
            del dataObj
                
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def addIDField(self, fieldName):
        '''
        Adds an ID field that will be unique based on the value of the OID
        for the recordset. The purpose of adding our own ID field is to allow
        ust to process a dataset and then being able to link information from
        the processed dataset back to the original dataset. Using the OID ensures
        that it is unique.

        Parameters:
            fieldName - name to give ID field. Note that if this field already
                        exists it will not be recreated but it will be
                        recalculated.
        '''

        # If it already exists then ensure that it is defined as a LONG.
        fieldDef = arcpy.ListFields(self.recordsetPath, fieldName)
        if fieldDef:
            if fieldDef[0].type.lower() != 'integer':
                raise SRD_Exception('ID field: %s exits but is not the correct data type (Integer), cannot calculate value' % fieldName)
    
        # Add the ID field as a LONG since will be calculated to OID.
        self.addField(fieldName, 'LONG')
        self.addFields()
        
        # Calculate the ID field to that of the OID field to ensure uniqueness
        desc = arcpy.Describe(self.recordsetPath)
        if desc.hasOID:
            oidFieldName = '!%s!' % desc.OIDFieldName
            arcpy.CalculateField_management(self.recordsetPath, fieldName, oidFieldName, 'PYTHON')
        else:
            raise SRD_Exception('Recordset: %s does not have an OID field, cannot add ID Field' % self.recordsetPath)
                
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def tstAddField1():

    srcFC = r'D:\testData\srd_recordset_tst.gdb\null_test_src'
    outFC = r'D:\testData\srd_recordset_tst.gdb\add_field_test_out'

    arcpy.env.overwriteOutput = True
    
    arcpy.CopyFeatures_management(srcFC, outFC)

    fldDefsObj = SRD_FieldDefs(outFC)

    fldDefsObj.addField('LNG','LONG')
    fldDefsObj.addField('CORE_AVI','SHORT')
    fldDefsObj.addField('MY_TEXT','TEXT',35)
    fldDefsObj.addField('MY_DATE','DATE')
    fldDefsObj.addFields(verbose=True)
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def tstAddField2():

    srcFC = r'D:\testData\srd_recordset_tst.gdb\null_test_src'
    outFC = r'D:\testData\srd_recordset_tst.gdb\add_field_test_out'

    arcpy.env.overwriteOutput = True
    
    arcpy.CopyFeatures_management(srcFC, outFC)

    fldDefsObj = SRD_FieldDefs(outFC)
    
    fldDef = SRD_FieldDef('LNG','LONG',fieldAlias='MY_LONG',fieldDefault=0)
    fldDefsObj.addFieldDef(fldDef)
    
    fldDefsObj.addFieldDef(SRD_FieldDef('MY_NAME','TEXT', 25))
    
    fldDefsObj.addFields()
    
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
def tstIDField():

    srcFC = r'D:\testData\srd_recordset_tst.gdb\null_test_src'
    outFC = r'D:\testData\srd_recordset_tst.gdb\add_field_test_out'

    arcpy.env.overwriteOutput = True
    
    arcpy.CopyFeatures_management(srcFC, outFC)

    fldDefsObj = SRD_FieldDefs(outFC)

    fldDefsObj.addIDField('tst_chr')
    
# --------------------------------------------------------------------
if __name__ == '__main__':

    try:

        tstAddField2()
        
        ##tstIDField()
        
    except SRD_Exception as e:
        print(e)
