#!/usr/bin/env python
"""
@@TR: This code is pretty much unsupported.

MondoReport.py -- Batching module for Python and Cheetah.

Version 2001-Nov-18.  Doesn't do much practical yet, but the companion
testMondoReport.py passes all its tests.
-Mike Orr (Iron)

TODO: BatchRecord.prev/next/prev_batches/next_batches/query, prev.query,
next.query.

How about Report: .page(), .all(), .summary()?  Or PageBreaker.
"""
import operator, types
try:
    from Cheetah.NameMapper import valueForKey as lookup_func
except ImportError:
    def lookup_func(obj, name):
        if hasattr(obj, name):
            return getattr(obj, name)
        else:
            return obj[name] # Raises KeyError.

########## CONSTANTS ##############################

True, False = (1==1), (1==0)
numericTypes = types.IntType, types.LongType, types.FloatType

########## PUBLIC GENERIC FUNCTIONS ##############################

class NegativeError(ValueError):
    pass

def isNumeric(v):
    return type(v) in numericTypes

def isNonNegative(v):
    ret = isNumeric(v)
    if ret and v < 0:
        raise NegativeError(v)

def isNotNone(v):
    return v is not None

def Roman(n):
    n = int(n) # Raises TypeError.
    if n < 1:
        raise ValueError("roman numeral for zero or negative undefined: " + n)
    roman = ''
    while n >= 1000:
            n = n - 1000
            roman = roman + 'M'
    while n >= 500:
            n = n - 500
            roman = roman + 'D'
    while n >= 100:
            n = n - 100
            roman = roman + 'C'
    while n >= 50:
            n = n - 50
            roman = roman + 'L'
    while n >= 10:
            n = n - 10
            roman = roman + 'X'
    while n >= 5:
            n = n - 5
            roman = roman + 'V'
    while n < 5 and n >= 1:
            n = n - 1
            roman = roman + 'I'
    roman = roman.replace('DCCCC', 'CM')
    roman = roman.replace('CCCC', 'CD')
    roman = roman.replace('LXXXX', 'XC')
    roman = roman.replace('XXXX', 'XL')
    roman = roman.replace('VIIII', 'IX')
    roman = roman.replace('IIII', 'IV')
    return roman


def sum(lis):
    return reduce(operator.add, lis, 0)
    
def mean(lis):
    """Always returns a floating-point number.
    """
    lis_len = len(lis)
    if lis_len == 0:
        return 0.00 # Avoid ZeroDivisionError (not raised for floats anyway)
    total = float( sum(lis) )
    return total / lis_len

def median(lis):
    lis = lis[:]
    lis.sort()
    return lis[int(len(lis)/2)]


def variance(lis):
    raise NotImplementedError()
    
def variance_n(lis):
    raise NotImplementedError()
    
def standardDeviation(lis):
    raise NotImplementedError()
    
def standardDeviation_n(lis):
    raise NotImplementedError()



class IndexFormats:
    """Eight ways to display a subscript index.
       ("Fifty ways to leave your lover....")
    """
    def __init__(self, index, item=None):
        self._index = index
        self._number = index + 1
        self._item = item

    def index(self):
        return self._index

    __call__ = index

    def number(self):
        return self._number

    def even(self):
        return self._number % 2 == 0

    def odd(self):
        return not self.even()

    def even_i(self):
        return self._index % 2 == 0

    def odd_i(self):
        return not self.even_i()

    def letter(self):
        return self.Letter().lower()

    def Letter(self):
        n = ord('A') + self._index
        return chr(n)

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

    def Roman(self):
        return Roman(self._number)

    def item(self):
        return self._item



########## PRIVATE CLASSES ##############################

class ValuesGetterMixin:
    def __init__(self, origList):
        self._origList = origList

    def _getValues(self, field=None, criteria=None):
        if field:
            ret = [lookup_func(elm, field) for elm in self._origList]
        else:
            ret = self._origList
        if criteria:
            ret = filter(criteria, ret)
        return ret


class RecordStats(IndexFormats, ValuesGetterMixin):
    """The statistics that depend on the current record.
    """
    def __init__(self, origList, index):
        record = origList[index] # Raises IndexError.
        IndexFormats.__init__(self, index, record)
        ValuesGetterMixin.__init__(self, origList)
    
    def length(self):
        return len(self._origList)

    def first(self):
        return self._index == 0
        
    def last(self):
        return self._index >= len(self._origList) - 1

    def _firstOrLastValue(self, field, currentIndex, otherIndex):
        currentValue = self._origList[currentIndex] # Raises IndexError.
        try:
            otherValue = self._origList[otherIndex]
        except IndexError:
            return True
        if field:
            currentValue = lookup_func(currentValue, field)
            otherValue = lookup_func(otherValue, field)
        return currentValue != otherValue

    def firstValue(self, field=None):
        return self._firstOrLastValue(field, self._index, self._index - 1)

    def lastValue(self, field=None):
        return self._firstOrLastValue(field, self._index, self._index + 1)

    # firstPage and lastPage not implemented.  Needed?

    def percentOfTotal(self, field=None, suffix='%', default='N/A', decimals=2):
        rec = self._origList[self._index]
        if field:
            val = lookup_func(rec, field)
        else:
            val = rec
        try:
            lis = self._getValues(field, isNumeric)
        except NegativeError:
            return default
        total = sum(lis)
        if total == 0.00: # Avoid ZeroDivisionError.
            return default
        val = float(val)
        try:
            percent = (val / total) * 100
        except ZeroDivisionError:
            return default
        if decimals == 0:
            percent = int(percent)
        else:
            percent = round(percent, decimals)
        if suffix:
            return str(percent) + suffix # String.
        else:
            return percent # Numeric.

    def __call__(self): # Overrides IndexFormats.__call__
        """This instance is not callable, so we override the super method.
        """
        raise NotImplementedError()

    def prev(self):
        if self._index == 0:
            return None
        else:
            length = self.length()
            start = self._index - length
            return PrevNextPage(self._origList, length, start)

    def next(self):
        if self._index + self.length() == self.length():
            return None
        else:
            length = self.length()
            start = self._index + length
            return PrevNextPage(self._origList, length, start)
            
    def prevPages(self):
        raise NotImplementedError()
        
    def nextPages(self):
        raise NotImplementedError()

    prev_batches = prevPages
    next_batches = nextPages

    def summary(self):
        raise NotImplementedError()



    def _prevNextHelper(self, start,end,size,orphan,sequence):
        """Copied from Zope's DT_InSV.py's "opt" function.
        """
        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 start > l: start=l

            if end > 0:
                if end < start: end=start
            else:
                end=start+size-1
                try: sequence[end+orphan-1]
                except: end=len(sequence)
                # if l - end < orphan: end=l
        elif end > 0:
            try: sequence[end-1]
            except: end=len(sequence)
            # if end > l: end=l
            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)
            # if l - end < orphan: end=l
        return start,end,size



