# Copyright (C) 2017-2020 Damon Lynch <damonlynch@gmail.com>

# This file is part of Rapid Photo Downloader.
#
# Rapid Photo Downloader 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.
#
# Rapid Photo Downloader 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 Rapid Photo Downloader.  If not,
# see <http://www.gnu.org/licenses/>.

"""
Display backup preferences
"""

__author__ = 'Damon Lynch'
__copyright__ = "Copyright 2017-2020, Damon Lynch"

from typing import Optional, Dict, Tuple, Union, Set, List, DefaultDict
import logging
import os
from collections import namedtuple, defaultdict

from PyQt5.QtCore import (Qt, pyqtSlot, QAbstractListModel, QModelIndex, QSize)
from PyQt5.QtWidgets import (
    QWidget, QSizePolicy, QVBoxLayout, QLabel, QLineEdit, QCheckBox, QScrollArea, QFrame,
    QStyledItemDelegate, QStyleOptionViewItem, QStyle, QGroupBox, QHBoxLayout, QGridLayout
)
from PyQt5.QtGui import (QPainter, QFontMetrics, QFont, QColor, QPalette, QIcon)

from raphodo.constants import (
    StandardFileLocations, ThumbnailBackgroundName, FileType,  Roles, ViewRowType,
    BackupLocationType
)
from raphodo.viewutils import (QFramedWidget, RowTracker)
from raphodo.rpdfile import FileTypeCounter, Photo, Video
from raphodo.panelview import QPanelView
from raphodo.preferences import Preferences
from raphodo.foldercombo import FolderCombo
import raphodo.qrc_resources as qrc_resources
from raphodo.storage import (ValidMounts, get_media_dir, StorageSpace, get_path_display_name)
from raphodo.devices import (BackupDeviceCollection, BackupVolumeDetails)
from raphodo.devicedisplay import (DeviceDisplay, BodyDetails, icon_size, DeviceView)
from raphodo.destinationdisplay import make_body_details, adjusted_download_size
from raphodo.storage import get_mount_size


BackupVolumeUse = namedtuple(
    'BackupVolumeUse', 'bytes_total bytes_free backup_type marked photos_size_to_download '
                       'videos_size_to_download'
)
BackupViewRow = namedtuple('BackupViewRow', 'mount display_name backup_type os_stat_device')


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(':icons/drive-removable-media.svg').pixmap(size)
        self.folderIcon = QIcon(':/icons/folder.svg').pixmap(size)
        self._initValues()

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

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

        # os_stat_device: Set[FileType]
        self._downloading_to = defaultdict(list)  # type: DefaultDict[int, Set[FileType]]

    @property
    def downloading_to(self):
        return self._downloading_to

    @downloading_to.setter
    def downloading_to(self, downloading_to: DefaultDict[int, Set[FileType]]):
        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()


class BackupDeviceDelegate(QStyledItemDelegate):
    def __init__(self, rapidApp, parent=None) -> None:
        super().__init__(parent)
        self.rapidApp = rapidApp
        self.deviceDisplay = DeviceDisplay()

    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 = index.data(Qt.DisplayRole)  # type: ViewRowType
        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 = index.data(Roles.storage)  # type: BackupVolumeUse
            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 = index.data(Qt.DisplayRole)  # type: ViewRowType
        if view_type == ViewRowType.header:
            height = self.deviceDisplay.device_name_height
        else:
            storage_space = index.data(Roles.storage)

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


class BackupOptionsWidget(QFramedWidget):
    """
    Display and allow editing of preference values for Downloads today
    and Stored Sequence Number and associated options, as well as
    the strip incompatible characters option.
    """

    def __init__(self, prefs: Preferences, parent, rapidApp) -> None:
        super().__init__(parent)

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

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

        backupLayout = QGridLayout()
        layout = QVBoxLayout()
        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(onlyExternalMounts=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)
        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 locatation comboboxes in case directory status has changed.
        """
        for combo in self.photoLocation, self.videoLocation:
            combo.refreshFolderList()


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

    def __init__(self,  parent) -> None:
        super().__init__(parent)

        assert parent is not None
        self.rapidApp = parent
        self.prefs = self.rapidApp.prefs  # type: Preferences

        self.backupDevices = BackupDeviceModel(parent=self)

        self.setFrameShape(QFrame.NoFrame)

        self.backupStoragePanel = QPanelView(
            label=_('Projected Backup Storage Use'),
            headerColor=QColor(ThumbnailBackgroundName),
            headerFontColor=QColor(Qt.white)
        )

        self.backupOptionsPanel = QPanelView(
            label=_('Backup Options'),
            headerColor=QColor(ThumbnailBackgroundName),
            headerFontColor=QColor(Qt.white)
        )

        self.backupDevicesView = BackupDeviceView(rapidApp=self.rapidApp, parent=self)
        self.backupStoragePanel.addWidget(self.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)

        widget = QWidget()
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        widget.setLayout(layout)
        layout.addWidget(self.backupStoragePanel)
        layout.addWidget(self.backupOptionsPanel)
        # layout.addStretch()
        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 locatation 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 = self.rapidApp.backup_devices  # type: BackupDeviceCollection
        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: DefaultDict[int, Set[FileType]]) -> None:
        self.backupDevices.downloading_to = downloading_to

