from .vpython import GlowWidget, baseObj, vector, canvas, _browsertype
from ._notebook_helpers import _in_spyder, _undo_vpython_import_in_spyder

from http.server import BaseHTTPRequestHandler, HTTPServer
import os
import platform
import sys
import threading
import json
import webbrowser as _webbrowser
import asyncio
from autobahn.asyncio.websocket import WebSocketServerProtocol, WebSocketServerFactory
import txaio
import copy
import socket
import multiprocessing


import signal
from urllib.parse import unquote

from .rate_control import rate


# Redefine `Thread.run` to not show a traceback for Spyder when stopping
# the server by raising a KeyboardInterrupt or SystemExit.
if _in_spyder:
    def install_thread_stopped_message():
        """
        Workaround to prevent showing a traceback when VPython server stops.

        See:
        https://bugs.python.org/issue1230540
        """
        run_old = threading.Thread.run

        def run(*args, **kwargs):
            try:
                run_old(*args, **kwargs)
            except (KeyboardInterrupt, SystemExit):
                print("VPython server stopped.")
            except:
                raise
        threading.Thread.run = run

    install_thread_stopped_message()



# Check for Ctrl+C. SIGINT will also be sent by our code if WServer is closed.
def signal_handler(signal, frame):
    stop_server()


signal.signal(signal.SIGINT, signal_handler)

# Requests from client to http server can be the following:
#    get glowcomm.html, library .js files, images, or font files


def find_free_port():
    s = socket.socket()
    s.bind(('', 0))  # find an available port
    return s.getsockname()[1]


__HTTP_PORT = find_free_port()
__SOCKET_PORT = find_free_port()

try:
    if platform.python_implementation() == 'PyPy':
        # use port number between 9000 and 9999 for PyPy
        __SOCKET_PORT = 9000 + __SOCKET_PORT % 1000
except:
    pass

# try: # machinery for reusing ports
    # fd = open('free_ports')
    # __HTTP_PORT = int(fd.readline())
    # __SOCKET_PORT = int(fd.readline())
# except:
    # __HTTP_PORT = find_free_port()
    # __SOCKET_PORT = find_free_port()
    # fd = open('free_ports', 'w') # this writes to user program's directory
    # fd.write(str(__HTTP_PORT))
    # fd.write('\n')
    # fd.write(str(__SOCKET_PORT))

# Make it possible for glowcomm.html to find out what the websocket port is:
js = __file__.replace(
    'no_notebook.py', 'vpython_libraries' + os.sep + 'glowcomm.html')

with open(js) as fd:
    glowcomm_raw = fd.read()


def glowcomm_with_socket_port(port):
    global glowcomm_raw
    # provide glowcomm.html with socket number
    return glowcomm_raw.replace('XXX', str(port))


glowcomm = glowcomm_with_socket_port(__SOCKET_PORT)

httpserving = False
websocketserving = False


class serveHTTP(BaseHTTPRequestHandler):
    serverlib = __file__.replace('no_notebook.py', 'vpython_libraries')
    serverdata = __file__.replace('no_notebook.py', 'vpython_data')
    mimes = {'html': ['text/html', serverlib],
             'js': ['application/javascript', serverlib],
             'css': ['text/css', serverlib],
             'jpg': ['image/jpg', serverdata],
             'png': ['image/png', serverlib],
             'otf': ['application/x-font-otf', serverdata],
             'ttf': ['application/x-font-ttf', serverdata],
             'ico': ['image/x-icon', serverdata]}

    def do_GET(self):
        global httpserving
        httpserving = True
        html = False
        if self.path == "/":
            self.path = 'glowcomm.html'
            html = True
        elif self.path[0] == "/":
            self.path = os.sep + self.path[1:]
        f = self.path.rfind('.')
        fext = None
        if f > 0:
            fext = self.path[f + 1:]
        if fext in self.mimes:
            mime = self.mimes[fext]
            # For example, mime[0] is image/jpg,
            # mime[1] is C:\Users\Bruce\Anaconda3\lib\site-packages\vpython\vpython_data
            self.send_response(200)
            self.send_header('Content-type', mime[0])
            self.end_headers()
            if not html:
                path = unquote(self.path)  # convert %20 to space, for example
                # Now path can be for example \Fig 4.6.jpg
                # user current working directory, e.g. D:\Documents\0GlowScriptWork\LocalServer
                cwd = os.getcwd()
                loc = cwd + path
                if not os.path.isfile(loc):
                    loc = mime[1] + path  # look in vpython_data
                fd = open(loc, 'rb')
                self.wfile.write(fd.read())
            else:
                # string.encode() is not available in Python 2.7, but neither is async
                self.wfile.write(glowcomm.encode('utf-8'))

    def log_message(self, format, *args):  # this overrides server stderr output
        return

