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

import re
from itertools import islice
from string import uppercase

from zope.interface import implements

from twisted.python import components

from nevow import rend, athena, inevow, static, url
from nevow.flat import flatten
from nevow.athena import expose

from epsilon import extime

from axiom import item, attributes
from axiom.dependency import dependsOn
from axiom.attributes import AND
from axiom.upgrade import registerUpgrader, registerAttributeCopyingUpgrader


from xmantissa import ixmantissa, webnav, webtheme, liveform
from xmantissa.webapp import PrivateApplication
from xmantissa.tdbview import TabularDataView, ColumnViewBase
from xmantissa.tdb import TabularDataModel
from xmantissa.scrolltable import ScrollingFragment, UnsortableColumn
from xmantissa.fragmentutils import dictFillSlots

try:
    from PIL import Image
except ImportError:
    Image = None

def makeThumbnail(infile, outfile, thumbSize, format='jpeg'):
    assert Image is not None, 'you need PIL installed if you want to thumbnail things'
    image = Image.open(infile)
    (width, height) = image.size
    scale = float(thumbSize) / max(max(width, height), thumbSize)
    image.resize((int(width * scale),
                  int(height * scale)), Image.ANTIALIAS).save(outfile, format)

class PeopleBenefactor(item.Item):
    implements(ixmantissa.IBenefactor)
    endowed = attributes.integer(default=0)
    powerupNames = ["xmantissa.people.AddPerson"]

class Person(item.Item):
    typeName = 'mantissa_person'
    schemaVersion = 1

    organizer = attributes.reference(
        "The L{Organizer} to which this Person belongs.")
    # we don't really use Person.name for anything -
    # it seems like a bit of a strange thing to have
    # in addition to a RealName
    name = attributes.text(
        "This name of this person.")
    created = attributes.timestamp()

    def __init__(self, **kw):
        kw['created'] = extime.Time()
        super(Person, self).__init__(**kw)

    def getDisplayName(self):
        # XXX figure out the default
        for rn in self.store.query(RealName, RealName.person == self):
            return rn.display
        return self.name

    def getEmailAddresses(self):
        """
        Return an iterator of all email addresses associated with this person.

        @return: an iterator of unicode strings in RFC2822 address format.
        """
        return self.store.query(
            EmailAddress,
            EmailAddress.person == self).getColumn('address')

    def getEmailAddress(self):
        """
        Return the default email address associated with this person.

        Note: this is effectively random right now if a person has more than
        one address.  It's just the first address returned.  This should be
        fixed in a future version.

        @return: a unicode string in RFC2822 address format.
        """
        for a in self.getEmailAddresses():
            return a

    def registerExtract(self, extract, timestamp=None):
        """
        @param extract: some Item that implements L{inevow.IRenderer}
        """
        if timestamp is None:
            timestamp = extime.Time()

        return ExtractWrapper(store=self.store,
                              extract=extract,
                              timestamp=timestamp,
                              person=self)

    def getExtractWrappers(self, n):
        return self.store.query(ExtractWrapper,
                                ExtractWrapper.person == self,
                                sort=ExtractWrapper.timestamp.desc,
                                limit=n)

class ExtractWrapper(item.Item):
    extract = attributes.reference(whenDeleted=attributes.reference.CASCADE)
    timestamp = attributes.timestamp(indexed=True)
    person = attributes.reference(reftype=Person,
                                  whenDeleted=attributes.reference.CASCADE)



