"""This module contains all the filter-related classes and functions.

The only class exposed is Filters which provides the following methods:
- ok(signal_name): to check if all the filters are passed;
- reset(): to reset all the applied filters;
- refresh(): used when the theme is changed."""

from functools import partial
import webbrowser
from PyQt5.QtWidgets import QListWidgetItem, QTreeWidgetItem
from PyQt5.QtCore import pyqtSlot, QObject
from constants import (
    Constants,
    Ftype,
    Signal,
)
from utilities import (
    uncheck_and_emit,
    connect_events_to_func,
    filters_limit,
    is_undef_freq,
    is_undef_band,
    safe_cast,
    show_matching_strings,
    get_field_entries,
)


class _BaseFilter(QObject):
    """Base class for all filters."""

    def __init__(self, owner):
        """Positional argument:
        owner - the object containing the filter screen."""
        super().__init__()
        self._owner = owner

    def refresh(self):
        """Refresh the screen."""
        pass


class _FreqBandMixIn:
    """Mixin class for the frequency and band filters.

    Provides some functions used in both classes."""

    @pyqtSlot()
    def _set_min_value_upper_limit(self, lower_combo_box,
                                   lower_spin_box,
                                   upper_combo_box,
                                   upper_spin_box):
        """Forbid to a lower limit to be greater than the corresponding upper one.

        Used for frequency and bandwidth screens."""
        if lower_spin_box.isEnabled():
            unit_conversion = {
                'Hz': ['kHz', 'MHz', 'GHz'],
                'kHz': ['MHz', 'GHz'],
                'MHz': ['GHz']
            }
            lower_units = lower_combo_box.currentText()
            upper_units = upper_combo_box.currentText()
            lower_value = lower_spin_box.value()
            inf_limit = (lower_value * Constants.CONVERSION_FACTORS[lower_units]) \
                // Constants.CONVERSION_FACTORS[upper_units]
            counter = 0
            while inf_limit > upper_spin_box.maximum():
                counter += 1
                inf_limit //= 1000
            if upper_spin_box.minimum() != inf_limit:
                upper_spin_box.setMinimum(inf_limit)
            if counter > 0:
                new_unit = unit_conversion[upper_units][counter - 1]
                upper_combo_box.disconnect()
                upper_combo_box.setCurrentText(new_unit)
                upper_combo_box.currentTextChanged.connect(
                    partial(
                        self._set_min_value_upper_limit,
                        lower_combo_box,
                        lower_spin_box,
                        upper_combo_box,
                        upper_spin_box
                    )
                )

    @pyqtSlot()
    def _reset_fb_filters(self, ftype):
        """Reset the Frequency or Bandwidth depending on 'ftype'.

        ftype can be either Ftype.FREQ or Ftype.BAND.
        """
        if ftype != Ftype.FREQ and ftype != Ftype.BAND:
            raise ValueError("Wrong ftype in function '_reset_fb_filters'")

        apply_remove_btn  = getattr(self._owner, 'apply_remove_'  + ftype + '_filter_btn')
        include_undef_btn = getattr(self._owner, 'include_undef_' + ftype + 's')
        activate_low      = getattr(self._owner, 'activate_low_'  + ftype + '_filter_btn')
        activate_up       = getattr(self._owner, 'activate_up_'   + ftype + '_filter_btn')
        lower_unit        = getattr(self._owner, 'lower_'         + ftype + '_filter_unit')
        upper_unit        = getattr(self._owner, 'upper_'         + ftype + '_filter_unit')
        lower_spinbox     = getattr(self._owner, 'lower_'         + ftype + '_spinbox')
        upper_spinbox     = getattr(self._owner, 'upper_'         + ftype + '_spinbox')
        lower_confidence  = getattr(self._owner, 'lower_'         + ftype + '_confidence')
        upper_confidence  = getattr(self._owner, 'lower_'         + ftype + '_confidence')

        default_val = 1 if ftype == Ftype.FREQ else 5000
        if ftype == Ftype.FREQ:
            for f in self._frequency_filters_btns:
                if f.isChecked():
                    f.setChecked(False)
        uncheck_and_emit(apply_remove_btn)
        if include_undef_btn.isChecked():
            include_undef_btn.setChecked(False)
        uncheck_and_emit(activate_low)
        uncheck_and_emit(activate_up)
        lower_unit.setCurrentText("MHz")
        upper_unit.setCurrentText("MHz")
        lower_spinbox.setValue(default_val)
        upper_spinbox.setMinimum(1)
        upper_spinbox.setValue(default_val)
        lower_confidence.setValue(0)
        upper_confidence.setValue(0)

    @pyqtSlot()
    def _set_band_filter_label(self,
                               activate_low_btn,
                               lower_spinbox,
                               lower_unit,
                               lower_confidence,
                               activate_up_btn,
                               upper_spinbox,
                               upper_unit,
                               upper_confidence,
                               range_lbl):
        """Display the actual range applied for the signal's property search.

        Used for frequency and bandwidth screens."""
        activate_low = False
        activate_high = False
        color = self._owner.inactive_color
        title = ''
        to_display = ''
        if activate_low_btn.isChecked():
            activate_low = True
            color = self._owner.active_color
            min_value = lower_spinbox.value()
            if lower_confidence.value() != 0:
                min_value -= lower_spinbox.value() * lower_confidence.value() / 100
            to_display += str(round(min_value, Constants.MAX_DIGITS)) \
                + ' ' + lower_unit.currentText()
        else:
            to_display += 'DC'
        to_display += Constants.RANGE_SEPARATOR
        if activate_up_btn.isChecked():
            max_value = upper_spinbox.value()
            activate_high = True
            color = self._owner.active_color
            if upper_confidence.value() != 0:
                max_value += upper_spinbox.value() * upper_confidence.value() / 100
            to_display += str(round(max_value, Constants.MAX_DIGITS)) + ' ' \
                + upper_unit.currentText()
        else:
            to_display += 'INF'
        if activate_low and activate_high:
            title = 'Band-pass\n\n'
        elif activate_low and not activate_high:
            title = 'Low-pass\n\n'
        elif not activate_low and activate_high:
            title = 'High-pass\n\n'
        else:
            title = "Selected range:\n\n"
            to_display = "Inactive"
        to_display = title + to_display
        range_lbl.setText(to_display)
        range_lbl.setStyleSheet(f'color: {color};')


