# coding: utf-8

"""
module to define some converter utils function
"""

import os
import logging
import h5py
from typing import Literal

import h5py._hl.selections as selection
from h5py import h5s as h5py_h5s
from silx.io.url import DataUrl
from nxtomomill.utils._os import abspath
from .hdf5 import open_hdf5_readonly

_logger = logging.getLogger(__name__)

__all__ = [
    "from_data_url_to_virtual_source",
    "from_virtual_source_to_data_url",
    "resolve_link",
]


def from_data_url_to_virtual_source(
    url: DataUrl, open_mode_for_reading: Literal["robust", "safe"]
) -> tuple:
    """
    :param url: url to be converted to a virtual source. It must target a 2D detector
    :return: (h5py.VirtualSource, tuple(shape of the virtual source), numpy.dtype: type of the dataset associated with the virtual source)
    """
    if not isinstance(url, DataUrl):
        raise TypeError(
            f"url is expected to be an instance of DataUrl and not {type(url)}"
        )

    with open_hdf5_readonly(
        url.file_path(), open_mode_for_reading=open_mode_for_reading
    ) as o_h5s:
        original_data_shape = o_h5s[url.data_path()].shape
        data_type = o_h5s[url.data_path()].dtype
        if len(original_data_shape) == 2:
            original_data_shape = (
                1,
                original_data_shape[0],
                original_data_shape[1],
            )

        vs_shape = original_data_shape
        if url.data_slice() is not None:
            vs_shape = (
                url.data_slice().stop - url.data_slice().start,
                original_data_shape[-2],
                original_data_shape[-1],
            )

    vs = h5py.VirtualSource(
        url.file_path(), url.data_path(), shape=vs_shape, dtype=data_type
    )

    if url.data_slice() is not None:
        vs.sel = selection.select(original_data_shape, url.data_slice())
    return vs, vs_shape, data_type


def from_virtual_source_to_data_url(vs: h5py.VirtualSource) -> DataUrl:
    if not isinstance(vs, h5py.VirtualSource):
        raise TypeError(
            f"vs is expected to be an instance of h5py.VirtualSorce and not {type(vs)}"
        )
    url = DataUrl(file_path=vs.path, data_path=vs.name, scheme="silx")
    return url


def resolve_link(
    dataset_url: DataUrl,
    open_mode_for_reading: Literal["safe", "robust"],
    dataset: h5py.Dataset | None = None,
    dataset_link: h5py.HardLink | h5py.SoftLink | h5py.ExternalLink | None = None,
) -> tuple[DataUrl]:
    """
    Solve links (external Links and virtual dataset). It replace original dataset by the 'solved' one when possible.

    :param dataset_url: URL of the dataset.
    :param dataset: HDF5 dataset if already accessible. Else will access it from ``dataset_url``.
    :param dataset_link: link associated to the HDF5 dataset if any. If ``dataset`` not provided will try to access it from ``dataset_url``.
    :param open_mode_for_reading: How to open file for accessing dataset and sub dataset.
    return: a tuple of DataUrl that defines all the virtual data source to create.
    """
    if dataset is not None and dataset_link is not None:
        return _resolve_link(
            dataset_url=dataset_url,
            dataset=dataset,
            dataset_link=dataset_link,
            open_mode_for_reading=open_mode_for_reading,
        )
    with open_hdf5_readonly(
        dataset_url.file_path(), open_mode_for_reading=open_mode_for_reading
    ) as h5s:
        dataset = h5s.get(dataset_url.data_path(), getlink=False)
        dataset_link = h5s.get(dataset_url.data_path(), getlink=True)
        return _resolve_link(
            dataset_url=dataset_url,
            dataset=dataset,
            dataset_link=dataset_link,
            open_mode_for_reading=open_mode_for_reading,
        )


