
import os

import h5py as hf

from omnibelt import unspecified_argument

import numpy as np
import torch
from torch.nn import functional as F
import torchvision

from ... import util
from ...data import register_dataset, Batchable, Deviced, Downloadable, ImageDataset


from .transforms import Interpolated





class Torchvision_Toy_Dataset(Batchable, Deviced, Downloadable, ImageDataset): # TODO: enable (pytorch) transforms
	available_modes = {'train', 'test'}

	_default_label_attr = 'targets'

	def __init__(self, A, dataroot=None, mode=None, labeled=None, label_attr=unspecified_argument,
	             din=None, dout=None,
	             dataset_kwargs=None, _req_kwargs=None, **unused):
		'''
		Requires dataset object to wrap it (since this is a `util.Simple_Child`).

		Images must be in `dataset.data` and labels in `dataset.[label_attr]` (if provided).

		:param dataset: compatible pytorch torchvision dataset object
		:param train: bool
		:param label_attr: attr name used to access labels in `dataset`
		:param din: optional (if it has to be overwritten)
		:param dout: optional (if it has to be overwritten)
		:param root: path to dataset files
		'''
		
		if dataroot is None:
			dataroot = self._get_dataroot(A)
			
		if mode is None:
			mode = A.pull('mode', 'train')
		
		if dataset_kwargs is None:
			dataset_kwargs = self._get_dataset_kwargs(A, mode=mode, **unused)
		if 'root' not in dataset_kwargs:
			dataset_kwargs['root'] = str(dataroot)

		resize = A.pull('resize', True)
		C, H, W = self.din
		if resize and (H != 32 or W != 32):
			self.din = C, 32, 32

		if label_attr is unspecified_argument:
			if labeled is None:
				labeled = A.pull('labeled', True)
			label_attr = A.pull('label_attr', self._default_label_attr) if labeled else None

		if label_attr is None:
			dout = self.din if din is None else din

		if _req_kwargs is None:
			_req_kwargs = dataset_kwargs
		else:
			_req_kwargs.update(dataset_kwargs)

		super().__init__(A, din=din, dout=dout, dataroot=dataroot,
		                 _req_kwargs=_req_kwargs,
		                 **unused)
		self.add_existing_modes('test')

		self.labeled = label_attr is not None

		# self.root = root

		images = self.data
		if isinstance(images, np.ndarray):
			images = torch.from_numpy(images)
		images = images.float().div(255)
		if images.ndimension() == 3:
			images = images.unsqueeze(1)
		
		if resize:
			images = F.interpolate(images, (32, 32), mode='bilinear')
		
		if label_attr is None:
			self.labels = None
		else:
			labels = getattr(self, label_attr)
			if not isinstance(labels, torch.Tensor):
				labels = torch.tensor(labels)
			self.register_buffer('labels', labels)
			# delattr(self, label_attr)

		self.register_buffer('images', images)
		# del self.data

	def download(cls, A, **kwargs):
		cls(A, download=True)

	def get_raw_data(self):
		if self.labeled:
			return self.images, self.labels
		return self.images

	def __len__(self):
		return len(self.images)

	def __getitem__(self, item):
		img = self.images[item]
		if self.labeled:
			return img, self.labels[item]
		return img
	
	@classmethod
	def _get_dataroot(cls, A=None, ident=None, silent=False):
		dataroot = util.get_data_dir(A, silent=silent)
		if ident is None:
			assert hasattr(cls, '_dataset_ident'), f'{cls.__name__} must be registered if no ident is provided'
			ident = cls._dataset_ident
		return dataroot / ident
	
	@classmethod
	def _get_dataset_kwargs(cls, A, mode=None, download=None, train=None, **unused):
		
		if mode is None:
			mode = A.pull('mode', 'train')
		
		if download is None:
			download = A.pull('download', False)
		
		if train is None:
			train = A.pull('train', mode != 'test')
		
		return {'download': download, 'train':train}



@register_dataset('mnist')
class MNIST(Torchvision_Toy_Dataset, util.InitWall, torchvision.datasets.MNIST):
	din = (1, 28, 28)
	dout = 10

@register_dataset('kmnist')
class KMNIST(Torchvision_Toy_Dataset, util.InitWall, torchvision.datasets.KMNIST):
	din = (1, 28, 28)
	dout = 10

