# COPYRIGHT (C) 2020-2024 Nicotine+ Contributors
# COPYRIGHT (C) 2018 Mutnick <mutnick@techie.com>
# COPYRIGHT (C) 2016-2017 Michael Labouebe <gfarmerfr@free.fr>
# COPYRIGHT (C) 2008-2011 quinox <quinox@users.sf.net>
# COPYRIGHT (C) 2009 hedonist <ak@sensi.org>
# COPYRIGHT (C) 2006-2009 daelstorm <daelstorm@gmail.com>
# COPYRIGHT (C) 2003-2004 Hyriand <hyriand@thegraveyard.org>
#
# GNU GENERAL PUBLIC LICENSE
#    Version 3, 29 June 2007
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

from gi.repository import GObject
from gi.repository import Gtk

from pynicotine.config import config
from pynicotine.core import core
from pynicotine.events import events
from pynicotine.gtkgui.application import GTK_API_VERSION
from pynicotine.gtkgui.dialogs.fileproperties import FileProperties
from pynicotine.gtkgui.widgets import clipboard
from pynicotine.gtkgui.widgets import ui
from pynicotine.gtkgui.widgets.accelerator import Accelerator
from pynicotine.gtkgui.widgets.popupmenu import PopupMenu
from pynicotine.gtkgui.widgets.popupmenu import FilePopupMenu
from pynicotine.gtkgui.widgets.popupmenu import UserPopupMenu
from pynicotine.gtkgui.widgets.theme import add_css_class
from pynicotine.gtkgui.widgets.theme import get_file_type_icon_name
from pynicotine.gtkgui.widgets.theme import remove_css_class
from pynicotine.gtkgui.widgets.treeview import TreeView
from pynicotine.gtkgui.widgets.treeview import create_grouping_menu
from pynicotine.slskmessages import FileListMessage
from pynicotine.slskmessages import TransferRejectReason
from pynicotine.transfers import Transfer
from pynicotine.transfers import TransferStatus
from pynicotine.utils import UINT64_LIMIT
from pynicotine.utils import human_length
from pynicotine.utils import human_size
from pynicotine.utils import human_speed
from pynicotine.utils import humanize