class FreqFilter(_BaseFilter, _FreqBandMixIn):
    """Frequency filter class."""

    def __init__(self, owner):
        super().__init__(owner)
        self.apply_remove_btn = self._owner.apply_remove_freq_filter_btn
        self.reset_btn = self._owner.reset_frequency_filters_btn
        self._frequency_filters_btns = (
            self._owner.elf_filter_btn,
            self._owner.slf_filter_btn,
            self._owner.ulf_filter_btn,
            self._owner.vlf_filter_btn,
            self._owner.lf_filter_btn,
            self._owner.mf_filter_btn,
            self._owner.hf_filter_btn,
            self._owner.vhf_filter_btn,
            self._owner.uhf_filter_btn,
            self._owner.shf_filter_btn,
            self._owner.ehf_filter_btn,
        )

        self.apply_remove_btn.set_texts(Constants.APPLY, Constants.REMOVE)
        self.apply_remove_btn.set_slave_filters(
            simple_ones=[
                *self._frequency_filters_btns,
                self._owner.include_undef_freqs,
                self._owner.activate_low_freq_filter_btn,
                self._owner.activate_up_freq_filter_btn
            ],
            radio_1=self._owner.activate_low_freq_filter_btn,
            ruled_by_radio_1=[
                self._owner.lower_freq_spinbox,
                self._owner.lower_freq_filter_unit,
                self._owner.lower_freq_confidence
            ],
            radio_2=self._owner.activate_up_freq_filter_btn,
            ruled_by_radio_2=[
                self._owner.upper_freq_spinbox,
                self._owner.upper_freq_filter_unit,
                self._owner.upper_freq_confidence
            ]
        )

        connect_events_to_func(
            events_to_connect=[self._owner.lower_freq_spinbox.valueChanged,
                               self._owner.upper_freq_spinbox.valueChanged,
                               self._owner.lower_freq_filter_unit.currentTextChanged,
                               self._owner.upper_freq_filter_unit.currentTextChanged,
                               self._owner.activate_low_freq_filter_btn.toggled],
            fun_to_connect=self._set_min_value_upper_limit,
            fun_args=[self._owner.lower_freq_filter_unit,
                      self._owner.lower_freq_spinbox,
                      self._owner.upper_freq_filter_unit,
                      self._owner.upper_freq_spinbox]
        )

        connect_events_to_func(
            events_to_connect=[self._owner.lower_freq_spinbox.valueChanged,
                               self._owner.upper_freq_spinbox.valueChanged,
                               self._owner.lower_freq_filter_unit.currentTextChanged,
                               self._owner.upper_freq_filter_unit.currentTextChanged,
                               self._owner.activate_low_freq_filter_btn.clicked,
                               self._owner.activate_up_freq_filter_btn.clicked,
                               self._owner.lower_freq_confidence.valueChanged,
                               self._owner.upper_freq_confidence.valueChanged],
            fun_to_connect=self._set_band_filter_label,
            fun_args=[self._owner.activate_low_freq_filter_btn,
                      self._owner.lower_freq_spinbox,
                      self._owner.lower_freq_filter_unit,
                      self._owner.lower_freq_confidence,
                      self._owner.activate_up_freq_filter_btn,
                      self._owner.upper_freq_spinbox,
                      self._owner.upper_freq_filter_unit,
                      self._owner.upper_freq_confidence,
                      self._owner.freq_range_lbl]
        )

        self._owner.activate_low_freq_filter_btn.toggled.connect(
            partial(self._owner.activate_if_toggled,
                    self._owner.activate_low_freq_filter_btn,
                    self._owner.lower_freq_spinbox,
                    self._owner.lower_freq_filter_unit,
                    self._owner.lower_freq_confidence)
        )

        self._owner.activate_up_freq_filter_btn.toggled.connect(
            partial(self._owner.activate_if_toggled,
                    self._owner.activate_up_freq_filter_btn,
                    self._owner.upper_freq_spinbox,
                    self._owner.upper_freq_filter_unit,
                    self._owner.upper_freq_confidence)
        )

    @pyqtSlot()
    def reset(self):
        """Reset the filter screen."""
        self._reset_fb_filters(Ftype.FREQ)

    def _ok(self, signal_name):
        """Evalaute if the signal matches the frequency filters."""
        if not self.apply_remove_btn.isChecked():
            return True
        undef_freq = is_undef_freq(self._owner.db.loc[signal_name])
        if undef_freq:
            if self._owner.include_undef_freqs.isChecked():
                return True
            else:
                return False

        signal_freqs = (
            safe_cast(self._owner.db.at[signal_name, Signal.INF_FREQ], int),
            safe_cast(self._owner.db.at[signal_name, Signal.SUP_FREQ], int)
        )

        band_filter_ok = False
        any_checked = False
        for btn, band_limits in zip(self._frequency_filters_btns, Constants.BANDS):
            if btn.isChecked():
                any_checked = True
                if signal_freqs[0] < band_limits.upper and signal_freqs[1] >= band_limits.lower:
                    band_filter_ok = True
        lower_limit_ok = True
        upper_limit_ok = True
        if self._owner.activate_low_freq_filter_btn.isChecked():
            if not signal_freqs[1] >= filters_limit(self._owner.lower_freq_spinbox,
                                                    self._owner.lower_freq_filter_unit,
                                                    self._owner.lower_freq_confidence, -1):
                lower_limit_ok = False
        if self._owner.activate_up_freq_filter_btn.isChecked():
            if not signal_freqs[0] < filters_limit(self._owner.upper_freq_spinbox,
                                                   self._owner.upper_freq_filter_unit,
                                                   self._owner.upper_freq_confidence):
                upper_limit_ok = False
        if any_checked:
            return band_filter_ok and lower_limit_ok and upper_limit_ok
        else:
            return lower_limit_ok and upper_limit_ok

    def refresh(self):
        """Extend _BaseFilter.refresh."""
        super().refresh()
        self._set_band_filter_label(
            self._owner.activate_low_band_filter_btn,
            self._owner.lower_band_spinbox,
            self._owner.lower_band_filter_unit,
            self._owner.lower_band_confidence,
            self._owner.activate_up_band_filter_btn,
            self._owner.upper_band_spinbox,
            self._owner.upper_band_filter_unit,
            self._owner.upper_band_confidence,
            self._owner.band_range_lbl
        )


