# -*- coding: utf-8 -*-
__doc__ = """
WebSocket within CherryPy is a tricky bit since CherryPy is
a threaded server which would choke quickly if each thread
of the server were kept attached to a long living connection
that WebSocket expects.

In order to work around this constraint, we take some advantage
of some internals of CherryPy as well as the introspection
Python provides.

Basically, whene the WebSocket upgrade is performed, we take over
the socket and let CherryPy take back the thread that was
associated with the upgrade request.

These operations require a bit of work at various levels of
the CherryPy framework but this module takes care of them
and from your application's perspective, this is abstracted.

Here are the various utilities provided by this module:

 * WebSocketTool: The tool is in charge to perform the
                  HTTP upgrade and detach the socket from
                  CherryPy. It runs at various hook points of the
                  request's processing. Enable that tool at
                  any path you wish to handle as a WebSocket
                  handler.
                  
 * WebSocketPlugin: The plugin tracks the web socket handler
                    instanciated. It also cleans out websocket handler
                    which connection have been closed down.
             
Simple usage example:

    import cherrypy
    from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool, WebSocketHandler
    from ws4py.server.handler.threadedhandler import EchoWebSocketHandler
    
    cherrypy.config.update({'server.socket_port': 9000})
    WebSocketPlugin(cherrypy.engine).subscribe()
    cherrypy.tools.websocket = WebSocketTool()

    class Root(object):
        @cherrypy.expose
        def index(self):
            return 'some HTML with a websocket javascript connection'

        @cherrypy.expose
        def ws(self):
            pass
        
    cherrypy.quickstart(Root(), '/', config={'/ws': {'tools.websocket.on': True,
                                                     'tools.websocket.handler_cls': EchoWebSocketHandler}})


Note that you can set the handler class on per-path basis,
meaning you could also dynamically change the class based
on other envrionmental settings (is the user authenticated for ex).

The current implementation of the handler is based on a thread that will
constantly read bytes from the socket and feed the stream instance with them
until an error or a close condition arise. This might be a bit
suboptimal and one could implement the handler in a different fashion
using a poll based socket handling (select, poll, tornado, gevent, etc.)
"""
import base64
from hashlib import sha1
import inspect
import socket

import cherrypy
from cherrypy import Tool
from cherrypy.process import plugins
from cherrypy.wsgiserver import HTTPConnection, HTTPRequest

from ws4py import WS_KEY
from ws4py.exc import HandshakeError
from ws4py.server.handler.threadedhandler import WebSocketHandler

__all__ = ['WebSocketTool', 'WebSocketPlugin']

class WebSocketTool(Tool):
    def __init__(self):
        Tool.__init__(self, 'before_request_body', self.upgrade)

    def _setup(self):
        conf = self._merged_args()
        hooks = cherrypy.serving.request.hooks
        p = conf.pop("priority", getattr(self.callable, "priority",
                                         self._priority))
        hooks.attach(self._point, self.callable, priority=p, **conf)
        hooks.attach('before_finalize', self.complete,
                     priority=p)
        hooks.attach('on_end_resource', self.cleanup_headers,
                     priority=70)
        hooks.attach('on_end_request', self.start_handler,
                     priority=70)

    def upgrade(self, protocols=None, extensions=None, version=8, handler_cls=WebSocketHandler):
        """
        Performs the upgrade of the connection to the WebSocket
        protocol.

        The provided protocols may be a list of WebSocket
        protocols supported by the instance of the tool.

        When no list is provided and no protocol is either
        during the upgrade, then the protocol parameter is
        not taken into account. On the other hand,
        if the protocol from the handshake isn't part
        of the provided list, the upgrade fails immediatly.
        """
        request = cherrypy.serving.request
        request.process_request_body = False
        
        ws_protocols = None
        ws_location = None
        ws_version = version
        ws_key = None
        ws_extensions = []
        
        if request.method != 'GET':
            raise HandshakeError('Method is not GET')

        for key, expected_value in [('Upgrade', 'websocket'),
                                     ('Connection', 'Upgrade')]:
            actual_value = request.headers.get(key)
            if not actual_value:
                raise HandshakeError('Header %s is not defined' % key)
            if expected_value and expected_value not in actual_value:
                raise HandshakeError('Illegal value for header %s: %s' %
                                     (key, actual_value))
            
        key = request.headers.get('Sec-WebSocket-Key')
        if key:
            ws_key = base64.b64decode(key)
            if len(ws_key) != 16:
                raise HandshakeError("WebSocket key's length is invalid")
        
        version = request.headers.get('Sec-WebSocket-Version')
        if version:
            if version != str(ws_version):
                raise HandshakeError('Unsupported WebSocket version')
        else:
            raise HandshakeError('WebSocket version required')
        
        protocols = protocols or []
        subprotocols = request.headers.get('Sec-WebSocket-Protocol')
        if subprotocols:
            ws_protocols = []
            for s in subprotocols.split(','):
                s = s.strip()
                if s in protocols:
                    ws_protocols.append(s)

        exts = extensions or []