# SPDX-FileCopyrightText: Copyright 2017-2024 Damon Lynch <damonlynch@gmail.com>
# SPDX-License-Identifier: GPL-3.0-or-later

"""
Display backup preferences
"""

import logging
import os
from collections import defaultdict
from typing import NamedTuple

from PyQt5.QtCore import (
    QAbstractListModel,
    QModelIndex,
    QSize,
    QStorageInfo,
    Qt,
    pyqtSignal,
    pyqtSlot,
)
from PyQt5.QtGui import QIcon, QPainter, QPalette
from PyQt5.QtWidgets import (
    QCheckBox,
    QGridLayout,
    QGroupBox,
    QHBoxLayout,
    QLabel,
    QLineEdit,
    QSizePolicy,
    QSplitter,
    QStyle,
    QStyledItemDelegate,
    QStyleOptionViewItem,
    QVBoxLayout,
    QWidget,
)

from raphodo.constants import (
    BackupLocationType,
    FileType,
    Roles,
    StandardFileLocations,
    ViewRowType,
)
from raphodo.devices import (
    BackupDeviceCollection,
    BackupVolumeDetails,
    DownloadingTo,
)
from raphodo.internationalisation.install import install_gettext
from raphodo.prefs.preferences import Preferences
from raphodo.rpdfile import FileTypeCounter
from raphodo.storage.storage import ValidMounts, get_media_dir, get_mount_size
from raphodo.tools.utilities import data_file_path
from raphodo.ui.destinationdisplay import adjusted_download_size, make_body_details
from raphodo.ui.devicedisplay import DeviceDisplay, DeviceView, icon_size
from raphodo.ui.foldercombo import FolderCombo
from raphodo.ui.panelview import QPanelView
from raphodo.ui.viewutils import (
    FlexiFrame,
    RowTracker,
    ScrollAreaNoFrame,
)

install_gettext()


class BackupVolumeUse(NamedTuple):
    bytes_total: int
    bytes_free: int
    backup_type: BackupLocationType
    marked: FileTypeCounter
    photos_size_to_download: int
    videos_size_to_download: int


class BackupViewRow(NamedTuple):
    mount: QStorageInfo
    display_name: str
    backup_type: BackupLocationType
    os_stat_device: int


