"""
tkinter HTML text widgets

MIT License
"""

# MIT License

# Copyright (c) 2020 Palash Bauri
# Copyright (c) 2018 paolo-gurisatti
# Copyright (c) 2022 Molecular Simulations Software Institute (MolSSI @ VT)

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from collections import OrderedDict
from copy import deepcopy
from html.parser import HTMLParser
from io import BytesIO
import os
import requests
import sys
import tkinter as tk
import tkinter.font as font
import tkinter.ttk as ttk
import webbrowser

from PIL import Image, ImageTk


class _ScrolledText(tk.Text):
    def __init__(self, master=None, **kw):
        self.frame = tk.Frame(master)

        self.vbar = ttk.Scrollbar(self.frame)
        kw.update({"yscrollcommand": self.vbar.set})
        self.vbar.pack(side=tk.RIGHT, fill=tk.Y)
        self.vbar["command"] = self.yview

        tk.Text.__init__(self, self.frame, **kw)
        self.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)

        text_meths = vars(tk.Text).keys()
        methods = vars(tk.Pack).keys() | vars(tk.Grid).keys() | vars(tk.Place).keys()
        methods = methods.difference(text_meths)

        for m in methods:
            if m[0] != "_" and m != "config" and m != "configure":
                setattr(self, m, getattr(self.frame, m))

    def __str__(self):
        return str(self.frame)


class HTMLScrolledText(_ScrolledText):
    """HTML scrolled text widget"""

    def __init__(self, *args, html=None, **kwargs):
        super().__init__(*args, **kwargs)
        self._w_init(kwargs)
        self.html_parser = HTMLTextParser()
        if isinstance(html, str):
            self.set_html(html)

    def _w_init(self, kwargs):
        if "wrap" not in kwargs.keys():
            self.config(wrap="word")
        if "background" not in kwargs.keys():
            if sys.platform.startswith("win"):
                self.config(background="SystemWindow")
            else:
                self.config(background="white")

    def fit_height(self):
        """
        Fit widget height to wrapped lines
        """
        for h in range(1, 4):
            self.config(height=h)
            self.master.update()
            if self.yview()[1] >= 1:
                break
        else:
            self.config(height=0.5 + 3 / self.yview()[1])

    def set_html(self, html, strip=True):
        """
        Set HTML widget text. If strip is enabled (default) it ignores spaces and new
        lines.
        """
        prev_state = self.cget("state")
        self.config(state=tk.NORMAL)
        self.delete("1.0", tk.END)
        self.tag_delete(self.tag_names)
        self.html_parser.w_set_html(self, html, strip=strip)
        self.config(state=prev_state)


class HTMLText(HTMLScrolledText):
    """
    HTML text widget
    """

    def _w_init(self, kwargs):
        super()._w_init(kwargs)
        self.vbar.pack_forget()

    def fit_height(self):
        super().fit_height()
        # self.master.update()
        self.vbar.pack_forget()


class HTMLLabel(HTMLText):
    """
    HTML label widget
    """

    def _w_init(self, kwargs):
        super()._w_init(kwargs)
        if "background" not in kwargs.keys():
            if sys.platform.startswith("win"):
                self.config(background="SystemButtonFace")
            else:
                self.config(background="#d9d9d9")

        if "borderwidth" not in kwargs.keys():
            self.config(borderwidth=0)

        if "padx" not in kwargs.keys():
            self.config(padx=3)

    def set_html(self, *args, **kwargs):
        super().set_html(*args, **kwargs)
        self.config(state=tk.DISABLED)


# ---------------------------------------------------------------------------------------
# HTML Parser
# ---------------------------------------------------------------------------------------
class Defs:
    DEFAULT_TEXT_FONT_FAMILY = ("Segoe ui", "Calibri", "Helvetica", "TkTextFont")
    FONT_SIZE = 14
    PREFORMATTED_FONT_FAMILY = ("Courier", "DejaVu Sans Mono", "TkFixedFont")
    HEADINGS_FONT_SIZE = {
        "h1": 32,
        "h2": 24,
        "h3": 18,
        "h4": 16,
        "h5": 13,
        "h6": 10,
    }


