"""
Components/Tabs
===============

.. seealso::

    `Material Design spec, Tabs <https://material.io/components/tabs>`_

.. rubric:: Tabs organize content across different screens, data sets,
    and other interactions.

.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tabs.png
    :align: center

.. Note:: Module provides tabs in the form of icons or text.

Usage
-----

To create a tab, you must create a new class that inherits from the
:class:`~MDTabsBase` class and the `Kivy` container, in which you will create
content for the tab.

.. code-block:: python

    class Tab(MDFloatLayout, MDTabsBase):
        '''Class implementing content for a tab.'''
        content_text = StringProperty("")

.. code-block:: kv

    <Tab>
        content_text

        MDLabel:
            text: root.content_text
            pos_hint: {"center_x": .5, "center_y": .5}

All tabs must be contained inside a :class:`~MDTabs` widget:

.. code-block:: kv

    Root:

        MDTabs:

            Tab:
                title: "Tab 1"
                content_text: f"This is an example text for {self.title}"

            Tab:
                title: "Tab 2"
                content_text: f"This is an example text for {self.title}"

            ...

Example with tab icon
---------------------

.. code-block:: python

    from kivy.lang import Builder

    from kivymd.app import MDApp
    from kivymd.uix.tab import MDTabsBase
    from kivymd.uix.floatlayout import MDFloatLayout
    from kivymd.icon_definitions import md_icons

    KV = '''
    MDBoxLayout:
        orientation: "vertical"

        MDTopAppBar:
            title: "Example Tabs"

        MDTabs:
            id: tabs
            on_tab_switch: app.on_tab_switch(*args)


    <Tab>

        MDIconButton:
            id: icon
            icon: root.icon
            user_font_size: "48sp"
            pos_hint: {"center_x": .5, "center_y": .5}
    '''


    class Tab(MDFloatLayout, MDTabsBase):
        '''Class implementing content for a tab.'''


    class Example(MDApp):
        icons = list(md_icons.keys())[15:30]

        def build(self):
            return Builder.load_string(KV)

        def on_start(self):
            for tab_name in self.icons:
                self.root.ids.tabs.add_widget(Tab(icon=tab_name))

        def on_tab_switch(
            self, instance_tabs, instance_tab, instance_tab_label, tab_text
        ):
            '''
            Called when switching tabs.

            :type instance_tabs: <kivymd.uix.tab.MDTabs object>;
            :param instance_tab: <__main__.Tab object>;
            :param instance_tab_label: <kivymd.uix.tab.MDTabsLabel object>;
            :param tab_text: text or name icon of tab;
            '''
            # get the tab icon.
            count_icon = instance_tab.icon
            # print it on shell/bash.
            print(f"Welcome to {count_icon}' tab'")


    Example().run()


.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tabs-simple-example.gif
    :align: center

Example with tab text
---------------------

.. Note:: The :class:`~MDTabsBase` class has an icon parameter and, by default,
    tries to find the name of the icon in the file
    ``kivymd/icon_definitions.py``.

    If the name of the icon is not found, the class will send a message
    stating that the icon could not be found.

    if the tab has no icon, title or tab_label_text, the class will raise a
    ValueError.

.. code-block:: python

    from kivy.lang import Builder

    from kivymd.app import MDApp
    from kivymd.uix.floatlayout import MDFloatLayout
    from kivymd.uix.tab import MDTabsBase

    KV = '''
    MDBoxLayout:
        orientation: "vertical"

        MDTopAppBar:
            title: "Example Tabs"

        MDTabs:
            id: tabs
            on_tab_switch: app.on_tab_switch(*args)


    <Tab>

        MDLabel:
            id: label
            text: "Tab 0"
            halign: "center"
    '''


    class Tab(MDFloatLayout, MDTabsBase):
        '''Class implementing content for a tab.'''


    class Example(MDApp):
        def build(self):
            return Builder.load_string(KV)

        def on_start(self):
            for i in range(20):
                self.root.ids.tabs.add_widget(Tab(title=f"Tab {i}"))

        def on_tab_switch(
            self, instance_tabs, instance_tab, instance_tab_label, tab_text
        ):
            '''Called when switching tabs.

            :type instance_tabs: <kivymd.uix.tab.MDTabs object>;
            :param instance_tab: <__main__.Tab object>;
            :param instance_tab_label: <kivymd.uix.tab.MDTabsLabel object>;
            :param tab_text: text or name icon of tab;
            '''

            instance_tab.ids.label.text = tab_text


    Example().run()

.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tabs-simple-example-text.gif
    :align: center

Example with tab icon and text
------------------------------

.. code-block:: python

    from kivy.lang import Builder

    from kivymd.app import MDApp
    from kivymd.uix.tab import MDTabsBase
    from kivymd.uix.floatlayout import MDFloatLayout
    from kivymd.icon_definitions import md_icons

    KV = '''
    MDBoxLayout:
        orientation: "vertical"

        MDTopAppBar:
            title: "Example Tabs"

        MDTabs:
            id: tabs
    '''


    class Tab(MDFloatLayout, MDTabsBase):
        pass


    class Example(MDApp):
        def build(self):
            return Builder.load_string(KV)

        def on_start(self):
            for name_tab in list(md_icons.keys())[15:30]:
                self.root.ids.tabs.add_widget(Tab(icon=name_tab, title=name_tab))


    Example().run()

.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tabs-simple-example-icon-text.png
    :align: center

Dynamic tab management
----------------------

.. code-block:: python

    from kivy.lang import Builder
    from kivy.uix.scrollview import ScrollView

    from kivymd.app import MDApp
    from kivymd.uix.tab import MDTabsBase

    KV = '''
    MDBoxLayout:
        orientation: "vertical"

        MDTopAppBar:
            title: "Example Tabs"

        MDTabs:
            id: tabs


    <Tab>

        MDList:

            MDBoxLayout:
                adaptive_height: True

                MDFlatButton:
                    text: "ADD TAB"
                    on_release: app.add_tab()

                MDFlatButton:
                    text: "REMOVE LAST TAB"
                    on_release: app.remove_tab()

                MDFlatButton:
                    text: "GET TAB LIST"
                    on_release: app.get_tab_list()
    '''


    class Tab(ScrollView, MDTabsBase):
        '''Class implementing content for a tab.'''


    class Example(MDApp):
        index = 0

        def build(self):
            return Builder.load_string(KV)

        def on_start(self):
            self.add_tab()

        def get_tab_list(self):
            '''Prints a list of tab objects.'''

            print(self.root.ids.tabs.get_tab_list())

        def add_tab(self):
            self.index += 1
            self.root.ids.tabs.add_widget(Tab(text=f"{self.index} tab"))

        def remove_tab(self):
            if self.index > 1:
                self.index -= 1
            self.root.ids.tabs.remove_widget(
                self.root.ids.tabs.get_tab_list()[-1]
            )


    Example().run()

.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tabs-dynamic-managmant.gif
    :align: center

Use on_ref_press method
-----------------------

You can use markup for the text of the tabs and use the ``on_ref_press``
method accordingly:

.. code-block:: python

    from kivy.lang import Builder

    from kivymd.app import MDApp
    from kivymd.uix.floatlayout import MDFloatLayout
    from kivymd.font_definitions import fonts
    from kivymd.uix.tab import MDTabsBase
    from kivymd.icon_definitions import md_icons

    KV = '''
    MDBoxLayout:
        orientation: "vertical"

        MDTopAppBar:
            title: "Example Tabs"

        MDTabs:
            id: tabs
            on_ref_press: app.on_ref_press(*args)


    <Tab>

        MDIconButton:
            id: icon
            icon: app.icons[0]
            user_font_size: "48sp"
            pos_hint: {"center_x": .5, "center_y": .5}
    '''


    class Tab(MDFloatLayout, MDTabsBase):
        '''Class implementing content for a tab.'''


    class Example(MDApp):
        icons = list(md_icons.keys())[15:30]

        def build(self):
            return Builder.load_string(KV)

        def on_start(self):
            for name_tab in self.icons:
                self.root.ids.tabs.add_widget(
                    Tab(
                        text=f"[ref={name_tab}][font={fonts[-1]['fn_regular']}]{md_icons['close']}[/font][/ref]  {name_tab}"
                    )
                )

        def on_ref_press(
            self,
            instance_tabs,
            instance_tab_label,
            instance_tab,
            instance_tab_bar,
            instance_carousel,
        ):
            '''
            The method will be called when the ``on_ref_press`` event
            occurs when you, for example, use markup text for tabs.

            :param instance_tabs: <kivymd.uix.tab.MDTabs object>
            :param instance_tab_label: <kivymd.uix.tab.MDTabsLabel object>
            :param instance_tab: <__main__.Tab object>
            :param instance_tab_bar: <kivymd.uix.tab.MDTabsBar object>
            :param instance_carousel: <kivymd.uix.tab.MDTabsCarousel object>
            '''

            # Removes a tab by clicking on the close icon on the left.
            for instance_tab in instance_carousel.slides:
                if instance_tab.text == instance_tab_label.text:
                    instance_tabs.remove_widget(instance_tab_label)
                    break


    Example().run()

.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/tabs-on-ref-press.gif
    :align: center

Switching the tab by name
-------------------------

.. code-block:: python

    from kivy.lang import Builder

    from kivymd.app import MDApp
    from kivymd.icon_definitions import md_icons
    from kivymd.uix.floatlayout import MDFloatLayout
    from kivymd.uix.tab import MDTabsBase

    KV = '''
    MDBoxLayout:
        orientation: "vertical"

        MDTopAppBar:
            title: "Example Tabs"

        MDTabs:
            id: tabs


    <Tab>

        MDBoxLayout:
            orientation: "vertical"
            pos_hint: {"center_x": .5, "center_y": .5}
            size_hint: None, None
            spacing: dp(48)

            MDIconButton:
                id: icon
                icon: "arrow-right"
                user_font_size: "48sp"
                on_release: app.switch_tab_by_name()

            MDIconButton:
                id: icon2
                icon: "page-next"
                user_font_size: "48sp"
                on_release: app.switch_tab_by_object()
    '''


    class Tab(MDFloatLayout, MDTabsBase):
        '''Class implementing content for a tab.'''


    class Example(MDApp):
        icons = list(md_icons.keys())[15:30]

        def build(self):
            self.iter_list_names = iter(list(self.icons))
            return Builder.load_string(KV)

        def on_start(self):
            for name_tab in list(self.icons):
                self.root.ids.tabs.add_widget(Tab(tab_label_text=name_tab))
            self.iter_list_objects = iter(list(self.root.ids.tabs.get_tab_list()))

        def switch_tab_by_object(self):
            try:
                x = next(self.iter_list_objects)
                print(f"Switch slide by object, next element to show: [{x}]")
                self.root.ids.tabs.switch_tab(x)
            except StopIteration:
                # reset the iterator an begin again.
                self.iter_list_objects = iter(list(self.root.ids.tabs.get_tab_list()))
                self.switch_tab_by_object()

        def switch_tab_by_name(self):
            '''Switching the tab by name.'''

            try:
                x = next(self.iter_list_names)
                print(f"Switch slide by name, next element to show: [{x}]")
                self.root.ids.tabs.switch_tab(x)
            except StopIteration:
                # Reset the iterator an begin again.
                self.iter_list_names = iter(list(self.icons))
                self.switch_tab_by_name()


    Example().run()

.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/switching-tab-by-name.gif
    :align: center
"""

