
from typing import Dict, List, Union, Callable

from . import HTML

from .bootstrap_HTML_container import *
from js import bootstrap  # type: ignore


class Button(HTML.Button, BootstrapContainer):
    _default_class_name = "btn"

    def make_large(self):
        self.remove_class("btn-sm")
        self.add_class("btn-lg")

    def make_small(self):
        self.remove_class("btn-lg")
        self.add_class("btn-sm")

    def make_collapse_toggle(self, target_element: HTML.Element):
        self.set_attribute("data-bs-toggle", "collapse")
        self.set_attribute("data-bs-target", f"#{target_element.id}")

    @property
    def is_toggle_button(self) -> bool:
        return self.has_attribute("data-bs-toggle")

    @is_toggle_button.setter
    def is_toggle_button(self, value: bool):
        if value:
            self.set_attribute("data-bs-toggle", "button")
            self.set_attribute("autocomplete", "off")
        else:
            self.remove_attribute("data-bs-toggle")
            self.remove_attribute("autocomplete")

    @property
    def is_toggle_button_active(self) -> Union[bool, None]:
        """
        returns the current state of the toggle button. If this is not a toggle button, returns None
        (use is_toggle_button to control whether this is a toggle button)
        """
        if not self.is_toggle_button:
            return None
        return self.has_class("active")

    @is_toggle_button_active.setter
    def is_toggle_button_active(self, value: bool):
        if not self.is_toggle_button:
            raise ValueError("This is not a toggle button")
        if value:
            self.add_class("active")
            self.set_attribute("aria-pressed", True)
        else:
            self.remove_class("active")
            self.set_attribute("aria-pressed", False)

    def toggle(self):
        self.is_toggle_button_active = not self.is_toggle_button_active

# Accordion: ------------------------------------------------------------------


class AccordionHeader(HTML.H2, BootstrapContainer):

    _default_class_name: str = "accordion-header"


class AccordionBody(BootstrapContainer):

    _default_class_name: str = "accordion-body"


class AccordionItem(BootstrapContainer):

    _default_class_name: str = "accordion-item"

    def __init__(self,
                 content: Union[str, HTML.Element],
                 header_title: str,
                 show_on_default: bool = False,
                 stay_open: bool = False,
                 id=None,
                 class_name=None,
                 parent=None,
                 inner_html=None) -> None:
        super().__init__(id=id,
                         class_name=class_name,
                         parent=parent,
                         inner_html=inner_html)

        header = AccordionHeader()

        button = Button(class_name="accordion-button",
                        inner_html=header_title, parent=header)
        collapsable_body_container = BootstrapContainer(
            class_name="accordion-collapse collapse")
        if show_on_default:
            collapsable_body_container.add_class("show")
        else:
            button.add_class("collapsed")
        if not stay_open:
            collapsable_body_container.set_attribute(
                "data-bs-parent", f"#{parent.id}")
        body = AccordionBody(parent=collapsable_body_container)

        if isinstance(content, str):
            body.inner_html = content
        else:
            body.append_child(content)

        button.make_collapse_toggle(collapsable_body_container)

        self._header = header
        self._body = body

        self.append_child(header)
        self.append_child(collapsable_body_container)

    @property
    def header(self) -> AccordionHeader:
        return self._header

    @property
    def body(self) -> AccordionBody:
        return self._body


class Accordion(BootstrapContainer):

    _default_class_name: str = "accordion"

    def add_accordion_item(self,
                           content: Union[str, HTML.Element],
                           header_title: str,
                           show_on_default: bool = False,
                           stay_open: bool = False) -> None:
        return AccordionItem(content=content,
                             header_title=header_title,
                             parent=self,
                             show_on_default=show_on_default,
                             stay_open=stay_open)


class AccordionFlush(Accordion):

    _default_class_name: str = "accordion-flush"

# Alerts: ---------------------------------------------------------------------


class Alert(BootstrapContainer):

    _default_class_name: str = "alert"

    def __init__(self,
                 content: Union[str, HTML.Element],
                 show_close_button: bool = True,
                 fade: bool = True,
                 id=None,
                 class_name=None,
                 parent=None,
                 inner_html=None) -> None:
        super().__init__(id=id,
                         class_name=class_name,
                         parent=parent,
                         inner_html=inner_html)

        if isinstance(content, str):
            self.inner_html = content
        else:
            self.append_child(content)

        self.set_attribute("role", "alert")

        if fade:
            self.add_class("fade")

        if show_close_button:
            self.add_classes("alert-dismissible", "show")
            close_btn = Button(class_name="btn-close")
            close_btn.set_attribute("data-bs-dismiss", "alert")
            close_btn.set_attribute("aria-label", "Close")
            self.append_child(close_btn)


class AlertPrimary(Alert):

    _default_class_name: str = "alert alert-primary"


