<reponame>editid0/dashcord
import asyncio
import inspect
import mimetypes
import urllib
import re

import socket
from socketserver import ThreadingMixIn
from http.server import SimpleHTTPRequestHandler, HTTPServer
from jinja2 import Template

import nest_asyncio

class DataTransfer:
    def __init__(self):
        self.routes = {}
        self.dynamics = {}

    pass

REGEX = {
    "int": r"(\d*)",
    "str": r"(\D*)"
}
CONVERSIONS = {
    "str": str,
    "int": int
}

DATA = DataTransfer()

def route(route: str, dynamic: bool = False):
    """Add a route to the application
    
    Arguments:
        route {str} -- The path to the route.
        dynamic {bool} -- This should be set to True if the route is dynamic
    """
    def wrapper(func):
        nonlocal route
        arguments = None
        regex = None
        
        if dynamic:
            matches = re.findall(r"(<(int)?[:]?.+?(?=>))", route)
            arguments = {}
            
            route = route.replace("<", "").replace(">", "")
            
            regex = "^%s$" % route
            for match in matches:
                variable = match[0].replace("%s:" % match[1], "").replace("<", "")

                if match[1] != "":
                    arguments[variable] = match[1]
                    regex = regex.replace("%s:%s" % (match[1], variable), REGEX[match[1]])
                else:
                    arguments[variable] = "str"
                    regex = regex.replace(variable, REGEX["str"])
                
                DATA.dynamics[regex] = (func, route, True, arguments, regex)
        else:
            DATA.routes[route] = (func, route, dynamic, arguments, regex)

        return func

    return wrapper

class HTTPResponse:
    """Object to hold data about the response. No data is given if a GET request is passed."""

    def __init__(self, method, **kwargs):
        """
        Parameters:
                method {[string]} -- The request method. Either GET or POST

        Keyword Arguments:
                data {[dict]} -- The POST request's json data (default: {None})
        """

        self.method = method

        self._original_data = kwargs.get("local_data")
        
        self.arguments = kwargs.get("arguments")

        if self._original_data:
            self._json = self._original_data.get("json")


    async def json(self):
        """Get the response json"""
        return self._json
    
    def _update_json(self, new_json):
        """Update the request json"""
        self._json = new_json

class ThreadingSimpleServer(ThreadingMixIn, HTTPServer):
    pass

