# -*- coding: utf-8 -*-
from __future__ import annotations

from typing import Callable, Final

from qtpy.QtCore import QAbstractTableModel, QLocale, QModelIndex, QPersistentModelIndex, Qt
from qtpy.QtWidgets import QWidget

from .settings import Settings
from ..catalog import CatalogEntryType
from ..utils import (FREQUENCY, INTENSITY, LINES, LOWER_STATE_ENERGY, SPECIES_TAG, best_name)

__all__ = ['FoundLinesModel']


class FoundLinesModel(QAbstractTableModel):
    ROW_BATCH_COUNT: Final[int] = 5

    class DataType:
        __slots__ = ['species_tag', 'name',
                     'frequency_str', 'frequency',
                     'intensity_str', 'intensity',
                     'lower_state_energy_str', 'lower_state_energy']

        def __init__(self,
                     species_tag: int, name: str,
                     frequency_str: str, frequency: float,
                     intensity_str: str, intensity: float,
                     lower_state_energy_str: str, lower_state_energy: float) -> None:
            self.species_tag: int = species_tag
            self.name: str = name
            self.frequency_str: str = frequency_str
            self.frequency: float = frequency
            self.intensity_str: str = intensity_str
            self.intensity: float = intensity
            self.lower_state_energy_str: str = lower_state_energy_str
            self.lower_state_energy: float = lower_state_energy

        def __eq__(self, other: 'FoundLinesModel.DataType') -> int:
            if not isinstance(other, FoundLinesModel.DataType):
                return NotImplemented
            return (self.species_tag == other.species_tag
                    and self.frequency == other.frequency
                    and self.intensity == other.intensity
                    and self.lower_state_energy == other.lower_state_energy)

        def __hash__(self) -> int:
            return hash(self.species_tag) ^ hash(self.frequency) ^ hash(self.lower_state_energy)

    def __init__(self, settings: Settings, parent: QWidget | None = None) -> None:
        super().__init__(parent)
        self._settings: Settings = settings
        self._entries: list[CatalogEntryType] = []
        self._data: list[FoundLinesModel.DataType] = []
        self._rows_loaded: int = FoundLinesModel.ROW_BATCH_COUNT

        unit_format: Final[str] = self.tr('{value} [{unit}]', 'unit format')
        self._header: Final[list[str]] = [
            self.tr('Substance'),
            unit_format.format(value=self.tr('Frequency'), unit=self._settings.frequency_unit_str),
            unit_format.format(value=self.tr('Intensity'), unit=self._settings.intensity_unit_str),
            unit_format.format(value=self.tr('Lower state energy'), unit=self._settings.energy_unit_str),
        ]

    def update_units(self) -> None:
        unit_format: Final[str] = self.tr('{value} [{unit}]', 'unit format')
        self._header[1] = unit_format.format(value=self.tr('Frequency'), unit=self._settings.frequency_unit_str)
        self._header[2] = unit_format.format(value=self.tr('Intensity'), unit=self._settings.intensity_unit_str)
        self._header[3] = unit_format.format(value=self.tr('Lower state energy'), unit=self._settings.energy_unit_str)

    def rowCount(self, parent: QModelIndex | QPersistentModelIndex = ...) -> int:
        return min(len(self._data), self._rows_loaded)

    def columnCount(self, parent: QModelIndex | QPersistentModelIndex = ...) -> int:
        return len(self._header)

    def data(self, index: QModelIndex | QPersistentModelIndex, role: int = Qt.ItemDataRole.DisplayRole) -> str | None:
        if index.isValid():
            if role == Qt.ItemDataRole.DisplayRole:
                item: FoundLinesModel.DataType = self._data[index.row()]
                column_index: int = index.column()
                if column_index == 0:
                    return item.name
                if column_index == 1:
                    return item.frequency_str
                if column_index == 2:
                    return item.intensity_str
                if column_index == 3:
                    return item.lower_state_energy_str
        return None

    def row(self, row_index: int) -> DataType:
        return self._data[row_index]

    def headerData(self, col: int, orientation: Qt.Orientation, role: int = ...) -> str | None:
        if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
            return self._header[col]
        return None

    def setHeaderData(self, section: int, orientation: Qt.Orientation, value: str, role: int = ...) -> bool:
        if (orientation == Qt.Orientation.Horizontal
                and role == Qt.ItemDataRole.DisplayRole
                and 0 <= section < len(self._header)):
            self._header[section] = value
            return True
        return False

    def clear(self) -> None:
        self.set_entries([])

    def set_entries(self, new_data: list[CatalogEntryType]) -> None:
        from_mhz: Callable[[float], float] = self._settings.from_mhz
        from_log10_sq_nm_mhz: Callable[[float], float] = self._settings.from_log10_sq_nm_mhz
        from_rec_cm: Callable[[float], float] = self._settings.from_rec_cm
        frequency_suffix: int = self._settings.frequency_unit
        precision: int = [4, 7, 8, 8][frequency_suffix]
        locale: QLocale = QLocale()
        decimal_point: str = locale.decimalPoint()

        def frequency_str(frequency: float) -> tuple[str, float]:
            frequency = from_mhz(frequency)
            return f'{frequency:.{precision}f}'.replace('.', decimal_point), frequency

        def intensity_str(intensity: float) -> tuple[str, float]:
            intensity = from_log10_sq_nm_mhz(intensity)
            if intensity == 0.0:
                return '0', intensity
            elif abs(intensity) < 0.1:
                return f'{intensity:.4e}'.replace('.', decimal_point), intensity
            else:
                return f'{intensity:.4f}'.replace('.', decimal_point), intensity

        def lower_state_energy_str(lower_state_energy: float) -> tuple[str, float]:
            lower_state_energy = from_rec_cm(lower_state_energy)
            if lower_state_energy == 0.0:
                return '0', lower_state_energy
            elif abs(lower_state_energy) < 0.1:
                return f'{lower_state_energy:.4e}'.replace('.', decimal_point), lower_state_energy
            else:
                return f'{lower_state_energy:.4f}'.replace('.', decimal_point), lower_state_energy

        self.beginResetModel()
        unique_entries: list[CatalogEntryType] = []
        non_unique_indices: set[int] = set()
        unique: bool
        all_unique: bool = True  # unless the opposite is proven
        for i in range(len(new_data)):
            if i in non_unique_indices:
                continue
            unique = True
            for j in range(i + 1, len(new_data)):
                if j in non_unique_indices:
                    continue
                if new_data[i] == new_data[j]:
                    non_unique_indices.add(j)
                    unique = False
                    all_unique = False
                    break
            if unique and not all_unique:
                unique_entries.append(new_data[i])
        if all_unique:
            self._entries = new_data.copy()
        else:
            self._entries = unique_entries
        entry: CatalogEntryType
        rich_text_in_formulas: bool = self._settings.rich_text_in_formulas
        self._data = list(set(
            FoundLinesModel.DataType(
                entry[SPECIES_TAG],
                best_name(entry, rich_text_in_formulas),
                *frequency_str(line[FREQUENCY]),
                *intensity_str(line[INTENSITY]),
                *lower_state_energy_str(line[LOWER_STATE_ENERGY]),
            )
            for entry in self._entries
            for line in entry[LINES]
        ))
        self._rows_loaded = FoundLinesModel.ROW_BATCH_COUNT
        self.endResetModel()

    def sort(self, column: int, order: Qt.SortOrder = Qt.SortOrder.AscendingOrder) -> None:
        self.beginResetModel()
        key = {
            0: (lambda l: (l.name, l.frequency, l.intensity, l.lower_state_energy)),
            1: (lambda l: (l.frequency, l.intensity, l.name, l.lower_state_energy)),
            2: (lambda l: (l.intensity, l.frequency, l.name, l.lower_state_energy)),
            3: (lambda l: (l.lower_state_energy, l.intensity, l.frequency, l.name))
        }[column]
        self._data.sort(key=key, reverse=bool(order != Qt.SortOrder.AscendingOrder))
        self.endResetModel()

    def canFetchMore(self, index: QModelIndex | QPersistentModelIndex = QModelIndex()) -> bool:
        return len(self._data) > self._rows_loaded

    def fetchMore(self, index: QModelIndex | QPersistentModelIndex = QModelIndex()) -> None:
        # https://sateeshkumarb.wordpress.com/2012/04/01/paginated-display-of-table-data-in-pyqt/
        remainder: int = len(self._data) - self._rows_loaded
        items_to_fetch: int = min(remainder, FoundLinesModel.ROW_BATCH_COUNT)
        self.beginInsertRows(QModelIndex(), self._rows_loaded, self._rows_loaded + items_to_fetch - 1)
        self._rows_loaded += items_to_fetch
        self.endInsertRows()
