from zope.interface import implements

from nevow import tags, athena, flat, loaders
from nevow.athena import expose

from formless.annotate import nameToLabel

from xmantissa.fragmentutils import PatternDictionary, dictFillSlots
from xmantissa import ixmantissa

# review the need to pass around instances of columns,
# rather than classes, same for actions also

class ColumnViewBase(object):
    def __init__(self, attributeID, displayName=None,
                 width=None, typeHint=None):
        """@param typeHint: text|datetime|action or None"""

        self.attributeID = attributeID
        if displayName is None:
            displayName = nameToLabel(attributeID)
        self.displayName = displayName
        self.width = width
        self.typeHint = typeHint

    def stanFromValue(self, idx, item, value):
        # called with the result of extractValue
        return unicode(value)

    def getWidth(self):
        if self.width is None:
            return ''
        else:
            return self.width

    def onclick(self, idx, item, value):
        return None

class EditableColumnView(ColumnViewBase):
    """Column View that can be edited within the table."""

    def __init__(self, attributeID, displayName=None, width=None,
                 typeHint=None, coercer=unicode, getter=None, setter=None,
                 **kw):
        super(EditableColumnView, self).__init__(attributeID=attributeID,
            displayName=displayName, width=width, typeHint=typeHint, **kw)
        if getter is not None:
            self.getter = getter
        if setter is not None:
            self.setter = setter

    def stanFromValue(self, idx, item, value):
        return EditableColumnField(self, idx, item, value)

    # custom getter/setter can be given as kwargs to override default

    def setter(self, item, value):
        setattr(item, self.attributeID, value)

    def getter(self, item):
        return getattr(item, self.attributeID)

class EditableColumnField(athena.LiveFragment):
    """Renders the input field in the table cell to edit data in-place."""

    docFactory = loaders.stan(
        tags.span(render=tags.directive('liveFragment'))[
            tags.span(render=tags.directive('field'))
        ])

    def __init__(self, original, idx, item, value, *a, **kw):
        athena.LiveFragment.__init__(self, original, *a, **kw)
        self.idx = idx
        self.item = item
        self.value = value

    def render_field(self, ctx, data):
        handler = "Nevow.Athena.Widget.get(this).callRemote('handleValueChange', this.value)"
        t = tags.input(type="text",
                onchange=handler,
                value=self.original.getter(self.item))
        return t

    def handleValueChange(self, value):
        return self.original.setter(self.item, self.original.coercer(value))
    expose(handleValueChange)



class DateColumnView(ColumnViewBase):
    def __init__(self, attributeID, displayName=None, width=None,
                 typeHint='datetime'):

        ColumnViewBase.__init__(self, attributeID, displayName,
                                width, typeHint)

    def stanFromValue(self, idx, item, value):
        # XXX timezones
        return value.asHumanly()

class ActionsColumnView(ColumnViewBase):
    def __init__(self, actions, width=None, typeHint='actions'):
        ColumnViewBase.__init__(self, 'Actions', width=width, typeHint=typeHint)
        self.actions = actions

    def stanFromValue(self, idx, item, value):
        # Value will generally be 'None' in this case...
        tag = tags.div()
        for action in self.actions:
            actionable = action.actionable(item)
            if actionable:
                iconURL = action.iconURL
            else:
                iconURL = action.disabledIconURL

            stan = tags.img(src=iconURL, **{'class' : 'tdb-action'})

            if actionable:
                linkstan = action.toLinkStan(idx, item)
                if linkstan is None:
                    handler = 'Mantissa.TDB.Controller.get(this).performAction(%r, %r); return false'
                    handler %= (action.actionID, idx)
                    stan = tags.a(href='#', onclick=handler)[stan]
                else:
                    stan = linkstan

            tag[stan]

        # at some point give the javascript the description to show
        # or something
        return tag

class Action(object):
    def __init__(self, actionID, iconURL, description, disabledIconURL=None):
        self.actionID = actionID
        self.iconURL = iconURL
        self.disabledIconURL = disabledIconURL
        self.description = description

    def performOn(self, item):
        """perform this action on the given item, returning
           None or a completion message that might be useful
           to the originator of the action"""
        raise NotImplementedError()

    def actionable(self, item):
        """return a boolean indicating whether it makes sense to
           perform this action on the given item"""
        raise NotImplementedError()

    def toLinkStan(self, idx, item):
        return None

class ToggleAction(Action):

    def actionable(self, item):
        return True

    def isOn(self, idx, item):
        raise NotImplementedError()

    def toLinkStan(self, idx, item):
        handler = 'Mantissa.TDB.Controller.get(this).performAction(%r, %r); return false'
        handler %= (self.actionID, idx)
        iconURL = (self.disabledIconURL, self.iconURL)[self.isOn(idx, item)]
        img = tags.img(src=iconURL, **{'class': 'tdb-action'})
        return tags.a(href='#', onclick=handler)[img]

