import json
import logging
import os
import re
import time
from math import ceil
from threading import Lock

import pandas as pd
from PyQt5.QtCore import QAbstractTableModel, Qt, pyqtSlot, QModelIndex, pyqtSignal, QItemSelection
from PyQt5.QtGui import QBrush, QColor, QKeySequence
from PyQt5.QtWidgets import QWidget, QColorDialog, QHeaderView, QShortcut, QButtonGroup, QPushButton, \
    QSpacerItem, QSizePolicy
from natsort import humansorted

import kamzik3
from kamzik3 import DeviceError, SEP, session, units
from kamzik3.constants import *
from kamzik3.devices.deviceSavedAttributes import UPDATE_INSERT_GROUPS, UPDATE_REMOVE_GROUP, UPDATE_INSERT_ROW, \
    UPDATE_VALUE, UPDATE_REMOVE_ROW
from kamzik3.gui.deviceWidget import DeviceWidget
from kamzik3.gui.templates.savedAttributesTemplate import Ui_Form
from kamzik3.snippets.snippetsUnits import device_units
from kamzik3.snippets.snippetsWidgets import show_question_dialog, show_prompt_dialog, show_error_message


class ViewModel(QAbstractTableModel):
    sig_value_update = pyqtSignal(int, int, str)
    colors = {}

    def __init__(self, model_data, from_index=0, to_index=0, parent=None):
        self.model_data = model_data
        self.from_index = from_index
        self.to_index = to_index
        self.previous_table = None
        QAbstractTableModel.__init__(self, parent)

    def columnCount(self, parent=None, *args, **kwargs):
        columns = self.model_data.columns
        return len(columns) - 1

    def rowCount(self, parent=None, *args, **kwargs):
        display = self.to_index - self.from_index
        return display

    def data(self, index, role=None):
        row, column = index.row(), index.column()
        row += self.from_index + 1
        if role == Qt.DisplayRole:
            if column <= 1:
                return "   " + self.model_data.loc[row][column + 1] + "   "
            else:
                return self.model_data.loc[row][column + 1]
        elif role == Qt.BackgroundRole:
            color = self.model_data.loc[row][0]
            if color not in self.colors:
                self.colors[color] = QBrush(QColor(color))
            return self.colors[color]
        elif role == Qt.EditRole:
            current_value = self.model_data.loc[row][column + 1]
            return current_value

    def headerData(self, p_int, Qt_Orientation, role=None):
        if role == Qt.DisplayRole and Qt_Orientation == Qt.Horizontal:
            data = self.model_data.columns[p_int + 1]
            return data
        elif role == Qt.DisplayRole and Qt_Orientation == Qt.Vertical:
            return " " + str(self.from_index + p_int + 1) + " "

    def setData(self, index, value, role=None):
        if role == Qt.EditRole:
            row, column = index.row(), index.column()
            self.sig_value_update.emit(self.from_index + row + 1, column + 1, value)
            return True
        return False

    def flags(self, index):
        if index.column() == 1:
            return Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled

        return Qt.ItemIsSelectable | Qt.ItemIsEnabled


