"""AdjustmentsInfo class to read adjustments data for photos edited in Apple's Photos.app
In Catalina and Big Sur, the adjustments data (data about edits done to the photo)
is stored in a plist file in
~/Pictures/Photos Library.photoslibrary/resources/renders/X/UUID.plist
where X is first character of the photo's UUID string and UUID is the full UUID,
e.g.: ~/Pictures/Photos Library.photoslibrary/resources/renders/3/30362C1D-192F-4CCD-9A2A-968F436DC0DE.plist

Thanks to @neilpa who figured out how to decode this information:
Reference: https://github.com/neilpa/photohack/issues/4
"""

import datetime
import json
import logging
import plistlib
import zlib
from typing import Any

from .datetime_utils import datetime_naive_to_utc

__all__ = ["AdjustmentsDecodeError", "AdjustmentsInfo"]

logger = logging.getLogger("osxphotos")


class AdjustmentsDecodeError(Exception):
    """Could not decode adjustments plist file"""

    def __init__(self, message):
        self.message = message
        super().__init__(self.message)


class AdjustmentsInfo:
    def __init__(self, plist_file):
        self._plist_file = plist_file
        self._plist = self._load_plist_file(plist_file)

        self._base_version = self._plist.get("adjustmentBaseVersion", None)
        self._data = self._plist.get("adjustmentData", None)
        self._editor_bundle_id = self._plist.get("adjustmentEditorBundleID", None)
        self._format_identifier = self._plist.get("adjustmentFormatIdentifier", None)
        self._format_version = self._plist.get("adjustmentFormatVersion")
        self._timestamp = self._plist.get("adjustmentTimestamp", None)
        if self._timestamp and type(self._timestamp) == datetime.datetime:
            self._timestamp = datetime_naive_to_utc(self._timestamp)

        try:
            self._adjustments = self._decode_adjustments_from_plist(self._plist)
        except Exception as e:
            logger.debug(f"Could not decode adjustments data: {plist_file}")
            self._adjustments = None

    def _decode_adjustments_from_plist(self, plist) -> dict | Any | None:
        """decode adjustmentData from Apple Photos adjustments

        Args:
            plist: a plist dict as loaded by plistlib

        Returns:
            decoded adjustmentData as a Python object (usually dict) or None if no data

        Raises:
            AdjustmentsDecodeError if the data cannot be decoded
        """
        data = plist.get("adjustmentData")
        if data is None:
            return None

        if isinstance(data, str):
            data = data.encode("utf-8")
        if not isinstance(data, (bytes, bytearray)):
            raise AdjustmentsDecodeError(
                f"'adjustmentData' must be bytes, got {type(data)}"
            )

        def _decode_bytes(b: bytes):
            """Try to decode raw bytes as binary plist, JSON, or XML plist.

            Returns decoded object or None if not decodable.
            """
            if b.startswith(b"bplist00"):
                try:
                    return plistlib.loads(b)
                except Exception:
                    pass

            try:
                return plistlib.loads(b)
            except Exception:
                pass

            try:
                return json.loads(b.decode("utf-8"))
            except Exception:
                pass

            return None

        result = _decode_bytes(data)
        if result is not None:
            return result

        # Result may be zlib compressed
        for wbits in (-zlib.MAX_WBITS, zlib.MAX_WBITS):
            try:
                decompressed = zlib.decompress(data, wbits)
            except zlib.error:
                continue

            result = _decode_bytes(decompressed)
            if result is not None:
                return result

        raise AdjustmentsDecodeError("Could not decode adjustment data")

    def _load_plist_file(self, plist_file):
        """Load plist file from disk

        Args:
            plist_file: full path to plist file

        Returns:
            plist as dict
        """
        with open(str(plist_file), "rb") as fd:
            try:
                plist_dict = plistlib.load(fd)
            except Exception as e:
                logger.debug(f"Could not load plist file: {plist_file}")
                plist_dict = {}
        return plist_dict

    @property
    def plist(self):
        """The actual adjustments plist content as a dict"""
        return self._plist

    @property
    def data(self):
        """The raw adjustments data as a binary blob"""
        return self._data

    @property
    def editor(self):
        """The editor bundle ID for app/plug-in which made the adjustments"""
        return self._editor_bundle_id

    @property
    def format_id(self):
        """The value of the adjustmentFormatIdentifier field in the plist"""
        return self._format_identifier

    @property
    def base_version(self):
        """Value of adjustmentBaseVersion field"""
        return self._base_version

    @property
    def format_version(self):
        """The value of the adjustmentFormatVersion in the plist"""
        return self._format_version

    @property
    def timestamp(self):
        """The time stamp of the adjustment as timezone aware datetime.datetime object or None if no timestamp"""
        return self._timestamp

    @property
    def adjustments(self):
        """List of adjustment dictionaries (or empty list if none or could not be decoded)"""
        try:
            return self._adjustments["adjustments"] if self._adjustments else []
        except (KeyError, TypeError):
            return []

    @property
    def adj_metadata(self):
        """Metadata dictionary or None if adjustment data could not be decoded"""
        try:
            return self._adjustments["metadata"] if self._adjustments else None
        except (KeyError, TypeError):
            return None

    @property
    def adj_orientation(self):
        """EXIF orientation of image or 0 if none specified or None if adjustments could not be decoded"""
        try:
            return self._adjustments["metadata"]["orientation"]
        except (KeyError, TypeError):
            # no orientation field or adjustments is None
            return 0

    @property
    def adj_format_version(self):
        """Format version for adjustments data (formatVersion field from adjustmentData) or None if adjustments could not be decoded"""
        try:
            return self._adjustments["formatVersion"] if self._adjustments else None
        except (KeyError, TypeError):
            return None

    @property
    def adj_version_info(self):
        """version info for adjustments data or None if adjustments data could not be decoded"""
        try:
            return self._adjustments["versionInfo"] if self._adjustments else None
        except (KeyError, TypeError):
            return None

    def asdict(self):
        """Returns all adjustments info as dictionary"""
        timestamp = self.timestamp
        if type(timestamp) == datetime.datetime:
            timestamp = timestamp.isoformat()

        return {
            "data": self.data,
            "editor": self.editor,
            "format_id": self.format_id,
            "base_version": self.base_version,
            "format_version": self.format_version,
            "adjustments": self.adjustments,
            "metadata": self.adj_metadata,
            "orientation": self.adj_orientation,
            "adjustment_format_version": self.adj_format_version,
            "version_info": self.adj_version_info,
            "timestamp": timestamp,
        }

    def __repr__(self):
        return f"AdjustmentsInfo(plist_file='{self._plist_file}')"