class Organizer(item.Item):
    """
    Oversee the creation, location, destruction, and modification of
    people in a particular set (eg, the set of people you know).
    """
    implements(ixmantissa.INavigableElement)

    typeName = 'mantissa_people'
    schemaVersion = 2

    _webTranslator = dependsOn(PrivateApplication)

    powerupInterfaces = (ixmantissa.INavigableElement,)

    def personByName(self, name):
        """
        Retrieve the L{Person} item for the given Q2Q address,
        creating it first if necessary.

        @type name: C{unicode}
        """
        return self.store.findOrCreate(Person, organizer=self, name=name)

    def personByEmailAddress(self, address):
        """
        Retrieve the L{Person} item for the given email address
        (or return None if no such person exists)

        @type name: C{unicode}
        """
        email = self.store.findUnique(EmailAddress,
                                      EmailAddress.address == address,
                                      default=None)
        if email is not None:
            return email.person


    def lastNamesBetweenComparison(self, begin, end):
        """
        Return an IComparison which will restrict a query for Person items to
        those with last names which compare greater than or equal to begin and
        less than end.
        """
        return AND(
            RealName.person == Person.storeID,
            RealName.last >= begin,
            RealName.last < end)


    def lastNameOrder(self):
        """
        Return an IAttribute to sort people by their last name.
        """
        return RealName.last


    def peoplePlugins(self, person):
        return (
            p.personalize(person)
            for p
            in self.powerupsFor(ixmantissa.IOrganizerPlugin))

    def linkToPerson(self, person):
        """
        @param person: L{Person} instance
        @return: string url at which C{person} will be rendered
        """
        return (self._webTranslator.linkTo(self.storeID) +
                '/' + self._webTranslator.toWebID(person))

    def getTabs(self):
        ourURL = self._webTranslator.linkTo(self.storeID)
        children = [webnav.Tab('All', self.storeID, 0.1)]

        letters = iter(uppercase)
        while True:
            chars = ''.join(islice(letters, 3))
            if 0 == len(chars):
                break

            linkURL = ourURL + '?show-group=' + chars
            children.append(webnav.Tab(chars, None, 0.0, linkURL=linkURL))

        return [webnav.Tab('People', self.storeID, 0.5,
                           authoritative=True, children=children)]

def organizer1to2(old):
    o = old.upgradeVersion(old.typeName, 1, 2)
    o._webTranslator = old.store.findOrCreate(PrivateApplication)
    return o

registerUpgrader(organizer1to2, Organizer.typeName, 1, 2)

class PersonNameColumn(UnsortableColumn):
    def extractValue(self, model, item):
        return item.getDisplayName()

class OrganizerFragment(athena.LiveFragment, rend.ChildLookupMixin):
    fragmentName = 'people-organizer'
    live = 'athena'
    title = 'People'
    jsClass = u'Mantissa.People.Organizer'

    def __init__(self, original):
        self.wt = original._webTranslator
        athena.LiveFragment.__init__(self, original)

    def _createPeopleScrollTable(self, baseComparison, sort):
        f = ScrollingFragment(
                self.original.store,
                Person,
                baseComparison,
                [PersonNameColumn(None, 'name'),
                 Person.created],
                defaultSortColumn=sort)
        # use linkToPerson() to make item links so that rows point to
        # a version of the person URL that'll highlight the people tab
        f.linkToItem = lambda item: unicode(self.original.linkToPerson(item), 'ascii')
        f.setFragmentParent(self)
        f.docFactory = webtheme.getLoader(f.fragmentName)
        return f

    def _getBaseComparison(self, ctx):
        req = inevow.IRequest(ctx)
        group = req.args.get('show-group', [''])[0].decode('ascii', 'replace')
        if group:
            # We assume the only groups being shown consist of consecutive
            # letters.  If someone gives us something else, too bad for them.
            return self.original.lastNamesBetweenComparison(
                group[0],
                unichr(ord(group[-1]) + 1))
        return RealName.person == Person.storeID

    def render_peopleTable(self, ctx, data):
        comparison = self._getBaseComparison(ctx)
        sort = self.original.lastNameOrder()
        return self._createPeopleScrollTable(comparison, sort)

    def head(self):
        return None

components.registerAdapter(OrganizerFragment, Organizer, ixmantissa.INavigableFragment)

class _AddressBook(item.Item):
    typeName = 'mantissa_organizer_personalized_addressbook'
    schemaVersion = 1

    person = attributes.reference(doc="""
    A reference to the Mantissa Person Item to which this pertains.
    """)

    displayName = attributes.text(doc="""
    Short string displayed in the user interface whenever a name for
    this person is called for.
    """)