class BandFilter(_BaseFilter, _FreqBandMixIn):
    """Band filter class."""
    def __init__(self, owner):
        super().__init__(owner)
        self.apply_remove_btn = self._owner.apply_remove_band_filter_btn
        self.reset_btn = self._owner.reset_band_filters_btn
        connect_events_to_func(
            events_to_connect=[self._owner.lower_band_spinbox.valueChanged,
                               self._owner.upper_band_spinbox.valueChanged,
                               self._owner.lower_band_filter_unit.currentTextChanged,
                               self._owner.upper_band_filter_unit.currentTextChanged,
                               self._owner.activate_low_band_filter_btn.toggled],
            fun_to_connect=self._set_min_value_upper_limit,
            fun_args=[self._owner.lower_band_filter_unit,
                      self._owner.lower_band_spinbox,
                      self._owner.upper_band_filter_unit,
                      self._owner.upper_band_spinbox]
        )

        connect_events_to_func(
            events_to_connect=[self._owner.lower_band_spinbox.valueChanged,
                               self._owner.upper_band_spinbox.valueChanged,
                               self._owner.lower_band_filter_unit.currentTextChanged,
                               self._owner.upper_band_filter_unit.currentTextChanged,
                               self._owner.activate_low_band_filter_btn.clicked,
                               self._owner.activate_up_band_filter_btn.clicked,
                               self._owner.lower_band_confidence.valueChanged,
                               self._owner.upper_band_confidence.valueChanged],
            fun_to_connect=self._set_band_filter_label,
            fun_args=[self._owner.activate_low_band_filter_btn,
                      self._owner.lower_band_spinbox,
                      self._owner.lower_band_filter_unit,
                      self._owner.lower_band_confidence,
                      self._owner.activate_up_band_filter_btn,
                      self._owner.upper_band_spinbox,
                      self._owner.upper_band_filter_unit,
                      self._owner.upper_band_confidence,
                      self._owner.band_range_lbl]
        )

        self._owner.activate_low_band_filter_btn.toggled.connect(
            partial(self._owner.activate_if_toggled,
                    self._owner.activate_low_band_filter_btn,
                    self._owner.lower_band_spinbox,
                    self._owner.lower_band_filter_unit,
                    self._owner.lower_band_confidence)
        )

        self._owner.activate_up_band_filter_btn.toggled.connect(
            partial(self._owner.activate_if_toggled,
                    self._owner.activate_up_band_filter_btn,
                    self._owner.upper_band_spinbox,
                    self._owner.upper_band_filter_unit,
                    self._owner.upper_band_confidence)
        )

        self.apply_remove_btn.set_texts(Constants.APPLY, Constants.REMOVE)
        self.apply_remove_btn.set_slave_filters(
            simple_ones=[
                self._owner.include_undef_bands,
                self._owner.activate_low_band_filter_btn,
                self._owner.activate_up_band_filter_btn
            ],
            radio_1=self._owner.activate_low_band_filter_btn,
            ruled_by_radio_1=[
                self._owner.lower_band_spinbox,
                self._owner.lower_band_filter_unit,
                self._owner.lower_band_confidence
            ],
            radio_2=self._owner.activate_up_band_filter_btn,
            ruled_by_radio_2=[
                self._owner.upper_band_spinbox,
                self._owner.upper_band_filter_unit,
                self._owner.upper_band_confidence
            ]
        )

    @pyqtSlot()
    def reset(self):
        """Reset the filter screen."""
        self._reset_fb_filters(Ftype.BAND)

    def _ok(self, signal_name):
        """Evalaute if the signal matches the band filters."""
        if not self.apply_remove_btn.isChecked():
            return True
        undef_band = is_undef_band(self._owner.db.loc[signal_name])
        if undef_band:
            if self._owner.include_undef_bands.isChecked():
                return True
            else:
                return False

        signal_bands = (
            safe_cast(self._owner.db.at[signal_name, Signal.INF_BAND], int),
            safe_cast(self._owner.db.at[signal_name, Signal.SUP_BAND], int)
        )

        lower_limit_ok = True
        upper_limit_ok = True
        if self._owner.activate_low_band_filter_btn.isChecked():
            if not signal_bands[1] >= filters_limit(self._owner.lower_band_spinbox,
                                                    self._owner.lower_band_filter_unit,
                                                    self._owner.lower_band_confidence, -1):
                lower_limit_ok = False
        if self._owner.activate_up_band_filter_btn.isChecked():
            if not signal_bands[0] < filters_limit(self._owner.upper_band_spinbox,
                                                   self._owner.upper_band_filter_unit,
                                                   self._owner.upper_band_confidence):
                upper_limit_ok = False
        return lower_limit_ok and upper_limit_ok

    def refresh(self):
        """Extend _BaseFilter.refresh."""
        super().refresh()
        self._set_band_filter_label(
            self._owner.activate_low_freq_filter_btn,
            self._owner.lower_freq_spinbox,
            self._owner.lower_freq_filter_unit,
            self._owner.lower_freq_confidence,
            self._owner.activate_up_freq_filter_btn,
            self._owner.upper_freq_spinbox,
            self._owner.upper_freq_filter_unit,
            self._owner.upper_freq_confidence,
            self._owner.freq_range_lbl
        )


