import os
import itertools
import sys

from typing import List, TypeVar, Iterable, Tuple, Union
from enum import Enum, unique
import numpy as np
from skimage import color
from ..backend.common import DataSpec, PrintException
from .image_common import gray_scale, image2array, mask2array, image_backend_adaptive, reverse_image_backend_adaptive, \
    unnormalize, array2image, ExpectDataType, GetImageMode
from .label_common import label_backend_adaptive
from .samplers import *
from collections import namedtuple
from ..backend.load_backend import get_backend

if get_backend() == 'pytorch':
    from ..backend.pytorch_backend import to_numpy, to_tensor, ReplayBuffer
    import torch

__all__ = ['Dataset', 'ImageDataset', 'MaskDataset', 'LabelDataset', 'BboxDataset', 'SsdBboxDataset', 'Iterator',
           'NumpyDataset', 'RandomNoiseDataset']

T = TypeVar('T', int, float, str, np.ndarray)


class Dataset(List):
    def __init__(self, symbol=None, expect_data_type=None, name=''):
        super().__init__()
        self.parameter = None
        self.name = name
        self.symbol = symbol
        self.is_pair_process = False
        self.expect_data_type = expect_data_type

    def __add__(self, other):
        if other is not None and hasattr(other, '__iter__'):
            for item in other:
                if isinstance(item, (int, float, str, np.ndarray)):
                    super().append(item)

    def __len__(self):
        return super().__len__()


class ImageDataset(Dataset):
    def __init__(self, images=None, expect_data_type: ExpectDataType = ExpectDataType.rgb,
                 get_image_mode: GetImageMode = GetImageMode.processed, symbol=None, name=''):
        super().__init__(symbol=symbol, expect_data_type=expect_data_type, name=name)
        self.__add__(images)
        self.dtype = np.float32
        self.get_image_mode = get_image_mode
        self.image_transform_funcs = []
        self.is_pair_process = False

    def __getitem__(self, index: int):
        img = super().__getitem__(index)  # self.pop(index)
        if isinstance(img, str) and self.get_image_mode == GetImageMode.path:
            self.parameter = DataSpec(self.name, self.symbol, "ImagePath", None)
            return img
        elif self.get_image_mode == GetImageMode.path:
            self.parameter = DataSpec(self.name, self.symbol, None, None)
            return None

        if isinstance(img, str):
            img = image2array(img)

        if self.get_image_mode == GetImageMode.raw:
            self.parameter = DataSpec(self.name, self.symbol, "ImagePath", None)
            return img
        if not isinstance(img, np.ndarray):
            raise ValueError('image data should be ndarray')
        elif isinstance(img, np.ndarray) and img.ndim not in [2, 3]:
            raise ValueError('image data dimension  should be 2 or 3, but get {0}'.format(img.ndim))
        elif self.expect_data_type == ExpectDataType.gray:
            img = color.rgb2gray(img).astype(self.dtype)
        elif self.expect_data_type == ExpectDataType.rgb and img.ndim == 2:
            img = np.repeat(np.expand_dims(img, -1), 3, -1).astype(self.dtype)
        elif self.expect_data_type == ExpectDataType.rgb and img.ndim == 3:
            img = img[:, :, :3].astype(self.dtype)
        elif self.expect_data_type == ExpectDataType.rgba:
            if img.ndim == 2:
                img = np.repeat(np.expand_dims(img, -1), 3, -1)
            if img.shape[2] == 3:
                img = np.concatenate([img, np.ones((img.shape[0], img.shape[1], 1)) * 255], axis=-1)
            img = img.astype(self.dtype)
        elif self.expect_data_type == ExpectDataType.multi_channel:
            img = img.astype(self.dtype)

        if self.get_image_mode == GetImageMode.expect and self.is_pair_process == False:
            return image_backend_adaptive(img)
        elif self.get_image_mode == GetImageMode.processed and self.is_pair_process == False:
            return self.image_transform(img)
        elif self.is_pair_process == True:
            return img

        return None

    def image_transform(self, img_data):
        if len(self.image_transform_funcs) == 0:
            return image_backend_adaptive(img_data)
        if isinstance(img_data, np.ndarray):
            # if img_data.ndim>=2:
            for fc in self.image_transform_funcs:
                img_data = fc(img_data)
            img_data = image_backend_adaptive(img_data)

            return img_data
        else:
            return img_data

    @property
    def reverse_image_transform_funcs(self):
        return_list = []
        return_list.append(reverse_image_backend_adaptive)
        for i in range(len(self.image_transform_funcs)):
            fn = self.image_transform_funcs[-1 - i]
            if fn.__qualname__ == 'normalize.<locals>.img_op':
                return_list.append(unnormalize(fn.mean, fn.std))
        return_list.append(array2image)
        return return_list

    def reverse_image_transform(self, img_data):
        if len(self.reverse_image_transform_funcs) == 0:
            return reverse_image_backend_adaptive(img_data)
        if isinstance(img_data, np.ndarray):
            # if img_data.ndim>=2:
            for fc in self.reverse_image_transform_funcs:
                img_data = fc(img_data)
            img_data = reverse_image_backend_adaptive(img_data)

            return img_data
        else:
            return img_data


