# -*- coding: utf-8 -*-
"""
Service parameter based filtering widget
"""

# .. note:: This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

__author__ = 'Original authors: Mario Locati, Roberto Vallone, Matteo Ghetta, Nyall Dawson'
__date__ = '29/01/2020'
__copyright__ = 'Istituto Nazionale di Geofisica e Vulcanologia (INGV)'
# This will get replaced with a git SHA1 when you do a git archive
__revision__ = '$Format:%H$'

from copy import deepcopy
from typing import Optional, List, Dict

from qgis.PyQt import uic
from qgis.PyQt.QtCore import (
    pyqtSignal,
    Qt,
    QDateTime
)
from qgis.PyQt.QtWidgets import QWidget
from qgis.core import (
    QgsProject,
    QgsCoordinateReferenceSystem,
    QgsCoordinateTransform,
    QgsCsException,
    QgsSettings,
    QgsUnitTypes,
    QgsRectangle,
    QgsPointXY
)
from qgis.gui import (
    QgsMapToolExtent,
    QgsMapToolEmitPoint,
)

from qquake.gui.base_filter_widget import BaseFilterWidget
from qquake.gui.gui_utils import GuiUtils
from qquake.gui.predefined_areas_dialog import PredefinedAreasDialog
from qquake.quakeml.common import EVENT_TYPES
from qquake.services import SERVICE_MANAGER

FORM_CLASS, _ = uic.loadUiType(GuiUtils.get_ui_file_path('filter_parameter_widget_base.ui'))

