# SPDX-FileCopyrightText: Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# SPDX-License-Identifier: GPL-3.0-or-later

"""Functions that return miscellaneous completion models."""

import datetime
import itertools
from collections.abc import Sequence

from qutebrowser.config import config, configdata
from qutebrowser.utils import objreg, log, utils
from qutebrowser.completion.models import completionmodel, listcategory, util
from qutebrowser.browser import inspector


def command(*, info):
    """A CompletionModel filled with non-hidden commands and descriptions."""
    model = completionmodel.CompletionModel(column_widths=(20, 60, 20))
    cmdlist = util.get_cmd_completions(info, include_aliases=True,
                                       include_hidden=False)
    model.add_category(listcategory.ListCategory("Commands", cmdlist))
    return model


def helptopic(*, info):
    """A CompletionModel filled with help topics."""
    model = completionmodel.CompletionModel(column_widths=(20, 70, 10))

    cmdlist = util.get_cmd_completions(info, include_aliases=False,
                                       include_hidden=True, prefix=':')
    settings = ((opt.name, opt.description, info.config.get_str(opt.name))
                for opt in configdata.DATA.values())

    model.add_category(listcategory.ListCategory("Commands", cmdlist))
    model.add_category(listcategory.ListCategory("Settings", settings))
    return model


def quickmark(*, info=None):
    """A CompletionModel filled with all quickmarks."""
    def delete(data: Sequence[str]) -> None:
        """Delete a quickmark from the completion menu."""
        name = data[0]
        quickmark_manager = objreg.get('quickmark-manager')
        log.completion.debug('Deleting quickmark {}'.format(name))
        quickmark_manager.delete(name)

    utils.unused(info)
    model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
    marks = objreg.get('quickmark-manager').marks.items()
    model.add_category(listcategory.ListCategory('Quickmarks', marks,
                                                 delete_func=delete,
                                                 sort=False))
    return model


def bookmark(*, info=None):
    """A CompletionModel filled with all bookmarks."""
    def delete(data: Sequence[str]) -> None:
        """Delete a bookmark from the completion menu."""
        urlstr = data[0]
        log.completion.debug('Deleting bookmark {}'.format(urlstr))
        bookmark_manager = objreg.get('bookmark-manager')
        bookmark_manager.delete(urlstr)

    utils.unused(info)
    model = completionmodel.CompletionModel(column_widths=(30, 70, 0))
    marks = objreg.get('bookmark-manager').marks.items()
    model.add_category(listcategory.ListCategory('Bookmarks', marks,
                                                 delete_func=delete,
                                                 sort=False))
    return model


def session(*, info=None):
    """A CompletionModel filled with session names."""
    from qutebrowser.misc import sessions
    utils.unused(info)
    model = completionmodel.CompletionModel()
    try:
        sess = ((name,) for name
                in sessions.session_manager.list_sessions()
                if not name.startswith('_'))
        model.add_category(listcategory.ListCategory("Sessions", sess))
    except OSError:
        log.completion.exception("Failed to list sessions!")
    return model


def _tabs(*, win_id_filter=lambda _win_id: True, add_win_id=True, cur_win_id=None):
    """Helper to get the completion model for tabs/other_tabs.

    Args:
        win_id_filter: A filter function for window IDs to include.
                       Should return True for all included windows.
        add_win_id: Whether to add the window ID to the completion items.
        cur_win_id: Window ID to be passed from info.win_id
    """
    def delete_tab(data):
        """Close the selected tab."""
        if cur_win_id is None:
            win_id, tab_index = data[0].split('/')
        else:
            win_id = cur_win_id
            tab_index = data[0]

        tabbed_browser = objreg.get('tabbed-browser', scope='window',
                                    window=int(win_id))
        tabbed_browser.on_tab_close_requested(int(tab_index) - 1)

    model = completionmodel.CompletionModel(column_widths=(6, 40, 46, 8))

    tabs_are_windows = config.val.tabs.tabs_are_windows
    # list storing all single-tabbed windows when tabs_are_windows
    windows: list[tuple[str, str, str, str]] = []

    for win_id in objreg.window_registry:
        if not win_id_filter(win_id):
            continue

        tabbed_browser = objreg.get('tabbed-browser', scope='window',
                                    window=win_id)
        if tabbed_browser.is_shutting_down:
            continue
        tab_entries: list[tuple[str, str, str, str]] = []
        for idx in range(tabbed_browser.widget.count()):
            tab = tabbed_browser.widget.widget(idx)
            tab_str = ("{}/{}".format(win_id, idx + 1) if add_win_id
                       else str(idx + 1))

            pid = tab.renderer_process_pid()

            tab_entries.append((
                tab_str,
                tab.url().toDisplayString(),
                tabbed_browser.widget.page_title(idx),
                "" if pid is None else f"PID {pid}",
            ))

        if tabs_are_windows:
            windows += tab_entries
        else:
            title = str(win_id) if add_win_id else "Tabs"
            cat = listcategory.ListCategory(
                title, tab_entries, delete_func=delete_tab, sort=False)
            model.add_category(cat)

    if tabs_are_windows:
        win = listcategory.ListCategory(
            "Windows", windows, delete_func=delete_tab, sort=False)
        model.add_category(win)

    return model