class SavedAttributesWidget(Ui_Form, DeviceWidget):
    default_row_color = "#ffffff"
    file_path = None
    page_size = 100
    sig_refresh_group_tables = pyqtSignal(str)
    sig_refresh_group_table = pyqtSignal(bool, str, object)
    displayed_group_path = None
    column_pattern = re.compile("(.*) \[(.*)\]\[(.*)\]")

    def __init__(self, device=None, config=None, parent=None):
        self.rows_selection_mode = False
        self.button_group = QButtonGroup()
        self.update_lock = Lock()
        self.cache_h5_file = os.path.join(kamzik3.session.get_value(ATTR_CACHE_DIRECTORY), "client_save.h5")
        self.cache_h5_content = None
        self.groups = {}
        self.logger = logging.getLogger("Gui.Device.SavedAttributeWidget")

        if device is None:
            QWidget.__init__(self, parent)
        else:
            DeviceWidget.__init__(self, device, config=config, parent=parent)

        self.manipulation_buttons = [self.button_set_value, self.button_change_color, self.button_remove]
        for button in self.manipulation_buttons:
            button.setDisabled(True)

        self.preset_group_buttons = (self.combo_preset, self.button_remove_preset_group, self.button_add_preset_group)
        self.sig_refresh_group_tables.connect(self.slot_refresh_group_tables)
        self.sig_refresh_group_table.connect(self.slot_refresh_group_table)

        self.shortcut_x = QShortcut(QKeySequence("Alt+X"), self)
        self.shortcut_x.activated.connect(self.set_attribute_value)

        self.shortcut_a = QShortcut(QKeySequence("Alt+A"), self)
        self.shortcut_a.activated.connect(self.add_row)

        self.shortcut_d = QShortcut(QKeySequence("Alt+D"), self)
        self.shortcut_d.activated.connect(self.remove_rows)

        self.shortcut_c = QShortcut(QKeySequence("Alt+C"), self)
        self.shortcut_c.activated.connect(self.select_color)

        self.table_attributes.setSelectionBehavior(self.table_attributes.SelectItems)
        self.table_attributes.setSelectionMode(self.table_attributes.SingleSelection)
        self.table_attributes.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        self.table_attributes.horizontalHeader().setStretchLastSection(True)
        self.table_attributes.doubleClicked.connect(self.on_double_click)

    @pyqtSlot("PyQt_PyObject")
    def slot_set_status(self, value):
        if value in READY_DEVICE_STATUSES and not self.configured:
            self.configured = False
            self.slot_handle_configuration()
            self.device.attach_attribute_callback(ATTR_LAST_UPDATE, self.content_update, key_filter=VALUE)
            with open(self.cache_h5_file, "wb") as h5_fp:
                h5_fp.write(self.device.get_value(ATTR_CONTENT))
            self.device.attach_attribute_callback(ATTR_GROUPS, self.groups_changed, key_filter=VALUE)
            self.configured = True

    @pyqtSlot(object)
    def groups_changed(self, groups):
        if groups is None:
            return

        selected_item = self.combo_preset.currentText()
        self.combo_preset.clear()
        self.groups = {}
        # Add only top groups to selection
        for group in groups:
            group_path = group.split("/")[1:]
            if group_path[0] not in self.groups:
                self.groups[group_path[0]] = [group_path[1]]
            else:
                self.groups[group_path[0]].append(group_path[1])

        # We want alphabetically sorted group names
        sorted_groups = humansorted(self.groups.keys())
        self.combo_preset.addItems(sorted_groups)
        item_index = self.combo_preset.findText(selected_item)

        # Select group which was previously selected
        if item_index != -1:
            self.combo_preset.setCurrentIndex(item_index)
        else:
            self.combo_preset.setCurrentIndex(0)

    def slot_handle_configuration(self):
        pass

    @pyqtSlot()
    def content_update(self, value):
        if value is None or not self.configured:
            return
        with self.update_lock:
            values = value.split(SEP)
            if values[0] == UPDATE_INSERT_GROUPS:
                groups_data = json.loads(values[1])
                for group, data in groups_data.items():
                    df = pd.DataFrame(data, columns=list(data.keys()))
                    df.to_hdf(self.cache_h5_file, key=group, format="table", complevel=1)
            elif values[0] == UPDATE_REMOVE_GROUP:
                with pd.HDFStore(self.cache_h5_file) as hdf:
                    for group_path in self.device.get_value(ATTR_GROUPS):
                        if group_path.startswith(f"/{values[1]}/"):
                            hdf.remove(group_path)
            elif values[0] == UPDATE_INSERT_ROW:
                group = values[1]
                df = pd.read_hdf(self.cache_h5_file, key=group, mode="a")
                data = json.loads(values[2])
                position = int(values[3])
                line = pd.DataFrame(data, index=[position])
                df2 = pd.concat([df.iloc[:position - 1], line, df.iloc[position - 1:]])
                df2.reset_index(drop=True, inplace=True)
                df2.to_hdf(self.cache_h5_file, group, format="fixed")
                subgroup = values[1].split("/")
                if subgroup[1] == self.combo_preset.currentText() and subgroup[
                    2] == self.button_group.checkedButton().text():
                    self.sig_refresh_group_table.emit(True, subgroup[-1], df2)
            elif values[0] == UPDATE_VALUE:
                group = values[1]
                df = pd.read_hdf(self.cache_h5_file, key=group, mode="a")
                df.loc[int(values[2]), df.columns[int(values[3])]] = values[4]
                df.to_hdf(self.cache_h5_file, group, format="fixed")
                subgroup = values[1].split("/")
                if subgroup[1] == self.combo_preset.currentText() and subgroup[
                    2] == self.button_group.checkedButton().text():
                    self.sig_refresh_group_table.emit(True, subgroup[-1], df)
            elif values[0] == UPDATE_REMOVE_ROW:
                group = values[1]
                df = pd.read_hdf(self.cache_h5_file, key=group, mode="a")
                df.drop(labels=int(values[2]), axis=0, inplace=True)
                df.reset_index(drop=True, inplace=True)
                df.to_hdf(self.cache_h5_file, group, format="fixed")
                subgroup = values[1].split("/")
                if subgroup[1] == self.combo_preset.currentText() and subgroup[
                    2] == self.button_group.checkedButton().text():
                    self.sig_refresh_group_table.emit(True, subgroup[-1], df)

    @pyqtSlot(str)
    def slot_refresh_group_table(self, group):
        self.preset_group_changed(self.combo_preset.currentText())

    @pyqtSlot(int, int, str)
    def slot_value_update(self, row, column, value):
        top_group = self.combo_preset.currentText()
        for sub_group in self.groups[top_group]:
            group_path = "/" + "/".join((top_group, sub_group))
            self.device.update_value(group=group_path, row=row, column=column, value=value)

    @pyqtSlot(QItemSelection, QItemSelection)
    def current_selection_changed(self, selected, deselected):
        if self.rows_selection_mode:
            for button in self.manipulation_buttons + [self.button_add]:
                button.setDisabled(True)
        else:
            self.button_add.setDisabled(False)
            for button in self.manipulation_buttons:
                button.setDisabled(True if len(selected) == 0 else False)

            if len(selected) > 0:
                if selected[0].topLeft().column() <= 1:
                    self.button_set_value.setDisabled(True)

    @pyqtSlot(QModelIndex)
    def on_double_click(self, index):
        selected_index = self.table_attributes.selectedIndexes()[0]
        if self.checkbox_allow_double_click.isChecked() and selected_index.column() > 0:
            self.set_attribute_value()

    @pyqtSlot()
    def set_attribute_value(self):
        row, column = self.get_selected_cell()
        if column is None or column <= 1:
            return

        attribute_value = self.table_attributes.model().model_data.iloc[row, column]
        column_header = self.table_attributes.model().model_data.columns[column]
        result = self.column_pattern.search(column_header)
        if result is not None:
            try:
                device_id, attribute_name, unit = result.group(1), result.group(2), result.group(3)
                self.logger.info(f"Set {device_id} {attribute_name} to value {attribute_value} {unit}")
                device = session.get_device(device_id)
                attribute_as_list = eval(f"[{attribute_name}]")
                value_in_device_units = device_units(device, attribute_as_list, units.Quantity(attribute_value, unit))
                device.set_value(attribute_as_list, value_in_device_units.m)
            except (DeviceError, Exception) as e:
                error_message = f"Cannot set {device_id} {attribute_name} to value {attribute_value} {unit}.\r\n{e}"
                show_error_message(error_message, self)

    def get_selected_cell(self):
        selected_index = self.table_attributes.selectedIndexes()
        if not selected_index:
            return None, None
        row = selected_index[0].row()

        if row is not None:
            row_offset = self.table_attributes.model().from_index
            row = row_offset + selected_index[0].row() + 1
            return row, selected_index[0].column() + 1
        else:
            return None, None

    @pyqtSlot()
    def remove_rows(self):
        with self.update_lock:
            row, column = self.get_selected_cell()
            if column is None:
                return

            answer = show_question_dialog(f"Do You really want to remove row {row} ?", "Confirmation", self)
            if answer:
                top_group = self.combo_preset.currentText()
                self.logger.info(f"Removing row {row} from group {top_group}")
                for sub_group in self.groups[top_group]:
                    group_path = "/" + "/".join((top_group, sub_group))
                    self.device.remove_row(group=group_path, row=row)

    @pyqtSlot()
    def add_row(self):
        with self.update_lock:
            user_comment = show_prompt_dialog("Type Your comment:", "Add new row", self)
            if user_comment:
                top_group = self.combo_preset.currentText()
                self.logger.info(f"Prepending new row in group {top_group}")
                for subgroup in self.groups[top_group]:
                    group_path = os.path.join("/", top_group, subgroup)
                    df = pd.read_hdf(self.cache_h5_file, key=group_path)
                    table_row = ["#fff", time.strftime("%d-%m-%Y %H:%M:%S"), user_comment]
                    for column in df.columns[3:]:
                        result = self.column_pattern.search(column)
                        if result is not None:
                            try:
                                device_id, attribute_name, unit = result.group(1), result.group(2), result.group(3)
                                attribute_as_list = eval(f"[{attribute_name}]")
                                device = session.get_device(device_id)
                                current_value = device.get_attribute(attribute_as_list).value().to(unit)
                                table_row.append(current_value.m)
                            except (DeviceError, Exception):
                                table_row.append(None)
                        else:
                            table_row.append(None)
                    table_row = [str(v) for v in table_row]
                    self.device.insert_row(group=group_path, data=dict(zip(df.columns, table_row)), position=2)

    @pyqtSlot()
    def select_color(self):
        with self.update_lock:
            row, column = self.get_selected_cell()
            if row is None:
                return

            color = QColorDialog().getColor()
            if color is not None and color.name() != "#000000":
                self.slot_value_update(row, 0, str(color.name()))

    @pyqtSlot()
    def add_preset_group(self):
        with self.update_lock:
            group_name = show_prompt_dialog("Set name of a new group", "New save group")
            if group_name and group_name != "":
                self.logger.info(f"Adding new group {group_name}")
                groups = {}
                top_group = self.combo_preset.currentText()
                for subgroup in self.groups[top_group]:
                    group_path = os.path.join("/", top_group, subgroup)
                    new_group_path = os.path.join("/", group_name, subgroup)
                    df = pd.read_hdf(self.cache_h5_file, key=group_path)
                    groups[new_group_path] = dict(zip(df.columns, [["None"]] * len(df.columns)))
                self.device.insert_groups(groups_data=groups)

    @pyqtSlot()
    def remove_preset_group(self):
        with self.update_lock:
            current_group = self.combo_preset.currentText()
            answer = show_question_dialog(f"Do You really want to remove group {current_group} ?", "Confirmation", self)
            if answer:
                self.logger.info(f"Removing group {current_group}")
                self.device.remove_group(group=current_group)

    @pyqtSlot("QString")
    def slot_refresh_group_tables(self, top_group):
        """
        Preset group was changed.
        We need to update attribute table.
        @param top_group: name of top group
        @type top_group: string
        """
        if top_group == "":
            return

        self.input_page.blockSignals(True)
        self.input_page.setValue(1)
        self.input_page.blockSignals(False)

        while self.layout_subgroups_select.count() != 0:
            self.layout_subgroups_select.removeItem(self.layout_subgroups_select.itemAt(0))
        self.button_group = QButtonGroup()
        for button_index, subgroup in enumerate(self.groups[top_group]):
            button = QPushButton(subgroup)
            button.setStyleSheet("QPushButton {background-color:silver;} QPushButton:checked {background-color:#f6b442;font-weight:bold}")
            button.setCheckable(True)
            button.setAutoExclusive(True)
            button.setMinimumHeight(30)
            button.toggled.connect(lambda state, subgroup=subgroup: self.slot_refresh_group_table(state, subgroup))
            self.button_group.addButton(button, button_index)
            self.layout_subgroups_select.addWidget(button)
        self.layout_subgroups_select.addSpacerItem(QSpacerItem(0, 0, hPolicy=QSizePolicy.Expanding))
        self.button_group.button(0).setChecked(True)
        self.check_page_borders()

    pyqtSignal(bool, str)
    pyqtSignal(bool, str, object)

    def slot_refresh_group_table(self, state, subgroup, model_data=None):
        if not state:
            return

        row_offset = (self.input_page.value() - 1) * self.page_size
        top_group = self.combo_preset.currentText()
        with self.update_lock:
            if model_data is None:
                model_data = pd.read_hdf(self.cache_h5_file, "/".join([top_group, subgroup]))
            row_count = len(model_data.index) - 1
            to_index = (row_offset + self.page_size) if row_count >= (row_offset + self.page_size) else row_count
            view_model = ViewModel(model_data, from_index=row_offset, to_index=to_index)
            view_model.sig_value_update.connect(self.slot_value_update)
            self.table_attributes.setModel(view_model)
            self.table_attributes.horizontalHeader().setSectionResizeMode(0, QHeaderView.ResizeToContents)
            self.table_attributes.horizontalHeader().setSectionResizeMode(1, QHeaderView.Interactive)
            self.table_attributes.selectionModel().selectionChanged.connect(self.current_selection_changed)
            self.input_page.blockSignals(True)
            pages_count = ceil(row_count / self.page_size)
            self.input_page.setMaximum(pages_count if pages_count > 0 else 1)
            self.input_page.blockSignals(False)

    @pyqtSlot()
    def slot_jump_to_first_page(self):
        self.input_page.setValue(1)

    @pyqtSlot()
    def slot_jump_to_last_page(self):
        self.input_page.setValue(self.input_page.maximum())

    @pyqtSlot("int")
    def slot_jump_to_page(self, page_index):
        self.slot_refresh_group_table(True, self.button_group.checkedButton().text())
        self.check_page_borders()

    def check_page_borders(self):
        page_index = self.input_page.value()
        at_max = page_index == self.input_page.maximum()
        at_min = page_index == 1
        self.input_page.setSuffix(f" / {self.input_page.maximum()}")
        self.button_next_page.setDisabled(at_max)
        self.button_last_page.setDisabled(at_max)
        self.button_previous_page.setDisabled(at_min)
        self.button_first_page.setDisabled(at_min)
