# -*- test-case-name: xmantissa.test.test_scrolltable -*-

from zope.interface import implements

from nevow.athena import LiveElement, expose

from axiom.attributes import timestamp

from xmantissa.ixmantissa import IWebTranslator, IColumn
from xmantissa.tdb import AttributeColumn, Unsortable

TYPE_FRAGMENT = 'fragment'

# these objects aren't for view junk - they allow the model
# to inform the javascript controller about which columns are
# sortable, as well as supporting non-attribute columns

class TimestampAttributeColumn(AttributeColumn):
    # timestamps are a special case; we need to get the posix timestamp
    # so we can send the attribute value to javascript.  we don't register
    # an adapter for attributes.timestamp because the TDB model uses
    # IColumn.extractValue() to determine the value of the query pivot,
    # and so it needs an extime.Time instance, not a float

    def extractValue(self, model, item):
        val = AttributeColumn.extractValue(self, model, item)
        if val is None:
            raise AttributeError("%r was None" % (self.attribute,))
        return val.asPOSIXTimestamp()

    def getType(self):
        return 'timestamp'

class UnsortableColumn(AttributeColumn):
    def sortAttribute(self):
        return None

class UnsortableColumnWrapper:
    """
    Wraps an L{AttributeColumn} and makes it unsortable

    @ivar col: L{AttributeColumn}
    """
    implements(IColumn)

    def __init__(self, col):
        self.col = col
        self.attribute = col.attribute
        self.attributeID = col.attributeID

    def extractValue(self, model, item):
        return self.col.extractValue(model, item)

    def sortAttribute(self):
        return None

    def getType(self):
        return self.col.getType()

class Scrollable(object):
    """
    Mixin for "model" implementations of an in-browser scrollable list of
    elements.

    @ivar webTranslator: A L{IWebTranslator} provider for resolving and
    creating web links for items.

    @ivar columns: A mapping of attribute identifiers to L{IColumn}
    providers.

    @ivar columnNames: A list of attribute identifiers.

    @ivar isAscending: A boolean indicating the current order of the sort.

    @ivar currentSortColumn: A L{axiom.attributes.SQLAttribute} representing
    the current sort key.
    """
    def __init__(self, webTranslator, columns, defaultSortColumn,
                 defaultSortAscending):
        self.webTranslator = webTranslator
        self.columns = {}
        self.columnNames = []
        for col in columns:
            # see comment in TimestampAttributeColumn
            if isinstance(col, timestamp):
                col = TimestampAttributeColumn(col)
            else:
                col = IColumn(col)

            if defaultSortColumn is None:
                defaultSortColumn = col.sortAttribute()

            attributeID = unicode(col.attributeID, 'ascii')
            self.columns[attributeID] = col
            self.columnNames.append(attributeID)

        self.currentSortColumn = defaultSortColumn
        self.isAscending = defaultSortAscending


    # Override these two in a subclass
    def performCount(self):
        """
        Override this in a subclass.

        @rtype: C{int}
        @return: The total number of elements in this scrollable.
        """
        raise NotImplementedError()


    def performQuery(self, rangeBegin, rangeEnd):
        """
        Override this in a subclass.

        @rtype: C{list}
        @return: Elements from C{rangeBegin} to C{rangeEnd} of the
        underlying data set, as ordered by the value of
        C{currentSortColumn} sort column in the order indicated by
        C{isAscending}.
        """
        raise NotImplementedError()


    # The rest takes care of responding to requests from the client.
    def getTableMetadata(self):
        """
        Retrieve a description of the various properties of this scrolltable.

        @return: A sequence containing 5 elements.  They are, in order, a
        list of the names of the columns present, a mapping of column names
        to two-tuples of their type and a boolean indicating their
        sortability, the total number of rows in the scrolltable, the name
        of the default sort column, and a boolean indicating whether or not
        the current sort order is ascending.
        """
        coltypes = {}
        for (colname, column) in self.columns.iteritems():
            sortable = column.sortAttribute() is not None
            coltype = column.getType()
            if coltype is not None:
                coltype = unicode(coltype, 'ascii')
            coltypes[colname] = (coltype, sortable)

        if self.currentSortColumn:
            csc = unicode(self.currentSortColumn.attrname, 'ascii')
        else:
            csc = None

        return [self.columnNames, coltypes, self.requestCurrentSize(),
                csc, self.isAscending]
    expose(getTableMetadata)


    def resort(self, columnName):
        """
        Re-sort the table.

        @param columnName: the name of the column to sort by.  This is a string
        because it is passed from the browser.
        """
        csc = self.currentSortColumn
        newSortColumn = self.columns[columnName].sortAttribute()
        if newSortColumn is None:
            raise Unsortable('column %r has no sort attribute' % (columnName,))
        if csc is newSortColumn:
            self.isAscending = not self.isAscending
        else:
            self.currentSortColumn = newSortColumn
            self.isAscending = True
        return self.isAscending
    expose(resort)


    def linkToItem(self, item):
        """
        Return a URL that the row for C{item} should link to, by asking the
        L{xmantissa.ixmantissa.IWebTranslator} in C{self.store}

        @return: C{unicode} URL
        """
        return unicode(self.webTranslator.toWebID(item), 'ascii')


    def itemFromLink(self, link):
        """
        Inverse of L{linkToItem}.

        @rtype: L{axiom.item.Item}
        """
        return self.webTranslator.fromWebID(link)


    def requestRowRange(self, rangeBegin, rangeEnd):
        """
        Retrieve display data for the given range of rows.
        """
        return self.constructRows(self.performQuery(rangeBegin, rangeEnd))
    expose(requestRowRange)


    def requestCurrentSize(self):
        return self.performCount()
    expose(requestCurrentSize)


    def performAction(self, name, rowID):
        method = getattr(self, 'action_' + name)
        item = self.itemFromLink(rowID)
        return method(item)
    expose(performAction)


