import arcade, arcade.gui, pyglet, os, ujson

from utils.constants import token_pattern, emoji_pattern

from http_client.connection import HTTPClient
from http_client.html_parser import HTML, Text, Element

BLOCK_ELEMENTS = [
    "html", "body", "article", "section", "nav", "aside",
    "h1", "h2", "h3", "h4", "h5", "h6", "hgroup", "header",
    "footer", "address", "p", "hr", "pre", "blockquote",
    "ol", "ul", "menu", "li", "dl", "dt", "dd", "figure",
    "figcaption", "main", "div", "table", "form", "fieldset",
    "legend", "details", "summary"
]

HSTEP = 13
VSTEP = 18

class BlockLayout:
    def __init__(self, node, parent, previous):
        self.node = node
        self.parent = parent
        self.previous = previous
        
        self.children = []
        self.display_list = []
        self.line = []
        
        self.font_cache = {}

        self.x, self.y, self.width, self.height = None, None, None, None

    def paint(self):
        return self.display_list

    def layout_mode(self):
        if isinstance(self.node, Text):
            return "inline"
        elif any([isinstance(child, Element) and \
                  child.tag in BLOCK_ELEMENTS
                  for child in self.node.children]):
            return "block"
        elif self.node.children:
            return "inline"
        else:
            return "block"

    def layout(self):
        self.x = self.parent.x
        self.width = self.parent.width

        if self.previous:
            self.y = self.previous.y + self.previous.height
        else:
            self.y = self.parent.y

        mode = self.layout_mode()
        if mode == "block":
            previous = None
            for child in self.node.children:
                next = BlockLayout(child, self, previous)
                self.children.append(next)
                previous = next
        else:
            self.cursor_x = 0
            self.cursor_y = 0
            self.weight = "normal"
            self.style = "roman"
            self.size = 16

            self.line = []
            self.recurse(self.node)
            self.flush()

        for child in self.children:
            child.layout()

        if mode == "block":
            self.height = sum([
                child.height for child in self.children])
        else:
            self.height = self.cursor_y

    def ensure_font(self, size, weight, style, emoji):
        if not (size, weight, style, emoji) in self.font_cache:
            self.font_cache[(size, weight, style, emoji)] = pyglet.font.load("Roboto", size, weight, style == "italic") if not emoji else pyglet.font.load("OpenMoji Color", size, weight, style == "italic")
        
        return self.font_cache[(size, weight, style, emoji)]

    def word(self, word: str, emoji=False):
        font = self.ensure_font(self.size, self.weight, self.style, emoji)

        w = font.get_text_size(word + ("  " if not emoji else " "))[0]
        
        if self.cursor_x + w > self.width:
            self.flush()

        self.line.append((self.cursor_x, word, font))
        self.cursor_x += w + font.get_text_size(" ")[0]
 
    def flush(self):
        if not self.line:
            return

        fonts_on_line = [font for x, word, font in self.line]
        max_ascent = max(font.ascent for font in fonts_on_line)
        max_descent = min(font.descent for font in fonts_on_line)

        baseline = self.cursor_y + 1.25 * max_ascent

        for rel_x, word, font in self.line:
            x = self.x + rel_x
            y = self.y + baseline - font.ascent
            self.display_list.append((x, y, word, font))

        self.cursor_x = 0
        self.line = []
        self.cursor_y = baseline + 1.25 * max_descent

    def recurse(self, tree):
        if isinstance(tree, Text):
            if "{" in tree.text or "}" in tree.text:
                return
            
            word_list = [match.group(0) for match in token_pattern.finditer(tree.text)]

            for word in word_list:
                if emoji_pattern.fullmatch(word):
                    self.word(word, emoji=True)
                else:
                    self.word(word)
        else:
            self.open_tag(tree.tag)
            for child in tree.children:
                self.recurse(child)
            self.close_tag(tree.tag)

    def open_tag(self, tag):
        if tag == "i":
            self.style = "italic"
        elif tag == "b":
            self.weight = "bold"
        elif tag == "small":
            self.size -= 2
        elif tag == "big":
            self.size += 4
        elif tag == "br":
            self.flush()
    
    def close_tag(self, tag):
        if tag == "i":
            self.style = "roman"
        elif tag == "b":
            self.weight = "normal"
        elif tag == "small":
            self.size += 2
        elif tag == "big":
            self.size -= 4
        elif tag == "p":
            self.flush()
            self.cursor_y += VSTEP