class FilterParameterWidget(QWidget, FORM_CLASS, BaseFilterWidget):  # pylint: disable=too-many-public-methods
    """
    Service parameter based filtering widget
    """
    changed = pyqtSignal()

    def __init__(self,  # pylint: disable=too-many-statements
                 iface,
                 service_type: str,
                 parent: Optional[QWidget] = None):
        """Constructor."""
        super().__init__(parent)

        self.setupUi(self)

        self.scroll_area.setStyleSheet("""
            QScrollArea { background: transparent; }
            QScrollArea > QWidget > QWidget { background: transparent; }
            QScrollArea > QWidget > QScrollBar { background: 1; }
        """)

        for m in range(1, 13):
            self.earthquake_max_intensity_greater_combo.addItem(str(m), m)

        self.iface = iface
        self.previous_map_tool = None
        self.extent_tool = None
        self.customize_areas_dialog = None

        self.set_extent_from_canvas_extent(self.iface.mapCanvas().extent())
        self.set_center_from_canvas_point(self.iface.mapCanvas().extent().center())

        self.fdsn_event_start_date.dateTimeChanged.connect(self._refresh_date)
        self.min_time_check.toggled.connect(self.changed)
        self.fdsn_event_start_date.dateTimeChanged.connect(self.changed)
        self.max_time_check.toggled.connect(self.changed)
        self.fdsn_event_end_date.dateTimeChanged.connect(self.changed)
        self.min_mag_check.toggled.connect(self.changed)
        self.fdsn_event_min_magnitude.valueChanged.connect(self.changed)
        self.max_mag_check.toggled.connect(self.changed)
        self.fdsn_event_max_magnitude.valueChanged.connect(self.changed)
        self.lat_min_spinbox.valueChanged.connect(self.changed)
        self.lat_max_spinbox.valueChanged.connect(self.changed)
        self.long_min_spinbox.valueChanged.connect(self.changed)
        self.long_max_spinbox.valueChanged.connect(self.changed)
        self.lat_min_checkbox.toggled.connect(self.changed)
        self.lat_max_checkbox.toggled.connect(self.changed)
        self.long_min_checkbox.toggled.connect(self.changed)
        self.long_max_checkbox.toggled.connect(self.changed)
        self.limit_extent_checkbox.toggled.connect(self.changed)
        self.radio_rectangular_area.toggled.connect(self.changed)
        self.radio_circular_area.toggled.connect(self.changed)
        self.radio_predefined_area.toggled.connect(self.changed)
        self.circular_lat_spinbox.valueChanged.connect(self.changed)
        self.circular_long_spinbox.valueChanged.connect(self.changed)
        self.radius_min_checkbox.toggled.connect(self.changed)
        self.radius_max_checkbox.toggled.connect(self.changed)
        self.radius_min_spinbox.valueChanged.connect(self.changed)
        self.radius_max_spinbox.valueChanged.connect(self.changed)
        self.radius_unit_combobox.currentIndexChanged.connect(self.changed)
        self.earthquake_max_intensity_greater_check.toggled.connect(self.changed)
        self.earthquake_max_intensity_greater_combo.currentIndexChanged.connect(self.changed)
        self.earthquake_number_mdps_greater_check.toggled.connect(self.changed)
        self.earthquake_number_mdps_greater_spin.valueChanged.connect(self.changed)
        self.output_table_options_widget.changed.connect(self.changed)
        self.event_type_check.toggled.connect(self.changed)
        self.event_type_combo.currentIndexChanged.connect(self.changed)
        self.events_updated_after_check.toggled.connect(self.changed)
        self.events_updated_after.dateChanged.connect(self.changed)

        self.rect_extent_draw_on_map.clicked.connect(self.draw_rect_on_map)
        self.circle_center_draw_on_map.clicked.connect(self.draw_center_on_map)

        self.radio_rectangular_area.toggled.connect(self._enable_widgets)
        self.radio_circular_area.toggled.connect(self._enable_widgets)
        self.radio_predefined_area.toggled.connect(self._enable_widgets)
        self.limit_extent_checkbox.toggled.connect(self._enable_widgets)
        self.lat_min_checkbox.toggled.connect(self._enable_widgets)
        self.lat_max_checkbox.toggled.connect(self._enable_widgets)
        self.long_min_checkbox.toggled.connect(self._enable_widgets)
        self.long_max_checkbox.toggled.connect(self._enable_widgets)
        self.radius_min_checkbox.toggled.connect(self._enable_widgets)
        self.radius_max_checkbox.toggled.connect(self._enable_widgets)
        self.min_time_check.toggled.connect(self._enable_widgets)
        self.max_time_check.toggled.connect(self._enable_widgets)
        self.min_mag_check.toggled.connect(self._enable_widgets)
        self.max_mag_check.toggled.connect(self._enable_widgets)
        self.output_table_options_widget.changed.connect(self._enable_widgets)
        self.earthquake_max_intensity_greater_check.toggled.connect(self._enable_widgets)
        self.earthquake_number_mdps_greater_check.toggled.connect(self._enable_widgets)
        self._enable_widgets()

        self._populated_predefined_areas()
        SERVICE_MANAGER.areasChanged.connect(self._populated_predefined_areas)
        self.combo_predefined_area.currentIndexChanged.connect(self._use_predefined_area)
        self.radio_predefined_area.toggled.connect(self._use_predefined_area)

        self.button_customize_areas.clicked.connect(self._customize_areas)

        self.event_type_check.toggled.connect(self._enable_widgets)
        for event_type in EVENT_TYPES:
            self.event_type_combo.addItem(event_type, event_type)

        self.events_updated_after_check.toggled.connect(self._enable_widgets)

        self.service_type = None
        self.service_id = None
        self.set_service_type(service_type)

    def is_valid(self) -> bool:
        for check in [self.min_time_check, self.max_time_check, self.min_mag_check, self.max_mag_check,
                      self.limit_extent_checkbox, self.earthquake_max_intensity_greater_check,
                      self.earthquake_number_mdps_greater_check]:
            if check.isChecked():
                return True

        return False

    def set_service_type(self, service_type: str):
        self.service_type = service_type

        self.time_coverage_group.setVisible(
            self.service_type in (SERVICE_MANAGER.MACROSEISMIC, SERVICE_MANAGER.FDSNEVENT, SERVICE_MANAGER.FDSNSTATION))
        self.magnitude_group.setVisible(self.service_type in (SERVICE_MANAGER.MACROSEISMIC, SERVICE_MANAGER.FDSNEVENT))
        self.macroseismic_data_group.setVisible(self.service_type == SERVICE_MANAGER.MACROSEISMIC)

        self.output_table_options_widget.set_service_type(service_type)

    def set_service_id(self, service_id: str):  # pylint:disable=too-many-branches
        self.service_id = service_id

        service_config = SERVICE_MANAGER.service_details(self.service_type, self.service_id)

        self.output_table_options_widget.set_service_id(service_id)

        if not service_config['settings'].get('querycircular', False):
            if self.radio_circular_area.isChecked():
                self.radio_rectangular_area.setChecked(True)
            self.radio_circular_area.setEnabled(False)
        else:
            self.radio_circular_area.setEnabled(True)

        if not service_config['settings'].get('queryeventtype', False):
            if self.event_type_check.isChecked():
                self.event_type_check.setChecked(False)
            self.event_type_check.setEnabled(False)
        else:
            self.event_type_check.setEnabled(True)

        if not service_config['settings'].get('queryupdatedafter', False):
            if self.events_updated_after_check.isChecked():
                self.events_updated_after_check.setChecked(False)
            self.events_updated_after_check.setEnabled(False)
        else:
            self.events_updated_after_check.setEnabled(True)

        self.radius_unit_combobox.clear()
        self.radius_unit_combobox.addItem(self.tr('Degrees'), QgsUnitTypes.DistanceDegrees)
        if service_config['settings'].get('querycircularradiuskm', False):
            self.radius_unit_combobox.insertItem(0, self.tr('Kilometers'), QgsUnitTypes.DistanceKilometers)
        self.radius_unit_combobox.setCurrentIndex(0)

    def restore_settings(self, prefix: str):  # pylint: disable=too-many-locals,too-many-branches,too-many-statements
        s = QgsSettings()

        if self.service_id:
            service_config = SERVICE_MANAGER.service_details(self.service_type, self.service_id)
        else:
            service_config = None

        last_event_start_date = s.value('/plugins/qquake/{}_last_event_start_date'.format(prefix))
        if last_event_start_date is not None:
            self.fdsn_event_start_date.setDateTime(last_event_start_date)
        last_event_end_date = s.value('/plugins/qquake/{}_last_event_end_date'.format(prefix))
        if last_event_end_date is not None:
            self.fdsn_event_end_date.setDateTime(last_event_end_date)
        last_event_min_magnitude = s.value('/plugins/qquake/{}_last_event_min_magnitude'.format(prefix))
        if last_event_min_magnitude is not None:
            self.fdsn_event_min_magnitude.setValue(float(last_event_min_magnitude))
        last_event_max_magnitude = s.value('/plugins/qquake/{}_last_event_max_magnitude'.format(prefix))
        if last_event_max_magnitude is not None:
            self.fdsn_event_max_magnitude.setValue(float(last_event_max_magnitude))
        self.limit_extent_checkbox.setChecked(
            s.value('/plugins/qquake/{}_last_event_extent_enabled2'.format(prefix), False, bool))
        self.radio_predefined_area.setChecked(
            s.value('/plugins/qquake/{}_last_event_extent_named'.format(prefix), False, bool))
        last_area = s.value('/plugins/qquake/{}_last_event_extent_name'.format(prefix), '', str)
        if last_area:
            self.combo_predefined_area.setCurrentIndex(self.combo_predefined_area.findData(last_area))
        self.radio_rectangular_area.setChecked(
            s.value('/plugins/qquake/{}_last_event_extent_rect2'.format(prefix), False, bool))

        if not service_config or service_config['settings'].get('querycircular', False):
            self.radio_circular_area.setChecked(
                s.value('/plugins/qquake/{}_last_event_extent_circle2'.format(prefix), False, bool))
        self.lat_min_checkbox.setChecked(
            s.value('/plugins/qquake/{}_last_event_min_lat_checked2'.format(prefix), False, bool))
        self.lat_max_checkbox.setChecked(
            s.value('/plugins/qquake/{}_last_event_max_lat_checked2'.format(prefix), False, bool))
        self.long_min_checkbox.setChecked(
            s.value('/plugins/qquake/{}_last_event_min_long_checked2'.format(prefix), False, bool))
        self.long_max_checkbox.setChecked(
            s.value('/plugins/qquake/{}_last_event_max_long_checked2'.format(prefix), False, bool))

        self.radius_min_checkbox.setChecked(
            s.value('/plugins/qquake/{}_last_event_circle_radius_min_checked2'.format(prefix), False, bool))
        self.radius_max_checkbox.setChecked(
            s.value('/plugins/qquake/{}_last_event_circle_radius_max_checked2'.format(prefix), False, bool))

        self.radius_unit_combobox.setCurrentIndex(
            max(0, self.radius_unit_combobox.findData(
                s.value('/plugins/qquake/{}_last_event_circle_unit'.format(prefix),
                        int(QgsUnitTypes.DistanceKilometers), int))))

        last_event_min_lat = s.value('/plugins/qquake/{}_last_event_min_lat'.format(prefix))
        if last_event_min_lat is not None:
            self.lat_min_spinbox.setValue(float(last_event_min_lat))
        last_event_max_lat = s.value('/plugins/qquake/{}_last_event_max_lat'.format(prefix))
        if last_event_max_lat is not None:
            self.lat_max_spinbox.setValue(float(last_event_max_lat))
        last_event_min_long = s.value('/plugins/qquake/{}_last_event_min_long'.format(prefix))
        if last_event_min_long is not None:
            self.long_min_spinbox.setValue(float(last_event_min_long))
        last_event_max_long = s.value('/plugins/qquake/{}_last_event_max_long'.format(prefix))
        if last_event_max_long is not None:
            self.long_max_spinbox.setValue(float(last_event_max_long))

        last_event_circle_lat = s.value('/plugins/qquake/{}_last_event_circle_lat'.format(prefix))
        if last_event_circle_lat is not None:
            self.circular_lat_spinbox.setValue(float(last_event_circle_lat))
        last_event_circle_long = s.value('/plugins/qquake/{}_last_event_circle_long'.format(prefix))
        if last_event_circle_long is not None:
            self.circular_long_spinbox.setValue(float(last_event_circle_long))

        last_event_min_radius = s.value('/plugins/qquake/{}_last_event_circle_min_radius'.format(prefix))
        if last_event_min_radius is not None:
            self.radius_min_spinbox.setValue(float(last_event_min_radius))
        last_event_max_radius = s.value('/plugins/qquake/{}_last_event_circle_max_radius'.format(prefix))
        if last_event_max_radius is not None:
            self.radius_max_spinbox.setValue(float(last_event_max_radius))

        self.min_time_check.setChecked(
            s.value('/plugins/qquake/{}_last_event_min_time_checked2'.format(prefix), False, bool))
        self.max_time_check.setChecked(
            s.value('/plugins/qquake/{}_last_event_max_time_checked2'.format(prefix), False, bool))
        self.min_mag_check.setChecked(
            s.value('/plugins/qquake/{}_last_event_min_mag_checked2'.format(prefix), False, bool))
        self.max_mag_check.setChecked(
            s.value('/plugins/qquake/{}_last_event_max_mag_checked2'.format(prefix), False, bool))
        self.earthquake_max_intensity_greater_check.setChecked(
            s.value('/plugins/qquake/{}_last_event_max_intensity_greater_checked2'.format(prefix), False, bool))
        v = s.value('/plugins/qquake/{}_last_event_max_intensity_greater'.format(prefix))
        if v is not None:
            self.earthquake_max_intensity_greater_combo.setCurrentIndex(
                self.earthquake_max_intensity_greater_combo.findData(float(v)))

        self.earthquake_number_mdps_greater_check.setChecked(
            s.value('/plugins/qquake/{}_last_event_mdps_greater_checked2'.format(prefix), False, bool))
        v = s.value('/plugins/qquake/{}_last_event_mdps_greater'.format(prefix))
        if v is not None:
            self.earthquake_number_mdps_greater_spin.setValue(int(v))

        self.output_table_options_widget.restore_settings(prefix, 'last')

        self.event_type_check.setChecked(
            s.value('/plugins/qquake/{}_last_event_type_checked'.format(prefix), False, bool))
        last_event_type = s.value('/plugins/qquake/{}_last_event_type'.format(prefix), '', str)
        if last_event_type is not None:
            self.event_type_combo.setCurrentIndex(self.event_type_combo.findData(last_event_type))

        self.events_updated_after_check.setChecked(
            s.value('/plugins/qquake/{}_events_updated_after_checked'.format(prefix), False, bool))
        last_updated_after_date = s.value('/plugins/qquake/{}_events_updated_after_date'.format(prefix))
        if last_updated_after_date is not None:
            self.events_updated_after.setDateTime(last_updated_after_date)

    def save_settings(self, prefix: str):
        s = QgsSettings()
        s.setValue('/plugins/qquake/{}_last_event_start_date'.format(prefix), self.fdsn_event_start_date.dateTime())
        s.setValue('/plugins/qquake/{}_last_event_end_date'.format(prefix), self.fdsn_event_end_date.dateTime())
        s.setValue('/plugins/qquake/{}_last_event_min_magnitude'.format(prefix), self.fdsn_event_min_magnitude.value())
        s.setValue('/plugins/qquake/{}_last_event_max_magnitude'.format(prefix), self.fdsn_event_max_magnitude.value())

        s.setValue('/plugins/qquake/{}_last_event_extent_enabled2'.format(prefix),
                   self.limit_extent_checkbox.isChecked())
        s.setValue('/plugins/qquake/{}_last_event_extent_named'.format(prefix), self.radio_predefined_area.isChecked())
        s.setValue('/plugins/qquake/{}_last_event_extent_name'.format(prefix), self.combo_predefined_area.currentData())

        s.setValue('/plugins/qquake/{}_last_event_extent_rect2'.format(prefix), self.radio_rectangular_area.isChecked())
        s.setValue('/plugins/qquake/{}_last_event_extent_circle2'.format(prefix), self.radio_circular_area.isChecked())
        s.setValue('/plugins/qquake/{}_last_event_min_lat_checked2'.format(prefix), self.lat_min_checkbox.isChecked())
        s.setValue('/plugins/qquake/{}_last_event_min_lat'.format(prefix), self.lat_min_spinbox.value())
        s.setValue('/plugins/qquake/{}_last_event_max_lat_checked2'.format(prefix), self.lat_max_checkbox.isChecked())
        s.setValue('/plugins/qquake/{}_last_event_max_lat'.format(prefix), self.lat_max_spinbox.value())
        s.setValue('/plugins/qquake/{}_last_event_min_long_checked2'.format(prefix), self.long_min_checkbox.isChecked())
        s.setValue('/plugins/qquake/{}_last_event_min_long'.format(prefix), self.long_min_spinbox.value())
        s.setValue('/plugins/qquake/{}_last_event_max_long_checked2'.format(prefix), self.long_max_checkbox.isChecked())
        s.setValue('/plugins/qquake/{}_last_event_max_long'.format(prefix), self.long_max_spinbox.value())

        s.setValue('/plugins/qquake/{}_last_event_circle_long'.format(prefix), self.circular_long_spinbox.value())
        s.setValue('/plugins/qquake/{}_last_event_circle_lat'.format(prefix), self.circular_lat_spinbox.value())
        s.setValue('/plugins/qquake/{}_last_event_circle_radius_min_checked2'.format(prefix),
                   self.radius_min_checkbox.isChecked())
        s.setValue('/plugins/qquake/{}_last_event_circle_radius_max_checked2'.format(prefix),
                   self.radius_max_checkbox.isChecked())
        s.setValue('/plugins/qquake/{}_last_event_circle_min_radius'.format(prefix), self.radius_min_spinbox.value())
        s.setValue('/plugins/qquake/{}_last_event_circle_max_radius'.format(prefix), self.radius_max_spinbox.value())
        s.setValue('/plugins/qquake/{}_last_event_circle_unit'.format(prefix),
                   int(self.radius_unit_combobox.currentData()))

        s.setValue('/plugins/qquake/{}_last_event_max_intensity_greater_checked2'.format(prefix),
                   self.earthquake_max_intensity_greater_check.isChecked())
        s.setValue('/plugins/qquake/{}_last_event_max_intensity_greater'.format(prefix),
                   self.earthquake_max_intensity_greater_combo.currentData())
        s.setValue('/plugins/qquake/{}_last_event_mdps_greater_checked2'.format(prefix),
                   self.earthquake_number_mdps_greater_check.isChecked())
        s.setValue('/plugins/qquake/{}_last_event_mdps_greater'.format(prefix),
                   self.earthquake_number_mdps_greater_spin.value())

        s.setValue('/plugins/qquake/{}_last_event_min_time_checked2'.format(prefix), self.min_time_check.isChecked())
        s.setValue('/plugins/qquake/{}_last_event_max_time_checked2'.format(prefix), self.max_time_check.isChecked())
        s.setValue('/plugins/qquake/{}_last_event_min_mag_checked2'.format(prefix), self.min_mag_check.isChecked())
        s.setValue('/plugins/qquake/{}_last_event_max_mag_checked2'.format(prefix), self.max_mag_check.isChecked())

        self.output_table_options_widget.save_settings(prefix, 'last')

        s.setValue('/plugins/qquake/{}_last_event_type'.format(prefix), self.event_type_combo.currentData())
        s.setValue('/plugins/qquake/{}_last_event_type_checked'.format(prefix),
                   self.event_type_check.isChecked())

        s.setValue('/plugins/qquake/{}_events_updated_after_date'.format(prefix), self.events_updated_after.dateTime())
        s.setValue('/plugins/qquake/{}_events_updated_after_checked'.format(prefix),
                   self.events_updated_after_check.isChecked())

    def _populated_predefined_areas(self):
        """
        Populates the predefined area choices
        """
        self.combo_predefined_area.clear()
        for name in SERVICE_MANAGER.available_predefined_bounding_boxes():
            extent = SERVICE_MANAGER.predefined_bounding_box(name)
            self.combo_predefined_area.addItem(extent['title'], name)

    def _use_predefined_area(self):
        """
        Retrieves the selected predefined area details and updates the widget state accordingly
        """
        if not self.radio_predefined_area.isChecked():
            return

        selected_extent_id = self.combo_predefined_area.currentData()
        if selected_extent_id not in SERVICE_MANAGER.available_predefined_bounding_boxes():
            return

        extent = SERVICE_MANAGER.predefined_bounding_box(selected_extent_id)['boundingbox']
        self.lat_min_spinbox.setValue(extent[1])
        self.lat_max_spinbox.setValue(extent[3])
        self.long_min_spinbox.setValue(extent[0])
        self.long_max_spinbox.setValue(extent[2])
        self.lat_max_checkbox.setChecked(True)
        self.long_max_checkbox.setChecked(True)
        self.lat_min_checkbox.setChecked(True)
        self.long_min_checkbox.setChecked(True)

    def set_extent_from_canvas_extent(self, rect: QgsRectangle):
        """
        Sets the widget extent from the canvas extent
        """
        ct = QgsCoordinateTransform(self.iface.mapCanvas().mapSettings().destinationCrs(),
                                    QgsCoordinateReferenceSystem('EPSG:4326'), QgsProject.instance())
        try:
            rect = ct.transformBoundingBox(rect)
            self.lat_min_spinbox.setValue(rect.yMinimum())
            self.lat_max_spinbox.setValue(rect.yMaximum())
            self.long_min_spinbox.setValue(rect.xMinimum())
            self.long_max_spinbox.setValue(rect.xMaximum())
        except QgsCsException:
            pass

    def set_center_from_canvas_point(self, point: QgsPointXY):
        """
        Sets the widget center point from a canvas point
        """
        ct = QgsCoordinateTransform(self.iface.mapCanvas().mapSettings().destinationCrs(),
                                    QgsCoordinateReferenceSystem('EPSG:4326'), QgsProject.instance())
        try:
            point = ct.transform(point)
            self.circular_lat_spinbox.setValue(point.y())
            self.circular_long_spinbox.setValue(point.x())
        except QgsCsException:
            pass

    def _enable_widgets(self):
        """
        Selectively enables widgets based on overall state
        """
        for w in [self.lat_min_checkbox,
                  self.lat_max_checkbox,
                  self.long_min_checkbox,
                  self.long_max_checkbox,
                  self.lat_min_spinbox,
                  self.lat_max_spinbox,
                  self.long_min_spinbox,
                  self.long_max_spinbox,
                  self.label_rect_lat,
                  self.label_rect_long,
                  self.rect_extent_draw_on_map]:
            w.setEnabled(self.radio_rectangular_area.isChecked() and self.limit_extent_checkbox.isChecked())
        self.lat_min_spinbox.setEnabled(self.lat_min_spinbox.isEnabled() and self.lat_min_checkbox.isChecked())
        self.lat_max_spinbox.setEnabled(self.lat_max_spinbox.isEnabled() and self.lat_max_checkbox.isChecked())
        self.long_min_spinbox.setEnabled(self.long_min_spinbox.isEnabled() and self.long_min_checkbox.isChecked())
        self.long_max_spinbox.setEnabled(self.long_max_spinbox.isEnabled() and self.long_max_checkbox.isChecked())

        self.combo_predefined_area.setEnabled(self.radio_predefined_area.isChecked())

        for w in [self.circular_lat_spinbox,
                  self.circular_long_spinbox,
                  self.radius_min_checkbox,
                  self.radius_min_spinbox,
                  self.radius_max_checkbox,
                  self.radius_max_spinbox,
                  self.label_circ_center,
                  self.label_circ_radius,
                  self.label_circ_lat,
                  self.label_circ_long,
                  self.circle_center_draw_on_map,
                  self.radius_unit_combobox]:
            w.setEnabled(self.radio_circular_area.isChecked() and self.limit_extent_checkbox.isChecked())
        self.radius_min_spinbox.setEnabled(self.radius_min_spinbox.isEnabled() and self.radius_min_checkbox.isChecked())
        self.radius_max_spinbox.setEnabled(self.radius_max_spinbox.isEnabled() and self.radius_max_checkbox.isChecked())

        self.fdsn_event_start_date.setEnabled(self.min_time_check.isChecked())
        self.fdsn_event_end_date.setEnabled(self.max_time_check.isChecked())
        self.fdsn_event_min_magnitude.setEnabled(self.min_mag_check.isChecked())
        self.fdsn_event_max_magnitude.setEnabled(self.max_mag_check.isChecked())

        self.earthquake_max_intensity_greater_combo.setEnabled(self.earthquake_max_intensity_greater_check.isChecked())
        self.earthquake_number_mdps_greater_spin.setEnabled(self.earthquake_number_mdps_greater_check.isChecked())

        self.event_type_combo.setEnabled(self.event_type_check.isChecked())
        self.events_updated_after.setEnabled(self.events_updated_after_check.isChecked())

    def draw_rect_on_map(self):
        """
        Triggers drawing an extent rectangle on the map
        """
        self.previous_map_tool = self.iface.mapCanvas().mapTool()
        if not self.extent_tool:
            self.extent_tool = QgsMapToolExtent(self.iface.mapCanvas())
            self.extent_tool.extentChanged.connect(self.extent_drawn)
            self.extent_tool.deactivated.connect(self.deactivate_tool)
        self.iface.mapCanvas().setMapTool(self.extent_tool)
        self.window().setVisible(False)

    def draw_center_on_map(self):
        """
        Triggers picking a center point from the map
        """
        self.previous_map_tool = self.iface.mapCanvas().mapTool()
        if not self.extent_tool:
            self.extent_tool = QgsMapToolEmitPoint(self.iface.mapCanvas())
            self.extent_tool.canvasClicked.connect(self.center_picked)
            self.extent_tool.deactivated.connect(self.deactivate_tool)
        self.iface.mapCanvas().setMapTool(self.extent_tool)
        self.window().setVisible(False)

    def extent_drawn(self, extent: QgsRectangle):
        """
        Called after an extent is drawn on the canvas
        """
        self.set_extent_from_canvas_extent(extent)
        self.iface.mapCanvas().setMapTool(self.previous_map_tool)
        self.window().setVisible(True)
        self.previous_map_tool = None
        self.extent_tool = None

    def center_picked(self, point: QgsPointXY, button):  # pylint: disable=unused-argument
        """
        Called after a center point is picked on the canvas
        """
        self.set_center_from_canvas_point(point)
        self.iface.mapCanvas().setMapTool(self.previous_map_tool)
        self.window().setVisible(True)
        self.previous_map_tool = None
        self.extent_tool = None

    def deactivate_tool(self):
        """
        Called when the current tool should be deactivated
        """
        self.window().setVisible(True)
        self.previous_map_tool = None
        self.extent_tool = None

    def _refresh_date(self):
        """
        Avoids negative date intervals by checking start_date > end_date
        """

        if self.fdsn_event_start_date.dateTime() > self.fdsn_event_end_date.dateTime():
            self.fdsn_event_end_date.setDate(self.fdsn_event_start_date.date())

    def set_date_range_limits(self, date_start: Optional[QDateTime] = None, date_end: Optional[QDateTime] = None):
        """
        Sets the widget date range limits
        """
        if date_start:
            self.fdsn_event_start_date.setMinimumDateTime(date_start)

            self.fdsn_event_start_date.setDateTime(date_start)
            self.min_time_check.setText(
                self.tr("Start (from {})").format(date_start.toString('yyyy-MM-dd hh:mm:ss')))
        else:
            self.fdsn_event_start_date.clearMinimumDate()

            self.min_time_check.setText(self.tr("Start"))

        if date_end:
            self.fdsn_event_start_date.setMaximumDateTime(date_end)

            self.max_time_check.setText(
                self.tr("End (until {})").format(date_end.toString('yyyy-MM-dd hh:mm:ss')))
        else:
            self.fdsn_event_start_date.clearMaximumDate()

            self.max_time_check.setText(self.tr("End"))

        if date_start:
            self.fdsn_event_end_date.setMinimumDateTime(date_start)
        else:
            self.fdsn_event_end_date.clearMinimumDate()
        if date_end:
            self.fdsn_event_end_date.setMaximumDateTime(date_end)
        else:
            self.fdsn_event_end_date.clearMaximumDate()

        # just make a week difference from START date
        if date_start:
            self.fdsn_event_end_date.setDateTime(date_start.addDays(7))

    def set_current_date_range(self, date_start: Optional[QDateTime] = None, date_end: Optional[QDateTime] = None):
        """
        Sets the current widget date range
        """
        if date_start is not None:
            self.fdsn_event_start_date.setDateTime(date_start)
            self.min_time_check.setChecked(True)
        else:
            self.min_time_check.setChecked(False)

        if date_end is not None:
            self.fdsn_event_end_date.setDateTime(date_end)
            self.max_time_check.setChecked(True)
        else:
            self.max_time_check.setChecked(False)

    def set_predefined_bounding_box(self, box):
        """
        Sets the current predefined bounding box
        """
        self.radio_predefined_area.setChecked(True)
        self.combo_predefined_area.setCurrentIndex(self.combo_predefined_area.findData(box))

    def set_min_latitude(self, lat: float):
        """
        Sets minimum latitude
        """
        self.radio_rectangular_area.setChecked(True)
        self.lat_min_spinbox.setValue(float(lat))

    def set_max_latitude(self, lat: float):
        """
        Sets maximum latitude
        """
        self.radio_rectangular_area.setChecked(True)
        self.lat_max_spinbox.setValue(float(lat))

    def set_min_longitude(self, long: float):
        """
        Sets minimum longitude
        """
        self.radio_rectangular_area.setChecked(True)
        self.long_min_spinbox.setValue(float(long))

    def set_max_longitude(self, long: float):
        """
        Sets the maximum longitude
        """
        self.radio_rectangular_area.setChecked(True)
        self.long_max_spinbox.setValue(float(long))

    def set_circle_longitude(self, long: float):
        """
        Sets the circle center longitude
        """
        self.radio_circular_area.setChecked(True)
        self.circular_long_spinbox.setValue(float(long))

    def set_circle_latitude(self, lat: float):
        """
        Sets the circle center latitude
        """
        self.radio_circular_area.setChecked(True)
        self.circular_lat_spinbox.setValue(float(lat))

    def set_min_circle_radius(self, radius: float):
        """
        Sets the minimum circle radius
        """
        self.radius_min_checkbox.setChecked(True)
        self.radius_min_spinbox.setValue(float(radius))

    def set_max_circle_radius(self, radius: float):
        """
        Sets the maximum circle radius
        """
        self.radius_max_checkbox.setChecked(True)
        self.radius_max_spinbox.setValue(float(radius))

    def set_min_magnitude(self, mag: Optional[float]):
        """
        Sets the minimum magnitude filter value
        """
        self.min_mag_check.setChecked(mag is not None)
        if mag is not None:
            self.fdsn_event_min_magnitude.setValue(float(mag))

    def set_max_magnitude(self, mag: Optional[float]):
        """
        Sets the maximum magnitude filter value
        """
        self.max_mag_check.setChecked(mag is not None)
        if mag is not None:
            self.fdsn_event_max_magnitude.setValue(float(mag))

    def set_max_intensity_greater(self, intensity: Optional[float]):
        """
        Sets the intensity greater than filter value
        """
        self.earthquake_max_intensity_greater_check.setChecked(intensity is not None)
        if intensity is not None:
            self.earthquake_max_intensity_greater_combo.setCurrentIndex(
                self.earthquake_max_intensity_greater_combo.findData(float(intensity)))

    def set_mdps_greater_than(self, mdps: Optional[float]):
        """
        Sets the MDPs greater than filter value
        """
        self.earthquake_number_mdps_greater_check.setChecked(mdps is not None)
        if mdps is not None:
            self.earthquake_number_mdps_greater_spin.setValue(float(mdps))

    def set_event_type(self, event_type: Optional[str]):
        """
        Sets the event type than filter value
        """
        self.event_type_check.setChecked(event_type is not None)
        if event_type is not None:
            self.event_type_combo.setCurrentIndex(self.event_type_combo.findData(event_type))

    def set_updated_after(self, after: Optional[QDateTime]):
        """
        Sets the updated after date
        """
        self.events_updated_after_check.setChecked(after is not None)
        if after is not None:
            self.events_updated_after.setDateTime(after)

    def set_extent_limit(self, box: List[float]):
        """
        Sets the extent limits
        """
        self.long_min_spinbox.setMinimum(box[0])
        self.long_max_spinbox.setMinimum(box[0])
        self.lat_min_spinbox.setMinimum(box[1])
        self.lat_max_spinbox.setMinimum(box[1])
        self.long_min_spinbox.setMaximum(box[2])
        self.long_max_spinbox.setMaximum(box[2])
        self.lat_min_spinbox.setMaximum(box[3])
        self.lat_max_spinbox.setMaximum(box[3])

        # reset previously selected predefined area, now that new min/max area has been applied
        self._use_predefined_area()

    def start_date(self) -> Optional[QDateTime]:
        """
        Returns the start date
        """
        if self.service_type not in (
                SERVICE_MANAGER.MACROSEISMIC, SERVICE_MANAGER.FDSNEVENT, SERVICE_MANAGER.FDSNSTATION):
            return None

        return self.fdsn_event_start_date.dateTime() if self.min_time_check.isChecked() else None

    def end_date(self) -> Optional[QDateTime]:
        """
        Returns the start date
        """
        if self.service_type not in (
                SERVICE_MANAGER.MACROSEISMIC, SERVICE_MANAGER.FDSNEVENT, SERVICE_MANAGER.FDSNSTATION):
            return None

        return self.fdsn_event_end_date.dateTime() if self.max_time_check.isChecked() else None

    def min_magnitude(self) -> Optional[float]:
        """
        Returns the maximum magnitude
        """
        if self.service_type not in (SERVICE_MANAGER.MACROSEISMIC, SERVICE_MANAGER.FDSNEVENT):
            return None

        return self.fdsn_event_min_magnitude.value() if self.min_mag_check.isChecked() else None

    def max_magnitude(self) -> Optional[float]:
        """
        Returns the maximum magnitude
        """
        if self.service_type not in (SERVICE_MANAGER.MACROSEISMIC, SERVICE_MANAGER.FDSNEVENT):
            return None

        return self.fdsn_event_max_magnitude.value() if self.max_mag_check.isChecked() else None

    def extent_rect(self) -> bool:
        """
        Returns True if extent should be filtered by rectangle
        """
        return self.limit_extent_checkbox.isChecked() and (self.radio_rectangular_area.isChecked() or
                                                           self.radio_predefined_area.isChecked())

    def min_latitude(self) -> Optional[float]:
        """
        Returns the minimum latitude for the extent
        """
        return self.lat_min_spinbox.value() if self.lat_min_checkbox.isChecked() else None

    def max_latitude(self) -> Optional[float]:
        """
        Returns the maximum latitude for the extent
        """
        return self.lat_max_spinbox.value() if self.lat_max_checkbox.isChecked() else None

    def min_longitude(self) -> Optional[float]:
        """
        Returns the minimum longitude for the extent
        """
        return self.long_min_spinbox.value() if self.long_min_checkbox.isChecked() else None

    def max_longitude(self) -> Optional[float]:
        """
        Returns the maximum longitude for the extent
        """
        return self.long_max_spinbox.value() if self.long_max_checkbox.isChecked() else None

    def limit_extent_circle(self) -> bool:
        """
        Returns True if extent should be limited to a circle region
        """
        return self.limit_extent_checkbox.isChecked() and self.radio_circular_area.isChecked()

    def circle_latitude(self) -> float:
        """
        Returns the circle longitude
        """
        return self.circular_lat_spinbox.value()

    def circle_longitude(self) -> float:
        """
        Returns the circle longitude
        """
        return self.circular_long_spinbox.value()

    def circle_min_radius(self) -> Optional[float]:
        """
        Returns the minimum circle radius distance
        """
        return self.radius_min_spinbox.value() if self.radius_min_checkbox.isChecked() else None

    def circle_max_radius(self) -> Optional[float]:
        """
        Returns the maximum circle radius distance
        """
        return self.radius_max_spinbox.value() if self.radius_max_checkbox.isChecked() else None

    def circle_radius_unit(self) -> QgsUnitTypes.DistanceUnit:
        """
        Returns the circle radius unit
        """
        return self.radius_unit_combobox.currentData()

    def earthquake_max_intensity_greater(self) -> Optional[float]:
        """
        Returns the max intensity greater than filter value
        """
        if self.service_type != SERVICE_MANAGER.MACROSEISMIC:
            return None
        return self.earthquake_max_intensity_greater_combo.currentData() if self.earthquake_max_intensity_greater_check.isChecked() else None

    def earthquake_number_mdps_greater(self) -> Optional[float]:
        """
        Returns the MDP greater than filter value
        """
        if self.service_type != SERVICE_MANAGER.MACROSEISMIC:
            return None

        return self.earthquake_number_mdps_greater_spin.value() if self.earthquake_number_mdps_greater_check.isChecked() else None

    def convert_negative_depths(self) -> bool:
        return self.output_options_widget.convert_negative_depths()

    def depth_unit(self) -> QgsUnitTypes.DistanceUnit:
        return self.output_options_widget.depth_unit()

    def event_type(self) -> Optional[str]:
        """
        Returns the selected event type, or None
        """
        return self.event_type_combo.currentData() if self.event_type_check.isChecked() else None

    def updated_after(self) -> Optional[QDateTime]:
        """
        Returns the selected updated after date, or None
        """
        return self.events_updated_after.dateTime() if self.events_updated_after_check.isChecked() else None

    def output_type(self) -> str:
        return self.output_table_options_widget.output_type()

    def output_fields(self) -> Optional[List[str]]:
        return self.output_table_options_widget.output_fields

    def selected_styles(self) -> Dict[str, str]:
        return self.output_table_options_widget.selected_styles()

    def to_service_definition(self) -> dict:
        """
        Converts the widget state to a service definition
        """
        base_config = deepcopy(SERVICE_MANAGER.service_details(self.service_type, self.service_id))

        defaults = base_config.get('default', {})

        for k, v in {'datestart': self.start_date().toString(Qt.ISODate) if self.start_date() else None,
                     'dateend': self.end_date().toString(Qt.ISODate) if self.end_date() else None,
                     'boundingboxpredefined': self.combo_predefined_area.currentData() if self.radio_predefined_area.isChecked() else None,
                     'minimumlatitude': self.min_latitude() if self.radio_rectangular_area.isChecked() else None,
                     'maximumlatitude': self.max_latitude() if self.radio_rectangular_area.isChecked() else None,
                     'minimumlongitude': self.min_longitude() if self.radio_rectangular_area.isChecked() else None,
                     'maximumlongitude': self.max_longitude() if self.radio_rectangular_area.isChecked() else None,
                     'circlelatitude': self.circle_latitude() if self.radio_circular_area.isChecked() else None,
                     'circlelongitude': self.circle_longitude() if self.radio_circular_area.isChecked() else None,
                     'minimumcircleradius': self.circle_min_radius() if self.radio_circular_area.isChecked() else None,
                     'maximumcircleradius': self.circle_max_radius() if self.radio_circular_area.isChecked() else None,
                     'minimummagnitude': self.min_magnitude(),
                     'maximummagnitude': self.max_magnitude(),
                     'macromaxintensitygreater': self.earthquake_max_intensity_greater(),
                     'macromdpsgreaterthan': self.earthquake_number_mdps_greater(),
                     'eventtype': self.event_type(),
                     'updatedafter': self.updated_after().toString(Qt.ISODate) if self.updated_after() else None,
                     }.items():
            if v:
                defaults[k] = v
            elif k in defaults:
                del defaults[k]

        base_config['default'] = defaults
        return base_config

    def _customize_areas(self):
        """
        Shows the customise areas dialog
        """
        if self.customize_areas_dialog:
            self.customize_areas_dialog.show()
            return

        self.customize_areas_dialog = PredefinedAreasDialog(self)
        self.customize_areas_dialog.setAttribute(Qt.WA_DeleteOnClose, True)
        self.customize_areas_dialog.show()

        def accepted():
            self.customize_areas_dialog = None

        def rejected():
            self.customize_areas_dialog = None

        self.customize_areas_dialog.accepted.connect(accepted)
        self.customize_areas_dialog.rejected.connect(rejected)