class AddressBookFragment(athena.LiveFragment):
    def __init__(self, original):
        athena.LiveFragment.__init__(self, original)
        self.docFactory = webtheme.getLoader('address-book')

    def data_names(self, ctx, data):
        ab = self.original
        s = ab.store
        return (
            {"first-name": p.first, "last-name": p.last}
            for p
            in s.query(RealName, RealName.person == ab.person))

    def addName(self, name):
        parts = name.rsplit(None, 1)
        if len(parts) == 1:
            first, last = u'', parts[0]
        else:
            first, last = parts
        RealName(
            store=self.original.store,
            person=self.original.person,
            first=first,
            last=last)
    expose(addName)


    def head(self):
        return None

components.registerAdapter(AddressBookFragment, _AddressBook, ixmantissa.INavigableFragment)

class RealName(item.Item):
    typeName = 'mantissa_organizer_addressbook_realname'
    schemaVersion = 1

    person = attributes.reference(allowNone=False)

    first = attributes.text()
    last = attributes.text(indexed=True)

    def _getDisplay(self):
        return ' '.join(filter(None, (self.first, self.last)))
    display = property(_getDisplay)

class EmailAddress(item.Item):
    typeName = 'mantissa_organizer_addressbook_emailaddress'
    schemaVersion = 3

    address = attributes.text(allowNone=False)
    person = attributes.reference(allowNone=False)
    label = attributes.text(
        """
        This is a label for the role of the email address, usually something like
        "home", "work", "school".
        """,
        allowNone=False,
        default=u'')

def emailAddress1to2(old):
    return old.upgradeVersion('mantissa_organizer_addressbook_emailaddress',
                              1, 2,
                              address=old.address,
                              person=old.person)

registerUpgrader(emailAddress1to2,
                 'mantissa_organizer_addressbook_emailaddress',
                 1, 2)

item.declareLegacyItem(EmailAddress.typeName, 2, dict(
                  address = attributes.text(allowNone=False),
                  person = attributes.reference(allowNone=False)))

registerAttributeCopyingUpgrader(EmailAddress, 2, 3)


class PhoneNumber(item.Item):
    typeName = 'mantissa_organizer_addressbook_phonenumber'
    schemaVersion = 3

    number = attributes.text(allowNone=False)
    person = attributes.reference(allowNone=False)
    label = attributes.text(
        """
        This is a label for the role of the phone number, usually something like
        "home", "office", "mobile".
        """,
        allowNone=False,
        default=u'',)

def phoneNumber1to2(old):
    return old.upgradeVersion('mantissa_organizer_addressbook_phonenumber',
                              1, 2,
                              number=old.number,
                              person=old.person)

item.declareLegacyItem(PhoneNumber.typeName, 2, dict(
                  number = attributes.text(allowNone=False),
                  person = attributes.reference(allowNone=False)))

registerUpgrader(phoneNumber1to2,
                 'mantissa_organizer_addressbook_phonenumber',
                 1, 2)

registerAttributeCopyingUpgrader(PhoneNumber, 2, 3)

class PostalAddress(item.Item):
    typeName = 'mantissa_organizer_addressbook_postaladdress'

    address = attributes.text(allowNone=False)
    person = attributes.reference(allowNone=False)

class Notes(item.Item):
    typeName = 'mantissa_organizer_addressbook_notes'

    notes = attributes.text(allowNone=False)
    person = attributes.reference(allowNone=False)


class AddPerson(item.Item):
    implements(ixmantissa.INavigableElement)

    typeName = 'mantissa_add_person'
    schemaVersion = 2

    powerupInterfaces = ixmantissa.INavigableElement
    organizer = dependsOn(Organizer)

    def getTabs(self):
        return [webnav.Tab('People', self.storeID, 0.0, children=[
                    webnav.Tab('Add Person', self.storeID, 0.2)],
                           authoritative=False)]

def addPerson1to2(old):
    ap = old.upgradeVersion(old.typeName, 1, 2)
    ap.organizer = old.store.findOrCreate(Organizer)
    return ap

registerUpgrader(addPerson1to2, AddPerson.typeName, 1, 2)

