#
# Copyright (c) 2009 Nokia Corporation and/or its subsidiary(-ies).
# All rights reserved.
# This component and the accompanying materials are made available
# under the terms of "Eclipse Public License v1.0"
# which accompanies this distribution, and is available
# at the URL "http://www.eclipse.org/legal/epl-v10.html".
#
# Initial Contributors:
# Nokia Corporation - initial contribution.
#
# Contributors:
#
# Description:
#
import re
from cone.public import exceptions, plugin
import crml_reader
from crml_model import *

class CenRepEntry(object):
    """
    Class representing an entry in a CenRep text file.
    """
    def __init__(self, **kwargs):
        self.int            = kwargs.get('int')
        self.crml_type      = kwargs.get('crml_type')
        self.confml_type    = kwargs.get('confml_type')
        self.value          = kwargs.get('value')
        self.orig_value     = kwargs.get('orig_value')
        self.access         = kwargs.get('access')
        self.backup         = kwargs.get('backup', False)
    
    @property
    def metadata(self):
        return _get_metadata(self.backup)
    
    def __lt__(self, other):
        return crml_reader.convert_num(self.int) < crml_reader.convert_num(other.int)

class CenRepRfsRecord(object):
    """
    Class representing an entry in the CenRep RFS text file.
    """
    def __init__(self, repo_uid, key_uids=None):
        self.repo_uid = repo_uid
        self.key_uids = key_uids or []
    
    def __eq__(self, other):
        if type(self) == type(other):   return self.repo_uid == other.repo_uid
        else:                           return False
    
    def __ne__(self, other):
        return not (self == other)
    
    def __lt__(self, other):
        return  self.repo_uid < other.repo_uid
    
    def __repr__(self):
        return "CenRepRfsRecord(repo_uid=%s, key_uids=%r)" % (self.repo_uid, self.key_uids)