__all__ = ("MDTabs", "MDTabsBase")

import os
from typing import Union

from kivy.clock import Clock
from kivy.graphics.texture import Texture
from kivy.lang import Builder
from kivy.logger import Logger
from kivy.metrics import dp
from kivy.properties import (
    AliasProperty,
    BooleanProperty,
    BoundedNumericProperty,
    ColorProperty,
    ListProperty,
    NumericProperty,
    ObjectProperty,
    OptionProperty,
    StringProperty,
)
from kivy.uix.anchorlayout import AnchorLayout
from kivy.uix.behaviors import ToggleButtonBehavior
from kivy.uix.scrollview import ScrollView
from kivy.uix.widget import Widget
from kivy.utils import boundary

from kivymd import uix_path
from kivymd.font_definitions import fonts, theme_font_styles
from kivymd.icon_definitions import md_icons
from kivymd.theming import ThemableBehavior, ThemeManager
from kivymd.uix.behaviors import (
    FakeRectangularElevationBehavior,
    RectangularRippleBehavior,
    SpecificBackgroundColorBehavior,
)
from kivymd.uix.boxlayout import MDBoxLayout
from kivymd.uix.carousel import MDCarousel
from kivymd.uix.label import MDLabel

with open(os.path.join(uix_path, "tab", "tab.kv"), encoding="utf-8") as kv_file:
    Builder.load_string(kv_file.read())