class BackupDeviceModel(QAbstractListModel):
    """
    Stores 'devices' used for backing up photos and videos.

    Want to display:
    (1) destination on local files systems
    (2) external devices, e.g., external hard drives

    Need to account for when download destination is same file system
    as backup destination.
    """

    def __init__(self, parent) -> None:
        super().__init__(parent)
        self.raidApp = parent.rapidApp
        self.prefs = parent.prefs
        size = icon_size()
        self.removableIcon = QIcon(
            data_file_path("icons/drive-removable-media.svg")
        ).pixmap(size)
        self.folderIcon = QIcon(data_file_path("icons/folder.svg")).pixmap(size)
        self._initValues()

    def _initValues(self):
        self.rows: RowTracker = RowTracker()
        self.row_id_counter: int = 0
        # {row_id}
        self.headers: set[int] = set()
        # path: BackupViewRow
        self.backup_devices: dict[str, BackupViewRow] = dict()
        self.path_to_row_ids: dict[str, list[int]] = defaultdict(list)
        self.row_id_to_path: dict[int, str] = dict()

        self.marked = FileTypeCounter()
        self.photos_size_to_download = self.videos_size_to_download = 0

        # os_stat_device: set[FileType]
        self._downloading_to: DownloadingTo = defaultdict(set)

    @property
    def downloading_to(self) -> DownloadingTo:
        return self._downloading_to

    @downloading_to.setter
    def downloading_to(self, downloading_to: DownloadingTo) -> None:
        self._downloading_to = downloading_to
        self.downloadSizeChanged()

    def reset(self) -> None:
        self.beginResetModel()
        self._initValues()
        self.endResetModel()

    def columnCount(self, parent=QModelIndex()):
        return 1

    def rowCount(self, parent=QModelIndex()):
        return max(len(self.rows), 1)

    def insertRows(self, position, rows=2, index=QModelIndex()):
        self.beginInsertRows(QModelIndex(), position, position + rows - 1)
        self.endInsertRows()
        return True

    def removeRows(self, position, rows=2, index=QModelIndex()):
        self.beginRemoveRows(QModelIndex(), position, position + rows - 1)
        self.endRemoveRows()
        return True

    def addBackupVolume(self, mount_details: BackupVolumeDetails) -> None:
        mount = mount_details.mount
        display_name = mount_details.name
        path = mount_details.path
        backup_type = mount_details.backup_type
        os_stat_device = mount_details.os_stat_device

        assert mount is not None
        assert display_name
        assert path
        assert backup_type

        # two rows per device: header row, and detail row
        row = len(self.rows)
        self.insertRows(position=row)
        logging.debug(
            "Adding %s to backup device display with root path %s at rows %s - %s",
            display_name,
            mount.rootPath(),
            row,
            row + 1,
        )

        for row_id in range(self.row_id_counter, self.row_id_counter + 2):
            self.row_id_to_path[row_id] = path
            self.rows[row] = row_id
            row += 1
            self.path_to_row_ids[path].append(row_id)

        header_row_id = self.row_id_counter
        self.headers.add(header_row_id)

        self.row_id_counter += 2

        self.backup_devices[path] = BackupViewRow(
            mount=mount,
            display_name=display_name,
            backup_type=backup_type,
            os_stat_device=os_stat_device,
        )

    def removeBackupVolume(self, path: str) -> None:
        """
        :param path: the value of the volume (mount's path), NOT a
        manually specified path!
        """

        row_ids = self.path_to_row_ids[path]
        header_row_id = row_ids[0]
        row = self.rows.row(header_row_id)
        logging.debug("Removing 2 rows from backup view, starting at row %s", row)
        self.rows.remove_rows(row, 2)
        self.headers.remove(header_row_id)
        del self.path_to_row_ids[path]
        del self.backup_devices[path]
        for row_id in row_ids:
            del self.row_id_to_path[row_id]
        self.removeRows(row, 2)

    def setDownloadAttributes(
        self, marked: FileTypeCounter, photos_size: int, videos_size: int, merge: bool
    ) -> None:
        """
        Set the attributes used to generate the visual display of the
        files marked to be downloaded

        :param marked: number and type of files marked for download
        :param photos_size: size in bytes of photos marked for download
        :param videos_size: size in bytes of videos marked for download
        :param merge: whether to replace or add to the current values
        """

        if not merge:
            self.marked = marked
            self.photos_size_to_download = photos_size
            self.videos_size_to_download = videos_size
        else:
            self.marked.update(marked)
            self.photos_size_to_download += photos_size
            self.videos_size_to_download += videos_size
        self.downloadSizeChanged()

    def downloadSizeChanged(self) -> None:
        # TODO possibly optimize for photo vs video rows
        for row in range(1, len(self.rows), 2):
            self.dataChanged.emit(self.index(row, 0), self.index(row, 0))

    def _download_size_by_backup_type(
        self, backup_type: BackupLocationType
    ) -> tuple[int, int]:
        """
        Include photos or videos in download size only if those file types
        are being backed up to this backup device
        :param backup_type: which file types are being backed up to this device
        :return: photos_size_to_download, videos_size_to_download
        """

        photos_size_to_download = videos_size_to_download = 0
        if backup_type != BackupLocationType.videos:
            photos_size_to_download = self.photos_size_to_download
        if backup_type != BackupLocationType.photos:
            videos_size_to_download = self.videos_size_to_download
        return photos_size_to_download, videos_size_to_download

    def data(self, index: QModelIndex, role=Qt.DisplayRole):
        if not index.isValid():
            return None

        row = index.row()

        # check for special case where no backup devices are active
        if len(self.rows) == 0:
            if role == Qt.DisplayRole:
                return ViewRowType.header
            elif role == Roles.device_details:
                if not self.prefs.backup_files:
                    return _("Backups are not configured"), self.removableIcon
                elif self.prefs.backup_device_autodetection:
                    return _("No backup devices detected"), self.removableIcon
                else:
                    return (
                        _("Valid backup locations not yet specified"),
                        self.folderIcon,
                    )

        # at least one device  / location is being used
        if row >= len(self.rows) or row < 0:
            return None
        if row not in self.rows:
            return None

        row_id = self.rows[row]
        path = self.row_id_to_path[row_id]

        if role == Qt.DisplayRole:
            if row_id in self.headers:
                return ViewRowType.header
            else:
                return ViewRowType.content
        else:
            device = self.backup_devices[path]
            mount = device.mount

            if role == Qt.ToolTipRole:
                return path
            elif role == Roles.device_details:
                if self.prefs.backup_device_autodetection:
                    icon = self.removableIcon
                else:
                    icon = self.folderIcon
                return device.display_name, icon
            elif role == Roles.storage:
                (
                    photos_size_to_download,
                    videos_size_to_download,
                ) = self._download_size_by_backup_type(backup_type=device.backup_type)

                (
                    photos_size_to_download,
                    videos_size_to_download,
                ) = adjusted_download_size(
                    photos_size_to_download=photos_size_to_download,
                    videos_size_to_download=videos_size_to_download,
                    os_stat_device=device.os_stat_device,
                    downloading_to=self._downloading_to,
                )

                bytes_total, bytes_free = get_mount_size(mount=mount)

                return BackupVolumeUse(
                    bytes_total=bytes_total,
                    bytes_free=bytes_free,
                    backup_type=device.backup_type,
                    marked=self.marked,
                    photos_size_to_download=photos_size_to_download,
                    videos_size_to_download=videos_size_to_download,
                )

        return None

    def sufficientSpaceAvailable(self) -> bool:
        """
        Detect if each backup device has sufficient space for backing up, taking
        into accoutn situations where downloads and backups are going to the same
        partition.

        :return: False if any backup device has insufficient space, else True.
         True if there are no backup devices.
        """

        for device in self.backup_devices.values():
            (
                photos_size_to_download,
                videos_size_to_download,
            ) = self._download_size_by_backup_type(backup_type=device.backup_type)
            photos_size_to_download, videos_size_to_download = adjusted_download_size(
                photos_size_to_download=photos_size_to_download,
                videos_size_to_download=videos_size_to_download,
                os_stat_device=device.os_stat_device,
                downloading_to=self._downloading_to,
            )

            bytes_total, bytes_free = get_mount_size(mount=device.mount)
            if photos_size_to_download + videos_size_to_download >= bytes_free:
                return False
        return True


