# Copyright (c) 2018-2020, NVIDIA CORPORATION.  All rights reserved.
#
# NVIDIA CORPORATION and its licensors retain all intellectual property
# and proprietary rights in and to this software, related documentation
# and any modifications thereto.  Any use, reproduction, disclosure or
# distribution of this software and related documentation without an express
# license agreement from NVIDIA CORPORATION is strictly prohibited.
#

# pylint: disable=attribute-defined-outside-init, protected-access,
# pylint: disable=access-member-before-definition, unnecessary-lambda-assignment

import asyncio
import contextlib
import fnmatch
from collections import OrderedDict, defaultdict
from enum import IntFlag
from functools import lru_cache
from typing import Callable, List, Optional

import carb
import carb.events
import carb.settings
import omni.kit.app
import omni.kit.commands
import omni.ui as ui
from omni.kit.widget.filter import FilterButton
from omni.kit.widget.options_menu import OptionItem
from omni.ui import color as cl

from .common import (
    EXTENSION_PULL_STARTED_EVENT,
    REGISTRIES_CHANGED_EVENT,
    ExtAuthorGroup,
    ExtensionCommonInfo,
    ExtSource,
    get_categories,
    get_open_example_links,
    is_community_tab_enabled,
)
from .ext_components import ExtensionToggle, ExtSourceSelector, SearchWidget
from .ext_controller import are_featured_exts_enabled
from .ext_export_import import import_ext
from .ext_template import create_new_extension_template
from .styles import (
    EXT_ICON_SIZE,
    EXT_ITEM_TITLE_BAR_H,
    EXT_LIST_ITEM_H,
    EXT_LIST_ITEM_ICON_ZONE_W,
    EXT_LIST_ITEMS_MARGIN_H,
)
from .utils import get_ext_info_dict, get_extpath_git_ext

ext_manager = None


# Sync only once for now
@lru_cache()
def _sync_registry():
    ext_manager.refresh_registry()


class ExtGroupItem(ui.AbstractItem):
    def __init__(self, name):
        super().__init__()
        self.name = name
        self.total_count = 0
        self.items = []
        self.ext_source = ExtSource.NVIDIA

    def contains(self, ext_summary):
        return True

    def is_expanded_by_default(self):
        return True


class ExtFeatureGroupItem(ExtGroupItem):
    def contains(self, ext_summary):
        return ext_summary.feature


class ExtToggleableGroupItem(ExtGroupItem):
    def contains(self, ext_summary):
        return ext_summary.toggleable


class ExtNonToggleableGroupItem(ExtGroupItem):
    def contains(self, ext_summary):
        return not ext_summary.toggleable

    def is_expanded_by_default(self):
        return False


class ExtAuthorGroupItem(ExtGroupItem):
    def __init__(self, name, author_group: ExtAuthorGroup):
        super().__init__(name)
        self.author_group = author_group
        self.ext_source = ExtSource.THIRD_PARTY

    def contains(self, ext_summary):
        return ext_summary.author_group == self.author_group

    def is_expanded_by_default(self):
        return True


class ExtSummaryItem(ui.AbstractItem):
    """An abstract item that represents an extension summary"""

    def refresh(self, ext_summary):
        self.fullname = ext_summary["fullname"]
        self.flags = ext_summary["flags"]
        self.enabled_ext = ext_summary["enabled_version"]
        self.latest_ext = ext_summary["latest_version"]

        self.default_ext = self.latest_ext
        self.enabled = bool(self.flags & omni.ext.EXTENSION_SUMMARY_FLAG_ANY_ENABLED)

        if self.enabled:
            self.default_ext = self.enabled_ext

        self.id = self.default_ext["id"]

        is_latest = self.enabled_ext["id"] == self.latest_ext["id"]
        self.can_update = self.enabled and not is_latest

        # Query more info
        ext_info, is_local = get_ext_info_dict(ext_manager, self.default_ext)

        # Take latest version (in terms of semver) and grab publish date from it if any, for sorting invert it.
        latest_info = ext_info if is_latest else get_ext_info_dict(ext_manager, self.latest_ext)[0]
        publish_date = latest_info.get("package", {}).get("publish", {}).get("date", 0)
        self.publish_date_rev = -publish_date

        # Move all attributes from ExtensionCommonInfo class to this class
        self.__dict__.update(ExtensionCommonInfo(self.id, ext_info, is_local).__dict__)