class MaskDataset(Dataset):
    def __init__(self, images=None, expect_data_type: ExpectDataType = ExpectDataType.label_mask,
                 get_image_mode: GetImageMode = GetImageMode.processed, symbol=None, name=''):
        super().__init__(symbol=symbol, expect_data_type=expect_data_type, name=name)
        if expect_data_type not in [ExpectDataType.label_mask, ExpectDataType.binary_mask]:
            raise ValueError('Only mask is valid expect image type. ')

        self.__add__(images)
        self.get_image_mode = get_image_mode
        self.image_transform_funcs = []
        self.is_pair_process = False

    def __getitem__(self, index: int):
        img = super().__getitem__(index)  # self.pop(index)
        if isinstance(img, str) and self.get_image_mode == GetImageMode.path:
            return img
        elif self.get_image_mode == GetImageMode.path:
            return None

        if isinstance(img, str):
            if self.expect_data_type == ExpectDataType.binary_mask:
                img = mask2array(img)
                img[img > 0] = 255
            else:
                img = image2array(img).astype(np.uint8)

        if self.get_image_mode == GetImageMode.raw:
            return img
        if not isinstance(img, np.ndarray):
            raise ValueError('image data should be ndarray')
        elif isinstance(img, np.ndarray) and img.ndim not in [2, 3]:
            raise ValueError('image data dimension  should be 2 or 3, but get {0}'.format(img.ndim))

        if self.get_image_mode == GetImageMode.expect and self.is_pair_process == False:
            return label_backend_adaptive(img)
        elif self.get_image_mode == GetImageMode.processed and self.is_pair_process == False:
            return self.image_transform(img)
        elif self.is_pair_process == True:
            return img

        return None

    def image_transform(self, img_data):
        if len(self.image_transform_funcs) == 0:
            return label_backend_adaptive(img_data, expect_data_type=self.expect_data_type)
        if isinstance(img_data, np.ndarray):
            # if img_data.ndim>=2:
            for fc in self.image_transform_funcs:
                if fc.__qualname__ == 'resize.<locals>.img_op' or fc.__qualname__ == 'rescale.<locals>.img_op':
                    fc, _, k, n = fc

                img_data = fc(img_data)
            img_data = label_backend_adaptive(img_data, expect_data_type=self.expect_data_type)

            return img_data
        else:
            return img_data

    @property
    def reverse_image_transform_funcs(self):
        return_list = []
        return_list.append(reverse_image_backend_adaptive)
        for i in range(len(self.image_transform_funcs)):
            fn = self.image_transform_funcs[-1 - i]
            if fn.__qualname__ == 'normalize.<locals>.img_op':
                return_list.append(unnormalize(fn.mean, fn.std))
        return_list.append(array2image)
        return return_list

    def reverse_image_transform(self, img_data):
        if len(self.reverse_image_transform_funcs) == 0:
            return reverse_image_backend_adaptive(img_data)
        if isinstance(img_data, np.ndarray):
            # if img_data.ndim>=2:
            for fc in self.reverse_image_transform_funcs:
                img_data = fc(img_data)
            img_data = reverse_image_backend_adaptive(img_data)

            return img_data
        else:
            return img_data


