from pathlib import Path

import numpy as np

from ..config import known_extensions
from .request import InitializationError, IOMode
from .v3_plugin_api import ImageProperties, PluginV3


def _legacy_default_index(format):
    if format._name == "FFMPEG":
        index = Ellipsis
    elif format._name == "GIF-PIL":
        index = Ellipsis
    else:
        index = 0

    return index


class LegacyPlugin(PluginV3):
    """A plugin to  make old (v2.9) plugins compatible with v3.0

    .. depreciated:: 2.9
        `legacy_get_reader` will be removed in a future version of imageio.
        `legacy_get_writer` will be removed in a future version of imageio.

    This plugin is a wrapper around the old FormatManager class and exposes
    all the old plugins via the new API. On top of this it has
    ``legacy_get_reader`` and ``legacy_get_writer`` methods to allow using
    it with the v2.9 API.

    Methods
    -------
    read(index=None, **kwargs)
        Read the image at position ``index``.
    write(image, **kwargs)
        Write image to the URI.
    iter(**kwargs)
        Iteratively yield images from the given URI.
    get_meta(index=None)
        Return the metadata for the image at position ``index``.
    legacy_get_reader(**kwargs)
        Returns the v2.9 image reader. (depreciated)
    legacy_get_writer(**kwargs)
        Returns the v2.9 image writer. (depreciated)

    Examples
    --------

    >>> import imageio.v3 as iio
    >>> with iio.imopen("/path/to/image.tiff", "r", legacy_mode=True) as file:
    >>>     reader = file.legacy_get_reader()  # depreciated
    >>>     for im in file.iter():
    >>>         print(im.shape)

    """

    def __init__(self, request, legacy_plugin):
        """Instantiate a new Legacy Plugin

        Parameters
        ----------
        uri : {str, pathlib.Path, bytes, file}
            The resource to load the image from, e.g. a filename, pathlib.Path,
            http address or file object, see the docs for more info.
        legacy_plugin : Format
            The (legacy) format to use to interface with the URI.

        """
        self._request = request
        self._format = legacy_plugin

        source = (
            "<bytes>"
            if isinstance(self._request.raw_uri, bytes)
            else self._request.raw_uri
        )
        if self._request.mode.io_mode == IOMode.read:
            if not self._format.can_read(request):
                raise InitializationError(
                    f"`{self._format.name}`" f" can not read `{source}`."
                )
        else:
            if not self._format.can_write(request):
                raise InitializationError(
                    f"`{self._format.name}`" f" can not write to `{source}`."
                )

    def legacy_get_reader(self, **kwargs):
        """legacy_get_reader(**kwargs)

        a utility method to provide support vor the V2.9 API

        Parameters
        ----------
        kwargs : ...
            Further keyword arguments are passed to the reader. See :func:`.help`
            to see what arguments are available for a particular format.
        """

        # Note: this will break thread-safety
        self._request._kwargs = kwargs

        # safeguard for DICOM plugin reading from folders
        try:
            assert Path(self._request.filename).is_dir()
        except OSError:
            pass  # not a valid path on this OS
        except AssertionError:
            pass  # not a folder
        else:
            return self._format.get_reader(self._request)

        self._request.get_file().seek(0)
        return self._format.get_reader(self._request)

    def read(self, *, index=None, **kwargs):
        """
        Parses the given URI and creates a ndarray from it.

        Parameters
        ----------
        index : {integer, None}
            If the URI contains a list of ndimages return the index-th
            image. If None, stack all images into an ndimage along the
            0-th dimension (equivalent to np.stack(imgs, axis=0)).
        kwargs : ...
            Further keyword arguments are passed to the reader. See
            :func:`.help` to see what arguments are available for a particular
            format.

        Returns
        -------
        ndimage : np.ndarray
            A numpy array containing the decoded image data.

        """

        if index is None:
            index = _legacy_default_index(self._format)

        if index is Ellipsis:
            img = np.stack([im for im in self.iter(**kwargs)])
            return img

        reader = self.legacy_get_reader(**kwargs)
        return reader.get_data(index)

    def legacy_get_writer(self, **kwargs):
        """legacy_get_writer(**kwargs)

        Returns a :class:`.Writer` object which can be used to write data
        and meta data to the specified file.

        Parameters
        ----------
        kwargs : ...
            Further keyword arguments are passed to the writer. See :func:`.help`
            to see what arguments are available for a particular format.
        """

        # Note: this will break thread-safety
        self._request._kwargs = kwargs
        return self._format.get_writer(self._request)

    def write(self, ndimage, *, is_batch=None, metadata=None, **kwargs):
        """
        Write an ndimage to the URI specified in path.

        If the URI points to a file on the current host and the file does not
        yet exist it will be created. If the file exists already, it will be
        appended if possible; otherwise, it will be replaced.

        Parameters
        ----------
        ndimage : numpy.ndarray
            The ndimage or list of ndimages to write.
        is_batch : bool
            If True, treat the supplied ndimage as a batch of images. If False,
            treat the supplied ndimage as a single image. If None, try to
            determine ``is_batch`` from the ndimage's shape and ndim.
        metadata : dict
            The metadata passed to write alongside the image.
        kwargs : ...
            Further keyword arguments are passed to the writer. See
            :func:`.help` to see what arguments are available for a
            particular format.


        Returns
        -------
        buffer : bytes
            When writing to the special target "<bytes>", this function will
            return the encoded image data as a bytes string. Otherwise it
            returns None.

        Notes
        -----
        Automatically determining ``is_batch`` may fail for some images due to
        shape aliasing. For example, it may classify a channel-first color image
        as a batch of gray images. In most cases this automatic deduction works
        fine (it has for almost a decade), but if you do have one of those edge
        cases (or are worried that you might) consider explicitly setting
        ``is_batch``.

        """

        if is_batch or isinstance(ndimage, (list, tuple)):
            pass  # ndimage is list of images
        elif is_batch is False:
            ndimage = [ndimage]
        else:
            # Write the largest possible block by guessing the meaning of each
            # dimension from the shape/ndim and then checking if any batch
            # dimensions are left.
            ndimage = np.asanyarray(ndimage)
            batch_dims = ndimage.ndim

            # two spatial dimensions
            batch_dims = max(batch_dims - 2, 0)

            # packed (channel-last) image
            if ndimage.ndim >= 3 and ndimage.shape[-1] < 5:
                batch_dims = max(batch_dims - 1, 0)

            # format supports volumetric images
            ext_infos = known_extensions.get(self._request.extension, list())
            for ext_info in ext_infos:
                if self._format.name in ext_info.priority and ext_info.volume_support:
                    batch_dims = max(batch_dims - 1, 0)
                    break

            if batch_dims == 0:
                ndimage = [ndimage]

        with self.legacy_get_writer(**kwargs) as writer:
            for image in ndimage:
                image = np.asanyarray(image)

                if image.ndim < 2:
                    raise ValueError(
                        "The image must have at least two spatial dimensions."
                    )

                if not np.issubdtype(image.dtype, np.number) and not np.issubdtype(
                    image.dtype, bool
                ):
                    raise ValueError(
                        f"All images have to be numeric, and not `{image.dtype}`."
                    )

                writer.append_data(image, metadata)

        return writer.request.get_result()

    def iter(self, **kwargs):
        """Iterate over a list of ndimages given by the URI

        Parameters
        ----------
        kwargs : ...
            Further keyword arguments are passed to the reader. See
            :func:`.help` to see what arguments are available for a particular
            format.
        """

        reader = self.legacy_get_reader(**kwargs)
        for image in reader:
            yield image

    def properties(self, index=None):
        """Standardized ndimage metadata.

        Parameters
        ----------
        index : int
            The index of the ndimage for which to return properties. If the
            index is out of bounds a ``ValueError`` is raised. If ``None``,
            return the properties for the ndimage stack. If this is impossible,
            e.g., due to shape missmatch, an exception will be raised.

        Returns
        -------
        properties : ImageProperties
            A dataclass filled with standardized image metadata.

        """

        if index is None:
            index = _legacy_default_index(self._format)

        # for backwards compatibility ... actually reads pixel data :(
        if index is Ellipsis:
            image = self.read(index=0)
            n_images = self.legacy_get_reader().get_length()
            return ImageProperties(
                shape=(n_images, *image.shape),
                dtype=image.dtype,
                n_images=n_images,
                is_batch=True,
            )

        image = self.read(index=index)
        return ImageProperties(
            shape=image.shape,
            dtype=image.dtype,
            is_batch=False,
        )

    def get_meta(self, *, index=None):
        """Read ndimage metadata from the URI

        Parameters
        ----------
        index : {integer, None}
            If the URI contains a list of ndimages return the metadata
            corresponding to the index-th image. If None, behavior depends on
            the used api

            Legacy-style API: return metadata of the first element (index=0)
            New-style API: Behavior depends on the used Plugin.

        Returns
        -------
        metadata : dict
            A dictionary of metadata.

        """

        return self.metadata(index=index, exclude_applied=False)

    def metadata(self, index=None, exclude_applied: bool = True):
        """Format-Specific ndimage metadata.

        Parameters
        ----------
        index : int
            The index of the ndimage to read. If the index is out of bounds a
            ``ValueError`` is raised. If ``None``, global metadata is returned.
        exclude_applied : bool
            This parameter exists for compatibility and has no effect. Legacy
            plugins always report all metadata they find.

        Returns
        -------
        metadata : dict
            A dictionary filled with format-specific metadata fields and their
            values.

        """

        if index is None:
            index = _legacy_default_index(self._format)

        return self.legacy_get_reader().get_meta_data(index=index)

    def __del__(self) -> None:
        pass
        # turns out we can't close the file here for LegacyPlugin
        # because it would break backwards compatibility
        # with legacy_get_writer and legacy_get_reader
        # self._request.finish()