class BackupDeviceView(DeviceView):
    def __init__(self, rapidApp, parent=None) -> None:
        super().__init__(rapidApp, parent)
        self.setMouseTracking(False)
        self.entered.disconnect()
        # TODO sizeHint() ?


class BackupDeviceDelegate(QStyledItemDelegate):
    widthChanged = pyqtSignal(int)

    def __init__(self, rapidApp, parent=None) -> None:
        super().__init__(parent)
        self.rapidApp = rapidApp
        self.deviceDisplay = DeviceDisplay(parent=self)
        self.deviceDisplay.widthChanged.connect(self.widthChanged)

    def paint(
        self, painter: QPainter, option: QStyleOptionViewItem, index: QModelIndex
    ) -> None:
        painter.save()

        x = option.rect.x()
        y = option.rect.y()
        width = option.rect.width()

        view_type: ViewRowType = index.data(Qt.DisplayRole)
        if view_type == ViewRowType.header:
            display_name, icon = index.data(Roles.device_details)

            self.deviceDisplay.paint_header(
                painter=painter,
                x=x,
                y=y,
                width=width,
                icon=icon,
                display_name=display_name,
            )
        else:
            assert view_type == ViewRowType.content

            data: BackupVolumeUse = index.data(Roles.storage)
            details = make_body_details(
                bytes_total=data.bytes_total,
                bytes_free=data.bytes_free,
                files_to_display=data.backup_type,
                marked=data.marked,
                photos_size_to_download=data.photos_size_to_download,
                videos_size_to_download=data.videos_size_to_download,
            )

            self.deviceDisplay.paint_body(
                painter=painter, x=x, y=y, width=width, details=details
            )

        painter.restore()

    def sizeHint(self, option: QStyleOptionViewItem, index: QModelIndex) -> QSize:
        view_type: ViewRowType = index.data(Qt.DisplayRole)
        if view_type == ViewRowType.header:
            height = self.deviceDisplay.dc.device_name_height
        else:
            storage_space = index.data(Roles.storage)

            if storage_space is None:
                height = self.deviceDisplay.dc.base_height
            else:
                height = self.deviceDisplay.dc.storage_height
        return QSize(self.deviceDisplay.width(), height)