class TabularDataView(athena.LiveFragment):
    implements(ixmantissa.INavigableFragment)

    jsClass = u'Mantissa.TDB.Controller'

    fragmentName = 'tdb'
    live = 'athena'
    title = ''
    patterns = None

    def __init__(self, model, columnViews, actions=(), width='', itemsCalled='Items'):
        """
        @param itemsCalled: something describing what a collection of tdb items is called,
                            e.g. 'Phone Numbers' or 'Clicks'
        """
        super(TabularDataView, self).__init__(model)
        self.columnViews = list(columnViews)
        if actions:
            self.columnViews.append(ActionsColumnView(actions))
        self.actions = {}
        for action in actions:
            self.actions[action.actionID] = action
        self.width = width
        self.itemsCalled = itemsCalled

    def constructTable(self):
        if self.patterns is None:
            self.patterns = PatternDictionary(self.docFactory)

        modelData = self.original.currentPage()

        if len(modelData) == 0:
            return self.patterns['no-rows'].fillSlots(
                                            'items-called', self.itemsCalled)

        tablePattern = self.patterns['table']

        headers = self.constructColumnHeaders()
        rows = self.constructRows(modelData)

        tablePattern = tablePattern.fillSlots('column-headers', list(headers))

        return tablePattern.fillSlots(
                'rows', list(rows)).fillSlots('width', self.width)

    def constructColumnHeaders(self):
        for cview in self.columnViews:
            model = self.original
            column = model.columns.get(cview.attributeID)

            if column is None:
                sortable = False
            else:
                sortable = column.sortAttribute() is not None

            if cview.attributeID == model.currentSortColumn.attributeID:
                headerPatternName = ['sorted-column-header-descending',
                                     'sorted-column-header-ascending'][model.isAscending]
            else:
                headerPatternName = ['column-header',
                                     'sortable-column-header'][sortable]

            header = self.patterns[headerPatternName].fillSlots(
                        'name', cview.displayName).fillSlots(
                                'width', cview.getWidth())

            if sortable:
                header = header.fillSlots('onclick',
                            'Mantissa.TDB.Controller.get(this).clickSort("%s")'%
                            (cview.attributeID,))

            yield header

    def constructRows(self, modelData):
        rowPattern = self.patterns['row']
        cellPattern = self.patterns['cell']

        for idx, row in enumerate(modelData):
            cells = []
            for cview in self.columnViews:
                value = row.get(cview.attributeID)
                cellContents = cview.stanFromValue(
                        idx, row['__item__'], value)
                if hasattr(cellContents, 'setFragmentParent'):
                    cellContents.setFragmentParent(self)
                handler = cview.onclick(idx, row['__item__'], value)
                cellStan = dictFillSlots(cellPattern,
                                         {'value': cellContents,
                                          'onclick': handler,
                                          'class': cview.typeHint})

                cells.append(cellStan)

            yield dictFillSlots(rowPattern,
                                {'cells': cells,
                                 'class': 'tdb-row-%s' % (idx,)})

    def getInitialArguments(self):
        return self.getPageState()

    def render_navigation(self, ctx, data):
        patterns = PatternDictionary(self.docFactory)
        return ctx.tag[patterns['navigation']()]

    def render_actions(self, ctx, data):
        return '(Actions not yet implemented)'

    def render_table(self, ctx, data):
        return self.constructTable()

    def replaceTable(self):
        # XXX TODO: the flatten here is encoding/decoding like 4 times; this
        # could be a lot faster.
        return unicode(flat.flatten(self.constructTable()), 'utf-8'), self.getPageState()
    expose(replaceTable)

    def getPageState(self):
        tdm = self.original
        return (tdm.hasPrevPage(), tdm.hasNextPage(),
                tdm.pageNumber, tdm.itemsPerPage, tdm.totalItems)
    expose(getPageState)

    def nextPage(self):
        self.original.nextPage()
        return self.replaceTable()
    expose(nextPage)

    def prevPage(self):
        self.original.prevPage()
        return self.replaceTable()
    expose(prevPage)

    def firstPage(self):
        self.original.firstPage()
        return self.replaceTable()
    expose(firstPage)

    def lastPage(self):
        self.original.lastPage()
        return self.replaceTable()
    expose(lastPage)

    def itemFromTargetID(self, targetID):
        modelData = list(self.original.currentPage())
        return modelData[targetID]['__item__']

    def performAction(self, actionID, targetID):
        target = self.itemFromTargetID(int(targetID))
        action = self.actions[actionID]
        result = action.performOn(target)
        return result, self.replaceTable()
    expose(performAction)

    def clickSort(self, attributeID):
        if attributeID == self.original.currentSortColumn.attributeID:
            self.original.resort(attributeID, not self.original.isAscending)
        else:
            self.original.resort(attributeID)
        self.original.firstPage()
        return self.replaceTable()
    expose(clickSort)

    def head(self):
        return None


syntax highlighted by Code2HTML, v. 0.9.1