# coding: utf-8
"""Module with utils in order to define series of scan (TomoScanBase)"""

import logging
from typing import Iterable, Optional

from tomoscan.scanbase import TomoScanBase
from tomoscan.tomoobject import TomoObject

from .factory import Factory
from .identifier import BaseIdentifier

_logger = logging.getLogger(__name__)


class Serie(list):
    """
    A serie can be view as an extented list of :class:`TomoObject`.
    This allow the user to define a relation between scans like:

    .. image:: /_static/../img/serie_tomoscanbase_class_diag.png
    """

    def __init__(
        self, name: Optional[str] = None, iterable=None, use_identifiers=False
    ) -> None:
        if name is not None and not isinstance(name, str):
            raise TypeError(
                f"name should be None os an instance of str. Get {type(name)} instead"
            )
        self._name = "Unknow" if name is None else name
        self.__use_identifiers = use_identifiers
        if iterable is None:
            iterable = []
        super().__init__()
        for item in iterable:
            self.append(item)

    @property
    def name(self) -> str:
        return self._name

    @name.setter
    def name(self, name: str):
        if not isinstance(name, str):
            raise TypeError("name is expected to be an instance of str")
        self._name = name

    @property
    def use_identifiers(self):
        return self.__use_identifiers

    def append(self, object: TomoObject):
        if not isinstance(object, TomoObject):
            raise TypeError(
                f"object is expected to be an instance of {TomoObject} not {type(object)}"
            )
        if self.use_identifiers:
            super().append(object.get_identifier().to_str())
        else:
            super().append(object)

    def remove(self, object: TomoObject):
        if not isinstance(object, TomoObject):
            raise TypeError(
                f"object is expected to be an instance of {TomoObject} not {type(object)}"
            )
        if self.use_identifiers:
            super().remove(object.get_identifier().to_str())
        else:
            super().remove(object)

    def to_dict_of_str(self) -> dict:
        """
        call for each scan DatasetIdentifier.to_str() if use dataset identifier.
        Otherwise return a default list with dataset identifiers
        """
        objects = []
        for dataset in self:
            if self.use_identifiers:
                objects.append(dataset)
            else:
                objects.append(dataset.get_identifier().to_str())
        return {
            "objects": objects,
            "name": self.name,
            "use_identifiers": self.use_identifiers,
        }

    @staticmethod
    def from_dict_of_str(
        dict_, factory=Factory, use_identifiers: Optional[bool] = None
    ):
        """
        create a Serie from it definition from a dictionary

        :param dict dict_: dictionary containing the serie to create
        :param factory: factory to use in order to create scans defined from there Identifier (as an instance of DatasetIdentifier or is str representation)
        :type factory: Factory
        :param Optional[bool] use_identifiers: use_identifiers can be overwrite when creating the serie

        :return: created Serie
        :rtype: Serie
        """
        name = dict_["name"]
        objects = dict_["objects"]
        if use_identifiers is None:
            use_identifiers = dict_.get("use_identifiers", False)
        instanciated_scans = []
        for tomo_obj in objects:
            if isinstance(tomo_obj, (str, BaseIdentifier)):
                instanciated_scans.append(
                    factory.create_tomo_object_from_identifier(identifier=tomo_obj)
                )
            else:
                raise TypeError(
                    f"elements of dict_['objects'] are expected to be an instance of TomoObject, DatasetIdentifier or str representing a DatasetIdentifier. Not {type(tomo_obj)}"
                )

        return Serie(
            name=name, use_identifiers=use_identifiers, iterable=instanciated_scans
        )

    def __contains__(self, tomo_obj: BaseIdentifier):
        if self.use_identifiers:
            key = tomo_obj.get_identifier().to_str()
        else:
            key = tomo_obj
        return super().__contains__(key)

    def __eq__(self, other):
        if not isinstance(other, Serie):
            return False
        return self.name == other.name and super().__eq__(other)

    def __ne__(self, other):
        return not self.__eq__(other)


def sequences_to_series_from_sample_name(scans: Iterable) -> tuple:
    """
    create a serie with the same sample name

    :param Iterable scans:
    :return: tuple of serie if as_tuple_of_list is false else a tuple of list (of TomoScanBase)
    """
    series = {}
    for scan in scans:
        if not isinstance(scan, TomoScanBase):
            raise TypeError("Elements are expected to be instances of TomoScanBase")
        if scan.sample_name is None:
            _logger.warning(f"no scan sample found for {scan}")
        if scan.sample_name not in series:
            series[scan.sample_name] = Serie(use_identifiers=False)
        series[scan.sample_name].append(scan)
    return tuple(series.values())


def check_serie_is_consistant_frm_sample_name(scans: Iterable):
    """
    Insure the provided group of scan is valid. Otherwise raise an error

    :param Iterable scans: group of TomoScanBAse to check
    """
    l_scans = set()
    for scan in scans:
        if not isinstance(scan, TomoScanBase):
            raise TypeError("Elements are expected to be instance of TomoScanBase")
        if scan in l_scans:
            raise ValueError("{} is present at least twice")
        elif len(l_scans) > 0:
            first_scan = next(iter((l_scans)))
            if first_scan.sample_name != scan.sample_name:
                raise ValueError(
                    f"{scan} and {first_scan} are from two different sample: {scan.sample_name} and {first_scan.sample_name}"
                )
        l_scans.add(scan)


def serie_is_complete_from_group_size(scans: Iterable) -> bool:
    """
    Insure the provided group of scan is valid. Otherwise raise an error

    :param Iterable scans: group of TomoScanBAse to check
    :return: True if the group is complete
    :rtype: bool
    """
    if len(scans) == 0:
        return True
    try:
        check_serie_is_consistant_frm_sample_name(scans=scans)
    except Exception as e:
        _logger.error(f"provided group is invalid. {e}")
        raise e
    else:
        group_size = next(iter(scans)).group_size
        if group_size is None:
            _logger.warning("No information found regarding group size")
            return True
        elif group_size == len(scans):
            return True
        elif group_size < len(scans):
            _logger.warning("more scans found than group_size")
            return True
        else:
            return False