class AlertSecondary(Alert):

    _default_class_name: str = "alert alert-secondary"


class AlertSuccess(Alert):

    _default_class_name: str = "alert alert-success"


class AlertDanger(Alert):

    _default_class_name: str = "alert alert-danger"


class AlertWarning(Alert):

    _default_class_name: str = "alert alert-warning"


class AlertInfo(Alert):

    _default_class_name: str = "alert alert-info"


class AlertLight(Alert):

    _default_class_name: str = "alert alert-light"


class AlertDark(Alert):

    _default_class_name: str = "alert alert-dark"

# Badges: ---------------------------------------------------------------------


class Badge(HTML.Span, BootstrapContainer):

    _default_class_name: str = "badge"

    def __init__(self,
                 content: Union[str, HTML.Element],
                 pill: bool = False,
                 id=None,
                 class_name=None,
                 parent=None,
                 inner_html=None) -> None:
        super().__init__(id=id,
                         class_name=class_name,
                         parent=parent,
                         inner_html=inner_html)

        if isinstance(content, str):
            self.inner_html = content
        else:
            self.append_child(content)

        if pill:
            self.add_class("rounded-pill")


class BadgePrimary(Badge):

    _default_class_name: str = "badge bg-primary"


class BadgeSecondary(Badge):

    _default_class_name: str = "badge bg-secondary"


class BadgeSuccess(Badge):

    _default_class_name: str = "badge bg-success"


class BadgeDanger(Badge):

    _default_class_name: str = "badge bg-danger"


class BadgeWarning(Badge):

    _default_class_name: str = "badge bg-warning"


class BadgeInfo(Badge):

    _default_class_name: str = "badge bg-info"


class BadgeLight(Badge):

    _default_class_name: str = "badge bg-light"


class BadgeDark(Badge):

    _default_class_name: str = "badge bg-dark"

# Breadcrumb: -----------------------------------------------------------------


class Breadcrumb(HTML.Nav, BootstrapContainer):

    _default_class_name: str = "breadcrumb"

    def __init__(self,
                 id=None,
                 class_name=None,
                 parent=None,
                 path: List[str] = None,
                 links: List[Union[str, None]] = None,
                 custom_divider: str = None,
                 inner_html=None) -> None:
        super().__init__(id=id,
                         class_name=class_name,
                         parent=parent,
                         inner_html=inner_html)

        self.set_attribute("aria-label", "breadcrumb")

        if custom_divider is not None:
            self.element.style = f"--bs-breadcrumb-divider: '{custom_divider}';"

        self._ordered_list = None
        self._paths = []
        self._links = []

        if path is not None and links is not None:
            self.set_path(path, links)

    def _add_breadcrumb_item(self, path_part, link, is_active: bool = False) -> None:
        li = HTML.Li(parent=self._ordered_list, class_name="breadcrumb-item")
        if is_active:
            li.add_class("active")
            li.set_attribute("aria-current", "page")
        if link is not None:
            li.append_child(HTML.A(href=link, inner_html=path_part))
        else:
            li.inner_html = path_part
        return li

    def _update_path(self):
        if self._ordered_list is not None:
            self._ordered_list.destroy()
        self._ordered_list = HTML.Ol(parent=self)

        n = len(self._paths)
        for i, (part, link) in enumerate(zip(self._paths, self._links)):
            self._add_breadcrumb_item(part, link, is_active=i == n-1)

    def set_path(self, path: List[str], links: List[str] = None) -> None:
        if links is None:
            links = [None] * len(path)

        self._paths = path
        self._links = links

        self._update_path()

    @property
    def path(self) -> List[str]:
        return self._paths

    @property
    def links(self) -> List[str]:
        return self._links


# Buttons:---------------------------------------------------------------------


class ButtonPrimary(Button, BootstrapContainer):
    _default_class_name = "btn btn-primary"


class ButtonSecondary(Button):
    _default_class_name = "btn btn-secondary"


class ButtonSuccess(Button):
    _default_class_name = "btn btn-success"


class ButtonDanger(Button):
    _default_class_name = "btn btn-danger"


class ButtonWarning(Button):
    _default_class_name = "btn btn-warning"


class ButtonInfo(Button):
    _default_class_name = "btn btn-info"


class ButtonLight(Button):
    _default_class_name = "btn btn-light"


class ButtonDark(Button):
    _default_class_name = "btn btn-dark"


class ButtonLink(Button):
    _default_class_name = "btn btn-link"


class ButtonOutlinePrimary(Button):
    _default_class_name = "btn btn-outline-primary"


class ButtonOutlineSecondary(Button):
    _default_class_name = "btn btn-outline-secondary"


class ButtonOutlineSuccess(Button):
    _default_class_name = "btn btn-outline-success"


class ButtonOutlineDanger(Button):
    _default_class_name = "btn btn-outline-danger"


class ButtonOutlineWarning(Button):
    _default_class_name = "btn btn-outline-warning"


