"""Image EXIF metadata interface module."""

import logging
import os
import warnings
from typing import Any, BinaryIO, Dict, List, Union

from plum.bigendian import uint16

from exif._constants import ATTRIBUTE_ID_MAP, ExifMarkers
from exif._app1_create import generate_empty_app1_bytes
from exif._app1_metadata import App1MetaData

logger = logging.getLogger(__name__)


class Image:

    """Image EXIF metadata interface class.

    :param img_file: image file with EXIF metadata
    :type image_file: str (file path), bytes (already-read contents), or File

    """

    def _parse_segments(self, img_bytes: bytes) -> None:
        cursor = 0

        # Traverse hexadecimal string until EXIF APP1 segment found.
        while img_bytes[cursor : cursor + len(ExifMarkers.APP1)] != ExifMarkers.APP1:
            cursor += len(ExifMarkers.APP1)
            if cursor > len(img_bytes):
                self._has_exif = False
                cursor = 2  # should theoretically go after SOI marker (if adding)
                break

        self._segments["preceding"] = img_bytes[:cursor]
        app1_start_index = cursor

        if self._has_exif:
            # Determine the expected length of the APP1 segment.
            app1_len = uint16.unpack(
                img_bytes[app1_start_index + 2 : app1_start_index + 4]
            )
            cursor += app1_len + 1

            # If the expected length stops early, keep traversing until another section is found.
            while img_bytes[cursor - 2 : cursor - 1] != ExifMarkers.SEG_PREFIX:
                cursor += 1
                # raise IOError("no subsequent EXIF segment found, is this an EXIF-encoded JPEG?")
                if cursor > len(img_bytes):
                    self._has_exif = False
                    break

        if self._has_exif:
            # Instantiate an APP1 segment object to create an EXIF tag interface.
            self._segments["APP1"] = App1MetaData(img_bytes[app1_start_index:cursor])
            self._segments["succeeding"] = img_bytes[cursor:]
        else:
            # Store the remainder of the image so that it can be reconstructed when exporting.
            self._segments["succeeding"] = img_bytes[app1_start_index:]

    def __init__(
        self,
        img_file: Union[BinaryIO, bytes, str],  # pylint: disable=unsubscriptable-object
    ) -> None:
        self._has_exif = True
        self._segments: Dict[
            str, Union[App1MetaData, bytes]  # pylint: disable=unsubscriptable-object
        ] = {}

        if hasattr(img_file, "read"):
            img_bytes = img_file.read()  # type: ignore
        elif isinstance(img_file, bytes):
            img_bytes = img_file
        elif os.path.isfile(img_file):  # type: ignore
            with open(img_file, "rb") as file_descriptor:  # type: ignore
                img_bytes = file_descriptor.read()
        else:  # pragma: no cover
            raise ValueError("expected file object, file path as str, or bytes")

        self._parse_segments(img_bytes)

    def __dir__(self) -> List[str]:
        members = [
            "delete",
            "delete_all",
            "get",
            "get_all",
            "get_file",
            "get_thumbnail",
            "has_exif",
            "list_all",
            "_segments",
        ]

        if self._has_exif:
            assert isinstance(self._segments["APP1"], App1MetaData)
            members += self._segments["APP1"].get_tag_list()

        return members

    def __getattr__(self, item):
        return getattr(self._segments["APP1"], item)

    def __setattr__(self, key, value):
        try:
            ATTRIBUTE_ID_MAP[key.lower()]
        except KeyError:
            super(Image, self).__setattr__(key, value)
        else:
            if not self._has_exif:
                self._segments["APP1"] = App1MetaData(generate_empty_app1_bytes())
                self._has_exif = True

            setattr(self._segments["APP1"], key.lower(), value)

    def __delattr__(self, item):
        try:
            ATTRIBUTE_ID_MAP[item]
        except KeyError:
            super(Image, self).__delattr__(item)
        else:
            delattr(self._segments["APP1"], item)

    def __getitem__(self, item):
        return self.__getattr__(item)

    def __setitem__(self, key, value):
        self.__setattr__(key, value)

    def __delitem__(self, key):
        self.__delattr__(key)

    def delete(self, attribute: str) -> None:
        """Remove the specified attribute from the image.

        :param attribute: image EXIF attribute name

        """
        self.__delattr__(attribute)

    def delete_all(self) -> None:
        """Remove all EXIF tags from the image."""
        for _ in range(
            2
        ):  # iterate twice to delete thumbnail tags the second time around
            assert isinstance(self._segments["APP1"], App1MetaData)
            for tag in self._segments["APP1"].get_tag_list():
                if not tag in ["_exif_ifd_pointer", "_gps_ifd_pointer", "exif_version"]:
                    try:
                        self.__delattr__(tag)
                    except AttributeError:
                        warnings.warn("could not delete tag " + tag, RuntimeWarning)

            self._parse_segments(self.get_file())

    def get(self, attribute: str, default: Any = None) -> Any:
        """Return the value of the specified tag.

        If the attribute is not available or set, return the value specified by the ``default``
        keyword argument.

        :param attribute: image EXIF attribute name
        :param default: return value if attribute does not exist
        :returns: tag value if present, ``default`` otherwise
        :rtype: corresponding Python type

        """
        try:
            retval = self.__getattr__(attribute)
        except (AttributeError, NotImplementedError):
            retval = default

        return retval

    def get_all(self) -> Dict[str, Any]:
        """Return dictionary containing all EXIF tag values keyed by tag name."""
        all_tags = {}

        for tag_name in self.list_all():
            try:
                tag_value = self.__getattr__(tag_name)
            except Exception:  # pylint: disable=broad-except
                logger.warning("unable to read tag %r", tag_name)
            else:
                all_tags[tag_name] = tag_value

        return all_tags

    def get_file(self) -> bytes:
        """Generate equivalent binary file contents.

        :returns: image binary with EXIF metadata

        """
        assert isinstance(self._segments["preceding"], bytes)
        img_bytes = self._segments["preceding"]

        if self._has_exif:
            assert isinstance(self._segments["APP1"], App1MetaData)
            img_bytes += self._segments["APP1"].get_segment_bytes()

        assert isinstance(self._segments["succeeding"], bytes)
        img_bytes += self._segments["succeeding"]

        return img_bytes

    def get_thumbnail(self) -> bytes:
        """Extract thumbnail binary contained in EXIF metadata.

        :returns: thumbnail binary
        :raises RuntimeError: image does not contain thumbnail

        """
        thumbnail_bytes = None

        try:
            app1_segment = self._segments["APP1"]
        except KeyError:
            pass
        else:
            assert isinstance(app1_segment, App1MetaData)
            thumbnail_bytes = app1_segment.thumbnail_bytes

        if not thumbnail_bytes:
            raise RuntimeError("image does not contain thumbnail")

        return thumbnail_bytes

    @property
    def has_exif(self) -> bool:
        """Report whether or not the image currently has EXIF metadata."""
        return self._has_exif

    def list_all(self) -> List[str]:
        """List all EXIF tags contained in the image."""
        tags_list = []

        if self._has_exif:
            assert isinstance(self._segments["APP1"], App1MetaData)
            tags_list += self._segments["APP1"].get_tag_list(include_unknown=False)

        return tags_list

    def set(self, attribute: str, value) -> None:
        """Set the value of the specified attribute.

        :param attribute: image EXIF attribute name
        :param value: tag value
        :type value: corresponding Python type

        """
        self.__setattr__(attribute, value)