def _hasLengthOrNone(s):
    WHITESPACE = re.compile(r'\s{2.}')

    s = s.strip()
    s = WHITESPACE.sub(' ', s)
    if 0 == len(s):
        return None
    return s

class AddPersonFragment(athena.LiveFragment):
    fragmentName = 'add-person'
    live = 'athena'
    
    def render_addPersonForm(self, ctx, data):
        def makeParam(name, desc, coerce=_hasLengthOrNone):
            return liveform.Parameter(name, liveform.TEXT_INPUT, coerce, desc)

        addPersonForm = liveform.LiveForm(
            self.addPerson,
            (makeParam('firstname', 'First Name'),
             makeParam('lastname', 'Last Name'),
             makeParam('email', 'Email Address'),
             makeParam('nickname', 'Nickname')),
             description='Add Person')
        addPersonForm.docFactory = webtheme.getLoader('liveform-compact')
        addPersonForm.setFragmentParent(self)
        return addPersonForm

    def head(self):
        return None

    def makePerson(self, nickname):
        return Person(store=self.original.store,
                      created=extime.Time(),
                      organizer=self.original.store.findUnique(Organizer),
                      name=nickname)

    def addPerson(self, nickname, firstname, lastname, email):
        if not (nickname or firstname or lastname):
            raise ValueError('please supply nickname or first/last name')
        store = self.original.store
        address = store.findFirst(EmailAddress, EmailAddress.address == email)
        if address is not None:
            raise ValueError('There is already a person with that email '
                             'address (%s): ' % (address.person.name,))

        if nickname is None:
            nickname = u''
        person = self.makePerson(nickname)

        if email:
            EmailAddress(store=store,
                         address=email,
                         person=person)

        if firstname is not None or lastname is not None:
            RealName(store=store,
                     person=person,
                     first=firstname,
                     last=lastname)
        return u'Made A Person!'
    expose(addPerson)

components.registerAdapter(AddPersonFragment, AddPerson, ixmantissa.INavigableFragment)

class AddressBook(item.Item):
    implements(ixmantissa.IOrganizerPlugin)

    typeName = 'mantissa_organizer_addressbook'
    schemaVersion = 1

    installedOn = attributes.reference(doc="""
    The Organizer on which this is installed.
    """)

    powerupInterfaces = (ixmantissa.IOrganizerPlugin,)

    def personalize(self, person):
        return self.store.findOrCreate(
            _AddressBook,
            person=person)

class PersonExtractFragment(TabularDataView):
    def render_navigation(self, ctx, data):
        return inevow.IQ(
                webtheme.getLoader('person-extracts')).onePattern('navigation')

class ExtractWrapperColumnView(ColumnViewBase):
    def stanFromValue(self, idx, item, value):
        return inevow.IRenderer(item.extract)

class MugshotUploadPage(rend.Page):
    def __init__(self, cbGotFile, redirectTo):
        self._cbGotFile = cbGotFile
        self._redirectTo = redirectTo
        rend.Page.__init__(self)

    def renderHTTP(self, ctx):
        req = inevow.IRequest(ctx)
        if req.method == 'POST':
            udata = req.fields['uploaddata']
            self._cbGotFile(udata.type, udata.file)
            req.redirect(url.URL.fromString(self._redirectTo))
            return ''
        else:
            return rend.Page.renderHTTP(self, ctx)