class ButtonOutlineInfo(Button):
    _default_class_name = "btn btn-outline-info"


class ButtonOutlineLight(Button):
    _default_class_name = "btn btn-outline-light"


class ButtonOutlineDark(Button):
    _default_class_name = "btn btn-outline-dark"


class ButtonClose(Button):
    _default_class_name = "btn btn-close"


class ButtonCloseWhite(ButtonClose):
    _default_class_name = "btn btn-close-white"

# Button Groups:---------------------------------------------------------------


class ButtonGroup(BootstrapContainer):
    _default_class_name = "btn-group"

    def add_button(self, button: Button):
        self.append_child(button)


class ButtonGroupVertical(ButtonGroup):
    _default_class_name = "btn-group-vertical"


class ButtonGroupToolbar(ButtonGroup):
    _default_class_name = "btn-toolbar"


class ButtonHamburger(Button):

    _default_class_name: str = "navbar-toggler"

    def __init__(self, inner_html: str = None, id: str = None, class_name: str = None, parent: HTML.Element = None, type: str = "button", name: str = None, value: str = None, onclick=None) -> None:
        super().__init__(inner_html=inner_html, id=id, class_name=class_name,
                         parent=parent, type=type, name=name, value=value, onclick=onclick)

        HTML.Span(parent=self, class_name="navbar-toggler-icon")

# Bootstrap Media: ------------------------------------------------------------


class Image(HTML.Image, BootstrapContainer):
    _default_class_name = "img-responsive"


class Video(HTML.Video, BootstrapContainer):
    _default_class_name = "img-responsive"


class ImageFluid(Image):
    _default_class_name = "img-fluid"


class Thumbnail(Image):
    _default_class_name = "img-thumbnail"


# Cards:-----------------------------------------------------------------------


class Card(BootstrapContainer):
    _default_class_name = "card"

    def __init__(self, card_body: HTML.Div,
                 title_image: Image = None,
                 card_header: HTML.Div = None,
                 inner_html: str = None,
                 id: str = None,
                 class_name: str = None,
                 parent: HTML.Element = None) -> None:

        super().__init__(inner_html, id, class_name, parent)

        if card_body is not None:
            card_body.add_class("card-body")

        if title_image is not None:
            title_image.add_class("card-img-top")

        if card_header is not None:
            card_header.class_name = "card-header"

        self._card_body = card_body
        self._title_image = title_image
        self._card_header = card_header

        self.append_child(card_body)
        self.append_child(title_image)
        self.append_child(card_header)

    @property
    def card_body(self) -> HTML.Div:
        return self._card_body

    @card_body.setter
    def card_body(self, card_body: HTML.Div) -> None:
        card_body.class_name = "card-body"
        if self._card_body is not None:
            self._card_body.destroy()
        self._card_body = card_body
        self.append_child(card_body)

    @property
    def title_image(self) -> HTML.Image:
        return self._title_image

    @title_image.setter
    def title_image(self, title_image: HTML.Image) -> None:
        title_image.class_name = "card-img-top"
        if self._title_image is not None:
            self._title_image.destroy()
        self._title_image = title_image
        self.append_child(title_image)

    @property
    def card_header(self) -> HTML.Div:
        return self._card_header

    @card_header.setter
    def card_header(self, card_header: HTML.Div) -> None:
        card_header.class_name = "card-header"
        if self._card_header is not None:
            self._card_header.destroy()
        self._card_header = card_header
        self.append_child(card_header)

# carousel:--------------------------------------------------------------------


