# from https://github.com/pytorch/vision/blob/master/torchvision/datasets/mnist.py

import torch.utils.data as data
from PIL import Image
import os
import os.path
import errno
import torch
import codecs

class MNIST(data.Dataset):
    """`MNIST <http://yann.lecun.com/exdb/mnist/>`_ Dataset.

    Args:
        root (string): Root directory of dataset where ``processed/training.pt``
            and  ``processed/test.pt`` exist.
        train (string, optional): train, valid or test to select teh corresponding dataset
        download (bool, optional): If true, downloads the dataset from the internet and
            puts it in root directory. If dataset is already downloaded, it is not
            downloaded again.
        transform (callable, optional): A function/transform that  takes in an PIL image
            and returns a transformed version. E.g, ``transforms.RandomCrop``
        target_transform (callable, optional): A function/transform that takes in the
            target and transforms it.
    """
    urls = [
        'http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz',
        'http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz',
        'http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz',
        'http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz',
    ]
    raw_folder = 'raw'
    processed_folder = 'processed'
    training_file = 'training.pt'
    test_file = 'test.pt'

    def __init__(self, root, dataset="test", transform=None, target_transform=None, download=False):
        self.root = os.path.expanduser(root)
        self.transform = transform
        self.target_transform = target_transform
        self.dataset = dataset  # train, valid or test set

        if download:
            self.download()

        if not self._check_exists():
            raise RuntimeError('Dataset not found.' +
                               ' You can use download=True to download it')

        if "train" in self.dataset:
            self.train_data, self.train_labels = torch.load(
                os.path.join(self.root, self.processed_folder, self.training_file))
            self.train_data, self.train_labels =  self.train_data[:-10000], self.train_labels[:-10000]
        elif "valid" in self.dataset:
            self.valid_data, self.valid_labels = torch.load(
                os.path.join(self.root, self.processed_folder, self.training_file))
            self.valid_data, self.valid_labels =  self.valid_data[-10000:], self.valid_labels[-10000:]
        elif "test" in self.dataset:
            self.test_data, self.test_labels = torch.load(
                os.path.join(self.root, self.processed_folder, self.test_file))
        else:
            raise RuntimeError('Dataset parameter can be: "train", "valid" or "test".')

    def __getitem__(self, index):
        """
        Args:
            index (int): Index

        Returns:
            tuple: (image, target) where target is index of the target class.
        """
        if "train" in self.dataset:
            img, target = self.train_data[index], self.train_labels[index]
        elif "valid" in self.dataset:
            img, target = self.valid_data[index], self.valid_labels[index]
        elif "test" in self.dataset:
            img, target = self.test_data[index], self.test_labels[index]
        else:
            raise RuntimeError('Dataset parameter can be: "train", "valid" or "test".')

        # doing this so that it is consistent with all other datasets
        # to return a PIL Image
        img = Image.fromarray(img.numpy(), mode='L')

        if self.transform is not None:
            img = self.transform(img)

        if self.target_transform is not None:
            target = self.target_transform(target)

        return img, target

    def __len__(self):
        if "train" in self.dataset:
            return len(self.train_data)
        elif "valid" in self.dataset:
            return len(self.valid_data)
        elif "test" in self.dataset:
            return len(self.test_data)
        else:
            raise RuntimeError('Dataset parameter can be: "train", "valid" or "test".')

    def _check_exists(self):
        return os.path.exists(os.path.join(self.root, self.processed_folder, self.training_file)) and \
               os.path.exists(os.path.join(self.root, self.processed_folder, self.test_file))

    def download(self):
        """Download the MNIST data if it doesn't exist in processed_folder already."""
        from six.moves import urllib
        import gzip

        if self._check_exists():
            return

        # download files
        try:
            os.makedirs(os.path.join(self.root, self.raw_folder))
            os.makedirs(os.path.join(self.root, self.processed_folder))
        except OSError as e:
            if e.errno == errno.EEXIST:
                pass
            else:
                raise

        for url in self.urls:
            print('Downloading ' + url)
            data = urllib.request.urlopen(url)
            filename = url.rpartition('/')[2]
            file_path = os.path.join(self.root, self.raw_folder, filename)
            with open(file_path, 'wb') as f:
                f.write(data.read())
            with open(file_path.replace('.gz', ''), 'wb') as out_f, \
                    gzip.GzipFile(file_path) as zip_f:
                out_f.write(zip_f.read())
            os.unlink(file_path)

        # process and save as torch files
        print('Processing...')

        training_set = (
            read_image_file(os.path.join(self.root, self.raw_folder, 'train-images-idx3-ubyte')),
            read_label_file(os.path.join(self.root, self.raw_folder, 'train-labels-idx1-ubyte'))
        )
        test_set = (
            read_image_file(os.path.join(self.root, self.raw_folder, 't10k-images-idx3-ubyte')),
            read_label_file(os.path.join(self.root, self.raw_folder, 't10k-labels-idx1-ubyte'))
        )
        with open(os.path.join(self.root, self.processed_folder, self.training_file), 'wb') as f:
            torch.save(training_set, f)
        with open(os.path.join(self.root, self.processed_folder, self.test_file), 'wb') as f:
            torch.save(test_set, f)

        print('Done!')

    def __repr__(self):
        fmt_str = self.__class__.__name__ + '\n'
        fmt_str += '    Number of datapoints: {}\n'.format(self.__len__())
        fmt_str += '    Root Location: {}\n'.format(self.root)
        tmp = '    Transforms (if any): '
        fmt_str += '{0}{1}\n'.format(tmp, self.transform.__repr__().replace('\n', '\n' + ' ' * len(tmp)))
        tmp = '    Target Transforms (if any): '
        fmt_str += '{0}{1}'.format(tmp, self.target_transform.__repr__().replace('\n', '\n' + ' ' * len(tmp)))
        return fmt_str


