from PyQt5 import QtCore, QtWidgets
from optimeed.core.collection import CollectionsToVisualise
from optimeed.core.tools import rgetattr, indentParagraph
import sys
import traceback
import numpy as np
from abc import ABCMeta, abstractmethod


if QtWidgets.QApplication.instance() is None:
    app = QtWidgets.QApplication(sys.argv)  # must initialize only once


class Action_on_selector_update(metaclass=ABCMeta):
    @abstractmethod
    def selector_updated(self, selection_name, the_collection, indices_data):
        """
        Action to perform once the data have been selected
        :param selection_name: name of the selection (deprecated ?)
        :param the_collection: the collection
        :param indices_data: indices of the data
        :return:
        """
        pass


class Attribute_selector:
    # Contains min max values for either list or scalar:
    # If list, displays from self.children_list [Attribute_selector]
    # Otherwise displays from self.min/max_value_box

    def __init__(self, attribute_name, value):
        self.attribute_name = attribute_name
        self.value = value
        self.min_value_box = None
        self.max_value_box = None
        self.children_list = []

    def add_child(self, child):
        self.children_list.append(child)

    def get_children(self):
        return self.children_list

    def get_name(self):
        return '{:.50}'.format(self.attribute_name)

    def get_min_max_attributes(self):
        min_max_attributes = []

        def _get_min_max_attributes(the_attribute_selector, min_max_attr):
            if not len(the_attribute_selector.children_list):
                attribute_min_valid = True
                try:
                    min_value = float(the_attribute_selector.min_value_box.text())
                except ValueError:
                    min_value = float("-inf")
                    attribute_min_valid = False

                attribute_max_valid = True
                try:
                    max_value = float(the_attribute_selector.max_value_box.text())
                except ValueError:
                    max_value = float("inf")
                    attribute_max_valid = False

                if attribute_max_valid or attribute_min_valid:
                    min_max_attr.append((min_value, max_value, the_attribute_selector.attribute_name))
            else:
                for child in the_attribute_selector.get_children():
                    _get_min_max_attributes(child, min_max_attr)

        _get_min_max_attributes(self, min_max_attributes)
        return min_max_attributes

    def __str__(self):
        theStr = ''
        for child in self.children_list:
            theStr += indentParagraph(str(child), 1)
        if not len(self.children_list):
            theStr += self.get_name() + ': \t' + str(self.value)
        return theStr


class Container_attribute_selector:
    def __init__(self, containerName):
        self.children = []
        self.containerName = containerName
        self.attribute_selectors = []

    def add_child(self, child):
        self.children.append(child)

    def add_attribute_selector(self, attribute_selector):
        self.attribute_selectors.append(attribute_selector)

    def set_attribute_selectors(self, attribute_selectors):
        self.attribute_selectors = attribute_selectors

    def get_name(self):
        return self.containerName

    def get_children(self):
        return self.children

    def get_attribute_selectors(self):
        return self.attribute_selectors

    def __str__(self):
        theStr = ''
        for item in self.attribute_selectors:
            theStr += str(item) + '\n'

        index = 1
        for item in self.children:
            theStr += str(index) + '\n'
            theStr += indentParagraph(str(item))
            index += 1
        return theStr