class Mugshot(item.Item):
    """
    An image that is associated with a person
    """
    schemaVersion = 2

    type = attributes.text(doc="""
    Content-type of image data
    """, allowNone=False)

    body = attributes.path(doc="""
    Path to image data
    """, allowNone=False)

    smallerBody = attributes.path(doc="""
    Path to smaller version of image data
    """, allowNone=False)

    person = attributes.reference(doc="""
    L{Person} this mugshot is of
    """, allowNone=False)

    size = 120
    smallerSize = 22

    def fromFile(cls, person, infile, format):
        """
        Create a Mugshot item from an image file.

        @param person: L{Person} instance (who the mugshot is of)
        @param infile: C{file} (where the image data is)
        @param format: C{unicode} (what format the image data is in)

        @return: L{Mugshot} instance, in the same store as C{person}
        """

        inst = person.store.findUnique(cls, cls.person == person, default=None)

        body = cls.makeThumbnail(infile, person, format)
        infile.seek(0)
        smallerBody = cls.makeThumbnail(infile, person, format, smaller=True)

        ctype = 'image/' + format

        if inst is None:
            inst = cls(store=person.store,
                       person=person,
                       type=ctype,
                       body=body,
                       smallerBody=smallerBody)
        else:
            inst.body = body
            inst.smallerBody = smallerBody
            inst.type = ctype

        return inst
    fromFile = classmethod(fromFile)

    def makeThumbnail(cls, infile, person, ctype, smaller=False):
        """
        Make a thumbnail of an image and store it on disk.

        @param infile: C{file} (where the image data is)
        @param person: L{Person} instance (who is this image of)
        @param ctype: content-type of data in C{infile}
        @param smaller: thumbnails are available in two sizes.
                        if C{smaller} is true, then the thumbnail
                        will be in the smaller of the two sizes.

        @return: filesystem path of the new thumbnail
        """


        dirsegs = ['mugshots', str(person.storeID)]

        if smaller:
            dirsegs.insert(1, 'smaller')
            size = cls.smallerSize
        else:
            size = cls.size

        outfile = person.store.newFile(*dirsegs)
        makeThumbnail(infile, outfile, size, ctype)
        outfile.close()
        return outfile.finalpath
    makeThumbnail = classmethod(makeThumbnail)


def mugshot1to2(old):
    smallerBody = Mugshot.makeThumbnail(old.body.open(),
                                        old.person,
                                        old.type.split('/')[1],
                                        smaller=True)

    return old.upgradeVersion(Mugshot.typeName, 1, 2,
                              type=old.type,
                              body=old.body,
                              person=old.person,
                              smallerBody=smallerBody)


registerUpgrader(mugshot1to2, Mugshot.typeName, 1, 2)

class MugshotResource(rend.Page):
    """
    Web accessible resource that serves Mugshot images. Serves
    a smaller mugshot if the final path segment is "smaller"
    """
    smaller = False

    def __init__(self, mugshot):
        """
        @param mugshot: L{Mugshot}
        """
        self.mugshot = mugshot
        rend.Page.__init__(self)

    def locateChild(self, ctx, segments):
        if segments == ('smaller',):
            self.smaller = True
            return (self, ())
        return rend.NotFound

    def renderHTTP(self, ctx):
        if self.smaller:
            path = self.mugshot.smallerBody
        else:
            path = self.mugshot.body

        return static.File(path.path, str(self.mugshot.type))