def _resolve_link(
    dataset_url: DataUrl,
    dataset: h5py.Dataset,
    open_mode_for_reading: Literal["safe", "robust"],
    dataset_link: h5py.HardLink | h5py.SoftLink | h5py.ExternalLink | None,
) -> tuple[DataUrl]:
    """
    Processing of 'resolve_link'
    """
    if not isinstance(dataset, h5py.Dataset):
        raise TypeError(
            f"dataset_url is expected to point to a dataset and not {type(dataset)}"
        )
    if isinstance(dataset_link, h5py.ExternalLink):
        wd = os.path.dirname(dataset_url.file_path())
        solved_link_file_path = os.path.relpath(dataset_link.filename, wd)
        solved_link_file_path = abspath(os.path.join(wd, solved_link_file_path))

        camera_dataset_url = DataUrl(
            file_path=solved_link_file_path,
            data_path=dataset_link.path,
            scheme="silx",
        )
        return resolve_link(
            dataset_url=camera_dataset_url, open_mode_for_reading=open_mode_for_reading
        )

    if not dataset.is_virtual:
        return (dataset_url,)

    sources = dataset.virtual_sources()

    solved_urls = list()
    can_be_solved = True
    expected_n_frames = dataset.shape[0]
    vs_n_frames = 0
    """number of frame found by solving vds. Must be consistent with the number of frames in the original dataset. If not, we consider that the vds cannot be solved."""
    can_check_n_frames = True

    for vs_info in sources:
        # here we could provide dataset but we won't to
        # insure file path will be relative.
        type_code = vs_info.src_space.get_select_type()

        dirname = os.path.dirname(abspath(dataset_url.file_path()))
        solved_link_file_path = os.path.relpath(vs_info.file_name, dirname)
        solved_link_file_path = abspath(os.path.join(dirname, solved_link_file_path))
        # check for unlimited selections in case where selection is regular
        # hyperslab, which is the only allowed case for h5s.UNLIMITED to be
        # in the selection
        if type_code == h5py_h5s.SEL_ALL:
            # if all the dataset is selected, we can solve it directly
            solved_url = DataUrl(
                file_path=solved_link_file_path,
                data_path=vs_info.dset_name,
                scheme="silx",
            )
            solved_urls.extend(
                resolve_link(solved_url, open_mode_for_reading=open_mode_for_reading)
            )
            can_check_n_frames = False
            # warning: for the 'h5py_h5s.SEL_ALL' somehow we cannot check the number of frames.
        elif (
            type_code == h5py_h5s.SEL_HYPERSLABS
            and vs_info.src_space.is_regular_hyperslab()
        ):
            # if a regular hyperslab is selected, we can solve it if the number of frames is consistent with the original dataset. We consider that the number of frames is the number of frames in the original dataset if the selection is unlimited in this dimension.
            select_bounds = vs_info.vspace.get_select_bounds()
            left_bound = select_bounds[0]
            if left_bound[0] != 0:
                # if an hyperslab check that we start the datasaet from the first element
                can_be_solved = False
            right_bound = select_bounds[1]
            vs_n_frames += right_bound[0] - left_bound[0] + 1

            solved_url = DataUrl(
                file_path=solved_link_file_path,
                data_path=vs_info.dset_name,
                scheme="silx",
            )
            solved_urls.extend(
                resolve_link(solved_url, open_mode_for_reading=open_mode_for_reading)
            )
        else:
            # if not a an hyperslab we cannot solve it.
            _logger.warning(
                f"Virtual source {vs_info.file_name}:{vs_info.dset_name} cannot be solved because it is not a regular hyperslab or selection all points."
            )
            can_be_solved = False
            break

    if can_be_solved and can_check_n_frames and vs_n_frames != expected_n_frames:
        # if seems to be solved but if the number of frames is not consistent with the original dataset, we consider that it cannot be solved.
        can_be_solved = False

    if can_be_solved:
        return tuple(solved_urls)
    else:
        return (dataset_url,)


def vs_file_path_to_real_path(file_path, vs_file_path):
    # get file path as absolute for the NXtomo. Simplify management of the
    # directories
    if os.path.isabs(vs_file_path):
        return vs_file_path
    else:
        return os.path.join(os.path.dirname(file_path), vs_file_path)