class Carousel(BootstrapContainer):
    _default_class_name = "carousel slide"

    def __init__(self,
                 id: str = None,
                 class_name: str = None,
                 parent: HTML.Element = None,
                 inner_html: str = None,
                 with_controls: bool = False,
                 with_indicators: bool = False,
                 fade_animation: bool = False) -> None:

        super().__init__(inner_html=inner_html, id=id, class_name=class_name, parent=parent)

        self.set_attribute("data-bs-ride", "carousel")

        if fade_animation:
            self.add_class("carousel-fade")

        self._controls = None
        self._control_prev = None
        self._control_next = None
        self._indicators = None
        self._carousel_inner = BootstrapContainer(
            class_name="carousel-inner", parent=self)

        self._slides = []

        if with_controls:
            self._controls = BootstrapContainer(
                class_name="carousel-controls", parent=self)
            self._control_prev = HTML.Button(
                class_name="carousel-control-prev", parent=self._controls)
            self._control_prev.set_attribute("data-bs-target", "#" + self.id)
            self._control_prev.set_attribute("data-bs-slide", "prev")
            self._control_prev.set_attribute("aria-label", "Previous")

            prev_icon = HTML.Span(
                class_name="carousel-control-prev-icon", parent=self._control_prev)
            prev_icon.set_attribute("aria-hidden", "true")

            HTML.Span(class_name="visually-hidden", parent=self._control_prev)

            self._control_next = HTML.Button(
                class_name="carousel-control-next", parent=self._controls)
            self._control_next.set_attribute("data-bs-target", "#" + self.id)
            self._control_next.set_attribute("data-bs-slide", "next")
            self._control_next.set_attribute("aria-label", "Next")

            next_icon = HTML.Span(
                class_name="carousel-control-next-icon", parent=self._control_next)
            next_icon.set_attribute("aria-hidden", "true")

            HTML.Span(class_name="visually-hidden", parent=self._control_next)

        if with_indicators:

            self._indicators = BootstrapContainer(
                class_name="carousel-indicators", parent=self)

    def add_slide(self, slide: BootstrapContainer, is_active: bool = False, interval: int = None) -> None:
        item = HTML.Div(parent=self._carousel_inner)
        item.add_class("carousel-item")
        if is_active:
            item.add_class("active")

        if interval is not None:
            item.set_attribute("data-bs-interval", str(interval))

        slide.display_property = DisplayProperty.BLOCK
        slide.w = 100

        item.append_child(slide)
        self._slides.append(item)

        if self._indicators is not None:
            indicator = HTML.Button(parent=self._indicators)
            indicator.set_attribute("data-bs-target", "#" + self.id)
            indicator.set_attribute(
                "data-bs-slide-to", str(len(self._slides) - 1))
            if is_active:
                indicator.add_class("active")
                indicator.set_attribute("aria-current", "true")

            self.set_attribute("data-bs-interval", "false")
            self.set_attribute("aria-label", "Slide " +
                               str(len(self._slides) - 1))
        return item


class CarouselDark(Carousel):
    _default_class_name = "carousel carousel-dark slide"


class DropdownButton(BootstrapContainer):

    _default_class_name: str = "dropdown"

    _default_dropdown_menu_class: str = "dropdown-menu"

    def __init__(self,
                 id: str = None,
                 class_name: str = None,
                 custom_title: str = None,
                 title_is_value: bool = True,
                 options: List[str] = None,
                 button_class=ButtonPrimary,
                 option_callback=None,
                 fire_callback_on_options_change: bool = True,
                 parent: HTML.Element = None,
                 inner_html: str = None,) -> None:
        super().__init__(inner_html=inner_html,
                         id=id,
                         class_name=class_name,
                         parent=parent)

        self._dropdown_button = None
        self._dropdown_list = None
        self._options = None
        self._callback_function = option_callback
        self._title_is_value = title_is_value
        self._custom_title = custom_title
        self._fire_callback_on_options_change = fire_callback_on_options_change
        self._button_class = button_class

        self._current_value = None

        self._create_dropdown_button(custom_title, button_class)
        self._create_options(options)

    def _create_dropdown_button(self, title: str, button_class) -> None:
        self._dropdown_button = button_class(inner_html=title, parent=self)
        self._dropdown_button.add_class("dropdown-toggle")
        self._dropdown_button.set_attribute("data-bs-toggle", "dropdown")
        self._dropdown_button.set_attribute("aria-expanded", "false")

    def _create_options(self, options):
        self._dropdown_list = BootstrapContainer(
            class_name=self.__class__._default_dropdown_menu_class, parent=self)
        self._dropdown_list.set_attribute(
            "aria-labelledby", self._dropdown_button.id)
        self._options = []
        for option in options:
            option_button = self._button_class(
                inner_html=option, parent=self._dropdown_list)
            option_button.add_class("dropdown-item")
            self._options.append(option)
            option_button.onclick = lambda _, opt=option: self._on_option_click(
                opt)

        if self._fire_callback_on_options_change:
            self._on_option_click(options[0])

    def _on_option_click(self, option) -> None:
        if self._callback_function is not None:
            self._callback_function(option)
        if self._title_is_value and self._custom_title is None:
            self.title = option

        self._current_value = option

    @property
    def title(self) -> str:
        return self._dropdown_button.inner_html

    @title.setter
    def title(self, title: str) -> None:
        self._dropdown_button.inner_html = title

    @property
    def options(self) -> List[str]:
        return self._options

    @options.setter
    def options(self, options: List[str]) -> None:
        self._dropdown_list.destroy()
        self._create_options(options)

    @property
    def current_value(self) -> str:
        return self._current_value

    @current_value.setter
    def current_value(self, value: str) -> None:
        if value not in self._options:
            raise ValueError("Value not in options")
        self._on_option_click(value)


class DropdownButtonDark(DropdownButton):

    _default_dropdown_menu_class: str = "dropdown-menu dropdown-menu-dark"


# containers ------------------------------------------------------------------


class ContainerSmall(BootstrapContainer):
    _default_class_name = "container-sm"


class ContainerMedium(BootstrapContainer):
    _default_class_name = "container-md"