class GuiDataSelector(QtWidgets.QMainWindow):
    def __init__(self, collections_in: CollectionsToVisualise, actionOnUpdate: Action_on_selector_update):
        """
        Base on collection, display a list of variables that can be updated.
        When the button "update" is pressed, select the datas in Collection and call the method methodToCallWhenUpdated
        - args:
            collection_in (type Collection) containing the data to select
            Action_on_selector_update
        """

        """Store variables"""
        self.theCollectionsToVisualise = collections_in
        self.theActionOnUpdate = actionOnUpdate

        """Generate GUI"""
        super(GuiDataSelector, self).__init__()
        self.mainbox = QtWidgets.QWidget()

        scrollarea = QtWidgets.QScrollArea()
        scrollarea.setWidget(self.mainbox)
        scrollarea.setWidgetResizable(True)
        scrollarea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
        self.setCentralWidget(scrollarea)

        layout = QtWidgets.QVBoxLayout(scrollarea)
        self.mainbox.setLayout(layout)

        # Create a layout for a global name
        self.selectionName = QtWidgets.QLineEdit("Default Name")
        self.mainbox.layout().addWidget(self.selectionName)

        # For each attribute, create new layout
        self.main_container = Container_attribute_selector('Device')
        get_attr_object(self.main_container, self.theCollectionsToVisualise.get_collections()[0].get_data()[0])

        def collapse(theGroupBox, theFrame):
            if theGroupBox.isChecked():
                theFrame.show()
            else:
                theFrame.hide()

        def create_frames(theContainer, parent_layout, curr_recursion):
            MAX_RECURSION = 2

            theGroupBox = QtWidgets.QGroupBox(theContainer.get_name())
            theGroupBox.setCheckable(True)
            theGroupBox.setChecked(True)

            parent_layout.addWidget(theGroupBox)

            # Little trick here: to be able to hide only the content, put a frame in a layout in the groupbox, and hide the frame
            new_layout_temp = QtWidgets.QVBoxLayout(theGroupBox)
            new_frame = QtWidgets.QFrame()
            theGroupBox.clicked.connect(lambda: collapse(theGroupBox, new_frame))

            if curr_recursion >= MAX_RECURSION:
                new_frame.hide()
                theGroupBox.setChecked(False)
            new_layout_temp.addWidget(new_frame)
            new_layout = QtWidgets.QVBoxLayout(new_frame)

            for attribute_selector in theContainer.get_attribute_selectors():
                # create
                horizontal_layout = QtWidgets.QHBoxLayout()
                new_layout.addLayout(horizontal_layout)

                # First: label
                label = QtWidgets.QLabel()
                label.setText(attribute_selector.get_name())
                horizontal_layout.addWidget(label, alignment=QtCore.Qt.AlignTop)

                vertical_layout = QtWidgets.QVBoxLayout()
                horizontal_layout.addLayout(vertical_layout)

                min_value_layout = QtWidgets.QHBoxLayout()
                vertical_layout.addLayout(min_value_layout)
                min_value_layout.addStretch()

                max_value_layout = QtWidgets.QHBoxLayout()
                vertical_layout.addLayout(max_value_layout)
                max_value_layout.addStretch()

                separator = QtWidgets.QFrame()
                separator.setFrameShape(QtWidgets.QFrame.HLine)
                new_layout.addWidget(separator)

                # Create buttons
                create_buttons(attribute_selector, min_value_layout, max_value_layout)

            for child in theContainer.get_children():
                create_frames(child, new_layout, curr_recursion=curr_recursion + 1)

        def create_buttons(the_attribute_selector, min_value_layout, max_value_layout):
            if not len(the_attribute_selector.get_children()):
                the_attribute_selector.min_value_box = QtWidgets.QLineEdit(None)
                the_attribute_selector.min_value_box.setFixedWidth(50)
                min_value_layout.addWidget(the_attribute_selector.min_value_box, alignment=QtCore.Qt.AlignLeft)

                the_attribute_selector.max_value_box = QtWidgets.QLineEdit(None)
                the_attribute_selector.max_value_box.setFixedWidth(50)
                max_value_layout.addWidget(the_attribute_selector.max_value_box, alignment=QtCore.Qt.AlignLeft)
            else:
                for child in the_attribute_selector.get_children():
                    create_buttons(child, min_value_layout, max_value_layout)

        create_frames(self.main_container, layout, 1)

        # Create a button in the window
        dock = QtWidgets.QDockWidget(self)
        button = QtWidgets.QPushButton('Update', self)
        dock.setWidget(button)
        self.addDockWidget(QtCore.Qt.TopDockWidgetArea, dock)

        button.clicked.connect(self.apply_filters)
        self.resize(self.mainbox.sizeHint())

    def apply_filters(self, _):
        try:
            theCollections = self.theCollectionsToVisualise.get_collections()
            for collection in theCollections:
                theData = collection.get_data()
                selected_data_indices = []
                for data_index in range(len(theData)):
                    the_item = theData[data_index]

                    if is_object_selected(self.main_container, the_item):
                        selected_data_indices.append(data_index)
                self.theActionOnUpdate.selector_updated(self.selectionName.text(), collection, selected_data_indices)
        except KeyboardInterrupt:
            raise
        except Exception:
            print("Following error occurred in selector :" + traceback.format_exc())

        # textboxValue = self.textbox.text()

    def run(self):
        self.show()
        # app.exec_()


def is_object_selected(container_in, object_in):
    for attribute_selector in container_in.get_attribute_selectors():
        min_max_attribute = attribute_selector.get_min_max_attributes()
        for min_attr, max_attr, name_attr in min_max_attribute:
            # Reverse boundaries if max < min
            if min_attr > max_attr:
                buff = min_attr
                min_attr = max_attr
                max_attr = buff
            # Check if item satisfies the condition
            try:
                if not (min_attr <= rgetattr(object_in, name_attr) <= max_attr):
                    return False
            except IndexError:
                pass
    return all([is_object_selected(container, rgetattr(object_in, container.get_name())) for container in container_in.get_children()])


def check_and_add_if_float(the_container, attribute_value, attribute_name, parent=None):
    if isinstance(attribute_value, float) or isinstance(attribute_value, int):
        new_attribute = Attribute_selector(attribute_name, attribute_value)
        if parent is not None:
            parent.add_child(new_attribute)  # We put the value to the parent (because it is a list)
        else:
            the_container.add_attribute_selector(new_attribute)  # We put the value to the container
        return True
    return False


def manage_list(the_container, in_object, _listOfValues, _listName):
    new_attribute = Attribute_selector(_listName, _listOfValues)

    if all(isinstance(n, int) or isinstance(n, float) for n in _listOfValues):
        the_container.add_attribute_selector(new_attribute)
        for index in range(len(_listOfValues)):
            if index < 10:
                attribute_name_nested = _listName + '[' + str(index) + ']'
                attribute_value_nested = rgetattr(in_object, attribute_name_nested)
                check_and_add_if_float(the_container, attribute_value_nested, attribute_name_nested, parent=new_attribute)
    elif all(getattr(n, '__dict__', None) is not None for n in _listOfValues):  # If has "__dict__" attribute, more likely a class ;)
        index = 0
        for item in _listOfValues:
            new_container = Container_attribute_selector(_listName + '[' + str(index) + ']')
            the_container.add_child(new_container)
            get_attr_object(new_container, item)
            index += 1
    else:
        pass


def get_attr_object(the_container, in_object):
    for attribute_name in vars(in_object):
        attribute_value = rgetattr(in_object, attribute_name)
        if isinstance(attribute_value, list) or isinstance(attribute_value, np.ndarray):
            manage_list(the_container, in_object, attribute_value, attribute_name)
        elif getattr(attribute_value, '__dict__', None) is not None:
            new_container = Container_attribute_selector(attribute_name)
            the_container.add_child(new_container)
            get_attr_object(new_container, attribute_value)
        else:
            check_and_add_if_float(the_container, attribute_value, attribute_name)