class DocumentLayout:
    def __init__(self, node):
        self.node = node
        self.parent = None
        self.children = []

    def layout(self):
        child = BlockLayout(self.node, self, None)
        self.children.append(child)

        self.width = arcade.get_window().width - 2 * HSTEP
        self.x = HSTEP
        self.y = VSTEP
        child.layout()
        self.height = child.height
        self.display_list = child.display_list

    def paint(self):
        return []

def paint_tree(layout_object, display_list):
    display_list.extend(layout_object.paint())

    for child in layout_object.children:
        paint_tree(child, display_list)

class Renderer():
    def __init__(self, http_client: HTTPClient, window: arcade.Window):
        self.content = ''
        self.request_scheme = 'http'

        self.http_client = http_client

        self.scroll_y = 0
        self.scroll_y_speed = 50
        self.allow_scroll = False
        self.smallest_y = 0

        self.text_labels: list[pyglet.text.Label] = []
        self.text_to_create = []

        self.window = window
        self.window.on_mouse_scroll = self.on_mouse_scroll
        self.window.on_resize = self.on_resize

        self.batch = pyglet.graphics.Batch()

    def hide_out_of_bounds_labels(self):
        for widget in self.text_labels:
            invisible = (widget.y + widget.content_height) > self.window.height * 0.925
            # Doing visible flag set manually since it takes a lot of time            
            if widget.visible:
                if invisible:
                    widget.visible = False
            else:
                if not invisible:
                    widget.visible = True

    def on_resize(self, width, height):
        self.http_client.needs_render = True

    def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
        if not self.allow_scroll:
            return
        
        old_y = self.scroll_y
        self.scroll_y = max(0, min(abs(self.scroll_y - (scroll_y * self.scroll_y_speed)), abs(self.smallest_y) - (self.window.height * 0.925) - 10)) # flip scroll direction

        for widget in self.text_labels:
            widget.y += (self.scroll_y - old_y)

        self.hide_out_of_bounds_labels()

    def add_text(self, x, y, text, font, multiline=False):
        self.text_labels.append(
            pyglet.text.Label(
                text=text,
                font_name=font.name,
                italic=font.italic,
                weight=font.weight,
                font_size=font.size,
                multiline=multiline,
                color=arcade.color.BLACK,
                x=x,
                y=(self.window.height * 0.925) - y,
                batch=self.batch
            )
        )

        if (self.window.height * 0.925) - y < self.smallest_y:
            self.smallest_y = y

    def update(self):
        if not self.http_client.needs_render:
            return

        self.http_client.needs_render = False
        self.allow_scroll = True

        for child in self.text_labels:
            child.delete()
            del child
        
        self.text_labels.clear()
        self.smallest_y = 0
        
        if self.http_client.view_source or self.http_client.scheme == "file":
            self.add_text(x=HSTEP, y=0, text=self.http_client.content_response, font=pyglet.font.load("Roboto", 16), multiline=True)
        elif self.http_client.scheme == "http" or self.http_client.scheme == "https":
            if not os.path.exists("http_cache"):
                os.makedirs("http_cache")

            cache_filename = f"{self.http_client.scheme}_{self.http_client.host}_{self.http_client.port}_{self.http_client.path.replace('/', '_')}.json"

            if cache_filename in os.listdir("http_cache"):
                with open(f"http_cache/{cache_filename}", "r") as file:
                    self.nodes = HTML.from_json(ujson.load(file))
            else:
                self.nodes = HTML(self.http_client.content_response).parse()
                with open(f"http_cache/{cache_filename}", "w") as file:
                    json_list = HTML.to_json(self.nodes)
                    file.write(ujson.dumps(json_list))

            self.document = DocumentLayout(self.nodes)
            self.document.layout()
            self.display_list = []
            paint_tree(self.document, self.display_list)
            
            for x, y, text, font in self.display_list:
                self.add_text(x, y, text, font)

            self.hide_out_of_bounds_labels()