import itertools, random, re
from zope.interface import implements

from epsilon.extime import Time

from twisted.python.components import registerAdapter

from nevow import rend, static, tags, url
from nevow.athena import LiveElement, expose
from nevow.inevow import IResource
from nevow.page import renderer
from nevow.vhost import VHostMonsterResource

from axiom import batch, userbase
from axiom.attributes import AND, integer, reference, text, timestamp
from axiom.dependency import dependsOn
from axiom.item import Item, transacted
from axiom.scheduler import Scheduler
from axiom.upgrade import registerAttributeCopyingUpgrader

from xmantissa.fragmentutils import FragmentCollector
from xmantissa.fulltext import SQLiteIndexer
from xmantissa.ixmantissa import (IFulltextIndexable, INavigableElement,
    INavigableFragment, ISessionlessSiteRootPlugin, ITemplateNameResolver)
from xmantissa.scrolltable import InequalityModel
from xmantissa.sharing import getEveryoneRole, itemFromProxy
from xmantissa.webapp import PrivateApplication
from xmantissa.webnav import Tab
from xmantissa.websharing import linkTo
from xmantissa.website import PrefixURLMixin
from xmantissa.webtheme import ThemedDocumentFactory

from methanal.model import Model, ValueParameter
from methanal.view import CheckboxInput, LiveForm, SelectInput, TextInput

from circus.color import colorizeLine
from circus.icircus import (IQuote, IQuoteAdder, IQuoteDatabase,
    IQuoteModeration, IQuoteSearcher)
from circus.roles import getModeratorsRole, getUserRole, inModerators
from circus.util import getServiceProvider


class Redirector(Item):
    """
    Redirect users away from C{/private}.
    """
    implements(INavigableElement)

    typeName = 'flyingcircus_redirector'
    schemaVersion = 1
    powerupInterfaces = [INavigableElement]

    privateApplication = dependsOn(PrivateApplication)

    def getTabs(self):
        return [Tab('Quotes', self.storeID, 1.0, authoritative=True, children=[
                    Tab('Overview', self.storeID, 1.0)])]


class MyQuotes(Item):
    """
    Personalised quote view.
    """
    implements(INavigableElement)

    typeName = 'flyingcircus_myquotes'
    schemaVersion = 1
    powerupInterfaces = [INavigableElement]

    privateApplication = dependsOn(PrivateApplication)

    def getTabs(self):
        return [Tab('Quotes', self.storeID, 1.0, authoritative=False, children=[
                    Tab('My Quotes', self.storeID, 0.8)])]


class QuoteDB(Item):
    implements(IQuoteAdder, IQuoteDatabase, IQuoteSearcher)

    typeName = 'flyingcircus_quotedb'
    schemaVersion = 3
    powerupInterfaces = [IQuoteDatabase]

    lastQid = integer(default=0)

    scheduler = dependsOn(Scheduler)
    searchIndexer = dependsOn(SQLiteIndexer)

    def getQuotesByIDs(self, quoteIDs, sort=None):
        """
        Query for L{Quote}s by their IDs.
        """
        return self.store.query(Quote,
                                Quote.qid.oneOf(quoteIDs),
                                sort=sort)

    # IQuoteAdder

    @transacted
    def addQuote(self, content, userID):
        self.lastQid = self.lastQid + 1
        quote = Quote(store=self.store,
                      qid=self.lastQid,
                      content=content,
                      userID=userID)

        createParticipants(quote)
        return shareQuote(quote)

    # IQuoteDatabase

    def getQuote(self, qid):
        return self.store.findUnique(Quote,
                                     Quote.qid == qid)

    def getRandomQuotes(self, limit=None):
        query = self.store.query(Quote,
                                 Quote.status != u'rejected')

        numQuotes = query.count()
        if limit is not None:
            numQuotes = min(numQuotes, limit)

        # XXX: This isn't very efficient.
        quoteIDs = list(query.getColumn('qid'))
        quoteIDs = random.sample(quoteIDs, numQuotes)
        return self.getQuotesByIDs(quoteIDs)

    # IQuoteSearcher

    def search(self, term, sort=None, limit=None):
        def getQuotes(results):
            quoteIDs = [r.uniqueIdentifier for r in results]
            return self.getQuotesByIDs(quoteIDs, sort)

        return self.searchIndexer.search(term, count=limit
            ).addCallback(getQuotes)