class HTTPRequestHandler(SimpleHTTPRequestHandler):
    """The handler that handles all of our website's requests, and calls the routing functions"""
    
    def do_GET(self):
        """This function handles the GET requests for the site."""

        if not DATA.bot:
            return self.wfile.write(b"Bot not supplied, wait a few seconds and refresh.")

        path = self.path
        
        if len(path.split("?")) > 1:
            split = path.split("?")
            path = split[0]
            
            data = {}
            
            for item in split[1].split("&"):
                data[item.split("=")[0]] = item.split("=")[1]
            
            response = HTTPResponse("GET", arguments=data)
        else:
            response = HTTPResponse("GET")
            
        if path.endswith(".js"):
            self.send_header("Content-type", "application/javascript")

            with open("%s/%s" % (DATA.static_path, path.replace("/","")), "r") as file:
                return self.wfile.write(file.read().encode())
        if path.endswith(".css"):
            self.send_header("Content-type", "text/css")

            with open("%s/%s" % (DATA.static_path,  path.replace("/","")), "r") as file:
                return self.wfile.write(file.read().encode())

        dynamic = False
        match_object = None
        
        if path in DATA.routes.keys():
            route_data = DATA.routes.get(path)
            route_func = route_data[0]
        else:
            if not path.endswith(".ico") and len(path.split("/")) > 1:
                for r in DATA.dynamics.keys():
                    match = re.match(r, path)
                    if not match:
                        continue

                    match_object = match

                    if match.groups():
                        route_data = DATA.dynamics[r]
                        break
                else:
                    return self.wfile.write(b"404, page not found")

                route_func = route_data[0]
                dynamic = True
            else:
                return self.wfile.write(b"404, page not found")

        if not inspect.iscoroutinefunction(route_func):
            raise ValueError("Route function must be a coroutine.")

        try:
            kwargs = {}
            if dynamic:
                groups = match_object.groups()
                for i, kv in enumerate(route_data[3].items()):
                    try:
                        if kv[1] == "str":
                            if groups[i].isnumeric():
                                return self.wfile.write(b"404, page not found")
                        elif kv[1] == "int":
                            if not groups[i].isnumeric():
                                return self.wfile.write(b"404, page not found")
                            
                        kwargs[kv[0]] = CONVERSIONS[kv[1]](groups[i])
                    except ValueError:
                        return self.wfile.write(b"404, page not found")

                result = DATA.loop.run_until_complete(route_func(DATA.bot, response, **kwargs))
            else:
                result = DATA.loop.run_until_complete(route_func(DATA.bot, response))

        except RuntimeError as error:
            if str(error).startswith("Cannot enter into task"):
                pass

        if not isinstance(result, str):
            result = str(result)

        mimetype, _ = mimetypes.guess_type(path)
        self.send_response(200)
        self.send_header("Content-type", mimetype)
        self.end_headers()

        return self.wfile.write(result.encode())

    def do_POST(self):
        """This function handles the POST requests for the site."""

        path = self.path
        
        if len(path.split("?")) > 1:
            split = path.split("?")
            path = split[0]
            
            data = {}
            
            for item in split[1].split("&"):
                data[item.split("=")[0]] = item.split("=")[1]
            
            response = HTTPResponse("POST", arguments=data)
        else:
            response = HTTPResponse("POST")
            
        if path.endswith(".js"):
            self.send_header("Content-type", "application/javascript")

            with open("%s/%s" % (DATA.static_path, path), "r") as file:
                return self.wfile.write(file.read().encode())
        if path.endswith(".css"):
            self.send_header("Content-type", "text/css")

            with open("%s/%s" % (DATA.static_path, path), "r") as file:
                return self.wfile.write(file.read().encode())

        body = self.rfile.read(int(self.headers.get("Content-Length"))).decode("utf-8").replace("+", " ")
        body = urllib.parse.unquote(body)

        json = {}
        for key_value in body.split("&"):
            key = key_value.split("=")[0]
            value = key_value.split("=")[1]

            json[key] = value

        self.send_response(200)
        self.end_headers()

        response._update_json({"json": json})

        dynamic = False
        if path in DATA.routes.keys():
            route_data = DATA.routes.get(path)
            route_func = route_data[0]
            
        else:
            if not path.endswith(".ico") and len(path.split("/")) > 1:
                for r in DATA.dynamics.keys():
                    match = re.match(r, path)
                    if not match:
                        continue

                    match_object = match

                    if match.groups():
                        route_data = DATA.dynamics[r]
                        break
                else:
                    return self.wfile.write(b"404, page not found")

                route_func = route_data[0]
                dynamic = True
            else:
                return self.wfile.write(b"404, page not found")


        self.send_response(200)

        if not inspect.iscoroutinefunction(route_func):
            raise ValueError("Route function must be a coroutine.")
        
        try:
            kwargs = {}
            if dynamic:
                groups = match_object.groups()
                for i, kv in enumerate(route_data[3].items()):
                    try:
                        if kv[1] == "str":
                            if groups[i].isnumeric():
                                return self.wfile.write(b"404, page not found")
                        elif kv[1] == "int":
                            if not groups[i].isnumeric():
                                return self.wfile.write(b"404, page not found")
                            
                        kwargs[kv[0]] = CONVERSIONS[kv[1]](groups[i])
                    except ValueError:
                        return self.wfile.write(b"404, page not found")

                result = DATA.loop.run_until_complete(route_func(DATA.bot, response, **kwargs))
            else:
                result = DATA.loop.run_until_complete(route_func(DATA.bot, response))

        except RuntimeError as error:
            if str(error).startswith("Cannot enter into task"):
                pass

        return self.wfile.write(str(result).encode())

class App:
    """The main dashcord application"""

    def __init__(self, bot, template_path, static_path, routing_file):
        """
        Parameters
        ----------
        bot
            The commands.Bot / discord.Client object.
        template_path
            The path for your html templates.
        static_path
            The path for your images, javascript and css files.
        routing_file
            The file holding your route functions.

            Usage is ```python
            import routing_file

            App(..., routing_file=routing_file)
            ```
        """
        self.started = False
        DATA.app = self
        
        self.bot = bot
        self.loop = bot.loop

        nest_asyncio.apply(self.loop)

        self.routing_file = routing_file
        DATA.routing_file = routing_file

        self.template_path = template_path
        DATA.template_path = template_path

        self.static_path = static_path
        DATA.static_path = static_path

        bot.dashboard = self

        DATA.bot = bot
        DATA.loop = self.loop

    def render_html(self, fp, **kwargs):
        """
        Load a html file. Supports templating.

        Parameters
        ----------
        fp
            The html file, you don't need to supply the directory path.
        **kwargs
            Keyword arguments in this function are used to give the data to the html file for templating.
        """
        with open("%s/%s" % (self.template_path, fp), "r") as html_file:
            contents = html_file.read()

            template = Template(contents)
            return template.render(**kwargs)

    def begin_server(self, host, port):
        while True:
            self.bot.server.handle_request()

    async def start(self, host, port):
        """Start the web server"""
        if self.started:
            return
        
        self.started = True
        print("Serving traffic from", host, "on port", port)

        self.server = ThreadingSimpleServer((host, port), HTTPRequestHandler)

        self.bot.server = self.server

        await self.loop.run_in_executor(None, self.begin_server, host, port)
