# -*- coding: utf-8 -*-
# imageio is distributed under the terms of the (new) BSD License.

"""Read/Write BSDF files.

Backend Library: internal

The BSDF format enables reading and writing of image data in the
BSDF serialization format. This format allows storage of images, volumes,
and series thereof. Data can be of any numeric data type, and can
optionally be compressed. Each image/volume can have associated
meta data, which can consist of any data type supported by BSDF.

By default, image data is lazily loaded; the actual image data is
not read until it is requested. This allows storing multiple images
in a single file and still have fast access to individual images.
Alternatively, a series of images can be read in streaming mode, reading
images as they are read (e.g. from http).

BSDF is a simple generic binary format. It is easy to extend and there
are standard extension definitions for 2D and 3D image data.
Read more at http://bsdf.io.


Parameters
----------
random_access : bool
    Whether individual images in the file can be read in random order.
    Defaults to True for normal files, and to False when reading from HTTP.
    If False, the file is read in "streaming mode", allowing reading
    files as they are read, but without support for "rewinding".
    Note that setting this to True when reading from HTTP, the whole file
    is read upon opening it (since lazy loading is not possible over HTTP).

compression : int
    Use ``0`` or "no" for no compression, ``1`` or "zlib" for Zlib
    compression (same as zip files and PNG), and ``2`` or "bz2" for Bz2
    compression (more compact but slower). Default 1 (zlib).
    Note that some BSDF implementations may not support compression
    (e.g. JavaScript).

"""

import numpy as np

from ..core import Format


def get_bsdf_serializer(options):
    from . import _bsdf as bsdf

    class NDArrayExtension(bsdf.Extension):
        """Copy of BSDF's NDArrayExtension but deal with lazy blobs."""

        name = "ndarray"
        cls = np.ndarray

        def encode(self, s, v):
            return dict(shape=v.shape, dtype=str(v.dtype), data=v.tobytes())

        def decode(self, s, v):
            return v  # return as dict, because of lazy blobs, decode in Image

    class ImageExtension(bsdf.Extension):
        """We implement two extensions that trigger on the Image classes."""

        def encode(self, s, v):
            return dict(array=v.array, meta=v.meta)

        def decode(self, s, v):
            return Image(v["array"], v["meta"])

    class Image2DExtension(ImageExtension):
        name = "image2d"
        cls = Image2D

    class Image3DExtension(ImageExtension):
        name = "image3d"
        cls = Image3D

    exts = [NDArrayExtension, Image2DExtension, Image3DExtension]
    serializer = bsdf.BsdfSerializer(exts, **options)

    return bsdf, serializer


class Image:
    """Class in which we wrap the array and meta data. By using an extension
    we can make BSDF trigger on these classes and thus encode the images.
    as actual images.
    """

    def __init__(self, array, meta):
        self.array = array
        self.meta = meta

    def get_array(self):
        if not isinstance(self.array, np.ndarray):
            v = self.array
            blob = v["data"]
            if not isinstance(blob, bytes):  # then it's a lazy bsdf.Blob
                blob = blob.get_bytes()
            self.array = np.frombuffer(blob, dtype=v["dtype"])
            self.array.shape = v["shape"]
        return self.array

    def get_meta(self):
        return self.meta


class Image2D(Image):
    pass


class Image3D(Image):
    pass