def tabs(*, info=None):
    """A model to complete on open tabs across all windows.

    Used for the tab-select command (and others).
    """
    utils.unused(info)
    return _tabs()


def other_tabs(*, info):
    """A model to complete on open tabs across all windows except the current.

    Used for the tab-take command.
    """
    return _tabs(
        win_id_filter=lambda win_id: win_id != info.win_id,
        cur_win_id=info.win_id)


def tab_focus(*, info):
    """A model to complete on open tabs in the current window."""
    model = _tabs(win_id_filter=lambda win_id: win_id == info.win_id,
                  add_win_id=False, cur_win_id=info.win_id)

    special = [
        ("last", "Focus the last-focused tab"),
        ("stack-next", "Go forward through a stack of focused tabs"),
        ("stack-prev", "Go backward through a stack of focused tabs"),
    ]
    model.add_category(listcategory.ListCategory("Special", special))

    return model


def window(*, info):
    """A model to complete on all open windows."""
    model = completionmodel.CompletionModel(column_widths=(6, 30, 64))

    windows = []

    for win_id in objreg.window_registry:
        if win_id == info.win_id:
            continue
        tabbed_browser = objreg.get('tabbed-browser', scope='window',
                                    window=win_id)
        tab_titles = (tab.title() for tab in tabbed_browser.widgets())
        windows.append(("{}".format(win_id),
                        objreg.window_registry[win_id].windowTitle(),
                        ", ".join(tab_titles)))

    model.add_category(listcategory.ListCategory("Windows", windows))

    return model


def inspector_position(*, info):
    """A model for possible inspector positions."""
    utils.unused(info)
    model = completionmodel.CompletionModel(column_widths=(100, 0, 0))
    positions = [(e.name,) for e in inspector.Position]
    category = listcategory.ListCategory("Position (optional)", positions)
    model.add_category(category)
    return model


def _qdatetime_to_completion_format(qdate):
    if not qdate.isValid():
        ts = 0
    else:
        ts = max(qdate.toMSecsSinceEpoch(), 0)
    pydate = datetime.datetime.fromtimestamp(ts / 1000)
    return pydate.strftime(config.val.completion.timestamp_format)


def _back_forward(info, go_forward):
    history = info.cur_tab.history
    current_idx = history.current_idx()
    model = completionmodel.CompletionModel(column_widths=(5, 36, 50, 9))

    if go_forward:
        start = current_idx + 1
        items = history.forward_items()
    else:
        start = 0
        items = history.back_items()

    entries = [
        (
            str(idx),
            entry.url().toDisplayString(),
            entry.title(),
            _qdatetime_to_completion_format(entry.lastVisited())
        )
        for idx, entry in enumerate(items, start)
    ]
    if not go_forward:
        # make sure the most recent is at the top for :back
        entries.reverse()

    cat = listcategory.ListCategory("History", entries, sort=False)
    model.add_category(cat)
    return model


def forward(*, info):
    """A model to complete on history of the current tab.

    Used for the :forward command.
    """
    return _back_forward(info, go_forward=True)


def back(*, info):
    """A model to complete on history of the current tab.

    Used for the :back command.
    """
    return _back_forward(info, go_forward=False)


def undo(*, info):
    """A model to complete undo entries."""
    tabbed_browser = objreg.get('tabbed-browser', scope='window',
                                window=info.win_id)
    model = completionmodel.CompletionModel(column_widths=(6, 84, 10))
    timestamp_format = config.val.completion.timestamp_format

    entries = [
        (
            str(idx),
            ', '.join(entry.url.toDisplayString() for entry in group),
            group[-1].created_at.strftime(timestamp_format)
        )
        for idx, group in
        enumerate(reversed(tabbed_browser.undo_stack), start=1)
    ]

    cat = listcategory.ListCategory("Closed tabs", entries, sort=False)
    model.add_category(cat)
    return model


def process(*, info):
    """A CompletionModel filled with processes."""
    utils.unused(info)
    from qutebrowser.misc import guiprocess
    model = completionmodel.CompletionModel(column_widths=(10, 10, 80))
    for what, processes in itertools.groupby(
            (p for p in guiprocess.all_processes.values() if p is not None),
            lambda proc: proc.what):

        # put successful processes last
        sorted_processes = sorted(
            processes,
            key=lambda proc: proc.outcome.state_str() == 'successful',
        )

        entries = [(str(proc.pid), proc.outcome.state_str(), str(proc))
                   for proc in sorted_processes]
        cat = listcategory.ListCategory(what.capitalize(), entries, sort=False)
        model.add_category(cat)
    return model
