"""
Definition of the Session class.
"""

import re
import sys
import time
import json
import base64
import random
import hashlib
import asyncio
import weakref
import datetime
from http.cookies import SimpleCookie

from ..event._component import new_type

from ._component2 import PyComponent, JsComponent, AppComponentMeta
from ._asset import Asset, Bundle, solve_dependencies
from ._assetstore import AssetStore, INDEX
from ._assetstore import assets as assetstore
from ._clientcore import serializer
from . import logger

from .. import config

reprs = json.dumps


# Use the system PRNG for session id generation (if possible)
# NOTE: secure random string generation implementation is adapted
#       from the Django project.

def get_random_string(length=24, allowed_chars=None):
    """ Produce a securely generated random string.

    With a length of 12 with the a-z, A-Z, 0-9 character set returns
    a 71-bit value. log_2((26+26+10)^12) =~ 71 bits
    """
    allowed_chars = allowed_chars or ('abcdefghijklmnopqrstuvwxyz' +
                                      'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')
    try:
        srandom = random.SystemRandom()
    except NotImplementedError:  # pragma: no cover
        srandom = random
        logger.warning('Falling back to less secure Mersenne Twister random string.')
        bogus = "%s%s%s" % (random.getstate(), time.time(), 'sdkhfbsdkfbsdbhf')
        random.seed(hashlib.sha256(bogus.encode()).digest())

    return ''.join(srandom.choice(allowed_chars) for i in range(length))


class Session:
    """ A connection between Python and the client runtime (JavaScript).

    The session is what holds together the app widget, the web runtime,
    and the websocket instance that connects to it.

    Responsibilities:

    * Send messages to the client and process messages received by the client.
    * Keep track of PyComponent instances used by the session.
    * Keep track of JsComponent instances associated with the session.
    * Ensure that the client has all the module definitions it needs.

    """

    STATUS = new_type('Enum', (), {'PENDING': 1, 'CONNECTED': 2, 'CLOSED': 0})

    def __init__(self, app_name, store=None,
                 request=None):  # Allow custom store for testing
        self._store = store if (store is not None) else assetstore
        assert isinstance(self._store, AssetStore)

        self._creation_time = time.time()  # used by app manager

        # Id and name of the app
        self._id = get_random_string()
        self._app_name = app_name

        # To keep track of what modules are defined at the client
        self._present_classes = set()  # Component classes known by the c
lient
        self._present_modules = set()  # module names that, plus deps
        self._present_assets = set()  # names of used associated assets
        self._assets_to_ignore = set()  # user settable

        # Data for this session (in addition to the data provided by the store)
        self._data = {}

        # More vars
        self._runtime = None  # init web runtime, will be set when used
        self._ws = None  # init websocket, will be set when a connecti
on is made
        self._closing = False  # Flag to help with shutdown

        # PyComponent or JsComponent instance, can be None if app_name is __default__
        self._component = None

        # The session assigns component id's and keeps track of component objects
        self._component_counter = 0
        self._component_instances = weakref.WeakValueDictionary()
        self._dead_component_ids = set()

        # Keep track of roundtrips. The _ping_calls elements are:
        # [ping_count, {objects}, *(callback, args)]
        self._ping_calls = []
        self._ping_counter = 0
        self._eval_result = {}
        self._eval_count = 0

        # While the client is not connected, we keep a queue of
        # commands, which are send to the client as soon as it connects
        self._pending_commands = []

        # request related information
        self._request = request
        if request and request.cookies:
            cookies = request.cookies
        else:
            cookies = {}
        self._set_cookies(cookies)

    def __repr__(self):
        t = '<%s for %r (%i) at 0x%x>'
        return t % (self.__class__.__name__, self.app_name, self.status, id(self))

    @property
    def request(self):
        """The tornado request that was at the origin of this session.
        """
        return self._request

    @property
    def id(self):
        """ The unique identifier of this session.
        """
        return self._id

    @property
    def app_name(self):
        """ The name of the application that this session represents.
        """
        return self._app_name

    @property
    def app(self):
        """ The root PyComponent or JsComponent instance that represents the app.
        """
        return self._component

    @property
    def runtime(self):
        """ The runtime that is rendering this app instance. Can be
        None if the client is a browser.
        """
        return self._runtime

    @property
    def status(self):
        """ The status of this session.
        The lifecycle for each session is:

        * status 1: pending
        * status 2: connected
        * status 0: closed
        """
        if self._ws is None:
            return self.STATUS.PENDING  # not connected yet
        elif self._ws.close_code is None:
            return self.STATUS.CONNECTED  # alive and kicking
        else:
            return self.STATUS.CLOSED  # connection closed

    @property
    def present_modules(self):
        """ The set of module names that is (currently) available at the client.
        """
        return set(self._present_modules)

    @property
    def assets_to_ignore(self):
        """ The set of names of assets that should *not* be pushed to
        the client, e.g. because they are already present on the page.
        Add names to this set to prevent them from being loaded.
        """
        return self._assets_to_ignore

    def close(self):
        """ Close the session: close websocket, close runtime, dispose app.
        """
        # Stop guarding objects to break down any circular refs
        self._ping_calls = []
        self._closing = True  # suppress warnings for session being closed.
        try:
            # Close the websocket
            if self._ws:
                self._ws.close_this()
            # Close the runtime
            if self._runtime:
                self._runtime.close()
            # Dispose the component and break the circular reference
            if self._component is not None:
                self._component.dispose()
                self._component = None
            # Discard data
            self._data = {}
        finally:
            self._closing = False

    ## Hooking up with app, websocket, runtime

    def _set_ws(self, ws):
        """ A session is always first created, so we know what page to
        serve. The client will connect the websocket, and communicate
        the session_id so it can be connected to the correct Session
        via this method
        """
        if self._ws is not None:
            raise RuntimeError('Session is already connected.')
        # Set websocket object - this is what changes the status to CONNECTED
        self._ws = ws
        self._ws.write_command(("PRINT", "Flexx session says hi"))
        # Send pending commands
        for command in self._pending_commands:
            self._ws.write_command(command)
        self._ws.write_command(('INIT_DONE', ))

    def _set_cookies(self, cookies=None):
        """ To set cookies, must be an http.cookie.SimpleCookie object.
        When the app is loaded as a web app, the cookies are set *before* the
        main component is instantiated. Otherwise they are set when the websocket
        is connected.
        """
        self._cookies = cookies if cookies else SimpleCookie()

    def _set_runtime(self, runtime):
        if self._runtime is not None:
            raise RuntimeError('Session already has a runtime.')
        self._runt