"""PhotosAlbum class to create an album in default Photos library and add photos to it"""

from __future__ import annotations

import unicodedata
from collections.abc import Iterable
from typing import Any, Callable, Optional

from more_itertools import chunked

from .photoinfo import PhotoInfo
from .platform import assert_macos
from .utils import noop, pluralize

assert_macos()

import photoscript
from photoscript import Album, Folder, Photo, PhotosLibrary

__all__ = ["PhotosAlbum", "PhotosAlbumPhotoScript"]


def get_unicode_variants(s: str) -> list[str]:
    """Get all unicode variants of string"""
    variants = []
    for form in ["NFC", "NFD", "NFKC", "NFKD"]:
        normalized = unicodedata.normalize(form, s)
        variants.append(normalized)
    return variants


def folder_by_path(
    folders: list[str], verbose: Optional[Callable[..., Any]] = None
) -> Folder:
    """Get (and create if necessary) a Photos Folder by path (passed as list of folder names)"""
    library = PhotosLibrary()
    verbose = verbose or noop
    top_folder_name = folders.pop(0)

    for folder_variant in get_unicode_variants(top_folder_name):
        top_folder = library.folder(folder_variant, top_level=True)
        if top_folder is not None:
            break
    else:
        verbose(f"Creating folder '{top_folder_name}'")
        top_folder = library.create_folder(top_folder_name)

    current_folder = top_folder
    for folder_name in folders:
        for folder_variant in get_unicode_variants(folder_name):
            folder = current_folder.folder(folder_variant)
            if folder is not None:
                break
        else:
            verbose(f"Creating folder '{folder_name}'")
            folder = current_folder.create_folder(folder_name)
        current_folder = folder
    return current_folder


def album_by_path(
    folders_album: list[str], verbose: Optional[Callable[..., Any]] = None
) -> Album:
    """Get (and create if necessary) a Photos Album by path (pass as list of folders, album name)"""
    library = PhotosLibrary()
    verbose = verbose or noop
    folders_album = folders_album.copy()
    if len(folders_album) > 1:
        # have folders
        album_name = folders_album.pop()
        folder = folder_by_path(folders_album, verbose)
        for album_variant in get_unicode_variants(album_name):
            # Get album if it exists
            # need to check every unicode variant to avoid creating duplicate albums with same visual representation (#1085)
            album = folder.album(album_variant)
            if album is not None:
                break
        else:
            verbose(f"Creating album '{album_name}'")
            album = folder.create_album(album_name)
    else:
        # only have album name
        album_name = folders_album[0]
        for album_variant in get_unicode_variants(album_name):
            album = library.album(album_variant, top_level=True)
            if album is not None:
                break
        else:
            # album doesn't exist, create it
            verbose(f"Creating album '{album_name}'")
            album = library.create_album(album_name)

    return album


class PhotosAlbum:
    """Add osxphotos.photoinfo.PhotoInfo objects to album"""

    def __init__(
        self,
        name: str,
        verbose: Optional[Callable[..., Any]] = None,
        split_folder: Optional[str] = None,
        rich: bool = False,
    ):
        """Return a PhotosAlbum object, creating the album if necessary

        Args:
            name: Name of album
            verbose: optional callable to print verbose output
            split_folder: if set, split album name on value of split_folder to create folders if necessary,
                e.g. if name = 'folder1/folder2/album' and split_folder='/',
                then folders 'folder1' and 'folder2' will be created and album 'album' will be created in 'folder2';
                if not set, album 'folder1/folder2/album' will be created
            rich: if True, use rich themes for verbose output
        """
        self.verbose = verbose or noop
        self.library = photoscript.PhotosLibrary()

        folders_album = name.split(split_folder) if split_folder else [name]
        if folders_album and folders_album[0] == "":
            folders_album = folders_album[1:]
        self.album = album_by_path(folders_album, verbose=verbose)
        self.name = name
        self.rich = rich

    def add(self, photo: PhotoInfo):
        if photo.shared or (photo.syndicated and not photo.saved_to_library):
            # shared / unsaved syndicated photos cannot be added to albums
            return
        photo_ = photoscript.Photo(photo.uuid)
        self.album.add([photo_])
        self.verbose(
            f"Added {self._format_name(photo.original_filename)} ({self._format_uuid(photo.uuid)}) to album {self._format_album(self.name)}"
        )

    def update(self, photos: Iterable[PhotoInfo]):
        photoscript_photos = []
        for p in photos:
            if p.shared or (p.syndicated and not p.saved_to_library):
                # shared / unsaved syndicated photos cannot be added to albums
                continue
            try:
                photoscript_photos.append(photoscript.Photo(p.uuid))
            except Exception as e:
                self.verbose(
                    f"Error creating Photo object for photo {self._format_uuid(p.uuid)}: {e}"
                )
        for photolist in chunked(photoscript_photos, 10):
            self.album.add(photolist)
        photo_len = len(photos)
        self.verbose(
            f"Added {self._format_num(photo_len)} {pluralize(photo_len, 'photo', 'photos')} to album {self._format_album(self.name)}"
        )

    def append(self, photo: PhotoInfo):
        """Add photo to album"""
        self.add(photo)

    def extend(self, photos: Iterable[PhotoInfo]):
        """Add list of photos to album"""
        self.update(photos)

    def photos(self):
        return self.album.photos()

    def _format_uuid(self, uuid: str) -> str:
        """ "Format uuid for verbose output"""
        return f"[uuid]{uuid}[/uuid]" if self.rich else uuid

    def _format_album(self, album: str) -> str:
        """ "Format album name for verbose output"""
        return f"[filepath]{album}[/filepath]" if self.rich else album

    def _format_name(self, name: str) -> str:
        """ "Format name for verbose output"""
        return f"[filename]{name}[/filename]" if self.rich else name

    def _format_num(self, num: int) -> str:
        """ "Format number for verbose output"""
        return f"[num]{num}[/num]" if self.rich else str(num)


class PhotosAlbumPhotoScript(PhotosAlbum):
    """Add photoscript.Photo objects to album"""

    def add(self, photo: Photo):
        self.album.add([photo])
        self.verbose(
            f"Added {self._format_name(photo.filename)} ({self._format_uuid(photo.uuid)}) to album {self._format_album(self.name)}"
        )

    def update(self, photos: Iterable[Photo]):
        for photolist in chunked(photos, 10):
            self.album.add(photolist)
        photo_len = len(photos)
        self.verbose(
            f"Added {self._format_num(photo_len)} {pluralize(photo_len, 'photo', 'photos')} to album {self._format_album(self.name)}"
        )

    def append(self, photo: Photo):
        """Add photo to album"""
        self.add(photo)

    def extend(self, photos: Iterable[Photo]):
        """Add list of photos to album"""
        self.update(photos)


class PhotosAlbumPhotoScriptByPath(PhotosAlbumPhotoScript):
    """Add photoscript.Photo objects to album"""

    def __init__(
        self,
        album_path: list[str],
        verbose: Optional[callable] = None,
        rich: bool = False,
    ):
        """Return a PhotosAlbum object, creating the album if necessary

        Args:
            album_path: Path of album, e.g. ["Folder", "Album"] or ["Album"]
            verbose: optional callable to print verbose output
            rich: if True, use rich themes for verbose output
        """
        self.verbose = verbose or noop
        self.library = photoscript.PhotosLibrary()

        self.album = album_by_path(album_path, verbose=verbose)
        self.name = album_path[-1]
        self.rich = rich