def get_int(b):
    return int(codecs.encode(b, 'hex'), 16)


def parse_byte(b):
    if isinstance(b, str):
        return ord(b)
    return b


def read_label_file(path):
    with open(path, 'rb') as f:
        data = f.read()
        assert get_int(data[:4]) == 2049
        length = get_int(data[4:8])
        labels = [parse_byte(b) for b in data[8:]]
        assert len(labels) == length
        return torch.LongTensor(labels)


def read_image_file(path):
    with open(path, 'rb') as f:
        data = f.read()
        assert get_int(data[:4]) == 2051
        length = get_int(data[4:8])
        num_rows = get_int(data[8:12])
        num_cols = get_int(data[12:16])
        images = []
        idx = 16
        for l in range(length):
            img = []
            images.append(img)
            for r in range(num_rows):
                row = []
                img.append(row)
                for c in range(num_cols):
                    row.append(parse_byte(data[idx]))
                    idx += 1
        assert len(images) == length
        return torch.ByteTensor(images).view(-1, 28, 28)


#######################################################################################################################
import random
import numpy as np
import tensorlayer.prepro as prepro

class DataAugmentation(object):
    def __init__(self, threshold=0.5):
        self.threshold = threshold

    def __call__(self, img):
        if random.random() < self.threshold:
            img = np.array(img)
            img = img.reshape(1, img.shape[0], img.shape[1])

            img = prepro.rotation(img.reshape(1, 28, 28), 7.5, is_random=True, row_index=1, col_index=2, channel_index=0)
            img = prepro.shift(img.reshape(1, 28, 28), 0.1, 0.1, is_random=True, row_index=1, col_index=2, channel_index=0)
            img = prepro.shear(img.reshape(1, 28, 28), 0.1, is_random=True, row_index=1, col_index=2, channel_index=0)
            img = prepro.zoom(img.reshape(1, 28, 28), (0.95, 1.05), is_random=True, row_index=1, col_index=2, channel_index=0)
            img = prepro.elastic_transform(img.reshape(28, 28), alpha=5.5, sigma=35, is_random=True)

            img = Image.fromarray(img)
        return img

#######################################################################################################################
