# Copyright (c) Facebook, Inc. and its affiliates.
import collections
import os

import torch
import torchvision
import torchvision.datasets.folder as tv_helpers

from mmf.utils.file_io import PathManager
from mmf.utils.general import get_absolute_path


def get_possible_image_paths(path):
    for ext in tv_helpers.IMG_EXTENSIONS:
        image_ext = ".".join(path.split(".")[:-1]) + ext
        if PathManager.isfile(image_ext):
            path = image_ext
            break
    return path


class ImageDatabase(torch.utils.data.Dataset):
    """ImageDatabase can be used to load images in MMF.
    This goes either in conjunction with AnnotationDatabase or
    can be separately used with function such as `from_path`.
    MMFDataset initializes its own copy of ImageDatabase if `use_images`
    is True. Rest everything works same as a normal torch Dataset if
    you pass the annotation_db as a parameter. For example for item
    1 from annotation db, you can pass same id to ImageDatabase to loads
    its image. If you don't pass it, you have two options. Either use
    .get which takes in an annotation db item or .from_path which directly
    takes in an image path. You are free to use your own dataset instead
    of image database or free to update or ignore MMFDataset's ImageDataset
    initiliazation. You can either reinitializ with transform and other
    params or use any of torchvision's datasets.
    """

    def __init__(
        self,
        config,
        path,
        annotation_db=None,
        transform=None,
        loader=tv_helpers.default_loader,
        is_valid_file=None,
        image_key=None,
        *args,
        **kwargs
    ):
        """Initialize an instance of ImageDatabase

        Args:
            torch ([type]): [description]
            config (DictConfig): Config object from dataset_config
            path (str): Path to images folder
            annotation_db (AnnotationDB, optional): Annotation DB to be used
                to be figure out image paths. Defaults to None.
            transform (callable, optional): Transform to be called upon loaded image.
                Defaults to None.
            loader (callable, optional): Custom loader for image which given a path
                returns a PIL Image. Defaults to torchvision's default loader.
            is_valid_file (callable, optional): Custom callable to filter out invalid
                files. If image is invalid, {"images": []} will returned which you can
                filter out in your dataset. Defaults to None.
            image_key (str, optional): Key that points to image path in annotation db.
                If not specified, ImageDatabase will make some intelligent guesses
                about the possible key. Defaults to None.
        """
        super().__init__()
        self.config = config
        self.base_path = get_absolute_path(path)
        self.transform = transform
        self.annotation_db = annotation_db
        self.loader = loader
        self.image_key = config.get("image_key", None)
        self.image_key = image_key if image_key else self.image_key
        self.is_valid_file = is_valid_file

    @property
    def annotation_db(self):
        return self._annotation_db

    @annotation_db.setter
    def annotation_db(self, annotation_db):
        self._annotation_db = annotation_db

    @property
    def transform(self):
        return self._transform

    @transform.setter
    def transform(self, transform):
        if isinstance(transform, collections.abc.MutableSequence):
            transform = torchvision.Compose(transform)
        self._transform = transform

    def __len__(self):
        self._check_annotation_db_present()
        return len(self.annotation_db)

    def __getitem__(self, idx):
        self._check_annotation_db_present()
        item = self.annotation_db[idx]
        return self.get(item)

    def _check_annotation_db_present(self):
        if not self.annotation_db:
            raise AttributeError(
                "'annotation_db' must be set for the database to use __getitem__."
                + " Use image_database.annotation_db to set it."
            )

    def get(self, item):
        possible_images = self._get_attrs(item)
        return self.from_path(possible_images)

    def from_path(self, paths):
        if isinstance(paths, str):
            paths = [paths]

        assert isinstance(
            paths, collections.abc.Iterable
        ), "Path needs to a string or an iterable"

        loaded_images = []
        for image in paths:
            path = None
            path = get_possible_image_paths(image)

            valid = self.is_valid_file(path) if self.is_valid_file is not None else True

            if not valid:
                continue

            if not path:
                # Create the full path without extension so it can be printed
                # for the error
                possible_path = os.path.join(
                    self.base_path, ".".join(image.split(".")[:-1])
                )

                raise RuntimeError(
                    "Image not found at path {}.{{jpeg|jpg|svg|png}}.".format(
                        possible_path
                    )
                )
            path = os.path.join(self.base_path, path)
            image = self.open_image(path)

            if self.transform:
                image = self.transform(image)
            loaded_images.append(image)

        return {"images": loaded_images}

    def open_image(self, path):
        return self.loader(path)

    def _get_attrs(self, item):
        """Returns possible attribute that can point to image id

        Args:
            item (Object): Object from the DB

        Returns:
            List[str]: List of possible images that will be copied later
        """
        if self.image_key:
            image = item[self.image_key]
            if isinstance(image, str):
                image = [image]
            return image

        image = None
        pick = None
        attrs = self._get_possible_attrs()

        for attr in attrs:
            image = item.get(attr, None)
            if image is not None:
                pick = attr
                break

        # Check if first one is nlvr2
        if pick == "identifier" and "left_url" in item and "right_url" in item:
            return [image + "-img0.jpg", image + "-img1.jpg"]
        elif pick == "image_name" or pick == "image_id":
            return [image + ".jpg"]
        else:
            return [image]

    def _get_possible_attrs(self):
        return [
            "Flickr30kID",
            "Flikr30kID",
            "identifier",
            "image_path",
            "image_name",
            "img",
            "image_id",
        ]
