import os

from nexusformat.nexus import NXentry, nxopen

from assonant._nexus import NeXusObjectFactory
from assonant.data_classes import AssonantDataClass

from .assonant_file_writer_interface import IAssonantFileWriter
from .exceptions import (
    AssonantFileWriterError,
    FileAlreadyExistsError,
    NeXusObjectAlreadyExistsError,
)


class NexusFileWriter(IAssonantFileWriter):
    """NeXus File Writer.

    File Writer specialized on accessing data from AssonantDataClasses and
    write into a HDF5 file following NeXus data model standards.
    """

    def __init__(self):
        self.nexus_factory = NeXusObjectFactory()
        self.file_extension = ".nxs"

    def _try_nxobject_insertion(self, parent, nxobject):
        """
        Attempts to insert an NXobject into a parent NXobject, handling conflicts
        based on the type of the NXobject.

        If the NXobject is not an NXentry and a conflict exists (i.e., an object
        with the same name already exists in the parent), an error is raised. If
        the NXobject is an NXentry and a conflict exists, the method recursively
        attempts to insert the components of the source NXentry into the target
        NXentry.

        Args:
            parent (NXobject): The parent NXobject where the nxobject is to be inserted.
            nxobject (NXobject): The NXobject to be inserted into the parent.

        Raises:
            NeXusObjectAlreadyExistsError: If a non-NXentry object with the same name
                already exists in the parent.
        """
        if nxobject.nxname in parent and not isinstance(nxobject, NXentry):
            # If Non-Entry object conflicts, raise error instantly
            raise NeXusObjectAlreadyExistsError(
                f"Non Entry Object '{nxobject.nxname}' already exists in parent '{parent.nxname}'"
            )
        elif nxobject.nxname in parent and isinstance(nxobject, NXentry):
            # If Entry object conflicts, try to insert components within source Entry
            # to target Entry recursively.
            for nx_obj in nxobject._entries.values():
                self._try_nxobject_insertion(parent[nxobject.nxname], nx_obj)
        else:
            # If no conflict, insert object directly
            parent.insert(nxobject)

    def write_data(self, filepath: str, filename: str, data: AssonantDataClass):
        """Writes data into a NeXus file.

        This method creates a new NeXus file if it does not exist, or appends data
        to the NXroot object of an existing file. The data to be written is provided
        as an instance of `AssonantDataClass`.

            filepath (str): The directory path where the NeXus file will be saved.
            filename (str): The name of the NeXus file (without extension).
            data (AssonantDataClass): An instance of `AssonantDataClass` containing
                the data to be written to the file.

        Raises:
            FileAlreadyExistsError: Raised if there is a conflict when attempting
                to write a NeXus object, indicating that the file already exists.
                File is considered to exist if an ExperimentStage Entry and its
                respective subentry conflicts.
            AssonantFileWriterError: Raised for any other errors encountered while
                writing data to the file.
        """
        nexus_filepath = os.path.join(filepath, os.path.splitext(filename)[0]) + self.file_extension
        mode = "rw" if os.path.isfile(nexus_filepath) else "a"

        try:
            with nxopen(nexus_filepath, mode=mode) as nxf:
                nxobject = self.nexus_factory.create_nxobject(data)
                # Try to insert Entry related to experiment stage into file
                self._try_nxobject_insertion(nxf, nxobject)
        except NeXusObjectAlreadyExistsError as e:
            raise FileAlreadyExistsError(
                f"Due to conflict on writing NeXus object, file {nexus_filepath} is considered to be already existent!"
            ) from e
        except Exception as e:
            raise AssonantFileWriterError(f"NeXusFileWriter failed to write data into {filepath}") from e

    def get_file_extension(self) -> str:
        """Getter method for the file extension of currently handled file writer.

        Returns:
            str: File extenstion of urrently handled file writer.
        """
        return self.file_extension