class MDTabsException(Exception):
    pass


class MDTabsLabel(ToggleButtonBehavior, RectangularRippleBehavior, MDLabel):
    """This class it represent the label of each tab."""

    text_color_normal = ColorProperty(None)
    text_color_active = ColorProperty(None)
    tab = ObjectProperty()
    tab_bar = ObjectProperty()
    font_name = StringProperty("Roboto")

    def __init__(self, **kwargs):
        self.split_str = " ,-"
        super().__init__(**kwargs)
        self.max_lines = 2
        self.size_hint_x = None
        self.size_hint_min_x = dp(90)
        self.min_space = dp(98)
        self.bind(
            text=self._update_text_size,
        )

    def on_release(self) -> None:
        self.tab_bar.parent.dispatch("on_tab_switch", self.tab, self, self.text)
        # If the label is selected load the relative tab from carousel.
        if self.state == "down":
            self.tab_bar.parent.carousel.load_slide(self.tab)

    def on_texture(self, instance_tabs_label, texture: Texture) -> None:
        # Just save the minimum width of the label based of the content.
        if texture:
            max_width = dp(360)
            min_width = dp(90)
            if texture.width > max_width:
                self.width = max_width
                self.text_size = (max_width, None)
            elif texture.width < min_width:
                self.width = min_width
            else:
                self.width = texture.width

    def _update_text_size(self, *args):
        if not self.tab_bar:
            return
        if self.tab_bar.parent.allow_stretch is True:
            self.text_size = (None, None)
        else:
            self.width = self.tab_bar.parent.fixed_tab_label_width
            self.text_size = (self.width, None)
        Clock.schedule_once(self.tab_bar._label_request_indicator_update, 0)