class BackupOptionsWidget(FlexiFrame):
    """
    Display backup options, such as automatic backup detection
    """

    def __init__(self, prefs: Preferences, parent, rapidApp: "RapidWindow") -> None:  # noqa: F821
        super().__init__(parent=parent)

        self.rapidApp = rapidApp
        self.prefs = prefs
        self.media_dir = get_media_dir()

        self.setBackgroundRole(QPalette.Base)
        self.setAutoFillBackground(True)

        backupLayout = QGridLayout()
        layout = self.layout()
        layout.addLayout(backupLayout)
        self.setLayout(layout)

        self.backupExplanation = QLabel(
            _(
                "You can have your photos and videos backed up to "
                "multiple locations as they are downloaded, e.g. "
                "external hard drives."
            )
        )
        self.backupExplanation.setWordWrap(True)
        self.backupExplanation.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Minimum)

        self.backup = QCheckBox(_("Back up photos and videos when downloading"))
        self.backup.setChecked(self.prefs.backup_files)
        self.backup.stateChanged.connect(self.backupChanged)

        checkbox_width = self.backup.style().pixelMetric(QStyle.PM_IndicatorWidth)

        self.autoBackup = QCheckBox(_("Automatically detect backup devices"))
        self.autoBackup.setChecked(self.prefs.backup_device_autodetection)
        self.autoBackup.stateChanged.connect(self.autoBackupChanged)

        self.folderExplanation = QLabel(
            _(
                "Specify the folder in which backups are stored on the "
                "device.<br><br>"
                "<i>Note: the presence of a folder with this name "
                "is used to determine if the device is used for backups. "
                "For each device you wish to use for backing up to, "
                "create a folder in it with one of these folder names. "
                "By adding both folders, the same device can be used "
                "to back up both photos and videos.</i>"
            )
        )
        self.folderExplanation.setWordWrap(True)
        self.folderExplanation.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Minimum)
        # Unless this next call is made, for some reason the widget is too high! :-(
        self.folderExplanation.setContentsMargins(0, 0, 1, 0)

        self.photoFolderNameLabel = QLabel(_("Photo folder name:"))
        self.photoFolderName = QLineEdit()
        self.photoFolderName.setText(self.prefs.photo_backup_identifier)
        self.photoFolderName.editingFinished.connect(self.photoFolderIdentifierChanged)

        self.videoFolderNameLabel = QLabel(_("Video folder name:"))
        self.videoFolderName = QLineEdit()
        self.videoFolderName.setText(self.prefs.video_backup_identifier)
        self.videoFolderName.editingFinished.connect(self.videoFolderIdentifierChanged)

        self.autoBackupExampleBox = QGroupBox(_("Example:"))
        self.autoBackupExample = QLabel()

        autoBackupExampleBoxLayout = QHBoxLayout()
        autoBackupExampleBoxLayout.addWidget(self.autoBackupExample)

        self.autoBackupExampleBox.setLayout(autoBackupExampleBoxLayout)

        valid_mounts = ValidMounts(only_external_mounts=True)

        self.manualLocationExplanation = QLabel(
            _("If you disable automatic detection, choose the exact backup locations.")
        )
        self.manualLocationExplanation.setWordWrap(True)
        self.manualLocationExplanation.setSizePolicy(
            QSizePolicy.Ignored, QSizePolicy.Minimum
        )
        # Translators: the word 'location' is optional in your translation. The left
        # side of the folder chooser combo box will always line up with the left side of
        # the the text entry boxes where the user can enter the photo folder name and
        # the video folder name. See
        # http://damonlynch.net/rapid/documentation/thumbnails/backup.png
        self.photoLocationLabel = QLabel(_("Photo backup location:"))
        self.photoLocationLabel.setWordWrap(True)
        if self.rapidApp.is_wsl2 and not self.rapidApp.wsl_drives_probed:
            # flag that once Windows drives volume names are known, update these
            # combo box values
            self.rapidApp.wsl_backup_drives_refresh_needed = True
        self.photoLocation = FolderCombo(
            self,
            prefs=self.prefs,
            file_type=FileType.photo,
            file_chooser_title=_("Select Photo Backup Location"),
            special_dirs=(StandardFileLocations.pictures,),
            valid_mounts=valid_mounts,
        )
        self.photoLocation.setPath(self.prefs.backup_photo_location)
        self.photoLocation.pathChosen.connect(self.photoPathChosen)

        # Translators: the word 'location' is optional in your translation. The left
        # side of the folder chooser combo box will always line up with the left side of
        # the the text entry boxes where the user can enter the photo folder name and
        # the video folder name. See
        # http://damonlynch.net/rapid/documentation/thumbnails/backup.png
        self.videoLocationLabel = QLabel(_("Video backup location:"))
        self.videoLocationLabel.setWordWrap(True)
        self.videoLocation = FolderCombo(
            self,
            prefs=self.prefs,
            file_type=FileType.video,
            file_chooser_title=_("Select Video Backup Location"),
            special_dirs=(StandardFileLocations.videos,),
            valid_mounts=valid_mounts,
        )
        self.videoLocation.setPath(self.prefs.backup_video_location)
        self.videoLocation.pathChosen.connect(self.videoPathChosen)

        backupLayout.addWidget(self.backupExplanation, 0, 0, 1, 4)
        backupLayout.addWidget(self.backup, 1, 0, 1, 4)
        backupLayout.addWidget(self.autoBackup, 2, 1, 1, 3)
        backupLayout.addWidget(self.folderExplanation, 3, 2, 1, 2)
        backupLayout.addWidget(self.photoFolderNameLabel, 4, 2, 1, 1)
        backupLayout.addWidget(self.photoFolderName, 4, 3, 1, 1)
        backupLayout.addWidget(self.videoFolderNameLabel, 5, 2, 1, 1)
        backupLayout.addWidget(self.videoFolderName, 5, 3, 1, 1)
        backupLayout.addWidget(self.autoBackupExampleBox, 6, 2, 1, 2)
        backupLayout.addWidget(
            self.manualLocationExplanation, 7, 1, 1, 3, Qt.AlignBottom
        )
        backupLayout.addWidget(self.photoLocationLabel, 8, 1, 1, 2)
        backupLayout.addWidget(self.photoLocation, 8, 3, 1, 1)
        backupLayout.addWidget(self.videoLocationLabel, 9, 1, 1, 2)
        backupLayout.addWidget(self.videoLocation, 9, 3, 1, 1)

        backupLayout.setColumnMinimumWidth(0, checkbox_width)
        backupLayout.setColumnMinimumWidth(1, checkbox_width)

        backupLayout.setRowMinimumHeight(7, checkbox_width * 2)

        layout.addStretch()

        self.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)
        self.setBackupButtonHighlight()

        # Group controls to enable / disable sets of them
        self._backup_controls_type = (self.autoBackup,)
        self._backup_controls_auto = (
            self.folderExplanation,
            self.photoFolderNameLabel,
            self.photoFolderName,
            self.videoFolderNameLabel,
            self.videoFolderName,
            self.autoBackupExampleBox,
        )
        self._backup_controls_manual = (
            self.manualLocationExplanation,
            self.photoLocationLabel,
            self.photoLocation,
            self.videoLocationLabel,
            self.videoLocation,
        )
        self.updateExample()
        self.enableControlsByBackupType()

    @pyqtSlot(int)
    def backupChanged(self, state: int) -> None:
        backup = state == Qt.Checked
        logging.info("Setting backup while downloading to %s", backup)
        self.prefs.backup_files = backup
        self.setBackupButtonHighlight()
        self.enableControlsByBackupType()
        self.rapidApp.resetupBackupDevices()

    @pyqtSlot(int)
    def autoBackupChanged(self, state: int) -> None:
        autoBackup = state == Qt.Checked
        logging.info("Setting automatically detect backup devices to %s", autoBackup)
        self.prefs.backup_device_autodetection = autoBackup
        self.setBackupButtonHighlight()
        self.enableControlsByBackupType()
        self.rapidApp.resetupBackupDevices()

    @pyqtSlot(str)
    def photoPathChosen(self, path: str) -> None:
        logging.info("Setting backup photo location to %s", path)
        self.prefs.backup_photo_location = path
        self.setBackupButtonHighlight()
        self.rapidApp.resetupBackupDevices()

    @pyqtSlot(str)
    def videoPathChosen(self, path: str) -> None:
        logging.info("Setting backup video location to %s", path)
        self.prefs.backup_video_location = path
        self.setBackupButtonHighlight()
        self.rapidApp.resetupBackupDevices()

    @pyqtSlot()
    def photoFolderIdentifierChanged(self) -> None:
        name = self.photoFolderName.text()
        logging.info("Setting backup photo folder name to %s", name)
        self.prefs.photo_backup_identifier = name
        self.setBackupButtonHighlight()
        self.rapidApp.resetupBackupDevices()

    @pyqtSlot()
    def videoFolderIdentifierChanged(self) -> None:
        name = self.videoFolderName.text()
        logging.info("Setting backup video folder name to %s", name)
        self.prefs.video_backup_identifier = name
        self.setBackupButtonHighlight()
        self.rapidApp.resetupBackupDevices()

    def updateExample(self) -> None:
        """
        Update the example paths in the backup panel
        """

        if (
            self.autoBackup.isChecked()
            and hasattr(self.rapidApp, "backup_devices")
            and len(self.rapidApp.backup_devices)
        ):
            drives = self.rapidApp.backup_devices.sample_device_paths()
        else:
            # Translators: this value is used as an example device when automatic backup
            # device detection is enabled. You should translate this.
            drive1 = os.path.join(self.media_dir, _("drive1"))
            # Translators: this value is used as an example device when automatic backup
            # device detection is enabled. You should translate this.
            drive2 = os.path.join(self.media_dir, _("drive2"))
            drives = (
                os.path.join(path, identifier)
                for path, identifier in (
                    (drive1, self.prefs.photo_backup_identifier),
                    (drive2, self.prefs.photo_backup_identifier),
                    (drive2, self.prefs.video_backup_identifier),
                )
            )
        paths = "\n".join(drives)
        self.autoBackupExample.setText(paths)

    def setBackupButtonHighlight(self) -> None:
        """
        Indicate error status in GUI by highlighting Backup button.

        Do so only if doing manual backups and there is a problem with one of the paths
        """

        self.rapidApp.backupButton.setHighlighted(
            self.prefs.backup_files
            and not self.prefs.backup_device_autodetection
            and (self.photoLocation.invalid_path or self.videoLocation.invalid_path)
        )

    def enableControlsByBackupType(self) -> None:
        """
        Enable or disable backup controls depending on what the user
        has enabled.
        """

        backupsEnabled = self.backup.isChecked()
        autoEnabled = backupsEnabled and self.autoBackup.isChecked()
        manualEnabled = not autoEnabled and backupsEnabled

        for widget in self._backup_controls_type:
            widget.setEnabled(backupsEnabled)
        for widget in self._backup_controls_manual:
            widget.setEnabled(manualEnabled)
        for widget in self._backup_controls_auto:
            widget.setEnabled(autoEnabled)

    def updateLocationCombos(self) -> None:
        """
        Update backup location comboboxes in case directory status has changed.
        """
        for combo in self.photoLocation, self.videoLocation:
            combo.refreshFolderList()


