from qgis.PyQt import QtCore
from qgis.PyQt.QtCore import QModelIndex, QVariant, pyqtSignal
from qgis.PyQt.QtGui import QColor, QFont, QStandardItemModel

from openlog.datamodel.assay.generic_assay import AssaySeriesType
from openlog.gui.import_assay.utils import sanitize_sql_name
from openlog.gui.utils.column_definition import ColumnDefinition

SPECIAL_COLUMNS = [
    "Hole ID*",
    "Depth*",
    "Time*",
    "Depth min.*",
    "Depth max.*",
    "Time min.*",
    "Time max.*",
]


class ColumnDefinitionTableModel(QStandardItemModel):
    COLUMN_ROW = 0
    MAPPING_ROW = 1
    UNIT_ROW = 2
    SERIES_TYPE_ROW = 3
    NO_DATA_ROW = 4
    DB_UPDATE_POLICY = 5

    FIXED_ROLE = QtCore.Qt.UserRole
    OPTIONAL_ROLE = QtCore.Qt.UserRole + 1

    READONLY_NODATA_COLUMNS = []
    columnNameChanged = pyqtSignal(QModelIndex, object, object)

    def __init__(self, parent=None) -> None:
        """
        QStandardItemModel for column definition display

        Args:
            parent: QWidget
        """
        super().__init__(parent)
        self.setVerticalHeaderLabels(
            [
                "Column",
                "Map",
                "Unit",
                "Type",
                "No data",
                "Conflicts",
            ]
        )

    def setData(self, index, value, role=QtCore.Qt.EditRole):
        """
        Override for storing old and new value when column name change.
        """
        if not index.isValid():
            return False

        if index.row() == self.COLUMN_ROW and role == QtCore.Qt.EditRole:
            try:
                # use this method instead of .data() because by default setData is not used at first for column names.
                old_def = self.get_column_definition(only_with_mapping=False)
                old_name, new_name = self._get_changing_column_name(
                    old_def, index, value
                )
                # we have to emit columnNameChanged before dataChanged (triggered by super().setData)
                if old_name is not None:
                    self.columnNameChanged.emit(index, old_name, new_name)
                result = super().setData(index, value, role)
                return result
            except Exception:
                pass

        result = super().setData(index, value, role)

        return result

    def _get_changing_column_name(
        self, old_def: list[ColumnDefinition], index: QModelIndex, new_value: str
    ) -> tuple:
        """
        Return old and new column name between old ColumnDefinition and value assigned to index.
        Returns (old_name, new_name)
        """
        for i, old_col in enumerate(old_def):
            if i != index.column():
                continue
            if old_col.column != new_value:
                return old_col.column, new_value

        return None, None

    def set_nodata_readonly(self, columns: list) -> None:
        """
        Set uneditable no-data field.
        Args:
            - columns: List[int] liste of column indexes to set uneditable
        """
        self.READONLY_NODATA_COLUMNS = columns

    def flags(self, index: QtCore.QModelIndex) -> QtCore.Qt.ItemFlags:
        """
        Override QStandardItemModel flags to remove edition for expected columns

        Args:
            index: QModelIndex

        Returns: index flags

        """
        flags = super().flags(index)
        if (
            index.row() == self.COLUMN_ROW
            or index.row() == self.UNIT_ROW
            or index.row() == self.SERIES_TYPE_ROW
        ):
            fixed = super().data(
                self.index(self.COLUMN_ROW, index.column()), self.FIXED_ROLE
            )
            series_type = AssaySeriesType[
                super().data(self.index(self.SERIES_TYPE_ROW, index.column()))
            ]
            fixed |= index.row() == self.UNIT_ROW and (
                series_type == AssaySeriesType.CATEGORICAL
                or series_type == AssaySeriesType.DATETIME
                or series_type == AssaySeriesType.NOMINAL
                or series_type == AssaySeriesType.IMAGERY
                or series_type == AssaySeriesType.SPHERICAL
            )

            if index.row() == self.UNIT_ROW and series_type == AssaySeriesType.POLAR:
                fixed = True

            if (
                index.row() == self.MAPPING_ROW
                and series_type == AssaySeriesType.SPHERICAL
            ):

                fixed = True

            if fixed:
                flags = (
                    flags & QtCore.Qt.ItemIsEnabled
                    | QtCore.Qt.ItemIsSelectable
                    | ~QtCore.Qt.ItemIsEditable
                )
        if (
            index.row() == self.NO_DATA_ROW
            and index.column() in self.READONLY_NODATA_COLUMNS
        ):
            flags = (
                flags & QtCore.Qt.ItemIsEnabled
                | QtCore.Qt.ItemIsSelectable
                | ~QtCore.Qt.ItemIsEditable
            )

        return flags

    def data(
        self, index: QtCore.QModelIndex, role: int = QtCore.Qt.DisplayRole
    ) -> QVariant:
        """
        Override QStandardItemModel data() for font of expected column

        Args:
            index: QModelIndex
            role: Qt role

        Returns: QVariant

        """
        result = super().data(index, role)
        if role == QtCore.Qt.FontRole and (
            index.row() == self.COLUMN_ROW
            or index.row() == self.UNIT_ROW
            or index.row() == self.SERIES_TYPE_ROW
        ):
            fixed = super().data(
                self.index(self.COLUMN_ROW, index.column()), self.FIXED_ROLE
            )
            if fixed:
                result = QFont()
                result.setBold(True)

        series_type_str = super().data(self.index(self.SERIES_TYPE_ROW, index.column()))
        if series_type_str is None:
            return
        series_type = AssaySeriesType[series_type_str]
        if (
            role == QtCore.Qt.DisplayRole
            and index.row() == self.UNIT_ROW
            and (
                series_type == AssaySeriesType.CATEGORICAL
                or series_type == AssaySeriesType.DATETIME
                or series_type == AssaySeriesType.NOMINAL
                or series_type == AssaySeriesType.IMAGERY
                or series_type == AssaySeriesType.SPHERICAL
            )
        ):
            result = ""

        if (
            role == QtCore.Qt.DisplayRole
            and index.row() == self.UNIT_ROW
            and series_type == AssaySeriesType.POLAR
        ):
            result = "°"

        if (
            role == QtCore.Qt.DisplayRole
            and index.row() == self.MAPPING_ROW
            and series_type == AssaySeriesType.SPHERICAL
        ):
            result = ""

        if role == QtCore.Qt.DisplayRole and index.row() == self.COLUMN_ROW:
            used_lower_column_name = {}
            for c in range(0, self.columnCount()):
                # Use column name in lower because SQLite is not case sensitive
                col_lower = str(super().data(self.index(index.row(), c))).lower()
                col_type = AssaySeriesType[
                    super().data(self.index(self.SERIES_TYPE_ROW, c))
                ]
                # If no column defined, use mapping
                if not col_lower:
                    if col_type == AssaySeriesType.SPHERICAL:
                        col_lower = "spherical"
                    else:
                        col_lower = str(
                            super().data(self.index(self.MAPPING_ROW, c))
                        ).lower()

                if col_lower in used_lower_column_name:
                    used_lower_column_name[col_lower].append(c)
                else:
                    used_lower_column_name[col_lower] = [c]

            # If no column defined, use mapping
            if not result:
                if series_type == AssaySeriesType.SPHERICAL:
                    result = "spherical"
                else:
                    result = str(
                        super().data(self.index(self.MAPPING_ROW, index.column()))
                    )

            # Check if duplicated column used
            col = result
            col_lower = col.lower()
            if col and len(used_lower_column_name[col_lower]) > 1:
                index_col = used_lower_column_name[col_lower].index(index.column())
                if index_col:
                    result = f"{col}_{index_col}"

        return result

    def add_column_definition(self, column_mapping: ColumnDefinition) -> None:
        """
        Add a row in QStandardItemModel

        Args:
            column_mapping:
        """
        self.insertColumn(self.columnCount())
        col = self.columnCount() - 1
        self._set_column_definition(col, column_mapping)

    def remove_column(self, column: str) -> None:
        """
        Remove a column mapping

        Args:
            column: (str) column name

        """
        for i in range(self.columnCount()):
            column_row = str(self.data(self.index(self.COLUMN_ROW, i)))
            if column_row == column:
                self.removeColumn(i)
                return

    def set_column_definition(self, columns: [ColumnDefinition]) -> None:
        """
        Define expected column and remove not used column

        Args:
            columns: expected column list
        """
        columns_name = [x.column for x in columns]
        for i in reversed(range(self.columnCount())):
            expected_col = str(self.data(self.index(self.COLUMN_ROW, i)))
            if expected_col not in columns_name:
                self.removeColumn(i)

        for col in columns:
            col_index = self._get_column_col_index(col.column)
            if col_index != -1:
                self._set_column_definition(col_index, col)
            else:
                self.add_column_definition(col)

    def set_column_mapping(self, column: str, mapping: str) -> None:
        col_index = self._get_column_col_index(column)
        if col_index != -1:
            self.setData(self.index(self.MAPPING_ROW, col_index), mapping)

    def get_column_definition(
        self, only_with_mapping: bool = False
    ) -> [ColumnDefinition]:
        """
        Return list of columns definition. Use only_with_mapping option to get only columns with defined mapping

        Args:
            only_with_mapping: True for expected columns only with mapping, False for all expected columns

        Returns: [ColumnDefinition]

        """
        result = []
        for i in range(self.columnCount()):
            expected_col = str(self.data(self.index(self.COLUMN_ROW, i)))
            if expected_col:
                column = ColumnDefinition(
                    column=str(self.data(self.index(self.COLUMN_ROW, i))),
                    mapping=str(self.data(self.index(self.MAPPING_ROW, i))),
                    unit=str(self.data(self.index(self.UNIT_ROW, i))),
                    fixed=self.data(self.index(self.COLUMN_ROW, i), self.FIXED_ROLE),
                    optional=self.data(
                        self.index(self.COLUMN_ROW, i), self.OPTIONAL_ROLE
                    ),
                    series_type=AssaySeriesType[
                        str(self.data(self.index(self.SERIES_TYPE_ROW, i)))
                    ],
                    no_data_pattern=self.data(self.index(self.NO_DATA_ROW, i)),
                )

                if only_with_mapping:
                    mapping = self.data(self.index(self.MAPPING_ROW, i))
                    if mapping:
                        result.append(column)
                else:
                    result.append(column)
        return result

    def get_import_policies(self) -> dict:
        """
        Return policies for each column.
        """
        result = {}
        for i in range(self.columnCount()):
            col_name = sanitize_sql_name(self.data(self.index(self.COLUMN_ROW, i)))
            mapping_col = self.data(self.index(self.MAPPING_ROW, i))
            policy = self.data(self.index(self.DB_UPDATE_POLICY, i))
            policy = "Ignore" if mapping_col == "" else policy
            result[col_name] = policy

        return result

    def set_column_color_and_tooltip(
        self, column: str, tooltip: str, color: QColor
    ) -> None:
        """
        Set color and tooltip for an expected column

        Args:
            column: (str) expected column
            tooltip:  (str) tooltip
            color: (QColor) foreground color
        """
        for i in range(self.columnCount()):
            if self.data(self.index(self.COLUMN_ROW, i)) == column:
                self.setData(
                    self.index(self.COLUMN_ROW, i), tooltip, QtCore.Qt.ToolTipRole
                )
                self.setData(
                    self.index(self.COLUMN_ROW, i), color, QtCore.Qt.ForegroundRole
                )

    def _set_column_definition(
        self, col: int, column_mapping: ColumnDefinition
    ) -> None:
        self.setData(self.index(self.COLUMN_ROW, col), column_mapping.column)
        # Add fixed data in COLUMN_ROW UserRole
        self.setData(
            self.index(self.COLUMN_ROW, col),
            column_mapping.fixed,
            self.FIXED_ROLE,
        )

        # Add optional data in COLUMN_ROW UserRole
        self.setData(
            self.index(self.COLUMN_ROW, col),
            column_mapping.optional,
            self.OPTIONAL_ROLE,
        )

        self.setData(self.index(self.MAPPING_ROW, col), column_mapping.mapping)
        self.setData(self.index(self.UNIT_ROW, col), column_mapping.unit)
        self.setData(
            self.index(self.SERIES_TYPE_ROW, col), column_mapping.series_type.name
        )
        self.setData(self.index(self.NO_DATA_ROW, col), column_mapping.no_data_pattern)
        self.setData(
            self.index(self.DB_UPDATE_POLICY, col),
            "Ignore" if column_mapping.column in SPECIAL_COLUMNS else "Overwrite",
        )

    def _get_column_col_index(self, col: str) -> int:
        """
        Get expected column column index (-1 if expected column not available)

        Args:
            col: (str) expected column

        Returns: expected column column index (-1 if expected column not available)

        """
        column = -1
        for i in range(self.columnCount()):
            val = str(self.data(self.index(self.COLUMN_ROW, i)))
            if val == col:
                column = i
        return column

    def _get_or_create_column(self, col: str) -> int:
        """
        Get or create column

        Args:
            col: expected column name

        Returns: column index

        """
        column = self._get_column_col_index(col)
        if column == -1:
            self.add_column_definition(ColumnDefinition(column=col))
            column = self.columnCount() - 1
        return column