class ContainerLarge(BootstrapContainer):
    _default_class_name = "container-lg"


class ContainerXL(BootstrapContainer):
    _default_class_name = "container-xl"


class ContainerXXL(BootstrapContainer):
    _default_class_name = "container-xxl"


class ContainerFluid(BootstrapContainer):
    _default_class_name = "container-fluid"


class Row(BootstrapContainer):
    _default_class_name = "row"


class Col(BootstrapContainer):

    _default_class_name = "col"

    def __init__(self,
                 inner_html: str = None,
                 id: str = None,
                 class_name: str = None,
                 parent: HTML.Element = None,
                 col: int = None,
                 col_sm: int = None,
                 col_md: int = None,
                 col_lg: int = None,
                 col_xl: int = None,
                 col_xxl: int = None) -> None:

        if col is not None:
            self.col = col

        super().__init__(inner_html, id, class_name, parent)

        if col_sm is not None:
            self.col_sm = col_sm

        if col_md is not None:
            self.col_md = col_md

        if col_lg is not None:
            self.col_lg = col_lg

        if col_xl is not None:
            self.col_xl = col_xl

        if col_xxl is not None:
            self.col_xxl = col_xxl

    @property
    def order(self) -> Union[str, int, None]:
        orders = list(self._search_class_list("order-{}"))
        if len(orders) == 0:
            return None
        order = orders[0]   # ignore other orders than the first one
        _, number = order.split("-")

        if number.isnumeric():
            return int(number)
        return number

    @order.setter
    def order(self, value: Union[str, int, None]) -> None:
        self._remove_classes_by_search_term("order-{}")
        if value is not None:
            self.add_class(f"order-{str(value)}")


class ListGroup(HTML.Ul, BootstrapContainer):
    _default_class_name = "list-group"

    def __init__(self, id: str = None,
                 class_name: str = None,
                 parent: HTML.Element = None,
                 inner_html: str = None,
                 items: List[Union[str, HTML.Element]] = None) -> None:
        super().__init__(inner_html=inner_html,
                         id=id,
                         class_name=class_name,
                         parent=parent)

        if items is not None:
            for item in items:
                self.add_item(item)

    def add_item(self, item: Union[str, HTML.Element], active: bool = False, disabled: bool = False, item_class=HTML.Li) -> None:
        li = item_class(class_name="list-group-item", parent=self)
        if active:
            li.add_class("active")
        if disabled:
            li.add_class("disabled")
        if isinstance(item, str):
            li.inner_html = item
        else:
            li.append(item)

        return li


class ListGroupSelectable(ListGroup):

    def __init__(self, id: str = None,
                 class_name: str = None,
                 parent: HTML.Element = None,
                 inner_html: str = None,
                 options: List[Union[HTML.Element, str]] = None,
                 button_class=ButtonPrimary) -> None:

        self._button_class = button_class
        self._items = []
        super().__init__(inner_html=inner_html,
                         id=id,
                         class_name=class_name,
                         parent=parent,
                         items=options)

    def add_item(self, item: Union[str, HTML.Element],
                 active: bool = False,
                 disabled: bool = False) -> None:
        item = super().add_item(item,
                                active,
                                disabled,
                                item_class=self._button_class)

        item.add_class("list-group-item-action")
        self._items.append(item)

        item.onclick = lambda _, item=item: item.set_class(
            "active", not item.has_class("active"))
        return item

    @property
    def selected_options(self) -> List[str]:

        selected = []
        for item in self._items:
            if item.has_class("active"):
                selected.append(item.inner_html)
                # TODO: will not work for html items
        return selected

# modal: ----------------------------------------------------------------------


class ModalTitle(HTML.H5, BootstrapContainer):
    _default_class_name: str = "modal-title"

    def __init__(self, title: str,
                 id: str = None,
                 class_name: str = None,
                 parent: HTML.Element = None,
                 inner_html: str = None) -> None:
        super().__init__(inner_html=inner_html,
                         id=id,
                         class_name=class_name,
                         parent=parent)

        self.inner_html = title


class ModalHeader(BootstrapContainer):

    _default_class_name: str = "modal-header"

    def __init__(self,
                 title: Union[str, HTML.Element],
                 has_close_button: bool = True,
                 id: str = None,
                 class_name: str = None,
                 parent: HTML.Element = None,
                 inner_html: str = None) -> None:
        super().__init__(inner_html=inner_html,
                         id=id,
                         class_name=class_name,
                         parent=parent)

        self._title = None
        self._close_button = None

        if title is not None:
            if isinstance(title, str):
                self._title = ModalTitle(title, parent=self)
            else:
                self._title = title
                self.append(title)

        if has_close_button:
            self._button = Button(class_name="btn-close", parent=self)
            self._button.set_attribute("data-bs-dismiss", "modal")
            self._button.set_attribute("aria-label", "Close")


