"""Generic connection cache manager.

Example:

 from ClientCookie import ConnectionCache
 cache = ConnectionCache()
 cache.deposit("http", "example.com", conn)
 conn = cache.withdraw("http", "example.com")


The ConnectionCache class provides cache expiration.


Copyright (C) 2004 John J Lee <jjl@pobox.com>.
Copyright (C) 2001 Gisle Aas.

This code is free software; you can redistribute it and/or modify it under
the terms of the BSD License (see the file COPYING included with the
distribution).

"""

# Ported from libwww-perl 5.75.

import time
try:
    from types import StringTypes
except ImportError:
    from types import StringType
    StringTypes = StringType

from _Util import compat_isinstance
from _Debug import getLogger
debug = getLogger("ClientCookie").debug

class _ConnectionRecord:
    def __init__(self, conn, scheme, key, time):
        self.conn, self.scheme, self.key, self.time = conn, scheme, key, time
    def __repr__(self):
        return "%s(%s, %s, %s, %s)" % (
            self.__class__.__name__,
            self.conn, self.scheme, self.key, self.time)

class ConnectionCache:
    """
    For specialized cache policy it makes sense to subclass ConnectionCache and
    perhaps override the .deposit(), ._enforce_limits() and ._dropping()
    methods.

    """
    def __init__(self, total_capacity=1):
        self._limit = {}
        self.total_capacity(total_capacity)

    def set_total_capacity(self, nr_connections):
        """Set limit for number of cached connections.

        Connections will start to be dropped when this limit is reached.  If 0,
        all connections are immediately dropped.  None means no limit.

        """
        self._limit_total = nr_connections
        self._enforce_limits()

    def total_capacity(self):
        """Return limit for number of cached connections."""
        return self._limit_total

    def set_capacity(self, scheme, nr_connections):
        """Set limit for number of cached connections of specifed scheme.

        scheme: URL scheme (eg. "http" or "ftp")

        """
        self._limit[scheme] = nr_connections
        self._enforce_limits(scheme)

    def capacity(self, scheme):
        """Return limit for number of cached connections of specifed scheme.

        scheme: URL scheme (eg. "http" or "ftp")

        """
        return self._limit[scheme]

    def drop(self, checker=None, reason=None):
        """Drop connections by some criteria.

        checker: either a callable, a number, a string, or None:
         If callable: called for each connection with arguments (conn, scheme,
          key, deposit_time); if it returns a true value, the connection is
          dropped (default is to drop all connections).
         If a number: all connections untouched for the given number of seconds
          or more are dropped.
         If a string: all connections of the given scheme are dropped.
         If None: all connections are dropped.
        reason: passed on to the dropped() method

        """
        if not callable(checker):
            if checker is None:
                checker = lambda cr: True  # drop all of them
            elif compat_isinstance(checker, StringTypes):
                scheme = checker
                if reason is None:
                    reason = "drop %s" % scheme
                checker = lambda cr, scheme=scheme: cr.scheme == scheme
            else:  # numeric
                age_limit = checker
                time_limit = time.time() - age_limit
                if reason is None:
                    reason = "older than %s" % age_limit
                checker = lambda cr, time_limit=time_limit: cr.time < time_limit
        if reason is None:
            reason = "drop"

##         local $SIG{__DIE__};  # don't interfere with eval below
##         local $@;
        crs = []
        for cr in self._conns:
            if checker(cr):
                self._dropping(cr, reason)
                drop = drop + 1
            if not drop:
                crs.append(cr)
        self._conns = crs

    def prune(self):
        """Drop all dead connections.

        This is tested by calling the .ping() method on the connections.  If
        the .ping() method exists and returns a false value, then the
        connection is dropped.

        """
        # XXX HTTPConnection doesn't have a .ping() method
        #self.drop(lambda cr: not cr.conn.ping(), "ping")
        pass

    def get_schemes(self):
        """Return list of cached connection URL schemes."""
        t = {}
        for cr in self._conns:
            t[cr.scheme] = None
        return t.keys()

    def get_connections(self, scheme=None):
        """Return list of all connection objects with the specified URL scheme.

        If no scheme is specified then all connections are returned.

        """
        cs = []
        for cr in self._conns:
            if scheme is None or (scheme and scheme == cr.scheme):
                c.append(cr.conn)
        return cs

# -------------------------------------------------------------------------
# Methods called by handlers to try to save away connections and get them
# back again.

    def deposit(self, scheme, key, conn):
        """Add a new connection to the cache.

        scheme: URL scheme (eg. "http")
        key: any object that can act as a dict key (usually a string or a
         tuple)

        As a side effect, other already cached connections may be dropped.
        Multiple connections with the same scheme/key might be added.

        """
        self._conns.append(_ConnectionRecord(conn, scheme, key, time.time()))
        self._enforce_limits(scheme)

    def withdraw(self, scheme, key):
        """Try to fetch back a connection that was previously deposited.

        If no cached connection with the specified scheme/key is found, then
        None is returned.  There is no guarantee that a deposited connection
        can be withdrawn, as the cache manger is free to drop connections at
        any time.

        """
        conns = self._conns
        for i in range(len(conns)):
            cr = conns[i]
            if not (cr.scheme == scheme and cr.key == key):
                continue
            conns.pop(i)  # remove it
            return cr.conn
        return None

# -------------------------------------------------------------------------
# Called internally.  Subclasses might want to override these.

    def _enforce_limits(self, scheme=None):
        """Drop some cached connections, if necessary.

        Called after a new connection is added (deposited) in the cache or
        capacity limits are adjusted.

        The default implementation drops connections until the specified
        capacity limits are not exceeded.

        """
        conns = self._conns
        if scheme:
            schemes = [scheme]
        else:
            schemes = self.get_schemes()
        for scheme in schemes:
            limit = self._limit.get(scheme)
            if limit is None:
                continue
            for i in range(len(conns), 0, -1):
                if conns[i].scheme != scheme:
                    continue
                limit = limit - 1
                if limit < 0:
                    self._dropping(
                        conns.pop(i),
                        "connection cache %s capacity exceeded" % scheme)

        total = self._limit_total
        if total is not None:
            while len(conns) > total:
                self._dropping(conns.pop(0),
                               "connection cache total capacity exceeded")

    def _dropping(self, conn_record, reason):
        """Called when a connection is dropped.

        conn_record: _ConnectionRecord instance for the dropped connection
        reason: string describing the reason for the drop

        """
        debug("DROPPING %s [%s]" % (conn_record, reason))
