class MultipleAdapters(Matchable):
    """
    Represent multiple adapters at once
    """

    def __init__(self, adapters: Sequence[Matchable]):
        super().__init__(name="multiple_adapters")
        self._adapters = adapters

    def enable_debug(self):
        for a in self._adapters:
            a.enable_debug()

    def __getitem__(self, item):
        return self._adapters[item]

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

    def match_to(self, sequence: str) -> Optional[SingleMatch]:
        """
        Find the adapter that best matches the sequence.

        Return either a Match instance or None if there are no matches.
        """
        best_match = None
        for adapter in self._adapters:
            match = adapter.match_to(sequence)
            if match is None:
                continue

            # the no. of matches determines which adapter fits best
            if (
                best_match is None
                or match.matches > best_match.matches
                or (
                    match.matches == best_match.matches
                    and match.errors < best_match.errors
                )
            ):
                best_match = match
        return best_match class SingleAdapter(Adapter, ABC):
    """
    This class can find a single adapter characterized by sequence, error rate,
    type etc. within reads.

    where --  A Where enum value. This influences where the adapter is allowed to appear within the
        read.

    sequence -- The adapter sequence as string. Will be converted to uppercase.
        Also, Us will be converted to Ts.

    max_errors -- Maximum allowed errors (non-negative float). If the values is less than 1, this is
        interpreted as a rate directly and passed to the aligner. If it is 1 or greater, the value
        is converted to a rate by dividing it by the number of non-N characters in the sequence.

        The error rate is the number of errors in the alignment divided by the length
        of the part of the alignment that matches the adapter.

    minimum_overlap -- Minimum length of the part of the alignment
        that matches the adapter.

    read_wildcards -- Whether IUPAC wildcards in the read are allowed.

    adapter_wildcards -- Whether IUPAC wildcards in the adapter are
        allowed.

    name -- optional name of the adapter. If not provided, the name is set to a
        unique number.
    """

    allows_partial_matches: bool = True

    def __init__(
        self,
        sequence: str,
        max_errors: float = 0.1,
        min_overlap: int = 3,
        read_wildcards: bool = False,
        adapter_wildcards: bool = True,
        name: Optional[str] = None,
        indels: bool = True,
    ):
        self.name: str = _generate_adapter_name() if name is None else name
        super().__init__(self.name)
        self._debug: bool = False
        self.sequence: str = sequence.upper().replace("U", "T")
        if not self.sequence:
            raise ValueError("Adapter sequence is empty")
        if max_errors >= 1 and self.sequence.count("N") != len(self.sequence):
            max_errors /= len(self.sequence) - self.sequence.count("N")
        self.max_error_rate: float = max_errors
        self.min_overlap: int = min(min_overlap, len(self.sequence))
        iupac = frozenset("ABCDGHKMNRSTUVWXY")
        if adapter_wildcards and not set(self.sequence) <= iupac:
            for c in self.sequence:
                if c not in iupac:
                    if c == "I":
                        extra = (
                            "For inosine, consider using N instead and please comment "
                            "on <https://github.com/marcelm/cutadapt/issues/546>."
                        )
                    else:
                        extra = "Use only characters 'ABCDGHKMNRSTUVWXY'."
                    raise InvalidCharacter(
                        f"Character '{c}' in adapter sequence '{self.sequence}' is "
                        f"not a valid IUPAC code. {extra}"
                    )
        # Optimization: Use non-wildcard matching if only ACGT is used
        self.adapter_wildcards: bool = adapter_wildcards and not set(
            self.sequence
        ) <= set("ACGT")
        self.read_wildcards: bool = read_wildcards
        self.indels: bool = indels
        self.aligner = self._aligner()

    def _make_aligner(self, flags: int) -> Aligner:
        # TODO
        # Indels are suppressed by setting their cost very high, but a different algorithm
        # should be used instead.
        indel_cost = 1 if self.indels else 100000
        return Aligner(
            self.sequence,
            self.max_error_rate,
            flags=flags,
            wildcard_ref=self.adapter_wildcards,
            wildcard_query=self.read_wildcards,
            indel_cost=indel_cost,
            min_overlap=self.min_overlap,
        )

    def __repr__(self):
        return (
            "<{cls}(name={name!r}, sequence={sequence!r}, "
            "max_error_rate={max_error_rate}, min_overlap={min_overlap}, "
            "read_wildcards={read_wildcards}, "
            "adapter_wildcards={adapter_wildcards}, "
            "indels={indels})>".format(cls=self.__class__.__name__, **vars(self))
        )

    @property
    def effective_length(self) -> int:
        return self.aligner.effective_length

    def enable_debug(self) -> None:
        """
        Print out the dynamic programming matrix after matching a read to an
        adapter.
        """
        self._debug = True
        self.aligner.enable_debug()

    @abstractmethod
    def _aligner(self):
        pass

    @abstractmethod
    def match_to(self, sequence: str):
        """
        Attempt to match this adapter to the given string.

        Return a Match instance if a match was found;
        return None if no match was found given the matching criteria (minimum
        overlap length, maximum error rate).
        """

    def __len__(self) -> int:
        return len(self.sequence) class IndexedPrefixAdapters(IndexedAdapters):
    @classmethod
    def _accept(cls, adapter):
        if not isinstance(adapter, PrefixAdapter):
            raise ValueError("Only 5' anchored adapters are allowed")
        return super()._accept(adapter)

    def _make_match(self, adapter, length, matches, errors, sequence):
        return RemoveBeforeMatch(
            astart=0,
            astop=len(adapter.sequence),
            rstart=0,
            rstop=length,
            matches=matches,
            errors=errors,
            adapter=adapter,
            sequence=sequence,
        )

    def _get_make_affix(self):
        return self._make_prefix

    @staticmethod
    def _make_prefix(s, n):
        return s[:n] class IndexedSuffixAdapters(IndexedAdapters):
    @classmethod
    def _accept(cls, adapter):
        if not isinstance(adapter, SuffixAdapter):
            raise ValueError("Only anchored 3' adapters are allowed")
        return super()._accept(adapter)

    def _make_match(self, adapter, length, matches, errors, sequence):
        return RemoveAfterMatch(
            astart=0,
            astop=len(adapter.sequence),
            rstart=len(sequence) - length,
            rstop=len(sequence),
            matches=matches,
            errors=errors,
            adapter=adapter,
            sequence=sequence,
        )

    def _get_make_affix(self):
        return self._make_suffix

    @staticmethod
    def _make_suffix(s, n):
        return s[-n:] class Match(ABC):

    adapter: "Adapter"

    @abstractmethod
    def remainder_interval(self) -> Tuple[int, int]:
        pass

    @abstractmethod
    def retained_adapter_interval(self) -> Tuple[int, int]:
        pass

    @abstractmethod
    def get_info_records(self, read) -> List[List]:
        pass

    @abstractmethod
    def trimmed(self, read):
        pass

    @abstractmethod
    def match_sequence(self):
        pass def remainder(matches: Sequence[Match]) -> Tuple[int, int]:
    """
    Determine which section of the read would not be trimmed. Return a tuple (start, stop)
    that gives the interval of the untrimmed part relative to the original read.

    matches must be non-empty
    """
    if not matches:
        raise ValueError("matches must not be empty")
    start = 0
    for match in matches:
        match_start, match_stop = match.remainder_interval()
        start += match_start
    length = match_stop - match_start
    return (start, start + length) class Adapter(Matchable, ABC):

    description = "adapter with one component"  # this is overriden in subclasses

    @abstractmethod
    def spec(self) -> str:
        """Return string representation of this adapter"""

    @abstractmethod
    def create_statistics(self) -> AdapterStatistics:
        pass

    @abstractmethod
    def descriptive_identifier(self) -> str:
        pass def tokenize_braces(s: str) -> Iterator[Token]:
    """
    >>> list(tokenize_braces(""))
    []
    >>> list(tokenize_braces("before {braced} after"))
    [StringToken("before "), BraceToken("braced"), StringToken(" after")]
    >>> list(tokenize_braces("ab{cd}{ef}"))
    [StringToken("ab"), BraceToken("cd"), BraceToken("ef")]
    """
    for value in re.split("({[^}]*})", s):
        if value == "":
            continue
        if value.startswith("{") and value.endswith("}"):
            value = value[1:-1]
            token_class: Type[Token] = BraceToken
        else:
            token_class = StringToken
        if "{" in value:
            raise TokenizeError("Unexpected '{' encountered")
        if "}" in value:
            raise TokenizeError("Unexpected '}' encountered")
        yield token_class(value) class TokenizeError(Exception):
    pass class Token:
    value: str

    def __repr__(self) -> str:
        return f'{self.__class__.__name__}("{self.value}")' class BraceToken(Token):
    pass def reverse_complemented_sequence(sequence: dnaio.Sequence):
    if sequence.qualities is None:
        qualities = None
    else:
        qualities = sequence.qualities[::-1]
    return dnaio.Sequence(
        sequence.name, reverse_complement(sequence.sequence), qualities
    )