class ModalBody(BootstrapContainer):

    _default_class_name: str = "modal-body"


class ModalFooter(BootstrapContainer):

    _default_class_name: str = "modal-footer"


class ModalContent(BootstrapContainer):

    _default_class_name: str = "modal-content"


class ModalDialog(BootstrapContainer):

    _default_class_name: str = "modal-dialog"


class Modal(BootstrapContainer):

    _default_class_name: str = "modal"

    def __init__(self,
                 title: Union[str, HTML.Element],
                 has_close_button: bool = True,
                 fade: bool = True,
                 static_backdrop: bool = False,
                 modal_scrollable: bool = False,
                 vertically_centered: bool = False,
                 body: Union[str, HTML.Element] = None,
                 id: str = None,
                 class_name: str = None,
                 parent: HTML.Element = None,
                 inner_html: str = None) -> None:
        super().__init__(inner_html=inner_html,
                         id=id,
                         class_name=class_name,
                         parent=parent)

        self.set_attribute("tabindex", -1)
        if fade:
            self.add_class("fade")

        if static_backdrop:
            self.set_attribute("data-bs-backdrop", "static")

        self._modal_dialog = ModalDialog(parent=self)

        if modal_scrollable:
            self._modal_dialog.add_class("modal-dialog-scrollable")

        if vertically_centered:
            self._modal_dialog.add_class("modal-dialog-centered")

        self._modal_content = ModalContent(parent=self._modal_dialog)

        self._header = ModalHeader(
            parent=self._modal_content, title=title, has_close_button=has_close_button)
        self._body = ModalBody(parent=self._modal_content)
        self._body.append_child(body)
        self._footer = ModalFooter(parent=self._modal_content)

        self._js_modal = bootstrap.Modal.new(self.element)

    @property
    def header(self) -> ModalHeader:
        return self._header

    @property
    def body(self) -> ModalBody:
        return self._body

    @property
    def footer(self) -> ModalFooter:
        return self._footer

    def show(self) -> None:
        # TODO: maybe modify elements directly?
        self._js_modal.show()

    def hide(self) -> None:
        self._js_modal.hide()

    def toggle(self) -> None:
        self._js_modal.toggle()


# navbar: ---------------------------------------------------------------------

class NavItem(HTML.Li, BootstrapContainer):
    _default_class_name: str = "nav-item"


class NavList(HTML.Ul, BootstrapContainer):

    _default_class_name: str = "nav"

    def add_item(self, item: HTML.Element):
        li_item = NavItem(parent=self)
        li_item.append_child(item)

    @property
    def nav_fill(self) -> None:
        self.has_class("nav-fill")

    @nav_fill.setter
    def nav_fill(self, value: bool) -> None:
        if value:
            self.add_class("nav-fill")
        else:
            self.remove_class("nav-fill")


class NavListVertical(NavList):

    _default_class_name: str = "nav flex-column"


class NavListTabs(NavList):

    _default_class_name: str = "nav nav-tabs"


class NavListPills(NavList):

    _default_class_name: str = "nav nav-pills"


class NavbarBrand(HTML.A, BootstrapContainer):
    _default_class_name: str = "navbar-brand"

    def __init__(self,
                 text: str,
                 href: str = "#",
                 id: str = None,
                 class_name: str = None,
                 parent: HTML.Element = None) -> None:
        super().__init__(inner_html=text,
                         id=id,
                         class_name=class_name,
                         parent=parent)

        self.set_attribute("href", href)