from selene.api import *
from selene.driver import SeleneDriver
from selenium.webdriver import Chrome
from selenium.webdriver.support.select import Select

def test_blaze():
    config.browser_name = 'chrome'
    browser.open_url('http://www.blazedemo.com/')
    Select(s(by.name('fromPort'))).select_by_visible_text('Boston')
    Select(s(by.name('toPort'))).select_by_visible_text('Berlin')
    s('input[type="submit"]').click()
    s('.container>h3').should(have.text('Flights from Boston to Berlin:'))
    s(by.xpath('//tbody/tr[last()]/td/input')).click()
    s('.container > h2').should(have.text('Your flight from Boston to Berlin has been reserved.'))
    s('.container > p').should(have.text('Airline: Lufthansa'))
    s(by.xpath('//div[@class="container"]/p[last()-1]/em')).should(have.text('748.74'))  # Total price

def test_blaze_with_explicit_driver():
    """Tried combining Selene and Selenium for better readability and usability"""
    driver = SeleneDriver.wrap(Chrome())
    try:
        driver.get('http://www.blazedemo.com/')
        Select(driver.element(by.name('fromPort'))).select_by_visible_text('Boston')
        Select(driver.element(by.name('toPort'))).select_by_visible_text('Berlin')
        driver.element('input[type="submit"]').click()
        driver.element('.container>h3').should(have.text('Flights from Boston to Berlin:'))
    finally:
        driver.quit()