class CatFilter(_BaseFilter):
    """Category filter class."""

    def __init__(self, owner):
        super().__init__(owner)
        self.apply_remove_btn = self._owner.apply_remove_cat_filter_btn
        self.reset_btn = self._owner.reset_cat_filters_btn
        # Order matters!
        self._cat_filter_btns = [
            self._owner.military_btn,
            self._owner.radar_btn,
            self._owner.active_btn,
            self._owner.inactive_btn,
            self._owner.ham_btn,
            self._owner.commercial_btn,
            self._owner.aviation_btn,
            self._owner.marine_btn,
            self._owner.analogue_btn,
            self._owner.digital_btn,
            self._owner.trunked_btn,
            self._owner.utility_btn,
            self._owner.sat_btn,
            self._owner.navigation_btn,
            self._owner.interfering_btn,
            self._owner.number_stations_btn,
            self._owner.time_signal_btn
        ]

        self.apply_remove_btn.set_texts(Constants.APPLY, Constants.REMOVE)
        self.apply_remove_btn.set_slave_filters(
            simple_ones=[
                *self._cat_filter_btns,
                self._owner.cat_at_least_one,
                self._owner.cat_all
            ]
        )

    @pyqtSlot()
    def reset(self):
        """Reset the category filter screen."""
        uncheck_and_emit(self.apply_remove_btn)
        for f in self._cat_filter_btns:
            if f.isChecked():
                f.setChecked(False)
        self._owner.cat_at_least_one.setChecked(True)

    def _ok(self, signal_name):
        """Evalaute if the signal matches the category filters."""
        if not self.apply_remove_btn.isChecked():
            return True
        cat_code = self._owner.db.at[signal_name, Signal.CATEGORY_CODE]
        cat_checked = 0
        positive_cases = 0
        for index, cat in enumerate(self._cat_filter_btns):
            if cat.isChecked():
                cat_checked += 1
                if cat_code[index] == '1':
                    positive_cases += 1
        if self._owner.cat_at_least_one.isChecked():
            return positive_cases > 0
        else:
            return cat_checked == positive_cases and cat_checked > 0