class Navbar(BootstrapContainer):

    _default_class_name: str = "navbar"
    _default_nav_list_class = NavList

    def __init__(self,
                 options: List[str] = None,
                 brand: Union[str, NavbarBrand] = None,
                 option_callback: Callable = None,
                 fire_callback_on_option_init: bool = True,
                 nav_fill: bool = False,
                 justification: JustifyContent = JustifyContent.START,
                 toggle_button_for_target: HTML.Element = None,
                 id: str = None,
                 class_name: str = None,
                 parent: HTML.Element = None,
                 inner_html: str = None) -> None:
        super().__init__(inner_html=inner_html,
                         id=id,
                         class_name=class_name,
                         parent=parent)

        if toggle_button_for_target is not None:
            nav_item = NavItem(parent=self)
            hamburger = ButtonHamburger(parent=nav_item)
            hamburger.make_collapse_toggle(toggle_button_for_target)

        if brand is not None:
            self._brand = brand if isinstance(
                brand, NavbarBrand) else NavbarBrand(brand)
            self.append_child(self._brand)

        self._nav_list = self.__class__._default_nav_list_class(parent=self)
        self._nav_list.justify_content = justification
        self._nav_list.nav_fill = nav_fill
        if nav_fill:
            self.add_class("navbar-expand-lg")

        self._options = {}
        self._current_nav_link = None
        self._current_nav_name = None
        self._fire_callback_on_option_init = fire_callback_on_option_init

        self._option_callback = option_callback

        if isinstance(brand, str):
            self._brand = HTML.A(brand, parent=self)
        else:
            self._brand = brand
            self.append_child(brand)

        self._nav_list.add_item(self._brand)

        if options is not None:
            for option in options:
                self.add_nav_option(option)

        if options is not None:
            self._on_click(options[0])

    def add_item(self, item: HTML.Element):
        self._nav_list.add_item(item)

    def _activate_nav_link(self, nav_link, nav_name):
        if self._current_nav_link is not None:
            self._current_nav_link.remove_class("active")
            self._current_nav_link.remove_attribute("aria-current")
        self._current_nav_link = nav_link
        self._current_nav_link.add_class("active")
        self._current_nav_link.set_attribute("aria-current", "page")

        self._current_nav_name = nav_name

    def _deactivate_nav_link(self):
        if self._current_nav_link is not None:
            self._current_nav_link.remove_class("active")
            self._current_nav_link.remove_attribute("aria-current")
        self._current_nav_link = None
        self._current_nav_name = None

    def _on_click(self, option: str, fire_callback: bool = True):
        self._activate_nav_link(self._options[option], option)
        if fire_callback and self._option_callback is not None:
            self._option_callback(option)

    def add_nav_option(self, option: str, is_active: bool = False):
        link = Button(option, class_name="nav-link", parent=self._nav_list)
        self._nav_list.add_item(link)
        self._options[option] = link

        if is_active:
            self._on_click(
                option, fire_callback=self._fire_callback_on_option_init)

        link.onclick = lambda _, option=option: self._on_click(option)

    @property
    def current_nav_option(self) -> Union[str, None]:
        return self._current_nav_name

    @property
    def option_callback(self) -> Callable:
        return self._option_callback

    @option_callback.setter
    def option_callback(self, value: Callable) -> None:
        self._option_callback = value


class NavbarVertical(Navbar):

    _default_nav_list_class = NavListVertical


class NavbarTabs(Navbar):

    _default_nav_list_class = NavListTabs


class NavbarPills(Navbar):

    _default_nav_list_class = NavListPills


class NavbarDark(Navbar):

    _default_class_name: str = "navbar navbar-dark bg-dark"


class NavbarVerticalDark(NavbarVertical):

    _default_class_name: str = "navbar navbar-dark bg-dark"


class NavbarTabsDark(NavbarTabs):

    _default_class_name: str = "navbar navbar-dark bg-dark"


class NavbarPillsDark(NavbarPills):

    _default_class_name: str = "navbar navbar-dark bg-dark"


class Tabs(BootstrapContainer):

    _default_navbar_tabs_class = NavbarTabs

    def __init__(self,
                 contents: Dict[str, BootstrapContainer],
                 navbar: Navbar = None,
                 id: str = None,
                 class_name: str = None,
                 parent: HTML.Element = None,
                 inner_html: str = None) -> None:
        super().__init__(inner_html=inner_html,
                         id=id,
                         class_name=class_name,
                         parent=parent)

        if navbar is None:
            navbar = self.__class__._default_navbar_tabs_class(
                options=list(contents.keys()))

        self._navbar = navbar
        self.append_child(self._navbar)

        self._content_container = BootstrapContainer(parent=self)
        self._content_container.w = 100
        self._content_container.h = 100

        self._active_tab: str = None
        self._contents = contents

        for content in contents.values():
            self._content_container.append_child(content)
            content.add_class("d-none")

        self._old_navbar_callback = self._navbar.option_callback
        self._navbar._option_callback = self._tab_click_callback

        self.active_tab = list(contents.keys())[0]

    def _tab_click_callback(self, tab_name: str):
        self._activate_tab(tab_name)
        if self._old_navbar_callback is not None:
            self._old_navbar_callback(tab_name)

    def _activate_tab(self, tab: str):
        if self._active_tab is not None:
            self._contents[self._active_tab].add_class("d-none")
        self._active_tab = tab
        self._contents[self._active_tab].remove_class("d-none")

    @property
    def active_tab(self) -> Union[str, None]:
        return self._active_tab

    @active_tab.setter
    def active_tab(self, value: str) -> None:
        self._activate_tab(value)


class TabsDark(Tabs):

    _default_navbar_tabs_class = NavbarTabsDark

class ToastHeader(BootstrapContainer):

    _default_class_name = "toast-header"

    def __init__(self, title:str,
                       inner_html: str = None,
                       id: str = None,
                       class_name: str = None,
                       parent: "Element" = None) -> None:
        super().__init__(inner_html=inner_html, id=id, class_name=class_name, parent=parent)

        HTML.Strong(title, class_name="me-auto", parent=self)
        close_button = HTML.Button(class_name="btn-close", parent=self)
        close_button.set_attribute("data-bs-dismiss", "toast")
        close_button.set_attribute("aria-label", "Close")

