# (C) Copyright 2005-2023 Enthought, Inc., Austin, TX
# All rights reserved.
#
# This software is provided without warranty under the terms of the BSD
# license included in LICENSE.txt and may be redistributed only under
# the conditions described in the aforementioned license. The license
# is also available online at http://www.enthought.com/licenses/BSD.txt
#
# Thanks for using Enthought open source!

import logging


from traits.api import (
    Bool,
    Callable,
    HasStrictTraits,
    Instance,
    List,
    Property,
    Str,
    Vetoable,
    observe,
)


from pyface.action.i_menu_bar_manager import IMenuBarManager
from pyface.action.i_status_bar_manager import IStatusBarManager
from pyface.action.i_tool_bar_manager import IToolBarManager
from pyface.application_window import ApplicationWindow
from pyface.tasks.action.task_action_manager_builder import (
    TaskActionManagerBuilder,
)
from pyface.tasks.i_dock_pane import IDockPane
from pyface.tasks.i_task_pane import ITaskPane
from pyface.tasks.i_task_window_backend import ITaskWindowBackend
from pyface.tasks.task import Task, TaskLayout
from pyface.tasks.task_window_layout import TaskWindowLayout

# Logging.
logger = logging.getLogger(__name__)


class TaskWindow(ApplicationWindow):
    """ The the top-level window to which tasks can be assigned.

    A TaskWindow is responsible for creating and the managing the controls of
    its tasks.
    """

    # IWindow interface ----------------------------------------------------

    #: Unless a title is specifically assigned, delegate to the active task.
    title = Property(Str, observe=["active_task.name", "_title"])

    # TaskWindow interface ------------------------------------------------

    #: The pane (central or dock) in the active task that currently has focus.
    active_pane = Instance(ITaskPane)

    #: The active task for this window.
    active_task = Instance(Task)

    #: The list of all tasks currently attached to this window. All panes of
    #: the inactive tasks are hidden.
    tasks = List(Task)

    #: The central pane of the active task, which is always visible.
    central_pane = Instance(ITaskPane)

    #: The list of all dock panes in the active task, which may or may not be
    #: visible.
    dock_panes = List(IDockPane)

    #: The factory for the window's TaskActionManagerBuilder, which is
    #: instantiated to translate menu and tool bar schemas into Pyface action
    #: managers. This attribute can overridden to introduce custom logic into
    #: the translation process, although this is not usually necessary.
    action_manager_builder_factory = Callable(TaskActionManagerBuilder)

    # Protected traits -----------------------------------------------------

    _active_state = Instance("pyface.tasks.task_window.TaskState")
    _states = List(Instance("pyface.tasks.task_window.TaskState"))
    _title = Str()
    _window_backend = Instance(ITaskWindowBackend)

    # ------------------------------------------------------------------------
    # 'Widget' interface.
    # ------------------------------------------------------------------------

    def destroy(self):
        """ Overridden to ensure that all task panes are cleanly destroyed.
        """
        if self.control is not None:
            # Allow the TaskWindowBackend to clean up first.
            self._window_backend.destroy()

            # Don't use 'remove_task' here to avoid changing the active state and
            # thereby removing the window's menus and toolbars. This can lead to
            # undesirable animations when the window is being closed.
            for state in self._states:
                self._destroy_state(state)
        super().destroy()

    # ------------------------------------------------------------------------
    # 'Window' interface.
    # ------------------------------------------------------------------------

    def open(self):
        """ Opens the window.

        Overridden to make the 'opening' event vetoable and to activate a task
        if one has not already been activated. Returns whether the window was
        opened.
        """
        self.opening = event = Vetoable()
        if not event.veto:
            # Create the control, if necessary.
            if self.control is None:
                self.create()

            # Activate a task, if necessary.
            if self._active_state is None and self._states:
                self.activate_task(self._states[0].task)

            self.show(True)
            self.opened = self

        return self.control is not None and not event.veto

    # ------------------------------------------------------------------------
    # Protected 'IApplicationWindow' interface.
    # ------------------------------------------------------------------------

    def _create_contents(self, parent):
        """ Delegate to the TaskWindowBackend.
        """
        return self._window_backend.create_contents(parent)

    # ------------------------------------------------------------------------
    # 'TaskWindow' interface.
    # ------------------------------------------------------------------------

    def activate_task(self, task):
        """ Activates a task that has already been added to the window.
        """
        state = self._get_state(task)
        if state and state != self._active_state:
            # Hide the panes of the currently active task, if necessary.
            if self._active_state is not None:
                self._window_backend.hide_task(self._active_state)

            # Initialize the new task, if necessary.
            if not state.initialized:
                task.initialized()
                state.initialized = True

            # Display the panes of the new task.
            self._window_backend.show_task(state)

            # Activate the new task. The menus, toolbars, and status bar will be
            # replaced at this time.
            self._active_state = state
            task.activated()
        elif not state:
            logger.warning(
                "Cannot activate task %r: task does not belong to the "
                "window." % task
            )

    def add_task(self, task):
        """ Adds a task to the window. The task is not activated.
        """
        if task.window is not None:
            logger.error(
                "Cannot add task %r: task has already been added "
                "to a window!" % task
            )
            return

        task.window = self
        state = TaskState(task=task, layout=task.default_layout)
        self._states.append(state)

        # Make sure the underlying control has been created, even if it is not
        # yet visible.
        if self.control is None:
            self.create()

        # Create the central pane.
        state.central_pane = task.create_central_pane()
        state.central_pane.task = task
        state.central_pane.create(self.control)

        # Create the dock panes.
        state.dock_panes = task.create_dock_panes()
        for dock_pane_factory in task.extra_dock_pane_factories:
            state.dock_panes.append(dock_pane_factory(task=task))
        for dock_pane in state.dock_panes:
            dock_pane.task = task
            dock_pane.create(self.control)

        # Build the menu and tool bars.
        builder = self.action_manager_builder_factory(task=task)
        state.menu_bar_manager = builder.create_menu_bar_manager()
        state.status_bar_manager = task.status_bar
        state.tool_bar_managers = builder.create_tool_bar_managers()

    def remove_task(self, task):
        """ Removes a task that has already been added to the window. All the
            task's panes are destroyed.
        """
        state = self._get_state(task)
        if state:
            # If the task is active, make sure it is de-activated before
            # deleting its controls.
            if self._active_state == state:
                self._window_backend.hide_task(state)
                self._active_state = None

            self._destroy_state(state)
            self._states.remove(state)
        else:
            logger.warning(
                "Cannot remove task %r: task does not belong to the "
                "window." % task
            )

    def focus_next_pane(self):
        """ Shifts focus to the "next" pane, taking into account the active pane
            and the pane geometry.
        """
        if self._active_state:
            panes = self._get_pane_ring()
            index = 0
            if self.active_pane:
                index = (panes.index(self.active_pane) + 1) % len(panes)
            panes[index].set_focus()

    def focus_previous_pane(self):
        """ Shifts focus to the "previous" pane, taking into account the active
            pane and the pane geometry.
        """
        if self._active_state:
            panes = self._get_pane_ring()
            index = -1
            if self.active_pane:
                index = panes.index(self.active_pane) - 1
            panes[index].set_focus()

    def get_central_pane(self, task):
        """ Returns the central pane for the specified task.
        """
        state = self._get_state(task)
        return state.central_pane if state else None

    def get_dock_pane(self, id, task=None):
        """ Returns the dock pane in the task with the specified ID, or
            None if no such dock pane exists. If a task is not specified, the
            active task is used.
        """
        if task is None:
            state = self._active_state
        else:
            state = self._get_state(task)
        return state.get_dock_pane(id) if state else None

    def get_dock_panes(self, task):
        """ Returns the dock panes for the specified task.
        """
        state = self._get_state(task)
        return state.dock_panes[:] if state else []

    def get_task(self, id):
        """ Returns the task with the specified ID, or None if no such task
            exists.
        """
        state = self._get_state(id)
        return state.task if state else None

    # Methods for saving and restoring the layout -------------------------#

    def get_layout(self):
        """ Returns a TaskLayout (for the active task) that reflects the state
            of the window.
        """
        if self._active_state:
            return self._window_backend.get_layout()
        return None

    def set_layout(self, layout):
        """ Applies a TaskLayout (which should be suitable for the active task)
            to the window.
        """
        if self._active_state:
            self._window_backend.set_layout(layout)

    def reset_layout(self):
        """ Restores the active task's default TaskLayout.
        """
        if self.active_task:
            self.set_layout(self.active_task.default_layout)

    def get_window_layout(self):
        """ Returns a TaskWindowLayout for the current state of the window.
        """
        result = TaskWindowLayout(
            position=self.position, size=self.size, size_state=self.size_state
        )
        for state in self._states:
            if state == self._active_state:
                result.active_task = state.task.id
                layout = self._window_backend.get_layout()
            else:
                layout = state.layout.clone_traits()
            layout.id = state.task.id
            result.items.append(layout)
        return result

    def set_window_layout(self, window_layout):
        """ Applies a TaskWindowLayout to the window.
        """
        # Set window size before laying it out.
        self.position = window_layout.position
        self.size = window_layout.size
        self.size_state = window_layout.size_state

        # Store layouts for the tasks, including the active task.
        for layout in window_layout.items:
            if isinstance(layout, str):
                continue
            state = self._get_state(layout.id)
            if state:
                state.layout = layout
            else:
                logger.warning(
                    "Cannot apply layout for task %r: task does not "
                    "belong to the window." % layout.id
                )

        # Attempt to activate the requested task.
        state = self._get_state(window_layout.get_active_task())
        if state:
            # If the requested active task is already active, calling
            # ``activate`` is a no-op, so we must force a re-layout.
            if state == self._active_state:
                self._window_backend.set_layout(state.layout)
            else:
                self.activate_task(state.task)

    # ------------------------------------------------------------------------
    # Protected 'TaskWindow' interface.
    # ------------------------------------------------------------------------

    def _destroy_state(self, state):
        """ Destroy all controls associated with a Task state.
        """
        # Notify the task that it is about to be destroyed.
        state.task.prepare_destroy()

        # Destroy action managers associated with the task, unless the task is
        # active, in which case this will be handled by our superclass.
        if state != self._active_state:
            if state.menu_bar_manager:
                state.menu_bar_manager.destroy()
            for tool_bar_manager in state.tool_bar_managers:
                tool_bar_manager.destroy()

        # Destroy all controls associated with the task.
        for dock_pane in state.dock_panes:
            dock_pane.destroy()
        state.central_pane.destroy()
        state.task.window = None

    def _get_pane_ring(self):
        """ Returns a list of visible panes ordered for focus switching.
        """
        # Proceed clockwise through the dock areas.
        # TODO: Also take into account ordering within dock areas.
        panes = []
        if self._active_state:
            layout = self.get_layout()
            panes.append(self.central_pane)
            for area in ("top", "right", "bottom", "left"):
                item = getattr(layout, area)
                if item:
                    panes.extend(
                        [
                            self.get_dock_pane(pane_item.id)
                            for pane_item in item.iterleaves()
                        ]
                    )
        return panes

    def _get_state(self, id_or_task):
        """ Returns the TaskState that contains the specified Task, or None if
            no such state exists.
        """
        for state in self._states:
            if state.task == id_or_task or state.task.id == id_or_task:
                return state
        return None

    # Trait initializers ---------------------------------------------------

    def __window_backend_default(self):
        from pyface.tasks.task_window_backend import TaskWindowBackend
        return TaskWindowBackend(window=self)

    # Trait property getters/setters ---------------------------------------

    def _get_title(self):
        if self._title or self.active_task is None:
            return self._title
        return self.active_task.name

    def _set_title(self, title):
        self._title = title

    # Trait change handlers ------------------------------------------------

    @observe("_active_state")
    def _update_traits_given_new_active_state(self, event):
        state = event.new
        if state is None:
            self.active_task = self.central_pane = None
            self.dock_panes = []
            self.menu_bar_manager = self.status_bar_manager = None
            self.tool_bar_managers = []
        else:
            self.active_task = state.task
            self.central_pane = state.central_pane
            self.dock_panes = state.dock_panes
            self.menu_bar_manager = state.menu_bar_manager
            self.status_bar_manager = state.status_bar_manager
            self.tool_bar_managers = state.tool_bar_managers

    @observe("central_pane:has_focus, dock_panes:items:has_focus")
    def _focus_updated(self, event):
        if event.new:
            self.active_pane = event.object

    @observe("_states.items")
    def _states_updated(self, event):
        self.tasks = [state.task for state in self._states]


class TaskState(HasStrictTraits):
    """ An object used internally by TaskWindow to maintain the state associated
        with an attached Task.
    """

    #: The Task that the state comes from.
    task = Instance(Task)

    #: The layout of panes in the TaskWindow.
    layout = Instance(TaskLayout)

    #: Whether the task state has been initialized.
    initialized = Bool(False)

    #: The central pane of the TaskWindow
    central_pane = Instance(ITaskPane)

    #: The list of all dock panes.
    dock_panes = List(Instance(IDockPane))

    #: The TaskWindow's menu bar manager instance.
    menu_bar_manager = Instance(IMenuBarManager)

    #: The TaskWindow's status bar instance.
    status_bar_manager = Instance(IStatusBarManager)

    #: The TaskWindow's tool bar instances.
    tool_bar_managers = List(Instance(IToolBarManager))

    def get_dock_pane(self, id):
        """ Returns the dock pane with the specified id, or None if no such dock
            pane exists.
        """
        for pane in self.dock_panes:
            if pane.id == id:
                return pane
        return None