class ModeFilter(_BaseFilter):
    """Mode filter class."""

    def __init__(self, owner):
        super().__init__(owner)
        self.apply_remove_btn = self._owner.apply_remove_mode_filter_btn
        self.reset_btn = self._owner.reset_mode_filters_btn
        self._set_mode_tree_widget()
        self._owner.mode_tree_widget.itemSelectionChanged.connect(
            self._manage_mode_selections
        )
        self.apply_remove_btn.set_texts(Constants.APPLY, Constants.REMOVE)
        self.apply_remove_btn.set_slave_filters(
            simple_ones=[
                self._owner.mode_tree_widget,
                self._owner.include_unknown_modes_btn
            ]
        )

    def _manage_mode_selections(self):
        """Rules the selection of childs items of the 'Mode' QTreeWidget.

        If a parent is selected all its children will be selected as well.
        """
        selected_items = self._owner.mode_tree_widget.selectedItems()
        parents = Constants.MODES.keys()
        for parent in parents:
            for item in selected_items:
                if parent == item.text(0):
                    for i in range(len(Constants.MODES[parent])):
                        item.child(i).setSelected(True)

    def _set_mode_tree_widget(self):
        """Construct the QTreeWidget for the 'Mode' screen."""
        for parent, children in Constants.MODES.items():
            iparent = QTreeWidgetItem([parent])
            self._owner.mode_tree_widget.addTopLevelItem(iparent)
            for child in children:
                ichild = QTreeWidgetItem([child])
                iparent.addChild(ichild)
        self._owner.mode_tree_widget.expandAll()

    @pyqtSlot()
    def reset(self):
        """Reset the mode filter screen."""
        uncheck_and_emit(self.apply_remove_btn)
        parents = Constants.MODES.keys()
        selected_children = []
        for item in self._owner.mode_tree_widget.selectedItems():
            if item.text(0) in parents:
                item.setSelected(False)
            else:
                selected_children.append(item)
        for children in selected_children:
            children.setSelected(False)
        if self._owner.include_unknown_modes_btn.isChecked():
            self._owner.include_unknown_modes_btn.setChecked(False)

    def _ok(self, signal_name):
        """Evalaute if the signal matches the mode filters."""
        if not self.apply_remove_btn.isChecked():
            return True
        signal_mode = self._owner.db.at[signal_name, Signal.MODE]
        if signal_mode == Constants.UNKNOWN:
            if self._owner.include_unknown_modes_btn.isChecked():
                return True
            else:
                return False
        selected_items = [item for item in self._owner.mode_tree_widget.selectedItems()]
        selected_items_text = [i.text(0) for i in selected_items]
        parents = [
            item for item in selected_items_text
            if item in Constants.MODES.keys()
        ]
        ok = []
        for item in selected_items:
            if item.text(0) in parents:
                ok.append(item.text(0) in signal_mode)
            elif not item.parent().isSelected():
                ok.append(item.text(0) == signal_mode)
        return any(ok)


