from dataclasses import dataclass
import datetime
import re
import os

import jinja_markdown

from flask import Flask, send_from_directory, render_template, make_response, url_for
from livereload import Server

# Importing performs monkeypatching
import xml_lexer  # NOQA: F401
import ini_lexer  # NOQA: F401


app = Flask(__name__)
extensions = app.jinja_options.setdefault('extensions', [])
extensions.append('jinja2_highlight.HighlightExtension')
extensions.append('jinja_markdown.MarkdownExtension')

jinja_markdown.EXTENSIONS.append("mdx_spantables")
jinja_markdown.EXTENSIONS.append("toc")


HTAG = re.compile(r"<h(\d)[^>]*id=\"([^\"]+)\"[^>]*>([^<]*)</h")
TOC_HTAG_LEVELS = {"1", "2"}

HOST = "https://bsnk.me"
TEMPLATES = "templates"
PAGES_BASE = "pages"
STATIC = ["images", "static", "mice"]

ROOT = os.environ.get("EA_ROOT", "")

EAMUSE_CONTENTS = {
    "getting_started.html": ("Getting started and following along", {
        0: ("A quick one-stop shop for getting setup with the tools you'll want on hand if you want to investigate "
            + "things for yourself.")
    }),
    "transport.html": ("Transport layer", None),
    "packet.html": ("The inner packet structure", None),
    "protocol.html": ("Communication protocol details", {
        0: ("There are a crazy number of sub pages here, so just go check the contents there.")
    }),
    "server.html": "Let's write a server",
    0: ("Misc pages", {
        "cardid.html": ("Parsing and converting card IDs", ())
    }),
}
SEGA_CONTENTS = {
    "intro.html": ("Introduction to RingEdge 2", ()),
    "hardware": ("Hardware", {
        "jvs.html": "JVS",
        "touch.html": "Touchscreen",
    }),
    "software": ("Software", {
        "pcp": ("PCP", {"libpcp.html": "libpcp"}),
        "drivers": ("Device drivers", None),
        "security": ("Security", {
            "alphadvd.html": "AlphaDVD",
        }),
        "groovemaster.html": "GrooveMaster.ini",
    }),
    "manual": ("Manual", {
        "errors.html": "Error Codes",
        "keychip.html": "Keychip Modding",
    }),
    # "network": ("Networking", {
    #     "allnet.html": "ALL.Net"
    # }),
    "misc": ("Misc", {
        "partition.html": "SEGA Partition Structure"
    }),
}
CONTENTS = {
    "": EAMUSE_CONTENTS,
    "sega": SEGA_CONTENTS
}


@dataclass
class Part:
    id: str
    name: str
    description: str = None
    page: str = None


PARTS = {
    "838-14971": Part("838-14971", "Aime NFC daughter board", "NFC RW BD TN32MSEC003S"),
    "838-15221": Part("838-15221", "Serial I/F BD Touchpanel Gunze",
                      "The interface board for the touchscreen on MaiMai versions pre-DX.")
}


def part(id_):
    if (part := PARTS.get(id_)):
        return (
            f'<span class="part" tabindex="0">{part.name}<span>'
            f'<span>ASSY ID</span><span>SEGA {part.id}</span>'
            f'<span>Description</span><span>{part.description}</span>'
            '</span></span>'
        )
    return f'<span class="part">{id_}</span>'


def generate_xrpc_list():
    output = "<ul>"
    proto = TEMPLATES + "/" + PAGES_BASE + "/proto"
    for base, _, files in os.walk(proto):
        prefix = base[len(proto):].replace("\\", "/").strip("/")
        if prefix:
            prefix = prefix.replace("/", ".") + "."
        for i in files:
            if i.startswith("~"):
                continue

            delim = "_" if prefix else "."
            href = f"{ROOT}/proto{base[len(proto):]}/{i}"
            output += f"<li><code><a href=\"{href}\">"
            output += prefix + i.replace(".html", delim + "%s")
            output += "</code></a></li>"
            with open(os.path.join(base, i)) as f:
                headers = re.findall('<h2 id="([^"]*?)">', f.read())
            output += "<ul>"
            for j in headers:
                output += f"<li><code><a href=\"{href}#{j}\">"
                output += prefix + i.replace(".html", delim + j)
                output += "</code></a></li>"
            output += "</ul>"
    return output + "</ul>"