class MDTabsBase(Widget):
    """
    This class allow you to create a tab.
    You must create a new class that inherits from MDTabsBase.
    In this way you have total control over the views of your tabbed panel.
    """

    icon = StringProperty()
    """
    This property will set the Tab's Label Icon.

    :attr:`icon` is an :class:`~kivy.properties.StringProperty`
    and defaults to `''`.
    """

    title_icon_mode = OptionProperty("Lead", options=["Lead", "Top"])
    """
    This property sets the mode in wich the tab's title and icon are shown.

    :attr:`title_icon_mode` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'Lead'`.
    """

    title = StringProperty()
    """
    This property will set the Name of the tab.

    .. note::
        As a side note.

        All tabs have set `markup = True`.
        Thanks to this, you can use the kivy markup language to set a colorful
        and fully customizable tabs titles.

    .. warning::
        The material design requires that every title label is written in
        capital letters, because of this, the `string.upper()` will be applied
        to it's contents.

    :attr:`title` is an :class:`~kivy.properties.StringProperty`
    and defaults to `''`.
    """

    title_is_capital = BooleanProperty(False)
    """
    This value controls wether if the title property should be converted to
    capital letters.

    :attr:`title_is_capital` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """

    text = StringProperty(deprecated=True)
    """
    This property is the actual title of the tab.
    use the property :attr:`icon` and :attr:`title` to set this property
    correctly.

    This property is kept public for specific and backward compatibility
    purposes.

    :attr:`text` is an :class:`~kivy.properties.StringProperty`
    and defaults to `''`.

    .. deprecated:: 1.0.0
        Use :attr:`tab_label_text` instead.
    """

    tab_label_text = StringProperty()
    """
    This property is the actual title's Label of the tab.
    use the property :attr:`icon` and :attr:`title` to set this property
    correctly.

    This property is kept public for specific and backward compatibility
    purposes.

    :attr:`tab_label_text` is an :class:`~kivy.properties.StringProperty`
    and defaults to `''`.
    """

    tab_label = ObjectProperty()
    """
    It is the label object reference of the tab.

    :attr:`tab_label` is an :class:`~kivy.properties.ObjectProperty`
    and defaults to `None`.
    """

    def _get_label_font_style(self):
        if self.tab_label:
            return self.tab_label.font_style

    def _set_label_font_style(self, value):
        if self.tab_label:
            if value in theme_font_styles:
                self.tab_label.font_style = value
            else:
                raise ValueError(
                    "tab_label_font_style:\n\t"
                    "font_style not found in theme_font_styles\n\t"
                    f"font_style = {value}"
                )
        else:
            Clock.schedule_once(lambda x: self._set_label_font_style(value))
            return True

    tab_label_font_style = AliasProperty(
        _get_label_font_style,
        _set_label_font_style,
        cache=True,
    )
    """
    :attr:`tab_label_font_style` is an :class:`~kivy.properties.AliasProperty`
    that behavies similar to an :class:`~kivy.properties.OptionProperty`.

    This property's behavior allows the developer to use any new label style
    registered to the app.

    This property will affect the Tab's Title Label widget.
    """

    def __init__(self, **kwargs):
        self.tab_label = MDTabsLabel(tab=self)
        super().__init__(**kwargs)
        self.bind(
            icon=self._update_text,
            title=self._update_text,
            title_icon_mode=self._update_text,
            text=self.update_label_text,
            tab_label_text=self.update_label_text,
            title_is_capital=self.update_label_text,
        )
        Clock.schedule_once(
            self._update_text
        )  # this will ensure the text is correct

    def _update_text(self, *args):
        # Ensures that the title is in capital letters.
        if self.title and self.title_is_capital is True:
            if self.title != self.title.upper():
                self.title = self.title.upper()
                # Avoids event recursion.
                return
        # Add the icon.
        if self.icon and self.icon in md_icons:
            self.tab_label_text = f"[size=24sp][font={fonts[-1]['fn_regular']}]{md_icons[self.icon]}[/size][/font]"
            if self.title:
                self.tab_label_text = (
                    self.text
                    + (" " if self.title_icon_mode == "Lead" else "\n")
                    + self.title
                )
        # Add the title.
        else:
            if self.icon:
                Logger.error(
                    f"{self}: [UID] = [{self.uid}]:\n\t"
                    f"Icon '{self.icon}' not found in md_icons"
                )
            if self.title:
                self.tab_label_text = self.title
            else:
                if not self.tab_label_text:
                    raise ValueError(
                        f"{self}: [UID] = [{self.uid}]:\n\t"
                        "No valid Icon was found.\n\t"
                        "No valid Title was found.\n\t"
                        f"Icon\t= '{self.icon}'\n\t"
                        f"Title\t= '{self.title}'\n\t"
                    )

        self.tab_label.padding = dp(16), 0
        self.update_label_text(None, self.tab_label_text)

    def update_label_text(self, instance_user_tab, text_tab: str) -> None:
        self.tab_label.text = self.text = self.tab_label_text

    def on_text(self, instance_user_tab, text_tab: str) -> None:
        self.tab_label_text = self.text


class MDTabsMain(MDBoxLayout):
    """
    This class is just a boxlayout that contain the carousel.
    It allows you to have control over the carousel.
    """