registerAttributeCopyingUpgrader(QuoteDB, 1, 2)
registerAttributeCopyingUpgrader(QuoteDB, 2, 3)


class Quote(Item):
    implements(IQuote, IQuoteModeration, IFulltextIndexable)

    typeName = 'flyingcircus_quote'
    schemaVersion = 2

    qid = integer(allowNone=False, indexed=True)
    content = text(allowNone=False)
    added = timestamp(allowNone=False, indexed=True, defaultFactory=lambda: Time())
    votesFor = integer(allowNone=False, default=0)
    votesAgainst = integer(allowNone=False, default=0)
    rating = integer(allowNone=False, default=0, indexed=True)
    votes = integer(allowNone=False, default=0)
    userID = text()
    status = text(allowNone=False, default=u'accepted')

    def __repr__(self):
        return '<%s qid=%s rating=%d votes=%d added=%r>' % (
            type(self).__name__,
            self.qid,
            self.rating,
            self.votes,
            self.added.asHumanly())

    def stored(self):
        # Tell the batch processor that we have data to index.
        qs = self.store.findUnique(QuoteSource)
        qs.itemAdded()

    # IQuote

    @transacted
    def plus(self):
        self.votesFor = self.votesFor + 1
        self.rating = self.rating + 1
        self.votes = self.votes + 1

    @transacted
    def minus(self):
        self.votesAgainst = self.votesAgainst + 1
        self.rating = self.rating - 1
        self.votes = self.votes + 1

    @transacted
    def requestModeration(self):
        self.status = u'moderate'

    def getParticipants(self):
        return self.store.query(Clown, Clown.quote == self).getColumn('nickname')

    def getUsername(self):
        userID = self.userID
        if userID is not None:
            return userID.split(u'@', 1)[0]
        return None

    # IQuoteModeration

    @transacted
    def reject(self):
        self.status = u'rejected'

    @transacted
    def accept(self):
        self.status = u'accepted'

    # IFulltextIndexable

    def uniqueIdentifier(self):
        return str(self.qid)

    def textParts(self):
        return [self.content]

registerAttributeCopyingUpgrader(Quote, 1, 2)


QuoteSource = batch.processor(Quote)


class Clown(Item):
    """
    A participant in a L{Quote}.
    """
    typeName = 'flyingcircus_clown'
    schemaVersion = 1

    quote = reference(doc="""
    L{Quote} this participant is involved in.
    """, allowNone=False, reftype=Quote, indexed=True)

    nickname = text(doc="""
    The participant's nickname.
    """, allowNone=False)


### Views

class RedirectorResource(object):
    implements(IResource)

    def __init__(self, redirector):
        pass

    def renderHTTP(self, ctx):
        # XXX: We should probably be generated a link from the appstore share
        # item itself.
        return url.root.child('FlyingCircus')

registerAdapter(RedirectorResource, Redirector, IResource)


class QuotesTab(LiveElement):
    """
    A simple container, intended for use with C{FragmentCollector}.
    """
    implements(INavigableFragment)

    docFactory = ThemedDocumentFactory('quotes-tab', 'resolver')

    def __init__(self, title, content, resolver):
        """
        Initialise the container.

        @type title: C{unicode}
        @param title: Title for the tab

        @type content: C{renderable}
        @param content: Renderable tab content

        @type resolver: C{ITemplateNameResolver}
        @param resolver: Template resolver
        """
        super(QuotesTab, self).__init__()
        self.title = title.encode('utf-8')
        self.content = content
        self.resolver = resolver

    @renderer
    def tabContent(self, req, tag):
        return tag[self.content]

    # INavigableFragment

    def head(self):
        return None