class ModulationFilter(_BaseFilter):
    """Modulation filter class."""

    def __init__(self, owner):
        super().__init__(owner)
        self.apply_remove_btn = self._owner.apply_remove_modulation_filter_btn
        self.reset_btn = self._owner.reset_modulation_filters_btn
        self._owner.search_bar_modulation.textEdited.connect(self._show_matching_modulations)
        self.apply_remove_btn.set_texts(Constants.APPLY, Constants.REMOVE)
        self.apply_remove_btn.set_slave_filters(
            simple_ones=[
                self._owner.search_bar_modulation,
                self._owner.modulation_list
            ]
        )
        self._owner.modulation_list.itemClicked.connect(self._remove_if_unselected_modulation)

    @pyqtSlot(QListWidgetItem)
    def _remove_if_unselected_modulation(self, item):
        """If an item is unselected from the modulations list, hide the item."""
        if not item.isSelected():
            self._show_matching_modulations(self.search_bar_modulation.text())

    @pyqtSlot(str)
    def _show_matching_modulations(self, text):
        """Show the modulations which matches 'text'.

        The match criterion is defined in 'show_matching_strings'."""
        show_matching_strings(self._owner.modulation_list, text)

    @pyqtSlot()
    def reset(self):
        """Reset the modulation filter screen."""
        uncheck_and_emit(self.apply_remove_btn)
        self._owner.search_bar_modulation.setText('')
        show_matching_strings(
            self._owner.modulation_list,
            self._owner.search_bar_modulation.text()
        )
        for i in range(self._owner.modulation_list.count()):
            if self._owner.modulation_list.item(i).isSelected():
                self._owner.modulation_list.item(i).setSelected(False)

    def _ok(self, signal_name):
        """Evalaute if the signal matches the modulation filters."""
        if not self.apply_remove_btn.isChecked():
            return True
        signal_modulation = get_field_entries(
            self._owner.db.at[signal_name, Signal.MODULATION]
        )
        for item in self._owner.modulation_list.selectedItems():
            if item.text() in signal_modulation:
                return True
        return False