class BsdfFormat(Format):
    """The BSDF format enables reading and writing of image data in the
    BSDF serialization format. This format allows storage of images, volumes,
    and series thereof. Data can be of any numeric data type, and can
    optionally be compressed. Each image/volume can have associated
    meta data, which can consist of any data type supported by BSDF.

    By default, image data is lazily loaded; the actual image data is
    not read until it is requested. This allows storing multiple images
    in a single file and still have fast access to individual images.
    Alternatively, a series of images can be read in streaming mode, reading
    images as they are read (e.g. from http).

    BSDF is a simple generic binary format. It is easy to extend and there
    are standard extension definitions for 2D and 3D image data.
    Read more at http://bsdf.io.

    Parameters for reading
    ----------------------
    random_access : bool
        Whether individual images in the file can be read in random order.
        Defaults to True for normal files, and to False when reading from HTTP.
        If False, the file is read in "streaming mode", allowing reading
        files as they are read, but without support for "rewinding".
        Note that setting this to True when reading from HTTP, the whole file
        is read upon opening it (since lazy loading is not possible over HTTP).

    Parameters for saving
    ---------------------
    compression : {0, 1, 2}
        Use ``0`` or "no" for no compression, ``1`` or "zlib" for Zlib
        compression (same as zip files and PNG), and ``2`` or "bz2" for Bz2
        compression (more compact but slower). Default 1 (zlib).
        Note that some BSDF implementations may not support compression
        (e.g. JavaScript).

    """

    def _can_read(self, request):
        if request.mode[1] in (self.modes + "?"):
            # if request.extension in self.extensions:
            #     return True
            if request.firstbytes.startswith(b"BSDF"):
                return True

    def _can_write(self, request):
        if request.mode[1] in (self.modes + "?"):
            if request.extension in self.extensions:
                return True

    # -- reader

    class Reader(Format.Reader):
        def _open(self, random_access=None):
            # Validate - we need a BSDF file consisting of a list of images
            # The list is typically a stream, but does not have to be.
            assert self.request.firstbytes[:4] == b"BSDF", "Not a BSDF file"
            # self.request.firstbytes[5:6] == major and minor version
            if not (
                self.request.firstbytes[6:15] == b"M\x07image2D"
                or self.request.firstbytes[6:15] == b"M\x07image3D"
                or self.request.firstbytes[6:7] == b"l"
            ):
                pass  # Actually, follow a more duck-type approach ...
                # raise RuntimeError('BSDF file does not look like an '
                #                   'image container.')
            # Set options. If we think that seeking is allowed, we lazily load
            # blobs, and set streaming to False (i.e. the whole file is read,
            # but we skip over binary blobs), so that we subsequently allow
            # random access to the images.
            # If seeking is not allowed (e.g. with a http request), we cannot
            # lazily load blobs, but we can still load streaming from the web.
            options = {}
            if self.request.filename.startswith(("http://", "https://")):
                ra = False if random_access is None else bool(random_access)
                options["lazy_blob"] = False  # Because we cannot seek now
                options["load_streaming"] = not ra  # Load as a stream?
            else:
                ra = True if random_access is None else bool(random_access)
                options["lazy_blob"] = ra  # Don't read data until needed
                options["load_streaming"] = not ra

            file = self.request.get_file()
            bsdf, self._serializer = get_bsdf_serializer(options)
            self._stream = self._serializer.load(file)
            # Another validation
            if (
                isinstance(self._stream, dict)
                and "meta" in self._stream
                and "array" in self._stream
            ):
                self._stream = Image(self._stream["array"], self._stream["meta"])
            if not isinstance(self._stream, (Image, list, bsdf.ListStream)):
                raise RuntimeError(
                    "BSDF file does not look seem to have an " "image container."
                )

        def _close(self):
            pass

        def _get_length(self):
            if isinstance(self._stream, Image):
                return 1
            elif isinstance(self._stream, list):
                return len(self._stream)
            elif self._stream.count < 0:
                return np.inf
            return self._stream.count

        def _get_data(self, index):
            # Validate
            if index < 0 or index >= self.get_length():
                raise IndexError(
                    "Image index %i not in [0 %i]." % (index, self.get_length())
                )
            # Get Image object
            if isinstance(self._stream, Image):
                image_ob = self._stream  # singleton
            elif isinstance(self._stream, list):
                # Easy when we have random access
                image_ob = self._stream[index]
            else:
                # For streaming, we need to skip over frames
                if index < self._stream.index:
                    raise IndexError(
                        "BSDF file is being read in streaming "
                        "mode, thus does not allow rewinding."
                    )
                while index > self._stream.index:
                    self._stream.next()
                image_ob = self._stream.next()  # Can raise StopIteration
            # Is this an image?
            if (
                isinstance(image_ob, dict)
                and "meta" in image_ob
                and "array" in image_ob
            ):
                image_ob = Image(image_ob["array"], image_ob["meta"])
            if isinstance(image_ob, Image):
                # Return as array (if we have lazy blobs, they are read now)
                return image_ob.get_array(), image_ob.get_meta()
            else:
                r = repr(image_ob)
                r = r if len(r) < 200 else r[:197] + "..."
                raise RuntimeError("BSDF file contains non-image " + r)

        def _get_meta_data(self, index):  # pragma: no cover
            return {}  # This format does not support global meta data

    # -- writer

    class Writer(Format.Writer):
        def _open(self, compression=1):
            options = {"compression": compression}
            bsdf, self._serializer = get_bsdf_serializer(options)
            if self.request.mode[1] in "iv":
                self._stream = None  # Singleton image
                self._written = False
            else:
                # Series (stream) of images
                file = self.request.get_file()
                self._stream = bsdf.ListStream()
                self._serializer.save(file, self._stream)

        def _close(self):
            # We close the stream here, which will mark the number of written
            # elements. If we would not close it, the file would be fine, it's
            # just that upon reading it would not be known how many items are
            # in there.
            if self._stream is not None:
                self._stream.close(False)  # False says "keep this a stream"

        def _append_data(self, im, meta):
            # Determine dimension
            ndim = None
            if self.request.mode[1] in "iI":
                ndim = 2
            elif self.request.mode[1] in "vV":
                ndim = 3
            else:
                ndim = 3  # Make an educated guess
                if im.ndim == 2 or (im.ndim == 3 and im.shape[-1] <= 4):
                    ndim = 2
            # Validate shape
            assert ndim in (2, 3)
            if ndim == 2:
                assert im.ndim == 2 or (im.ndim == 3 and im.shape[-1] <= 4)
            else:
                assert im.ndim == 3 or (im.ndim == 4 and im.shape[-1] <= 4)
            # Wrap data and meta data in our special class that will trigger
            # the BSDF image2D or image3D extension.
            if ndim == 2:
                ob = Image2D(im, meta)
            else:
                ob = Image3D(im, meta)
            # Write directly or to stream
            if self._stream is None:
                assert not self._written, "Cannot write singleton image twice"
                self._written = True
                file = self.request.get_file()
                self._serializer.save(file, ob)
            else:
                self._stream.append(ob)

        def set_meta_data(self, meta):  # pragma: no cover
            raise RuntimeError("The BSDF format only supports " "per-image meta data.")