class HTML:
    """
    List of supported HTML tags and attrs
    """

    class Tag:
        BR = "br"
        UL = "ul"
        OL = "ol"
        LI = "li"
        IMG = "img"
        A = "a"
        B = "b"
        STRONG = "strong"
        I = "i"  # noqa: E741
        EM = "em"
        U = "u"
        MARK = "mark"
        SPAN = "span"
        DIV = "div"
        P = "p"
        PRE = "pre"
        CODE = "code"
        H1 = "h1"
        H2 = "h2"
        H3 = "h3"
        H4 = "h4"
        H5 = "h5"
        H6 = "h6"

    class Attrs:
        STYLE = "style"
        HREF = "href"
        SRC = "src"
        WIDTH = "width"
        HEIGHT = "height"
        TYPE = "type"

    class TypeOrderedList:
        _1 = "1"
        a = "a"
        A = "A"

    class Style:
        COLOR = "color"
        BACKGROUND_COLOR = "background-color"
        FONT_FAMILY = "font-family"
        FONT_SIZE = "font-size"
        TEXT_ALIGN = "text-align"
        TEXT_DECORATION = "text-decoration"

    class StyleTextDecoration:
        UNDERLINE = "underline"
        LINE_THROUGH = "line-through"

    HEADING_TAGS = (
        Tag.H1,
        Tag.H2,
        Tag.H3,
        Tag.H4,
        Tag.H5,
        Tag.H6,
    )

    TEXT_ALIGN_TAGS = HEADING_TAGS + (
        Tag.UL,
        Tag.OL,
        Tag.LI,
        Tag.DIV,
        Tag.P,
        Tag.PRE,
        Tag.CODE,
    )

    NEW_LINE_TAGS = HEADING_TAGS + (
        Tag.UL,
        Tag.OL,
        Tag.DIV,
        Tag.P,
        Tag.PRE,
        Tag.CODE,
    )

    STYLE_TAGS = TEXT_ALIGN_TAGS + (
        Tag.A,
        Tag.B,
        Tag.STRONG,
        Tag.I,
        Tag.EM,
        Tag.U,
        Tag.MARK,
        Tag.SPAN,
    )


# --------------------------------------------------------------------------------------------------
# Text widget defs


class WCfg:
    KEY = "config"
    BACKGROUND = "background"
    FOREGROUND = "foreground"
    JUSTIFY = "justify"
    TABS = "tabs"


class Fnt:
    KEY = "font"
    FAMILY = "family"
    SIZE = "size"
    WEIGHT = "weight"
    SLANT = "slant"
    UNDERLINE = "underline"
    OVERSTRIKE = "overstrike"


class Bind:
    KEY = "bind"
    LINK = "link"
    IMAGE = "image"


class WTag:
    START_INDEX = "start_index"
    END_INDEX = "end_index"


DEFAULT_STACK = {
    WCfg.KEY: {
        WCfg.BACKGROUND: [],
        WCfg.FOREGROUND: [("__DEFAULT__", "black")],
        WCfg.JUSTIFY: [("__DEFAULT__", "left")],
        WCfg.TABS: [("__DEFAULT__", ())],
    },
    Fnt.KEY: {
        Fnt.FAMILY: [],
        Fnt.SIZE: [("__DEFAULT__", Defs.FONT_SIZE)],
        Fnt.WEIGHT: [("__DEFAULT__", "normal")],
        Fnt.SLANT: [("__DEFAULT__", "roman")],
        Fnt.UNDERLINE: [("__DEFAULT__", False)],
        Fnt.OVERSTRIKE: [("__DEFAULT__", False)],
    },
    Bind.KEY: {
        Bind.LINK: [("__DEFAULT__", None)],
    },
}


def get_existing_font(font_families):
    try:
        return next(
            filter(
                lambda f: f.lower() in (f.lower() for f in font.families()),
                font_families,
            )
        )
    except Exception:
        return "TkTextFont"