class TabView(FragmentCollector):
    docFactory = ThemedDocumentFactory('tab-view', 'resolver')

    title = None
    viewTitle = None

    def __init__(self, resolver, tabs):
        super(TabView, self).__init__(translator=resolver,
                                      collect=tabs)
        self.resolver = resolver

    def render_viewTitle(self, ctx, data):
        return ctx.tag[self.viewTitle]


class MyQuotesView(TabView):
    """
    Personalised quote view.
    """
    implements(INavigableFragment)

    title = u'Slipgate Quote Database - My Quotes'
    viewTitle = u'My Quotes'

    LIMIT = 5

    def __init__(self, myquotes):
        self.myquotes = myquotes
        self.store = self.myquotes.store

        self.resolver = ITemplateNameResolver(self.store.parent)

        quoteDB = getServiceProvider(self.store, IQuoteDatabase)
        userID = u'@'.join(userbase.getAccountNames(self.store).next())

        appStore = quoteDB.store

        role = getUserRole(appStore, userID)

        def pagerTab(title, status, allUsers=False, sortAscending=False):
            criteria = [Quote.status == status]
            if not allUsers:
                criteria.append(Quote.userID == userID)

            baseConstraint = AND(*criteria)
            pager = QuotePager(store=appStore,
                               resolver=self.resolver,
                               role=role,
                               attribute=Quote.added,
                               sortAscending=sortAscending,
                               limit=self.LIMIT,
                               baseConstraint=baseConstraint)
            return QuotesTab(title=title,
                             content=pager,
                             resolver=self.resolver)

        tabs = [
            pagerTab(u'Accepted', u'accepted'),
            pagerTab(u'Rejected', u'rejected'),
            ]

        if inModerators(role):
            tabs.append(pagerTab(u'Moderation', u'moderate', True, True))

        super(MyQuotesView, self).__init__(
            resolver=self.resolver,
            tabs=tabs)

registerAdapter(MyQuotesView, MyQuotes, INavigableFragment)


class QuoteDBView(LiveElement):
    """
    View for listing quotes from an L{IQuoteDatabase}.
    """
    implements(INavigableFragment)

    title = u'Slipgate Quote Database'
    docFactory = ThemedDocumentFactory('qdb-view', 'resolver')

    LIMIT = 15

    def __init__(self, quoteDB, userID=None):
        super(QuoteDBView, self).__init__()
        self.quoteDB = quoteDB
        self.appStore = itemFromProxy(self.quoteDB).store
        self.resolver = ITemplateNameResolver(self.appStore.parent)
        self.role = getUserRole(self.appStore, userID)

    @renderer
    def content(self, req, tag):
        def firstOr(seq, default=None):
            if not seq:
                return default
            return seq[0]

        args = req.args
        sortType = firstOr(args.get('sort'))
        sortAscending = firstOr(args.get('dir'), 'desc') != 'desc'
        limit = firstOr(args.get('limit'), self.LIMIT)
        limit = min(int(limit), self.LIMIT)

        baseConstraint = Quote.status != u'rejected'

        elem = None
        if sortType is not None:
            attribute = translateSortType(sortType, None)
            if attribute is not None:
                elem = QuotePager(store=self.appStore,
                                  resolver=self.resolver,
                                  role=self.role,
                                  attribute=attribute,
                                  sortAscending=sortAscending,
                                  limit=limit,
                                  baseConstraint=baseConstraint)
            elif sortType == 'random':
                query = self.quoteDB.getRandomQuotes(limit)
                quotes = list(self.role.asAccessibleTo(query))
                elem = QuoteList(quotes=quotes,
                                 resolver=self.resolver)
        else:
            elem = QuoteDatabaseOverview(store=self.appStore,
                                         role=self.role,
                                         resolver=self.resolver)

        elem.setFragmentParent(self)
        return tag[elem]

    # INavigableFragment

    def customizeFor(self, userID):
        return type(self)(self.quoteDB, userID)