def generate_toc(base, name, route, start=1):
    parts = route.strip("/").split("/")

    toc = CONTENTS
    for i in parts:
        if i in toc:
            toc = toc[i]
            if isinstance(toc, tuple):
                toc = toc[1]
            if not isinstance(toc, dict):
                return ""
        else:
            return ""

    def walk(toc, path, start=1):
        unordered = len(toc) == 1 and 0 in toc
        out = f'<{"u" if unordered else "o"}l start="{start}">'
        for url in toc:
            if isinstance(toc[url], tuple):
                name, children = toc[url]
            elif isinstance(toc[url], str):
                name, children = toc[url], -1

            out += "<li>"
            if isinstance(url, str):
                fqu = f"{ROOT}/{path}"
                if not url.startswith("#"):
                    fqu += "/"
                fqu += url
                while "//" in fqu:
                    fqu = fqu.replace("//", "/")
                if not fqu.endswith((".html", "/")) and "#" not in fqu:
                    fqu += "/"
                out += f'<a href="{fqu}">{name}</a>'
            else:
                out += name
            out += "</li>"

            if children == -1:
                continue
            if children is None:
                filename = "/".join((TEMPLATES, PAGES_BASE, path, url))
                while "//" in filename:
                    filename = filename.replace("//", "/")

                if url == "":
                    filename += "index.html"
                if "." not in filename:
                    filename += "/index.html"

                with open(filename) as page:
                    headers = HTAG.findall(page.read())

                children = {}
                for level, anchor, text in headers:
                    if level in TOC_HTAG_LEVELS:
                        children[f"#{anchor}"] = text
                if not children:
                    children = None

            if children is not None:
                out += walk(children, f"{path}/{url}" if isinstance(url, str) else path)

        out += f'</{"u" if unordered else "o"}l>'

        return out

    return walk(toc, route, start)


def generate_footer(base, name, route):
    parts = route.strip("/").split("/")
    if not parts:
        return ""

    toc = CONTENTS
    path = []
    for i in parts[:-1]:
        if i in toc:
            path.append(i)
            toc = toc[i]
            if isinstance(toc, tuple):
                toc = toc[1]
            if not isinstance(toc, dict):
                toc = None
                break
        elif toc == CONTENTS:
            toc = toc[""]
        else:
            toc = None
            break

    if toc == CONTENTS and len(parts) == 1:
        toc = toc[""]

    if toc is None:
        siblings = None
    else:
        siblings = [i for i in toc.keys() if isinstance(i, str)]
        try:
            us_idx = siblings.index(parts[-1])
        except ValueError:
            us_idx = -1
        parent = ROOT + "/" + "/".join(parts[:-1])
        if not parent.endswith("/"):
            parent += "/"

    footer = "<footer><span>"
    if siblings and us_idx > 0:
        footer += f'<a href="{parent}{siblings[us_idx - 1]}">Previous page</a>'
    footer += "</span><span>"

    if parts:
        crumbs = []
        built = ROOT + "/"
        for i in parts[:-1]:
            built += f"{i}"
            if not built.endswith((".html", "/")):
                built += "/"
            crumbs.append(f'<a href="{built}">{i}</a>')
        crumbs.append(parts[-1])
        footer += "/".join(crumbs)

    footer += "</span><span>"

    if siblings and us_idx < len(siblings) - 1 and us_idx != -1:
        footer += f'<a href="{parent}{siblings[us_idx + 1]}">Next page</a>'

    footer += "</span></footer>"
    return footer