@register_dataset('fmnist')
class FashionMNIST(Torchvision_Toy_Dataset, util.InitWall, torchvision.datasets.FashionMNIST):
	din = (1, 28, 28)
	dout = 10

@register_dataset('emnist')
class EMNIST(Torchvision_Toy_Dataset, util.InitWall, torchvision.datasets.EMNIST):
	din = (1, 28, 28)
	dout = 26
	
	_split_keys = {
		'byclass': '<KEY>',
		'bymerge': '<KEY>',
		'letters': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
		'digits': '0123456789',
		'mnist': '0123456789',
		'balanced': '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabdefghnqrt',
	}
	
	def __init__(self, A, **kwargs):
		
		# split = A.pull('group', 'letters')
		# if split != 'letters':
		# 	raise NotImplementedError
		
		# dataset = torchvision.datasets.EMNIST(str(dataroot), split=split, **kwargs)


		# labeled = A.pull('labeled', False)
		
		
		# labels_key = self._split_keys.get(split, None) if labeled else None
		# dout = len(labels_key) if labeled else None
		
		# TODO: Automodifier for selected_classes
		# selected_classes = A.pull('selected_classes', None)
		# if selected_classes is not None:
		# 	sel = None
		# 	lbls = dataset.targets.clone()
		# 	full_key = labels_key
		# 	labels_key = []
		#
		# 	for i, c in enumerate(selected_classes):
		#
		# 		s = lbls == c
		# 		if sel is None:
		# 			sel = s
		# 		else:
		# 			sel += s
		# 		dataset.targets[s] = i
		# 		if full_key is not None:
		# 			labels_key.append(full_key[c])
		#
		# 	dataset.targets = dataset.targets[sel]
		# 	dataset.data = dataset.data[sel]
		#
		# 	if labeled:
		# 		dout = len(selected_classes)

		super().__init__(A, **kwargs)
		
		self.images = self.images.permute(0,1,3,2)
		
		if self.split == 'letters' and self.labels is not None:
			self.labels -= 1  # targets use 1-based indexing :(
		
		self.labels_key = self._split_keys[self.split]
	
	@classmethod
	def _get_dataset_kwargs(cls, A, split=None, **other):
		kwargs = super()._get_dataset_kwargs(A, **other)
		if split is None:
			split = A.pull('group', 'letters')
		kwargs['split'] = split
		return kwargs


@register_dataset('svhn')
class SVHN(Torchvision_Toy_Dataset, util.InitWall, torchvision.datasets.SVHN):
	din = (3, 32, 32)
	dout = 10
	
	_default_label_attr = 'labels'

	@classmethod
	def _get_dataset_kwargs(cls, A, **unused):
		kwargs = super()._get_dataset_kwargs(A, **unused)
		kwargs['split'] = 'train' if kwargs['train'] else 'test'
		del kwargs['train']
		return kwargs


@register_dataset('cifar10')
class CIFAR10(Torchvision_Toy_Dataset, util.InitWall, torchvision.datasets.CIFAR10):
	din = (3, 32, 32)
	dout = 10

	def _get_dataroot(cls, *args, ident=None, **kwargs):
		return super()._get_dataroot(*args, ident='cifar', **kwargs)


@register_dataset('cifar100')
class CIFAR100(Torchvision_Toy_Dataset, util.InitWall, torchvision.datasets.CIFAR100):
	din = (3, 32, 32)
	dout = 100

	def _get_dataroot(cls, *args, ident=None, **kwargs):
		return super()._get_dataroot(*args, ident='cifar', **kwargs)



import os

import h5py as hf

from omnibelt import unspecified_argument

import numpy as np
import torch
from torch.nn import functional as F
import torchvision

from ... import util
from ...data import register_dataset, Batchable, Deviced, Downloadable, ImageDataset


from .transforms import Interpolated