class CrmlTxtWriter(object):
    """
    Writer class for generating CenRep .txt files based on a CRML model.
    """
    
    def __init__(self, context, log):
        self.context = context
        self.log = log
    
    def get_cenrep_txt_data(self, repository, changed_refs=None):
        """
        Return the text data for the CenRep txt generated based on the given
        CRML repository model.
        @param changed_refs: List of changed refs. If this is passed, only a delta
            CenRep txt file is generated (i.e. containing only the changed settings).
            If None, the whole CenRep file is generated normally.
        @return: Text data for the CenRep text file.
        """
        delta_cenrep = changed_refs is not None
        data = []
        
        # Generate header lines 
        data.extend(self.get_header_lines(repository, delta_cenrep))
        
        self._check_repository_attrs(repository)
        
        # Generate CenRep entries for all keys
        cenrep_entries = []
        for key in repository.keys:
            # If generating a delta CenRep file, ignore keys that don't
            # use any of the changed settings
            if delta_cenrep:
                if not plugin.uses_ref(changed_refs, key.get_refs()):
                    continue
            cenrep_entries.extend(self.get_cenrep_entries(key))
        
        # Generate entry lines based on the entries
        cenrep_entries.sort()
        for entry in cenrep_entries:
            data.append(self.get_cenrep_entry_line(entry, delta_cenrep))
        
        data.append('')
        
        # Remove Nones from the line list
        data = filter(lambda val: val is not None, data)
        
        return '\r\n'.join(data)
    
    def get_cenrep_rfs_txt_data(self, rfs_records):
        """
        Return the text data for the CenRep RFS txt generated based on the given
        CenRep RFS record list.
        """
        data = []
        
        # Make a distinct and sorted array of the records
        records = []
        for r in rfs_records:
            if r not in records: records.append(r)
        records.sort()
        
        for record in records:
            repo_uid = record.repo_uid
            
            # Add padding zeros to the UID
            if len(repo_uid) < 8:
                repo_uid = (8 - (len(repo_uid) % 8)) * '0' + repo_uid
            temp = "CR %s" % repo_uid
            if record.key_uids:
                temp += " %s" % ' '.join(record.key_uids)
            data.append(temp)
        
        return '\r\n'.join(data)
    
    def get_cenrep_rfs_record(self, repository):
        """
        Return the RFS record for the given CRML repository.
        
        @return: A CenRepRfsRecord object if the repository should be listed
            in cenrep_rfs.txt, None if not.
        """
        # Get the UID as a hex value without the leading 0x
        repo_uid = _translate_key_uid(repository.uid_value)[2:]
        
        # Check if the whole repository has RFS=true
        if repository.rfs:
            return CenRepRfsRecord(repo_uid)
        
        # Collect the UIDs of the keys that should be listed
        rfs_key_uids = []
        for key in repository.keys:
            if self._key_is_rfs(key) and key.int:
                # Get the UID as a hex value without the leading 0x
                uid = _translate_key_uid(key.int)[2:]
                rfs_key_uids.append(uid)
                    
        if rfs_key_uids:
            return CenRepRfsRecord(repo_uid, rfs_key_uids)
        else:
            return None
    
    def _key_is_rfs(self, key):
        """
        @return: True if the key UID should be listed in cenrep_rfs.txt
        """
        if isinstance(key, CrmlSimpleKey):
            return bool(self._get_rfs_value(key.ref))
        elif isinstance(key, CrmlBitmaskKey):
            for bit in key.bits:
                if self._get_rfs_value(bit.ref):
                    return True
        else:
            return False
    
    def _get_rfs_value(self, ref):
        """
        @return: The RFS value for the given setting, or None if not available.
        """
        if ref is None: return
        
        try:
            feature = self._get_feature(ref)
        except exceptions.NotFound:
            # Feature not found in the configuration
            return None
        
        return feature.get_value(attr='rfs')
    
    def get_header_lines(self, repository, delta_cenrep=False):
        """
        Return a list of lines to be written in the header section of the CenRep text file.
        @param delta_cenrep: If True, only the data needed for a delta CenRep
            file is returned.
        """
        data = ['cenrep',
                'version %s' % repository.version]
        
        # Owner seems to be required even for delta CenReps
        if repository.owner:
            data.append('[owner]')
            data.append(repository.owner)
        
        data.append('[defaultmeta]')
        if not delta_cenrep:
            data.append(' %d' % _get_metadata(repository.backup))
            for key in repository.keys:
                data.append(self.get_defaultmeta_line(key))
        
        data.append('[platsec]')
        if not delta_cenrep:
            acc_text = self.get_access_line(repository.access)
            if acc_text: acc_text = ' ' + acc_text
            data.append(acc_text)
            for key in repository.keys:
                data.append(self.get_platsec_line(key, repository))
        
        data.append('[Main]')
        return data
    
    def get_cenrep_entries(self, key):
        """
        Generate CenRep entries based on the given CRML key object.
        @return: A list of CenRepEntry objects.
        """
        if isinstance(key, CrmlSimpleKey):
            feature = self._get_feature(key.ref)
            entry = CenRepEntry(int          = key.int,
                                crml_type    = key.type,
                                confml_type  = feature.get_type(),
                                value        = feature.get_value(),
                                orig_value   = feature.get_original_value(),
                                backup       = key.backup,
                                access       = key.access)
            return [entry]
        elif isinstance(key, CrmlBitmaskKey):
            return self.get_bitmask_key_cenrep_entries(key)
        elif isinstance(key, CrmlKeyRange):
            return self.get_key_range_cenrep_entries(key)
        else:
            raise TypeError("Unsupported CRML key object type %s" % type(key))
    
    def get_key_range_cenrep_entries(self, key_range):
        """
        Generate CenRep entries based on the given CrmlKeyRange object.
        @return: A list of CenRepEntry objects.
        """
        entries = []
        count = 0
        
        # Generate the countInt entry if necessary
        if key_range.count_int is not None and key_range.ref is not None:
            try:
                feature = self._get_feature(key_range.ref)
                
                # For CT2 output compatibility
                if feature.get_type() != 'sequence':
                    return []
                
                values = feature.get_value()
            except exceptions.NotFound:
                values = []
            
            count = len(values)
                
            entry = CenRepEntry(int         = key_range.count_int,
                                crml_type   = 'int',
                                confml_type = 'int',
                                value       = count,
                                backup      = key_range.backup,
                                access      = key_range.access)
            entries.append(entry)
        
        # Generate entries based on the sequence values
        for subkey in key_range.subkeys:
            full_ref = "%s.%s"% (key_range.ref, subkey.ref)
            
            try:
                feature = self._get_feature(full_ref)
                values = feature.get_value()
                confml_type = feature.get_type()
                backup = key_range.backup
            except exceptions.NotFound:
                # For CT2 output compatibility
                values = ['null' for i in xrange(count)]
                confml_type = None
                backup = False
                
            for i, value in enumerate(values):
                # Calculate the index of the entry
                index = self.get_index(crml_reader.convert_num(key_range.first_int),
                                       crml_reader.convert_num(key_range.first_index),
                                       crml_reader.convert_num(key_range.index_bits),
                                       i,
                                       crml_reader.convert_num(subkey.int))
                
                entry = CenRepEntry(int         = "0x%x" % index,
                                    crml_type   = subkey.type,
                                    confml_type = confml_type,
                                    value       = value,
                                    orig_value  = value,
                                    backup      = backup,
                                    access      = key_range.access)
                entries.append(entry)
        
        return entries
    
    def get_bitmask_key_cenrep_entries(self, key):
        """
        Generate CenRep entries based on the given CrmlBitmaskKey object.
        @return: A list of CenRepEntry objects.
        """
        # Calculate the value based on the bit values
        # -------------------------------------------
        value = 0
        for bit in key.bits:
            feature = self._get_feature(bit.ref)
            bit_value = feature.get_value()
            if bit.invert:  bit_value = not bit_value
            if bit_value:   value |= 1 << (bit.index - 1)
        
        # Generate the textual representation of the bitmask value.
        # This is done at this point because in get_cenrep_entry_line()
        # we don't know anymore if the key was a bitmask key or a
        # simple key.
        # -------------------------------------------------------------
        if key.type == 'binary':
            orig_value = "%X" % value
            # Add padding zeroes so that the number of digits
            # is divisible by 8 (done manually since the length
            # of a binary bitmask is unbounded).
            padding_zeroes = (8 - len(orig_value) % 8) * '0' 
            # 4 is a special case for CT2 output compatibility
            if len(orig_value) != 4:
                orig_value = padding_zeroes + orig_value
        else:
            orig_value = str(value)
        
        entry = CenRepEntry(int         = key.int,
                            crml_type   = key.type,
                            confml_type = 'int',
                            value       = value,
                            orig_value  = orig_value,
                            backup      = key.backup,
                            access      = key.access)
        return [entry]
    
    def get_defaultmeta_line(self, key):
        """
        Return the defaultmeta section line for the given CRML key object.
        """
        if not isinstance(key, CrmlKeyRange): return None
        
        return "%s %s %d" % (key.first_int,
                             key.last_int,
                             _get_metadata(key.backup))
    
    def get_platsec_line(self, key, repository):
        """
        Return the platsec section line for the given CRML key object.
        """
        if not isinstance(key, CrmlKeyRange): return None
        
        # In a key range platsec entry something must be present, so if
        # the access object is empty, use cap_rd and cap_wr from the repository's
        # global access definition
        access = key.access.copy()
        is_empty = True
        for attrname in ('sid_rd', 'cap_rd', 'sid_wr', 'cap_wr'):
            if getattr(access, attrname) not in ('', None):
                is_empty = False
        if is_empty:
            access.cap_rd = repository.access.cap_rd
            access.cap_wr = repository.access.cap_wr
        
        acc_text = self.get_access_line(access)
        if acc_text: acc_text = ' ' + acc_text
        
        return "%s %s%s" % (key.first_int,
                             key.last_int,
                             acc_text)
        
    
    def get_cenrep_entry_line(self, entry, delta_cenrep=False):
        """
        Return the text line for a CenRepEntry object.
        @param delta_cenrep: If True, only the data needed for a delta CenRep
            file is returned.
        """
        value = None
        if entry.crml_type in ('string', 'string8'):
            if entry.confml_type is None:
                pass
            else:
                if entry.orig_value is None:
                    value = '""'
                else:
                    value = '"%s"' % entry.orig_value
        elif entry.crml_type == 'int':
            if entry.confml_type == 'boolean':
                if entry.value: value = '1'
                else:           value = '0'
            else:
                value = entry.orig_value
        elif entry.crml_type == 'real':
            value = entry.orig_value or ''
        elif entry.crml_type == 'binary':
            # Empty binary values are denoted by a single dash
            value = entry.orig_value or '-'
            
            if value != '-':
                # Make sure that the number of digits is divisible by two
                if len(value) % 2 != 0:
                    value = '0' + value
        
        if value is None:
            value = unicode(entry.value)
        
        self._check_value(entry, value)
        
        if delta_cenrep:
            return '%s %s %s' % (_translate_key_uid(entry.int),
                                 entry.crml_type,
                                 value)
        else:
            acc_text = self.get_access_line(entry.access)
            if acc_text: acc_text = ' ' + acc_text
            
            return '%s %s %s %d%s' % (_translate_key_uid(entry.int),
                                      entry.crml_type,
                                      value,
                                      entry.metadata,
                                      acc_text)
    
    def _check_value(self, entry, value):
        """
        Check that the given value is valid for the given CenRep entry,
        and log a warning if it is not.
        """
        if entry.crml_type == 'int':
            # Check if the value is a string, since it may already
            # be an integer
            if not isinstance(value, basestring):
                return
            
            try:
                value = value.strip()
                if value.lower().startswith('0x'):
                    long(value, 16)
                else:
                    long(value)
            except ValueError:
                self.log.warn("Key %s: Invalid integer value '%s'" % (entry.int, value))
        elif entry.crml_type == 'real':
            try:
                float(value)
            except ValueError:
                self.log.warn("Key %s: Invalid real value '%s'" % (entry.int, value))
        elif entry.crml_type == 'binary':
            if value != '-' and re.match(r'^(0[xX])?[0-9a-fA-F]+$', value) is None:
                self.log.warn("Key %s: Invalid binary value '%s'" % (entry.int, value))
    
    def _check_repository_attrs(self, repository):
        """
        Check that the attributes of the given repository are valid and
        log warnings if not.
        """
        if repository.owner is not None:
            owner = repository.owner.strip()
            # An empty owner UID is OK, it doesn't generate anything
            # invalid into the output
            if owner != '':
                try:
                    if owner.lower().startswith('0x'):
                        long(owner, 16)
                    else:
                        long(owner)
                except ValueError:
                    self.log.warn("Invalid owner UID '%s'" % owner)
    
    def get_access_line(self, access):
        """
        Generate a line containing access information based on a CrmlAccess object.
        """
        # Write the access information in a specific order, because it
        # won't work otherwise
        var_order = ['sid_rd', 'cap_rd', 'sid_wr', 'cap_wr']
        data = []
        for varname in var_order:
            val = getattr(access, varname)
            if val not in ('', None):
                # Using _translate_capability_string() on all, since a SID should
                # not contain anything that could be messed up by the function
                data.append('%s=%s' % (varname, _translate_capability_string(val)))
        
        return ' '.join(data)
    
    def _get_feature(self, ref):
        return self.context.configuration.get_default_view().get_feature(ref)
    
    @classmethod
    def get_index(cls,firstInt,firstIndex,indexBits,seqIndex, subIndex):
        """
        @param firstIndex: The first value available in the keyrange 
        @param lastInt: The last value available in the keyrange 
        @param indexBits: The index bits or mask for sequence index
        @param seqIndex: The sequence index
        @param subIndex: The sequence sub element index
        @return: an numeric value for the encoded index
        """
        rangeshift = cls.get_range_shift(indexBits)
        return (((seqIndex+firstIndex) << rangeshift) + firstInt) + subIndex

    @classmethod
    def get_seqid(cls,firstInt,firstIndex,indexBits,cenrepkey):
        """
        @param firstIndex: The first value available in the keyrange 
        @param lastInt: The last value available in the keyrange 
        @param indexBits: The index bits or mask for sequence index
        @param cenrepkey: Crml key id
        @return: an numeric value for the encoded index
        """
        rangeshift = cls.get_range_shift(indexBits)
        return (((cenrepkey & indexBits) -firstInt) >> rangeshift)-firstIndex

    @classmethod
    def get_subseqid(cls,firstInt,firstIndex,indexBits,cenrepkey):
        """
        @param firstIndex: The first value available in the keyrange 
        @param lastInt: The last value available in the keyrange 
        @param indexBits: The index bits or mask for sequence index
        @param cenrepkey: Crml key id
        @return: an numeric value for the encoded index
        """
        range = cls.get_range(indexBits)
        return (cenrepkey - firstInt) & range

    @classmethod
    def get_range_shift(cls,indexBits):
        """ Get the bit left to the """
        seqrange = cls.get_range(indexBits)
        shiftamount = 0
        for i in range(0,32):
            if (seqrange >> i)  == 0:
                shiftamount = i
                break
        return shiftamount

    @classmethod
    def get_range(cls,indexBits):
        """ Get the bit left to the """
        indexshift = 0 
        for i in range(0,32):
            if (indexBits >> i)  == 0:
                indexshift = i
                break
        return ((0x1 << indexshift) - indexBits)-1


# =============================================================================
# Utility functions
# =============================================================================

def _get_metadata(backup):
    metadata = 0
    if backup: metadata |= 0x1000000
    return metadata
        
def _translate_key_uid(uid):
    """Translate a key UID given in CRML so that it matches the output of CT2."""
    if uid.lower().startswith("0x"):
        prefix = uid[:2]
        temp = uid[2:]
        if int(temp, 16) == 0:  return prefix + "0"
        else:                   return prefix + uid[2:].lstrip('0')
    else:
        if int(uid) == 0:       return "0"
        else:                   return uid.lstrip('0')

def _translate_capability_string(cap_str):
    """
    Translate a capability string so that it is
    suitable for writing to a CenRep txt file.
    """
    cap_str = cap_str.replace('AlwaysPass', 'alwayspass').replace('AlwaysFail', 'alwaysfail')
    
    # The capability string can be a list separated either by
    # whitespace or commas
    if ',' in cap_str:  caps = cap_str.split(',')
    else:               caps = cap_str.split()
    
    # The output must always be comma-separated
    return ','.join(caps)