#!/usr/bin/env python3
import sys
import time
from typing import List

import requests
import datetime
from PyQt5 import QtWidgets
from PyQt5 import QtCore
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPixmap

from gui.py.setting_window import Ui_Dialog
from gui.py.window import Ui_MainWindow
from gui.py.task_box import Ui_frame
from gui.py.custome_window import Ui_Custom
from gui.py.custome_task import Ui_Frame as Ui_CustomTask

API_key = None
# Forcing setting file back in to the application folder.
# This will make sure that I don't leave any junk files on peoples computers. Since there is no uninstall button.
settings = QtCore.QSettings("setting.ini", QtCore.QSettings.IniFormat)

with open("icons/statistics.txt", "r") as file:
    task_options = file.readlines()
    task_options = [task.removesuffix("\n") for task in task_options]

# Setting window contains setting for the program
# Here you can choose what tasks will be shown and more.
class SettingWindow(QtWidgets.QDialog, Ui_Dialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setupUi(self)
        self.buttonBox.accepted.connect(self.save)

        # Displayed Tasks setting

        # Sets all checkboxes, needs to be parsed to int because it returns string and bool parser always give true
        for child in self.findChildren(QtWidgets.QCheckBox):  # type: QtWidgets.QCheckBox
            # assert isinstance(child, QtWidgets.QCheckBox)
            child.setChecked(settings.value(child.objectName(), type=bool, defaultValue=1))

        # Advanced Options
        self.busts_number.setValue(settings.value("busts_number", type=int, defaultValue=3))
        self.addiction_amount.setValue(settings.value("addiction_amount", type=int, defaultValue=10))

    def save(self):  # Saves check box states in to a setting.ini

        for child in self.findChildren(QtWidgets.QCheckBox):  # type: QtWidgets.QCheckBox
            # assert isinstance(child, QtWidgets.QCheckBox)
            settings.setValue(child.objectName(), child.isChecked())

        # Advanced Options
        settings.setValue("busts_number", self.busts_number.value())
        settings.setValue("addiction_amount", self.addiction_amount.value())

        the_button_was_clicked()  # Force Updates the list so the new setting is applied

class TaskBox(QtWidgets.QFrame, Ui_frame):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setupUi(self)

class CustomTask(QtWidgets.QFrame, Ui_CustomTask):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setupUi(self)

        self.comboBox_4.addItems(task_options)
        self.comboBox_4.setInsertPolicy(QtWidgets.QComboBox.NoInsert)
        self.comboBox_4.completer().setCompletionMode(QtWidgets.QCompleter.PopupCompletion)

        self.comboBox_3.addItems(["increased by"])

class CustomWindow(QtWidgets.QDialog, Ui_Custom):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setupUi(self)
        self.buttonBox.accepted.connect(self.save)
        self.item_list: List[CustomTask] = list()

        saved_list = settings.value("customs", defaultValue=[], type=list)

        for task in saved_list:
            loaded_task = CustomTask(self.frame)

            loaded_task.lineEdit.setText(task.get("description", ""))
            loaded_task.comboBox_4.setCurrentText(task.get("stat", ""))
            loaded_task.comboBox_3.setCurrentText(task.get("check", "increased by"))
            loaded_task.spinBox_2.setValue(task.get("value", 0))

            loaded_task.pushButton_2.clicked.connect(lambda: self.remove_task(loaded_task))

            self.verticalLayout_2.addWidget(loaded_task)
            self.item_list.append(loaded_task)

        self.verticalLayout.setAlignment(Qt.AlignTop)
        self.new_task = CustomTask(self.frame)

        self.new_task.pushButton_2.setText("➕")
        self.new_task.pushButton_2.clicked.connect(self.add_task)

        self.verticalLayout_2.addWidget(self.new_task)

    pass

    def add_task(self):
        self.new_task.pushButton_2.clicked.disconnect()

        temp = self.new_task

        self.item_list.append(temp)

        self.new_task.pushButton_2.clicked.connect(lambda: self.remove_task(temp))
        self.new_task.pushButton_2.setText("🗑")

        self.new_task = CustomTask(self.frame)
        self.new_task.pushButton_2.setText("➕")
        self.new_task.pushButton_2.clicked.connect(self.add_task)
        self.verticalLayout_2.addWidget(self.new_task)

    def remove_task(self, task):
        self.verticalLayout_2.removeWidget(task)
        self.item_list.remove(task)

    def save(self):

        customs = []

        for child in self.item_list:

            if child.lineEdit.text() == "":
                continue

            customs.append({
                "description": child.lineEdit.text(),
                "stat": child.comboBox_4.currentText(),
                "check": child.comboBox_3.currentText(),
                "value": child.spinBox_2.value()
            })

        settings.setValue("customs", customs)
        pass

class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setupUi(self)

        self.pushButton.clicked.connect(the_button_was_clicked)
        self.actionSetting.triggered.connect(self.open_setting)
        self.actionCustome_Tasks.triggered.connect(self.open_custome)
        self.timer = QtCore.QTimer()
        self.timer.timeout.connect(update_tasks)
        self.error.hide()

        self.verticalLayout_2.setAlignment(Qt.AlignTop)

    # The timer needs to be stop so the UI doesn't freez on the user
    def open_setting(self):
        dialog = SettingWindow(self)
        self.timer.stop()
        dialog.open()
        the_button_was_clicked()

    def open_custome(self):
        dialog = CustomWindow(self)
        self.timer.stop()
        dialog.open()
        the_button_was_clicked()
        pass

    def generate_save_file(self):
        dialog = SettingWindow(self)
        dialog.save()

class Task:
    priority = 0
    ID = 0

    def __init__(self, task_name, priority=0, link="link", image="icons/missing_icon.png", ID=0):
        self.frame = TaskBox(window.scrollAreaWidgetContents)

        self.frame.task_name.setText(task_name)
        self.frame.icon.setAutoFillBackground(False)
        self.frame.icon.setPixmap(QPixmap(image).scaled(self.frame.icon.size()))

        self.frame.link.setText(link)
        self.frame.link.setOpenExternalLinks(True)

        self.frame.show()
        self.ID = ID
        self.priority = priority

    def __del__(self):
        self.frame.deleteLater()

    def move(self, position):
        window.verticalLayout_2.addWidget(self.frame)
        pass

def the_button_was_clicked():
    global API_key
    API_key = window.textEdit.toPlainText()

    code = get_request("https://api.torn.com/user/?selections=timestamp&key={}".format(API_key))

    if code == 2:  # Checks if the API_key is valid, shows error message if it isn't.
        window.error.show()
        return False

    if settings.value("API_key") != API_key:  # if the keys different and the new key is valid save key to file.
        settings.setValue("API_key", API_key)

    window.timer.start(30500)  # every 30-second update (new data is shown every 30 seconds so no need to update sooner)
    window.error.hide()
    update_tasks()

tasks = list()

# Sorts Task based on priority. Should be called every time new task list is updated.
def reorder_task():
    offset = 0
    for i in range(10):
        for task in tasks:
            if task.priority == i:
                task.move(offset)
                offset += 1

def get_request(url):  # Code that waits and retryes request when error occurs (most comonly running out off API calls)
    json_info = requests.get(url).json()

    while True:
        if json_info.get("error") is None:
            break
        elif json_info.get("error").get("code") != "5":
            return json_info.get("error").get("code")

        json_info = requests.get(url).json()
    return json_info

def link(url):
    return "Visit".format(url)

# gets either statistics from timestamp, or the current ones, if time is not provided
def get_stats(time_start=None):
    statistics = settings.value("customs", [], list)
    statistics = [statistics[i:i + 10] for i in range(0, len(statistics), 10)]

    data = dict()
    for batch in statistics:
        url = "https://api.torn.com/user/?selections=personalstats&stat="
        for stat in batch:
            url += stat.get("stat") + ","

        if time_start is not None:
            url += "&timestamp=" + str(time_start)

        url += "&key=" + settings.value("API_key")

        temp = dict(get_request(url).get("personalstats"))
        data.update(temp)
        # Even thou correctly cashing shouldn't get triggerd with different request, it seems it still happens if the
        # requests are close together.
        time.sleep(1)

    return data

old_data = None
def update_tasks():
    global old_data
    tasks.clear()
    print("Updating")

    # This is when asking for logs to only ask for the one that are from current day.
    t_time = datetime.datetime.utcnow().time()
    timestamp = datetime.datetime.now().timestamp()
    time_from_midnight = t_time.second + t_time.minute * 60 + t_time.hour * 3600
    time_day_start = int(timestamp - time_from_midnight)

    info = get_request(
        f'https://api.torn.com/user/?selections=cooldowns,profile,refills,networth,bars,icons&key={API_key}')
    count_logs = str(get_request(
        f'https://api.torn.com/user/?selections=log&log=5360,7815,7810,7805,8370,8371,6005&from={time_day_start}&key={API_key}'))

    if old_data is None:
        old_data = get_stats(time_day_start)

    new_data = get_stats()

    # -Used log IDs
    # 5360 - Bust success                       # Can be rewritten to use stats
    # 7815 - Missions complete                  # Can be rewritten to use stats
    # 7810 - Missions fail
    # 7805 - Missions decline
    # 8370 - Casino spin the wheel start
    # 8371 - Casino spin the wheel free spin
    # 6005 - Rehab

    # Notes
    # NPC_shop should be skipped automatically when is completed.

    # count_newsletter = str(get_request(f'https://api.torn.com/user/?selections=log&log=400,401&key={API_key}'))

    # Drug cooldown task
    drugs = int(info.get("cooldowns").get("drug"))
    if drugs < 60 and int(settings.value("drug")):
        tasks.append(Task("Take a drug!", 1, ID=1, image=("icons/drugs.png")))

        # Medical cooldown task
    medical = int(info.get("cooldowns").get("medical"))
    life = (float(info.get("life").get("current")) / float(info.get("life").get("maximum"))) * 100
    if medical < 14400 and life > 90 and int(
            settings.value("medical")):  # set up that you get task only if you can fill 3 blood packs
        tasks.append(Task("Fill a Blood Bag", 5, ID=1, image=str("icons/medical.png")))

    # Booster cooldown task
    booster = int(info.get("cooldowns").get("booster"))
    if booster < 28800 and int(settings.value("booster")):
        tasks.append(Task("Go Drink some Beer", 4, ID=1, image="icons/booster.png"))

    # Energy refill task
    refills = info.get("refills")
    if not bool(refills.get("energy_refill_used")) and int(settings.value("energy_refill")):
        tasks.append(Task("Use Your Energy Refill", 4, link=link("https://www.torn.com/points.php"), ID=2,
                          image="icons/e_refill.png"))

    # Nerve refill task
    if not bool(refills.get("nerve_refill_used")) and int(settings.value("nerve_refill")):
        tasks.append(Task("Use Your Nerve Refill", 5, link=link("https://www.torn.com/points.php"), ID=2))

    # Casino refill task
    if not bool(refills.get("token_refill_used")) and int(settings.value("casino_refill")):
        tasks.append(Task("Use Your Casino Tokens Refill", 5, link=link("https://www.torn.com/points.php"), ID=2,
                          image="icons/casino.png"))

    # Energy task
    energy = info.get("energy")
    if int(energy.get("current")) == int(energy.get("maximum")) and int(settings.value("energy")):
        tasks.append(Task("Spend your Energy on something", 2, ID=3, image="icons/energy.png"))

    # Crime task
    nerve = info.get("nerve")
    if int(nerve.get("current")) >= int(nerve.get("maximum")) and int(settings.value("nerve")):
        tasks.append(Task("Go Commit some Crimes", 2, link=link("https://www.torn.com/crimes.php"), ID=3,
                          image="icons/crimes.png"))

    # Unpaidfees task
    unpaidfees = int(info.get("networth").get("unpaidfees"))
    if unpaidfees != 0 and int(settings.value("bills")):
        tasks.append(
            Task("You have -${:,} unpaidfees".format(-1 * unpaidfees), 6, link("https://www.torn.com/loan.php"), ID=5,
                 image="icons/fees.png"))

    # Racing task
    race = info.get("icons").get("icon17")
    if race is None and int(settings.value("race")):
        tasks.append(Task("You should enter a race", 5, link=link("https://www.torn.com/loader.php?sid=racing"), ID=8,
                          image="icons/race.png"))

    # Rehab task
    addiction = info.get("icons").get("icon57")
    if addiction is not None and int(settings.value("rehab")):
        addiction = str(addiction)[(addiction.find("(") + 1):addiction.find(")")].strip("-%")
        if int(addiction) >= 3:
            tasks.append(Task("Go to rehab", 6, ID=11, image="icons/rehab.png"))

    # Employee effectiveness
    # If player already rehab that day, don't show
    # We have to do this since addiction only updates once a day,
    # So the task would stay there long after the player already rehabbed
    rehab = count_logs.count("g\': 6005")
    if not rehab and int(settings.value("rehab")):
        company = get_request(f'https://api.torn.com/company/?selections=employees&key={API_key}')
        addiction = company.get("company_employees").get(str(info.get("player_id"))).get("effectiveness").get(
            "addiction")
        if addiction <= int(settings.value("addiction_amount")) * -1 and not rehab:
            tasks.append(Task("Go to rehab your addiction is {}".format(addiction), 6, ID=11))

    # #Use newsletter bonus
    # newsletters_unused = count_newsletter.count("g\': 400") - count_newsletter.count("g\': 401")
    # if newsletters_unused > 0 :
    #     tasks.append(Task("You have {} unused newsletter bonuses".format(newsletters_unused),10,ID=15))

    # Daily mission task
    missions = count_logs.count("g\': 7815") + count_logs.count("g\': 7810") + count_logs.count("g\': 7805")
    if missions == 0 and int(settings.value("missions")):
        tasks.append(
            Task("Complete your daily mission", 9, link=link("https://www.torn.com/loader.php?sid=missions"), ID=6,
                 image="icons/mission.png"))

    # Bust people task
    if settings.value("busts_number") is not None:  # Just in case value doesn't exist in setting.
        busts = count_logs.count("g\': 5360")
        busts_number = int(settings.value("busts_number"))
        if busts < busts_number and int(settings.value("busts")):
            tasks.append(
                Task("Do {} more busts".format(busts_number - busts), 7, link=link("https://www.torn.com/jailview.php"),
                     ID=7, image="icons/busts.png"))
            pass

    # Wheel of Fortune
    spins = count_logs.count("g\': 8370") - count_logs.count("g\': 8371")
    if spins < 3 and int(settings.value("wheels")):
        tasks.append(Task("You have {} wheels to be spun".format(3 - spins), 8,
                          link=link("https://www.torn.com/loader.php?sid=spinTheWheel"), ID=10,
                          image="icons/wheel.png"))

    # Buy stuff at NPC shop task
    if int(settings.value("npc")):
        npc_shop = get_request(
            f'https://api.torn.com/user/?selections=log&log=4200&from={time_day_start}&key={API_key}')
        npc_shop = npc_shop.get("log")
        brought = 0
        if npc_shop is not None:
            for log in npc_shop:
                brought += int(npc_shop.get(log).get("data").get("quantity"))
        if brought < 100 and int(settings.value("npc")):
            tasks.append(Task("Buy {} items at NPC shop".format(100 - brought), 9,
                              link=link("https://www.torn.com/shops.php?step=bitsnbobs"), ID=8,
                              image="icons/bits_bobs.png"))

    if int(settings.value("virus")):
        virus = get_request(f'https://api.torn.com/user/?selections=log&log=5800,5801,5802&key={API_key}')
        virus = virus.get("log")
        if virus[next(iter(virus))].get("log") != 5800:
            tasks.append(Task("Start programming virus", 9, ID=10, image="icons/virus.png"))

    for stat in settings.value("customs", defaultValue=[]):
        key = stat.get("stat")

        value = stat.get("value", 0) - (new_data[key] - old_data[key])

        if value > 0:
            description = str(stat.get("description")).replace("${value}", str(value))
            tasks.append(Task(description, 9, image="icons/note.png", link=link("https://www.torn.com/index.php")))

    reorder_task()

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)  # not sure what it does
    window = MainWindow()  # initializing window.py basically
    window.show()

    if len(settings.allKeys()) != 19:  # Checks if setting file exists if no forces window to generate new one.
        window.generate_save_file()

    if settings.value("API_key") is not None:  # inserst key in to field if it exists.
        window.textEdit.setText(settings.value("API_key"))

    """
    Below code can be used to get data for statistics.txt
    key = settings.value("API_key")
    data = get_request(f'https://api.torn.com/user/?selections=personalstats&key={key}').get("personalstats")

    print(data)

    for key in data:
        print(key)

    """

    app.exec()


