# -*- test-case-name: xquotient.test.test_grabber -*-

from epsilon import hotfix
hotfix.require('twisted', 'deferredgenerator_tfailure')

import time, datetime

from twisted.mail import pop3, pop3client
from twisted.internet import protocol, defer, ssl, error
from twisted.python import log, components, failure
from twisted.protocols import policies

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

from epsilon import descriptor, extime

from axiom import item, attributes, iaxiom
from axiom.dependency import dependsOn
from axiom.upgrade import registerUpgrader

from xmantissa import ixmantissa,  webtheme, liveform
from xmantissa.webapp import PrivateApplication

from xmantissa.scrolltable import ScrollingFragment, AttributeColumn, TYPE_FRAGMENT
from xmantissa.stats import BandwidthMeasuringFactory

from xquotient.mail import DeliveryAgent


PROTOCOL_LOGGING = True


class Status(item.Item):
    """
    Represents the latest status of a particular grabber.
    """

    when = attributes.timestamp(doc="""
    Time at which this status was set.
    """)

    message = attributes.text(doc="""
    A short string describing the current state of the grabber.
    """)

    success = attributes.boolean(doc="""
    Flag indicating whether this status indicates a successful action
    or not.
    """)

    changeObservers = attributes.inmemory(doc="""
    List of single-argument callables which will be invoked each time
    this status changes.
    """)


    def __repr__(self):
        return '<Status %r>' % (self.message,)


    def activate(self):
        self.changeObservers = []
        self.message = u"idle"


    def addChangeObserver(self, observer):
        self.changeObservers.append(observer)
        return lambda: self.changeObservers.remove(observer)


    def setStatus(self, message, success=True):
        self.when = extime.Time()
        self.message = message
        self.success = success
        for L in self.changeObservers:
            try:
                L(message)
            except:
                log.err(None, "Failure in status update")



class GrabberBenefactor(item.Item):
    """
    Installs a GrabberConfiguration (and any requisite website
    powerups) on avatars.
    """

    endowed = attributes.integer(doc="""
    The number of avatars who have been endowed by this benefactor.
    """, default=0)
    powerupNames = ["xquotient.grabber.GrabberConfiguration"]


class GrabberConfiguration(item.Item):
    """
    Manages the creation, operation, and destruction of grabbers
    (items which retrieve information from remote sources).
    """
    schemaVersion = 3

    paused = attributes.boolean(doc="""
    Flag indicating whether grabbers created by this Item will be
    allowed to run.
    """, default=False)

    privateApplication = dependsOn(PrivateApplication)
    deliveryAgent = dependsOn(DeliveryAgent)

    def addGrabber(self, username, password, domain, ssl):
        # DO IT
        if ssl:
            port = 995
        else:
            port = 110

        pg = POP3Grabber(
            store=self.store,
            username=username,
            password=password,
            domain=domain,
            port=port,
            config=self,
            ssl=ssl)
        # DO IT *NOW*
        iaxiom.IScheduler(self.store).schedule(pg, extime.Time())
        # OR MAYBE A LITTLE LATER

item.declareLegacyItem(GrabberConfiguration.typeName, 1, dict(
    paused=attributes.boolean(default=False),
    installedOn=attributes.reference()))

def _grabberConfiguration1to2(old):
    new = old.upgradeVersion(GrabberConfiguration.typeName, 1, 2,
                             paused=old.paused,
                             privateApplication = old.store.findOrCreate(PrivateApplication),
                             deliveryAgent = old.store.findOrCreate(DeliveryAgent))
    return new
registerUpgrader(_grabberConfiguration1to2, GrabberConfiguration.typeName, 1, 2)

item.declareLegacyItem(GrabberConfiguration.typeName, 2, dict(
    paused=attributes.boolean(default=False),
    scheduler=attributes.reference(),
    privateApplication=attributes.reference(),
    deliveryAgent=attributes.reference(),
    ))


def _grabberConfiguration2to3(old):
    """
    Copy all the remaining attributes.
    """
    new = old.upgradeVersion(GrabberConfiguration.typeName, 2, 3,
                             paused=old.paused,
                             privateApplication = old.store.findOrCreate(PrivateApplication),
                             deliveryAgent = old.store.findOrCreate(DeliveryAgent))
    return new
registerUpgrader(_grabberConfiguration2to3, GrabberConfiguration.typeName, 2, 3)


class POP3UID(item.Item):
    grabberID = attributes.text(doc="""
    A string identifying the email-address/port parts of a
    configured grabber
    """, indexed=True)

    value = attributes.bytes(doc="""
    A POP3 UID which has already been retrieved.
    """, indexed=True)

    failed = attributes.boolean(doc="""
    When set, indicates that an attempt was made to retrieve this UID,
    but for some reason was unsuccessful.
    """, indexed=True, default=False)



class POP3Grabber(item.Item):
    """
    Item for retrieving email messages from a remote POP server.
    """

    config = attributes.reference(doc="""
    The L{GrabberConfiguration} which created this grabber.
    """)

    status = attributes.reference(doc="""
    The current state of this grabber.  This indicates whether a grab
    is currently being run, if a password is incorrect, etc.
    """)

    paused = attributes.boolean(doc="""
    Flag indicating whether this particular grabber will try to get
    scheduled to retrieve messages.
    """, default=False)

    username = attributes.text(doc="""
    Username in the remote system with which to authenticate.
    """, allowNone=False)

    password = attributes.text(doc="""
    Password in the remote system with which to authenticate.
    """, allowNone=False)

    domain = attributes.text(doc="""
    The address of the remote system to which to connect.
    """, allowNone=False)

    port = attributes.integer(doc="""
    TCP port number on the remote system to which to connect.
    """, default=110)

    ssl = attributes.boolean(doc="""
    Flag indicating whether to connect using SSL (note: this does not
    affect whether TLS will be negotiated post-connection.)
    """, default=False)

    messageCount = attributes.integer(doc="""
    The number of messages which have been retrieved by this grabber.
    """, default=0)

    running = attributes.inmemory(doc="""
    Flag indicating whether an attempt to retrieve messages is
    currently in progress.  Only one attempt is allowed outstanding at
    any given time.
    """)

    protocol = attributes.inmemory(doc="""
    While self.running=True this attribute will point to the
    ControlledPOP3GrabberProtocol that is grabbing stuff
    for me""")

    connector = attributes.inmemory(doc="""
    implementor of L{twisted.internet.interfaces.IConnector}, representing
    our connection to the POP server
    """)

    scheduled = attributes.timestamp(doc="""
    When this grabber is next scheduled to run.
    """)

    debug = attributes.boolean(doc="""
    Flag indicating whether to log traffic from this grabber or not.
    """, default=False)

    created = attributes.timestamp(doc="""
    Creation time of this grabber.  Used when deciding whether a grabbed
    message is old enough to automatically archive.
    """)

    _pop3uids = attributes.inmemory(doc="""
    A set of strings representing all the POP3 UIDs which have already been
    downloaded by this grabber.
    """)


    class installedOn(descriptor.attribute):
        def get(self):
            return self.config.installedOn


    def __init__(self, **kw):
        if 'created' not in kw:
            kw['created'] = extime.Time()
        return super(POP3Grabber, self).__init__(**kw)


    def activate(self):
        self._pop3uids = None
        self.running = False
        self.protocol = None
        if self.status is None:
            self.status = Status(store=self.store, message=u'idle')

    def delete(self):
        iaxiom.IScheduler(self.store).unscheduleAll(self)
        if self.running:
            if self.protocol is not None:
                self.protocol.stop()
                self.protocol.grabber = None
            else:
                self.connector.disconnect()