class Transfers:

    STATUSES = {
        TransferStatus.QUEUED: _("Queued"),
        f"{TransferStatus.QUEUED} (prioritized)": _("Queued (prioritized)"),
        f"{TransferStatus.QUEUED} (privileged)": _("Queued (privileged)"),
        TransferStatus.GETTING_STATUS: _("Getting status"),
        TransferStatus.TRANSFERRING: _("Transferring"),
        TransferStatus.CONNECTION_CLOSED: _("Connection closed"),
        TransferStatus.CONNECTION_TIMEOUT: _("Connection timeout"),
        TransferStatus.USER_LOGGED_OFF: _("User logged off"),
        TransferStatus.PAUSED: _("Paused"),
        TransferStatus.CANCELLED: _("Cancelled"),
        TransferStatus.FINISHED: _("Finished"),
        TransferStatus.FILTERED: _("Filtered"),
        TransferStatus.DOWNLOAD_FOLDER_ERROR: _("Download folder error"),
        TransferStatus.LOCAL_FILE_ERROR: _("Local file error"),
        TransferRejectReason.BANNED: _("Banned"),
        TransferRejectReason.FILE_NOT_SHARED: _("File not shared"),
        TransferRejectReason.PENDING_SHUTDOWN: _("Pending shutdown"),
        TransferRejectReason.FILE_READ_ERROR: _("File read error")
    }

    path_separator = path_label = retry_label = abort_label = None
    deprioritized_statuses = ()
    transfer_page = user_counter = file_counter = expand_button = expand_icon = grouping_button = None

    def __init__(self, window, transfer_type):

        (
            self.clear_all_button,
            self.clear_all_label,
            self.container,
            self.tree_container
        ) = ui.load(scope=self, path=f"{transfer_type}s.ui")

        self.window = window
        self.type = transfer_type

        if GTK_API_VERSION >= 4:
            inner_button = next(iter(self.clear_all_button))
            self.clear_all_button.set_has_frame(False)
            self.clear_all_label.set_mnemonic_widget(inner_button)

        self.transfer_list = {}
        self.users = {}
        self.paths = {}
        self.pending_folder_rows = set()
        self.pending_user_rows = set()
        self.grouping_mode = None
        self.row_id = 0
        self.file_properties = None
        self.pending_parent_rows_timer_id = None

        # Use dict instead of list for faster membership checks
        self.selected_users = {}
        self.selected_transfers = {}

        self.tree_view = TreeView(
            window, parent=self.tree_container, name=transfer_type,
            multi_select=True, persistent_sort=True, activate_row_callback=self.on_row_activated,
            delete_accelerator_callback=self.on_clear_transfers_accelerator,
            columns={
                # Visible columns
                "user": {
                    "column_type": "text",
                    "title": _("User"),
                    "width": 200
                },
                "path": {
                    "column_type": "text",
                    "title": self.path_label,
                    "width": 200,
                    "expand_column": True,
                    "tooltip_callback": self.on_file_path_tooltip
                },
                "file_type": {
                    "column_type": "icon",
                    "title": _("File Type"),
                    "width": 40,
                    "hide_header": True
                },
                "filename": {
                    "column_type": "text",
                    "title": _("Filename"),
                    "width": 200,
                    "expand_column": True,
                    "tooltip_callback": self.on_file_path_tooltip
                },
                "status": {
                    "column_type": "text",
                    "title": _("Status"),
                    "width": 140
                },
                "queue_position": {
                    "column_type": "number",
                    "title": _("Queue"),
                    "width": 90,
                    "sort_column": "queue_position_data"
                },
                "percent": {
                    "column_type": "progress",
                    "title": _("Percent"),
                    "width": 90
                },
                "size": {
                    "column_type": "number",
                    "title": _("Size"),
                    "width": 180,
                    "sort_column": "size_data"
                },
                "speed": {
                    "column_type": "number",
                    "title": _("Speed"),
                    "width": 100,
                    "sort_column": "speed_data"
                },
                "time_elapsed": {
                    "column_type": "number",
                    "title": _("Time Elapsed"),
                    "width": 140,
                    "sort_column": "time_elapsed_data"
                },
                "time_left": {
                    "column_type": "number",
                    "title": _("Time Left"),
                    "width": 140,
                    "sort_column": "time_left_data"
                },

                # Hidden data columns
                "size_data": {"data_type": GObject.TYPE_UINT64},
                "current_bytes_data": {"data_type": GObject.TYPE_UINT64},
                "speed_data": {"data_type": GObject.TYPE_UINT64},
                "queue_position_data": {"data_type": GObject.TYPE_UINT},
                "time_elapsed_data": {"data_type": int},
                "time_left_data": {"data_type": GObject.TYPE_UINT64},
                "transfer_data": {"data_type": GObject.TYPE_PYOBJECT},
                "id_data": {
                    "data_type": GObject.TYPE_UINT64,
                    "default_sort_type": "ascending",
                    "iterator_key": True
                }
            }
        )

        Accelerator("t", self.tree_view.widget, self.on_abort_transfers_accelerator)
        Accelerator("r", self.tree_view.widget, self.on_retry_transfers_accelerator)
        Accelerator("<Alt>Return", self.tree_view.widget, self.on_file_properties_accelerator)

        menu = create_grouping_menu(
            window, config.sections["transfers"][f"group{transfer_type}s"], self.on_toggle_tree)
        self.grouping_button.set_menu_model(menu)

        if GTK_API_VERSION >= 4:
            inner_button = next(iter(self.grouping_button))
            add_css_class(widget=inner_button, css_class="image-button")

        self.expand_button.connect("toggled", self.on_expand_tree)
        self.expand_button.set_active(config.sections["transfers"][f"{transfer_type}sexpanded"])

        self.popup_menu_users = UserPopupMenu(window.application, tab_name="transfers")
        self.popup_menu_clear = PopupMenu(window.application)
        self.clear_all_button.set_menu_model(self.popup_menu_clear.model)

        self.popup_menu_copy = PopupMenu(window.application)
        self.popup_menu_copy.add_items(
            ("#" + _("Copy _File Path"), self.on_copy_file_path),
            ("#" + _("Copy _URL"), self.on_copy_url),
            ("#" + _("Copy Folder U_RL"), self.on_copy_folder_url)
        )

        self.popup_menu = FilePopupMenu(
            window.application, parent=self.tree_view.widget, callback=self.on_popup_menu
        )
        self.popup_menu.add_items(
            ("#" + _("_Open File"), self.on_open_file),
            ("#" + _("Open in File _Manager"), self.on_open_file_manager),
            ("#" + _("F_ile Properties"), self.on_file_properties),
            ("", None),
            ("#" + self.retry_label, self.on_retry_transfer),
            ("#" + self.abort_label, self.on_abort_transfer),
            ("#" + _("_Clear"), self.on_clear_transfer),
            ("", None),
            ("#" + _("View User _Profile"), self.on_user_profile),
            ("#" + _("_Browse Folder"), self.on_browse_folder),
            ("#" + _("_Search"), self.on_file_search),
            ("", None),
            (">" + _("Copy"), self.popup_menu_copy),
            (">" + _("Clear All"), self.popup_menu_clear),
            (">" + _("User Actions"), self.popup_menu_users)
        )

    def destroy(self):

        self.tree_view.destroy()
        self.popup_menu.destroy()
        self.popup_menu_users.destroy()
        self.popup_menu_clear.destroy()
        self.popup_menu_copy.destroy()

        if self.pending_parent_rows_timer_id is not None:
            events.cancel_scheduled(self.pending_parent_rows_timer_id)

        self.__dict__.clear()

    def on_focus(self, *_args):

        self.update_model(select_parent=(self.pending_parent_rows_timer_id is not None))
        self.window.notebook.remove_tab_changed(self.transfer_page)

        if self.container.get_visible():
            self.tree_view.grab_focus()
            return True

        return False

    def init_transfers(self, transfer_list):

        self.transfer_list = transfer_list

        self.container.get_parent().set_visible(bool(transfer_list))
        self.update_model()

    def select_transfers(self):

        self.selected_transfers.clear()
        self.selected_users.clear()

        for iterator in self.tree_view.get_selected_rows():
            transfer = self.tree_view.get_row_value(iterator, "transfer_data")
            self.select_transfer(transfer, select_user=True)

    def select_child_transfers(self, transfer):

        if transfer.virtual_path is not None:
            return

        # Dummy Transfer object for user/folder rows
        user = transfer.username
        folder_path = self.get_transfer_folder_path(transfer)

        if folder_path is not None:
            user_folder_path = user + folder_path
            row_data = self.paths[user_folder_path]
        else:
            row_data = self.users[user]

        _row_iter, child_transfers = row_data

        for i_transfer in child_transfers:
            self.select_transfer(i_transfer)

    def select_transfer(self, transfer, select_user=False):

        if transfer.virtual_path is not None and transfer not in self.selected_transfers:
            self.selected_transfers[transfer] = None

        if select_user and transfer.username not in self.selected_users:
            self.selected_users[transfer.username] = None

        self.select_child_transfers(transfer)

    def on_file_search(self, *_args):

        transfer = next(iter(self.selected_transfers), None)

        if not transfer:
            return

        try:
            _folder_path, basename = transfer.virtual_path.rsplit("\\", 1)

        except ValueError:
            basename = transfer.virtual_path

        self.window.search_entry.set_text(basename)
        self.window.change_main_page(self.window.search_page)

    def translate_status(self, status):

        translated_status = self.STATUSES.get(status)

        if translated_status:
            return translated_status

        return status

    def update_num_users_files(self):
        self.user_counter.set_text(humanize(len(self.users)))
        self.file_counter.set_text(humanize(len(self.transfer_list)))

    def update_model(self, transfer=None, update_parent=True, select_parent=False):

        if self.window.current_page_id != self.transfer_page.id:
            if transfer is not None and transfer.iterator is None:
                self.window.notebook.request_tab_changed(self.transfer_page)

            # No need to do unnecessary work if transfers are not visible
            return

        if self.pending_parent_rows_timer_id is None:
            # Limit individual parent row updates to once per second
            self.pending_parent_rows_timer_id = events.schedule(
                delay=1, callback=self._update_pending_parent_rows, repeat=True)

        update_counters = False

        if transfer is not None:
            update_counters = self.update_specific(transfer, select_parent=select_parent)

        elif self.transfer_list:
            for transfer_i in self.transfer_list:
                row_added = self.update_specific(transfer_i, select_parent=select_parent)

                if row_added:
                    select_parent = False
                    update_counters = True

        if update_parent:
            self.update_parent_rows(transfer)

        if update_counters:
            self.update_num_users_files()

        self.tree_view.redraw()

    def _update_pending_parent_rows(self):

        for user_folder_path in self.pending_folder_rows:
            if user_folder_path not in self.paths:
                continue

            user_folder_path_iter, user_folder_path_child_transfers = self.paths[user_folder_path]
            self.update_parent_row(
                user_folder_path_iter, user_folder_path_child_transfers, user_folder_path=user_folder_path)

        for username in self.pending_user_rows:
            if username not in self.users:
                continue

            user_iter, user_child_transfers = self.users[username]
            self.update_parent_row(user_iter, user_child_transfers, username=username)

        self.pending_folder_rows.clear()
        self.pending_user_rows.clear()

    def update_parent_rows(self, transfer=None):

        if self.grouping_mode == "ungrouped":
            return

        if transfer is not None:
            username = transfer.username

            if self.paths:
                user_folder_path = username + self.get_transfer_folder_path(transfer)
                self.pending_folder_rows.add(user_folder_path)

            self.pending_user_rows.add(username)
            return

        if self.paths:
            for user_folder_path, (user_folder_path_iter, child_transfers) in self.paths.copy().items():
                self.update_parent_row(user_folder_path_iter, child_transfers, user_folder_path=user_folder_path)

        for username, (user_iter, child_transfers) in self.users.copy().items():
            self.update_parent_row(user_iter, child_transfers, username=username)

    @staticmethod
    def get_hqueue_position(queue_position):
        return str(queue_position) if queue_position > 0 else ""

    @staticmethod
    def get_hsize(current_byte_offset, size):
        return f"{human_size(current_byte_offset)} / {human_size(size)}"

    @staticmethod
    def get_hspeed(speed):
        return human_speed(speed) if speed > 0 else ""

    @staticmethod
    def get_helapsed(elapsed):
        return human_length(elapsed) if elapsed > 0 else ""

    @staticmethod
    def get_hleft(left):
        return human_length(left) if left >= 1 else ""

    @staticmethod
    def get_percent(current_byte_offset, size):

        if current_byte_offset > size or size <= 0:
            return 100

        return (100 * current_byte_offset) // size

    def update_parent_row(self, iterator, child_transfers, username=None, user_folder_path=None):

        speed = 0.0
        total_size = current_byte_offset = 0
        elapsed = 0
        parent_status = TransferStatus.FINISHED

        if not child_transfers:
            # Remove parent row if no children are present anymore
            if user_folder_path:
                transfer = self.tree_view.get_row_value(iterator, "transfer_data")
                _user_iter, user_child_transfers = self.users[transfer.username]
                user_child_transfers.remove(transfer)
                del self.paths[user_folder_path]
            else:
                del self.users[username]

            self.tree_view.remove_row(iterator)

            if not self.tree_view.iterators:
                # Show tab description
                self.container.get_parent().set_visible(False)

            self.update_num_users_files()
            return

        for transfer in child_transfers:
            status = transfer.status

            if status == TransferStatus.TRANSFERRING:
                # "Transferring" status always has the highest priority
                parent_status = status
                speed += transfer.speed or 0

            elif parent_status in self.deprioritized_statuses and status != TransferStatus.FINISHED:
                # "Finished" status always has the lowest priority
                parent_status = status

            if status == TransferStatus.FILTERED and transfer.virtual_path:
                # We don't want to count filtered files when calculating the progress
                continue

            elapsed += transfer.time_elapsed or 0
            total_size += transfer.size or 0
            current_byte_offset += transfer.current_byte_offset or 0

        transfer = self.tree_view.get_row_value(iterator, "transfer_data")
        total_size = min(total_size, UINT64_LIMIT)
        current_byte_offset = min(current_byte_offset, UINT64_LIMIT)
        should_update_size = False

        if transfer.status != parent_status:
            self.tree_view.set_row_value(iterator, "status", self.translate_status(parent_status))
            transfer.status = parent_status

        if transfer.speed != speed:
            self.tree_view.set_row_value(iterator, "speed", self.get_hspeed(speed))
            self.tree_view.set_row_value(iterator, "speed_data", speed)
            transfer.speed = speed

        if transfer.time_elapsed != elapsed:
            left = (total_size - current_byte_offset) / speed if speed and total_size > current_byte_offset else 0
            self.tree_view.set_row_value(iterator, "time_elapsed", self.get_helapsed(elapsed))
            self.tree_view.set_row_value(iterator, "time_left", self.get_hleft(left))
            self.tree_view.set_row_value(iterator, "time_elapsed_data", elapsed)
            self.tree_view.set_row_value(iterator, "time_left_data", left)
            transfer.time_elapsed = elapsed

        if transfer.current_byte_offset != current_byte_offset:
            self.tree_view.set_row_value(iterator, "current_bytes_data", current_byte_offset)
            transfer.current_byte_offset = current_byte_offset
            should_update_size = True

        if transfer.size != total_size:
            self.tree_view.set_row_value(iterator, "size_data", total_size)
            transfer.size = total_size
            should_update_size = True

        if should_update_size:
            self.tree_view.set_row_value(iterator, "percent", self.get_percent(current_byte_offset, total_size))
            self.tree_view.set_row_value(iterator, "size", self.get_hsize(current_byte_offset, total_size))

    def update_specific(self, transfer, select_parent=False):

        current_byte_offset = transfer.current_byte_offset or 0
        queue_position = transfer.queue_position or 0
        status = transfer.status or ""

        if transfer.modifier and status == TransferStatus.QUEUED:
            # Priority status
            status += f" ({transfer.modifier})"

        size = transfer.size or 0
        speed = transfer.speed or 0
        elapsed = transfer.time_elapsed or 0
        left = transfer.time_left or 0
        iterator = transfer.iterator

        # Modify old transfer
        if iterator is not None:
            translated_status = self.translate_status(status)
            should_update_size = False

            if self.tree_view.get_row_value(iterator, "status") != translated_status:
                self.tree_view.set_row_value(iterator, "status", translated_status)

            if self.tree_view.get_row_value(iterator, "speed_data") != speed:
                self.tree_view.set_row_value(iterator, "speed", self.get_hspeed(speed))
                self.tree_view.set_row_value(iterator, "speed_data", speed)

            if self.tree_view.get_row_value(iterator, "time_elapsed_data") != elapsed:
                self.tree_view.set_row_value(iterator, "time_elapsed", self.get_helapsed(elapsed))
                self.tree_view.set_row_value(iterator, "time_left", self.get_hleft(left))
                self.tree_view.set_row_value(iterator, "time_elapsed_data", elapsed)
                self.tree_view.set_row_value(iterator, "time_left_data", left)

            if self.tree_view.get_row_value(iterator, "current_bytes_data") != current_byte_offset:
                self.tree_view.set_row_value(iterator, "current_bytes_data", current_byte_offset)
                should_update_size = True

            if self.tree_view.get_row_value(iterator, "size_data") != size:
                self.tree_view.set_row_value(iterator, "size_data", size)
                should_update_size = True

            if self.tree_view.get_row_value(iterator, "queue_position_data") != queue_position:
                self.tree_view.set_row_value(iterator, "queue_position", self.get_hqueue_position(queue_position))
                self.tree_view.set_row_value(iterator, "queue_position_data", queue_position)

            if should_update_size:
                self.tree_view.set_row_value(iterator, "percent", self.get_percent(current_byte_offset, size))
                self.tree_view.set_row_value(iterator, "size", self.get_hsize(current_byte_offset, size))

            return False

        expand_user = False
        expand_folder = False
        user_iterator = None
        user_folder_path_iterator = None
        parent_iterator = None

        user = transfer.username
        basename = transfer.virtual_path.split("\\")[-1]
        original_folder_path = folder_path = self.get_transfer_folder_path(transfer)

        if config.sections["ui"]["reverse_file_paths"]:
            folder_path = self.path_separator.join(reversed(folder_path.split(self.path_separator)))

        if not self.tree_view.iterators:
            # Hide tab description
            self.container.get_parent().set_visible(True)

        if self.grouping_mode != "ungrouped":
            # Group by folder or user

            select_iterator = None
            empty_int = 0
            empty_str = ""

            if user not in self.users:
                # Create parent if it doesn't exist
                iterator = self.tree_view.add_row(
                    [
                        user,
                        empty_str,
                        empty_str,
                        empty_str,
                        empty_str,
                        empty_str,
                        empty_int,
                        empty_str,
                        empty_str,
                        empty_str,
                        empty_str,
                        empty_int,
                        empty_int,
                        empty_int,
                        empty_int,
                        empty_int,
                        empty_int,
                        Transfer(user),  # Dummy Transfer object
                        self.row_id
                    ], select_row=False
                )

                if self.grouping_mode == "folder_grouping":
                    expand_user = True
                else:
                    expand_user = self.expand_button.get_active()

                self.row_id += 1
                self.users[user] = (iterator, [])

            user_iterator, user_child_transfers = self.users[user]

            if self.grouping_mode == "folder_grouping":
                # Group by folder

                # Make sure we don't add files to the wrong user in the TreeView
                user_folder_path = user + original_folder_path

                if user_folder_path not in self.paths:
                    path_transfer = Transfer(user, folder_path=original_folder_path)  # Dummy Transfer object
                    iterator = self.tree_view.add_row(
                        [
                            user,
                            folder_path,
                            empty_str,
                            empty_str,
                            empty_str,
                            empty_str,
                            empty_int,
                            empty_str,
                            empty_str,
                            empty_str,
                            empty_str,
                            empty_int,
                            empty_int,
                            empty_int,
                            empty_int,
                            empty_int,
                            empty_int,
                            path_transfer,
                            self.row_id
                        ], select_row=False, parent_iterator=user_iterator
                    )
                    user_child_transfers.append(path_transfer)
                    expand_folder = self.expand_button.get_active()
                    self.row_id += 1
                    self.paths[user_folder_path] = (iterator, [])

                user_folder_path_iterator, user_folder_path_child_transfers = self.paths[user_folder_path]
                parent_iterator = user_folder_path_iterator
                user_folder_path_child_transfers.append(transfer)

                if select_parent:
                    self.tree_view.expand_row(user_iterator)
                    select_iterator = user_folder_path_iterator

                # Group by folder, path not visible in file rows
                folder_path = ""
            else:
                parent_iterator = user_iterator
                user_child_transfers.append(transfer)

                if select_parent:
                    select_iterator = user_iterator

            if select_iterator and (not self.tree_view.is_row_selected(select_iterator)
                                    or self.tree_view.get_num_selected_rows() != 1):
                # Select parent row of newly added transfer, and scroll to it.
                # Unselect any other rows to prevent accidental actions on previously
                # selected transfers.
                self.tree_view.unselect_all_rows()
                self.tree_view.select_row(select_iterator, expand_rows=False)

        else:
            # No grouping
            if user not in self.users:
                self.users[user] = (None, [])

            user_iterator, user_child_transfers = self.users[user]
            user_child_transfers.append(transfer)

        # Add a new transfer
        row = [
            user,
            folder_path,
            get_file_type_icon_name(basename),
            basename,
            self.translate_status(status),
            self.get_hqueue_position(queue_position),
            self.get_percent(current_byte_offset, size),
            self.get_hsize(current_byte_offset, size),
            self.get_hspeed(speed),
            self.get_helapsed(elapsed),
            self.get_hleft(left),
            size,
            current_byte_offset,
            speed,
            queue_position,
            elapsed,
            left,
            transfer,
            self.row_id
        ]

        transfer.iterator = self.tree_view.add_row(row, select_row=False, parent_iterator=parent_iterator)
        self.row_id += 1

        if expand_user:
            self.tree_view.expand_row(user_iterator)

        if expand_folder:
            self.tree_view.expand_row(user_folder_path_iterator)

        return True

    def clear_model(self):

        self.users.clear()
        self.paths.clear()
        self.selected_transfers.clear()
        self.selected_users.clear()
        self.tree_view.clear()
        self.row_id = 0

        for transfer in self.transfer_list:
            transfer.iterator = None

    def get_transfer_folder_path(self, _transfer):
        # Implemented in subclasses
        raise NotImplementedError

    def retry_selected_transfers(self):
        # Implemented in subclasses
        raise NotImplementedError

    def abort_selected_transfers(self):
        # Implemented in subclasses
        raise NotImplementedError

    def clear_selected_transfers(self):
        # Implemented in subclasses
        raise NotImplementedError

    def abort_transfer(self, transfer, status_message=None, update_parent=True):
        if status_message is not None and status_message != TransferStatus.QUEUED:
            self.update_model(transfer, update_parent=update_parent)

    def abort_transfers(self, _transfers, _status_message=None):
        self.update_parent_rows()

    def clear_transfer(self, transfer, update_parent=True):

        if transfer.iterator is None:
            return

        user = transfer.username

        if self.grouping_mode == "folder_grouping":
            user_folder_path = user + self.get_transfer_folder_path(transfer)
            _user_folder_path_iter, user_folder_path_child_transfers = self.paths[user_folder_path]
            user_folder_path_child_transfers.remove(transfer)
        else:
            _user_iter, user_child_transfers = self.users[user]
            user_child_transfers.remove(transfer)

            if self.grouping_mode == "ungrouped" and not user_child_transfers:
                del self.users[user]

        self.tree_view.remove_row(transfer.iterator)
        transfer.iterator = None

        if update_parent:
            self.update_parent_rows(transfer)
            self.update_num_users_files()

        if not self.tree_view.iterators:
            # Show tab description
            self.container.get_parent().set_visible(False)

    def clear_transfers(self, *_args):
        self.update_parent_rows()

    def add_popup_menu_user(self, popup, user):

        popup.add_items(
            ("", None),
            ("#" + _("Select User's Transfers"), self.on_select_user_transfers, user)
        )
        popup.update_model()
        popup.toggle_user_items()

    def populate_popup_menu_users(self):

        self.popup_menu_users.clear()

        if not self.selected_users:
            return

        # Multiple users, create submenus for each user
        if len(self.selected_users) > 1:
            for user in self.selected_users:
                popup = UserPopupMenu(self.window.application, username=user, tab_name="transfers")
                self.add_popup_menu_user(popup, user)
                self.popup_menu_users.add_items((">" + user, popup))
                self.popup_menu_users.update_model()
            return

        # Single user, add items directly to "User Actions" submenu
        user = next(iter(self.selected_users), None)
        self.popup_menu_users.setup_user_menu(user)
        self.add_popup_menu_user(self.popup_menu_users, user)

    def on_expand_tree(self, *_args):

        expanded = self.expand_button.get_active()

        if expanded:
            icon_name = "view-restore-symbolic"
            self.tree_view.expand_all_rows()
        else:
            icon_name = "view-fullscreen-symbolic"
            self.tree_view.collapse_all_rows()

            if self.grouping_mode == "folder_grouping":
                self.tree_view.expand_root_rows()

        icon_args = (Gtk.IconSize.BUTTON,) if GTK_API_VERSION == 3 else ()  # pylint: disable=no-member
        self.expand_icon.set_from_icon_name(icon_name, *icon_args)

        config.sections["transfers"][f"{self.type}sexpanded"] = expanded
        config.write_configuration()

    def on_toggle_tree(self, action, state):

        mode = state.get_string()
        active = mode != "ungrouped"
        popover = self.grouping_button.get_popover()

        if popover is not None:
            popover.set_visible(False)

        if GTK_API_VERSION >= 4:
            # Ensure buttons are flat in libadwaita
            css_class_function = add_css_class if active else remove_css_class
            css_class_function(widget=self.grouping_button.get_parent(), css_class="linked")

        config.sections["transfers"][f"group{self.type}s"] = mode
        self.tree_view.set_show_expanders(active)
        self.expand_button.set_visible(active)

        self.grouping_mode = mode

        self.clear_model()
        self.tree_view.has_tree = active
        self.tree_view.create_model()

        if self.transfer_list:
            self.update_model(select_parent=False)

        action.set_state(state)

    def on_popup_menu(self, menu, _widget):

        self.select_transfers()
        menu.set_num_selected_files(len(self.selected_transfers))

        self.populate_popup_menu_users()

    def on_file_path_tooltip(self, treeview, iterator):
        transfer = treeview.get_row_value(iterator, "transfer_data")
        return transfer.virtual_path or self.get_transfer_folder_path(transfer)

    def on_row_activated(self, _treeview, iterator, _column_id):

        if self.tree_view.collapse_row(iterator):
            return

        if self.tree_view.expand_row(iterator):
            return

        self.select_transfers()
        action = config.sections["transfers"][f"{self.type}_doubleclick"]

        if action == 1:    # Open File
            self.on_open_file()

        elif action == 2:  # Open in File Manager
            self.on_open_file_manager()

        elif action == 3:  # Search
            self.on_file_search()

        elif action == 4:  # Pause / Abort
            self.abort_selected_transfers()

        elif action == 5:  # Clear
            self.clear_selected_transfers()

        elif action == 6:  # Resume / Retry
            self.retry_selected_transfers()

        elif action == 7:  # Browse Folder
            self.on_browse_folder()

    def on_select_user_transfers(self, *args):

        if not self.selected_users:
            return

        selected_user = args[-1]
        _user_iterator, user_child_transfers = self.users[selected_user]

        self.tree_view.unselect_all_rows()

        for transfer in user_child_transfers:
            iterator = transfer.iterator

            if iterator:
                self.tree_view.select_row(iterator, should_scroll=False)
                continue

            # Dummy Transfer object for folder rows
            user_folder_path = transfer.username + self.get_transfer_folder_path(transfer)
            user_folder_path_data = self.paths.get(user_folder_path)

            if not user_folder_path_data:
                continue

            _user_folder_path_iter, user_folder_path_child_transfers = user_folder_path_data

            for i_transfer in user_folder_path_child_transfers:
                self.tree_view.select_row(i_transfer.iterator, should_scroll=False)

    def on_abort_transfers_accelerator(self, *_args):
        """T - abort transfer."""

        self.select_transfers()
        self.abort_selected_transfers()
        return True

    def on_retry_transfers_accelerator(self, *_args):
        """R - retry transfers."""

        self.select_transfers()
        self.retry_selected_transfers()
        return True

    def on_clear_transfers_accelerator(self, *_args):
        """Delete - clear transfers."""

        self.select_transfers()
        self.clear_selected_transfers()
        return True

    def on_file_properties_accelerator(self, *_args):
        """Alt+Return - show file properties dialog."""

        self.select_transfers()
        self.on_file_properties()
        return True

    def on_user_profile(self, *_args):

        username = next(iter(self.selected_users), None)

        if username:
            core.userinfo.show_user(username)

    def on_file_properties(self, *_args):

        data = []
        selected_size = 0
        selected_length = 0

        for transfer in self.selected_transfers:
            file_path = transfer.virtual_path
            file_size = transfer.size
            file_attributes = transfer.file_attributes
            _bitrate, length, *_unused = FileListMessage.parse_file_attributes(file_attributes)
            selected_size += file_size

            if length:
                selected_length += length

            try:
                folder_path, basename = file_path.rsplit("\\", 1)

            except ValueError:
                folder_path = ""
                basename = file_path

            data.append({
                "user": transfer.username,
                "file_path": file_path,
                "basename": basename,
                "virtual_folder_path": folder_path,
                "real_folder_path": transfer.folder_path,
                "queue_position": transfer.queue_position,
                "speed": transfer.speed,
                "size": file_size,
                "file_attributes": file_attributes
            })

        if data:
            if self.file_properties is None:
                self.file_properties = FileProperties(self.window.application)

            self.file_properties.update_properties(data, selected_size, selected_length)
            self.file_properties.present()

    def on_copy_url(self, *_args):
        # Implemented in subclasses
        raise NotImplementedError

    def on_copy_folder_url(self, *_args):
        # Implemented in subclasses
        raise NotImplementedError

    def on_copy_file_path(self, *_args):

        transfer = next(iter(self.selected_transfers), None)

        if transfer:
            clipboard.copy_text(transfer.virtual_path)

    def on_open_file(self, *_args):
        # Implemented in subclasses
        raise NotImplementedError

    def on_open_file_manager(self, *_args):
        # Implemented in subclasses
        raise NotImplementedError

    def on_browse_folder(self, *_args):
        # Implemented in subclasses
        raise NotImplementedError

    def on_retry_transfer(self, *_args):
        self.select_transfers()
        self.retry_selected_transfers()

    def on_abort_transfer(self, *_args):
        self.select_transfers()
        self.abort_selected_transfers()

    def on_clear_transfer(self, *_args):
        self.select_transfers()
        self.clear_selected_transfers()
