from typing import (
    cast,
    Dict,
    Iterable,
    Optional,
    Tuple,
    Union,
)

from dataladmetadatamodel.datasettree import DatasetTree
from dataladmetadatamodel.mappableobject import MappableObject
from dataladmetadatamodel.metadatapath import MetadataPath
from dataladmetadatamodel.metadatarootrecord import MetadataRootRecord
from dataladmetadatamodel.mapper.reference import Reference


class VersionRecord:
    def __init__(self,
                 time_stamp: str,
                 path: Optional[MetadataPath],
                 element: Union[DatasetTree, MetadataRootRecord]):

        assert isinstance(time_stamp, str)
        assert isinstance(path, (type(None), MetadataPath))
        assert isinstance(element, (DatasetTree, MetadataRootRecord))

        self.time_stamp = time_stamp
        self.path = path or MetadataPath("")
        self.element = element

    def deepcopy(self,
                 new_mapper_family: Optional[str] = None,
                 new_destination: Optional[str] = None
                 ) -> "VersionRecord":

        return VersionRecord(
            self.time_stamp,
            self.path,
            self.element.deepcopy(
                new_mapper_family,
                new_destination))


class VersionList(MappableObject):
    def __init__(self,
                 initial_set: Optional[Dict[str, VersionRecord]] = None,
                 realm: Optional[str] = None,
                 reference: Optional[Reference] = None):

        assert isinstance(initial_set, (type(None), dict))
        assert isinstance(realm, (type(None), str))
        assert isinstance(reference, (type(None), Reference))

        super().__init__(realm, reference)
        self.version_set: Dict[str, VersionRecord] = initial_set or dict()

    def purge_impl(self):
        for version, version_record in self.version_set.items():
            version_record.element.purge()
        self.version_set = dict()

    def modifiable_sub_objects_impl(self) -> Iterable[MappableObject]:
        yield from map(
            lambda version_record: version_record.element,
            self.version_set.values())

    def _get_version_record(self, primary_data_version) -> VersionRecord:
        return self.version_set[primary_data_version]

    def versions(self) -> Iterable:
        return self.version_set.keys()

    def get_versioned_element(self,
                              primary_data_version: str
                              ) -> Tuple[str, MetadataPath, MappableObject]:
        """
        Get the dataset tree or metadata root record,
        its timestamp and path for the given version.
        If it is not mapped yet, it will be mapped.
        """
        version_record = self._get_version_record(primary_data_version)
        version_record.element.read_in()
        return (
            version_record.time_stamp,
            version_record.path,
            version_record.element)

    @property
    def versioned_elements(self
                           ) -> Iterable[Tuple[str, Tuple[str, MetadataPath, MappableObject]]]:
        """
        Get an iterable of all versions and their records
        """
        for version, version_record in self.version_set.items():
            yield version, (
                version_record.time_stamp,
                version_record.path,
                version_record.element)

    def set_versioned_element(self,
                              primary_data_version: str,
                              time_stamp: str,
                              path: MetadataPath,
                              element: Union[DatasetTree, MetadataRootRecord]):
        """
        Set a new or updated dataset tree.
        Existing references are deleted.
        The entry is marked as dirty.
        """
        self.touch()
        self.version_set[primary_data_version] = VersionRecord(
            time_stamp,
            path,
            element)

    def unget_versioned_element(self,
                                primary_data_version: str,
                                new_destination: Optional[str] = None):
        """
        Remove a metadata record from memory. First, persist the
        current status via save_object ---which will write it to
        the backend, if it was changed or--- then purge it
        from memory.
        """
        version_record = self.version_set[primary_data_version]
        version_record.element.write_out(new_destination)
        version_record.element.purge()

    def deepcopy_impl(self,
                      new_mapper_family: Optional[str] = None,
                      new_destination: Optional[str] = None,
                      **kwargs
                      ) -> "VersionList":

        path_prefix = kwargs.get("path_prefix", MetadataPath(""))

        copied_version_list = VersionList()
        for primary_data_version, version_record in self.version_set.items():
            copied_version_list.set_versioned_element(
                primary_data_version,
                version_record.time_stamp,
                path_prefix / version_record.path,
                version_record.element.deepcopy(new_mapper_family,
                                                new_destination))

        copied_version_list.write_out(new_destination)
        copied_version_list.purge()
        return copied_version_list


class TreeVersionList(VersionList):
    """
    Thin wrapper around version list to support tree-version list
    specific mapping, for example in the git mapper, which will
    update a reference, if mapping an TreeVersionList instance.
    """

    def get_dataset_tree(self,
                         primary_data_version: str
                         ) -> Tuple[str, DatasetTree]:

        time_stamp, _, dataset_tree = super().get_versioned_element(
            primary_data_version)
        assert isinstance(dataset_tree, DatasetTree)
        return time_stamp, dataset_tree

    def set_dataset_tree(self,
                         primary_data_version: str,
                         time_stamp: str,
                         dataset_tree: DatasetTree
                         ):

        self.touch()

        return super().set_versioned_element(
            primary_data_version,
            time_stamp,
            MetadataPath(""),
            dataset_tree)

    def unget_dataset_tree(self,
                           primary_data_version: str,
                           new_destination: str):

        assert isinstance(primary_data_version, str)
        assert isinstance(new_destination, str)
        super().unget_versioned_element(primary_data_version,
                                        new_destination)

    def deepcopy_impl(self,
                      new_mapper_family: Optional[str] = None,
                      new_destination: Optional[str] = None,
                      **kwargs
                      ) -> "TreeVersionList":

        path_prefix = kwargs.get("path_prefix", MetadataPath(""))

        copied_version_list = TreeVersionList()
        for primary_data_version, version_record in self.version_set.items():

            copied_version_list.set_versioned_element(
                primary_data_version,
                version_record.time_stamp,
                path_prefix / version_record.path,
                cast(MetadataRootRecord,
                     version_record.element.deepcopy(
                        new_mapper_family,
                        new_destination)))

        copied_version_list.write_out(new_destination)
        copied_version_list.purge()
        return copied_version_list