from datetime import datetime
# UI Libraries
import tkinter as tk
from tkinter import ttk
# Project Modules
from parsing.scoreboards import ScoreboardDatabase

class Scoreboard(ttk.Frame):
    """Frame containing a Treeview showing Scoreboard data"""

    HEADINGS = {
        "#0": "Name",
        "kills": "Kills",
        "assists": "Assists",
        "deaths": "Deaths",
        "damage": "Damage",
        "hit": "Hit",
        "objectives": "Objectives"
    }

    TYPES = {
        "#0": str,
        "kills": int,
        "assists": int,
        "deaths": int,
        "damage": int,
        "hit": int,
        "objectives": int,
    }

    def __init__(self, parent: tk.Widget):
        """Initialize the Frame and child widgets"""
        ttk.Frame.__init__(self, parent)

        self.score_label = ttk.Label(self, text="No match selected or scoreboard not available")
        self.l_label, self.r_label = ttk.Label(self), ttk.Label(self)
        self.tree = ttk.Treeview(
            self, columns=("kills", "assists", "deaths", "damage", "hit", "objectives"),
            show=("headings", "tree"))
        self.scroll = ttk.Scrollbar(self, command=self.tree.yview)
        self.tree.config(yscrollcommand=self.scroll.set)

        self.setup_treeview()
        self.grid_widgets()

    def grid_widgets(self):
        """Configured child widgets in grid geometry manager"""
        self.score_label.grid(row=0, column=0, sticky="nsw", padx=5, pady=(5, 0))
        self.l_label.grid(row=0, column=1, sticky="nsw", padx=(0, 5), pady=(5, 0))
        self.r_label.grid(row=0, column=2, sticky="nsw", padx=(0, 5), pady=(5, 0))
        self.tree.grid(row=1, column=0, sticky="nswe", padx=5, pady=5, columnspan=5)
        self.scroll.grid(row=1, column=5, sticky="nsw", pady=5)

    def setup_treeview(self):
        """Setup the Treeview columns and headings"""
        self.tree.column("#0", width=130, anchor=tk.W)
        self.tree.column("kills", width=50, anchor=tk.E)
        self.tree.column("assists", width=60, anchor=tk.E)
        self.tree.column("deaths", width=60, anchor=tk.E)
        self.tree.column("damage", width=80, anchor=tk.E)
        self.tree.column("hit", width=40, anchor=tk.E)
        self.tree.column("objectives", width=100, anchor=tk.E)

        for c, t in Scoreboard.HEADINGS.items():
            self.tree.heading(c, text=t, command=lambda c=c: self.tree_sort(c, False, Scoreboard.TYPES[c]))

        self.tree.configure(height=13)
        self.tree.tag_configure("ally", background="#b8ffad", foreground="black")
        self.tree.tag_configure("enemy", background="#f9b7a7", foreground="black")

    def update_match(self, match: datetime):
        """Update the Widget with the scoreboard of a match"""
        scoreboard = ScoreboardDatabase.get_scoreboard(match)
        if scoreboard is None:
            self.reset()
            return
        self.update_scoreboard(scoreboard)

    def update_scoreboard(self, scoreboard):
        """Update Widget with data from scoreboard"""
        l, r, i = scoreboard["score"]
        labels = [self.l_label, self.r_label]
        self.score_label.config(text="End score:")
        self.l_label.config(text=str(l))
        self.r_label.config(text=str(r))
        labels[i].config(foreground="green")
        labels[int(not bool(i))].config(foreground="red")
        for ally in scoreboard["allies"]:
            text, values = ally[0], ally[1:]
            self.tree.insert("", tk.END, text=text, values=values, tags=("ally",))
        for enemy in scoreboard["enemies"]:
            text, values = enemy[0], enemy[1:]
            self.tree.insert("", tk.END, text=text, values=values, tags=("enemy",))

    def reset(self):
        """Delete all the data from the widget"""
        self.tree.delete(*self.tree.get_children(""))
        self.score_label.config(text="No match selected or scoreboard not available")
        map(lambda l: l.config(text=""), (self.l_label, self.r_label))

    def tree_sort(self, column, reverse, type):
        """Sort a column by its value"""
        self.tree.heading(column, command=lambda: self.tree_sort(column, not reverse, type))
        if column == "#0":
            children = self.tree.get_children("")
            data = {self.tree.item(iid)["text"]: self.tree.item(iid) for iid in children}
            self.tree.delete(*children)
            iterator = sorted(data.items()) if reverse is False else reversed(sorted(data.items()))
            for _, kwargs in iterator:
                self.tree.insert("", tk.END, **kwargs)
        else:
            values = list(sorted(((self.tree.set(k, column), k) for k in self.tree.get_children('')),
                                 key=lambda t: type(t[0]), reverse=reverse))
            for index, (val, k) in enumerate(values):
                self.tree.move(k, '', index)