registerAdapter(QuoteDBView, IQuoteDatabase, INavigableFragment)


class QuoteSearchView(LiveElement):
    """
    A view for performing quote searches.
    """
    implements(INavigableFragment)

    title = u'Slipgate Quote Database - Search'
    docFactory = ThemedDocumentFactory('qdb-search', 'resolver')
    jsClass = u'FlyingCircus.Search.QuoteSearch'

    LIMIT = 30

    def __init__(self, quoteDB, userID=None):
        super(QuoteSearchView, self).__init__()
        self.quoteDB = quoteDB
        self.userID = userID

        appStore = itemFromProxy(quoteDB).store
        self.resolver = ITemplateNameResolver(appStore.parent)
        self.role = getUserRole(appStore, self.userID)

    def performSearch(self, term, sortType, sortAscending):
        def makeResultList(query):
            quotes = self.role.asAccessibleTo(query)
            elem = QuoteList(quotes=quotes,
                             resolver=self.resolver)
            elem.setFragmentParent(self)
            return elem, query.count()

        sort = translateSortType(sortType, sortAscending)

        return self.quoteDB.search(term=term, sort=sort, limit=self.LIMIT
            ).addCallback(makeResultList)

    @renderer
    def searchForm(self, req, tag):
        model = Model(callback=self.performSearch,
                      params=[ValueParameter(name='term',
                                             doc=u'Terms',
                                             value=None),
                              ValueParameter(name='sortType',
                                             doc=u'Sort by',
                                             value=u'rating'),
                              ValueParameter(name='sortAscending',
                                             doc=u'Ascending?',
                                             value=False)],
                      doc=u'Search')
        form = LiveForm(store=None, model=model, fragmentParent=self)
        form.jsClass = u'FlyingCircus.Search.QuoteSearchForm'
        TextInput(parent=form, name='term')

        sortValues = [
            (u'rating', u'Rating'),
            (u'date',   u'Date')]
        SelectInput(parent=form, name='sortType', values=sortValues)
        CheckboxInput(parent=form, name='sortAscending')

        return tag[form]

    # INavigableFragment

    def customizeFor(self, userID):
        return type(self)(self.quoteDB, userID)

registerAdapter(QuoteSearchView, IQuoteSearcher, INavigableFragment)


class QuoteDatabaseOverview(LiveElement):
    """
    Overview for a quote database.
    """
    docFactory = ThemedDocumentFactory('qdb-overview', 'resolver')

    LIMIT = 2

    def __init__(self, store, role, resolver):
        super(QuoteDatabaseOverview, self).__init__()
        self.store = store
        self.role = role
        self.resolver = resolver

    def makePager(self, attribute):
        baseConstraint = Quote.status != u'rejected'
        elem = QuotePager(store=self.store,
                          resolver=self.resolver,
                          role=self.role,
                          attribute=attribute,
                          sortAscending=False,
                          limit=self.LIMIT,
                          baseConstraint=baseConstraint)
        elem.setFragmentParent(self)
        return elem

    @renderer
    def recent(self, req, tag):
        return tag[self.makePager(Quote.added)]

    @renderer
    def top(self, req, tag):
        return tag[self.makePager(Quote.rating)]


class QuoteAdderView(LiveElement):
    """
    View for L{IQuoteAdder}s.

    Providers users with an interface for submitting a new quote.
    """
    implements(INavigableFragment)

    title = u'Slipgate Quote Database - Add Quote'
    docFactory = ThemedDocumentFactory('add-quote', 'resolver')
    jsClass = u'FlyingCircus.Quotes.QuoteAdder'

    def __init__(self, quoteAdder):
        super(QuoteAdderView, self).__init__()
        self.quoteAdder = quoteAdder

        appStore = itemFromProxy(self.quoteAdder).store
        self.resolver = ITemplateNameResolver(appStore.parent)

    @expose
    def addQuote(self, content):
        quote = self.quoteAdder.addQuote(content, self.userID)
        url = linkTo(quote)
        return unicode(url)

    # INavigableFragment

    def customizeFor(self, userID):
        self.userID = userID
        return self

