from __future__ import annotations

from concurrent.futures import Future, ThreadPoolExecutor
from contextlib import contextmanager
from logging import getLogger
from pathlib import Path
import threading
import uuid
from typing import (
    Any,
    Callable,
    Sequence,
    Generic,
    Iterator,
    Literal,
    TypeVar,
    overload,
    TYPE_CHECKING,
)
import warnings
from app_model.expressions import create_context
from psygnal import SignalGroup, Signal

from himena._app_model import AppContext, HimenaApplication
from himena._open_recent import RecentFileManager, RecentSessionManager
from himena._utils import get_widget_class_id, import_object
from himena.consts import NO_RECORDING_FIELD
from himena.plugins import _checker, actions as _actions
from himena.profile import AppProfile, load_app_profile
from himena.style import Theme
from himena.standards import BaseMetadata
from himena.types import (
    ClipboardDataModel,
    Size,
    WidgetDataModel,
    NewWidgetBehavior,
    DockArea,
    DockAreaString,
    BackendInstructions,
    WindowRect,
)
from himena.utils.misc import is_subtype
from himena.widgets._backend import BackendMainWindow
from himena.widgets._hist import HistoryContainer, FileDialogHistoryDict
from himena.widgets._initialize import remove_instance
from himena.widgets._typemap import ObjectTypeMap, register_defaults
from himena.widgets._widget_list import TabList, TabArea, DockWidgetList
from himena.widgets._wrapper import ParametricWindow, SubWindow, DockWidget
from himena.workflow import ProgrammaticMethod
from himena._socket import SocketInfo

if TYPE_CHECKING:
    from app_model.types import KeyBindingRule
    from app_model.expressions import BoolOp
    from himena.widgets._widget_list import PathOrPaths

_W = TypeVar("_W")  # backend widget type
_T = TypeVar("_T")  # internal data type
_F = TypeVar("_F")  # function type
_LOGGER = getLogger(__name__)


class MainWindowEvents(SignalGroup, Generic[_W]):
    """Main window events."""

    tab_activated = Signal(TabArea[_W])
    window_activated = Signal(SubWindow[_W])