class LocFilter(_BaseFilter):
    """Location filter class."""

    def __init__(self, owner):
        super().__init__(owner)
        self.apply_remove_btn = self._owner.apply_remove_location_filter_btn
        self.reset_btn = self._owner.reset_location_filters_btn
        self._owner.search_bar_location.textEdited.connect(
            self._show_matching_locations
        )
        self.apply_remove_btn.set_texts(Constants.APPLY, Constants.REMOVE)
        self.apply_remove_btn.set_slave_filters(
            simple_ones=[
                self._owner.search_bar_location,
                self._owner.locations_list
            ]
        )
        self._owner.locations_list.itemClicked.connect(self._remove_if_unselected_location)

    @pyqtSlot(str)
    def _show_matching_locations(self, text):
        """Show the locations which matches 'text'.

        The match criterion is defined in 'show_matching_strings'."""
        show_matching_strings(self._owner.locations_list, text)

    @pyqtSlot(QListWidgetItem)
    def _remove_if_unselected_location(self, item):
        """If an item is unselected from the locations list, hide the item."""
        if not item.isSelected():
            self._show_matching_locations(self._owner.search_bar_location.text())

    @pyqtSlot()
    def reset(self):
        """Reset the location filter screen."""
        uncheck_and_emit(self.apply_remove_btn)
        self._owner.search_bar_location.setText('')
        show_matching_strings(
            self._owner.locations_list,
            self._owner.search_bar_location.text()
        )
        for i in range(self._owner.locations_list.count()):
            if self._owner.locations_list.item(i).isSelected():
                self._owner.locations_list.item(i).setSelected(False)

    def _ok(self, signal_name):
        """Evalaute if the signal matches the location filters."""
        if not self.apply_remove_btn.isChecked():
            return True
        signal_locations = get_field_entries(
            self._owner.db.at[signal_name, Signal.LOCATION]
        )
        for item in self._owner.locations_list.selectedItems():
            if item.text() in signal_locations:
                return True
        return False