class HLinkSlot:
    def __init__(self, w, tag_name, url):
        self._w = w
        self.tag_name = tag_name
        self.URL = url

    def call(self, event):
        webbrowser.open(self.URL)
        self._w.tag_config(self.tag_name, foreground="purple")

    def enter(self, event):
        self._w.config(cursor="hand2")

    def leave(self, event):
        self._w.config(cursor="")


class ListTag:
    def __init__(self, ordered: bool, list_type=None):
        self.ordered = ordered
        self.type = list_type
        self.index = 0

    def add(self):
        if self.ordered:
            self.index += 1

    def line_index(self):
        if self.ordered:
            if self.type == HTML.TypeOrderedList._1:
                return str(self.index)
            elif self.type == HTML.TypeOrderedList.a:
                return self._index_to_str(self.index).lower()
            elif self.type == HTML.TypeOrderedList.A:
                return self._index_to_str(self.index).upper()
        else:
            return chr(8226)

    def _index_to_str(self, index):
        prefix = ""
        if index > 26:
            prefix = self._index_to_str(index // 26)
            index = index % 26

        return prefix + chr(0x60 + index)


class HTMLTextParser(HTMLParser):
    def __init__(self):
        super().__init__()
        # set list tabs
        self.cached_images = {}

        self.DEFAULT_TEXT_FONT_FAMILY = get_existing_font(Defs.DEFAULT_TEXT_FONT_FAMILY)
        self.PREFORMATTED_FONT_FAMILY = get_existing_font(Defs.PREFORMATTED_FONT_FAMILY)

    def _parse_attrs(self, attrs):
        attrs_dict = {
            HTML.Attrs.STYLE: {},
            HTML.Attrs.HREF: None,
            HTML.Attrs.SRC: None,
            HTML.Attrs.WIDTH: None,
            HTML.Attrs.HEIGHT: None,
            HTML.Attrs.TYPE: None,
        }
        for k, v in attrs:
            k = k.lower()
            if k == HTML.Attrs.STYLE:
                for p in v.split(";"):
                    try:
                        p_key = p.split(":")[0].strip().lower()
                        p_value = p.split(":")[1].strip().lower()
                        attrs_dict[HTML.Attrs.STYLE][p_key] = p_value
                    except Exception:
                        pass
            elif k in (
                HTML.Attrs.HREF,
                HTML.Attrs.SRC,
                HTML.Attrs.WIDTH,
                HTML.Attrs.HEIGHT,
                HTML.Attrs.TYPE,
            ):
                attrs_dict[k] = v
        return attrs_dict

    def _w_tags_add(self):
        tag = {WCfg.KEY: {}, Fnt.KEY: {}, Bind.KEY: {}}

        for k1 in (WCfg.KEY, Fnt.KEY, Bind.KEY):
            for k2 in DEFAULT_STACK[k1]:
                tag[k1][k2] = self.stack[k1][k2][-1][1]

        self._w_tags[self._w.index("end-1c")] = tag

    def _stack_get_main_key(self, key):
        if key in WCfg.__dict__.values():
            main_key = WCfg.KEY
        elif key in Fnt.__dict__.values():
            main_key = Fnt.KEY
        elif key in Bind.__dict__.values():
            main_key = Bind.KEY
        else:
            raise ValueError("key %s doesn't exists" % key)

        return main_key

    def _stack_add(self, tag, key, value=None):
        main_key = self._stack_get_main_key(key)

        if value is None:
            # if value is none, add the previous value
            value = self.stack[main_key][key][-1][1]

        self.stack[main_key][key].append((tag, value))

    def _stack_index(self, tag, key):
        main_key = self._stack_get_main_key(key)
        index = None
        for i, v in enumerate(self.stack[main_key][key]):
            if v[0] == tag:
                index = i

        return index

    def _stack_pop(self, tag, key):
        main_key = self._stack_get_main_key(key)

        index = None
        if len(self.stack[main_key][key]) > 1:
            index = self._stack_index(tag, key)

        if index is not None:
            return self.stack[main_key][key].pop(index)[1]

    def _parse_styles(self, tag, attrs):
        # [ COLOR ]
        if HTML.Style.COLOR in attrs[HTML.Attrs.STYLE].keys():
            self._stack_add(
                tag, WCfg.FOREGROUND, attrs[HTML.Attrs.STYLE][HTML.Style.COLOR]
            )
        elif tag == HTML.Tag.A and attrs[HTML.Attrs.HREF]:
            self._stack_add(tag, WCfg.FOREGROUND, "blue")
        else:
            self._stack_add(tag, WCfg.FOREGROUND)

        # [ BACKGROUND_COLOR ]
        if HTML.Style.BACKGROUND_COLOR in attrs[HTML.Attrs.STYLE].keys():
            self._stack_add(
                tag,
                WCfg.BACKGROUND,
                attrs[HTML.Attrs.STYLE][HTML.Style.BACKGROUND_COLOR],
            )
        elif tag == HTML.Tag.MARK:
            self._stack_add(tag, WCfg.BACKGROUND, "yellow")
        else:
            self._stack_add(tag, WCfg.BACKGROUND)

        # [ FONT_FAMILY ]
        if HTML.Style.FONT_FAMILY in attrs[HTML.Attrs.STYLE].keys():
            font_family = Defs.DEFAULT_TEXT_FONT_FAMILY
            for f in attrs[HTML.Attrs.STYLE][HTML.Style.FONT_FAMILY].split(","):
                f = f.strip()
                if f in map(lambda f: f.lower(), font.families()):
                    font_family = f
                    break
            self._stack_add(tag, Fnt.FAMILY, font_family)
        elif tag in (HTML.Tag.PRE, HTML.Tag.CODE):
            self._stack_add(tag, Fnt.FAMILY, self.PREFORMATTED_FONT_FAMILY)
        else:
            self._stack_add(tag, Fnt.FAMILY)

        # [ FONT_SIZE ]
        if HTML.Style.FONT_SIZE in attrs[HTML.Attrs.STYLE].keys():
            size = Defs.FONT_SIZE
            if attrs[HTML.Attrs.STYLE][HTML.Style.FONT_SIZE].endswith("px"):
                if attrs[HTML.Attrs.STYLE][HTML.Style.FONT_SIZE][:-2].isdigit():
                    size = int(attrs[HTML.Attrs.STYLE][HTML.Style.FONT_SIZE][:-2])
            elif attrs[HTML.Attrs.STYLE][HTML.Style.FONT_SIZE].endswith(r"%"):
                if attrs[HTML.Attrs.STYLE][HTML.Style.FONT_SIZE][:-1].isdigit():
                    size = int(
                        (
                            int(attrs[HTML.Attrs.STYLE][HTML.Style.FONT_SIZE][:-1])
                            * Defs.FONT_SIZE
                        )
                        / 100
                    )
            self._stack_add(tag, Fnt.SIZE, size)
        elif tag.startswith("h") and len(tag) == 2:
            self._stack_add(tag, Fnt.SIZE, Defs.HEADINGS_FONT_SIZE[tag])
        else:
            self._stack_add(tag, Fnt.SIZE)

        # [ TEXT_ALIGN ]
        if (
            HTML.Style.TEXT_ALIGN in attrs[HTML.Attrs.STYLE].keys()
            and tag in HTML.TEXT_ALIGN_TAGS
        ):
            self._stack_add(
                tag, WCfg.JUSTIFY, attrs[HTML.Attrs.STYLE][HTML.Style.TEXT_ALIGN]
            )
        else:
            self._stack_add(tag, WCfg.JUSTIFY)

        # [ TEXT_DECORATION ]
        if HTML.Style.TEXT_DECORATION in attrs[HTML.Attrs.STYLE].keys():
            if tag == HTML.Tag.STRONG:
                self._stack_add(tag, Fnt.UNDERLINE, False)
                self._stack_add(tag, Fnt.OVERSTRIKE, False)
            elif (
                HTML.StyleTextDecoration.UNDERLINE
                in attrs[HTML.Attrs.STYLE][HTML.Style.TEXT_DECORATION]
            ):
                self._stack_add(tag, Fnt.UNDERLINE, True)
                self._stack_add(tag, Fnt.OVERSTRIKE, False)
            elif (
                HTML.StyleTextDecoration.LINE_THROUGH
                in attrs[HTML.Attrs.STYLE][HTML.Style.TEXT_DECORATION]
            ):
                self._stack_add(tag, Fnt.UNDERLINE, False)
                self._stack_add(tag, Fnt.OVERSTRIKE, True)
            else:
                self._stack_add(tag, Fnt.UNDERLINE)
                self._stack_add(tag, Fnt.OVERSTRIKE)
        else:
            if tag == HTML.Tag.A and attrs[HTML.Attrs.HREF]:
                self._stack_add(tag, Fnt.UNDERLINE, True)
                self._stack_add(tag, Fnt.OVERSTRIKE, False)
            elif tag == HTML.Tag.U:
                self._stack_add(tag, Fnt.UNDERLINE, True)
                self._stack_add(tag, Fnt.OVERSTRIKE, False)
            else:
                self._stack_add(tag, Fnt.UNDERLINE)
                self._stack_add(tag, Fnt.OVERSTRIKE)

    def handle_starttag(self, tag, attrs):
        tag = tag.lower()
        attrs = self._parse_attrs(attrs)

        if tag in HTML.STYLE_TAGS:
            # [ STYLED_TAGS ]
            self._parse_styles(tag, attrs)

            if tag == HTML.Tag.B or tag == HTML.Tag.STRONG or tag in HTML.HEADING_TAGS:
                self._stack_add(tag, Fnt.WEIGHT, "bold")

            elif tag == HTML.Tag.I or tag == HTML.Tag.EM:
                self._stack_add(tag, Fnt.SLANT, "italic")

            elif tag == HTML.Tag.A:
                self._stack_add(tag, Bind.LINK, attrs[HTML.Attrs.HREF])

            elif tag == HTML.Tag.OL:
                # [ ORDERED_LISTS ]
                if (
                    attrs[HTML.Attrs.TYPE]
                    and attrs[HTML.Attrs.TYPE] in HTML.TypeOrderedList.__dict__.values()
                ):
                    list_type = attrs[HTML.Attrs.TYPE]
                else:
                    list_type = HTML.TypeOrderedList._1
                self.list_tags.append(ListTag(ordered=True, list_type=list_type))

                tabs = []
                for i in range(len(self.list_tags)):
                    offset = 30 * (i + 1)
                    tabs += [offset, tk.RIGHT, offset + 5, tk.LEFT]
                self._stack_add(tag, WCfg.TABS, tabs)

            elif tag == HTML.Tag.UL:
                # [ UNORDERED_LISTS ]
                self.list_tags.append(ListTag(ordered=False))

                tabs = []
                for i in range(len(self.list_tags)):
                    offset = 30 * (i + 1)
                    tabs += [offset, tk.RIGHT, offset + 5, tk.LEFT]
                self._stack_add(tag, WCfg.TABS, tabs)

            elif tag == HTML.Tag.LI:
                # [ LISTS_LINES ]
                level = len(self.list_tags)
                if level:
                    self.list_tags[-1].add()

                    if self.strip:
                        self._insert_new_line()

                    line_index = self.list_tags[-1].line_index()
                    if self.list_tags[-1].ordered:
                        line_index = "\t" + "\t\t" * (level - 1) + line_index + ".\t"
                    else:
                        line_index = "\t" + "\t\t" * (level - 1) + line_index + "\t"

                    self._stack_add(tag, Fnt.UNDERLINE, False)
                    self._stack_add(tag, Fnt.OVERSTRIKE, False)
                    self._w_tags_add()
                    self._w.insert(tk.INSERT, line_index)
                    self._stack_pop(tag, Fnt.UNDERLINE)
                    self._stack_pop(tag, Fnt.OVERSTRIKE)

        elif tag == HTML.Tag.IMG and attrs[HTML.Attrs.SRC]:
            # [ UNSTYLED_TAGS ]
            image = None
            if attrs[HTML.Attrs.SRC].startswith(("https://", "ftp://", "http://")):
                if attrs[HTML.Attrs.SRC] in self.cached_images.keys():
                    image = deepcopy(self.cached_images[attrs[HTML.Attrs.SRC]])
                else:
                    try:
                        image = Image.open(
                            BytesIO(requests.get(attrs[HTML.Attrs.SRC]).content)
                        )
                        self.cached_images[attrs[HTML.Attrs.SRC]] = deepcopy(image)
                    except Exception:
                        pass

            if attrs[HTML.Attrs.SRC] in self.cached_images.keys():
                image = deepcopy(self.cached_images[attrs[HTML.Attrs.SRC]])
            elif os.path.exists(attrs[HTML.Attrs.SRC]):
                image = Image.open(attrs[HTML.Attrs.SRC])
                self.cached_images[attrs[HTML.Attrs.SRC]] = deepcopy(image)
            if image:
                width = image.size[0]
                height = image.size[1]
                resize = False
                if str(attrs[HTML.Attrs.WIDTH]).isdigit():
                    width = int(attrs[HTML.Attrs.WIDTH])
                    resize = True
                if str(attrs[HTML.Attrs.HEIGHT]).isdigit():
                    height = int(attrs[HTML.Attrs.HEIGHT])
                    resize = True
                if resize:
                    image = image.resize((width, height), Image.ANTIALIAS)
                self.images.append(ImageTk.PhotoImage(image))
                self._w.image_create(tk.INSERT, image=self.images[-1])

        if self.strip:
            # [ NEW_LINES ]
            if tag == HTML.Tag.BR:
                self._insert_new_line()
            else:
                self.html_tags.append(tag)

            if (
                tag in HTML.NEW_LINE_TAGS
                and self.strip
                and self._w.index("end-1c") != "1.0"
            ):
                if tag in (HTML.Tag.DIV,):
                    self._insert_new_line()
                elif tag in (HTML.Tag.UL, HTML.Tag.OL):
                    if len(self.list_tags) == 1:
                        self._insert_new_line(double=True)
                    else:
                        self._insert_new_line(double=False)
                else:
                    self._insert_new_line(double=True)

        self._w_tags_add()

    def handle_charref(self, data):
        try:
            char = chr(int(data))
            self._w.insert(tk.INSERT, char)
        except Exception:
            pass

    def _insert_new_line(self, double=False):
        self._remove_last_space()
        if self._w.get("end-3c", "end-1c") == "\n\n":
            pass
        elif self._w.get("end-2c", "end-1c") == "\n":
            if double:
                self._w.insert(tk.INSERT, "\n")
        else:
            if double:
                self._w.insert(tk.INSERT, "\n\n")
            else:
                self._w.insert(tk.INSERT, "\n")

    def _text_rstrip(self):
        for _ in range(3):
            if self._w.get("end-2c", "end-1c") in (" ", "\n"):
                self._w.delete("end-2c", "end-1c")

    def _remove_last_space(self):
        if self._w.get("end-2c", "end-1c") == " ":
            self._w.delete("end-2c", "end-1c")

    def _remove_multi_spaces(self, data):
        data = data.replace("  ", " ")
        if "  " in data:
            data = self._remove_multi_spaces(data)
        return data

    def handle_data(self, data):
        if self.strip:
            if len(self.html_tags) and self.html_tags[-1] in (
                HTML.Tag.PRE,
                HTML.Tag.CODE,
            ):
                pass
            elif not data.strip():
                data = ""
            else:
                # left strip
                if self._w.index("end-1c").endswith(".0"):
                    data = data.lstrip()
                elif self._w.get("end-2c", "end-1c") == " ":
                    data = data.lstrip()

                data = data.replace("\n", " ").replace("\t", " ")
                data = data + " "
                data = self._remove_multi_spaces(data)
                if len(self.html_tags):
                    level = len(self.list_tags)
                    if self.html_tags[-1] in (HTML.Tag.UL, HTML.Tag.OL):
                        self._w.insert(tk.INSERT, "\t" * 2 * level)

        self._w.insert(tk.INSERT, data)

    def handle_endtag(self, tag):
        tag = tag.lower()

        try:
            index = len(self.html_tags) - self.html_tags[::-1].index(tag) - 1
            self.html_tags.pop(index)
        except Exception:
            pass

        if tag in HTML.STYLE_TAGS:

            self._stack_pop(tag, WCfg.FOREGROUND)
            self._stack_pop(tag, WCfg.BACKGROUND)
            self._stack_pop(tag, WCfg.JUSTIFY)
            self._stack_pop(tag, Fnt.FAMILY)
            self._stack_pop(tag, Fnt.SIZE)
            self._stack_pop(tag, Fnt.UNDERLINE)
            self._stack_pop(tag, Fnt.OVERSTRIKE)

            if tag == HTML.Tag.B or tag == HTML.Tag.STRONG or tag in HTML.HEADING_TAGS:
                self._stack_pop(tag, Fnt.WEIGHT)

            elif tag == HTML.Tag.I or tag == HTML.Tag.EM:
                self._stack_pop(tag, Fnt.SLANT)

            elif tag == HTML.Tag.A:
                self._stack_pop(tag, Bind.LINK)

            elif tag == HTML.Tag.OL or tag == HTML.Tag.UL:
                if len(self.list_tags):
                    self.list_tags = self.list_tags[:-1]

                self._stack_pop(tag, WCfg.TABS)

        if tag in HTML.NEW_LINE_TAGS and self.strip:
            self._insert_new_line()

        self._w_tags_add()

        if tag in HTML.NEW_LINE_TAGS and self.strip:
            if tag in (HTML.Tag.DIV, HTML.Tag.UL, HTML.Tag.OL):
                if not len(self.list_tags):
                    self._insert_new_line(double=True)
            else:
                self._insert_new_line(double=True)

    def _w_tags_apply_all(self):
        # update indexes
        if self.strip:
            self._text_rstrip()
        end_index = tk.END
        for key, tag in reversed(tuple(self._w_tags.items())):
            tag[WTag.START_INDEX] = key
            tag[WTag.END_INDEX] = end_index
            end_index = key

        # add tags
        self.hlink_slots = []
        for key, tag in self._w_tags.items():
            self._w.tag_add(key, tag[WTag.START_INDEX], tag[WTag.END_INDEX])
            self._w.tag_config(key, font=font.Font(**tag[Fnt.KEY]), **tag[WCfg.KEY])
            if tag[Bind.KEY][Bind.LINK]:
                self.hlink_slots.append(
                    HLinkSlot(self._w, key, tag[Bind.KEY][Bind.LINK])
                )
                self._w.tag_bind(key, "<Button-1>", self.hlink_slots[-1].call)
                self._w.tag_bind(key, "<Leave>", self.hlink_slots[-1].leave)
                self._w.tag_bind(key, "<Enter>", self.hlink_slots[-1].enter)

    def w_set_html(self, w, html, strip):
        self._w = w
        self.stack = deepcopy(DEFAULT_STACK)
        self.stack[WCfg.KEY][WCfg.BACKGROUND].append(
            ("__DEFAULT__", self._w.cget("background"))
        )
        self.stack[Fnt.KEY][Fnt.FAMILY].append(
            ("__DEFAULT__", self.DEFAULT_TEXT_FONT_FAMILY)
        )
        self._w_tags = OrderedDict()
        self.html_tags = []
        self.images = []
        self.list_tags = []
        self.strip = strip
        self._w_tags_add()
        self.feed(html)
        self._w_tags_apply_all()
        del self._w