class ExtSummariesModel(ui.AbstractItemModel):
    """Extension summary model that watches the changes in ext manager"""

    class SortDirection(IntFlag):
        NONE = 0
        Ascending = 1
        Descending = 2

    def __init__(self, flat=None):
        super().__init__()

        global ext_manager
        app = omni.kit.app.get_app()
        ext_manager = app.get_extension_manager()

        self._ext_summaries = {}

        # Order matters here, because contains() matches first group
        self._groups = [
            ExtAuthorGroupItem("User", ExtAuthorGroup.USER),
            ExtAuthorGroupItem("Partner", ExtAuthorGroup.PARTNER),
            ExtAuthorGroupItem("Community - Verified", ExtAuthorGroup.COMMUNITY_VERIFIED),
            ExtAuthorGroupItem("Community - Unverified", ExtAuthorGroup.COMMUNITY_UNVERIFIED),
            ExtFeatureGroupItem("Feature"),
            ExtToggleableGroupItem("General"),
            ExtNonToggleableGroupItem("Non-Toggleable"),
        ]

        # Attribute in ExtSummaryItem to sort by. For each attribute store order of sort in a dict (can be toggled)
        self._sort_attr = "name"
        self._sort_direction = ExtSummariesModel.SortDirection.Ascending

        # exscribe for ext manager changes and generic event
        def on_change(*_):
            self._resync_exts()

        self._subs = []
        self._subs.append(
            ext_manager.get_change_event_stream().create_subscription_to_pop(
                on_change, name="ExtSummariesModel watch for exts"
            )
        )
        bus = app.get_message_bus_event_stream()
        self._subs.append(bus.create_subscription_to_pop_by_type(REGISTRIES_CHANGED_EVENT, on_change))
        self._subs.append(bus.create_subscription_to_pop_by_type(EXTENSION_PULL_STARTED_EVENT, on_change))

        # Current name filter
        self._filter_name_text = []

        # Current category filter
        self._filter_category = ""

        # Current ext source
        self._filter_ext_source = ExtSource.NVIDIA

        # Hacky link back to tree view to be able to update expanded state
        self.tree_view = None

        # on refresh callback
        self.on_refresh_cb = None

        # Builds a list
        self._resync_exts()

    def resync_registry(self):
        ext_manager.refresh_registry()

    def refresh_all(self):
        self._resync_exts()

    def _resync_exts(self):
        get_extpath_git_ext.cache_clear()
        for (_, _, clear_cache) in ExtsListWidget.searches.values():
            if clear_cache is not None:
                clear_cache()

        _sync_registry()

        ext_summaries = ext_manager.fetch_extension_summaries()

        # groups
        for g in self._groups:
            g.total_count = 0

        self._ext_summaries = {}
        for ext_s in ext_summaries:
            item = ExtSummaryItem()
            item.refresh(ext_s)

            # assign group
            item.group = next(x for x in self._groups if x.contains(item))
            item.group.total_count += 1

            self._item_changed(item)
            self._ext_summaries[ext_s["fullname"]] = item

        self._item_changed(None)

        # Update expanded state after tree was built and rendered
        async def _delayed_expand():
            await omni.kit.app.get_app().next_update_async()
            if self.tree_view:
                for g in self._groups:
                    self.tree_view.set_expanded(g, g.is_expanded_by_default(), False)

        if self.tree_view:
            asyncio.ensure_future(_delayed_expand())

        self._sorted_summaries = list(self._ext_summaries.values())
        self._make_sure_sorted()

    def _refresh_item_group_lists(self):
        # Filtering logic here:
        matching_fns = []

        # Split text in words and look for @keywords. Add matching functions and remove from the list.
        # To remove from list while iterating use backward iteration trick.
        parts = self._filter_name_text.copy()
        for i in range(len(parts) - 1, -1, -1):
            w = parts[i]
            if w.startswith("@"):
                # Predefined keywords or use default ones
                fn = ExtsListWidget.searches.get(w, [None, None, None])[1]
                if fn is None:
                    fn = lambda item, w=w: w[1:] in item.keywords
                matching_fns.append(fn)
                del parts[i]

        # Whatever left just use as wildcard search:
        if len(parts) > 0:
            for part in parts:
                filter_str = f"*{part.lower()}*"
                matching_fns.append(
                    lambda item, filter_str=filter_str: fnmatch.fnmatch(item.name.lower(), filter_str)
                    or fnmatch.fnmatch(item.fullname.lower(), filter_str)
                )

        # If category filter enabled, add matching function for it
        if len(self._filter_category) != 0:
            matching_fns.append(lambda item: self._filter_category == item.category)
        else:
            hidden_categories = [c for c, v in get_categories().items() if v.get("hidden", False)]
            matching_fns.append(lambda item: item.category not in hidden_categories)

        # Show item only if it matches all matching functions
        for group in self._groups:
            group.items = [v for v in self._sorted_summaries if v.group == group and all(f(v) for f in matching_fns)]
            self._item_changed(group)

        # callback
        if self.on_refresh_cb:
            self.on_refresh_cb()

    def get_item_children(self, item):
        """Reimplemented from AbstractItemModel"""

        if item is None:
            self._refresh_item_group_lists()
            return [g for g in self._groups if g.total_count and g.ext_source == self._filter_ext_source]
        if isinstance(item, ExtGroupItem):
            return item.items

        return []

    def get_item_value_model_count(self, item):
        """Reimplemented from AbstractItemModel"""
        return 1

    def get_item_value_model(self, item, column_id):
        """Reimplemented from AbstractItemModel"""
        return

    def filter_by_text(self, filter_name_text, filters):
        """Specify the filter string that is used to reduce the model"""
        new_list = filters + [filter_name_text]
        if self._filter_name_text == new_list and not new_list:
            return

        self._filter_name_text = new_list

        # Avoid refreshing on every key typed, as it can be slow. Add delay:
        self._refresh_once_with_delay(delay=0.3)

    def select_category(self, filter_category):
        # Special case for "All" => ""
        if self._filter_category == filter_category:
            return

        self._filter_category = filter_category
        self._refresh_once_with_delay(delay=0.1)

    def select_ext_source(self, ext_source):
        if self._filter_ext_source == ext_source:
            return False

        self._filter_ext_source = ext_source
        self._resync_exts()
        return True

    def _refresh_once_with_delay(self, delay: float):
        """Call refresh once after `delay` seconds. If called again before `delay` is passed the timer gets reset."""

        async def _delayed_refresh():
            await asyncio.sleep(delay)
            self._item_changed(None)

        with contextlib.suppress(Exception):
            self._refresh_task.cancel()

        self._refresh_task = asyncio.ensure_future(_delayed_refresh())

    def sort_by_attr(self, attr: str):
        """Sort by item attribute."""
        if attr is None:
            return

        if self._sort_attr != attr:
            self._sort_attr = attr

        self._make_sure_sorted()

    def sort_direction(self, direction: SortDirection):
        """Sort by direction."""
        if direction is None:
            return

        self._sort_direction = direction

        self._make_sure_sorted()

    def _make_sure_sorted(self):
        # Sort order setting is global
        reverse = self._sort_direction == ExtSummariesModel.SortDirection.Descending

        # Attribute value getter
        key_fn = lambda x: getattr(x, self._sort_attr)
        cmp_fn = (lambda x, y: x >= y) if reverse else (lambda x, y: x <= y)

        # Check that it is already sorted to avoid list rebuild
        is_sorted_fn = lambda l: all(cmp_fn(key_fn(l[i]), key_fn(l[i + 1])) for i in range(len(l) - 1))

        if not is_sorted_fn(self._sorted_summaries):
            self._sorted_summaries = sorted(self._sorted_summaries, key=key_fn, reverse=reverse)
            self._item_changed(None)

    def destroy(self):
        self._ext_summaries = {}
        self._sorted_summaries = []
        self._subs = None
        self.tree_view = None
        self.on_refresh_cb = None


