from http.cookies import SimpleCookie
from collections import namedtuple
from collections.abc import Mapping
from itertools import product, chain
import time
import random

from . import httpd
from . import Context, context_activate


# Name generation for contexts and sessions
def name_parts():
    """
    This generator will create an endless list of steadily increasing
    name part lists.
    """
    # name parts
    letters = ['alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta',
               'theta', 'iota', 'kappa', 'lambda', 'mu', 'nu', 'xi', 'omicron',
               'pi', 'rho', 'sigma', 'tau', 'upsilon', 'phi', 'chi', 'psi',
               'omega']
    colours = ['red', 'orange', 'yellow', 'green', 'blue', 'violet']

    # randomize order
    random.shuffle(letters)
    random.shuffle(colours)

    # yield initial sequence (letter-colour)
    parts = [letters, colours]
    yield parts

    # forever generate longer sequences by appending the letter list
    # over and over. Note that this is the *same* letter list, so it will have
    # the exact order.
    while True:
        random.shuffle(letters)
        random.shuffle(colours)
        parts.append(letters)
        yield parts

# construct an iterator that will endlessly generate names:
# 1) for each parts list p in name_parts() we take the cartesian product
# 2) the product iterators are generated by the for...in generator
# 3) we chain these iterators so that when the first is exhausted, we can
#    continue with the second, etc.
# 4) we map the function '-'.join over the list of parts from the chain
names =  map('-'.join, chain.from_iterable((product(*p) for p in name_parts())))


class SessionCookie(httpd.Filter):
    """
    The actual HTTP filter that will apply the cookie handling logic to each
    request. This filter defers to the SessionManager with respect to the
    cookie name to use and the activation of sessions.
    """
    def bind(self, manager):
        """Post constructor configuration of filter."""
        self.manager = manager

    def handle(self):
        """
        Determine if a cookie needs to be set and let the session manager
        handle activation.
        """
        cookies = self.request.cookies
        morsel = cookies.get(self.manager.cookie)

        if not morsel:
            # Determine new cookie
            value = self.manager.generate_name()

            # Set new cookie
            cookies[self.manager.cookie] = value
            cookies[self.manager.cookie]['path'] = '/'

            # Send the new cookie as header
            self.request.send_header('Set-Cookie', cookies[self.manager.cookie].OutputString())
        else:
            value = morsel.value

        self.manager.activate(value)


class Session:
    """
    The Session bookkeeping data.
    """
    def __init__(self, context, seen):
        self.context = context
        self.seen = seen

    def activate(self):
        """Activate the session. Updates last seen time."""
        self.seen = time.time()
        context_activate(self.context)


class SessionManager:
    """
    The SessionManager class. This class is callable so it can be used in place
    of a constructor in the configuration.
    """
    def __init__(self, cookie_name):
        self.sessions = {}
        self.cookie = cookie_name

    def __call__(self, *args, **kwargs):
        handler = SessionCookie(*args, **kwargs)
        handler.bind(self)
        return handler

    def generate_name(self):
        result = next(names)
        while result in self.sessions:
            result = next(names)
        return result

    def _new_session(self, name):
        result = Session(Context(name=name, init_data={'name': name}), time.time())
        result.context.start()
        return result

    def activate(self, name):
        if name not in self.sessions:
            self.sessions[name] = self._new_session(name)
        self.sessions[name].activate()

