from typing import Dict, List, Union
from PySide2 import QtWidgets, QtCore
from PySide2.QtWidgets import QDialogButtonBox, QDialog, QPlainTextEdit

from sohstationviewer.database.process_db import (
    execute_db, trunc_add_db, execute_db_dict)

from sohstationviewer.controller.processing import (
    read_mseed_channels, detect_data_type
)
from sohstationviewer.controller.util import display_tracking_info

from sohstationviewer.view.util.enums import LogType
from sohstationviewer.view.util.one_instance_at_a_time import \
    OneWindowAtATimeDialog

INSTRUCTION = """
Select the list of SOH channels to be used in plotting.\n
Edit lists of channels to be read in the Preferred SOH List field.\n
Click "Save - Add to Main" to save the changes and add the selected list to \
main window.
"""
TOTAL_ROW = 20

COL = {'sel': 0, 'name': 1, 'dataType': 2, 'IDs': 3, 'edit': 4, 'clr': 5}


class InputDialog(QDialog):
    def __init__(self, parent=None, text=''):
        super().__init__(parent)
        self.resize(500, 300)
        self.text_box = QPlainTextEdit(self)
        self.text_box.setPlainText(text)

        button_box = QDialogButtonBox(
            QDialogButtonBox.Ok | QDialogButtonBox.Cancel, self)

        layout = QtWidgets.QFormLayout(self)
        layout.addRow(self.text_box)
        layout.addWidget(button_box)
        button_box.accepted.connect(self.accept)
        button_box.rejected.connect(self.reject)

    def get_input(self):
        return self.text_box.toPlainText()