registerAdapter(QuoteAdderView, IQuoteAdder, INavigableFragment)


class QuoteView(LiveElement):
    """
    Navigable resource for a single L{IQuote}.

    Visiting the C{raw} child resource results in a stripped down quote.
    """
    implements(INavigableFragment)

    docFactory = ThemedDocumentFactory('qdb-view', 'resolver')

    def __init__(self, quote):
        super(QuoteView, self).__init__()
        self.quote = quote

        appStore = itemFromProxy(self.quote).store
        self.resolver = ITemplateNameResolver(appStore.parent)

    def locateChild(self, ctx, segments):
        if segments and segments[0] == 'raw':
            quote = self.quote
            lines = quote.content.splitlines()
            lines.insert(0, '#%s (%s/%s)' % (quote.qid, quote.rating, quote.votes))
            text = '\n'.join(lines)
            return static.Data(text.encode('utf-8'), 'text/plain; charset=UTF-8'), ()

        return rend.NotFound

    @property
    def title(self):
        return u'Slipgate Quote Database - #%s' % (self.quote.qid,)

    @renderer
    def content(self, req, tag):
        elem = QuoteElement(quote=self.quote,
                            resolver=self.resolver)
        elem.setFragmentParent(self)
        return tag[elem]

registerAdapter(QuoteView, IQuote, INavigableFragment)


class QuoteElement(LiveElement):
    """
    View for a single quote.

    An Athena interface for voting and retrieving the quote rating is exposed.

    If the quote has participant information, it will be used for highlighting.
    """
    docFactory = ThemedDocumentFactory('quote', 'resolver')
    jsClass = u'FlyingCircus.Quotes.Quote'

    _statusIndicators = {
        u'rejected': u'!',
        u'moderate': u'*'}

    def __init__(self, quote, resolver):
        """
        Initialise the element.

        @type quote: C{SharedProxy} for an L{IQuote} interface

        @type resolver: C{ITemplateNameResolver}
        """
        super(QuoteElement, self).__init__()
        self.quote = quote
        self.resolver = resolver

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

    def getStatusInfo(self):
        """
        Get information related to the current quote status.

        @rtype: C{(unicode, unicode)}
        @return: C{(statusIndicator, statusCSSClassName)}
        """
        status = self.quote.status
        return self._statusIndicators.get(status, u''), u'status-%s' % (status,)

    def colorizeContent(self):
        """
        Highlight nicknames in the quote content.

        @rtype: C{iterable} of renderable elements, each one representing
            a line from the quote content
        """
        def nickNode(nickname, color):
            style = 'color: #%x%x%x' % color
            return tags.span(style=style)[nickname]

        return (colorizeLine(nickNode, line, self.quote.getParticipants())
                for line in self.quote.content.strip().splitlines())

    @expose
    def getRating(self):
        return self.quote.rating, self.quote.votes

    @expose
    def plus(self):
        self.quote.plus()
        return self.getRating()

    @expose
    def minus(self):
        self.quote.minus()
        return self.getRating()

    @expose
    def moderate(self):
        self.quote.requestModeration()
        return self.getStatusInfo()

    @expose
    def reject(self):
        self.quote.reject()
        return self.getStatusInfo()

    @expose
    def accept(self):
        self.quote.accept()
        return self.getStatusInfo()

    @renderer
    def quoteInfo(self, req, tag):
        quote = self.quote
        tag.fillSlots('permalink', linkTo(quote))
        tag.fillSlots('qid', quote.qid)
        statusIndicator, statusClass = self.getStatusInfo()
        tag.fillSlots('statusClass', statusClass)
        tag.fillSlots('statusIndicator', statusIndicator)
        tag.fillSlots('created', quote.added.asHumanly())
        username = quote.getUsername()
        if username is not None:
            username = [u'by ', tags.strong[username]]
        else:
            username = []
        tag.fillSlots('user', username)

        return tag

    @renderer
    def quoteContent(self, req, tag):
        content = itertools.izip(self.colorizeContent(), itertools.repeat(tags.br))
        return tag[content]

    @renderer
    def moderation(self, req, tag):
        status = self.quote.status

        def buttons():
            if IQuoteModeration.providedBy(self.quote):
                if status != u'rejected':
                    yield tag.onePattern('rejectButton')
                if status == u'moderate':
                    yield tag.onePattern('acceptButton')
            elif status == u'accepted':
                yield tag.onePattern('moderateButton')

        return tag[buttons()]