class LabelDataset(Dataset):
    def __init__(self, labels=None, expect_data_type=ExpectDataType.classification_label, class_names=None, symbol=None,
                 name=''):
        super().__init__(symbol=symbol, expect_data_type=expect_data_type, name=name)
        self.__add__(labels)
        self.dtype = np.int64
        self.class_names = {}
        self._lab2idx = {}
        self._idx2lab = {}
        if class_names is not None:
            self.class_names = class_names

        self.label_transform_funcs = []

    def binding_class_names(self, class_names=None, language=None):
        if class_names is not None and hasattr(class_names, '__len__'):
            if language is None:
                language = 'en-us'
            self.class_names[language] = list(class_names)
            self.__default_language__ = language
            self._lab2idx = {v: k for k, v in enumerate(self.class_names[language])}
            self._idx2lab = {k: v for k, v in enumerate(self.class_names[language])}

    def __getitem__(self, index: int):
        label = super().__getitem__(index)
        return self.label_transform(label)

    def label_transform(self, label_data):
        label_data = label_backend_adaptive(label_data, self.class_names)
        if isinstance(label_data, list) and all(isinstance(elem, np.ndarray) for elem in label_data):
            label_data = np.asarray(label_data)
        if isinstance(label_data, np.ndarray):
            # if img_data.ndim>=2:
            for fc in self.label_transform_funcs:
                label_data = fc(label_data)
            return label_data
        else:
            return label_data


class BboxDataset(Dataset):
    def __init__(self, boxes=None, image_size=None, expect_data_type=ExpectDataType.absolute_bbox, class_names=None,
                 symbol=None, name=''):
        super().__init__(symbol=symbol, expect_data_type=expect_data_type, name=name)
        self.__add__(boxes)
        self.dtype = np.int64
        self.image_size = image_size
        self.class_names = {}
        self._lab2idx = {}
        self._idx2lab = {}
        if class_names is not None:
            self.class_names = class_names

    def binding_class_names(self, class_names=None, language=None):
        if class_names is not None and hasattr(class_names, '__len__'):
            if language is None:
                language = 'en-us'
            self.class_names[language] = list(class_names)
            self.__default_language__ = language
            self._lab2idx = {v: k for k, v in enumerate(self.class_names[language])}
            self._idx2lab = {k: v for k, v in enumerate(self.class_names[language])}
            self._current_idx = -1

    def __getitem__(self, index: int):
        self._current_idx = index
        bboxes = super().__getitem__(index).astype(np.float32)
        if self.expect_data_type == ExpectDataType.relative_bbox and (self.image_size is None):
            raise RuntimeError('You need provide image size information for calculate relative_bbox. ')
        elif self.expect_data_type == ExpectDataType.relative_bbox:
            height, width = self.image_size
            bboxes[:, 0] = bboxes[:, 0] / width
            bboxes[:, 2] = bboxes[:, 2] / width
            bboxes[:, 1] = bboxes[:, 1] / height
            bboxes[:, 3] = bboxes[:, 3] / height
            return np.array(bboxes).astype(np.float32)

        elif self.expect_data_type == ExpectDataType.absolute_bbox:
            return np.array(bboxes).astype(np.float32)

    def bbox_transform(self, *bbox):

        return bbox


