##############################################################################
#
# Copyright (c) 2002 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Sequence variables support

$Id: dt_insv.py 25177 2004-06-02 13:17:31Z jim $
"""

from math import sqrt
from types import IntType, TupleType

mv = None # Missing value


class sequence_variables:

    def __init__(self, items=None, query_string='', start_name_re=None):
        self.items = items
        self.query_string = query_string
        self.start_name_re = start_name_re

        self.data = data = {
            'previous-sequence': 0,
            'next-sequence': 0,
            'sequence-start': 1,
            'sequence-end': 0,
            }


    def __len__(self):
        return 1

    def number(self, index):
        return index+1

    def even(self, index):
        return index%2 == 0

    def odd(self, index):
        return index%2

    def letter(self, index):
        return chr(ord('a') + index)

    def Letter(self, index):
        return chr(ord('A') + index)

    def key(self,index):
        return self.items[index][0]

    def item(self,index, tt = TupleType):
        i = self.items[index]
        if isinstance(i, tt) and len(i)==2:
            return i[1]
        return i

    def roman(self, index):
        return self.Roman(index).lower()

    def Roman(self,num):
        # Force number to be an integer value
        num = int(num) + 1

        # Initialize roman as an empty string
        roman = ''

        while num >= 1000:
            num = num - 1000
            roman = '%sM' % roman

        while num >= 500:
            num = num - 500
            roman = '%sD' % roman

        while num >= 100:
            num = num - 100
            roman = '%sC' % roman

        while num >= 50:
            num = num - 50
            roman = '%sL' % roman

        while num >= 10:
            num = num - 10
            roman = '%sX' % roman

        while num >= 5:
            num = num - 5
            roman = '%sV' % roman

        while num < 5 and num >= 1:
            num = num - 1
            roman = '%sI' % roman

        # Replaces special cases in Roman Numerals

        roman = sub('DCCCC', 'CM', roman)
        roman = sub('CCCC', 'CD', roman)
        roman = sub('LXXXX', 'XC', roman)
        roman = sub('XXXX', 'XL', roman)
        roman = sub('VIIII', 'IX', roman)
        roman = sub('IIII', 'IV', roman)

        return roman


    def value(self, index, name):
        data = self.data
        item = self.items[index]
        if isinstance(item, TupleType) and len(item)==2:
            item = item[1]
        if data['mapping']:
            return item[name]
        return getattr(item, name)

    def first(self, name, key=''):
        data = self.data
        if data['sequence-start']:
            return 1
        index = data['sequence-index']
        return self.value(index, name) != self.value(index-1, name)

    def last(self, name, key=''):
        data = self.data
        if data['sequence-end']:
            return 1
        index = data['sequence-index']
        return self.value(index, name) != self.value(index+1, name)

    def length(self, ignored):
        l=self.data['sequence-length'] = len(self.items)
        return l

    def query(self, *ignored):
        if self.start_name_re is None:
            raise KeyError, 'sequence-query'
        query_string = self.query_string
        while query_string and query_string[:1] in '?&':
            query_string = query_string[1:]
        while query_string[-1:] == '&':
            query_string = query_string[:-1]
        if query_string:
            query_string = '&%s&' % query_string
            re = self.start_name_re
            l = re.search_group(query_string, (0,))
            if l:
                v = l[1]
                l = l[0]
                query_string = (query_string[:l] +
                                query_string[l + len(v) - 1:])
            query_string = '?' + query_string[1:]
        else:
            query_string = '?'
        self.data['sequence-query'] = query_string
        return query_string


    statistic_names = (
        'total', 'count', 'min', 'max', 'median', 'mean',
        'variance', 'variance-n','standard-deviation', 'standard-deviation-n',
        )

    def statistics(self, name, key):
        items = self.items
        data = self.data
        mapping = data['mapping']
        count = sum = sumsq = 0
        min = max = None
        scount = smin = smax = None
        values = []
        svalues = []
        for item in items:
            try:
                if mapping:
                    item = item[name]
                else:
                    item = getattr(item, name)
                try:
                    if item is mv:
                        item = None
                    if isinstance(item, IntType):
                        s = item * long(item)
                    else:
                        s = item * item
                    sum = sum + item
                    sumsq = sumsq + s
                    values.append(item)
                    if min is None:
                        min = max = item
                    else:
                        if item < min:
                            min = item
                        if item > max:
                            max = item
                except:
                    if item is not None and item is not mv:
                        if smin is None:
                            smin = smax = item
                        else:
                            if item < smin:
                                smin = item
                            if item > smax:
                                smax = item
                        svalues.append(item)
            except: pass

        # Initialize all stats to empty strings:
        for stat in self.statistic_names:
            data['%s-%s' % (stat,name)] = ''

        count = len(values)
        try: # Numeric statistics
            n = float(count)
            mean = sum/n
            sumsq = sumsq/n - mean*mean
            data['mean-%s' % name] = mean
            data['total-%s' % name] = sum
            data['variance-n-%s' % name] = sumsq
            data['standard-deviation-n-%s' % name] = sqrt(sumsq)
            if count > 1:
                sumsq = sumsq * n/(n-1)
                data['variance-%s' % name] = sumsq
                data['standard-deviation-%s' % name] = sqrt(sumsq)
            else:
                data['variance-%s' % name] = ''
                data['standard-deviation-%s' % name] = ''
        except:
            if min is None: min, max, values = smin, smax, svalues
            else:
                if smin < min:
                    min = smin
                if smax > max:
                    max = smax
                values = values + svalues
            count = len(values)

        data['count-%s' % name] = count
        # data['_values']=values
        if min is not None:
            data['min-%s' % name] = min
            data['max-%s' % name] = max
            values.sort()
            if count == 1:
                data['median-%s' % name] = min
            else:
                n=count+1
                if n/2 * 2 == n:
                    data['median-%s' % name] = values[n/2 - 1]
                else:
                    n = n/2
                    try:
                        data['median-%s' % name] = (values[n]+values[n-1])/2
                    except:
                        try:
                            data['median-%s' % name] = (
                                "between %s and %s" % (values[n], values[n-1]))
                        except: pass

        return data[key]

    def next_batches(self, suffix='batches', key=''):
        if suffix != 'batches':
            raise KeyError, key
        data = self.data
        sequence = self.items
        try:
            if not data['next-sequence']:
                return ()
            sz = data['sequence-step-size']
            start = data['sequence-step-start']
            end = data['sequence-step-end']
            l = len(sequence)
            orphan = data['sequence-step-orphan']
            overlap = data['sequence-step-overlap']
        except:
            AttributeError, 'next-batches'
        r = []
        while end < l:
            start, end, spam = opt(end+1-overlap, 0, sz, orphan, sequence)
            v = sequence_variables(self.items,
                                   self.query_string, self.start_name_re)
            d = v.data
            d['batch-start-index'] = start-1
            d['batch-end-index'] = end-1
            d['batch-size'] = end+1-start
            d['mapping'] = data['mapping']
            r.append(v)
        data['next-batches'] = r
        return r

    def previous_batches(self, suffix='batches', key=''):
        if suffix != 'batches':
            raise KeyError, key
        data = self.data
        sequence = self.items
        try:
            if not data['previous-sequence']:
                return ()
            sz = data['sequence-step-size']
            start = data['sequence-step-start']
            end = data['sequence-step-end']
            l = len(sequence)
            orphan = data['sequence-step-orphan']
            overlap = data['sequence-step-overlap']
        except:
            AttributeError, 'previous-batches'
        r = []
        while start > 1:
            start, end, spam = opt(0, start-1+overlap, sz, orphan, sequence)
            v = sequence_variables(self.items,
                                   self.query_string, self.start_name_re)
            d = v.data
            d['batch-start-index'] = start-1
            d['batch-end-index'] = end-1
            d['batch-size'] = end+1-start
            d['mapping'] = data['mapping']
            r.append(v)
        r.reverse()
        data['previous-batches'] = r
        return r


    special_prefixes = {
        'first': first,
        'last': last,
        'previous': previous_batches,
        'next': next_batches,
        # These two are for backward compatability with a missfeature:
        'sequence-index': lambda self, suffix, key: self['sequence-'+suffix],
        'sequence-index-is': lambda self, suffix, key: self['sequence-'+suffix],
        }
    for n in statistic_names:
        special_prefixes[n] = statistics

    def __getitem__(self,key,
                    special_prefixes=special_prefixes,
                    special_prefix=special_prefixes.has_key
                    ):
        data = self.data
        if data.has_key(key):
            return data[key]

        l = key.rfind('-')
        if l < 0:
            raise KeyError, key

        suffix = key[l+1:]
        prefix = key[:l]

        if hasattr(self, suffix):
            try:
                v = data[prefix+'-index']
            except:
                pass
            else:
                return getattr(self, suffix)(v)

        if special_prefix(prefix):
            return special_prefixes[prefix](self, suffix, key)

        if prefix[-4:] == '-var':
            prefix = prefix[:-4]
            try:
                return self.value(data[prefix+'-index'], suffix)
            except:
                pass

        if key == 'sequence-query':
            return self.query()

        raise KeyError, key



def sub(s1, s2, src):
    return s2.join(src.split(s1))

def opt(start, end, size, orphan, sequence):
    if size < 1:
        if start > 0 and end > 0 and end >= start:
            size = end+1 - start
        else: size = 7

    if start > 0:

        try:
            sequence[start-1]
        except:
            start = len(sequence)

        if end > 0:
            if end < start:
                end = start
        else:
            end = start + size-1
            try:
                sequence[end+orphan-1]
            except:
                end = len(sequence)

    elif end > 0:
        try:
            sequence[end-1]
        except:
            end=len(sequence)
        start = end+1 - size
        if start - 1 < orphan:
            start = 1

    else:
        start = 1
        end = start + size-1
        try:
            sequence[end+orphan-1]
        except:
            end = len(sequence)

    return start, end, size