class PersonDetailFragment(athena.LiveFragment, rend.ChildLookupMixin):
    fragmentName = 'person-detail'
    live = 'athena'
    jsClass = u'Mantissa.People.PersonDetail'

    def __init__(self, person):
        athena.LiveFragment.__init__(self, person)
        self.person = person
        self.organizer = person.organizer
        self.title = person.getDisplayName()
        self.email = person.getEmailAddress()
        if self.email is not None:
            self.title += ' (%s)' % (self.email,)

        self.personFragments = list(ixmantissa.IPersonFragment(p)
                                        for p in self.organizer.peoplePlugins(person))

        self.myURL = self.organizer.linkToPerson(person)

    def _gotMugshotFile(self, ctype, infile):
        (majortype, minortype) = ctype.split('/')

        if majortype == 'image':
            Mugshot.fromFile(self.person, infile, unicode(minortype, 'ascii'))

    def child_uploadMugshot(self, ctx):
        return MugshotUploadPage(self._gotMugshotFile, self.myURL)

    def child_mugshot(self, ctx):
        return MugshotResource(
                    self.person.store.findUnique(
                        Mugshot, Mugshot.person == self.person))

    def render_mugshotLink(self, ctx, data):
        self.mugshot = self.person.store.findUnique(
                            Mugshot, Mugshot.person == self.person, default=None)
        if self.mugshot is None:
            return '/Mantissa/images/mugshot-placeholder.png'
        return self.myURL + '/mugshot'

    def render_mugshotFormAction(self, ctx, data):
        return self.myURL + '/uploadMugshot'

    def editContactInfoItem(self, typeName, oldValue, newValue):
        for (cls, attr) in self.contactInfoItemTypes:
            if typeName == cls.__name__:
                item = self.person.store.findFirst(cls,
                            attributes.AND(
                                getattr(cls, attr) == oldValue,
                                cls.person == self.person))
                setattr(item, attr, newValue)
                break
    expose(editContactInfoItem)

    def createContactInfoItem(self, typeName, value):
        for (cls, attr) in self.contactInfoItemTypes:
            if typeName == cls.__name__:
                cls(person=self.person,
                    store=self.person.store,
                    **{attr: value})
                p = inevow.IQ(self.docFactory).onePattern('contact-info-item')
                return unicode(flatten(p.fillSlots('value', value)), 'utf-8')
    expose(createContactInfoItem)

    def deleteContactInfoItem(self, typeName, value):
        for (cls, attr) in self.contactInfoItemTypes:
            if typeName == cls.__name__:
                self.person.store.findFirst(cls,
                        attributes.AND(
                            getattr(cls, attr) == value,
                            cls.person == self.person)).deleteFromStore()
                break
    expose(deleteContactInfoItem)

    def head(self):
        return None

    def render_personName(self, ctx, data):
        return ctx.tag[self.person.getDisplayName()]

    contactInfoItemTypes = ((PhoneNumber, 'number'),
                            (EmailAddress, 'address'),
                            (PostalAddress, 'address'),
                            (Notes, 'notes')
                            )

    def render_contactInfoSummary(self, ctx, data):
        iq = inevow.IQ(self.docFactory)
        itemPattern = iq.patternGenerator('contact-info-item')
        sectionPattern = iq.patternGenerator('contact-info-section')
        sections = []

        return ctx.tag.fillSlots('sections',
            (dictFillSlots(sectionPattern,
                           {'type': itemType.__name__,
                            'icon-path': '/Mantissa/images/' + itemType.__name__ + '-icon.png',
                            'items': (itemPattern.fillSlots('value', value)
                                         for value in self.person.store.query(
                                             itemType, itemType.person == self.person).getColumn(valueColumn))})
                for (itemType, valueColumn) in self.contactInfoItemTypes))

    def render_extracts(self, ctx, data):
        tdm = TabularDataModel(
                self.person.store,
                ExtractWrapper,
                (ExtractWrapper.timestamp,),
                itemsPerPage=10,
                defaultSortAscending=False)

        f = PersonExtractFragment(tdm, (ExtractWrapperColumnView('extract'),))
        f.docFactory = webtheme.getLoader(f.fragmentName)
        f.setFragmentParent(self)
        return f

    def render_organizerPlugins(self, ctx, data):
        pat = inevow.IQ(self.docFactory).patternGenerator('person-fragment')
        for f in self.personFragments:
            if hasattr(f, 'setFragmentParent'):
                f.setFragmentParent(self)
            yield dictFillSlots(pat,
                                dict(title=f.title,
                                     fragment=f))

components.registerAdapter(PersonDetailFragment, Person, ixmantissa.INavigableFragment)

class PersonFragment(rend.Fragment):
    def __init__(self, person, contactMethod=None):
        rend.Fragment.__init__(self, person,
                               webtheme.getLoader('person-fragment'))
        self.person = person
        self.contactMethod = contactMethod

    def render_person(self, ctx, data):
        detailURL = self.person.organizer.linkToPerson(self.person)

        mugshot = self.person.store.findUnique(Mugshot,
                                               Mugshot.person == self.person,
                                               default=None)
        if mugshot is None:
            mugshotURL = '/Mantissa/images/smaller-mugshot-placeholder.png'
        else:
            mugshotURL = detailURL + '/mugshot/smaller'

        name = self.person.getDisplayName()
        return dictFillSlots(ctx.tag, {'name': name,
                                       'detail-url': detailURL,
                                       'contact-method': self.contactMethod or name,
                                       'mugshot-url': mugshotURL})


syntax highlighted by Code2HTML, v. 0.9.1