import re
import logging
from types import SimpleNamespace
from typing import Sequence, List, Tuple, Optional, Set
from abc import ABC, abstractmethod
from dnaio import record_names_match, Sequence as SequenceRecord
from .qualtrim import quality_trim_index, nextseq_trim_index
from .adapters import (
    MultipleAdapters,
    SingleAdapter,
    IndexedPrefixAdapters,
    IndexedSuffixAdapters,
    Match,
    remainder,
    Adapter,
)
from .tokenizer import tokenize_braces, TokenizeError, Token, BraceToken
from .utils import reverse_complemented_sequence

class PrefixSuffixAdder(SingleEndModifier):
    """
    Add a suffix and a prefix to read names
    """

    def __init__(self, prefix, suffix):
        self.prefix = prefix
        self.suffix = suffix

    def __repr__(self):
        return f"PrefixSuffixAdder(prefix='{self.prefix}', suffix='{self.suffix}')"

    def __call__(self, read, info):
        read = read[:]
        adapter_name = info.matches[-1].adapter.name if info.matches else "no_adapter"
        read.name = (
            self.prefix.replace("{name}", adapter_name)
            + read.name
            + self.suffix.replace("{name}", adapter_name)
        )
        return read


class InvalidTemplate(Exception):
    pass


class Renamer(SingleEndModifier):
    """
    Rename reads using a template

    The template string can contain the following placeholders:

    - {header} -- full, unchanged header
    - {id} -- the part of the header before the first whitespace
    - {comment} -- the part of the header after the ID, excluding initial whitespace
    - {cut_prefix} -- prefix removed by UnconditionalCutter (with positive length argument)
    - {cut_suffix} -- suffix removed by UnconditionalCutter (with negative length argument)
    - {adapter_name} -- name of the *last* adapter match or no_adapter if there was none
    - {match_sequence} -- the sequence that matched the adapter (this includes possible errors)
          or an empty string if there was no match
    - {rc} -- the string 'rc' if the read was reverse complemented (with --revcomp) or '' otherwise
    """

    variables = {
        "header",
        "id",
        "comment",
        "cut_prefix",
        "cut_suffix",
        "adapter_name",
        "rc",
        "match_sequence",
    }

    def __init__(self, template: str):
        try:
            self._tokens = list(tokenize_braces(template))
        except TokenizeError as e:
            raise InvalidTemplate(f"Error in template '{template}': {e}")
        self.raise_if_invalid_variable(self._tokens, self.variables)
        self._template = template

    def __repr__(self):
        return f"{self.__class__.__name__}('{self._template}')"

    @staticmethod
    def raise_if_invalid_variable(tokens: List[Token], allowed: Set[str]) -> None:
        for token in tokens:
            if not isinstance(token, BraceToken):
                continue
            value = token.value
            if value not in allowed:
                raise InvalidTemplate(
                    f"Error in template: Variable '{value}' not recognized"
                )

    @staticmethod
    def parse_name(read_name: str) -> Tuple[str, str]:
        """Parse read header and return (id, comment) tuple"""
        fields = read_name.split(maxsplit=1)
        if len(fields) == 2:
            return (fields[0], fields[1])
        else:
            return (read_name, "")

    def __call__(self, read: SequenceRecord, info: ModificationInfo) -> SequenceRecord:
        id_, comment = self.parse_name(read.name)
        read.name = self._template.format(
            header=read.name,
            id=id_,
            comment=comment,
            cut_prefix=info.cut_prefix if info.cut_prefix else "",
            cut_suffix=info.cut_suffix if info.cut_suffix else "",
            adapter_name=info.matches[-1].adapter.name
            if info.matches
            else "no_adapter",
            rc="rc" if info.is_rc else "",
            match_sequence=info.matches[-1].match_sequence() if info.matches else "",
        )
        return read


class PairedEndRenamer(PairedEndModifier):
    """
    Rename paired-end reads using a template. The template is applied to both
    R1 and R2, and the same template variables as in the (single-end) renamer
    are allowed. However,
    these variables are evaluated separately for each read. For example, if `{comment}`
    is used, it gets replaced with the R1 comment in the R1 header, and with the R2
    comment in the R2 header.

    Additionally, all template variables except `id` can be used in the read-specific
    forms `{r1.variablename}` and `{r2.variablename}`. For example, `{r1.comment}`
    always gets replaced with the R1 comment, even in R2.
    """

    def __init__(self, template: str):
        try:

            self._tokens = list(tokenize_braces(template))