Add a :class:`Sample` to this :class:`SampleSheet`.

        All samples are validated against the first sample added to the sample
        sheet to ensure there are no ID collisions or incompatible read
        structures (if supplied). All samples are also validated against the
        ``"[Reads]"`` section of the sample sheet if it has been defined.

        The following validation is performed when adding a sample:

            - :class:`Read_Structure` is identical in all samples, if supplied
            - :class:`Read_Structure` is compatible with ``"[Reads]"``, if \
                supplied
            - Samples on the same ``"Lane"`` cannot have the same \
                ``"Sample_ID"`` and ``"Library_ID"``.
            - Samples cannot have the same ``"Sample_ID"`` if no ``"Lane"`` \
                has been defined.
            - The same ``"index"`` or ``"index2"`` combination cannot exist \
                per flowcell or per lane if lanes have been defined.
            - All samples have the same index design (``"index"``, \
                ``"index2"``) per flowcell or per lane if lanes have been \
                defined.

        Args:
            sample: :class:`Sample` to add to this :class:`SampleSheet`.

        Note:

            It is unclear if the Illumina specification truly allows for
            equivalent samples to exist on the same sample sheet. To mitigate
            the warnings in this library when you encounter such a case, use
            a code pattern like the following:

            >>> import warnings
            >>> warnings.simplefilter("ignore")
            >>> from sample_sheet import SampleSheet
            >>> SampleSheet('tests/resources/single-end-colliding-sample-ids.csv');
            SampleSheet('tests/resources/single-end-colliding-sample-ids.csv')

def add_sample(self, sample: Sample) -> None:
        """Add a :class:`Sample` to this :class:`SampleSheet`.

        All samples are validated against the first sample added to the sample
        sheet to ensure there are no ID collisions or incompatible read
        structures (if supplied). All samples are also validated against the
        ``"[Reads]"`` section of the sample sheet if it has been defined.

        The following validation is performed when adding a sample:

            - :class:`Read_Structure` is identical in all samples, if supplied
            - :class:`Read_Structure` is compatible with ``"[Reads]"``, if \
                supplied
            - Samples on the same ``"Lane"`` cannot have the same \
                ``"Sample_ID"`` and ``"Library_ID"``.
            - Samples cannot have the same ``"Sample_ID"`` if no ``"Lane"`` \
                has been defined.
            - The same ``"index"`` or ``"index2"`` combination cannot exist \
                per flowcell or per lane if lanes have been defined.
            - All samples have the same index design (``"index"``, \
                ``"index2"``) per flowcell or per lane if lanes have been \
                defined.

        Args:
            sample: :class:`Sample` to add to this :class:`SampleSheet`.

        Note:

            It is unclear if the Illumina specification truly allows for
            equivalent samples to exist on the same sample sheet. To mitigate
            the warnings in this library when you encounter such a case, use
            a code pattern like the following:

            >>> import warnings
            >>> warnings.simplefilter("ignore")
            >>> from sample_sheet import SampleSheet
            >>> SampleSheet('tests/resources/single-end-colliding-sample-ids.csv');
            SampleSheet('tests/resources/single-end-colliding-sample-ids.csv')

        """
        # Do not allow samples without Sample_ID defined.
        if sample.Sample_ID is None:
            raise ValueError('Sample must have "Sample_ID" defined.')

        # Set whether the samples will have ``index`` or ``index2``.
        if len(self.samples) == 0:
            self.samples_have_index = sample.index is not None
            self.samples_have_index2 = sample.index2 is not None

        if (
            len(self.samples) == 0
            and sample.Read_Structure is not None
            and self.Read_Structure is None
        ):
            # If this is the first sample added to the sample sheet then
            # assume the ``SampleSheet.Read_Structure`` inherits the
            # ``sample.Read_Structure`` only if ``SampleSheet.Read_Structure``
            # has not already been defined. If ``SampleSheet.reads`` has been
            # defined then validate the new read_structure against it.
            if (
                self.is_paired_end
                and not sample.Read_Structure.is_paired_end
                or self.is_single_end  # noqa
                and not sample.Read_Structure.is_single_end
            ):
                raise ValueError(
                    f'Sample sheet pairing has been set with '
                    f'Reads:"{self.Reads}" and is not compatible with sample '
                    f'read structure: {sample.Read_Structure}'
                )

            # Make a copy of this samples read_structure for the sample sheet.
            self.Read_Structure = sample.Read_Structure.copy()

        # Validate this sample against the ``SampleSheet.Read_Structure``
        # attribute, which can be None, to ensure they are the same.
        if self.Read_Structure != sample.Read_Structure:
            raise ValueError(
                f'Sample read structure ({sample.Read_Structure}) different '
                f'than read structure in samplesheet ({self.Read_Structure}).'
            )

        # Compare this sample against all those already defined to ensure none
        # have equal ``Sample_ID``, ``Library_ID``, and ``Lane`` attributes.
        # Ensure that all samples have attributes ``index``, ``index2``, or
        # both if they have been defined.
        for other in self.samples:
            if sample == other:
                message = (
                    f'Two equivalent samples added:'
                    f'\n\n1): {sample.__repr__()}\n2): {other.__repr__()}\n'
                )
                # TODO: Look into if this is truly illegal or not.
                warnings.warn(UserWarning(message))
            if sample.index is None and self.samples_have_index:
                raise ValueError(
                    f'Cannot add a sample without attribute `index` if a '
                    f'previous sample has `index` set: {sample})'
                )
            if sample.index2 is None and self.samples_have_index2:
                raise ValueError(
                    f'Cannot add a sample without attribute `index2` if a '
                    f'previous sample has `index2` set: {sample})'
                )

            # Prevent index collisions when samples are dual-indexed
            if (
                self.samples_have_index
                and self.samples_have_index2
                and sample.index == other.index
                and sample.index2 == other.index2
                and sample.Lane == other.Lane
            ):
                raise ValueError(
                    f'Sample index combination for {sample} has already been '
                    f'added on this lane or flowcell: {other}'
                )

            # Prevent index collisions when samples are single-indexed (index)
            if (
                self.samples_have_index
                and not self.samples_have_index2
                and sample.index == other.index
                and sample.Lane == other.Lane
            ):
                raise ValueError(
                    f'First sample index for {sample} has already been '
                    f'added on this lane or flowcell: {other}'
                )

            # Prevent index collisions when samples are single-indexed (index2)
            if (
                not self.samples_have_index
                and self.samples_have_index2
                and sample.index2 == other.index2
                and sample.Lane == other.Lane
            ):
                raise ValueError(
                    f'Second sample index for {sample} has already been '
                    f'added on this lane or flowcell: {other}'
                )

        sample.sample_sheet = self
        self._samples.append(sample)