#####
##### This file is part of Mail-in-a-Box-LDAP which is released under the
##### terms of the GNU Affero General Public License as published by the
##### Free Software Foundation, either version 3 of the License, or (at
##### your option) any later version. See file LICENSE or go to
##### https://github.com/downtownallday/mailinabox-ldap for full license
##### details.
#####



class DictQuery(object):

    @staticmethod
    def find(data_list, q_list, return_first_exact=False, reverse=False):
        '''find items in list `data_list` using the query specified in
           `q_list` (a list of dicts).

            side-effects:
               q_list is modified ('_val' is added)

        '''
        if data_list is None:
            if return_first_exact:
                return None
            else:
                return []

        if type(q_list) is not list:
            q_list = [ q_list ]

        # set _val to value.lower() if ignorecase is True
        for q in q_list:
            if q=='*': continue
            ignorecase = q.get('ignorecase', False)
            match_val = q['value']
            if ignorecase and match_val is not None:
                match_val = match_val.lower()
            q['_val'] = match_val

        # find all matches
        matches = []
        direction = -1 if reverse else 1
        idx = len(data_list)-1 if reverse else 0
        while (reverse and idx>=0) or (not reverse and idx<len(data_list)):
            item = data_list[idx]
            if 'rank' in item and 'item' in item:
                # for re-querying... 
                item=item['item']
            count_mismatch = 0
            autoset_list = []
            optional_list = []

            for q in q_list:
                if q=='*': continue
                cmp_val = item.get(q['key'])
                if cmp_val is not None and q.get('ignorecase'):
                    cmp_val = cmp_val.lower()

                op = q.get('op', '=')
                mismatch = False
                if op == '=':
                    mismatch = q['_val'] != cmp_val
                elif op == '!=':
                    mismatch = q['_val'] == cmp_val
                else:
                    raise TypeError('No such op:  ' + op)
                    
                if mismatch:
                    count_mismatch += 1                        
                    if cmp_val is None:
                        if q.get('autoset'):
                            autoset_list.append(q)
                        elif q.get('optional'):
                            optional_list.append(q)
                    if return_first_exact:
                        break

            if return_first_exact:
                if count_mismatch == 0:
                    return item
            else:            
                optional_count = len(autoset_list) + len(optional_list)
                if count_mismatch - optional_count == 0:
                    rank = '{0:05d}.{1:08d}'.format(
                        optional_count,
                        len(data_list) - idx if reverse else idx
                    )
                    matches.append({
                        'exact': ( optional_count == 0 ),
                        'rank': rank,
                        'autoset_list': autoset_list,
                        'optional_list': optional_list,
                        'item': item
                    })

            idx += direction

        if not return_first_exact:
            # return the list sorted so the items with the fewest
            # number of required autoset/optional's appear first
            matches.sort(key=lambda x: x['rank'])
            return matches


    @staticmethod
    def autoset(match, incl_optional=False):
        item = match['item']
        for q in match['autoset_list']:
            assert item.get(q['key']) is None
            item[q['key']] = q['value']
        if incl_optional:
            for q in match['optional_list']:
                item[q['key']] = q['value']