import pytest
import tasks
from tasks import Task

@pytest.fixture(autouse = True)
def init_tasks_db(tmpdir):
    """Connect to db before testing, disconnect after"""
    tasks.start_tasks_db(str(tmpdir), 'tiny')

    # Yield and allow testing to happend
    yield

    # Teardown and stop db instance
    tasks.stop_tasks_db()

@pytest.mark.skipif(tasks.__version__ < '0.2.0', reason='not testable till version 0.2.1')
def test_unique_id():
    """Calling unique_id() twice should give different ints"""
    id1 = tasks.unique_id()
    id2 = tasks.unique_id()

    assert(id1 != id2)

def test_unique_id_2():
    """unique_id should return int of id ununsed in db"""
    ids = []
    ids.append(tasks.add(Task('one job')))
    ids.append(tasks.add(Task('second job')))
    ids.append(tasks.add(Task('third job')))

    uuid = tasks.unique_id()

    assert(uuid not in ids)


# SPDX-License-Identifier: MIT

from __future__ import annotations

import json
import logging
import sys
import urllib.parse

from ...util import requests
from . import cli

logger = logging.getLogger(__name__)

class ClientCve:
    server: str

    @staticmethod
    @cli.register(
        'cve',
        arguments=[
            cli.prepare_argument(
                'cve',
                help='the CVE to look up',
                metavar='CVE',
            ),
        ]
    )
    def run(*, argparser: None, cve: str, server: str, debug: bool) -> None:
        logging.basicConfig(level=debug and logging.DEBUG or logging.INFO)
        ClientCve(server)(cve)

    def __init__(self, server: str) -> None:
        self.server = server

    def __call__(self, cve: str) -> None:
        with requests.RetrySession() as rsession:
            resp = rsession.get(
                urllib.parse.urljoin(self.server, f'v1/cves/{cve}'),
            )
            if resp.status_code == 200:
                data = resp.json()
                json.dump(data, sys.stdout, indent=2)
            elif resp.status_code == 404:
                print(f'{cve} not found', file=sys.stderr)
            else:
                resp.raise_for_status()

if __name__ == '__main__':
    ClientCve.run()


#!/usr/bin/python
import json
import re
from imgurpython import ImgurClient
from imgurpython.helpers.error import ImgurClientRateLimitError

from shared import load_secrets

def get_imgur_client():
  # visit https://api.imgur.com/oauth2/authorize?client_id=77fbcca7c596528&response_type=token
  # to regenerate access token and refresh token and save in the secrets file
  # application is registered under fooddayextension@gmail.com, can regenerate secret under Applications
  secrets = load_secrets()
  client = ImgurClient(secrets['IMGUR_CLIENT_ID'], secrets['IMGUR_CLIENT_SECRET'], secrets['IMGUR_ACCESS_TOKEN'], secrets['IMGUR_REFRESH_TOKEN'])
  return client

def upload_to_imgur():
  client = get_imgur_client()
  new_links = {}
  with open('images.txt') as f:
    images = json.loads(f.read())
  rate_limited = False
  for key, link in images.items():
    try:
      if 'imgur' in link or rate_limited:
        new_links[key] = link
      else:
        res = client.upload_from_url(link)
        print('uploaded %s to %s' % (key, res['link']))
        new_links[key] = res['link']
    except ImgurClientRateLimitError as e:
      rate_limited = True
      print('got rate limited')
      new_links[key] = link
    except:
      new_links[key] = link
  with open('images2.txt', 'w') as f:
    f.write(json.dumps(new_links, indent=2, sort_keys=True))
  return new_links