class ACFFilter(_BaseFilter):
    """Autocorrelation function filter class."""

    def __init__(self, owner):
        super().__init__(owner)
        self.apply_remove_btn = self._owner.apply_remove_acf_filter_btn
        self.reset_btn = self._owner.reset_acf_filters_btn
        self.apply_remove_btn.set_texts(Constants.APPLY, Constants.REMOVE)
        self.apply_remove_btn.set_slave_filters(
            simple_ones=[
                self._owner.include_undef_acf,
                self._owner.include_variable_acf,
                self._owner.acf_spinbox,
                self._owner.acf_confidence
            ]
        )
        self._owner.acf_info_btn.clicked.connect(lambda: webbrowser.open(Constants.ACF_DOCS))

        connect_events_to_func(
            events_to_connect=[self._owner.acf_spinbox.valueChanged,
                               self._owner.acf_confidence.valueChanged],
            fun_to_connect=self._set_acf_interval_label,
            fun_args=None
        )

    @pyqtSlot()
    def _set_acf_interval_label(self):
        """Display the actual acf interval for the search."""
        tolerance = self._owner.acf_spinbox.value() * self._owner.acf_confidence.value() / 100
        if tolerance > 0:
            val = round(self._owner.acf_spinbox.value() - tolerance, Constants.MAX_DIGITS)
            to_display = f"Selected range:\n\n{val}" + Constants.RANGE_SEPARATOR \
                + f"{round(self._owner.acf_spinbox.value() + tolerance, Constants.MAX_DIGITS)} ms"
        else:
            to_display = f"Selected value:\n\n{self._owner.acf_spinbox.value()} ms"
        self._owner.acf_range_lbl.setText(to_display)
        self._owner.acf_range_lbl.setStyleSheet(f"color: {self._owner.active_color}")

    @pyqtSlot()
    def reset(self):
        """Reset the acf filter screen."""
        uncheck_and_emit(self.apply_remove_btn)
        if self._owner.include_undef_acf.isChecked():
            self._owner.include_undef_acf.setChecked(False)
        if self._owner.include_variable_acf.isChecked():
            self._owner.include_variable_acf.setChecked(False)
        self._owner.acf_spinbox.setValue(50)
        self._owner.acf_confidence.setValue(0)

    def _ok(self, signal_name):
        """Evalaute if the signal matches the acf filters."""
        if not self.apply_remove_btn.isChecked():
            return True
        signal_acf_list = self._owner.db.at[signal_name, Signal.ACF]
        if signal_acf_list[0].unknown:  # Unknown acf are the only acf of the signal.
            if self._owner.include_undef_acf.isChecked():
                return True
            else:
                return False
        else:
            tolerance = self._owner.acf_spinbox.value() * self._owner.acf_confidence.value() / 100
            upper_limit = self._owner.acf_spinbox.value() + tolerance
            lower_limit = self._owner.acf_spinbox.value() - tolerance
            for v in signal_acf_list:
                if v.is_numeric:
                    if lower_limit <= v.numeric_value <= upper_limit:
                        return True
                elif self._owner.include_variable_acf.isChecked():
                    return True
            return False

    def refresh(self):
        """Extend _BaseFilter.refresh."""
        super().refresh()
        self._set_acf_interval_label()


class Filters(QObject):
    """Global filter class.

    Provides the information about all the filters. Its only public attribute
    is filters, which is a dictionary containing instances of all the filters.
    The only exposed methods are reset(), ok(signal_name) and refresh().
    The class also connects the apply and reset buttons to the relevant functions."""

    def __init__(self, owner):
        super().__init__()
        self.filters = {
            "freq_filter": FreqFilter(owner),
            "band_filter": BandFilter(owner),
            "cat_filter": CatFilter(owner),
            "mode_filter": ModeFilter(owner),
            "modulation_filter": ModulationFilter(owner),
            "location_filter": LocFilter(owner),
            "acf_filter": ACFFilter(owner),
        }
        self._owner = owner
        self._owner.reset_filters_btn.clicked.connect(self._reset)

        # Connect Apply and Reset buttons clicks to functions.
        for f in self._values:
            f.apply_remove_btn.clicked.connect(self._display_signals)
            f.reset_btn.clicked.connect(f.reset)

    @property
    def _values(self):
        return self.filters.values()

    @pyqtSlot()
    def _display_signals(self):
        self._owner.display_signals()

    @pyqtSlot()
    def _reset(self):
        """Reset all the filters."""
        for f in self._values:
            f.reset()

    def ok(self, signal_name):
        """Check whether all the filters are passed."""
        return all(f._ok(signal_name) for f in self._values)

    def refresh(self):
        """Refresh the relevant widgets when changing theme."""
        for f in self._values:
            f.refresh()