"""Image EXIF metadata interface module."""

import logging
import os
import warnings
from typing import Any, BinaryIO, Dict, List, Union

from plum.bigendian import uint16

from exif._constants import ATTRIBUTE_ID_MAP, ExifMarkers
from exif._app1_create import generate_empty_app1_bytes
from exif._app1_metadata import App1MetaData

logger = logging.getLogger(__name__)


class Image:

    """Image EXIF metadata interface class.

    :param img_file: image file with EXIF metadata
    :type image_file: str (file path), bytes (already-read contents), or File

    """

    def _parse_segments(self, img_bytes: bytes) -> None:
        cursor = 0

        # Traverse hexadecimal string until EXIF APP1 segment found.
        while img_bytes[cursor : cursor + len(ExifMarkers.APP1)] != ExifMarkers.APP1:
            cursor += len(ExifMarkers.APP1)
            if cursor > len(img_bytes):
                self._has_exif = False
                cursor = 2  # should theoretically go after SOI marker (if adding)
                break

        self._segments["preceding"] = img_bytes[:cursor]
        app1_start_index = cursor

        if self._has_exif:
            # Determine the expected length of the APP1 segment.
            app1_len = uint16.unpack(
                img_bytes[app1_start_index + 2 : app1_start_index + 4]
            )
            cursor += app1_len + 1

            # If the expected length stops early, keep traversing until another section is found.
            while img_bytes[cursor - 2 : cursor - 1] != ExifMarkers.SEG_PREFIX:
                cursor += 1
                # raise IOError("no subsequent EXIF segment found, is this an EXIF-encoded JPEG?")
                if cursor > len(img_bytes):
                    self._has_exif = False
                    break

        if self._has_exif:
            # Instantiate an APP1 segment object to create an EXIF tag interface.
            self._segments["APP1"] = App1MetaData(img_bytes[app1_start_index:cursor])
            self._segments["succeeding"] = img_bytes[cursor:]
        else:
            # Store the remainder of the image so that it can be reconstructed when exporting.
            self._segments["succeeding"] = img_bytes[app1_start_index:]

    def __init__(
        self,
        img_file: Union[BinaryIO, bytes, str],  # pylint: disable=unsubscriptable-object
    ) -> None:
        self._has_exif = True
        self._segments: Dict[
            str, Union[App1MetaData, bytes]  # pylint: disable=unsubscriptable-object
        ] = {}

        if hasattr(img_file, "read"):
            img_bytes = img_file.read()  # type: ignore
        elif isinstance(img_file, bytes):
            img_bytes = img_file
        elif os.path.isfile(img_file):  # type: ignore
            with open(img_file, "rb") as file_descriptor:  # type: ignore
                img_bytes = file_descriptor.read()
        else:  # pragma: no cover
            raise ValueError("expected file object, file path as str, or bytes")

        self._parse_segments(img_bytes)

    def __dir__(self) -> List[str]:
        members = [
            "delete",
            "delete_all",
            "get",
            "get_all",
            "get_file",
            "get_thumbnail",
            "has_exif",
            "list_all",
            "_segments",
        ]

        if self._has_exif:
            assert isinstance(self._segments["APP1"], App1MetaData)
            members += self._segments["APP1"].get_tag_list()

        return members

    def __getattr__(self, item):
        return getattr(self._segments["APP1"], item)

    def __setattr__(self, key, value):
        try:
            ATTRIBUTE_ID_MAP[key.lower()]
        except KeyError:
            super(Image, self).__setattr__(key, value)
        else:
            if not self._has_exif:
                self._segments["APP1"] = App1MetaData(generate_empty_app1_bytes())
                self._has_exif = True

            setattr(self._segments["APP1"], key.lower(), value)

    def __delattr__(self, item):
        try:
            ATTRIBUTE_ID_MAP[item]
        except KeyError:
            super(Image, self).__delattr__(item)
        else:
            delattr(self._segments["APP1"], item)

    def __getitem__(self, item):
        return self.__getattr__(item)

    def __setitem__(self, key, value):
        self.__setattr__(key, value)

    def __delitem__(self, key):
        self.__delattr__(key)

    def delete(self, attribute: str) -> None:
        """Remove the specified attribute from the image.

        :param attribute: image EXIF attribute name

        """
        self.__delattr__(attribute)

    def delete_all(self) -> None:
        """Remove all EXIF tags from the image."""
        for _ in range(
            2
        ):  # iterate twice to delete thumbnail tags the second time around
            assert isinstance(self._segments["APP1"], App1MetaData)
            for tag in self._segments["APP1"].get_tag_list():
                if not tag in ["_exif_ifd_pointer", "_gps_ifd_pointer", "exif_version"]:
                    try:
                        self.__delattr__(tag)
                    except AttributeError:
                        warnings.warn("could not delete tag " + tag, RuntimeWarning)

            self._parse_segments(self.get_file())

    def get(self, attribute: str, default: Any = None) -> Any:
        """Return the value of the specified tag.

        If the attribute is not available or set, return the value specified by the ``default``
        keyword argument.

        :param attribute: image EXIF attribute name
        :param default: return value if attribute does not exist
        :returns: tag value if present, ``default`` otherwise
        :rtype: corresponding Python type

        """
        try:
            retval = self.__getattr__(attribute)
        except (AttributeError, NotImplementedError):
            retval = default

        return retval

    def get_all(self) -> Dict[str, Any]:
        """Return dictionary containing all EXIF tag values keyed by tag name."""
        all_tags = {}

        for tag_name in self.list_all():
            try:
                tag_value = self.__getattr__(tag_name)
            except Exception:  # pylint: disable=broad-except
                logger.warning("unable to read tag %r", tag_name)
            else:
                all_tags[tag_name] = tag_value

        return all_tags

    def get_file(self) -> bytes:
        """Generate equivalent binary file contents.

        :returns: image binary with EXIF metadata

        """
        assert isinstance(self._segments["preceding"], bytes)
        img_bytes = self._segments["preceding"]

        if self._has_exif:
            assert isinstance(self._segments["APP1"], App1MetaData)
            img_bytes += self._segments["APP1"].get_segment_bytes()

        assert isinstance(self._segments["succeeding"], bytes)
        img_bytes += self._segments["succeeding"]

        return img_bytes

    def get_thumbnail(self) -> bytes:
        """Extract thumbnail binary contained in EXIF metadata.

        :returns: thumbnail binary
        :raises RuntimeError: image does not contain thumbnail

        """
        thumbnail_bytes = None

        try:
            app1_segment = self._segments["APP1"]
        except KeyError:
            pass
        else:
            assert isinstance(app1_segment, App1MetaData)
            thumbnail_bytes = app1_segment.thumbnail_bytes

        if not thumbnail_bytes:
            raise RuntimeError("image does not contain thumbnail")

        return thumbnail_bytes

    @property
    def has_exif(self) -> bool:
        """Report whether or not the image currently has EXIF metadata."""
        return self._has_exif

    def list_all(self) -> List[str]:
        """List all EXIF tags contained in the image."""
        tags_list = []

        if self._has_exif:
            assert isinstance(self._segments["APP1"], App1MetaData)
            tags_list += self._segments["APP1"].get_tag_list(include_unknown=False)

        return tags_list

    def set(self, attribute: str, value) -> None:
        """Set the value of the specified attribute.

        :param attribute: image EXIF attribute name
        :param value: tag value
        :type value: corresponding Python type

        """
        self.__setattr__(attribute, value)