class ScrollableView(object):
    """
    Mixin for structuring model data in the way expected by
    Mantissa.ScrollTable.ScrollingWidget.
    """
    jsClass = u'Mantissa.ScrollTable.ScrollingWidget'
    fragmentName = 'scroller'

    def constructRows(self, items):
        rows = []
        for item in items:
            row = dict((colname, col.extractValue(self, item))
                            for (colname, col) in self.columns.iteritems())
            link = self.linkToItem(item)
            if link is not None:
                row[u'__id__'] = link
            rows.append(row)

        return rows



class _ScrollingElement(LiveElement):
    def getInitialArguments(self):
        return [self.getTableMetadata()]



class ItemQueryScrollingFragment(Scrollable, ScrollableView, _ScrollingElement):
    def __init__(self, store, itemType, baseConstraint, columns,
                 defaultSortColumn=None, defaultSortAscending=True,
                 *a, **kw):

        Scrollable.__init__(self, IWebTranslator(store, None), columns,
                            defaultSortColumn, defaultSortAscending)
        LiveElement.__init__(self, *a, **kw)
        self.store = store
        self.itemType = itemType
        self.baseConstraint = baseConstraint


    def performCount(self):
        return self.store.query(self.itemType, self.baseConstraint).count()


    def performQuery(self, rangeBegin, rangeEnd):
        if self.isAscending:
            sort = self.currentSortColumn.ascending
        else:
            sort = self.currentSortColumn.descending
        return list(self.store.query(self.itemType,
                                     self.baseConstraint,
                                     offset=rangeBegin,
                                     limit=rangeEnd - rangeBegin,
                              sort=sort))
ScrollingFragment = ItemQueryScrollingFragment



class SequenceScrollingFragment(Scrollable, ScrollableView, _ScrollingElement):
    """
    Scrolltable implementation backed by any Python L{axiom.item.Item}
    sequence.
    """
    def __init__(self, store, elements, columns, defaultSortColumn=None,
                 defaultSortAscending=True, *a, **kw):
        Scrollable.__init__(self, IWebTranslator(store, None), columns,
                            defaultSortColumn, defaultSortAscending)

        LiveElement.__init__(self, *a, **kw)
        self.store = store
        self.elements = elements


    def getInitialArguments(self):
        return [self.getTableMetadata()]


    def performCount(self):
        return len(self.elements)


    def performQuery(self, rangeBegin, rangeEnd):
        step = 1
        if not self.isAscending:
            # The ranges are from the end, not the beginning.
            rangeBegin = max(0, len(self.elements) - rangeBegin - 1)

            # Python is so very very confusing:
            # s[1:0:-1] == []
            # s[1:None:-1] == [s[0]]
            # s[1:-1:-1] == some crazy thing you don't even want to know
            rangeEnd = max(-1, len(self.elements) - rangeEnd - 1)
            if rangeEnd == -1:
                rangeEnd = None
            step = -1
        return self.elements[rangeBegin:rangeEnd:step]



class StoreIDSequenceScrollingFragment(SequenceScrollingFragment):
    """
    Scrolltable implementation like L{SequenceScrollingFragment} but which is
    backed by a sequence of Item storeID values rather than Items themselves.
    """
    def performQuery(self, rangeBegin, rangeEnd):
        return map(
            self.store.getItemByID,
            super(
                StoreIDSequenceScrollingFragment,
                self).performQuery(rangeBegin, rangeEnd))


syntax highlighted by Code2HTML, v. 0.9.1