class ToastBody(BootstrapContainer):
    _default_class_name: str = "toast-body"

class Toast(BootstrapContainer):

    _default_class_name: str = "toast"

    def __init__(self, inner_html: str = None,
                       title: str = "Toast",
                       id: str = None,
                       class_name: str = None,
                       parent: "Element" = None) -> None:

        super().__init__(id=id, class_name=class_name, parent=parent)

        self.set_attribute("role", "alert")
        self.set_attribute("aria-live", "assertlive")
        self.set_attribute("aria-atomic", True)
        self.set_attribute("style", "position: absolute")

        ToastHeader(title, parent=self)
        ToastBody(inner_html, parent=self)

        self._js_toast = bootstrap.Toast.new(self.element)
    
    def show(self):
        self._js_toast.show()
    
    def hide(self):
        self._js_toast.hide()

class OffcanvasTitle(HTML.H5, BootstrapContainer):

    _default_class_name: str = "offcanvas-title"


class OffcanvasHeader(BootstrapContainer):

    _default_class_name: str = "offcanvas-header"

    def __init__(self,
                 title: Union[str, OffcanvasTitle],
                 close_button:bool = True,
                 parent: HTML.Element = None,
                 inner_html: str = None) -> None:
        super().__init__(inner_html=inner_html,
                         parent=parent)

        if isinstance(title, str):
            title = OffcanvasTitle(title)
        self.append_child(title)

        if close_button:
            btn = Button(class_name="btn-close text-reset")
            btn.set_attribute("aria-label", "Close")
            btn.set_attribute("data-bs-dismiss", "offcanvas")
            self.append_child(btn)


class OffcanvasBody(BootstrapContainer):

    _default_class_name: str = "offcanvas-body"

class Offcanvas(BootstrapContainer):

    _default_class_name: str = "offcanvas offcanvas-start"

    def __init__(self,
                 header: Union[str, OffcanvasHeader],
                 content = HTML.Element,
                 parent: HTML.Element = None,
                 inner_html: str = None) -> None:
        super().__init__(inner_html=inner_html,
                         parent=parent)


        self.set_attribute("tabindex", "-1")

        self._js_offcanvas = bootstrap.Offcanvas.new(self.element)

        if isinstance(header, str):
            header = OffcanvasHeader(header)

        self._header = header
        self.append_child(header)

        self._body = OffcanvasBody(parent=self)
        self._body.append_child(content)
    
    def show(self):
        self._js_offcanvas.show()
    
    def hide(self) -> None:
        self._js_offcanvas.hide()
    
    def toggle(self) -> None:
        self._js_offcanvas.toggle()
    
class OffcanvasStart(Offcanvas):

    pass

class OffcanvasTop(Offcanvas):

    _default_class_name: str = "offcanvas offcanvas-top"

class OffcanvasEnd(Offcanvas):

    _default_class_name: str = "offcanvas offcanvas-end"

class OffcanvasBottom(Offcanvas):

    _default_class_name: str = "offcanvas offcanvas-bottom"


class Progress(BootstrapContainer):

    _default_class_name: str = "progress"

    def __init__(self,
                 value: int = 0,
                 max: int = 100,
                 striped: bool = True,
                 animated: bool = True,
                 parent: HTML.Element = None,
                 inner_html: str = None) -> None:
        super().__init__(inner_html=inner_html,
                         parent=parent)

        self._value = value
        self._max = max

        self._progress_bar = BootstrapContainer(parent=self, class_name="progress-bar")
        
        self.is_striped = striped
        self.is_animated = animated

        self._update_progress_bar()



    def _update_progress_bar(self):
        self._progress_bar.set_attribute("role", "progressbar")
        self._progress_bar.set_attribute("aria-valuenow", str(self._value))
        self._progress_bar.set_attribute("aria-valuemin", "0")
        self._progress_bar.set_attribute("aria-valuemax", str(self._max))
        self._progress_bar.set_attribute("style", "width: " + str(int(100 * self._value / self._max)) + "%")

    @property
    def value(self) -> int:
        return self._value

    @value.setter
    def value(self, value: int) -> None:
        self._value = value
        self._update_progress_bar()

    @property
    def max(self) -> int:
        return self._max

    @max.setter
    def max(self, value: int) -> None:
        self._max = value
        self._update_progress_bar()
    
    @property
    def is_striped(self) -> bool:
        return self._progress_bar.has_class("progress-bar-striped")
    
    @is_striped.setter
    def is_striped(self, value: bool) -> None:
        self._progress_bar.set_class("progress-bar-striped", value)
    
    @property
    def is_animated(self) -> bool:
        return self._progress_bar.has_class("progress-bar-animated")
    
    @is_animated.setter
    def is_animated(self, value: bool) -> None:
        self._progress_bar.set_class("progress-bar-animated", value)