class Torchvision_Toy_Dataset(Batchable, Deviced, Downloadable, ImageDataset): # TODO: enable (pytorch) transforms
	available_modes = {'train', 'test'}

	_default_label_attr = 'targets'

	def __init__(self, A, dataroot=None, mode=None, labeled=None, label_attr=unspecified_argument,
	             din=None, dout=None,
	             dataset_kwargs=None, _req_kwargs=None, **unused):
		'''
		Requires dataset object to wrap it (since this is a `util.Simple_Child`).

		Images must be in `dataset.data` and labels in `dataset.[label_attr]` (if provided).

		:param dataset: compatible pytorch torchvision dataset object
		:param train: bool
		:param label_attr: attr name used to access labels in `dataset`
		:param din: optional (if it has to be overwritten)
		:param dout: optional (if it has to be overwritten)
		:param root: path to dataset files
		'''
		
		if dataroot is None:
			dataroot = self._get_dataroot(A)
			
		if mode is None:
			mode = A.pull('mode', 'train')
		
		if dataset_kwargs is None:
			dataset_kwargs = self._get_dataset_kwargs(A, mode=mode, **unused)
		if 'root' not in dataset_kwargs:
			dataset_kwargs['root'] = str(dataroot)

		resize = A.pull('resize', True)
		C, H, W = self.din
		if resize and (H != 32 or W != 32):
			self.din = C, 32, 32

		if label_attr is unspecified_argument:
			if labeled is None:
				labeled = A.pull('labeled', True)
			label_attr = A.pull('label_attr', self._default_label_attr) if labeled else None

		if label_attr is None:
			dout = self.din if din is None else din

		if _req_kwargs is None:
			_req_kwargs = dataset_kwargs
		else:
			_req_kwargs.update(dataset_kwargs)

		super().__init__(A, din=din, dout=dout, dataroot=dataroot,
		                 _req_kwargs=_req_kwargs,
		                 **unused)
		self.add_existing_modes('test')

		self.labeled = label_attr is not None

		# self.root = root

		images = self.data
		if isinstance(images, np.ndarray):
			images = torch.from_numpy(images)
		images = images.float().div(255)
		if images.ndimension() == 3:
			images = images.unsqueeze(1)
		
		if resize:
			images = F.interpolate(images, (32, 32), mode='bilinear')
		
		if label_attr is None:
			self.labels = None
		else:
			labels = getattr(self, label_attr)
			if not isinstance(labels, torch.Tensor):
				labels = torch.tensor(labels)
			self.register_buffer('labels', labels)
			# delattr(self, label_attr)

		self.register_buffer('images', images)
		# del self.data

	def download(cls, A, **kwargs):
		cls(A, download=True)

	def get_raw_data(self):
		if self.labeled:
			return self.images, self.labels
		return self.images

	def __len__(self):
		return len(self.images)

	def __getitem__(self, item):
		img = self.images[item]
		if self.labeled:
			return img, self.labels[item]
		return img
	
	@classmethod
	def _get_dataroot(cls, A=None, ident=None, silent=False):
		dataroot = util.get_data_dir(A, silent=silent)
		if ident is None:
			assert hasattr(cls, '_dataset_ident'), f'{cls.__name__} must be registered if no ident is provided'
			ident = cls._dataset_ident
		return dataroot / ident
	
	@classmethod
	def _get_dataset_kwargs(cls, A, mode=None, download=None, train=None, **unused):
		
		if mode is None:
			mode = A.pull('mode', 'train')
		
		if download is None:
			download = A.pull('download', False)
		
		if train is None:
			train = A.pull('train', mode != 'test')
		
		return {'download': download, 'train':train}



@register_dataset('mnist')
class MNIST(Torchvision_Toy_Dataset, util.InitWall, torchvision.datasets.MNIST):
	din = (1, 28, 28)
	dout = 10

@register_dataset('kmnist')
class KMNIST(Torchvision_Toy_Dataset, util.InitWall, torchvision.datasets.KMNIST):
	din = (1, 28, 28)
	dout = 10

@register_dataset('fmnist')
class FashionMNIST(Torchvision_Toy_Dataset, util.InitWall, torchvision.datasets.FashionMNIST):
	din = (1, 28, 28)
	dout = 10