Image EXIF metadata interface module. Image EXIF metadata interface class.      :param img_file: image file with EXIF metadata     :type image_file: str (file path), bytes (already-read contents), or File # Traverse hexadecimal string until EXIF APP1 segment found. # should theoretically go after SOI marker (if adding) # Determine the expected length of the APP1 segment. # If the expected length stops early, keep traversing until another section is found. # raise IOError("no subsequent EXIF segment found, is this an EXIF-encoded JPEG?") # Instantiate an APP1 segment object to create an EXIF tag interface. # Store the remainder of the image so that it can be reconstructed when exporting. # pylint: disable=unsubscriptable-object # pylint: disable=unsubscriptable-object # type: ignore # type: ignore # type: ignore # pragma: no cover Remove the specified attribute from the image.          :param attribute: image EXIF attribute name Remove all EXIF tags from the image. # iterate twice to delete thumbnail tags the second time around Return the value of the specified tag.          If the attribute is not available or set, return the value specified by the ``default``         keyword argument.          :param attribute: image EXIF attribute name         :param default: return value if attribute does not exist         :returns: tag value if present, ``default`` otherwise         :rtype: corresponding Python type Return dictionary containing all EXIF tag values keyed by tag name. # pylint: disable=broad-except Generate equivalent binary file contents.          :returns: image binary with EXIF metadata Extract thumbnail binary contained in EXIF metadata.          :returns: thumbnail binary         :raises RuntimeError: image does not contain thumbnail Report whether or not the image currently has EXIF metadata. List all EXIF tags contained in the image. Set the value of the specified attribute.          :param attribute: image EXIF attribute name         :param value: tag value         :type value: corresponding Python type