class ExtsDelegate(ui.AbstractItemDelegate):
    def build_branch(self, model, item, column_id, level, expanded):
        """ui.AbstractItemDelegate API: Create a branch widget that opens or closes subtree"""
        return

    def build_widget(self, model, item, column_id, level, expanded):
        """ui.AbstractItemDelegate API: Create a widget per column"""

        if isinstance(item, ExtGroupItem):
            with ui.ZStack():
                with ui.VStack():
                    ui.Spacer(height=1)
                    ui.Rectangle(height=30, name="ExtensionListGroup")
                with ui.HStack():
                    image_name = "expanded" if expanded else ""
                    ui.Image(
                        style_type_name_override="ExtensionList.Group.Icon",
                        name=image_name,
                        width=31,
                        height=31,
                        alignment=ui.Alignment.LEFT_CENTER,
                    )
                    ui.Label(
                        "{} ({}/{})".format(item.name, len(item.items), item.total_count),
                        style_type_name_override="ExtensionList.Group.Label",
                    )
            return

        h = EXT_LIST_ITEM_H - EXT_ITEM_TITLE_BAR_H
        with ui.VStack(height=EXT_LIST_ITEM_H + EXT_LIST_ITEMS_MARGIN_H):
            with ui.ZStack():
                with ui.VStack(height=0):
                    ui.Rectangle(height=EXT_LIST_ITEM_H, style_type_name_override="ExtensionList.Background")
                    ui.Rectangle(height=EXT_LIST_ITEMS_MARGIN_H, style_type_name_override="ExtensionList.Separator")

                with ui.VStack():
                    # Top row
                    with ui.HStack(height=EXT_ITEM_TITLE_BAR_H):
                        # Title
                        ui.Label(
                            item.title, style_type_name_override="ExtensionList.Label", name="Title", elided_text=True
                        )
                        # Toggle
                        ExtensionToggle(item, show_install_button=not item.is_untrusted)
                        ui.Spacer(width=5)

                    with ui.HStack():
                        with ui.HStack(width=EXT_LIST_ITEM_ICON_ZONE_W):
                            ui.Spacer()
                            with ui.VStack(width=EXT_ICON_SIZE[0]):
                                ui.Spacer()
                                ui.Image(item.icon_path, height=EXT_ICON_SIZE[1], style={"color": 0xFFFFFFFF})
                                ui.Spacer()
                            ui.Spacer()

                        with ui.ZStack():
                            ui.Rectangle(height=h, style_type_name_override="ExtensionList.Foreground")
                            with ui.VStack():
                                ui.Spacer(height=5)
                                # Category name
                                category = get_categories().get(item.category)
                                ui.Label(
                                    category["name"],
                                    height=0,
                                    style_type_name_override="ExtensionList.Label",
                                    name="Category",
                                )
                                # Extension id
                                ui.Label(
                                    item.fullname,
                                    height=0,
                                    style_type_name_override="ExtensionList.Label",
                                    name="Id",
                                    elided_text=True,
                                )
                                # Version
                                if item.version and item.version[0:3] != (0, 0, 0):
                                    ui.Label(
                                        "v" + item.version,
                                        style_type_name_override="ExtensionList.Label",
                                        name="Version",
                                        alignment=ui.Alignment.BOTTOM,
                                    )

                            # Update available?
                            # if item.can_update:
                            with ui.VStack():
                                ui.Spacer(height=10)
                                with ui.HStack():
                                    ui.Spacer()
                                    if item.can_update:
                                        ui.Image(
                                            name="UpdateAvailable",
                                            width=28,
                                            height=31,
                                            alignment=ui.Alignment.RIGHT_CENTER,
                                        )
                                    ui.Spacer(width=10)
                                if item.can_update:
                                    ui.Label(
                                        "UPDATE AVAILABLE", alignment=ui.Alignment.RIGHT_CENTER, name="UpdateAvailable"
                                    )
                                elif item.author_group != ExtAuthorGroup.NVIDIA:
                                    with ui.HStack():
                                        ui.Spacer()
                                        with ui.VStack(width=148, height=0):
                                            ui.Spacer()
                                            with ui.ZStack():
                                                ui.Image(
                                                    name="Community",
                                                    width=148,
                                                    height=32,
                                                    alignment=ui.Alignment.RIGHT_BOTTOM,
                                                )
                                                name = item.author_group.get_ui_name()
                                                ui.Label(name, alignment=ui.Alignment.RIGHT_CENTER, name="Community")
                                elif item.location_tag:
                                    ui.Label(item.location_tag, alignment=ui.Alignment.RIGHT_CENTER, name="LocationTag")
            ui.Spacer()

    def build_header(self, column_id):
        pass