class PagerInequalityModel(InequalityModel):
    """
    InequalityModel for paginated quote views.
    """
    def __init__(self, store, role, attribute, sortAscending, baseConstraint):
        super(PagerInequalityModel, self).__init__(store=store,
                                                   itemType=Quote,
                                                   baseConstraint=baseConstraint,
                                                   columns=[attribute],
                                                   defaultSortColumn=attribute,
                                                   defaultSortAscending=False)
        self.role = role
        self.sortAscending = sortAscending

    def constructRows(self, query):
        rows = list(self.role.asAccessibleTo(query))
        if not self.sortAscending:
            rows = list(reversed(rows))
        return rows


class QuotePager(LiveElement):
    """
    A paginated quote list view.
    """
    docFactory = ThemedDocumentFactory('quote-pager', 'resolver')
    jsClass = u'FlyingCircus.Quotes.QuotePager'

    def __init__(self, store, resolver, role, attribute, sortAscending, limit, baseConstraint=None):
        """
        Initialise the pager element.

        @type store: Axiom store
        @param store: Store to query for L{Quote}s in

        @type resolver: C{ITemplateNameResolver}

        @type role: C{xmantissa.sharing.Role}
        @param role: Role to use when exposing shared items to the view

        @type attribute: Axiom item attribute
        @param attribute: L{Quote} attribute to use for queries and sorting

        @type sortAscending: C{boolean}
        @param sortAscending: Determine whether to sort the query results
            ascending or not

        @type limit: C{int}
        @param limit: Number of items per page

        @type baseConstraint: Axiom query or C{None}
        @param baseConstraint: Base constraint to apply to queries or C{None}
        """
        super(QuotePager, self).__init__()
        self.store = store
        self.resolver = resolver
        self.attribute = attribute
        self.sortAscending = sortAscending
        self.limit = limit

        self.firstValue = self.lastValue = None
        self.imodel = PagerInequalityModel(store=self.store,
                                           role=role,
                                           attribute=self.attribute,
                                           sortAscending=sortAscending,
                                           baseConstraint=baseConstraint)

        getters = [self.imodel.rowsBeforeItem, self.imodel.rowsAfterItem]
        getNext, getPrev = getters[sortAscending], getters[not sortAscending]

        self.getNext = lambda: getNext(self.lastValue, self.limit)
        self.getPrev = lambda: getPrev(self.firstValue, self.limit)

    def makeQuoteList(self, quotes, allowEmpty=False):
        """
        Create a L{QuoteList} widget.

        @type quotes: C{sequence} of C{SharedProxy}s implementing L{IQuote}

        @param allowEmpty: Create an empty L{QuoteList} if L{quotes} is empty
        """
        if not quotes and not allowEmpty:
            return None

        if quotes:
            self.firstValue = itemFromProxy(quotes[0])
            self.lastValue = itemFromProxy(quotes[-1])

        elem = QuoteList(quotes=quotes,
                         resolver=self.resolver)
        elem.setFragmentParent(self)
        return elem

    @renderer
    def quoteList(self, req, tag):
        if self.sortAscending:
            quotes = self.imodel.rowsAfterValue(None, self.limit)
        else:
            quotes = self.imodel.rowsBeforeValue(None, self.limit)
        return tag[self.makeQuoteList(quotes, True)]

    @expose
    def nextPage(self):
        """
        Retrieve a widget containing the next page of quotes.
        """
        quotes = []
        if self.lastValue is not None:
            quotes = self.getNext()
        return self.makeQuoteList(quotes)

    @expose
    def prevPage(self):
        """
        Retrieve a widget containing the previous page of quotes.
        """
        quotes = []
        if self.firstValue is not None:
            quotes = self.getPrev()
        return self.makeQuoteList(quotes)