class SsdBboxDataset(BboxDataset):
    def __init__(self, boxes=None, image_size=None, priors=None, center_variance=0.1, size_variance=0.2,
                 expect_data_type=ExpectDataType.absolute_bbox, class_names=None, symbol=None, name=''):
        super().__init__(boxes=boxes, image_size=image_size, expect_data_type=expect_data_type, class_names=class_names,
                         symbol=symbol, name=name)
        self.priors = priors
        self.center_variance = center_variance
        self.size_variance = size_variance
        self.label_transform_funcs = []
        self.bbox_post_transform_funcs = []

    def area_of(self, left_top, right_bottom):
        """Compute the areas of rectangles given two corners.

        Args:
            left_top (N, 2): left top corner.
            right_bottom (N, 2): right bottom corner.

        Returns:
            area (N): return the area.
        """
        hw = np.clip(right_bottom - left_top, 0.0, None)
        return hw[..., 0] * hw[..., 1]

    def iou_of(self, boxes0, boxes1, eps=1e-5):
        """Return intersection-over-union (Jaccard index) of boxes.

        Args:
            boxes0 (N, 4): ground truth boxes.
            boxes1 (N or 1, 4): predicted boxes.
            eps: a small number to avoid 0 as denominator.
        Returns:
            iou (N): IoU values.
        """
        overlap_left_top = np.maximum(boxes0[..., :2], boxes1[..., :2])
        overlap_right_bottom = np.minimum(boxes0[..., 2:], boxes1[..., 2:])

        overlap_area = self.area_of(overlap_left_top, overlap_right_bottom)
        area0 = self.area_of(boxes0[..., :2], boxes0[..., 2:])
        area1 = self.area_of(boxes1[..., :2], boxes1[..., 2:])
        return overlap_area / (area0 + area1 - overlap_area + eps)


    def convert_boxes_to_locations(self, center_form_boxes, center_form_priors):
        if center_form_boxes.ndim==1:
            center_form_boxes=np.expand_dims(center_form_boxes, 0)
        if center_form_priors.ndim==1:
            center_form_priors=np.expand_dims(center_form_priors, 0)
        return np.concatenate([(center_form_boxes[..., :2] - center_form_priors[..., :2]) / center_form_priors[...,
                                                                                            2:] / self.center_variance,
                               np.log(center_form_boxes[..., 2:] / center_form_priors[..., 2:]) / self.size_variance],
            axis=len(center_form_boxes.shape) - 1)

    def center_form_to_corner_form(self, locations):
        return np.concatenate(
            [locations[..., :2] - locations[..., 2:] / 2, locations[..., :2] + locations[..., 2:] / 2],
            len(locations.shape) - 1)

    def corner_form_to_center_form(self, boxes):
        return np.concatenate([(boxes[..., :2] + boxes[..., 2:]) / 2, boxes[..., 2:] - boxes[..., :2]],
                              len(boxes.shape) - 1)

    def assign_priors(self, gt_boxes, gt_labels, center_form_priors, iou_threshold):
        # """Assign ground truth boxes and targets to priors.
        #
        # Args:
        #     gt_boxes (num_targets, 4): ground truth boxes.
        #     gt_labels (num_targets): labels of targets.
        #     priors (num_priors, 4): corner form priors
        # Returns:
        #     boxes (num_priors, 4): real values for priors.
        #     labels (num_priros): labels for priors.
        # """
        # # size: num_priors x num_targets
        # # if gt_boxes is not None :
        #
        # ious = self.iou_of(gt_boxes.unsqueeze(0), corner_form_priors.unsqueeze(1))
        # # size: num_priors
        # best_target_per_prior, best_target_per_prior_index = ious.max(1)
        # # size: num_targets
        # best_prior_per_target, best_prior_per_target_index = ious.max(0)
        #
        # for target_index, prior_index in enumerate(best_prior_per_target_index):
        #     best_target_per_prior_index[prior_index] = target_index
        # # 2.0 is used to make sure every target has a prior assigned
        # best_target_per_prior.index_fill_(0, best_prior_per_target_index, 2)
        # # size: num_priors
        # labels = gt_labels[best_target_per_prior_index]
        # labels[best_target_per_prior < iou_threshold] = 0  # the backgournd id
        # boxes = gt_boxes[best_target_per_prior_index]
        # return boxes, labels
        corner_form_priors = self.center_form_to_corner_form(center_form_priors)
        center_form_boxs = self.corner_form_to_center_form(gt_boxes)

        boxes = np.zeros((corner_form_priors.shape[0], 4)).astype(np.float32)
        labels = np.zeros((corner_form_priors.shape[0])).astype(np.int64)

        #
        # _pred[:, 2] = _pred[:, 2] + _pred[:, 0]
        # _pred[:, 3] = _pred[:, 3] + _pred[:, 1]
        # _gt[:, 2] = _gt[:, 2] + _gt[:, 0]
        # _gt[:, 3] = _gt[:, 3] + _gt[:, 1]
        overlaps = self.iou_of(np.expand_dims(corner_form_priors, 0), np.expand_dims(gt_boxes, 1))
        # size: num_priors
        best_target_to_prior, best_target_to_prior_index =np.max(overlaps,axis=0), np.argmax(overlaps,axis=0)
        # size: num_targets
        best_prior_to_target, best_prior_to_target_index =np.max(overlaps,axis=1),np.argmax(overlaps,axis=1)
        center_priors=center_form_priors[best_prior_to_target_index]

        for target_index, prior_index in enumerate(best_prior_to_target_index):
            gt_overlap=overlaps[target_index]
            overlap_match=np.where(gt_overlap>(best_prior_to_target[target_index]*0.75))[0]
            labels[overlap_match] = gt_labels[target_index]
            loc= self.convert_boxes_to_locations(self.corner_form_to_center_form(gt_boxes[target_index]), center_form_priors[overlap_match])
            boxes[overlap_match] =loc

        for target_index, prior_index in enumerate(best_prior_to_target_index):
            best_target_to_prior_index[prior_index] = target_index
            labels[prior_index]=gt_labels[target_index]
            loc=self.convert_boxes_to_locations(self.corner_form_to_center_form(gt_boxes[target_index]), center_form_priors[prior_index])
            boxes[prior_index] =loc
        return boxes, labels

    def binding_class_names(self, class_names=None, language=None):
        if class_names is not None and hasattr(class_names, '__len__'):
            if language is None:
                language = 'en-us'
            self.class_names[language] = list(class_names)
            self.__default_language__ = language
            self._lab2idx = {v: k for k, v in enumerate(self.class_names[language])}
            self._idx2lab = {k: v for k, v in enumerate(self.class_names[language])}

    def bbox_transform(self, bbox):
        if bbox is None or len(bbox) == 0:
            return np.zeros((self.priors.shape[0], 4)).astype(np.float32), np.zeros((self.priors.shape[0])).astype(
                np.int64)
        else:
            height, width = self.image_size
            bbox[:, 0] = bbox[:, 0] / width
            bbox[:, 2] = bbox[:, 2] / width
            bbox[:, 1] = bbox[:, 1] / height
            bbox[:, 3] = bbox[:, 3] / height
            if bbox is None or len(bbox) == 0:
                boxes, labels = self.assign_priors(None, None, center_form_to_corner_form(to_numpy(self.priors)), 0.3)
                return to_numpy(boxes), to_numpy(labels)
            elif bbox.shape[-1] == 5:
                gt_box = bbox[:, :4]
                gt_label = bbox[:, 4]
                boxes, labels = self.assign_priors(gt_box, gt_label, to_numpy(self.priors), 0.3)
                return boxes.astype(np.float32), labels.astype(np.int64)
            else:
                return bbox