class ExtsListWidget:
    # Search configuration information as SearchKey:(SearchUiName, FilterBySearchKey, ClearSearchCache)
    searches = OrderedDict(
        {
            "@startup": ("Startup", lambda item: item.is_startup, None),
            "@featured": ("Featured", lambda item: item.is_featured or item.ext_source == ExtSource.THIRD_PARTY, None),
            "@bundled": ("Bundled", lambda item: bool(item.flags & omni.ext.EXTENSION_SUMMARY_FLAG_BUILTIN), None),
            "@user": ("User", lambda item: item.author_group == ExtAuthorGroup.USER, None),
            "@app": ("App", lambda item: item.is_app, None),
            "@enabled": ("Enabled", lambda item: bool(item.flags & omni.ext.EXTENSION_SUMMARY_FLAG_ANY_ENABLED), None),
            "@update": ("Update Available", lambda item: item.can_update, None),
            "@installed": (
                "Installed",
                lambda item: bool(item.flags & omni.ext.EXTENSION_SUMMARY_FLAG_INSTALLED),
                None,
            ),
            "@remote": ("Remote", lambda item: not bool(item.flags & omni.ext.EXTENSION_SUMMARY_FLAG_BUILTIN), None),
        }
    )

    # remove "Featured" when not in use
    if not are_featured_exts_enabled():
        del searches["@featured"]

    def __init__(self):
        # Extensions List Data Model
        self._model = ExtSummariesModel()
        self._model.on_refresh_cb = self._on_ext_list_refresh

        menu_style = {
            "padding": 2,
            "Menu.Item.CheckMark": {"color": cl.shade(cl("#34C7FF"))},
            "Titlebar.Background": {"background_color": cl.shade(cl("#1F2123"))},
            "Titlebar.Title": {"color": cl.shade(cl("#848484"))},
            "Titlebar.Reset": {"background_color": 0},
            "Titlebar.Reset.Label": {"color": cl.shade(cl("#2E86A9"))},
            "Titlebar.Reset.Label:hovered": {"color": cl.shade(cl("#34C7FF"))},
        }

        # Delegate to build list rows.
        self._delegate = ExtsDelegate()

        self.set_show_graph_fn(None)
        self.set_show_properties_fn(None)

        # Filter button
        self._filter_button: Optional[FilterButton] = None

        # Sort menu. One day it will be a hamburger. One can dream.
        self._sortby_menu = ui.Menu("Sort By", style=menu_style, menu_compatibility=False, tearable=False)
        self.rebuild_sortby_menu()

        self._options_menu = ui.Menu("Options", style=menu_style, menu_compatibility=False, tearable=False)
        self.rebuild_options_menu()

        # Extension creation menu
        self._create_menu = ui.Menu("Create", style=menu_style, menu_compatibility=False, tearable=False)
        self.rebuild_create_menu()

    def _menu_header(self, title, clicked_fn):
        with ui.ZStack(height=0):
            ui.Rectangle(style_type_name_override="Titlebar.Background")
            with ui.VStack():
                ui.Spacer(height=3)
                with ui.HStack():
                    ui.Spacer(width=10)
                    ui.Label(title, style_type_name_override="Titlebar.Title")
                    ui.Spacer()
                    ui.Button(
                        "Reset all" if clicked_fn else " ",
                        style_type_name_override="Titlebar.Reset",
                        clicked_fn=clicked_fn,
                    )
                    ui.Spacer(width=10)
                ui.Spacer(height=2)

    def rebuild_filter_menu(self):
        if self._filter_button:
            option_items = self._build_filter_items()
            self._filter_button.model.rebuild_items(option_items)

    def rebuild_sortby_menu(self):
        def reset_sort():
            asyncio.ensure_future(update_sort("name", ExtSummariesModel.SortDirection.Ascending))

        async def update_sort(sb: str, sd: ExtSummariesModel.SortDirection):
            if sb:
                await omni.kit.app.get_app().next_update_async()
                self._model.sort_by_attr(sb)

            if sd is not ExtSummariesModel.SortDirection.NONE:
                await omni.kit.app.get_app().next_update_async()
                self._model.sort_direction(sd)

            sort_by = self._model._sort_attr
            sort_direction = self._model._sort_direction

            # change button color
            self._sortby_button.name = (
                "sortby"
                if sort_by == "name" and sort_direction == ExtSummariesModel.SortDirection.Ascending
                else "sortby_on"
            )

            self._sortby_menu.clear()
            with self._sortby_menu:
                self._menu_header("Sort", reset_sort)

                ui.MenuItem(
                    "Name",
                    triggered_fn=lambda: asyncio.ensure_future(
                        update_sort("name", ExtSummariesModel.SortDirection.NONE)
                    ),
                    checkable=True,
                    checked=sort_by == "name",
                )
                ui.MenuItem(
                    "Enabled",
                    triggered_fn=lambda: asyncio.ensure_future(
                        update_sort("enabled", ExtSummariesModel.SortDirection.NONE)
                    ),
                    checkable=True,
                    checked=sort_by == "enabled",
                )
                ui.MenuItem(
                    "Publish Date",
                    triggered_fn=lambda: asyncio.ensure_future(
                        update_sort("publish_date_rev", ExtSummariesModel.SortDirection.NONE)
                    ),
                    checkable=True,
                    checked=sort_by == "publish_date_rev",
                )
                ui.Separator()
                ui.MenuItem(
                    "Ascending",
                    triggered_fn=lambda: asyncio.ensure_future(
                        update_sort(None, ExtSummariesModel.SortDirection.Ascending)
                    ),
                    checkable=True,
                    checked=sort_direction == ExtSummariesModel.SortDirection.Ascending,
                )
                ui.MenuItem(
                    "Descending",
                    triggered_fn=lambda: asyncio.ensure_future(
                        update_sort(None, ExtSummariesModel.SortDirection.Descending)
                    ),
                    checkable=True,
                    checked=sort_direction == ExtSummariesModel.SortDirection.Descending,
                )

        asyncio.ensure_future(update_sort(None, ExtSummariesModel.SortDirection.NONE))

    def rebuild_options_menu(self):
        self._options_menu.clear()
        with self._options_menu:
            self._menu_header("Options", None)
            ui.MenuItem("Settings", triggered_fn=self._show_properties)
            ui.MenuItem("Refresh", triggered_fn=self._model.refresh_all)
            ui.MenuItem("Resync Registry", triggered_fn=self._resync_registry)
            ui.MenuItem("Show Extension Graph", triggered_fn=self._show_graph)
            ui.MenuItem("Import Extension", triggered_fn=import_ext)

    def rebuild_create_menu(self):
        def open_url(url):
            import webbrowser

            webbrowser.open(url)

        self._create_menu.clear()
        with self._create_menu:
            self._menu_header("Create Extension", None)
            ui.MenuItem("New Extension Template Project", triggered_fn=create_new_extension_template)
            for name, url in get_open_example_links():
                ui.MenuItem(name, triggered_fn=lambda u=url: open_url(u))

    def _on_key_pressed(self, key, mod, pressed):
        """Allow up/down arrow to be used to select prev/next extension"""

        def _select_next(treeview: ui.TreeView, model: ui.AbstractItemModel, after=True):
            full_list = model.get_item_children(None)
            selection = treeview.selection
            if not selection:
                treeview.selection = [full_list[0]]
            else:
                index = full_list.index(selection[0])
                index += 1 if after else -1
                if index < 0 or index >= len(full_list):
                    return
                treeview.selection = [full_list[index]]

        if not pressed:
            return
        if mod == 0 and key == int(carb.input.KeyboardInput.DOWN):
            _select_next(self.tree_view, self._model, after=True)
        elif mod == 0 and key == int(carb.input.KeyboardInput.UP):
            _select_next(self.tree_view, self._model, after=False)

    def _on_selection_changed(self, selection):
        item = selection[0] if len(selection) > 0 else None
        if item and isinstance(item, ExtGroupItem):
            self.tree_view.set_expanded(item, not self.tree_view.is_expanded(item), False)
            self.tree_view.clear_selection()
        else:
            self._ext_selected_fn(item)

    def _on_ext_list_refresh(self):
        cnt = defaultdict(int)
        for group in self._model._groups:
            cnt[group.ext_source] += len(group.items)
        for source, value in cnt.items():
            self._ext_source_selector.set_badge_number(source, value)

    async def __update_filter(self, search_key: str) -> None:
        if search_key:
            self._search.toggle_filter(search_key)

        await omni.kit.app.get_app().next_update_async()
        self._filter_by_text(self._search.get_text(), self._search.get_filters())

    def _build_filter_items(self) -> List[OptionItem]:
        option_items = []
        filters = self._search.get_filters()
        for search_key, (search_description, _, _) in self.searches.items():
            option_item = OptionItem(
                search_description,
                on_value_changed_fn=lambda m, sk=search_key: asyncio.ensure_future(self.__update_filter(sk)),
            )
            option_item.model.set_value(search_key in filters)
            option_items.append(option_item)

        return option_items

    def _build_filter_button(self):
        option_items = self._build_filter_items()
        self._filter_button = FilterButton(option_items)

    def build(self):
        with ui.VStack(spacing=3):
            with ui.HStack(height=0):
                # Create button
                ui.Button(name="create", width=22, height=22, clicked_fn=self._create_menu.show)

                # Search field
                with ui.HStack(width=ui.Fraction(2)):
                    self._search = SearchWidget(
                        on_search_fn=lambda t: self._filter_by_text(t, self._search.get_filters())
                    )
                    if are_featured_exts_enabled():
                        self._search.toggle_filter("@featured")

                ui.Spacer(width=10)
                # Category selector
                category_list = [("All", "")] + [(v["name"], c) for c, v in get_categories().items()]
                self._category_combo = ui.ComboBox(0, *[c[0] for c in category_list], style={"padding": 4})
                self._category_combo.model.add_item_changed_fn(
                    lambda model, item: self._select_category(category_list[model.get_item_value_model(item).as_int][1])
                )

                ui.Spacer(width=5)
                # Filter button
                self._build_filter_button()

                # Sort-By button
                self._sortby_button = ui.Button(name="sortby", width=24, height=24, clicked_fn=self._sortby_menu.show)

                # Properies
                ui.Button(name="options", width=24, height=24, clicked_fn=self._options_menu.show)

            # Source selector (community tab)
            self._ext_source_selector = None
            if is_community_tab_enabled():
                self._ext_source_selector = ExtSourceSelector(self._select_ext_source)

            frame = ui.ScrollingFrame(
                horizontal_scrollbar_policy=ui.ScrollBarPolicy.SCROLLBAR_ALWAYS_OFF,
                vertical_scrollbar_policy=ui.ScrollBarPolicy.SCROLLBAR_ALWAYS_ON,
                style_type_name_override="TreeView",
            )
            with frame:
                with ui.ZStack():
                    self.tree_view = ui.TreeView(
                        self._model,
                        header_visible=False,
                        delegate=self._delegate,
                        column_widths=[ui.Fraction(1)],
                        root_visible=False,
                        keep_expanded=False,
                    )
                    self._model.tree_view = self.tree_view
                    self.tree_view.set_selection_changed_fn(self._on_selection_changed)

                    # make that public
                    self.clear_selection = self.tree_view.clear_selection

            frame.set_key_pressed_fn(self._on_key_pressed)

    def set_show_graph_fn(self, fn: Callable):
        self._show_graph_fn = fn

    def set_ext_selected_fn(self, fn: Callable):
        self._ext_selected_fn = fn

    def _resync_registry(self):
        self._model.resync_registry()

    def _show_graph(self):
        if self._show_graph_fn:
            self._show_graph_fn()

    def set_show_properties_fn(self, fn: Callable):
        self._show_properties_fn = fn

    def _show_properties(self):
        if self._show_properties_fn:
            self._show_properties_fn()

    def _filter_by_text(self, filter_text: str, filters: list):
        """Set the search filter string to the models and widgets"""
        self._model.filter_by_text(filter_text, filters)

    def _select_category(self, category: str):
        """Set the category to show"""
        self._model.select_category(category)

    def _select_ext_source(self, ext_source):
        self._model.select_ext_source(ext_source)

    def destroy(self):
        self._delegate = None
        if self._filter_button:
            self._filter_button.destroy()
            self._filter_button = None
        self._sortby_menu = None
        self._options_menu = None
        self._search.destroy()
        self._search = None
        self._category_combo = None
        self._model.destroy()
        self._model = None