# Requests from client to websocket server can be the following:
# trigger event; return data (constructors, attributes, methods)
# other event; pause, waitfor, pick, compound


class WSserver(WebSocketServerProtocol):
    # Data sent and received must be type "bytes", so use string.encode and string.decode
    connection = None

    def onConnect(self, request):
        self.connection = self

    def onOpen(self):
        global websocketserving
        websocketserving = True

    # For Python 3.5 and later, the newer syntax eliminates "@asyncio.coroutine"
    # in favor of "async def onMessage...", and "yield from" with "await".
    # Attempting to use the older Python 3.4 syntax was not successful, so this
    # no-notebook version of VPython requires Python 3.5.3 or later.
    # @asyncio.coroutine
    # def onMessage(self, data, isBinary): # data includes canvas update, events, pick, compound
    # data includes canvas update, events, pick, compound
    async def onMessage(self, data, isBinary):
        baseObj.handle_attach()  # attach arrow and attach trail

        baseObj.sent = False  # tell main thread that we're preparing to send data to browser
        while True:
            try:
                objdata = copy.deepcopy(baseObj.updates)
                attrdata = copy.deepcopy(baseObj.attrs)
                baseObj.initialize()  # reinitialize baseObj.updates
                break
            except:
                pass
        for a in attrdata:  # a is [idx, attr]
            idx, attr = a
            val = getattr(baseObj.object_registry[idx], attr)
            if type(val) is vector:
                val = [val.x, val.y, val.z]
            if idx in objdata['attrs']:
                objdata['attrs'][idx][attr] = val
            else:
                objdata['attrs'][idx] = {attr: val}
        objdata = baseObj.package(objdata)
        jdata = json.dumps(objdata, separators=(',', ':')).encode('utf_8')
        self.sendMessage(jdata, isBinary=False)
        baseObj.sent = True

        if data != b'trigger':  # b'trigger' just asks for updates
            d = json.loads(data.decode("utf_8"))  # update_canvas info
            for m in d:
                # Must send events one at a time to GW.handle_msg because bound events need the loop code:
                # message format used by notebook
                msg = {'content': {'data': [m]}}
                loop = asyncio.get_event_loop()
                await loop.run_in_executor(None, GW.handle_msg, msg)

    def onClose(self, wasClean, code, reason):
        """Called when browser tab is closed."""
        global websocketserving

        self.connection = None

        # We r done serving, let everyone else know...
        websocketserving = False

        # The cleanest way to get a fresh browser tab going in spyder
        # is to force vpython to be reimported each time the code is run.
        #
        # Even though this code is repeated in stop_server below we also
        # need it here because in spyder the script may have stopped on its
        # own ( because it has no infinite loop in it ) so the only signal
        # that the tab has been closed comes via the websocket.
        if _in_spyder:
            _undo_vpython_import_in_spyder()

        # We want to exit, but the main thread is running.
        # Only the main thread can properly call sys.exit, so have a signal
        # handler call it on the main thread's behalf.
        if platform.system() == 'Windows':
            if threading.main_thread().is_alive() and not _in_spyder:
                # On windows, if we get here then this signal won't be caught
                # by our signal handler. Just call it ourselves.
                os.kill(os.getpid(), signal.CTRL_C_EVENT)
            else:
                stop_server()
        else:
            os.kill(os.getpid(), signal.SIGINT)