class MDTabsCarousel(MDCarousel):
    lock_swiping = BooleanProperty(False)
    """
    If True - disable switching tabs by swipe.

    :attr:`lock_swiping` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    def on_touch_move(self, touch):
        if self.lock_swiping:  # lock a swiping
            return
        if not self.touch_mode_change:
            if self.ignore_perpendicular_swipes and self.direction in (
                "top",
                "bottom",
            ):
                if abs(touch.oy - touch.y) < self.scroll_distance:
                    if abs(touch.ox - touch.x) > self.scroll_distance:
                        self._change_touch_mode()
                        self.touch_mode_change = True
            elif self.ignore_perpendicular_swipes and self.direction in (
                "right",
                "left",
            ):
                if abs(touch.ox - touch.x) < self.scroll_distance:
                    if abs(touch.oy - touch.y) > self.scroll_distance:
                        self._change_touch_mode()
                        self.touch_mode_change = True

        if self._get_uid("cavoid") in touch.ud:
            return
        if self._touch is not touch:
            super().on_touch_move(touch)
            return self._get_uid() in touch.ud
        if touch.grab_current is not self:
            return True

        ud = touch.ud[self._get_uid()]
        direction = self.direction[0]

        if ud["mode"] == "unknown":
            if direction in "rl":
                distance = abs(touch.ox - touch.x)
            else:
                distance = abs(touch.oy - touch.y)
            if distance > self.scroll_distance:
                ev = self._change_touch_mode_ev
                if ev is not None:
                    ev.cancel()
                ud["mode"] = "scroll"
        else:
            if direction in "rl":
                self._offset += touch.dx
            if direction in "tb":
                self._offset += touch.dy
        return True


class MDTabsScrollView(ScrollView):
    """This class hacked version to fix scroll_x manual setting."""

    def goto(
        self, scroll_x: Union[float, None], scroll_y: Union[float, None]
    ) -> None:
        """Update event value along with scroll_*."""

        def _update(e, x):
            if e:
                e.value = (e.max + e.min) * x

        if not (scroll_x is None):
            self.scroll_x = scroll_x
            _update(self.effect_x, scroll_x)

        if not (scroll_y is None):
            self.scroll_y = scroll_y
            _update(self.effect_y, scroll_y)


class MDTabsBar(
    ThemableBehavior, FakeRectangularElevationBehavior, MDBoxLayout
):
    """
    This class is just a boxlayout that contains the scroll view for tabs.
    It is also responsible for resizing the tab shortcut when necessary.
    """

    target = ObjectProperty(None, allownone=True)
    """
    It is the carousel reference of the next tab / slide.
    When you go from `'Tab A'` to `'Tab B'`, `'Tab B'` will be the
    target tab / slide of the carousel.

    :attr:`target` is an :class:`~kivy.properties.ObjectProperty`
    and default to `None`.
    """

    def get_rect_instruction(self):
        canvas_instructions = self.layout.canvas.before.get_group(
            "Indicator_line"
        )
        return canvas_instructions[0]

    indicator = AliasProperty(get_rect_instruction, cache=True)
    """
    It is the :class:`~kivy.graphics.vertex_instructions.RoundedRectangle`
    instruction reference of the tab indicator.

    :attr:`indicator` is an :class:`~kivy.properties.AliasProperty`.
    """

    def get_last_scroll_x(self):
        return self.scrollview.scroll_x

    last_scroll_x = AliasProperty(
        get_last_scroll_x, bind=("target",), cache=True
    )
    """
    Is the carousel reference of the next tab/slide.
    When you go from `'Tab A'` to `'Tab B'`, `'Tab B'` will be the
    target tab/slide of the carousel.

    :attr:`last_scroll_x` is an :class:`~kivy.properties.AliasProperty`.
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)

    def update_indicator(
        self, x: Union[float, int], w: Union[float, int], radius=None
    ) -> None:
        # Update position and size of the indicator.
        if self.parent.tab_indicator_type == "line-round":
            self.parent._line_x = x
            self.parent._line_width = w
            self.parent._line_height = self.parent.tab_indicator_height
            self.parent._line_radius = self.parent.tab_indicator_height / 2
        elif self.parent.tab_indicator_type == "line-rect":
            self.parent._line_x = x
            self.parent._line_width = w
            self.parent._line_height = self.parent.tab_indicator_height
        else:
            self.indicator.pos = (x, 0)
            self.indicator.size = (w, self.parent.tab_indicator_height)
            if radius:
                self.indicator.radius = radius

    def tab_bar_autoscroll(self, instance_tab_label: MDTabsLabel, step: float):
        # Automatic scroll animation of the tab bar.
        bound_left = self.center_x - self.x
        bound_right = self.layout.width - bound_left
        dt = instance_tab_label.center_x - bound_left
        sx, sy = self.scrollview.convert_distance_to_scroll(dt, 0)
        lsx = self.last_scroll_x  # ast scroll x of the tab bar
        scroll_is_late = lsx < sx  # determine scroll direction
        dst = abs(lsx - sx) * step  # distance to run

        if not dst:
            return
        if scroll_is_late and instance_tab_label.center_x > bound_left:
            x = lsx + dst
        elif not scroll_is_late and instance_tab_label.center_x < bound_right:
            x = lsx - dst
        else:
            return
        x = boundary(x, 0.0, 1.0)
        self.scrollview.goto(x, None)

    def android_animation(
        self, instance_carousel: MDTabsCarousel, offset: Union[float, int]
    ):
        # Try to reproduce the android animation effect.
        if offset != 0 and abs(offset) < instance_carousel.width:
            forward = offset < 0
            offset = abs(offset)
            step = offset / float(instance_carousel.width)
            indicator_animation = self.parent.tab_indicator_anim

            skip_slide = (
                instance_carousel.slides[instance_carousel._skip_slide]
                if instance_carousel._skip_slide is not None
                else None
            )
            next_slide = (
                instance_carousel.next_slide
                if forward
                else instance_carousel.previous_slide
            )
            self.target = skip_slide if skip_slide else next_slide

            if not self.target:
                return

            a = instance_carousel.current_slide.tab_label
            b = self.target.tab_label
            self.tab_bar_autoscroll(b, step)

            # Avoids the animation if `indicator_animation` is True.
            if indicator_animation is False:
                return
            gap_x = abs((a.x) - (b.x))
            gap_w = (b.width) - (a.width)
            if forward:
                x_step = a.x + (gap_x * step)
            else:
                x_step = a.x - gap_x * step
            w_step = a.width + (gap_w * step)
            self.update_indicator(x_step, w_step)

    def _label_request_indicator_update(self, *args):
        widget = self.carousel.current_slide.tab_label
        self.update_indicator(widget.x, widget.width)