class Summary(ValuesGetterMixin):
    """The summary statistics, that don't depend on the current record.
    """
    def __init__(self, origList):
        ValuesGetterMixin.__init__(self, origList)
        
    def sum(self, field=None):
        lis = self._getValues(field, isNumeric)
        return sum(lis)

    total = sum

    def count(self, field=None):
        lis = self._getValues(field, isNotNone)
        return len(lis)
        
    def min(self, field=None):
        lis = self._getValues(field, isNotNone)
        return min(lis) # Python builtin function min.
        
    def max(self, field=None):
        lis = self._getValues(field, isNotNone)
        return max(lis) # Python builtin function max.

    def mean(self, field=None):
        """Always returns a floating point number.
        """
        lis = self._getValues(field, isNumeric)
        return mean(lis)

    average = mean

    def median(self, field=None):
        lis = self._getValues(field, isNumeric)
        return median(lis)

    def variance(self, field=None):
        raiseNotImplementedError()

    def variance_n(self, field=None):
        raiseNotImplementedError()

    def standardDeviation(self, field=None):
        raiseNotImplementedError()

    def standardDeviation_n(self, field=None):
        raiseNotImplementedError()


class PrevNextPage:
    def __init__(self, origList, size, start):
        end = start + size
        self.start = IndexFormats(start, origList[start])
        self.end = IndexFormats(end, origList[end])
        self.length = size
        

########## MAIN PUBLIC CLASS ##############################
class MondoReport:
    _RecordStatsClass = RecordStats
    _SummaryClass = Summary

    def __init__(self, origlist):
        self._origList = origlist

    def page(self, size, start, overlap=0, orphan=0):
        """Returns list of ($r, $a, $b)
        """
        if overlap != 0:
            raise NotImplementedError("non-zero overlap")
        if orphan != 0:
            raise NotImplementedError("non-zero orphan")
        origList = self._origList
        origList_len = len(origList)
        start = max(0, start)
        end = min( start + size, len(self._origList) )
        mySlice = origList[start:end]
        ret = []
        for rel in range(size):
            abs_ = start + rel
            r = mySlice[rel]
            a = self._RecordStatsClass(origList, abs_)
            b = self._RecordStatsClass(mySlice, rel)
            tup = r, a, b
            ret.append(tup)
        return ret


    batch = page

    def all(self):
        origList_len = len(self._origList)
        return self.page(origList_len, 0, 0, 0)
    
    
    def summary(self):
        return self._SummaryClass(self._origList)

"""
**********************************
    Return a pageful of records from a sequence, with statistics.

       in : origlist, list or tuple.  The entire set of records.  This is
              usually a list of objects or a list of dictionaries.
            page, int >= 0.  Which page to display.
            size, int >= 1.  How many records per page.
            widow, int >=0.  Not implemented.
            orphan, int >=0.  Not implemented.
            base, int >=0.  Number of first page (usually 0 or 1).

       out: list of (o, b) pairs.  The records for the current page.  'o' is
              the original element from 'origlist' unchanged.  'b' is a Batch
              object containing meta-info about 'o'.
       exc: IndexError if 'page' or 'size' is < 1.  If 'origlist' is empty or
              'page' is too high, it returns an empty list rather than raising
              an error.
        
        origlist_len = len(origlist)
        start = (page + base) * size
        end = min(start + size, origlist_len)
        ret = []
        # widow, orphan calculation: adjust 'start' and 'end' up and down, 
        # Set 'widow', 'orphan', 'first_nonwidow', 'first_nonorphan' attributes.
        for i in range(start, end):
            o = origlist[i]
            b = Batch(origlist, size, i)
            tup = o, b
            ret.append(tup)
        return ret

    def prev(self):
        # return a PrevNextPage or None

    def next(self):
        # return a PrevNextPage or None

    def prev_batches(self):
        # return a list of SimpleBatch for the previous batches

    def next_batches(self):
        # return a list of SimpleBatch for the next batches

########## PUBLIC MIXIN CLASS FOR CHEETAH TEMPLATES ##############
class MondoReportMixin:
    def batch(self, origList, size=None, start=0, overlap=0, orphan=0):
        bat = MondoReport(origList)
        return bat.batch(size, start, overlap, orphan)
    def batchstats(self, origList):
        bat = MondoReport(origList)
        return bat.stats()
"""

# vim: shiftwidth=4 tabstop=4 expandtab textwidth=79
