# SPDX-FileCopyrightText: © 2008-2022 Oprea Dan
# SPDX-FileCopyrightText: © 2008-2022 Bart de Koning
# SPDX-FileCopyrightText: © 2008-2022 Richard Bailey
# SPDX-FileCopyrightText: © 2008-2022 Germar Reitze
# SPDX-FileCopyrightText: © 2008-2022 Taylor Raak
# SPDX-FileCopyrightText: © 2024 Christian BUHTZ <c.buhtz@posteo.jp>
#
# SPDX-License-Identifier: GPL-2.0-or-later
#
# This file is part of the program "Back In Time" which is released under GNU
# General Public License v2 (GPLv2). See file/folder LICENSE or go to
# <https://spdx.org/licenses/GPL-2.0-or-later.html>.
"""The widget to setup scheduling backup jobs."""
import datetime
from PyQt6.QtWidgets import (QCheckBox,
                             QFormLayout,
                             QGroupBox,
                             QHBoxLayout,
                             QLabel,
                             QLineEdit,
                             QSpinBox,
                             QWidget)
from bitbase import ScheduleMode, TimeUnit, HOURLY_BACKUPS
import config
import tools
import qttools
from manageprofiles import combobox


class ScheduleWidget(QGroupBox):
    """Widget about schedule snapshots.

    That widget is used in the 'General' tab of the 'Manage profiles' dialog.
    """
    # pylint: disable=too-many-instance-attributes

    def __init__(self, parent: QWidget, allow_udev: bool = True):
        super().__init__(title=_('Schedule'), parent=parent)

        main_layout = QFormLayout(self)

        def _create_form_entry(label: str, widget: QWidget = None) -> int:
            """Helper to create a row with a label and widget in the form
            layout.

            The returned index is used later to toggle visibility of that
            row.
            """
            if widget:
                main_layout.addRow(label, widget)
            else:
                lbl = QLabel(label)
                lbl.setWordWrap(True)
                main_layout.addRow(lbl)

            return main_layout.rowCount() - 1

        # Schedule modes
        self._combo_schedule_mode = self._schedule_mode_combobox()
        main_layout.addRow(self._combo_schedule_mode)

        # Day
        self._combo_day = self._day_combobox()
        self._rowidx_day = _create_form_entry(_('Day:'), self._combo_day)

        # Weekday
        self._combo_weekday = self._weekday_combobox()
        self._rowidx_weekday = _create_form_entry(
            _('Weekday:'), self._combo_weekday)

        # Time (formerly known as Hour)
        self._combo_time = self._time_combobox()
        self._rowidx_time = _create_form_entry(
            _('Time:'), self._combo_time)

        # HourS
        self._edit_cronpattern = QLineEdit(self)
        self._rowidx_cronpattern = _create_form_entry(
            _('Hours:'), self._edit_cronpattern)

        # Offset
        self._spin_offset = QSpinBox(self)
        self._spin_offset.setSingleStep(1)
        self._spin_offset.setRange(0, 59)
        hlayout = QHBoxLayout()
        hlayout.addWidget(self._spin_offset)
        hlayout.addWidget(QLabel(_('after the hour'), self))
        hlayout.addStretch()
        self._rowidx_offset = _create_form_entry(_('Minutes:'), hlayout)

        # Udev
        self._rowidx_udev = _create_form_entry(
            _('Run Back In Time as soon as the drive is connected (only once'
              ' every X days). A sudo password prompt will appear.'))

        # Repeatedly (like anacron)
        self._rowidx_repeated = _create_form_entry(
            _('Run Back In Time repeatedly. This is useful if the computer '
              'is not running regularly.'))

        # Repeatedly - Every (value) (units)
        self._spin_period = QSpinBox(self)
        self._spin_period.setSingleStep(1)
        self._spin_period.setRange(1, 10000)
        self._combo_repeated_unit = self._repeated_unit_combobox()
        hlayout = QHBoxLayout()
        hlayout.addWidget(self._spin_period)
        hlayout.addWidget(self._combo_repeated_unit)
        hlayout.addStretch()
        self._rowidx_period = _create_form_entry(_('Every:'), hlayout)

        # Debug logging
        self._check_debug = QCheckBox(self)
        self._check_debug.setText(_('Enable logging of debug messages'))
        qttools.set_wrapped_tooltip(
            self._check_debug,
            [
                _('Writes debug-level messages into the system log via '
                  '"--debug".'),
                _('Caution: Only use this temporarily for diagnostics, as it '
                  'generates a large amount of output.')
            ]
        )
        self._rowidx_debug = main_layout.rowCount()
        main_layout.addRow(self._check_debug)

        if not allow_udev:
            self.allow_udev(False)

        # Signal
        self._combo_schedule_mode.currentIndexChanged.connect(
            self._slot_schedule_mode_changed)
        self._slot_schedule_mode_changed(None)

    def _schedule_mode_combobox(self) -> combobox.BitComboBox:
        """Create the the combobox for schedule mode.

        Returns:
            BitComboBox: The widget.
        """
        # pylint: disable=protected-access

        # Regular schedule modes for that combo box
        schedule_modes = {
            config.Config.NONE: _('Disabled'),
            config.Config.AT_EVERY_BOOT: _('At every boot/reboot'),
            config.Config._5_MIN: ngettext(
                'Every minute', 'Every {n} minutes', 5).format(n=5),
            config.Config._10_MIN: ngettext(
                'Every minute', 'Every {n} minutes', 10).format(n=10),
            config.Config._30_MIN: ngettext(
                'Every minute', 'Every {n} minutes', 30).format(n=30),
            config.Config._1_HOUR: ngettext(
                'Every hour', 'Every {n} hours', 1).format(n=1),
            config.Config._2_HOURS: ngettext(
                'Every hour', 'Every {n} hours', 2).format(n=2),
            config.Config._4_HOURS: ngettext(
                'Every hour', 'Every {n} hours', 4).format(n=4),
            config.Config._6_HOURS: ngettext(
                'Every hour', 'Every {n} hours', 6).format(n=6),
            config.Config._12_HOURS: ngettext(
                'Every hour', 'Every {n} hours', 12).format(n=12),
            config.Config.CUSTOM_HOUR: _('Custom hours'),
            config.Config.DAY: _('Every day'),
            config.Config.REPEATEDLY: _('Repeatedly (anacron)'),
            config.Config.UDEV: _('When drive gets connected (udev)'),
            config.Config.WEEK: _('Every week'),
            config.Config.MONTH: _('Every month'),
            config.Config.YEAR: _('Every year')
        }

        return combobox.BitComboBox(self, schedule_modes)

    def allow_udev(self, allow: bool):
        """Enable or disable the udev-schedule entry."""
        # If "Udev" is selected but not allowed anymore set scheduling back to
        # "Disabled"
        if (self._combo_schedule_mode.current_data == config.Config.UDEV
                and not allow):
            self._combo_schedule_mode.select_by_data(config.Config.NONE)

        self._combo_schedule_mode.enable_by_data(config.Config.UDEV, allow)

    def _time_combobox(self) -> combobox.BitComboBox:
        """Combobox with time/hours (e.g. 03:00).

        Returns:
            BitComboBox: The widget.
        """

        # Dev note (buhtz): strftime is needed because of localization
        # {0: '00:00', 100: '01:00', ..., 2200: '22:00', 2300: '23:00'}
        times = {
            val*100: datetime.time(val, 0).strftime('%H:%M')
            for val in range(0, 24)
        }
        return combobox.BitComboBox(self, times)

    def _day_combobox(self) -> combobox.BitComboBox:
        """Combobox with day number in the month.

        Returns:
            BitComboBox: The widget.
        """
        days = {day: str(day) for day in range(1, 29)}
        return combobox.BitComboBox(self, days)

    def _weekday_combobox(self) -> combobox.BitComboBox:
        """Combobox with name of the weekday.

        Returns:
            BitComboBox: The widget.
        """
        sunday = datetime.date(2011, 11, 6)
        weekdays = {
            day: (sunday + datetime.timedelta(days=day)).strftime('%A')
            for day in range(1, 8)
        }
        return combobox.BitComboBox(self, weekdays)

    def _repeated_unit_combobox(self):
        """Combobox for "Every ..." schedule mode to select the units to use.

        Returns:
            BitComboBox: The widget.
        """
        repeatedly_units = {
            TimeUnit.HOUR: _('Hour(s)'),
            TimeUnit.DAY: _('Day(s)'),
            TimeUnit.WEEK: _('Week(s)'),
            TimeUnit.MONTH: _('Month(s)')
        }

        return combobox.BitComboBox(self, repeatedly_units)

    def _slot_schedule_mode_changed(self, _idx):
        """Handle value changed events for schedule mode combobox."""
        self._set_child_visibilities(self._combo_schedule_mode.current_data)

    def _set_child_visibilities(self, schedule_mode: ScheduleMode):
        """Modify the visibility of child widgets (addressed by their index in
        the form layout) based on the selected schedule mode.
        """
        layout = self.layout()

        layout.setRowVisible(
            self._rowidx_cronpattern,
            schedule_mode == ScheduleMode.CUSTOM_HOUR)

        layout.setRowVisible(
            self._rowidx_weekday,
            schedule_mode == ScheduleMode.WEEK)

        layout.setRowVisible(
            self._rowidx_day,
            schedule_mode == ScheduleMode.MONTH)

        layout.setRowVisible(
            self._rowidx_time,
            schedule_mode.value >= ScheduleMode.DAY.value)

        layout.setRowVisible(
            self._rowidx_offset,
            schedule_mode in HOURLY_BACKUPS
        )

        vis = ScheduleMode.REPEATEDLY.value <= schedule_mode.value \
            <= ScheduleMode.UDEV.value
        layout.setRowVisible(
            self._rowidx_period,
            vis)

        if vis is True:
            layout.setRowVisible(self._rowidx_time, False)

        layout.setRowVisible(
            self._rowidx_repeated,
            schedule_mode == ScheduleMode.REPEATEDLY)

        layout.setRowVisible(
            self._rowidx_udev,
            schedule_mode == ScheduleMode.UDEV)

    def load_values(self, cfg: config.Config):
        """Set the values of the widgets regarding the current config."""

        self._combo_schedule_mode.select_by_data(cfg.scheduleMode())

        self._combo_time.select_by_data(cfg.scheduleTime())
        self._combo_day.select_by_data(cfg.scheduleDay())
        self._combo_weekday.select_by_data(cfg.scheduleWeekday())
        self._spin_offset.setValue(cfg.schedule_offset())

        self._edit_cronpattern.setText(cfg.customBackupTime())

        self._spin_period.setValue(cfg.scheduleRepeatedPeriod())
        self._combo_repeated_unit.select_by_data(cfg.scheduleRepeatedUnit())

        self._check_debug.setChecked(cfg.scheduleDebug())

        self._slot_schedule_mode_changed(
            self._combo_schedule_mode.currentIndex())

    def store_values(self, cfg: config.Config) -> bool:
        """Store the widgets values in the config instance.

        Args:
            cfg: The configuration data instance.

        Returns:
            bool: Success or not.
        """

        if self._combo_schedule_mode.current_data == cfg.CUSTOM_HOUR:
            # Dev note (buhtz, 2024-05): IMHO checkCronPattern() is not needed
            # because the "crontab" command itself will validate this. See
            # schedule.write_crontab().
            # We just need to take care to catch an the error in the GUI
            # and report it to the user.
            # An alternative solution would be a GUI element where the user
            # is not able to input an invalid value. See #1449 about redesign
            # the schedule section in the Manage Profiles dialog.
            if not tools.checkCronPattern(self._edit_cronpattern.text()):

                cfg.errorHandler(
                    _('Custom hours can only be a comma separated list of '
                      'hours (e.g. 8,12,18,23) or */3 for periodic '
                      'backups every 3 hours.')
                )

                return False

        cfg.setScheduleMode(self._combo_schedule_mode.current_data)
        cfg.setScheduleTime(self._combo_time.current_data)

        if cfg.scheduleMode() in config.Config.HOURLY_BACKUPS:
            cfg.set_schedule_offset(self._spin_offset.value())
        else:
            cfg.set_schedule_offset(config.Config.DEFAULT_OFFSET)

        cfg.setScheduleWeekday(self._combo_weekday.current_data)
        cfg.setScheduleDay(self._combo_day.current_data)
        cfg.setCustomBackupTime(self._edit_cronpattern.text())
        cfg.setScheduleRepeatedPeriod(self._spin_period.value())
        cfg.setScheduleRepeatedUnit(self._combo_repeated_unit.current_data)
        cfg.setScheduleDebug(self._check_debug.isChecked())

        return True