def remove_from_imgur():
  client = get_imgur_client()
  with open('unused.txt') as f:
    images = json.loads(f.read())
  regex = r'https://i.imgur.com/([0-9a-zA-Z]*).'
  for key, link in images.items():
    print(link)
    image_id = re.match(regex, link).group(1)
    print('removing %s' % image_id)
    client.delete_image(image_id)

#upload_to_imgur()
#remove_from_imgur()


# Lint as: python3
"""Tests for epi_forecast_stat_mech.statistical_models.network_models.py."""

from absl.testing import absltest
from absl.testing import parameterized
from epi_forecast_stat_mech.statistical_models import network_models
from epi_forecast_stat_mech.statistical_models import tree_util
import jax
import jax.numpy as jnp
from jax.config import config

config.parse_flags_with_absl()  # Necessary for running on TPU.

class NetworkModelsTest(parameterized.TestCase):

  @parameterized.parameters(
      dict(predict_module=network_models.PerceptronModule,
           dict_keys=None,
           num_locations=7,
           num_covariates=5,
           num_observations=3,
           error_model='full'),
      dict(predict_module=network_models.LinearModule,
           dict_keys=None,
           num_locations=7,
           num_covariates=5,
           num_observations=3,
           error_model='full'),
      dict(predict_module=network_models.PerceptronModule,
           dict_keys=('one', 'two'),
           num_locations=7,
           num_covariates=5,
           num_observations=3,
           error_model='plugin'),
  )
  def testShapes(
      self,
      predict_module,
      dict_keys,
      num_locations,
      num_covariates,
      num_observations,
      error_model
  ):
    """Tests that `log_likelihood` and `predict` return expected shapes."""
    rng = jax.random.PRNGKey(42)
    c_rng, o_rng, init_rng = jax.random.split(rng, 3)
    covariates = jax.random.uniform(c_rng, (num_locations, num_covariates))
    observations = jax.random.uniform(o_rng, (num_locations, num_observations))
    if dict_keys is not None:
      observations = {key: observations for key in dict_keys}

    if error_model == 'full':
      scale_eps = 1E-2
    else:
      # The length of this needs to agree with the flattened number of
      # "observations" per row. I.e. flattened observations is (7, 6)
      # for the 'plugin' test because the two dict keys double the
      # three observations per location.
      scale_eps = jnp.asarray([.1, .2, .3, .4, .5, .6])
    stat_model = network_models.NormalDistributionModel(
        predict_module=predict_module,
        error_model=error_model,
        scale_eps=scale_eps)
    params = stat_model.init_parameters(init_rng, covariates, observations)

    log_likelihood = stat_model.log_likelihood(params, covariates, observations)

    if error_model == 'full':
      actual_shapes = tree_util.tree_map(lambda x: x.shape, log_likelihood)
      expected_shapes = tree_util.tree_map(lambda x: x.shape, observations)
      self.assertEqual(actual_shapes, expected_shapes)
      distributions_over_observations = stat_model.predict(
          params, covariates, observations)
      actual_shapes = tree_util.tree_map(
          lambda x: x.mean().shape, distributions_over_observations)
      expected_shapes = tree_util.tree_map(lambda x: x.shape, observations)
      self.assertEqual(actual_shapes, expected_shapes)

if __name__ == '__main__':
  absltest.main()


import os
os.environ['SIDEKIT'] = 'theano=false,libsvm=false,mpi=false'
from torch.autograd import Variable
from torch import optim
import pyro.optim as pyro_optim
import logging
import torch
from torch import nn
import numpy
from torch.utils.data import DataLoader
from torchvision import transforms
# from torch.optim.lr_scheduler import ReduceLROnPlateau
import h5py
from FaceDatasets import SiameseNetworkDatasetDifficult, SoftMaxDatabase
from FaceNetworks import SoftMaxNetwork, SiameseNetwork, GME_SiameseNetwork, GME_SoftmaxNetwork, VAE
from MetaEmbeddings import GaussianMetaEmbedding
from Config import ConfigNetwork, ConfigFaceDatasets
from utils_test import test_model
import sidekit
from sidekit.factor_analyser import fa_model_loop
import copy

__author__ = "Andreas Nautsch, Themos Stafylakis"
__maintainer__ = "Andreas Nautsch"
__email__ = "andreas.nautsch@h-da.de"
__status__ = "Development"
__docformat__ = 'reStructuredText'
__credits__ = ["Niko Brümmer, Adrian Bulat"]

def net_distance(data, net, epoch=None):
    sample0, sample1, label = data
    sample0, sample1, label = Variable(sample0).cuda(), Variable(sample1).cuda(), Variable(label).cuda()
    if ConfigNetwork.train_with_softmax:
        # note: still face dependend
        if ConfigNetwork.train_with_meta_embeddings:
            output1_a, output1_B = net(sample0, None)
            output2_a, output2_B = net(sample1, None)
            return GaussianMetaEmbedding.net_distance(data=(output1_a, output1_B, output2_a, output2_B, label), net=net, epoch=epoch)
        else:
            output1 = net(sample0, None)
            output2 = net(sample1, None)
            return SoftMaxNetwork.distance_pairwise_euclidean(output1, output2), label
    elif ConfigNetwork.train_vae:
            output1_a, output1_B, output2_a, output2_B = net(sample0,sample1)
            return VAE.net_distance(data=(output1_a, output1_B, output2_a, output2_B, label), net=net, epoch=epoch)
    else:
        if ConfigNetwork.train_with_meta_embeddings:
            output1_a, output1_B, output2_a, output2_B = net(sample0,sample1)
            return GaussianMetaEmbedding.net_distance(data=(output1_a, output1_B, output2_a, output2_B, label), net=net, epoch=epoch)
        else:
            # note: still face dependend
            output1, output2 = net(sample0, sample1)
            return SiameseNetwork.net_distance(data=(output1, output2, label), net=net, epoch=epoch)

def train_epoch(train_dataloader, net, optimizer, epoch, iteration_number):
    net.train()
    if ConfigNetwork.train_with_softmax:
        if ConfigNetwork.train_with_meta_embeddings:
            GME_SoftmaxNetwork.train_epoch(train_dataloader, net, optimizer, epoch, iteration_number)
        else:
            # same for ConfigNetwork.train_with_meta_embeddings:
            SoftMaxNetwork.train_epoch(train_dataloader, net, optimizer, epoch, iteration_number)
    elif ConfigNetwork.train_vae:
        VAE.train_epoch(train_dataloader, net, optimizer, epoch, iteration_number)
    else:
        if ConfigNetwork.train_with_meta_embeddings:
            # GME_SiameseNetwork.train_epoch(train_dataloader, net, optimizer, epoch, iteration_number)
            GaussianMetaEmbedding.train_epoch(train_dataloader, net, optimizer, epoch, iteration_number)
        else:
            SiameseNetwork.train_epoch(train_dataloader, net, optimizer, epoch, iteration_number)