def ioctl(original):
    original = eval(original)  # Unsafe as hell

    def CTL_CODE(DeviceType, Function, Method, Access):
        return ((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method)

    deviceType = original >> 16
    access = (original >> 14) & 0x3
    function = (original >> 2) & 0xfff
    method = original & 0x3

    assert hex(CTL_CODE(deviceType, function, method, access)) == hex(original)

    deviceType = hex(deviceType)
    if deviceType == "0x9c40":
        deviceType = "FILE_DEVICE_SEGA"
    function = hex(function)
    method = "METHOD_" + ["BUFFERED", "IN_DIRECT", "OUT_DIRECT", "NEITHER"][method]
    access = ["FILE_ANY_ACCESS", "FILE_READ_ACCESS", "FILE_WRITE_ACCESS"][access]

    return (f"CTL_CODE({deviceType}, {function}, {method}, {access})")


@app.route("/styles.css")
def styles():
    return send_from_directory(".", "styles.css")


@app.route("/tango.css")
def tango():
    return send_from_directory(".", "tango.css")


@app.route("/headers.js")
def header_script():
    return send_from_directory(".", "headers.js")


for i in STATIC:
    for base, _, files in os.walk(i):
        for name in files:
            def handler(base, name):
                def handler():
                    return send_from_directory(base, name)
                return handler
            local_base = base.replace("\\", "/").strip(".").strip("/")
            route = local_base + "/" + name
            if not route.startswith("/"):
                route = "/" + route

            handler = handler(base, name)
            handler.__name__ == route
            app.add_url_rule(route, route, handler)


for base, _, files in os.walk(TEMPLATES + "/" + PAGES_BASE):
    if ".git" in base:
        continue
    if base.startswith(TEMPLATES):
        base = base[len(TEMPLATES):]

    for name in files:
        if name.endswith(".html"):
            def handler(base, name, route):
                def handler():
                    return render_template(
                        os.path.join(base, name).strip("/").replace("\\", "/"),
                        ROOT=ROOT,
                        CANONICAL=ROOT + route,
                        generate_xrpc_list=generate_xrpc_list,
                        generate_toc=lambda start=1: generate_toc(base, name, route, start),
                        generate_footer=lambda: generate_footer(base, name, route),
                        relative=lambda path: os.path.join(base, path).strip("/").replace("\\", "/"),
                        part=part,
                        ioctl=ioctl,
                    )
                return handler

            local_base = base.replace("\\", "/").strip(".").strip("/")
            if local_base.startswith(PAGES_BASE):
                local_base = local_base[len(PAGES_BASE):]

            route = local_base + "/" + name
            if route.endswith("/index.html"):
                route = route[:-10]
            if not route.startswith("/"):
                route = "/" + route

            handler = handler(base, name, route)
            handler.__name__ == route
            app.add_url_rule(route, route, handler)


@app.route("/sitemap.xml")
def sitemap():
    host_base = HOST + ROOT

    links = []
    for rule in app.url_map.iter_rules():
        if "GET" in rule.methods and len(rule.arguments) == 0:
            url = url_for(rule.endpoint, **(rule.defaults or {}))
            if not url.endswith(("/", ".html", ".png")):
                continue
            path = rule.endpoint
            if path.endswith("/"):
                path += "index.html"
            path = os.path.join(TEMPLATES, PAGES_BASE, path.lstrip("/"))
            if os.path.exists(path):
                mod_time = os.path.getmtime(path)
                mod_time = datetime.datetime.fromtimestamp(mod_time).strftime("%Y-%m-%dT%H:%M:%SZ")
            else:
                mod_time = None
            links.append((host_base + url, mod_time))
    response = make_response(render_template("sitemap.xml", urls=links[::-1]))
    response.headers["Content-Type"] = "application/xml"
    return response


def run_dev():
    app.config['TEMPLATES_AUTO_RELOAD'] = True
    app.config['DEBUG'] = True

    # app.run(debug=True, port=3000, host="0.0.0.0")

    server = Server(app.wsgi_app)
    server.watch(".")
    server.serve(port=3000)


if __name__ == '__main__':
    run_dev()