class MainWindow(Generic[_W]):
    """The main window handler object."""

    def __init__(
        self,
        backend: BackendMainWindow[_W],
        app: HimenaApplication,  # must be initialized
        theme: Theme,
    ) -> None:
        from himena.widgets._initialize import set_current_instance

        self._events = MainWindowEvents()
        self._backend_main_window = backend
        self._internal_clipboard_data: Any | None = None
        self._tab_list = TabList(backend)
        self._new_widget_behavior = NewWidgetBehavior.WINDOW
        self._model_app = app
        self._instructions = BackendInstructions()
        self._history_tab = HistoryContainer[int](max_size=20)
        self._history_command = HistoryContainer[str](max_size=200)
        self._history_closed = HistoryContainer[tuple[Path, str | None]](max_size=10)
        self._file_dialog_hist = FileDialogHistoryDict()
        app.commands.executed.connect(self._on_command_execution)
        backend._connect_main_window_signals(self)
        self._ctx_keys = AppContext(create_context(self, max_depth=0))
        self._tab_list.changed.connect(backend._update_context)
        self._dock_widget_list = DockWidgetList(backend)
        self._recent_manager = RecentFileManager.default(app)
        self._recent_session_manager = RecentSessionManager.default(app)
        if "." not in app.name:
            # likely a mock instance
            set_current_instance(app.name, self)
            self._recent_manager.update_menu()
            self._recent_session_manager.update_menu()
        self._executor = ThreadPoolExecutor(max_workers=5)
        self._global_lock = threading.Lock()
        self._object_type_map = ObjectTypeMap()
        self.theme = theme
        register_defaults(self._object_type_map)

    @property
    def events(self) -> MainWindowEvents[_W]:
        """Main window events."""
        return self._events

    @property
    def object_type_map(self) -> ObjectTypeMap:
        """Mapping object to string that describes the type."""
        return self._object_type_map

    @property
    def socket_info(self) -> SocketInfo:
        """Socket information."""
        eh = self._backend_main_window._event_loop_handler
        return SocketInfo(host=eh._host, port=eh._port)

    @property
    def theme(self) -> Theme:
        """Get the current color theme of the main window."""
        return self._theme

    @theme.setter
    def theme(self, theme: str | Theme) -> None:
        """Set the style of the main window."""
        if isinstance(theme, str):
            theme = Theme.from_global(theme)
        self._theme = theme
        self._backend_main_window._update_widget_theme(theme)

        # if child implements "theme_changed_callback", call it
        for win in self.iter_windows():
            _checker.call_theme_changed_callback(win.widget, theme)
        for dock in self.dock_widgets:
            _checker.call_theme_changed_callback(dock.widget, theme)
        return None

    @property
    def app_profile(self) -> AppProfile:
        """Get the current application profile object."""
        return load_app_profile(self._model_app.name)

    def submit_async_task(
        self,
        func: Callable,
        *args,
        progress_description: str | None = None,
        **kwargs,
    ) -> Future:
        """Submit a task to the thread pool.

        Parameters
        ----------
        func : callable
            Function to run in the background.
        progress_description : str, optional
            Description of the task in the progress bar.
        """
        future = self._executor.submit(func, *args, **kwargs)
        if progress_description is None:
            progress_description = f"Running {func!r}"
        self._backend_main_window._add_job_progress(
            future, desc=progress_description, total=0
        )
        self.model_app.injection_store.process(future)
        return future

    @property
    def tabs(self) -> TabList[_W]:
        """Tab list object."""
        return self._tab_list

    def windows_for_type(self, types: str | list[str]) -> list[SubWindow[_W]]:
        """Get all sub-windows for the given types."""
        windows = []
        if isinstance(types, str):
            types = [types]
        if tab := self.tabs.current():
            for win in tab:
                mtype = win.model_type()
                if mtype and any(is_subtype(mtype, t) for t in types):
                    windows.append(win)
        return windows

    @property
    def dock_widgets(self) -> DockWidgetList[_W]:
        """Dock widget list object."""
        return self._dock_widget_list

    @property
    def model_app(self) -> HimenaApplication:
        """The app-model application instance."""
        return self._model_app

    @property
    def area_size(self) -> tuple[int, int]:
        """(width, height) of the main window tab area."""
        return self._backend_main_window._area_size()

    @property
    def rect(self) -> WindowRect:
        """Window rect (left, top, width, height) of the main window."""
        return self._backend_main_window._main_window_rect()

    @rect.setter
    def rect(self, value) -> None:
        rect = WindowRect.from_tuple(*value)
        return self._backend_main_window._set_main_window_rect(rect)

    @property
    def size(self) -> Size:
        """Size (width, height) of the main window."""
        return self.rect.size()

    @size.setter
    def size(self, size) -> None:
        r0 = self.rect
        s0 = Size(*size)
        self.rect = WindowRect(r0.left, r0.top, s0.width, s0.height)

    @property
    def clipboard(self) -> ClipboardDataModel | None:
        """Get the clipboard data as a ClipboardDataModel instance."""
        model = self._backend_main_window._clipboard_data()
        model.internal_data = self._internal_clipboard_data
        return model

    @clipboard.setter
    def clipboard(self, data: str | ClipboardDataModel) -> None:
        """Set the clipboard data."""
        if isinstance(data, str):
            data = ClipboardDataModel(text=data)
        elif not isinstance(data, ClipboardDataModel):
            raise ValueError("Clipboard data must be a ClipboardDataModel instance.")
        _LOGGER.info("Setting clipboard data: %r", data)
        self._backend_main_window._set_clipboard_data(data)
        self._internal_clipboard_data = data.internal_data

    def set_clipboard(
        self,
        *,
        text: str | None = None,
        html: str | None = None,
        image: Any | None = None,
        files: list[str | Path] | None = None,
        internal_data: Any | None = None,
    ) -> None:
        """Set clipboard data."""
        self.clipboard = ClipboardDataModel(
            text=text,
            html=html,
            image=image,
            files=files or [],
            internal_data=internal_data,
        )

    def add_tab(self, title: str | None = None) -> TabArea[_W]:
        """Add a new tab of given name."""
        return self.tabs.add(title)

    def window_for_id(self, identifier: uuid.UUID) -> SubWindow[_W] | None:
        """Retrieve a sub-window by its identifier."""
        if not isinstance(identifier, uuid.UUID):
            raise ValueError(f"Expected UUID, got {identifier!r}.")
        for win in self.iter_windows():
            if win._identifier == identifier:
                return win

    def _window_for_workflow_id(self, identifier: uuid.UUID) -> SubWindow[_W] | None:
        """Retrieve a sub-window by its workflow identifier."""
        for win in self.iter_windows():
            try:
                last_id = win._widget_workflow.last_id()
            except Exception:
                return None
            if last_id == identifier:
                return win

    def _current_or_new_tab(self) -> tuple[int, TabArea[_W]]:
        if self._new_widget_behavior is NewWidgetBehavior.WINDOW:
            if len(self.tabs) == 0:
                self.add_tab()
                idx = 0
            else:
                idx = self._backend_main_window._current_tab_index()
            tabarea = self.tabs[idx]
        else:
            tabarea = self.add_tab()
            idx = len(self.tabs) - 1
        return idx, tabarea

    def add_widget(
        self,
        widget: _W,
        *,
        title: str | None = None,
    ) -> SubWindow[_W]:
        """Add a widget to the sub window.

        Any widget that can be interpreted by the backend can be added. For example, for
        Qt application, you can add any QWidget instance:

        ```python
        ui.add_widget(QtW.QLabel("Hello world!"), title="my widget!")
        ```

        Parameters
        ----------
        widget : Any
            Widget to add.
        title : str, optional
            Title of the sub-window. If not given, its name will be automatically
            generated.

        Returns
        -------
        SubWindow
            The sub-window handler.
        """
        _, tabarea = self._current_or_new_tab()
        return tabarea.add_widget(widget, title=title)

    def add_dock_widget(
        self,
        widget: _W,
        *,
        title: str | None = None,
        area: DockAreaString | DockArea | None = DockArea.RIGHT,
        allowed_areas: list[DockAreaString | DockArea] | None = None,
        _identifier: uuid.UUID | None = None,
    ) -> DockWidget[_W]:
        """Add a custom widget as a dock widget of the main window.

        Parameters
        ----------
        widget : Widget type
            Widget instance that is allowed for the backend.
        title : str, optional
            Title of the dock widget.
        area : dock area, default DockArea.RIGHT
            String or DockArea enum that describes where the dock widget initially
            appears.
        allowed_areas : list of dock area, optional
            List of allowed dock areas for the widget.

        Returns
        -------
        DockWidget
            The dock widget handler.
        """
        dock = DockWidget(widget, self._backend_main_window, identifier=_identifier)
        dock_native = dock._split_interface_and_frontend()[1]
        self._backend_main_window.add_dock_widget(
            dock_native, title=title, area=area, allowed_areas=allowed_areas
        )
        self._dock_widget_list._add_dock_widget(dock)
        _checker.call_widget_added_callback(widget)
        _checker.call_theme_changed_callback(widget, self.theme)
        return dock

    def add_object(
        self,
        value: Any,
        *,
        type: str | None = None,
        title: str | None = None,
        force_open_with: str | None = None,
        metadata: Any | None = None,
    ) -> SubWindow[_W]:
        """Add any data as a widget data model.

        Parameters
        ----------
        value : Any
            Any object. Whether it can be represented as a widget is dependent on the
            plugins that are installed.
        type : str, optional
            Any str that describes the type of the object. This type must be registered
            with a proper widget class.
        title : str, optional
            Title of the sub-window.

        Returns
        -------
        SubWindow
            The sub-window handler.
        """
        if type is None:
            if isinstance(metadata, BaseMetadata):
                type = metadata.expected_type()
        if type is None:
            type = self._object_type_map.pick_type(value)
        wd = WidgetDataModel(
            value=value,
            type=type,
            title=title,
            force_open_with=force_open_with,
            metadata=metadata,
            workflow=ProgrammaticMethod(output_model_type=type).construct_workflow(),
        )
        return self.add_data_model(wd)

    def add_data_model(self, model_data: WidgetDataModel) -> SubWindow[_W]:
        """Add a widget data model as a widget."""
        _, tabarea = self._current_or_new_tab()
        return tabarea.add_data_model(model_data)

    def add_function(
        self,
        func: Callable[..., _T],
        *,
        preview: bool = False,
        title: str | None = None,
        show_parameter_labels: bool = True,
        auto_close: bool = True,
        run_async: bool = False,
        result_as: Literal["window", "below", "right"] = "window",
    ) -> ParametricWindow[_W]:
        """Add a function as a parametric sub-window.

        The input function must return a `WidgetDataModel` instance, which can be
        interpreted by the application.

        Parameters
        ----------
        func : function (...) -> WidgetDataModel
            Function that generates a model from the input parameters.
        title : str, optional
            Title of the sub-window.

        Returns
        -------
        SubWindow
            The sub-window instance that represents the output model.
        """
        _, tabarea = self._current_or_new_tab()
        return tabarea.add_function(
            func, title=title, preview=preview, result_as=result_as,
            show_parameter_labels=show_parameter_labels, auto_close=auto_close,
            run_async=run_async,
        )  # fmt: skip

    def add_parametric_widget(
        self,
        widget: _W,
        callback: Callable | None = None,
        *,
        title: str | None = None,
        preview: bool = False,
        auto_close: bool = True,
        auto_size: bool = True,
        run_async: bool = False,
        result_as: Literal["window", "below", "right"] = "window",
    ) -> ParametricWindow[_W]:
        _, area = self._current_or_new_tab()
        return area.add_parametric_widget(
            widget, callback, title=title, preview=preview, auto_close=auto_close,
            auto_size=auto_size, result_as=result_as, run_async=run_async,
        )  # fmt: skip

    def read_file(
        self,
        file_path: PathOrPaths,
        plugin: str | None = None,
    ) -> SubWindow[_W]:
        """Read local file(s) and open as a new sub-window."""
        _, tabarea = self._current_or_new_tab()
        return tabarea.read_file(file_path, plugin=plugin)

    def read_files(self, file_paths: PathOrPaths):
        """Read multiple files one by one and open as new sub-windows in a same tab."""
        _, tabarea = self._current_or_new_tab()
        return tabarea.read_files(file_paths)

    def read_files_async(self, file_paths: PathOrPaths, plugin: str | None = None):
        """Read multiple files asynchronously and open as new sub-windows."""
        _, tabarea = self._current_or_new_tab()
        return tabarea.read_files_async(file_paths, plugin=plugin)

    def load_session(self, path: str | Path) -> None:
        """Read a session file and update the main window based on the content."""
        from himena.session import update_from_zip, update_from_directory

        fp = Path(path)
        if fp.suffix == ".zip":
            update_from_zip(self, fp)
        elif fp.is_dir():
            update_from_directory(self, fp)
        else:
            raise ValueError(f"Session must be a zip file or a directory, got {fp}.")
        # always plugin=None for reading a session file as a session
        self._recent_session_manager.append_recent_files([(fp, None)])
        self.set_status_tip(f"Session loaded: {fp}", duration=5)

    def save_session(
        self,
        path: str | Path,
        *,
        save_copies: bool = False,
        allow_calculate: Sequence[str] = (),
    ) -> None:
        """Save the current session to a zip file as a stand-along file."""
        from himena.session import dump_zip, dump_directory

        path = Path(path)
        if path.suffix == ".zip":
            dump_zip(
                self, path, save_copies=save_copies, allow_calculate=allow_calculate
            )
        else:
            dump_directory(
                self, path, save_copies=save_copies, allow_calculate=allow_calculate
            )
        self.set_status_tip(f"Session saved to {path}")
        self._recent_session_manager.append_recent_files([(path, None)])

    def clear(self) -> None:
        """Clear all widgets in the main window."""
        self.tabs.clear()
        self.dock_widgets.clear()

    def set_status_tip(self, text: str, duration: float = 10.0) -> None:
        """Set the status tip of the main window.

        This method can be safely called from any thread.

        Parameters
        ----------
        text : str
            Text to show in the status bar.
        duration : float, default 10.0
            Duration (seconds) to show the status tip.
        """
        self._backend_main_window._set_status_tip(text, duration)
        return None

    def show_notification(self, text: str, duration: float = 5.0) -> None:
        """Show a temporary notification in the main window."""
        self._backend_main_window._show_notification(text, duration)

    @overload
    def register_function(
        self,
        func: None = None,
        *,
        menus: str | Sequence[str] = "plugins",
        title: str | None = None,
        types: str | Sequence[str] | None = None,
        enablement: BoolOp | None = None,
        keybindings: Sequence[KeyBindingRule] | None = None,
        command_id: str | None = None,
    ) -> None: ...  # noqa: E501
    @overload
    def register_function(
        self,
        func: _F,
        *,
        menus: str | Sequence[str] = "plugins",
        title: str | None = None,
        types: str | Sequence[str] | None = None,
        enablement: BoolOp | None = None,
        keybindings: Sequence[KeyBindingRule] | None = None,
        command_id: str | None = None,
    ) -> _F: ...  # noqa: E501

    def register_function(
        self,
        func=None,
        *,
        menus="plugins",
        title=None,
        types=None,
        enablement=None,
        keybindings=None,
        command_id=None,
    ):
        """Register a function as a callback in runtime.

        Example
        -------
        ``` python
        @ui.register_function(menus="plugins", title="Test functions)
        def test_function():
            print("test")
        ```

        Parameters
        ----------
        func : callable, optional
            Function to register as an action.
        menus : str or sequence of str, default "plugins"
            Menu(s) to add the action. Submenus are separated by `/`.
        title : str, optional
            Title of the action. Name of the function will be used if not given.
        types: str or sequence of str, optional
            The `type` parameter(s) allowed as the WidgetDataModel. If this parameter
            is given, action will be grayed out if the active window does not satisfy
            the listed types.
        enablement: Expr, optional
            Expression that describes when the action will be enabled. As this argument
            is a generalized version of `types` argument, you cannot use both of them.
        command_id : str, optional
            Command ID. If not given, the function qualname will be used.
        """

        def _inner(f):
            action = _actions.make_action_for_function(
                f,
                menus=menus,
                title=title,
                types=types,
                enablement=enablement,
                keybindings=keybindings,
                command_id=command_id,
            )
            _actions.AppActionRegistry.instance().add_action(action)
            added_menus = _actions.AppActionRegistry.instance().install_to(
                self.model_app, [action]
            )
            self._backend_main_window._rebuild_for_runtime(added_menus)
            return f

        return _inner(func) if func else _inner

    def exec_action(
        self,
        id: str,
        *,
        model_context: WidgetDataModel | None = None,
        window_context: SubWindow | None = None,
        with_params: dict[str, Any] | None = None,
        process_model_output: bool = True,
    ) -> Any:
        """Execute an action by its ID.

        Parameters
        ----------
        id : str
            Action ID.
        model_context : WidgetDataModel, optional
            If given, this model will override the application context for the type
            `WidgetDataModel` before the execution.
        window_context : SubWindow, optional
            If given, this window will override the application context for the type
            `SubWindow` before the execution.
        with_params : dict, optional
            Parameters to pass to the parametric action. These parameters will directly
            be passed to the parametric window created after the action is executed.
        process_model_output : bool, default True
            If True, the output result will be processed by the application context. If
            the command return a `WidgetDataModel` instance, it will be converted to a
            sub-window.
        """
        providers: list[tuple[Any, type]] = []
        if model_context is not None:
            providers.append((model_context, WidgetDataModel, 1000))
        if window_context is not None:
            if isinstance(window_context, SubWindow):
                _window_context = window_context
            else:
                raise TypeError(
                    f"`window_context` must be SubWindow or UUID, got {window_context}"
                )
            providers.append((_window_context, SubWindow, 1000))
            if model_context is None and _window_context.supports_to_model:
                providers.append((_window_context.to_model(), WidgetDataModel, 100))
        # execute the command under the given context
        with (
            self.model_app.injection_store.register(providers=providers),
            self._execute_in_context(
                is_gui=with_params is None, process_model_output=process_model_output
            ),
        ):
            result = self.model_app.commands.execute_command(id).result()
            if with_params is not None:
                if (tab := self.tabs.current()) is not None and len(tab) > 0:
                    param_widget = tab[-1]
                else:  # pragma: no cover
                    raise RuntimeError("Unreachable code.")
                if not isinstance(param_widget, ParametricWindow):
                    if len(with_params) == 0:
                        if isinstance(result, Future):
                            return result.result()  # or appropriate handling
                        return result
                    raise ValueError(
                        f"Parametric widget expected but got {param_widget}."
                    )
                # run the callback with the given parameters synchronously
                result = param_widget._callback_with_params(
                    with_params,
                    force_sync=True,
                    force_close=True,
                )
        return result

    @overload
    def exec_choose_one_dialog(
        self,
        title: str,
        message: str,
        choices: list[tuple[str, _T]],
        how: Literal["buttons", "radiobuttons"] = "buttons",
    ) -> _T | None: ...
    @overload
    def exec_choose_one_dialog(
        self,
        title: str,
        message: str,
        choices: list[str],
        how: Literal["buttons", "radiobuttons"] = "buttons",
    ) -> str | None: ...

    def exec_choose_one_dialog(self, title, message, choices, how="buttons"):
        """Execute a dialog to choose one from the given choices.

        Parameters
        ----------
        title : str
            Window title of the dialog.
        message : str
            HTML Message to show in the dialog.
        choices : list
            List of choices. Each choice can be a string or a tuple of (text, value).
            This method will return the selected value.
        how : str, default "buttons"
            How to show the choices. "buttons" for horizontal buttons, "radiobuttons"
            for vertically arranged radio buttons.
        """
        if res := self._instructions.choose_one_dialog_response:
            return res()
        _choices_normed = []
        for choice in choices:
            if isinstance(choice, str):
                _choices_normed.append((choice, choice))
            else:
                text, value = choice
                _choices_normed.append((text, value))
        return self._backend_main_window._request_choice_dialog(
            title, message, _choices_normed, how=how
        )

    @overload
    def exec_file_dialog(
        self,
        mode: Literal["r", "d", "w"] = "r",
        *,
        extension_default: str | None = None,
        allowed_extensions: list[str] | None = None,
        caption: str | None = None,
        start_path: str | Path | None = None,
        group: str | None = None,
    ) -> Path | None: ...
    @overload
    def exec_file_dialog(
        self,
        mode: Literal["rm"],
        *,
        extension_default: str | None = None,
        allowed_extensions: list[str] | None = None,
        caption: str | None = None,
        start_path: str | Path | None = None,
        group: str | None = None,
    ) -> list[Path] | None: ...

    def exec_file_dialog(
        self,
        mode: Literal["r", "d", "w", "rm"] = "r",
        *,
        extension_default=None,
        allowed_extensions=None,
        caption=None,
        start_path=None,
        group: str | None = None,
    ):
        """Execute a file dialog to get file path(s)."""
        if mode not in {"r", "d", "w", "rm"}:
            raise ValueError(f"`mode` must be 'r', 'd', 'w' or 'rm', got {mode!r}.")
        if res := self._instructions.file_dialog_response:
            return res()
        if group is None:
            group = mode

        if mode == "w":
            if start_path is None:
                _start_path = self._file_dialog_hist.get_path(group)
            elif Path(start_path).parent != Path("."):
                _start_path = Path(start_path)
            else:  # filename only is given
                _start_path = self._file_dialog_hist.get_path(group, str(start_path))
        else:
            _start_path = Path(start_path or self._file_dialog_hist.get_path(group))
        result = self._backend_main_window._open_file_dialog(
            mode,
            extension_default=extension_default,
            allowed_extensions=allowed_extensions,
            caption=caption,
            start_path=_start_path,
        )
        if result is None:
            return None
        if mode in ["r", "w", "d"]:
            self._file_dialog_hist.update(group, result.parent)
        elif result:
            self._file_dialog_hist.update(group, result[0].parent)
        return result

    def show(self, run: bool = False) -> None:
        """
        Show the main window.

        Parameters
        ----------
        run : bool, default False
            If True, run the application event loop.
        """
        self._backend_main_window.show(run)
        return None

    def close(self) -> None:
        """Close the main window."""
        self._backend_main_window._exit_main_window(confirm=False)
        remove_instance(self.model_app.name, self)
        return None

    @property
    def current_window(self) -> SubWindow[_W] | None:
        """Get the current sub-window."""
        idx_tab = self._backend_main_window._current_tab_index()
        if idx_tab is None:
            return None
        idx_win = self._backend_main_window._current_sub_window_index(idx_tab)
        if idx_win is None:
            return None
        return self.tabs[idx_tab][idx_win]

    @current_window.setter
    def current_window(self, win: SubWindow[_W] | None) -> None:
        """Set the current sub-window."""
        _main = self._backend_main_window
        if win is None:
            _main._set_current_tab_index(None)
            i_tab = _main._current_tab_index()
            if i_tab is not None:
                _main._set_current_sub_window_index(i_tab, None)
        else:
            for i_tab, tab in self.tabs.enumerate():
                for i_win, sub in tab.enumerate():
                    if sub is win:
                        _main._set_current_tab_index(i_tab)
                        _main._set_current_sub_window_index(i_tab, i_win)
                        return None
        return None

    @property
    def current_model(self) -> WidgetDataModel | None:
        """Get the current model of the active sub-window."""
        if sub := self.current_window:
            return sub.to_model()
        return None

    def iter_windows(self) -> Iterator[SubWindow[_W]]:
        """Iterate over all the sub-windows in this main window."""
        for tab in self.tabs:
            yield from tab

    def _provide_file_output(self) -> tuple[WidgetDataModel, SubWindow[_W]]:
        if sub := self.current_window:
            model = sub.to_model()
            return model, sub
        else:
            raise ValueError("No active window.")

    def _tab_activated(self, i: int):
        if i < 0:
            return None
        tab = self.tabs.get(i)
        if tab is not None:
            self.events.tab_activated.emit(tab)
            self._main_window_resized(self.area_size)  # update layout and anchor
        if self._history_tab.get_from_last(1) != i:
            self._history_tab.add(i)
        return None

    def move_window(self, sub: SubWindow[_W], target_index: int) -> None:
        """Move the sub-window to the target tab index."""
        i_tab = i_win = None
        for _i_tab, tab in self.tabs.enumerate():
            for _i_win, win in tab.enumerate():
                if win is sub:
                    i_tab = _i_tab
                    i_win = _i_win
                    break

        if i_tab is None or i_win is None or target_index == i_tab:
            return None
        title = self.tabs[i_tab][i_win].title
        old_rect = self.tabs[i_tab][i_win].rect
        win, widget = self.tabs[i_tab]._pop_no_emit(i_win)
        if target_index < 0:
            self.add_tab()
        self.tabs[target_index].append(win, title)
        win.rect = old_rect
        if layout := win._parent_layout_ref():
            layout.remove(win)
        self.tabs.current_index = i_tab
        return None

    def _window_activated(self):
        back = self._backend_main_window
        back._update_context()
        i_tab = back._current_tab_index()
        if i_tab is None:
            return back._update_control_widget(None)
        tab = self.tabs.get(i_tab)
        if tab is None or len(tab) == 0:
            return back._update_control_widget(None)
        i_win = back._current_sub_window_index(i_tab)
        if i_win is None or len(tab) <= i_win:
            return back._update_control_widget(None)
        win = tab[i_win]
        back._update_control_widget(win.widget)
        _checker.call_widget_activated_callback(win.widget)
        self.events.window_activated.emit(win)

    def _main_window_resized(self, size: Size):
        if tab := self.tabs.current():
            for layout in tab.layouts:
                layout.anchor = layout.anchor.update_for_window_rect(size, layout.rect)
                layout._reanchor(size)
            for win in tab:
                win.anchor = win.anchor.update_for_window_rect(size, win.rect)
                win._reanchor(size)

    @contextmanager
    def _execute_in_context(
        self,
        is_gui: bool = False,
        process_model_output: bool = True,
        unwrap_future: bool = True,
    ):
        with self._global_lock:
            old_inst = self._instructions.model_copy()
            self._instructions = self._instructions.updated(
                gui_execution=is_gui,
                process_model_output=process_model_output,
                unwrap_future=unwrap_future,
            )
            try:
                yield None
            finally:
                self._instructions = old_inst

    def _iter_widget_class(self, model: WidgetDataModel) -> Iterator[type[_W]]:
        """Pick the most suitable widget class for the given model."""
        if model.force_open_with:
            yield import_object(model.force_open_with)
            return
        widget_classes, fallback_class = self._backend_main_window._list_widget_class(
            model.type
        )
        if not widget_classes:
            warnings.warn(
                f"No widget class is registered for model type {model.type!r}.",
                RuntimeWarning,
                stacklevel=2,
            )
            yield fallback_class
            return
        complete_match = [
            (tup.priority, tup.widget_class)
            for tup in widget_classes
            if tup.type == model.type and tup.priority >= 0
        ]
        if complete_match:
            yield from _iter_sorted(complete_match)
        subtype_match = [
            ((tup.type.count("."), tup.priority), tup.widget_class)
            for tup in widget_classes
            if tup.priority >= 0
        ]
        yield from _iter_sorted(subtype_match)

    def _pick_widget(self, model: WidgetDataModel) -> _W:
        """Pick the most suitable widget for the given model."""
        exceptions: list[tuple[Any, Exception]] = []
        for factory in self._iter_widget_class(model):
            try:
                try:
                    widget = factory(self)
                except TypeError:
                    widget = factory()
                widget_id = get_widget_class_id(type(widget))
                reg = _actions.AppActionRegistry.instance()
                if self.model_app.name != "." and (
                    plugin_configs := self.app_profile.plugin_configs.get(widget_id)
                ):
                    params = {}
                    for k, v in plugin_configs.items():
                        params[k] = v["value"]
                    cfgs = reg._plugin_default_configs
                    cfg_type = cfgs[widget_id].config_class
                    # widget should always have `update_configs` in this case
                    widget.update_configs(cfg_type(**params))
                widget.update_model(model)
            except Exception as e:
                exceptions.append((factory, e))
            else:
                break
        else:
            raise ValueError(
                f"Failed to create a widget for {_short_repr(model)}. Errors:\n"
                f"{_format_exceptions(exceptions)}"
            ) from exceptions[-1][1]
        if exceptions:
            raise exceptions[-1][1]

        return widget

    def _on_command_execution(self, id: str, result: Future):
        if exc := result.exception():
            _LOGGER.exception("Command %r failed: %r", id, exc)
            return
        if action := self.model_app.registered_actions.get(id):
            if getattr(action.callback, NO_RECORDING_FIELD, False):
                return None
            self._history_command.add(id)


def _short_repr(obj: Any) -> str:
    obj_repr = repr(obj)
    if len(obj_repr) > 50:
        obj_repr = obj_repr[:50] + "..."
    return obj_repr


def _format_exceptions(exceptions: list[tuple[Any, Exception]]) -> str:
    strs: list[str] = []
    for factory, e in exceptions:
        fname = getattr(factory, "__himena_widget_id__", repr(factory))
        strs.append(f"- {type(e).__name__} in {fname}\n  {e}")
    return "\n".join(strs)


def _iter_sorted(matched: list[tuple[tuple[int, int], type]]) -> Iterator[type]:
    _s = sorted(matched, key=lambda x: x[0], reverse=True)
    yield from (cls for _, cls in _s)