@register_dataset('emnist')
class EMNIST(Torchvision_Toy_Dataset, util.InitWall, torchvision.datasets.EMNIST):
	din = (1, 28, 28)
	dout = 26
	
	_split_keys = {
		'byclass': '<KEY>',
		'bymerge': '<KEY>',
		'letters': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
		'digits': '0123456789',
		'mnist': '0123456789',
		'balanced': '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabdefghnqrt',
	}
	
	def __init__(self, A, **kwargs):
		
		# split = A.pull('group', 'letters')
		# if split != 'letters':
		# 	raise NotImplementedError
		
		# dataset = torchvision.datasets.EMNIST(str(dataroot), split=split, **kwargs)


		# labeled = A.pull('labeled', False)
		
		
		# labels_key = self._split_keys.get(split, None) if labeled else None
		# dout = len(labels_key) if labeled else None
		
		# TODO: Automodifier for selected_classes
		# selected_classes = A.pull('selected_classes', None)
		# if selected_classes is not None:
		# 	sel = None
		# 	lbls = dataset.targets.clone()
		# 	full_key = labels_key
		# 	labels_key = []
		#
		# 	for i, c in enumerate(selected_classes):
		#
		# 		s = lbls == c
		# 		if sel is None:
		# 			sel = s
		# 		else:
		# 			sel += s
		# 		dataset.targets[s] = i
		# 		if full_key is not None:
		# 			labels_key.append(full_key[c])
		#
		# 	dataset.targets = dataset.targets[sel]
		# 	dataset.data = dataset.data[sel]
		#
		# 	if labeled:
		# 		dout = len(selected_classes)

		super().__init__(A, **kwargs)
		
		self.images = self.images.permute(0,1,3,2)
		
		if self.split == 'letters' and self.labels is not None:
			self.labels -= 1  # targets use 1-based indexing :(
		
		self.labels_key = self._split_keys[self.split]
	
	@classmethod
	def _get_dataset_kwargs(cls, A, split=None, **other):
		kwargs = super()._get_dataset_kwargs(A, **other)
		if split is None:
			split = A.pull('group', 'letters')
		kwargs['split'] = split
		return kwargs


@register_dataset('svhn')
class SVHN(Torchvision_Toy_Dataset, util.InitWall, torchvision.datasets.SVHN):
	din = (3, 32, 32)
	dout = 10
	
	_default_label_attr = 'labels'

	@classmethod
	def _get_dataset_kwargs(cls, A, **unused):
		kwargs = super()._get_dataset_kwargs(A, **unused)
		kwargs['split'] = 'train' if kwargs['train'] else 'test'
		del kwargs['train']
		return kwargs


@register_dataset('cifar10')
class CIFAR10(Torchvision_Toy_Dataset, util.InitWall, torchvision.datasets.CIFAR10):
	din = (3, 32, 32)
	dout = 10

	def _get_dataroot(cls, *args, ident=None, **kwargs):
		return super()._get_dataroot(*args, ident='cifar', **kwargs)


@register_dataset('cifar100')
class CIFAR100(Torchvision_Toy_Dataset, util.InitWall, torchvision.datasets.CIFAR100):
	din = (3, 32, 32)
	dout = 100

	def _get_dataroot(cls, *args, ident=None, **kwargs):
		return super()._get_dataroot(*args, ident='cifar', **kwargs)


# TODO: enable (pytorch) transforms Requires dataset object to wrap it (since this is a `util.Simple_Child`).  		Images must be in `dataset.data` and labels in `dataset.[label_attr]` (if provided).  		:param dataset: compatible pytorch torchvision dataset object 		:param train: bool 		:param label_attr: attr name used to access labels in `dataset` 		:param din: optional (if it has to be overwritten) 		:param dout: optional (if it has to be overwritten) 		:param root: path to dataset files # self.root = root # delattr(self, label_attr) # del self.data # split = A.pull('group', 'letters') # if split != 'letters': # 	raise NotImplementedError # dataset = torchvision.datasets.EMNIST(str(dataroot), split=split, **kwargs) # labeled = A.pull('labeled', False) # labels_key = self._split_keys.get(split, None) if labeled else None # dout = len(labels_key) if labeled else None # TODO: Automodifier for selected_classes # selected_classes = A.pull('selected_classes', None) # if selected_classes is not None: # 	sel = None # 	lbls = dataset.targets.clone() # 	full_key = labels_key # 	labels_key = [] # # 	for i, c in enumerate(selected_classes): # # 		s = lbls == c # 		if sel is None: # 			sel = s # 		else: # 			sel += s # 		dataset.targets[s] = i # 		if full_key is not None: # 			labels_key.append(full_key[c]) # # 	dataset.targets = dataset.targets[sel] # 	dataset.data = dataset.data[sel] # # 	if labeled: # 		dout = len(selected_classes) # targets use 1-based indexing :(