try:
    if platform.python_implementation() == 'PyPy':
        server_address = ('', 0)      # let HTTPServer choose a free port
        __server = HTTPServer(server_address, serveHTTP)
        port = __server.server_port   # get the chosen port
        # Change the global variable to store the actual port used
        __HTTP_PORT = port
        _webbrowser.open('http://localhost:{}'.format(port)
                         )  # or webbrowser.open_new_tab()
    else:
        __server = HTTPServer(('', __HTTP_PORT), serveHTTP)
        # or webbrowser.open_new_tab()
        if _browsertype == 'default':  # uses default browser
            _webbrowser.open('http://localhost:{}'.format(__HTTP_PORT))

except:
    pass


if _browsertype == 'pyqt':
    if platform.python_implementation() == 'PyPy':
        raise RuntimeError('The pyqt browser cannot be used PyPy. Please use '
                           'the default browser instead by removing '
                           'set_browser("pyqt") from your code.')
    elif sys.platform.startswith('win'):
        raise RuntimeError('The pyqt browser cannot be used on Windows. '
                           'Please use the default browser instead by '
                           'removing set_browser("pyqt") from your code.')
    elif sys.version_info.major == 3 and sys.version_info.minor >= 8:
        raise RuntimeError('The pyqt browser cannot be used on Python 3.8. '
                           'Please use the default browser instead by '
                           'removing set_browser("pyqt") from your code.')


def start_Qapp(port):
    # creates a python browser with PyQt5
    # runs qtbrowser.py in a separate process
    filepath = os.path.dirname(__file__)
    filename = filepath + '/qtbrowser.py'
    os.system('python ' + filename + ' http://localhost:{}'.format(port))


# create a browser in its own process
if _browsertype == 'pyqt':
    __m = multiprocessing.Process(target=start_Qapp, args=(__HTTP_PORT,))
    __m.start()

__w = threading.Thread(target=__server.serve_forever)
__w.start()


def start_websocket_server():
    """
    Function to get the websocket server going and run the event loop
    that feeds it.
    """
    # We need a new loop in case some other process has already started the
    # main loop. In principle we might be able to do a check for a running
    # loop but this works whether or not a loop is running.
    __interact_loop = asyncio.new_event_loop()

    # Need to do two things before starting the server factory:
    #
    # 1. Set our loop to be the default event loop on this thread
    asyncio.set_event_loop(__interact_loop)
    # 2. Line below is courtesy of
    # https://github.com/crossbario/autobahn-python/issues/1007#issuecomment-391541322
    txaio.config.loop = __interact_loop

    # Now create the factory, start the server then run the event loop forever.
    __factory = WebSocketServerFactory(u"ws://localhost:{}/".format(__SOCKET_PORT))
    __factory.protocol = WSserver
    __coro = __interact_loop.create_server(__factory, '0.0.0.0', __SOCKET_PORT)
    __interact_loop.run_until_complete(__coro)
    __interact_loop.run_forever()


# Put the websocket server in a separate thread running its own event loop.
# That works even if some other program (e.g. spyder) already running an
# async event loop.
__t = threading.Thread(target=start_websocket_server)
__t.start()


def stop_server():
    """Shuts down all threads and exits cleanly."""
    global __server
    __server.shutdown()
    event_loop = txaio.config.loop
    event_loop.stop()
    # We've told the event loop to stop, but it won't shut down until we poke
    # it with a simple scheduled task.
    event_loop.call_soon_threadsafe(lambda: None)

    # If we are in spyder, undo our import. This gets done in the websocket
    # server onClose above if the browser tab is closed but is not done
    # if the user stops the kernel instead.
    if _in_spyder:
        _undo_vpython_import_in_spyder()

    # We don't want Ctrl-C to try to sys.exit inside spyder, i.e.
    # in an ipython console with a separate python kernel running.
    if _in_spyder:
        raise KeyboardInterrupt

    if threading.main_thread().is_alive():
        sys.exit(0)
    else:
        pass
        # If the main thread has already stopped, the python interpreter
        # is likely just running .join on the two remaining threads (in
        # python/threading.py:_shutdown). Since we just stopped those threads,
        # we'll now exit.


GW = GlowWidget()

while not (httpserving and websocketserving):  # try to make sure setup is complete
    rate(60)

# Dummy variable to import
_ = None