class NumpyDataset(Dataset):
    def __init__(self, data=None, expect_data_type=ExpectDataType.array_data, symbol=None, name=''):
        super().__init__(symbol=symbol, expect_data_type=expect_data_type, name=name)

        self.__add__(data)

        self.dtype = np.float32

    def __getitem__(self, index: int):
        data = super().__getitem__(index)
        return data


class RandomNoiseDataset(Dataset):
    def __init__(self, shape, expect_data_type=ExpectDataType.random_noise, random_mode='normal', symbol=None, name=''):
        super().__init__(symbol=symbol, expect_data_type=expect_data_type, name=name)

        self.dtype = np.float32
        self.shape = shape
        self.random_mode = random_mode

    def __getitem__(self, index: int):
        if self.random_mode == 'normal':
            return np.random.standard_normal(self.shape)
        elif self.random_mode == 'uniform':
            return np.random.uniform(-1, 1, self.shape)

    def __len__(self):
        return sys.maxsize


class Iterator(object):
    def __init__(self, data=None, label=None, mask=None, unpair=None, minibatch_size=8):
        self.is_pair_process = False
        self.signature = None
        self._data = NumpyDataset()
        self._label = LabelDataset()
        self._unpair = NumpyDataset()
        if data is not None and isinstance(data, Dataset):
            self._data = data
        if label is not None and isinstance(label, Dataset):
            self._label = label
            if isinstance(self._label, (MaskDataset, ImageDataset, BboxDataset)) and isinstance(self._data, ImageDataset) and len(self._label) == len(self._data):
                self._label.is_pair_process = self._data.is_pair_process = self.is_pair_process = True
            else:
                self._label.is_pair_process = self._data.is_pair_process = self.is_pair_process = False

        if unpair is not None and isinstance(unpair, Dataset):
            self._unpair = unpair

        self._minibatch_size = minibatch_size
        self.paired_transform_funcs = []
        self.batch_sampler = BatchSampler(self, self._minibatch_size, is_shuffle=True, drop_last=False)
        self._sample_iter = iter(self.batch_sampler)

    @property
    def data(self):
        return self._data

    @data.setter
    def data(self, value):
        self._data = value
        if self._label is not None and isinstance(self._label, (MaskDataset, ImageDataset)) and isinstance(self._data,
                                                                                                           ImageDataset) and len(
                self._label) == len(self._data):
            self._label.is_pair_process = self._data.is_pair_process = self.is_pair_process = True
        else:
            self._label.is_pair_process = self._data.is_pair_process = self.is_pair_process = False

        self.batch_sampler = BatchSampler(self, self._minibatch_size, is_shuffle=True, drop_last=False)
        self._sample_iter = iter(self.batch_sampler)

    @property
    def label(self):
        return self._label

    @label.setter
    def label(self, value):
        self._label = value
        if isinstance(self._label, (MaskDataset, ImageDataset,BboxDataset)) and isinstance(self._data, ImageDataset) and len(self._label) == len(self._data):
            self._label.is_pair_process = self._data.is_pair_process = self.is_pair_process = True
        else:
            self._label.is_pair_process = self._data.is_pair_process = self.is_pair_process = False

        self.batch_sampler = BatchSampler(self, self._minibatch_size, is_shuffle=True, drop_last=False)
        self._sample_iter = iter(self.batch_sampler)

    @property
    def unpair(self):
        return self._unpair

    @unpair.setter
    def unpair(self, value):
        self._unpair = value
        self.batch_sampler = BatchSampler(self, self._minibatch_size, is_shuffle=True, drop_last=False)
        self._sample_iter = iter(self.batch_sampler)

    @property
    def minibatch_size(self):
        return self._minibatch_size

    @minibatch_size.setter
    def minibatch_size(self, value):
        self._minibatch_size = value
        self.batch_sampler = BatchSampler(self, self._minibatch_size, is_shuffle=True, drop_last=False)
        self._sample_iter = iter(self.batch_sampler)

    def update_signature(self, arg_names):
        if self.signature is None:
            self.next()
            self.signature = namedtuple('signature', arg_names)
        elif not isinstance(arg_names, (list, tuple)):
            raise ValueError('arg_names should be list or tuple')
        elif len(self.signature._fields) != len(arg_names):
            raise ValueError('data deed and arg_names should be the same length')
        else:
            self.signature = namedtuple('signature', arg_names)

    def paired_transform(self, img_data, paired_img):

        if isinstance(img_data, list) and all(isinstance(elem, np.ndarray) for elem in img_data):
            img_data = np.asarray(img_data)
        if isinstance(img_data, str) and os.path.isfile(img_data) and os.path.exists(img_data):
            img_data = image2array(img_data)

        if len(self.paired_transform_funcs) == 0:
            return img_data, paired_img
        if isinstance(img_data, np.ndarray):
            # if img_data.ndim>=2:
            for fc in self.paired_transform_funcs:
                try:
                    img_data, paired_img = fc(img_data, paired_img)
                except:
                    PrintException()

            return img_data, paired_img
        else:
            return img_data, paired_img

    def __getitem__(self, index: int):
        try:
            bbox = None
            mask = None
            data = self.data.__getitem__(index % len(self.data)) if len(self.data) > 0 else None
            label = self.label.__getitem__(index % len(self.label)) if len(self.label) > 0 else None
            if isinstance(self.label, BboxDataset):
                data, label = self.paired_transform(data, label)
                if hasattr(self.data, 'image_transform'):
                    data = self.data.image_transform(data)
                if hasattr(self.label, 'bbox_transform'):
                    new_label = self.label.bbox_transform(label)
                    if isinstance(new_label, tuple):
                        bbox, label = new_label
                    else:
                        bbox = new_label

            elif isinstance(self.label, MaskDataset):
                if isinstance(label, tuple):
                    mask, label = label
                else:
                    mask = label
                    label = None
                data, mask = self.paired_transform(data, mask)


            if hasattr(self.label, 'label_transform') and not isinstance(self.label, BboxDataset):
                label = self.label.label_transform(label)

            unpair = self.unpair.__getitem__(index % len(self.unpair)) if len(self.unpair) > 0 else None
            return_data = []
            if self.signature is None:
                names = []
                names.append('data' if data is not None else '')
                names.append('bbox' if bbox is not None else '')
                names.append('mask' if mask is not None else '')
                names.append('label' if label is not None else '')
                names.append('unpair' if unpair is not None else '')
                self.signature = namedtuple('signature', [item for item in names if item != ''])

            if data is not None:
                return_data.append(data)
            if bbox is not None:
                return_data.append(bbox)
            if mask is not None:
                return_data.append(mask)
            if label is not None:
                return_data.append(label)
            if unpair is not None:
                return_data.append(unpair)
            return tuple(return_data)
        except:
            PrintException()

    def _next_index(self):
        return next(self._sample_iter)

    def __iter__(self):
        return self._sample_iter

    def next(self):
        return next(self._sample_iter)

    def __next__(self):
        return next(self._sample_iter)

    def __len__(self):
        return max([len(self.data) if self.data is not None else 0, len(self.unpair) if self.unpair is not None else 0])