class QuoteList(LiveElement):
    """
    View for multiple quotes.

    Quotes will be zebra-striped and if there are no quotes an "empty" view
    will be rendered.
    """
    docFactory = ThemedDocumentFactory('quote-list', 'resolver')

    def __init__(self, quotes, resolver):
        """
        Initialise quote list element.

        @type quotes: C{iterable} of C{SharedProxy}s for L{IQuote}
        """
        super(QuoteList, self).__init__()
        self.quotes = list(quotes)
        self.resolver = resolver

    @renderer
    def quoteList(self, req, tag):
        if not self.quotes:
            return tag[tag.onePattern('empty')]

        oddeven = itertools.cycle(['even', 'odd'])

        def content():
            for quote, cls in itertools.izip(self.quotes, oddeven):
                elem = QuoteElement(quote=quote,
                                    resolver=self.resolver)
                elem.setFragmentParent(self)
                yield tags.div(class_=cls)[elem]

        return tag[content()]


class VHost(Item, PrefixURLMixin):
    implements(ISessionlessSiteRootPlugin)

    typeName = 'flyingcircus_vhost'
    schemaVersion = 1

    sessionless = True

    prefixURL = text(default=u'vhost')

    def createResource(self):
        return VHostMonsterResource()


_nicknamePatterns = [
    re.compile(r'<[ +%@&~]?(.*?) ?> '),  # Addressing: < foo> hi  <@bar> hi there
    re.compile(r'^\s*\* (.*?) '),        # Actions: * foo does a thing
    ]

def extractParticipants(content):
    """
    Extract a list of unique nicknames that participate in a quote.

    @param content: Text representation of a quote's content
    @type content: C{unicode}

    @rtype: C{tuple} of C{unicode}
    @return: The featured nicknames
    """
    nicknames = set()

    for line in content.splitlines():
        for pattern in _nicknamePatterns:
            match = pattern.search(line)
            if match is not None and match.group(1):
                nicknames.add(match.group(1))
                break

    return tuple(nicknames)


def shareQuote(quote):
    """
    Share a L{Quote} item.
    """
    shareID = unicode(quote.qid)

    sharedQuote = getEveryoneRole(quote.store).shareItem(
        sharedItem=quote,
        shareID=shareID,
        interfaces=[IQuote])

    modRole = getModeratorsRole(quote.store)
    modRole.shareItem(
        sharedItem=quote,
        shareID=shareID,
        interfaces=[IQuoteModeration])

    return sharedQuote


@transacted
def createParticipants(quote):
    """
    Create L{Clown} items for each participant in C{quote}.

    @type quote: L{Quote}
    """
    store = quote.store
    store.query(Clown, Clown.quote == quote).deleteFromStore();

    for nickname in extractParticipants(quote.content):
        Clown(store=store,
              quote=quote,
              nickname=nickname)


_sortTypes = {
    'date':   Quote.added,
    'rating': Quote.rating}

def translateSortType(sortType, sortAscending):
    attribute = _sortTypes.get(sortType)
    if attribute is not None and sortAscending is not None:
        attribute = getattr(attribute, ['descending', 'ascending'][sortAscending])
    return attribute