class ChannelPreferDialog(OneWindowAtATimeDialog):
    def __init__(self, parent, dir_names):
        """
        Dialog to create lists of preferred SOH channels that users want to
            select for plotting.
        User can either fill in manually, get all channels for selected data
            type from DB or scan the current selected folder for the list.

        :param parent: QWidget/QMainWindow - widget that calls this plotting
            widget
        :param dir_names: str - absolute path to the folder that contains
            data
        """
        super(ChannelPreferDialog, self).__init__()
        self.parent = parent
        self.dir_names = dir_names
        self.setWindowTitle("SOH Channel Preferences")
        self.setGeometry(100, 100, 1100, 800)
        main_layout = QtWidgets.QVBoxLayout()
        main_layout.setContentsMargins(7, 7, 7, 7)
        self.setLayout(main_layout)

        main_layout.addWidget(QtWidgets.QLabel(INSTRUCTION))
        # ===================== soh_list_table_widget =========================
        """
        soh_list_table_widget: the table to display/edit different
            preferred channel id list
        """
        self.soh_list_table_widget: Union[QtWidgets.QTableWidget, None] = None
        """
        avail_data_types: list of available data types in DB
        """
        self.avail_data_types: List[str] = []
        """
        curr_row: current row
        """
        self.curr_row: int = -1

        # ========================== buttons ============================
        """
        add_db_chan_btn: Button to add all channels for selected
            data type from DB to add to the current row
        """
        self.add_db_chan_btn: Union[QtWidgets.QPushButton, None] = None
        """
        scan_chan_btn: Button to scan through all channels from dir_names
            folder for the list of channels and data type to add
            to the current row
        """
        self.scan_chan_btn: Union[QtWidgets.QPushButton, None] = None
        """
        save_btn: Button to save the selected preferred channel list to DB
        """
        self.save_btn: Union[QtWidgets.QPushButton, None] = None
        """
        save_add_main_btn: Button to save the selected preferred channel list
            to DB, close the dialog and add to
            parent.curr_pref_soh_list_name_txtbox
        """
        self.save_add_main_btn: Union[QtWidgets.QPushButton, None] = None
        """
        close_btn: button to close the dialog and do nothing
        """
        self.close_btn: Union[QtWidgets.QPushButton, None] = None

        self.changed = False
        # ======================= pre-define ==========================
        """
        soh_list_name_item: text box widget for name of preferred channel
            list
        """
        self.soh_list_name_item: Union[QtWidgets.QLineEdit, None] = None
        """
        data_type_combobox: dropdown list to select datatype for
            preferred channel list
        """
        self.data_type_combobox: Union[QtWidgets.QComboBox, None] = None
        """
        soh_list_item: text box for preferred channel list
        """
        self.soh_list_item: Union[QtWidgets.QLineEdit, None] = None
        """
        clear_btn: QPushButton - button to clear the preferred channel
        """
        self.clear_btn: Union[QtWidgets.QPushButton, None] = None
        """
        data_type: str - data_type of the current preferred channel
        """
        self.data_type: Union[str, None] = None
        """
        row_sel_radio_btns: list of radio buttons to select rows
        """
        self.row_sel_radio_btns: List[QtWidgets.QRadioButton] = []
        # ============================ GUI ===============================
        self.create_soh_list_table_widget()
        main_layout.addWidget(self.soh_list_table_widget, 1)
        button_layout = self.create_buttons_section()
        main_layout.addLayout(button_layout)
        """
        tracking_info_text_browser:  - to display tracking info
            when scanning for available channel list from given folder
        """
        self.tracking_info_text_browser: QtWidgets.QTextBrowser = \
            QtWidgets.QTextBrowser(self)
        self.tracking_info_text_browser.setFixedHeight(80)
        main_layout.addWidget(self.tracking_info_text_browser)

    def create_buttons_section(self):
        """
        Create the row of buttons
        """
        h_layout = QtWidgets.QHBoxLayout()
        self.add_db_chan_btn = QtWidgets.QPushButton(
            self, text='Add DB Channels')
        self.add_db_chan_btn.clicked.connect(self.add_db_channels)
        h_layout.addWidget(self.add_db_chan_btn)

        self.scan_chan_btn = QtWidgets.QPushButton(
            self, text='Scan Channels from Data Source')
        self.scan_chan_btn.clicked.connect(self.scan_channels)
        h_layout.addWidget(self.scan_chan_btn)

        self.save_btn = QtWidgets.QPushButton(self, text='Save')
        self.save_btn.clicked.connect(self.save)
        h_layout.addWidget(self.save_btn)

        self.save_add_main_btn = QtWidgets.QPushButton(
            self, text='Save - Add to Main')
        self.save_add_main_btn.clicked.connect(self.save_add_to_main_and_close)
        h_layout.addWidget(self.save_add_main_btn)

        self.close_btn = QtWidgets.QPushButton(self, text='Cancel')
        self.close_btn.clicked.connect(self.close)
        h_layout.addWidget(self.close_btn)
        return h_layout

    def create_soh_list_table_widget(self):
        """
        Create self.soh_list_table_widget to view and edit different lists of
            preferred channels
        """
        self.soh_list_table_widget = QtWidgets.QTableWidget(self)
        self.soh_list_table_widget.cellClicked.connect(self.cell_clicked)
        self.soh_list_table_widget.itemChanged.connect(self.input_changed)
        self.soh_list_table_widget.setColumnCount(6)

        col_headers = ['', 'Name', 'DataType', 'Preferred SOH List',
                       'Edit', 'Clear']
        self.soh_list_table_widget.setHorizontalHeaderLabels(col_headers)
        header = self.soh_list_table_widget.horizontalHeader()
        header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
        header.setSectionResizeMode(3, QtWidgets.QHeaderView.Stretch)

        self.soh_list_table_widget.setRowCount(TOTAL_ROW)
        self.avail_data_types = self.get_data_types()
        for row_idx in range(TOTAL_ROW):
            self.add_row(row_idx)
        self.update_data_table_widget_items()

    def cell_clicked(self, row, col):
        """
        Turn the radio button of row to checked and set the row selected
        :param row: row id
        :type row: int
        :param col: column id
        :type col: int
        """
        self.row_sel_radio_btns[row].setChecked(True)
        self.curr_sel_changed(row)

    @QtCore.Slot()
    def add_row(self, row_idx):
        """
        Add a row to self.soh_list_table_widget

        :param row_idx: row index
        """
        self.row_sel_radio_btns.append(QtWidgets.QRadioButton(self))
        self.row_sel_radio_btns[row_idx].clicked.connect(
            lambda checked: self.curr_sel_changed(row_idx))
        self.soh_list_table_widget.setCellWidget(
            row_idx, COL['sel'], self.row_sel_radio_btns[row_idx])

        name_item = QtWidgets.QTableWidgetItem()
        self.soh_list_table_widget.setItem(row_idx, COL['name'], name_item)

        data_type_combo_box = QtWidgets.QComboBox(self)
        data_type_combo_box.textActivated.connect(
            lambda: self.cell_clicked(row_idx, 2))
        data_type_combo_box.currentIndexChanged.connect(self.input_changed)
        data_type_combo_box.addItems(['Unknown'] + self.avail_data_types)
        data_type_combo_box.setCurrentIndex(-1)
        self.soh_list_table_widget.setCellWidget(
            row_idx, COL['dataType'], data_type_combo_box)

        soh_list_item = QtWidgets.QTableWidgetItem()
        self.soh_list_table_widget.setItem(row_idx, COL['IDs'], soh_list_item)

        edit_button = QtWidgets.QPushButton(self, text='EDIT')
        edit_button.clicked.connect(lambda arg: self.edit_soh_list(row_idx))
        self.soh_list_table_widget.setCellWidget(
            row_idx, COL['edit'], edit_button)

        del_button = QtWidgets.QPushButton(self, text='CLR')
        del_button.clicked.connect(lambda arg: self.clear_soh_list(row_idx))
        self.soh_list_table_widget.setCellWidget(
            row_idx, COL['clr'], del_button)

    @QtCore.Slot()
    def input_changed(self):
        """
        Set self.changed to True if there is any info have been edit to
            display a warning message be for processing DB
        """
        self.changed = True

    def update_data_table_widget_items(self):
        """
        Update the content of self.soh_list_table_widget based on data read
            from DB
        """
        soh_list_rows = self.get_soh_list_rows()
        # first row
        self.soh_list_table_widget.cellWidget(0, COL['sel']).setChecked(True)
        self.curr_sel_changed(0)

        count = 0
        for r in soh_list_rows:
            self.soh_list_table_widget.cellWidget(
                count, COL['sel']).setChecked(
                True if r['current'] == 1 else False)
            self.soh_list_table_widget.item(
                count, COL['name']).setText(r['name'])
            self.soh_list_table_widget.cellWidget(
                count, COL['dataType']).setCurrentText(r['dataType'])
            self.soh_list_table_widget.item(
                count, COL['IDs']).setText(r['IDs'])
            if r['current'] == 1:
                self.curr_sel_changed(count)
                self.soh_list_table_widget.selectRow(count)
            count += 1
        self.update()

    def get_row(self, row_idx):
        """
        Get content of a self.soh_list_table_widget's row

        :param row_idx: int - index of a row
        """
        soh_list_name_item = self.soh_list_table_widget.item(
            row_idx, COL['name'])
        data_type_combobox = self.soh_list_table_widget.cellWidget(
            row_idx, COL['dataType'])
        soh_list_item = self.soh_list_table_widget.item(
            row_idx, COL['IDs'])
        clear_widget = self.soh_list_table_widget.cellWidget(
            row_idx, COL['clr'])
        return (soh_list_name_item,
                data_type_combobox,
                soh_list_item,
                clear_widget)

    @QtCore.Slot()
    def curr_sel_changed(self, row_idx):
        """
        When check the radio button of a row, set self.curr_row with new
            row_idx and get content of the row.

        :param row_idx: int - index of a row
        """
        if self.row_sel_radio_btns[row_idx].isChecked():
            self.curr_row = row_idx
            (self.soh_list_name_item, self.data_type_combobox,
             self.soh_list_item, self.clear_btn) = self.get_row(row_idx)

    @QtCore.Slot()
    def edit_soh_list(self, row_idx):
        soh_list_item = self.soh_list_table_widget.item(row_idx, COL['IDs'])
        edit_dialog = InputDialog(text=soh_list_item.text())
        if edit_dialog.exec_():
            soh_list_item.setText(edit_dialog.get_input())

    @QtCore.Slot()
    def clear_soh_list(self, row_idx):
        """
        When 'Clear' button at the end of a row is click. Confirm with user and
            then clear the content of soh_list_item in that row.

        :param row_idx: int - index of a row
        """
        (soh_list_name_item, data_type_combobox,
         soh_list_item, clear_widget) = self.get_row(row_idx)
        if soh_list_item.text().strip() != "":
            msg = ("Are you sure you want to delete the SOH channel list of  "
                   "row #%s?" % (row_idx + 1))
            result = QtWidgets.QMessageBox.question(
                self, "Confirmation", msg,
                QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
            if result == QtWidgets.QMessageBox.No:
                return
            self.changed = True
        if soh_list_name_item.text() != '':
            self.changed = True
        soh_list_name_item.setText('')
        if data_type_combobox.currentText != '':
            self.changed = True
        data_type_combobox.setCurrentIndex(-1)
        if soh_list_item.text() != '':
            self.changed = True
        soh_list_item.setText('')

    def validate_row(self, check_data_type=False):
        """
        Make sure there is a row selected by checking the radio button at the
            beginning of a row.
        Check if data_type is selected if required.

        :param check_data_type: bool - flag to require checking data type
        """
        if self.curr_row == -1:
            msg = "Please select a row."
            QtWidgets.QMessageBox.information(self, "Select row", msg)
            return False

        if check_data_type:
            self.data_type = self.data_type_combobox.currentText()
            if self.data_type not in self.avail_data_types:
                msg = ("Please select a data type that isn't 'Unknown' for "
                       "the selected row.")
                QtWidgets.QMessageBox.information(
                    self, "Select data type", msg)
                return False

        return True

    @QtCore.Slot()
    def add_db_channels(self):
        """
        Add channels from DB to preferred channel list.
        """
        if not self.validate_row(check_data_type=True):
            return

        db_channels = self.get_db_channels(self.data_type)
        self.soh_list_item.setText(','.join(db_channels))

    @QtCore.Slot()
    def scan_channels(self):
        """
        Scan for all available channels in folder self.dir_names to add to
            preferred channel list.
        For RT130, all SOH channels are kept in a log file. It will be more
            reasonable to get channels from DB because the task of getting
            channels from log files is like reading data from it.
        """
        if not self.validate_row():
            return
        if self.parent.rt130_das_dict != {}:
            data_type = 'RT130'
        else:
            try:
                data_type, is_multiplex = detect_data_type(self.dir_names)
            except Exception as e:
                QtWidgets.QMessageBox.warning(self, "Scan Channels", str(e))
                return
        if data_type in self.avail_data_types:
            self.data_type_combobox.setCurrentText(data_type)
        else:
            self.data_type_combobox.setCurrentText('Unknown')

        if data_type == 'RT130':
            self.scan_chan_btn.setEnabled(False)
            msg = ("Data type of the current data set is RT130 of which "
                   "Scan Channel from Data Source isn't available.\n\n"
                   "Do you want to add channels from DB?")
            result = QtWidgets.QMessageBox.question(
                self, "Add Channels from DB for RT130", msg,
                QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
            if result == QtWidgets.QMessageBox.Ok:
                self.add_db_channels()
        else:
            self.scan_chan_btn.setEnabled(True)
            ret = read_mseed_channels(self.tracking_info_text_browser,
                                      self.dir_names, is_multiplex)
            if ret == ([], [], [], []):
                msg = "No data can be read from " + ', '.join(self.dir_names)
                return QtWidgets.QMessageBox.warning(self, "No data", msg)

            (soh_chan_ids, mass_pos_chan_ids,
             wf_chan_ids, spr_gt_1_chan_ids) = ret

            self.soh_list_item.setText(','.join(soh_chan_ids))
            msg = ""
            if mass_pos_chan_ids:
                msg += f"Mass position channels (MP): " \
                       f"{','.join(mass_pos_chan_ids)}\n"
            if wf_chan_ids:
                msg += f"Waveform channels (WF): {','.join(wf_chan_ids)}\n"
            if spr_gt_1_chan_ids:
                msg += (
                    f"Non-WF with spr>1: "
                    f"{','.join(spr_gt_1_chan_ids)}\nNon-WF with spr>1 can be "
                    f"added to SOH channel list at your own risk because they "
                    f"slow down the performance significantly.")
            if msg != "":
                msg = f"<pre>{msg}</pre>"
                display_tracking_info(self.tracking_info_text_browser, msg)

    @QtCore.Slot()
    def save(self):
        """
        Save changes to DB by doing the following steps:
            + If there are any changes to inform user before save.
            + Create insert sql for each row
            + If there is nothing in preference list reset parent's setting for
                preferred channels and finish.
            + Do truncate and execute insert sqls above
            + Set parent's setting for preferred channels with info from the
                current selected row.
        """
        if not self.validate_row():
            return
        if self.changed:
            msg = ("All IDs in the database will be overwritten with "
                   "current IDs in the dialog.\nClick Cancel to stop updating "
                   "database.")
            result = QtWidgets.QMessageBox.question(
                self, "Confirmation", msg,
                QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
            if result == QtWidgets.QMessageBox.Cancel:
                return False
        sql_list = []
        for row_idx in range(TOTAL_ROW):
            sql = self.create_save_row_sql(row_idx)
            if sql is not None:
                sql_list.append(sql)
        if len(sql_list) == 0:
            self.parent.pref_soh_list = []
            self.parent.pref_soh_list_name = ''
            self.parent.pref_soh_list_data_type = 'Unknown'
            return True

        ret = trunc_add_db('ChannelPrefer', sql_list)
        if ret is not True:
            display_tracking_info(self.parent, ret, LogType.ERROR)
        self.parent.pref_soh_list = [
            t.strip() for t in self.soh_list_item.text().split(',')]
        self.parent.pref_soh_list_name = self.soh_list_name_item.text().strip()
        self.parent.pref_soh_list_data_type = \
            self.data_type_combobox.currentText()
        self.changed = False
        return True

    def create_save_row_sql(self, row_idx):
        """
        Read info from the row with index row_idx to create insert sql.

        :param row_idx: int - index of a row
        :return: str - insert sql with the row's info
        """
        current = 1 if self.soh_list_table_widget.cellWidget(
            row_idx, COL['sel']).isChecked() else 0
        name = self.soh_list_table_widget.item(
            row_idx, COL['name']).text()
        data_type = self.soh_list_table_widget.cellWidget(
            row_idx, COL['dataType']).currentText()
        idx = self.soh_list_table_widget.item(
            row_idx, COL['IDs']).text()
        if idx.strip() == '':
            return
        if name.strip() == '' and idx.strip() != '':
            msg = f"Please add Name for row {row_idx}."
            QtWidgets.QMessageBox.information(self, "Missing info", msg)
            return
        return (f"INSERT INTO ChannelPrefer (name, IDs, dataType, current)"
                f"VALUES ('{name}', '{idx}', '{data_type}', {current})")

    @QtCore.Slot()
    def save_add_to_main_and_close(self):
        """
        Save changes to DB and reset main window's preferred channels with info
            of the selected rows.
        """
        if not self.save():
            return
        self.parent.curr_pref_soh_list_name_txtbox.setText(
            self.parent.pref_soh_list_name)
        self.parent.all_soh_chans_check_box.setChecked(False)
        self.close()

    @staticmethod
    def get_data_types():
        """
        Get list of data types from DB.

        :return: [str, ] - list of data types
        """
        data_type_rows = execute_db(
            'SELECT * FROM DataTypes ORDER BY dataType ASC')
        return [d[0] for d in data_type_rows]

    @staticmethod
    def get_db_channels(data_type) -> List[str]:
        """
        Get all channels defined in DB table Channels for the given data type.

        :param data_type: str - the given data type
        """
        channel_rows = execute_db(
            f"SELECT channel FROM CHANNELS"
            f" WHERE dataType='{data_type}'"
            f"  AND param NOT IN ('Seismic data', 'Mass position')"
            f" ORDER BY dataType ASC")
        return sorted([c[0] for c in channel_rows])

    @staticmethod
    def get_soh_list_rows() -> List[Dict]:
        """
        Get all data from DB table ChannelPrefer to fill up
            self.soh_list_table_widget

        :return id_rows: [dict,] - list of data for each row
        """
        id_rows = execute_db_dict(
            "SELECT name, IDs, dataType, current FROM ChannelPrefer "
            " ORDER BY name ASC")
        return id_rows