# -*- coding: utf-8 -*-
"""
/***************************************************************************
 FastFieldFillerDockWidget
                                 A QGIS plugin
 The plugin was created to quickly fill in the fields in the attribute table.
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                             -------------------
        begin                : 2022-05-30
        git sha              : $Format:%H$
        copyright            : (C) 2022 by MrChebur
        email                : mrCheburPlugins@inbox.ru
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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.                                   *
 *                                                                         *
 ***************************************************************************/
"""

import os
import json

import qgis.core
from qgis.PyQt import QtGui, QtWidgets, uic
from qgis.PyQt.QtWidgets import QShortcut, QMessageBox, QAction, QMenu
from qgis.PyQt.QtCore import pyqtSignal, Qt
from qgis.core import Qgis

FORM_CLASS, _ = uic.loadUiType(os.path.join(os.path.dirname(__file__), 'fast_field_filler_dockwidget_base.ui'))


class FastFieldFillerDockWidget(QtWidgets.QDockWidget, FORM_CLASS):
    closingPlugin = pyqtSignal()
    plugin_name = 'Fast Field Filler'

    def __init__(self, iface, parent=None):
        """Constructor."""
        super(FastFieldFillerDockWidget, self).__init__(parent)
        self.setupUi(self)
        self.iface = iface

        # other parameters
        self.check_box_redraw.setChecked(True)
        self.use_numpad_hotkeys.setChecked(True)
        self.check_box_use_hotkeys.setChecked(False)
        self.number_of_rows_in_ui = 20
        self.fill_value = None
        self.max_features_selected = 7
        self.max_classes = 10
        self.max_classes_prev = self.max_classes
        self.json_path = None

        # dictionaries
        self.buttons_dict = {}
        self.line_edits_dict = {}
        self.classes_dict = {key: None for key in range(1, self.max_classes + 1, 1)}
        self.hotkeys_shortcuts_dict = {key: None for key in range(1, self.max_classes + 1, 1)}

        # hotkeys
        self.numbers_dict = {1: Qt.Key_1,
                             2: Qt.Key_2,
                             3: Qt.Key_3,
                             4: Qt.Key_4,
                             5: Qt.Key_5,
                             6: Qt.Key_6,
                             7: Qt.Key_7,
                             8: Qt.Key_8,
                             9: Qt.Key_9,
                             10: Qt.Key_0,
                             }
        self.f_numbers_dict = {1: Qt.Key_F1,
                               2: Qt.Key_F2,
                               3: Qt.Key_F3,
                               4: Qt.Key_F4,
                               5: Qt.Key_F5,
                               6: Qt.Key_F6,
                               7: Qt.Key_F7,
                               8: Qt.Key_F8,
                               9: Qt.Key_F9,
                               10: Qt.Key_F10,
                               11: Qt.Key_F11,
                               12: Qt.Key_F12,
                               }

        # connections: hotkeys
        self.check_box_use_hotkeys.stateChanged.connect(self.turn_all_hotkeys_on_off)
        self.check_box_use_hotkeys.stateChanged.connect(self.update_current_hotkeys_label)
        # connections: hotkeys - modifiers
        self.check_box_shift.stateChanged.connect(self.change_all_hotkeys)
        self.check_box_alt.stateChanged.connect(self.change_all_hotkeys)
        self.check_box_ctrl.stateChanged.connect(self.change_all_hotkeys)
        # connections: hotkeys - numpad or F1-F12
        self.use_f_hotkeys.clicked.connect(self.change_all_hotkeys)
        self.use_numpad_hotkeys.clicked.connect(self.change_all_hotkeys)

        # connections: max classes
        self.spin_box_max_classes.setValue(self.max_classes)
        self.spin_box_max_classes.valueChanged.connect(self.updateMaxClassesSpinBox)
        self.spin_box_max_classes.valueChanged.connect(self.redraw_buttons_and_line_edits)
        self.spin_box_max_classes.textChanged.connect(self.updateMaxClassesSpinBox)
        self.spin_box_max_classes.textChanged.connect(self.redraw_buttons_and_line_edits)

        # connections: layer selection
        self.cbo_layer.setFilters(qgis.core.QgsMapLayerProxyModel.HasGeometry)
        self.cbo_layer.layerChanged.connect(self.connect_current_layer)
        self.cbo_layer.layerChanged.connect(self.updateSelectedFeaturesLabel)
        self.cbo_layer.layerChanged.connect(lambda: self.cbo_attribute.setLayer(self.cbo_layer.currentLayer()))
        self.cbo_layer.layerChanged.connect(self.layer_changed_commit_buttons_on_off)
        self.cbo_layer.layerChanged.connect(self.updateSelectedFeaturesLabel)

        # parameters: max features
        self.line_edit_max_features.setValidator(QtGui.QIntValidator(1, 2147483647, self))  # only integer
        self.line_edit_max_features.setText(str(self.max_features_selected))
        # connections: max features
        self.line_edit_max_features.textChanged.connect(self.updateMaxSelectedFeaturesLineEdit)
        self.line_edit_max_features.textChanged.connect(self.updateSelectedFeaturesLabel)

        # commit and undo buttons
        self.push_button_commit.clicked.connect(self.commit_editing)
        self.push_button_undo.clicked.connect(self.undo_editing)

        # save and load: json buttons
        self.push_button_save_json.clicked.connect(self.save_params_as_json)
        self.push_button_load_json.clicked.connect(self.load_json_as_params)
        # save and load: path box
        self.json_path_qgs_file_widget.fileChanged.connect(self.updateLineEditJson)
        self.json_path_qgs_file_widget.setFilter(filter='*.json')

        # initialize work
        self.add_all_buttons_and_line_edits()
        self.layer_changed_commit_buttons_on_off()
        self.cbo_layer.setLayer(None)
        self.connect_current_layer()

        self.update_current_hotkeys_label()

    def closeEvent(self, event):
        self.closingPlugin.emit()
        event.accept()

    def updateSelectedFeaturesLabel(self):
        selected = None
        if isinstance(self.cbo_layer.currentLayer(), qgis.core.QgsVectorLayer):
            selected = self.cbo_layer.currentLayer().selectedFeatureCount()
        self.label_selected_features.setText(f'{selected}')

        if selected is None or self.max_features_selected is None:
            self.label_selected_features.setStyleSheet("")

        # If selected more than allowed => colorize label
        elif selected / self.max_features_selected > 1:
            self.label_selected_features.setStyleSheet("font-weight: bold; color: white; "
                                                       "background-color: red; border: 1px solid black;")
        else:
            self.label_selected_features.setStyleSheet("")

    def updateMaxSelectedFeaturesLineEdit(self):
        try:
            self.max_features_selected = int(self.line_edit_max_features.text())
        except ValueError:
            self.max_features_selected = 1

    def updateMaxClassesSpinBox(self):
        self.max_classes = self.spin_box_max_classes.value()

    def updateLineEditClass(self, class_number):
        line_edit = self.line_edits_dict[class_number]
        line_edit_value = line_edit.text()
        if line_edit_value == '':
            line_edit_value = None
        self.classes_dict[class_number] = line_edit_value

    def fillFeatures(self, class_number):
        layer = self.cbo_layer.currentLayer()
        value = self.classes_dict[class_number]

        if layer is None:
            self.message('Choose a layer!')
            return

        # The problem is that the type of field depends on the data source!
        # Must be lower case!
        supported_field_types = {'int': ['integer', 'integer64', 'int2', 'int4', 'int8'],
                                 'real': ['real', 'float8', 'decimal', 'numeric'],
                                 'bool': ['boolean', 'bool'],
                                 'str': ['string', 'char', 'varchar', 'text', 'citext'],
                                 }

        if layer.selectedFeatureCount() <= 0:
            self.message(f'Select at least 1 feature!')
        elif layer.selectedFeatureCount() > self.max_features_selected:
            self.message(f'{layer.selectedFeatureCount()} features selected, '
                         f'but maximum is: {self.max_features_selected}')
        else:
            if not layer.isEditable():
                started_editing = layer.startEditing()
                if not started_editing:
                    self.message(f'Unable to start editing layer: {layer.source()}', duration=8)

            if layer.isEditable():
                field_str = self.cbo_attribute.currentField()
                field_index = layer.fields().indexFromName(field_str)
                field_type = layer.fields()[field_index].typeName().lower()

                supported_field_types_list = sum(supported_field_types.values(), [])
                if field_type not in supported_field_types_list:

                    self.message(f'The field `{field_str}` of type `{field_type}` is not supported.'
                                 f' Supported types are: {supported_field_types}', duration=60)
                else:
                    try:
                        if value is not None:
                            if field_type in supported_field_types['str']:
                                pass
                            elif field_type in supported_field_types['int']:
                                value = int(value)
                            elif field_type in supported_field_types['real']:
                                value = float(value)
                            elif field_type in supported_field_types['bool']:
                                if value.lower() in ['true', '1']:
                                    value = True
                                elif value.lower() in ['false', '0']:
                                    value = False
                                else:
                                    raise ValueError
                    except ValueError:
                        self.message(f'Cannot convert `{value}` to `{field_type}` field type.', duration=60)
                    else:
                        all_successful = []
                        for feature in layer.getSelectedFeatures():
                            success = layer.changeAttributeValue(feature.id(), field_index, value)
                            all_successful.append(success)
                            if all(all_successful):
                                self.message(f'{value} inserted into {layer.selectedFeatureCount()} features.',
                                             warn_type=Qgis.Success, duration=3)
        self.refresh_map_canvas()

    def move_widget_to_bottom(self, grid_layout, widget, row_shift, column):
        grid_layout.addWidget(widget, grid_layout.rowCount() + 1 + row_shift, column)
        self.set_zero_row_stretch(grid_layout)
        self.set_max_row_stretch_for_last_widget(grid_layout)

    @staticmethod
    def set_zero_row_stretch(grid_layout):
        stretch = 0
        for num in (0, grid_layout.rowCount()):
            grid_layout.setRowStretch(num, stretch)

    @staticmethod
    def set_max_row_stretch_for_last_widget(grid_layout):
        stretch = 9999999
        grid_layout.setRowStretch(grid_layout.rowCount(), stretch)

    def update_current_hotkeys_label(self):
        top_text = f'Currently enabled hotkeys:\n'
        hotkeys_text = ''
        for num, q_shortcut in self.hotkeys_shortcuts_dict.items():  # q_shortcut is qgis.PyQt.QtWidgets.QShortcut()
            if q_shortcut is None:
                continue
            elif q_shortcut.isEnabled():
                q_shortcut_as_text = q_shortcut.key().toString()
                hotkeys_text += f'Class {num}: {q_shortcut_as_text}\n'

        if hotkeys_text == '':
            hotkeys_text = 'None'

        self.label_current_hotkeys.setText(top_text + hotkeys_text)

    def turn_hotkey_on_off(self, class_number, forced=False):
        if forced:
            state = True
        else:
            state = self.check_box_use_hotkeys.checkState() and self.buttons_dict[class_number].isVisible()

        dictionary = self.hotkeys_shortcuts_dict
        if class_number in dictionary:
            q_shortcut = dictionary[class_number]
            if isinstance(q_shortcut, QShortcut):
                q_shortcut.setEnabled(state)
                dictionary[class_number] = q_shortcut

        self.update_current_hotkeys_label()

    def turn_all_hotkeys_on_off(self):
        current_state = self.check_box_use_hotkeys.checkState()
        for class_number in range(1, self.max_classes + 1):
            self.turn_hotkey_on_off(class_number, forced=current_state)

    def add_new_line_edit(self, class_number):
        UI_COLUMN = 0
        ROW_SPAN = 1
        line_edit = QtWidgets.QLineEdit()
        line_edit.setAlignment(Qt.AlignRight)
        line_edit.textChanged.connect(lambda: self.updateLineEditClass(class_number))
        self.line_edits_dict[class_number] = line_edit
        self.main_tab_grid.addWidget(line_edit, self.number_of_rows_in_ui + class_number, UI_COLUMN, ROW_SPAN,
                                     self.main_tab_grid.columnCount() - 1)
        if class_number in self.classes_dict:
            line_edit.setText(self.classes_dict[class_number])

        self.move_widget_to_bottom(self.main_tab_grid, self.label_empty1, 0, 1)

    def refresh_map_canvas(self):
        if self.check_box_redraw.checkState():
            self.iface.mapCanvas().redrawAllLayers()

    def add_all_buttons_and_line_edits(self):
        for num in range(1, self.max_classes + 1):
            if num not in self.classes_dict:
                self.classes_dict[num] = None
            self.add_new_line_edit(num)
            self.add_new_button(num)

    def add_new_button(self, class_number):
        button = QtWidgets.QPushButton(f'Fill class {class_number}')
        button.clicked.connect(lambda: self.fillFeatures(class_number))

        self.buttons_dict[class_number] = button
        self.main_tab_grid.addWidget(button,
                                     self.number_of_rows_in_ui + class_number,
                                     self.main_tab_grid.columnCount() - 1)
        self.assign_hotkey(class_number)

    def assign_hotkey(self, class_number):
        if self.use_f_hotkeys.isChecked():
            hotkeys_dict = self.f_numbers_dict
            max_class = 12

        elif self.use_numpad_hotkeys.isChecked():
            hotkeys_dict = self.numbers_dict
            max_class = 10

        else:
            error_text = 'DEBUG! This message should not appear.' \
                         'Use one of the dictionaries is a must: ' \
                         'self.f_numbers_dict' \
                         'self.numbers_dict'
            self.message_print_for_debug(error_text)
            raise error_text

        if 1 <= class_number <= max_class:
            q_shortcut = QShortcut(QtGui.QKeySequence(Qt.KeypadModifier + hotkeys_dict[class_number]),
                                   self.iface.mainWindow())
            self.__assign_hotkey(q_shortcut, lambda: self.fillFeatures(class_number))
            state = self.check_box_use_hotkeys.checkState()
            q_shortcut.setEnabled(state)
            self.hotkeys_shortcuts_dict[class_number] = q_shortcut

    @staticmethod
    def __assign_hotkey(q_shortcut, lambda_method):
        """
        :param q_shortcut: Example:
        QShortcut(QtGui.QKeySequence(Qt.Key_F9), self.iface.mainWindow())
        :param lambda_method: Example:
        lambda: self.message(f'Pressed: {q_shortcut2.key().toString()}')
        :return:
        """
        shortcut = q_shortcut
        shortcut.setContext(Qt.ApplicationShortcut)
        shortcut.activated.connect(lambda_method)

    def change_all_hotkeys(self):
        # use CTRL ALT SHIFT
        key_modifiers = []
        if self.check_box_ctrl.checkState():
            key_modifiers.append(Qt.CTRL)
        if self.check_box_alt.checkState():
            key_modifiers.append(Qt.ALT)
        if self.check_box_shift.checkState():
            key_modifiers.append(Qt.SHIFT)

        # use F1-F12
        if self.use_f_hotkeys.isChecked():
            hotkeys_dict = self.f_numbers_dict
        # use numpad 0-9
        elif self.use_numpad_hotkeys.isChecked():
            key_modifiers.append(Qt.KeypadModifier)
            hotkeys_dict = self.numbers_dict
        # This shouldn't happen!
        else:
            hotkeys_dict = None

        # change every hotkey
        for class_number in range(1, self.max_classes + 1):
            new_key = sum(key_modifiers) + hotkeys_dict[class_number]
            self.change_button_hotkey(class_number, new_key)
        self.update_current_hotkeys_label()

    def change_button_hotkey(self, class_number: int, new_key: Qt.Key):
        if class_number in self.hotkeys_shortcuts_dict:
            hotkey = self.hotkeys_shortcuts_dict[class_number]
            hotkey.setKey(new_key)  # example: setKey(Qt.CTRL + Qt.Key_P) setKey("Ctrl+P")
            self.hotkeys_shortcuts_dict[class_number] = hotkey

    def redraw_buttons_and_line_edits(self):
        # more => show
        if self.max_classes_prev < self.max_classes:
            for num in range(1, self.max_classes + 1):
                self.line_edits_dict[num].show()
                self.buttons_dict[num].show()
                self.turn_hotkey_on_off(num)

                if num not in self.classes_dict:
                    self.classes_dict[num] = None

        # less => hide
        elif self.max_classes_prev > self.max_classes:
            for num in range(self.max_classes_prev, self.max_classes, -1):
                self.line_edits_dict[num].hide()
                self.buttons_dict[num].hide()
                self.turn_hotkey_on_off(num)

        # equal => update only the line edit values (in case of loading json data)
        elif self.max_classes_prev == self.max_classes:
            for num in range(1, self.max_classes + 1):
                if num in self.classes_dict:
                    self.line_edits_dict[num].setText(self.classes_dict[num])
                    self.turn_hotkey_on_off(num)

        self.max_classes_prev = self.max_classes
        self.update_current_hotkeys_label()

    def commit_editing(self):
        commited = self.cbo_layer.currentLayer().commitChanges()
        if commited:
            self.message(f'Saved!', Qgis.Success)
            self.disable_commit_buttons()
        else:
            self.message(f'Unable to commit changes: {self.cbo_layer.currentLayer().commitErrors()}', Qgis.Warning)

    def undo_editing(self):
        self.cbo_layer.currentLayer().rollBack()
        self.message(f'All the changes are rolled back!', Qgis.Success)
        self.disable_commit_buttons()

    def enable_commit_buttons(self):
        self.push_button_commit.setDisabled(False)
        self.push_button_undo.setDisabled(False)

    def disable_commit_buttons(self):
        self.push_button_commit.setDisabled(True)
        self.push_button_undo.setDisabled(True)

    def layer_changed_commit_buttons_on_off(self):
        if self.cbo_layer.currentLayer() is not None:
            if self.cbo_layer.currentLayer().isEditable():
                self.enable_commit_buttons()
            else:
                self.disable_commit_buttons()

    def connect_current_layer(self):
        layer = self.cbo_layer.currentLayer()

        if layer is None:
            # expected layers list are empty somehow (dunno why)
            expected_layers = self.cbo_layer.exceptedLayerList()

            if len(expected_layers) >= 1:
                layer = expected_layers[0]
                self.cbo_layer.setLayer(layer)

        if layer is not None:
            # self.disconnect_previous_layer()
            layer.selectionChanged.connect(self.updateSelectedFeaturesLabel)
            # enable commit buttons
            layer.editCommandStarted.connect(self.enable_commit_buttons)
            layer.editCommandDestroyed.connect(self.enable_commit_buttons)
            layer.beforeEditingStarted.connect(self.enable_commit_buttons)
            # disable commit buttons
            layer.editCommandEnded.connect(self.disable_commit_buttons)
            layer.afterRollBack.connect(self.disable_commit_buttons)
            # self.previous_layer = layer
            # self.select_first_available_field()

    def updateLineEditJson(self):
        path = self.json_path_qgs_file_widget.filePath()
        if path == '':
            self.json_path = None
        else:
            self.json_path = path

    def save_params_as_json(self):
        json_dict = {'max_features_selected': self.max_features_selected,
                     'max_classes': self.max_classes,
                     'classes_dict': self.classes_dict,
                     }
        if self.json_path is None:
            self.message(f'The path to the Json file is not set!')
        else:
            folder = os.path.dirname(self.json_path)
            if not self.json_path.endswith('.json'):
                self.message(f'The file extention is not *.json: {self.json_path}')

            elif not os.path.exists(folder):
                self.message(f'There is no such folder: {folder}')
            else:
                with open(self.json_path, 'w') as file:
                    json.dump(json_dict, file, indent=4)
                self.message(f'Saved as json file: {self.json_path}', Qgis.Success)

    def load_json_as_params(self):
        if self.json_path is None:  # empty
            self.message(f'The path to the Json file is not set!')

        elif not os.path.isfile(self.json_path):  # not a file
            self.message(f'The Json file does not exist: {self.json_path}')

        elif os.path.isfile(self.json_path):  # is file

            if not self.json_path.endswith('.json'):  # not json format
                self.message(f'The file extention is not *.json: {self.json_path}')
            else:
                with open(self.json_path, 'r') as file:
                    json_dict = json.load(file)

                # update parameters and all UI
                self.classes_dict = {int(key): val for key, val in json_dict['classes_dict'].items()}
                self.spin_box_max_classes.setValue(int(json_dict['max_classes']))
                self.redraw_buttons_and_line_edits()
                self.turn_all_hotkeys_on_off()
                self.message(f'New classes loaded from json file: {self.json_path}', Qgis.Success)

    def message_print_for_debug(self, text):
        self.iface.messageBar().pushMessage(FastFieldFillerDockWidget.plugin_name, str(text), Qgis.Warning)

    def message(self, text, warn_type=Qgis.Warning, duration=5):
        self.iface.messageBar().pushMessage(FastFieldFillerDockWidget.plugin_name, text, warn_type, duration)

# TODO: support as many data types as possible
# TODO: if "save combobox" is empty - you can't save a file! You have to manually create new json file and overwrite it.
#  Example:
#  {
#     "max_features_selected": 10,
#     "max_classes": 6,
#     "classes_dict": {
#         "1": "circle",
#         "2": "triangle",
#         "3": "square",
#         "4": "blue",
#         "5": "green",
#         "6": "red",
#         "7": null,
#         "8": null
#     }
# }
