# -*- coding: utf-8 -*-
# Copyright (c) 2020, NVIDIA CORPORATION.  All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from dataclasses import dataclass
from os.path import expanduser
from typing import Optional

from torchvision.datasets import MNIST
from torchvision.transforms import Compose, Resize, ToTensor

from nemo.core.classes import Dataset
from nemo.core.config import Config
from nemo.core.neural_types.axes import AxisKind, AxisType
from nemo.core.neural_types.elements import ClassificationTarget, Index, NormalizedImageValue, StringLabel
from nemo.core.neural_types.neural_type import NeuralType
from nemo.utils.decorators import add_port_docs, experimental


@dataclass
class MNISTDatasetConfig(Config):
    """
    Structured config for MNISTDataset class.

    Args:
        height: image height (DEFAULT: 28)
        width: image width (DEFAULT: 28)
        data_folder: path to the folder with data, can be relative to user (DEFAULT: "~/data/mnist")
        train: use train or test splits (DEFAULT: True)
        name: Name of the module (DEFAULT: None)
    """

    height: int = 28
    width: int = 28
    data_folder: str = "~/data/mnist"
    train: bool = True
    download: bool = True


class MNISTDataset(Dataset):
    """
    A "thin wrapper" around the torchvision's MNIST dataset.
    """

    def __init__(self, cfg: MNISTDatasetConfig = MNISTDatasetConfig()):
        """
        Initializes the MNIST dataset.

        Args:
            cfg: Configuration object of type MNISTDatasetConfig.

        """
        # Call the base class constructor of Dataset.
        Dataset.__init__(self)  # , name=name)

        # Store height and width.
        self._height = cfg.height
        self._width = cfg.width

        # Create transformations: up-scale and transform to tensors.
        mnist_transforms = Compose([Resize((self._height, self._width)), ToTensor()])

        # Get absolute path.
        abs_data_folder = expanduser(cfg.data_folder)

        # Create the MNIST dataset object.
        self._dataset = MNIST(root=abs_data_folder, train=cfg.train, download=cfg.download, transform=mnist_transforms)

        # Class names.
        labels = 'Zero One Two Three Four Five Six Seven Eight Nine'.split(' ')
        word_to_ix = {labels[i]: i for i in range(10)}

        # Reverse mapping.
        self._ix_to_word = {value: key for (key, value) in word_to_ix.items()}

    @property
    def output_types(self):
        """
        Creates definitions of output ports.
        """
        return {
            "indices": NeuralType(tuple('B'), elements_type=Index()),
            "images": NeuralType(
                axes=(
                    AxisType(kind=AxisKind.Batch),
                    AxisType(kind=AxisKind.Channel, size=1),
                    AxisType(kind=AxisKind.Height, size=self._height),
                    AxisType(kind=AxisKind.Width, size=self._width),
                ),
                elements_type=NormalizedImageValue(),  # float, <0-1>
            ),
            "targets": NeuralType(tuple('B'), elements_type=ClassificationTarget()),  # Target are ints!
            "labels": NeuralType(tuple('B'), elements_type=StringLabel()),  # Labels is string!
        }

    def __len__(self):
        """
        Returns:
            Length of the dataset.
        """
        return len(self._dataset)

    def __getitem__(self, index: int):
        """
        Returns a single sample.

        Args:
            index: index of the sample to return.
        """
        # Get image and target.
        img, target = self._dataset.__getitem__(index)

        # Return sample.
        return index, img, target, self._ix_to_word[target]

    @property
    def ix_to_word(self):
        """
        Returns:
            Dictionary with mapping of target indices (int) to labels (class names as strings)
            that can we used by other modules.
        """
        return self._ix_to_word
