"""Handle all interaction with ``.ini`` format configuration options.
"""
from genomic_config import base_config as bc
import configparser
# import pprint as pp


# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
class IniConfig(bc._BaseConfig):
    """Handles interaction with the genomic configuration options in ``.ini``
    format (reading only).

    Parameters
    ----------
    config_file : `str` or `NoneType`, optional, default: `NoneType`
        A configuration file location. If this is provided, it is simple
        checked for existence and returned. If it has not been provided
        then the default locations are checked and returned.

    Raises
    ------
    FileNotFoundError
        If a location for the default config file is not found
    PermissionError
        If a location for the default config file can't be read

    See also
    --------
    get_config_file
    genomic_config.base_config._BaseConfig

    Examples
    --------
    open a config file with a context manager:

    >>> from genomic_config import ini_config
    >>> with ini_config.IniConfig("my_config.ini") as cfg: # doctest: +SKIP
    >>>     # do stuff with the configuration
    >>>     pass

    open not using the context manager:

    >>> cfg = ini_config.IniConfig("my_config.ini") # doctest: +SKIP
    >>> cfg.open()
    >>> # do stuff with the configuration
    >>> cfg.close()
    """
    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @classmethod
    def split_heading(cls, section):
        """Split a section heading into it's component parts and test that they
        are defined.

        Parameters
        ----------
        section : `str`
            The section to split. The delimiter should be a ``.`` and should be
            of the format section_name.<something_else> where something else
            might be a specific dataset name or a species name (depending on
            the section)

        Returns
        -------
        split_section : `list` of `str`
            The split section. All the strings in the returned list are
            guaranteed to have a length.

        Raises
        ------
        ValueError
            If after splitting some of the component parts have length == 0.
        """
        return section.split(bc.DETAIL_DELIMITER)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @classmethod
    def open_ini(cls, file_path):
        """Helper method for opening an ``.ini`` config file using
        `configparser` with the appropriate settings for use with
        `genomic_config.ini_config.IniConfig`.

        Parameters
        ----------
        file_path : `str`
            The path to the `.ini` file.

        Returns
        -------
        parser : `configparser.ConfigParser`
            The parser object to interact with

        Notes
        -----
        This can be used to pre-open a configuration file, if you want to do
        something with it before passing it to the
        `genomic_config.ini_config.IniConfig` class. This opens the
        configuration file to only accept ``=`` as the key/value delimiter
        ``delimiters=('=',)``, it allows key only fields
        ``allow_no_value=True`` and the keys are case sensitive
        ``parser.optionxform = str``.
        """
        parser = configparser.ConfigParser(
            allow_no_value=True, delimiters=('=',)
        )

        # This ensures that the keys are case-sensitive
        parser.optionxform = str
        parser.read(file_path)
        return parser

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def open(self):
        """Parse the content of a ``.ini`` formatted genomic-config file.

        This relies on `configparser` package now in Python 3

        Returns
        -------
        has_been_parsed : `bool`
             There is a possibility to open via the context manager or not,
             this is just an indicator that the config has been parsed. So the
             first call will produce ``True``, subsequent calls will produce
             ``False``. In most instances this can be ignored by the user and
             is mainly for internal use or use if sub-classing.

        Notes
        -----
        You only need to use ``open()`` if you are not using the context
        manager. The ``.ini`` files supported by `IniConfig` should use ``=``
        as the key-value separators and not ``:``. The keys are also case
        sensitive. These differ from the standard ``.ini`` format.

        Examples
        --------

        Open not using the context manager:

        >>> cfg = ini_config.IniConfig("my_config.ini") # doctest: +SKIP
        >>> cfg.open()
        >>> # do stuff with the configuration
        >>> cfg.close()
        """
        # The super class will return True or False depending on if the config
        # is already open (False - i.e. has not been opened) or True - has
        # been opened, if False, we do not re-parse it
        if super().open() is False:
            return False

        # TODO: Check that we do not already have a config parser object
        if isinstance(self._config_file, configparser.ConfigParser):
            self._parser = self._config_file
        else:
            # Get a configparser instance to interact with the config file.
            # Note the changes in behaviour from the default configfile
            self._parser = self.__class__.open_ini(self._config_file)

        self._parse_general()
        self._parse_species_synonyms()
        self._parse_assembly_synonyms()
        self._parse_chr_name_synonyms()
        self._parse_chr_name_sort()
        self._parse_populations()
        self._parse_samples()
        self._parse_chain_files()
        self._parse_reference_genome()
        self._parse_mapping_file()
        self._parse_cohorts()
        return True

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @property
    def seed(self):
        """The ``seed`` defined in the config file (`int` or `NoneType`)
        """
        try:
            return self._cnf[bc.GENERAL_SECTION][bc.SEED_KEY]
        except KeyError:
            return None

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    @property
    def version(self):
        """The ``version`` defined in the config file (`str` or `NoneType`)
        """
        try:
            return self._cnf[bc.GENERAL_SECTION][bc.VERSION_KEY]
        except KeyError:
            return None

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def find_section(self, prefix):
        """Attempt to locate a section with a defined ``prefix``. Where prefix
        is one of the defined sections headings (but this is not enforced).

        Parameters
        ----------
        prefix : `str`
            The prefix of the sections that we want to find.

        Returns
        -------
        section_names : `list` of `str`
            Matching section names. This will be empty if none are found.

        Notes
        -----
        Section names with no prefix will not be found.

        See also
        --------
        genomic_config.base_config.GENERAL_SECTION
        genomic_config.base_config.CHAIN_FILE_PREFIX
        genomic_config.base_config.ASSEMBLY_PREFIX
        genomic_config.base_config.ASSEMBLY_SYN_SECTION
        genomic_config.base_config.CHR_NAME_SYN_PREFIX
        genomic_config.base_config.SPECIES_SYN_SECTION
        genomic_config.base_config.SAMPLE_PREFIX
        genomic_config.base_config.COHORT_PREFIX
        """
        prefix = "{0}.".format(prefix)

        section_matches = []
        for i in self._parser.sections():
            if i.startswith(prefix):
                section_matches.append(i)
        return section_matches

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _parse_general(self):
        """Parse the ``general`` options section in the config file.

        There are currently a single option available in this: ``seed`. If the
        seed is not provided then ``NoneType`` is used (no seed).
        """
        try:
            # Do we have a general section to parse?
            section = self._parser[bc.GENERAL_SECTION]
        except KeyError:
            # No general section
            return

        try:
            # Has a seed been specified?
            self._cnf[bc.GENERAL_SECTION][bc.SEED_KEY] = \
                int(section[bc.SEED_KEY])
        except KeyError:
            self._cnf[bc.GENERAL_SECTION][bc.SEED_KEY] = None
        except Exception as e:
            # Other errors such as the user passing a string
            self.__class__._raise_parse_error(e)

        try:
            # Has a seed been specified?
            self._cnf[bc.GENERAL_SECTION][bc.VERSION_KEY] = \
                section[bc.VERSION_KEY]
        except KeyError:
            self._cnf[bc.GENERAL_SECTION][bc.VERSION_KEY] = None
        except Exception as e:
            # Other errors such as the user passing a string
            self.__class__._raise_parse_error(e)

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _parse_species_synonyms(self):
        """Parse the ``species_synonyms`` section of the a genomic-config file

        Raises
        ------
        ValueError
            If any of the species synonyms are empty

        Notes
        -----
        The ``species_synonyms`` section is optional, if it is not present
        then no errors are raised. Also, this introduces "self" mappings so
        speciesA = speciesA, to it is always translated.
        """
        try:
            # Do we have an assembly synonyms section to parse?
            section = self._parser[bc.SPECIES_SYN_SECTION]
        except KeyError:
            # No section
            return

        # We will also hold all the values (i.e. species mappings), this is
        # so we can introduce self mappings at the end to exsure we have
        # complete coverage
        unique_values = set()

        # If we get here then there is a section to parse and the synonyms are
        # just treated like key value pairs
        for k, v in section.items():
            # Make sure a synonym has actually been defined
            if v is None or v == '':
                raise ValueError("no assembly synonym for '{0}'")

            unique_values.add(v)
            self._species_synonyms[k] = v

        # Introduce the self mappings
        for i in unique_values:
            self._species_synonyms[i] = i

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _parse_assembly_synonyms(self):
        """Parse the ``assembly_synonyms`` section of the a
        genomic-config file.

        Notes
        -----
        The ``assembly_synonyms`` section is optional, if it is not present
        then no errors are raised. ``assembly_synonyms`` sections are species
        specific and should be suffixed with a species name i.e.
        ``assembly_synonyms.human`` and if that species name is a target for a
        species synonym then you will always be able to find it.

        Raises
        ------
        ValueError
            If any of the assembly synonym definitions are empty.
        """
        # Id all the assembly_synonyms sections and process in turn
        for i in self.find_section(bc.ASSEMBLY_SYN_PREFIX):
            try:
                section, species = self.__class__.split_heading(i)
            except ValueError as e:
                # Too many/not enough values to unpack...
                raise ValueError(
                    "problem with assembly_synonym section: {0}".format(i)
                ) from e

            # Make sure the species section exists for the species defined
            # in the section heading
            self.set_species_section(
                species, use_species_synonyms=self.use_species_synonyms
            )

            # Now get the section info
            section = self._parser[i]

            # If we get here then there is a section to parse and the synonyms
            # are just treated like key value pairs
            for k, v in section.items():
                # Make sure a synonym has actually been defined
                if v is None or v == '':
                    raise ValueError("no assembly synonym: '{0}'")

                self._species[species][bc.ASSEMBLY_SYN_PREFIX][k] = v

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _parse_chr_name_synonyms(self):
        """Parse the ``chr_name_synonyms`` section of the a
        genomic-config file.

        Notes
        -----
        The ``chr_name_synonyms`` section is optional, if it is not present
        then no errors are raised. ``chr_name_synonyms`` sections are species
        and genome assembly specific and should be suffixed with a species name
        and an assembly nameIt should also have a unique name
        i.e. ``chr_name_sort.human.b38.DEFAULT``, so different synonyms sets
        can be specified for the same species/assembly.

        Raises
        ------
        ValueError
            If any of the chromosome name synonym definitions are empty.

        Notes
        -----
        The chromosome names are treated as strings.
        """
        # Id all the assembly_synonyms sections and process in turn
        for i in self.find_section(bc.CHR_NAME_SYN_PREFIX):
            try:
                section, species, assembly, name = \
                    self.__class__.split_heading(i)
            except ValueError as e:
                # Too many/not enough values to unpack...
                raise ValueError(
                    "problem with chromosome synonym section: {0}".format(i)
                ) from e

            # Make sure the species section exists for the species defined
            # in the section heading
            self.set_species_section(
                species, use_species_synonyms=self.use_species_synonyms
            )

            self.set_assembly_section(
                species, assembly,
                use_species_synonyms=self.use_species_synonyms,
                use_assembly_synonyms=self.use_assembly_synonyms
            )

            # Now get the section info
            section = self._parser[i]

            assembly_section = self._check_assembly(species, assembly)

            # Make sure the name parameter is set
            assembly_section[bc.CHR_NAME_SYN_PREFIX].setdefault(
                name, dict()
            )

            for k, v in section.items():
                if v is None:
                    raise ValueError(
                        "chromosome synonym options should have a name"
                    )

                try:
                    # It should not exist if it does it means it has been
                    # defined twice, perhaps under different synonyms
                    assembly_section[bc.CHR_NAME_SYN_PREFIX][name][k]
                    self._raise_option_defined(section, k)
                except KeyError:
                    # Does not already exist so all good to add
                    assembly_section[bc.CHR_NAME_SYN_PREFIX][name][k] = v

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _parse_chr_name_sort(self):
        """Parse the ``chr_name_sort`` section of a genomic-config file.

        Notes
        -----
        The ``chr_name_sort`` section is optional, if it is not present
        then no errors are raised. ``chr_name_sort`` sections are species
        and genome assembly specific and should be suffixed with a species name
        and an assembly name. It should also have a unique name
        i.e. ``chr_name_sort.human.b38.DEFAULT``, so different sorts can be
        specified for the same species/assembly (i.e. perhaps for different
        chromosome representations.

        Raises
        ------
        ValueError
            If any of the chromosome name sort definitions are empty.

        Notes
        -----
        The chromosome names are treated as strings.
        """
        # Id all the assembly_synonyms sections and process in turn
        for i in self.find_section(bc.CHR_NAME_SORT_PREFIX):
            try:
                section, species, assembly, name = \
                    self.__class__.split_heading(i)
            except ValueError as e:
                # Too many/not enough values to unpack...
                raise ValueError(
                    "problem with chromosome synonym section: {0}".format(i)
                ) from e

            # Make sure the species section exists for the species defined
            # in the section heading
            self.set_species_section(
                species, use_species_synonyms=self.use_species_synonyms
            )

            self.set_assembly_section(
                species, assembly,
                use_species_synonyms=self.use_species_synonyms,
                use_assembly_synonyms=self.use_assembly_synonyms
            )

            # Now get the section info
            section = self._parser[i]

            assembly_section = self._check_assembly(species, assembly)

            # The name should not be in the same species,assembly,chr_name_sort
            # if it is it means it has been defined twice
            if name in assembly_section[bc.CHR_NAME_SORT_PREFIX]:
                self._raise_option_defined(bc.CHR_NAME_SORT_PREFIX, name)

            # Make sure the name parameter is set
            assembly_section[bc.CHR_NAME_SORT_PREFIX].setdefault(
                name, []
            )

            for k, v in section.items():
                if v is not None:
                    raise ValueError(
                        "chromosome sort options should only have a single value"
                    )
                assembly_section[bc.CHR_NAME_SORT_PREFIX][name].append(k)


    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _parse_samples(self):
        """Parse all the samples sections within the configuration file

        Raises
        ------
        ValueError
            If any of the sample section headers are not sample.<NAME> format
            or if any of the sample Ids have values associated with them.
        """
        # Id all the assembly_synonyms sections and process in turn
        for i in self.find_section(bc.SAMPLE_PREFIX):
            try:
                section, name = self.__class__.split_heading(i)
            except ValueError as e:
                # Too many/not enough values to unpack...
                raise ValueError(
                    "problem with sample section: {0}".format(i)
                ) from e

            self._cnf[bc.SAMPLE_PREFIX].setdefault(name, [])
            idx = 0
            for k, v in self._parser[i].items():
                # Samples should not have any value associated with them
                if v is None:
                    self._cnf[bc.SAMPLE_PREFIX][name].append(k)
                else:
                    raise ValueError(
                        "sample IDs should not have any value associated with"
                        " them: ({2}:{3}) {0}={1}".format(k, v, i, idx)
                    )
                idx += 1

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _parse_populations(self):
        """Parse all the population mapping sections within the configuration
        file.

        Raises
        ------
        ValueError
            If any of the sample section headers are not population.<NAME>
            format or if any of the sample Ids have values associated with
            them.
        """
        # Id all the assembly_synonyms sections and process in turn
        for i in self.find_section(bc.POPULATION_PREFIX):
            try:
                section, name = self.__class__.split_heading(i)
            except ValueError as e:
                # Too many/not enough values to unpack...
                raise ValueError(
                    "problem with population section: {0}".format(i)
                ) from e

            self._cnf[bc.POPULATION_PREFIX].setdefault(name, [])
            idx = 0
            for k, v in self._parser[i].items():
                # Samples should not have any value associated with them
                if v is None:
                    self._cnf[bc.POPULATION_PREFIX][name].append(k)
                else:
                    raise ValueError(
                        "population mappings should not have any value associated with"
                        " them: ({2}:{3}) {0}={1}".format(k, v, i, idx)
                    )
                idx += 1

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _parse_chain_files(self):
        """Parse the chain_files section(s) in a config file

        The chain files section are all have the structure:
        ``chain_files.<SOURCE GENOME ASSEMBLY>``. Within each section there are
        key value pairs with the structure:
        ``<TARGET GENOME ASSEMBLY>: <ABSOLUTE CHAIN FILE PATH>``. The chain
        files are check to make sure they exist

        Raises
        ------
        FileNotFoundError
            If a location for the default config file is not found
        PermissionsError
            If a location for the default config file can not be read
        """
        for i in self.find_section(bc.CHAIN_FILE_PREFIX):
            try:
                section, species, source_assembly = \
                    self.__class__.split_heading(i)
            except ValueError as e:
                # Too many/not enough values to unpack...
                raise ValueError(
                    "problem with chain_file section: {0}".format(i)
                ) from e

            # Make sure the species section exists for the species defined
            # in the section heading
            self.set_species_section(
                species, use_species_synonyms=self.use_species_synonyms
            )

            self.set_assembly_section(
                species, source_assembly,
                use_species_synonyms=self.use_species_synonyms,
                use_assembly_synonyms=self.use_assembly_synonyms
            )

            # Now get the section info
            section = self._parser[i]
            assembly_section = self._check_assembly(species, source_assembly)

            for target_assembly, chain_file in section.items():
                self.validate_conifg_file(chain_file, file_type="chain file")

                if self.use_assembly_synonyms is True:
                    target_assembly = self.get_assembly_synonym(
                        species, target_assembly
                    )

                try:
                    # It should not exist if it does it means it has been
                    # defined twice, perhaps under different synonyms
                    assembly_section[bc.CHAIN_FILE_PREFIX][target_assembly]
                    self._raise_option_defined(section, target_assembly)
                except KeyError:
                    # Does not already exist so all good to add
                    assembly_section[bc.CHAIN_FILE_PREFIX][target_assembly] = \
                        chain_file

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _parse_reference_genome(self):
        """Parse the reference genome sequence section in a config file.

        The reference genome section can take the structure:
        ``reference_genome.<SPECIES>.<GENOME ASSEMBLY>``. These should have
        a single option which is the path to the reference genome sequence file
        . The fasta file should be indexed and the presence of an index is
        checked if ``validate_indexes=True``.

        Raises
        ------
        FileNotFoundError
            If a location for the default config file is not found
        PermissionsError
            If a location for the default config file can't be read
        """
        for i in self.find_section(bc.REFERENCE_GENOME_PREFIX):
            try:
                section, species, assembly = \
                    self.__class__.split_heading(i)
            except ValueError as e:
                # Too many/not enough values to unpack...
                raise ValueError(
                    "problem with reference genome section: {0}".format(i)
                ) from e

            # Make sure the species section exists for the species defined
            # in the section heading
            self.set_species_section(
                species, use_species_synonyms=self.use_species_synonyms
            )

            self.set_assembly_section(
                species, assembly,
                use_species_synonyms=self.use_species_synonyms,
                use_assembly_synonyms=self.use_assembly_synonyms
            )

            # Now get the section info
            section = self._parser[i]

            assembly_section = self._check_assembly(species, assembly)

            for k, v in section.items():
                if v is None:
                    raise ValueError(
                        "reference genome options should have a name"
                    )
                self.validate_conifg_file(v)
                self.validate_file_index(
                    v, bc.REFERENCE_GENOME_INDEXES,
                    file_type="reference genome file"
                )

                try:
                    # It should not exist if it does it means it has been
                    # defined twice, perhaps under different synonyms
                    assembly_section[bc.REFERENCE_GENOME_PREFIX][k]
                    self._raise_option_defined(section, k)
                except KeyError:
                    # Does not already exist so all good to add
                    assembly_section[bc.REFERENCE_GENOME_PREFIX][k] = v

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _parse_mapping_file(self):
        """Parse the mapping file section in a config file.

        The mapping file section can take the structure:
        ``mapping_file.<SPECIES>.<GENOME ASSEMBLY>``. These should contain
        key value pairs with the name for a mapping file as the key and the
        mapping file path as a value. The mapping file should be indexed and
        the presence of an index is checked if ``validate_indexes=True``.

        Raises
        ------
        FileNotFoundError
            If a location for the default config file is not found
        PermissionsError
            If a location for the default config file can't be read
        """
        for i in self.find_section(bc.MAPPING_FILE_PREFIX):
            try:
                section, species, assembly = \
                    self.__class__.split_heading(i)
            except ValueError as e:
                # Too many/not enough values to unpack...
                raise ValueError(
                    "problem with mapping_file section: {0}".format(i)
                ) from e

            # Make sure the species section exists for the species defined
            # in the section heading
            self.set_species_section(
                species, use_species_synonyms=self.use_species_synonyms
            )

            self.set_assembly_section(
                species, assembly,
                use_species_synonyms=self.use_species_synonyms,
                use_assembly_synonyms=self.use_assembly_synonyms
            )

            # Now get the section info
            section = self._parser[i]
            assembly_section = self._check_assembly(species, assembly)

            for name, mapping_file in section.items():
                self.validate_conifg_file(mapping_file,
                                          file_type="mapping file")
                self.validate_file_index(
                    mapping_file, bc.VCF_INDEXES,
                    all_indexes=False,
                    file_type="mapping file"
                )

                try:
                    # It should not exist if it does it means it has been
                    # defined twice, perhaps under different synonyms
                    assembly_section[bc.MAPPING_FILE_PREFIX][name]
                    self._raise_option_defined(section, name)
                except KeyError:
                    # Does not already exist so all good to add
                    assembly_section[bc.MAPPING_FILE_PREFIX][name] = \
                        mapping_file

    # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    def _parse_cohorts(self):
        """Parse all the cohort sections within the configuration file
        """
        for i in self.find_section(bc.COHORT_PREFIX):
            try:
                section, species, assembly, file_format, cohort_name = \
                    self.__class__.split_heading(i)
            except ValueError as e:
                # Too many/not enough values to unpack...
                raise ValueError(
                    "problem with cohort section: {0}".format(i)
                ) from e

            # Make sure the file format is standardised
            file_format = file_format.lower()

            try:
                ext, all_index, indexes = bc.KNOWN_FILES[file_format]
            except KeyError:
                indexes = []
                all_index = False

            # Make sure the species section exists for the species defined
            # in the section heading
            self.set_species_section(
                species, use_species_synonyms=self.use_species_synonyms
            )

            self.set_assembly_section(
                species, assembly,
                use_species_synonyms=self.use_species_synonyms,
                use_assembly_synonyms=self.use_assembly_synonyms
            )

            # TODO: Check that the cohot has not all ready been defined?
            species, assembly, cohort_section = self.set_cohort_section(
                species, assembly, cohort_name, file_format,
                use_species_synonyms=self.use_species_synonyms,
                use_assembly_synonyms=self.use_assembly_synonyms
            )

            # Now get the section info
            section = self._parser[i]

            if cohort_section[file_format] is not None:
                raise KeyError(
                    "section already defined (check for other synonyms?):"
                    " {0}".format(i)
                )

            file_hints = []
            for k, v in section.items():
                # TODO: what happens if there is no hinting returned?
                for hint, is_remote, file_path in self.__class__.get_hinting(
                        k, v
                ):
                    self.validate_conifg_file(file_path)
                    self.validate_file_index(
                        file_path, indexes, all_indexes=all_index,
                        file_type="{0} cohort file".format(file_format)
                    )
                    file_hints.append((hint, file_path))

            self.add_cohort_files(
                species, assembly, file_format, cohort_name, file_hints
            )