class MDTabs(ThemableBehavior, SpecificBackgroundColorBehavior, AnchorLayout):
    """
    You can use this class to create your own tabbed panel.

    :Events:
        `on_tab_switch`
            Called when switching tabs.
        `on_slide_progress`
            Called while the slide is scrolling.
        `on_ref_press`
            The method will be called when the ``on_ref_press`` event
            occurs when you, for example, use markup text for tabs.
    """

    tab_bar_height = NumericProperty("48dp")
    """
    Height of the tab bar.

    :attr:`tab_bar_height` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `'48dp'`.
    """

    tab_padding = ListProperty([0, 0, 0, 0])
    """
    Padding of the tab bar.

    :attr:`tab_padding` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[0, 0, 0, 0]`.
    """

    tab_indicator_anim = BooleanProperty(False)
    """
    Tab indicator animation. If you want use animation set it to ``True``.

    :attr:`tab_indicator_anim` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    tab_indicator_height = NumericProperty("2dp")
    """
    Height of the tab indicator.

    :attr:`tab_indicator_height` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `'2dp'`.
    """

    tab_indicator_type = OptionProperty(
        "line", options=["line", "fill", "round", "line-round", "line-rect"]
    )
    """
    Type of tab indicator. Available options are: `'line'`, `'fill'`,
    `'round'`, `'line-rect'` and `'line-round'`.

    :attr:`tab_indicator_type` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'line'`.
    """

    tab_hint_x = BooleanProperty(False)
    """
    This option affects the size of each child. if it's `True`, the size of
    each tab will be ignored and will use the size available by the container.

    :attr:`tab_hint_x` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    anim_duration = NumericProperty(0.2)
    """
    Duration of the slide animation.

    :attr:`anim_duration` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0.2`.
    """

    anim_threshold = BoundedNumericProperty(
        0.8, min=0.0, max=1.0, errorhandler=lambda x: 0.0 if x < 0.0 else 1.0
    )
    """
    Animation threshold allow you to change the tab indicator animation effect.

    :attr:`anim_threshold` is an :class:`~kivy.properties.BoundedNumericProperty`
    and defaults to `0.8`.
    """

    allow_stretch = BooleanProperty(True)
    """
    If `True`, the tab will update dynamically (if :attr:`tab_hint_x` is `True`)
    to it's content width, and wrap any text if the widget is wider than `"360dp"`.

    If `False`, the tab won't update to it's maximum texture width.
    this means that the `fixed_tab_label_width` will be used as the label
    width. this will wrap any text inside to fit the fixed value.

    :attr:`allow_stretch` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """

    fixed_tab_label_width = NumericProperty("140dp")
    """
    If :attr:`allow_stretch` is `False`, the class will set this value as the
    width to all the tabs title label.

    :attr:`fixed_tab_label_width` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `140dp`.
    """

    background_color = ColorProperty(None)
    """
    Background color of tabs in ``rgba`` format.

    :attr:`background_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    underline_color = ColorProperty([0, 0, 0, 0])
    """
    Underline color of tabs in ``rgba`` format.

    :attr:`underline_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `[0, 0, 0, 0]`.
    """

    text_color_normal = ColorProperty(None)
    """
    Text color of the label when it is not selected.

    :attr:`text_color_normal` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    text_color_active = ColorProperty(None)
    """
    Text color of the label when it is selected.

    :attr:`text_color_active` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    elevation = NumericProperty(0)
    """
    Tab value elevation.

    .. seealso::

        `Behaviors/Elevation <https://kivymd.readthedocs.io/en/latest/behaviors/elevation/index.html>`_

    :attr:`elevation` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `0`.
    """

    indicator_color = ColorProperty(None)
    """
    Color indicator in ``rgba`` format.

    :attr:`indicator_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    lock_swiping = BooleanProperty(False)
    """
    If True - disable switching tabs by swipe.

    :attr:`lock_swiping` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    font_name = StringProperty("Roboto")
    """
    Font name for tab text.

    :attr:`font_name` is an :class:`~kivy.properties.StringProperty`
    and defaults to `'Roboto'`.
    """

    ripple_duration = NumericProperty(2)
    """
    Ripple duration when long touching to tab.

    :attr:`ripple_duration` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `2`.
    """

    no_ripple_effect = BooleanProperty(True)
    """
    Whether to use the ripple effect when tapping on a tab.

    :attr:`no_ripple_effect` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """

    title_icon_mode = OptionProperty("Lead", options=["Lead", "Top"])
    """
    This property sets the mode in wich the tab's title and icon are shown.

    :attr:`title_icon_mode` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'Lead'`.
    """

    force_title_icon_mode = BooleanProperty(True)
    """
    If this property is se to `True`, it will force the class to update every
    tab inside the scroll view to the current `title_icon_mode`

    :attr:`force_title_icon_mode` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `True`.
    """

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.register_event_type("on_tab_switch")
        self.register_event_type("on_ref_press")
        self.register_event_type("on_slide_progress")
        Clock.schedule_once(self._carousel_bind, 1)
        self.theme_cls.bind(
            primary_palette=self.update_icon_color,
            theme_style=self.update_icon_color,
        )
        self.bind(
            force_title_icon_mode=self._parse_icon_mode,
            title_icon_mode=self._parse_icon_mode,
        )
        self.bind(tab_hint_x=self._update_tab_hint_x)

    def update_icon_color(
        self,
        instance_theme_manager: ThemeManager,
        name_theme_style_name_palette: str,
    ) -> None:
        """
        Called when the app's color scheme or style has changed
        (dark theme/light theme).
        """

        for tab_label in self.get_tab_list():
            if not self.text_color_normal:
                tab_label.text_color_normal = self.theme_cls.text_color
            if not self.text_color_active:
                tab_label.text_color_active = self.specific_secondary_text_color

    def switch_tab(self, name_tab: Union[MDTabsLabel, str], search_by="text"):
        """
        This method switch between tabs
        name_tab can be either a String or a :class:`~MDTabsBase`.

        `search_by` will look up through the properties of every tab.

        If the value doesnt match, it will raise a ValueError.

        Search_by options:
            text : will search by the raw text of the label (`tab_label_text`)
            icon : will search by the `icon` property
            title : will search by the `title` property
        """

        if isinstance(name_tab, str):
            if search_by == "title":
                for tab_instance in self.tab_bar.parent.carousel.slides:
                    if tab_instance.title_is_capital is True:
                        _name_tab = name_tab.upper()
                    else:
                        _name_tab = name_tab
                    if tab_instance.title == _name_tab:
                        self.carousel.load_slide(tab_instance)
                        return
            # Search by icon.
            elif search_by == "icon":
                for tab_instance in self.tab_bar.parent.carousel.slides:
                    if tab_instance.icon == name_tab:
                        self.carousel.load_slide(tab_instance)
                        return
            # Search by title.
            else:
                for tab_instance in self.tab_bar.parent.carousel.slides:
                    if tab_instance.tab_label_text == name_tab:
                        self.carousel.load_slide(tab_instance)
                        return
            raise ValueError(
                "switch_tab:\n\t"
                "name_tab not found in the tab list\n\t"
                f"search_by = {repr(search_by)} \n\t"
                f"name_tab = {repr(name_tab)} \n\t"
            )
        else:
            self.carousel.load_slide(name_tab.tab)

    def get_tab_list(self) -> list:
        """Returns a list of :class:`~MDTabsLabel` objects."""

        return self.tab_bar.layout.children[::-1]

    def get_slides(self) -> list:
        """Returns a list of user tab objects."""

        return self.carousel.slides

    def get_current_tab(self):
        """
        Returns current tab object.

        .. versionadded:: 1.0.0
        """

        return self.carousel.current_slide

    def add_widget(self, widget, index=0, canvas=None):
        # You can add only subclass of MDTabsBase.
        if not isinstance(widget, (MDTabsBase, MDTabsMain, MDTabsBar)):
            raise ValueError(
                f"MDTabs[{self.uid}].add_widget:\n\t"
                "The widget provided is not a subclass of MDTabsBase."
            )
        if len(self.children) >= 2:
            try:
                # FIXME: Can't set the value of the `no_ripple_effect`
                #  and `ripple_duration` properties for widget.tab_label.
                widget.tab_label._no_ripple_effect = self.no_ripple_effect
                widget.tab_label.ripple_duration_in_slow = self.ripple_duration
                widget.tab_label.group = str(self)
                widget.tab_label.tab_bar = self.tab_bar
                widget.tab_label.font_name = self.font_name
                widget.tab_label.text_color_normal = (
                    self.text_color_normal
                    if self.text_color_normal
                    else self.specific_secondary_text_color
                )
                widget.tab_label.text_color_active = (
                    self.text_color_active
                    if self.text_color_active
                    else self.specific_text_color
                )
                self.bind(
                    allow_stretch=widget.tab_label._update_text_size,
                    fixed_tab_label_width=widget.tab_label._update_text_size,
                    font_name=widget.tab_label.setter("font_name"),
                    text_color_active=widget.tab_label.setter(
                        "text_color_active"
                    ),
                    text_color_normal=widget.tab_label.setter(
                        "text_color_normal"
                    ),
                )
                Clock.schedule_once(widget.tab_label._update_text_size, 0)
                self.tab_bar.layout.add_widget(widget.tab_label)
                self.carousel.add_widget(widget)
                if self.force_title_icon_mode is True:
                    widget.title_icon_mode = self.title_icon_mode
                Clock.schedule_once(
                    self.tab_bar._label_request_indicator_update, 0
                )
                return
            except AttributeError:
                pass
        if isinstance(widget, (MDTabsMain, MDTabsBar)):
            return super().add_widget(widget)

    def remove_widget(self, widget):
        # You can remove only subclass of MDTabsLabel or MDTabsBase.
        if not issubclass(widget.__class__, (MDTabsLabel, MDTabsBase)):
            raise MDTabsException(
                "MDTabs can remove only subclass of MDTabsLabel or MDTabsBase"
            )
        # If the widget is an instance of MDTabsBase, then the widget is
        # set as the widget's tab_label object.
        if issubclass(widget.__class__, MDTabsBase):
            slide = widget
            title_label = widget.tab_label
        else:
            # We already got the label, so we set the slide reference.
            slide = widget.tab
            title_label = widget
        # Set memory.
        # Search object next tab.
        # Clean all bindings to allow the widget to be collected.
        self.unbind(
            allow_stretch=title_label._update_text_size,
            fixed_tab_label_width=title_label._update_text_size,
            font_name=title_label.setter("font_name"),
            text_color_active=title_label.setter("text_color_active"),
            text_color_normal=title_label.setter("text_color_normal"),
        )
        self.carousel.remove_widget(slide)
        self.tab_bar.layout.remove_widget(title_label)
        # Clean the references.
        slide = None
        title_label = None
        widget = None

    def on_slide_progress(self, *args) -> None:
        """
        This event is deployed every available frame while the tab is scrolling.
        """

    def on_carousel_index(self, instance_tabs_carousel, index: int) -> None:
        """
        Called when the Tab index have changed.

        This event is deployed by the built in carousel of the class.
        """

        # When the index of the carousel change, update tab indicator,
        # select the current tab and reset threshold data.
        if instance_tabs_carousel.current_slide:
            current_tab_label = instance_tabs_carousel.current_slide.tab_label
            if current_tab_label.state == "normal":
                # current_tab_label._do_press()
                current_tab_label.dispatch("on_release")
                current_tab_label._release_group(self)
                current_tab_label.state = "down"

            if self.tab_indicator_type == "round":
                self.tab_indicator_height = self.tab_bar_height
                if index == 0:
                    radius = [
                        0,
                        self.tab_bar_height / 2,
                        self.tab_bar_height / 2,
                        0,
                    ]
                    self.tab_bar.update_indicator(
                        current_tab_label.x, current_tab_label.width, radius
                    )
                elif index == len(self.get_tab_list()) - 1:
                    radius = [
                        self.tab_bar_height / 2,
                        0,
                        0,
                        self.tab_bar_height / 2,
                    ]
                    self.tab_bar.update_indicator(
                        current_tab_label.x, current_tab_label.width, radius
                    )
                else:
                    radius = [
                        self.tab_bar_height / 2,
                    ]
                    self.tab_bar.update_indicator(
                        current_tab_label.x, current_tab_label.width, radius
                    )
            elif (
                self.tab_indicator_type == "fill"
                or self.tab_indicator_type == "line-round"
                or self.tab_indicator_type == "line-rect"
            ):
                self.tab_indicator_height = self.tab_bar_height
                self.tab_bar.update_indicator(
                    current_tab_label.x, current_tab_label.width
                )
            else:
                self.tab_bar.update_indicator(
                    current_tab_label.x, current_tab_label.width
                )

    def on_ref_press(self, *args) -> None:
        """
        This event will be launched every time the user press a markup enabled
        label with a link or reference inside.
        """

    def on_tab_switch(self, *args) -> None:
        """This event is launched every time the current tab is changed."""

    def on_size(self, instance_tab, size: list) -> None:
        """Called when the application screen is resized."""

        if self.carousel.current_slide:
            self._update_indicator(self.carousel.current_slide.tab_label)

    def _update_tab_hint_x(self, *args):
        if not self.ids.layout.children:
            return
        if self.tab_hint_x is True:
            self.fixed_tab_label_width = self.width // len(
                self.ids.layout.children
            )
            self.allow_stretch = False
        else:
            self.allow_stretch = True

    def _parse_icon_mode(self, *args):
        if self.force_title_icon_mode is True:
            for slide in self.carousel.slides:
                slide.title_icon_mode = self.title_icon_mode
                if self.title_icon_mode == "Top":
                    self.tab_bar_height = dp(72)
                else:
                    self.tab_bar_height = dp(48)

    def _carousel_bind(self, interval):
        self.carousel.bind(on_slide_progress=self._on_slide_progress)

    def _on_slide_progress(self, *args):
        self.dispatch("on_slide_progress", args)

    def _update_indicator(self, current_tab_label):
        def update_indicator(interval):
            self.tab_bar.update_indicator(
                current_tab_label.x, current_tab_label.width
            )

        if not current_tab_label:
            current_tab_label = self.tab_bar.layout.children[-1]
        Clock.schedule_once(update_indicator)

    def _update_padding(self, layout, *args):
        if self.tab_hint_x is True:
            layout.padding = [0, 0]
            Clock.schedule_once(self._update_tab_hint_x)
            return True
        padding = [0, 0]
        # FIXME: It's not entirely clear why the `padding = [dp (52), 0]`
        #  instruction is needed? This creates an extra 52px left padding and
        #  looks like a bug. This instruction was added by the contributors in
        #  previous commits and I have not yet figured out why this was done.
        # This is more efficient than to use sum([layout.children]).
        # width = layout.width - (layout.padding[0] * 2)
        # Forces the padding of the tab_bar when the tab_bar is scrollable.
        # if width > self.width:
        #     padding = [dp(52), 0]
        # Set the new padding.
        layout.padding = padding
        # Update the indicator.
        if self.carousel.current_slide:
            self._update_indicator(self.carousel.current_slide.tab_label)
            Clock.schedule_once(
                lambda x: setattr(
                    self.carousel.current_slide.tab_label, "state", "down"
                ),
                -1,
            )
        return True
