"""class to represent data section of config.ini file """
from pathlib import Path

import attr
from attr import validators, converters
from attr.validators import instance_of

from ..utils.general import projroot_path


def is_list_of_pos_int(instance, attribute, value):
    if type(value) != list:
        raise ValueError(
            f'type of {attribute.name} must be a list'
        )

    for ind, item in enumerate(value):
        if type(item) != int:
            raise ValueError(
                f'all elements in {attribute.name} must be int but item at index {ind} was: {type(item)}'
            )
        if item < 1:
            raise ValueError(
                f'all elements in {attribute.name} must be positive integer, but item at index {ind} was: {item}'
            )


def is_list_of_str(instance, attribute, value):
    if type(value) != list:
        raise ValueError(
            f'type of {attribute.name} must be a list'
        )

    for ind, item in enumerate(value):
        if type(item) != str:
            raise ValueError(
                f'all elements in {attribute.name} must be str but item at index {ind} was: {type(item)}'
            )


def is_valid_proportion(instance, attribute, value):
    if type(value) is float:
        if value < 0. or value > 1.:
            raise ValueError(
                f'if specified as a proportion, {attribute} must be between 0 and 1'
            )


@attr.s
class DataConfig:
    """class to represent [DATA] section of config.ini file

    Attributes
    ----------
    csv_file_in : str
        path to .csv file generated by searchstims package. Typically in root of train_dir.
    train_size : integer or float
        If integer, number of images to include in training set. If float, percent of dataset to use for training set.
    dataset_type : str
        one of {'searchstims', 'VSD'}. Specifies whether dataset is images generated by searchstims package, or
        images from Pascal-VOC dataset that were used to create the Visual Search Difficulty 1.0 dataset.
    root : str
        path to dataset root. Used with VOCDetection dataset to specify where VOC data was/should be downloaded
        to. (Note that download will take a while.)
    random_crop : bool
        if True, crop randomly
    crop_size : int
        Default is 224 (size of input to AlexNet).
    threshold : float
        between 0 and 1. Amount of target bounding box that must still be within the image
        after cropping for it to be included in annotation.
        If None, overlap is not calculated and all annotations are included.
        Default is 0.5.
    csv_file_out : str
        name of .csv file generated by searchnets.data.split.
    stim_types : list
        of strings; specifies which visual search stimulus types to use when creating dataset. Strings must be keys in
        .json file within train_dir. Default is None, in which case all types found in .csv file will be used.
    val_size : integer or float
        If integer, number of images to include in validation set. If float, percent of dataset to use in validation
        set. Validation set is used after each epoch to estimate accuracy of trained network.
        Default is None.
    test_size : integer or float
        If integer, number of images to include in test set. If float, percent of dataset to use in test set.
        Default is None.
    set_sizes : list
        Specifies the 'set size' of the visual search stimuli.  The set size is the total number of targets and
        distractors, AKA "items", in a visual search stimulus. Set sizes will already have been specified when
        creating the images with `searchstims`, but here the user can choose to use all or only some of the available
        set sizes. Images for each set size are saved in separate folders by `searchstims`, so this list will be used
        to find the paths to those folders within `TRAIN_DIR`.
    train_size_per_set_size, val_size_per_set_size, test_size_per_set_size : list
        number of samples in split per visual search set size. Default is None, in which case the total number of
        samples for each stimulus type will be divided by the number of set sizes, so that an equal number is used
        for each set size.
    """
    csv_file_in = attr.ib(converter=projroot_path, validator=instance_of(Path))
    train_size = attr.ib(validator=[instance_of((int, float)), is_valid_proportion])
    dataset_type = attr.ib(validator=instance_of(str), default='searchstims')
    num_classes = attr.ib(validator=validators.optional(instance_of(int)), default=2)
    root = attr.ib(converter=converters.optional(projroot_path),
                   validator=validators.optional(instance_of(Path)),
                   default=None)
    pad_size = attr.ib(validator=validators.optional(instance_of(int)), default=500)
    @dataset_type.validator
    def check_dataset_type(self, attribute, value):
        if value not in {'searchstims', 'VSD'}:
            raise ValueError(
                f"dataset_type must be one of {{'searchstims', 'VSD'}}, but was {value}."
            )

    csv_file_out = attr.ib(converter=converters.optional(projroot_path),
                           validator=validators.optional(instance_of(Path)),
                           default=None)
    stim_types = attr.ib(validator=validators.optional(is_list_of_str), default=None)
    val_size = attr.ib(validator=validators.optional([instance_of((int, float)), is_valid_proportion]), default=None)
    test_size = attr.ib(validator=validators.optional([instance_of((int, float)), is_valid_proportion]), default=None)
    set_sizes = attr.ib(validator=validators.optional(instance_of(list)), default=None)
    @set_sizes.validator
    def check_set_sizes(self, attribute, value):
        if value is not None:
            for ind, set_size in enumerate(value):
                if type(set_size) != int:
                    raise TypeError('all values in set_sizes should be int but '
                                    f'got type {type(set_size)} for element {ind}')

    train_size_per_set_size = attr.ib(validator=validators.optional(is_list_of_pos_int), default=None)
    val_size_per_set_size = attr.ib(validator=validators.optional(is_list_of_pos_int), default=None)
    test_size_per_set_size = attr.ib(validator=validators.optional(is_list_of_pos_int), default=None)

    def __attrs_post_init__(self):
        if any(
            [type(size) is float for size in [self.train_size, self.val_size, self.test_size]]
        ):
            if not all([type(size) is float or size is None
                        for size in [self.train_size, self.val_size, self.test_size]]):
                raise TypeError('if the size of any split of the dataset is specified as a proportion, then '
                                'they all must be specified as a proportion')
            size_sum = sum([size if size is not None else 0
                            for size in [self.train_size, self.val_size, self.test_size]])
            if size_sum > 1:
                raise ValueError(
                    "when specified as proportions, total of train_size, val_size, and test_size cannot be greater "
                    f"than 1, but was {size_sum}"
                )
        if self.csv_file_out is None:
            self.csv_file_out = self.csv_file_in + '_split.csv'