def select_difficult_pairs(net, train_dataloader):
    net.eval()
    if not os.path.exists(ConfigNetwork.embeddings_file):
        dataset = SoftMaxDatabase(imageFolderDataset=ConfigFaceDatasets.dataset_class(root=ConfigFaceDatasets.training_dir),
                                  transform=train_dataloader.dataset.transform,
                                  should_invert=False)
        embeddings_loader = DataLoader(dataset,
                                       shuffle=False,
                                       num_workers=ConfigNetwork.num_workers,
                                       batch_size=ConfigNetwork.batch_size_train)
        with h5py.File(ConfigNetwork.embeddings_file, "a") as embd_file:
            for i, data in enumerate(embeddings_loader, 0):
                img0, label = data
                img0, label = Variable(img0).cuda(), Variable(label).cuda()
                output0 = net.forward_once(img0)
                embd_file.create_dataset("{}".format(i),
                                         data=numpy.column_stack((output0.data.cpu().numpy(), label.data.cpu().numpy())),
                                         compression="gzip",
                                         fletcher32=True)
        logging.critical('extracted embeddings for difficult pairs')

    if not os.path.exists(ConfigNetwork.select_difficult_pairs_idx + '_genuine.npy') or not os.path.exists(ConfigNetwork.select_difficult_pairs_idx + '_impostor.npy'):
        data = []
        with h5py.File(ConfigNetwork.embeddings_file, "r") as h5f:
            for key, value in h5f.items():
                data.append(value.value)
        data = numpy.concatenate(data)
        embeddings = data[:,:ConfigNetwork.embedding_size]
        embeddings = (embeddings.T / numpy.linalg.norm(embeddings, axis=1)).T # prepare cosine distance
        embedding_labels = data[:, ConfigNetwork.embedding_size:]
        subjects = numpy.unique(embedding_labels.flatten())
        # concat & score embeddings
        lbls = embedding_labels + 1 # all labels are IDs 0 to inf, however a "0" label destroy the later lbls@lbls-lbls**2 idea as any other label times zero is zero...
        # figure out labels, unselect same-sample comparison, i.e. diagonal
        split_num = int(1000)
        # assure #embeddings / split_num < ConfigNetwork.select_difficult_pairs_top_N /
        """
        if data.shape[0] / split_num > ConfigNetwork.select_difficult_pairs_top_N / 4:
            split_num = int(data.shape[0] / ConfigNetwork.select_difficult_pairs_top_N * 4)
        indice_tuples_genuine = numpy.zeros((ConfigNetwork.select_difficult_pairs_top_N, 3)) # idx1, idx2, score
        indice_tuples_impostor = numpy.zeros((ConfigNetwork.select_difficult_pairs_top_N, 3)) # idx1, idx2, score
        """
        # idx1, idx2, score, label1, label2
        indice_tuples_genuine = numpy.zeros((subjects.shape[0] * ConfigNetwork.select_difficult_pairs_topN_per_subject, 5))
        indice_tuples_impostor = numpy.zeros((subjects.shape[0] * ConfigNetwork.select_difficult_pairs_topN_per_subject, 5))
        indice_tuples_genuine[:,2] = -numpy.infty
        indice_tuples_impostor[:,2] = numpy.infty
        indice_tuples_genuine[:,3] = numpy.repeat(subjects, ConfigNetwork.select_difficult_pairs_topN_per_subject)
        indice_tuples_genuine[:,4] = indice_tuples_genuine[:,3]
        indice_tuples_impostor[:,3] = numpy.repeat(subjects, ConfigNetwork.select_difficult_pairs_topN_per_subject)
        indice_tuples_impostor[:,4] = -1
        splits = numpy.array_split(range(embeddings.shape[0]), split_num)
        split_lens = numpy.array([len(s) for s in splits])
        for ii, sid in enumerate(splits):
            if ii % 100 == 0:
                logging.debug('emb idx: {}'.format(ii))
            embedding_scores = - embeddings[sid,:] @ embeddings.T # negative cosine similarity
            offset = split_lens[:ii].sum()
            num_rows = embedding_scores.shape[0]
            num_cols = embedding_scores.shape[1]

            lbls_scores = numpy.abs((lbls[sid] @ lbls.T - lbls.flatten() ** 2))  # leaves zeros on same class, also: diag
            # also, remove duplicates due to symmetric comparison
            center_of_storm = numpy.eye(num_rows, dtype=bool)
            center_of_storm[numpy.tril_indices(num_rows)] = True
            eye_idx = numpy.hstack((
                numpy.ones((num_rows,offset), dtype=bool),
                center_of_storm,
                numpy.zeros((num_rows,num_cols - num_rows - offset), dtype=bool)
            ))
            embedding_scores[eye_idx] = -100  # exclude same image comparisons
            lbls_scores[eye_idx] = -100  # same sample = -100, tar = 0, non > 0

            # for each reference, find difficult pairs
            batch_subjects = numpy.unique(embedding_labels.flatten()[offset:offset+num_rows])
            for sidx, subj in enumerate(batch_subjects):
                subj_idx_in_list = subjects == subj

                batch_row_idx = (embedding_labels[offset:offset+num_rows] == subj).flatten()
                batch_row_pos = numpy.argwhere(batch_row_idx)

                subj_scores = embedding_scores[batch_row_idx,:]
                tar_idx = lbls_scores[batch_row_idx,:] == 0
                non_idx = lbls_scores[batch_row_idx,:] > 0
                tar_pos = numpy.argwhere(tar_idx)
                non_pos = numpy.argwhere(non_idx)

                tar = subj_scores[tar_idx]
                non = subj_scores[non_idx]

                tar_argsort = tar.argsort()[::-1]
                non_argsort = non.argsort()

                if len(tar_argsort) > 0:
                    subj_tar_in_list = indice_tuples_genuine[:,3] == subj
                    subj_tar_idx = numpy.argwhere(subj_tar_in_list).flatten()
                    subj_topN_tar_argsort = indice_tuples_genuine[subj_tar_in_list,3].argsort()[::-1]
                    tar_cnt = 0
                    for nidx in subj_topN_tar_argsort:
                        if tar_cnt >= tar_argsort.shape[0]:
                            break
                        if indice_tuples_genuine[subj_tar_idx[nidx],2] < tar[tar_argsort[tar_cnt]]:
                            idx_subj_scr = tar_pos[tar_argsort[tar_cnt]]
                            col = idx_subj_scr[1]
                            row = batch_row_pos[idx_subj_scr[0]]
                            indice_tuples_genuine[subj_tar_idx[nidx], 0:3] = numpy.array([offset + row, col, embedding_scores[row, col]]).T
                            tar_cnt += 1

                if len(non_argsort) > 0:
                    subj_non_in_list = indice_tuples_impostor[:,3] == subj
                    subj_non_idx = numpy.argwhere(subj_non_in_list).flatten()
                    subj_topN_non_argsort = indice_tuples_impostor[subj_non_in_list,3].argsort()
                    non_cnt = 0
                    for nidx in subj_topN_non_argsort:
                        if non_cnt >= non_argsort.shape[0]:
                            break
                        if indice_tuples_impostor[subj_non_idx[nidx],2] > non[non_argsort[non_cnt]]:
                            idx_subj_scr = non_pos[non_argsort[non_cnt]]
                            col = idx_subj_scr[1]
                            row = batch_row_pos[idx_subj_scr[0]]
                            indice_tuples_impostor[subj_non_idx[nidx], 0:3] = numpy.array([offset + row, col, embedding_scores[row, col]]).T
                            non_cnt += 1
        numpy.save(ConfigNetwork.select_difficult_pairs_idx + '_genuine_all.npy', indice_tuples_genuine)
        numpy.save(ConfigNetwork.select_difficult_pairs_idx + '_impostor_all.npy', indice_tuples_impostor)
        indice_tuples_genuine = indice_tuples_genuine[numpy.argwhere(
            ~numpy.isinf(indice_tuples_genuine[:, 2])).flatten(), :]
        indice_tuples_impostor = indice_tuples_impostor[numpy.argwhere(
            ~numpy.isinf(indice_tuples_impostor[:, 2])).flatten(), :]
        numpy.save(ConfigNetwork.select_difficult_pairs_idx + '_genuine.npy', indice_tuples_genuine)
        numpy.save(ConfigNetwork.select_difficult_pairs_idx + '_impostor.npy', indice_tuples_impostor)
        logging.critical('saved difficult pairs')

    indice_tuples_genuine = numpy.load(ConfigNetwork.select_difficult_pairs_idx + '_genuine.npy').astype(int)
    indice_tuples_impostor = numpy.load(ConfigNetwork.select_difficult_pairs_idx + '_impostor.npy').astype(int)

    # new dataloader
    diff_dataset = SiameseNetworkDatasetDifficult(
        imageFolderDataset=ConfigFaceDatasets.dataset_class(root=ConfigFaceDatasets.training_dir),
        indice_tuples_genuine=indice_tuples_genuine,
        indice_tuples_impostor=indice_tuples_impostor,
        transform=transforms.Compose([transforms.Scale((100, 100)),
                                      transforms.ToTensor()
                                      ]),
        should_invert=False)
    train_dataloader = DataLoader(diff_dataset,
                                  shuffle=False,
                                  num_workers=ConfigNetwork.num_workers,
                                  batch_size=ConfigNetwork.batch_size_train)
    logging.critical('swapped train data loader to difficult pairs')
    return train_dataloader

def train_net(net, train_dataloader, test_dataloader):
    if not os.path.exists(ConfigNetwork.modelname):
        last_model_loaded = False # True # False
        # inits
        iteration_number = 0
        for epoch in range(0,ConfigNetwork.train_number_epochs):
            """
            if ConfigNetwork.learning_rate_scheduler:
                optimizer = optim.Adam(net.parameters(),
                                       lr = ConfigNetwork.learning_rate)
                scheduler = ReduceLROnPlateau(optimizer, 'min')
            else:
            """
            epoch_learning_rate_exponent = max(0, epoch - (ConfigNetwork.learning_rate_defactor_after_epoch - 1))
            lr = ConfigNetwork.learning_rate * ConfigNetwork.learning_rate_defactor**epoch_learning_rate_exponent
            parameters = filter(lambda p: p.requires_grad, net.parameters())
            if ConfigNetwork.train_vae:
                optimizer = pyro_optim.Adam({'lr': lr})
            else:
                optimizer = optim.Adam(parameters, lr = lr)
            base_file_pattern = os.path.join(ConfigNetwork.storage_dir, '{}_epoch_{}'.format(ConfigNetwork.modelname, epoch))
            epoch_net_file = '{}_model'.format(base_file_pattern)
            if epoch < ConfigNetwork.freeze_ResNet_epochs:
                net.set_ResNet_requires_grad(requires_grad=False)
            else:
                net.set_ResNet_requires_grad(requires_grad=True)
            if not os.path.exists(epoch_net_file) and epoch == 0:
                # init meta embeddings network
                if ConfigNetwork.train_with_meta_embeddings:
                    logging.debug('init B with plda expectation')
                    if not os.path.exists(ConfigNetwork.embeddings_file):
                        dataset = SoftMaxDatabase(
                            imageFolderDataset=ConfigFaceDatasets.dataset_class(root=ConfigFaceDatasets.training_dir),
                            transform=train_dataloader.dataset.transform,
                            should_invert=False)
                        embeddings_loader = DataLoader(dataset,
                                                       shuffle=False,
                                                       num_workers=ConfigNetwork.num_workers,
                                                       batch_size=ConfigNetwork.batch_size_train)
                        softmax_net = net.to_softmaxNetwork()
                        softmax_net.normalize = False
                        with h5py.File(ConfigNetwork.embeddings_file, "a") as embd_file:
                            for i, data in enumerate(embeddings_loader, 0):
                                img0, label = data
                                img0, label = Variable(img0).cuda(), Variable(label).cuda()
                                output0 = super(SoftMaxNetwork, softmax_net).forward_once(img0)
                                embd_file.create_dataset("{}".format(i),
                                                         data=numpy.column_stack(
                                                             (output0.data.cpu().numpy(), label.data.cpu().numpy())),
                                                         compression="gzip",
                                                         fletcher32=True)
                            logging.critical('extracted embeddings')
                    if not os.path.exists(ConfigNetwork.embeddings_file_plda) or not os.path.exists(ConfigNetwork.embeddings_mean_file):
                        data = []
                        with h5py.File(ConfigNetwork.embeddings_file, "r") as h5f:
                            for key, value in h5f.items():
                                data.append(value.value)
                        data = numpy.concatenate(data)
                        embeddings = data[:,:ConfigNetwork.embedding_size]
                        embeddings_mean = embeddings.mean(0)
                        numpy.save(ConfigNetwork.embeddings_mean_file, embeddings_mean)
                        logging.debug('embeddings mean: {}'.format(embeddings.mean(0)))
                        embeddings -= embeddings.mean(0)
                        embeddings = (embeddings.T / numpy.linalg.norm(embeddings, axis=1)).T # prepare cosine distance
                        embedding_labels = data[:, ConfigNetwork.embedding_size:].squeeze()

                        s = sidekit.StatServer()
                        s.modelset = embedding_labels
                        s.segset = numpy.arange(embedding_labels.shape[0]).astype(str)
                        s.stat0 = numpy.ones((embedding_labels.shape[0], 1))
                        s.stat1 = copy.deepcopy(embeddings)
                        s.start = numpy.empty(embedding_labels.shape[0], dtype='|O')
                        s.stop = numpy.empty(embedding_labels.shape[0], dtype='|O')
                        s.validate()
                        ids = numpy.unique(s.modelset)
                        class_nb = ids.shape[0]

                        f = sidekit.FactorAnalyser()
                        rank_f = ConfigNetwork.embedding_size
                        f.plda(s, rank_f=rank_f)
                        f.write(ConfigNetwork.embeddings_file_plda)
                    else:
                        f = sidekit.FactorAnalyser(ConfigNetwork.embeddings_file_plda)

                    e_mu = torch.from_numpy(f.mean).type(torch.FloatTensor)
                    e_B = torch.from_numpy(numpy.linalg.inv(f.Sigma).diagonal()).type(torch.FloatTensor)
                    # e_B = torch.from_numpy(numpy.linalg.inv(f.Sigma)).type(torch.FloatTensor)
                    assert(isinstance(net, GME_SoftmaxNetwork))
                    net = GME_SoftmaxNetwork(num_train_classes=net.num_train_classes,pretrained_siamese_net=net.pretrained_net,expected_mu=e_mu,expected_B=e_B).cuda()
                    logging.debug('init B with plda done')

            if not os.path.exists(epoch_net_file):
                if last_model_loaded:
                    logging.critical('run validation on epoch {}'.format(epoch-1))
                    test_model(database_dir=test_dataloader,
                               net=net,
                               net_distance=net_distance,
                               epoch=None
                               )
                    last_model_loaded = False

                if ConfigNetwork.select_difficult_pairs_epoch is not None:
                    if epoch == ConfigNetwork.select_difficult_pairs_epoch:
                        train_dataloader = select_difficult_pairs(net, train_dataloader)

                # train an epoch
                net.train()
                train_epoch(train_dataloader=train_dataloader, net=net, optimizer=optimizer, epoch=epoch, iteration_number=iteration_number)
                torch.save(obj=net.state_dict(), f=epoch_net_file)
            else:
                net.load_state_dict(torch.load(epoch_net_file))
                logging.info('loaded model for epoch: {}'.format(epoch))
                last_model_loaded = True
                continue

            logging.critical('run validation on epoch {}'.format(epoch))
            test_model(database_dir=test_dataloader,
                       net=net,
                       net_distance=net_distance,
                       epoch=None
                       )
        torch.save(obj=net.state_dict(), f='{}'.format(ConfigNetwork.modelname))
        logging.info('training completed, model stored.')