class BackupPanel(ScrollAreaNoFrame):
    """
    Backup preferences widget, for photos and video backups while
    downloading.
    """

    def __init__(self, parent) -> None:
        super().__init__(name="backupPanel", parent=parent)

        assert parent is not None
        self.rapidApp = parent
        self.prefs: Preferences = self.rapidApp.prefs
        self.setObjectName("backupPanelScrollArea")

        self.backupDevices = BackupDeviceModel(parent=self)

        self.backupStoragePanel = QPanelView(
            label=_("Projected Backup Storage Use"),
        )
        self.backupStoragePanel.setObjectName("backupStoragePanel")

        self.backupOptionsPanel = QPanelView(
            label=_("Backup Options"),
        )
        self.backupOptionsPanel.setObjectName("backupOptionsPanel")

        self.backupDevicesView = BackupDeviceView(rapidApp=self.rapidApp, parent=self)
        self.backupDevicesView.setObjectName("backupDevicesView")
        self.backupDevicesView.setModel(self.backupDevices)
        self.backupDevicesView.setItemDelegate(
            BackupDeviceDelegate(rapidApp=self.rapidApp)
        )
        self.backupDevicesView.setSizePolicy(
            QSizePolicy.MinimumExpanding, QSizePolicy.Fixed
        )
        self.backupOptionsPanel.setSizePolicy(
            QSizePolicy.Preferred, QSizePolicy.MinimumExpanding
        )

        self.backupOptions = BackupOptionsWidget(
            prefs=self.prefs, parent=self, rapidApp=self.rapidApp
        )

        self.backupOptionsPanel.addWidget(self.backupOptions)
        self.backupStoragePanel.addWidget(self.backupDevicesView)

        self.verticalScrollBarVisible.connect(
            self.backupDevicesView.containerVerticalScrollBar
        )
        self.verticalScrollBarVisible.connect(
            self.backupOptions.containerVerticalScrollBar
        )
        self.horizontalScrollBarVisible.connect(
            self.backupOptions.containerHorizontalScrollBar
        )
        widget = QWidget()
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setSpacing(QSplitter().handleWidth())
        widget.setLayout(layout)
        layout.addWidget(self.backupStoragePanel)
        layout.addWidget(self.backupOptionsPanel)
        self.setWidget(widget)
        self.setWidgetResizable(True)
        self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)

    def updateExample(self) -> None:
        """
        Update the example paths in the backup panel
        """

        self.backupOptions.updateExample()

    def updateLocationCombos(self) -> None:
        """
        Update backup location comboboxes in case directory status has changed.
        """

        self.backupOptions.updateLocationCombos()

    def addBackupVolume(self, mount_details: BackupVolumeDetails) -> None:
        self.backupDevices.addBackupVolume(mount_details=mount_details)
        self.backupDevicesView.updateGeometry()

    def removeBackupVolume(self, path: str) -> None:
        self.backupDevices.removeBackupVolume(path=path)
        self.backupDevicesView.updateGeometry()

    def resetBackupDisplay(self) -> None:
        self.backupDevices.reset()
        self.backupDevicesView.updateGeometry()

    def setupBackupDisplay(self) -> None:
        """
        Sets up the backup view list regardless of whether backups
        are manual specified by the user, or auto-detection is on
        """

        if not self.prefs.backup_files:
            logging.debug("No backups configured: no backup destinations to display")
            return

        backup_devices: BackupDeviceCollection = self.rapidApp.backup_devices
        if self.prefs.backup_device_autodetection:
            for path in backup_devices:
                self.backupDevices.addBackupVolume(
                    mount_details=backup_devices.get_backup_volume_details(path=path)
                )
        else:
            # manually specified backup paths
            try:
                mounts = backup_devices.get_manual_mounts()
                if mounts is None:
                    return

                self.backupDevices.addBackupVolume(mount_details=mounts[0])
                if len(mounts) > 1:
                    self.backupDevices.addBackupVolume(mount_details=mounts[1])
            except Exception:
                logging.exception(
                    "An unexpected error occurred when adding backup destinations. "
                    "Exception:"
                )
        self.backupDevicesView.updateGeometry()

    def setDownloadAttributes(
        self, marked: FileTypeCounter, photos_size: int, videos_size: int, merge: bool
    ) -> None:
        """
        Set the attributes used to generate the visual display of the
        files marked to be downloaded

        :param marked: number and type of files marked for download
        :param photos_size: size in bytes of photos marked for download
        :param videos_size: size in bytes of videos marked for download
        :param merge: whether to replace or add to the current values
        """

        self.backupDevices.setDownloadAttributes(
            marked=marked, photos_size=photos_size, videos_size=videos_size, merge=merge
        )

    def sufficientSpaceAvailable(self) -> bool:
        """
        Check to see that there is sufficient space with which to perform a download.

        :return: True or False value if sufficient space. Will always return True if
         backups are disabled or there are no backup devices.
        """
        if self.prefs.backup_files:
            return self.backupDevices.sufficientSpaceAvailable()
        else:
            return True

    def setDownloadingTo(self, downloading_to: DownloadingTo) -> None:
        self.backupDevices.downloading_to = downloading_to
