# Path: src/cutadapt/filters.py
# class Predicate(ABC):
#     @abstractmethod
#     def test(self, read, info: ModificationInfo) -> bool:
#         """
#         Return True if the filtering criterion matches.
#         """
# 
#     @classmethod
#     def descriptive_identifier(cls) -> str:
#         """
#         Return a short name for this predicate based on the class name such as "too_long",
#         "too_many_expected_errors".
#         This is used as identifier in the JSON report.
#         """
#         return "".join(
#             ("_" + ch.lower() if ch.isupper() else ch) for ch in cls.__name__
#         )[1:]
# 
# Path: src/cutadapt/modifiers.py
# class ModificationInfo:
#     """
#     An object of this class is created for each read that passes through the pipeline.
#     Any information (except the read itself) that needs to be passed from one modifier
#     to one later in the pipeline or from one modifier to the filters is recorded here.
#     """
# 
#     __slots__ = ["matches", "original_read", "cut_prefix", "cut_suffix", "is_rc"]
# 
#     def __init__(self, read):
#         self.matches = []  # type: List[Match]
#         self.original_read = read
#         self.cut_prefix = None
#         self.cut_suffix = None
#         self.is_rc = None
# 
#     def __repr__(self):
#         return (
#             "ModificationInfo("
#             f"matches={self.matches!r}, "
#             f"original_read={self.original_read}, "
#             f"cut_prefix={self.cut_prefix}, "
#             f"cut_suffix={self.cut_suffix}, "
#             f"is_rc={self.is_rc})"
#         )
# 
# Path: src/cutadapt/statistics.py
# class ReadLengthStatistics:
#     """
#     Keep track of the lengths of written reads or read pairs
#     """
# 
#     def __init__(self) -> None:
#         # It would be more natural to use a Counter, but a
#         # defaultdict is much faster
#         self._written_lengths1: DefaultDict[int, int] = defaultdict(int)
#         self._written_lengths2: DefaultDict[int, int] = defaultdict(int)
# 
#     def update(self, read) -> None:
#         """Add a single-end read to the statistics"""
#         self._written_lengths1[len(read)] += 1
# 
#     def update2(self, read1, read2) -> None:
#         """Add a paired-end read to the statistics"""
#         self._written_lengths1[len(read1)] += 1
#         self._written_lengths2[len(read2)] += 1
# 
#     def written_reads(self) -> int:
#         """Return number of written reads or read pairs"""
#         return sum(self._written_lengths1.values())
# 
#     def written_bp(self) -> Tuple[int, int]:
#         return (
#             self._compute_total_bp(self._written_lengths1),
#             self._compute_total_bp(self._written_lengths2),
#         )
# 
#     def written_lengths(self) -> Tuple[Counter, Counter]:
#         return (Counter(self._written_lengths1), Counter(self._written_lengths2))
# 
#     @staticmethod
#     def _compute_total_bp(counts: DefaultDict[int, int]) -> int:
#         return sum(length * count for length, count in counts.items())
# 
#     def __iadd__(self, other):
#         written_lengths1, written_lengths2 = other.written_lengths()
#         for length, count in written_lengths1.items():
#             self._written_lengths1[length] += count
#         for length, count in written_lengths2.items():
#             self._written_lengths2[length] += count
#         return self
# 
# Path: src/cutadapt/utils.py
# 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
#     )


from abc import ABC, abstractmethod
from typing import Tuple, Dict, Optional, Any
from .filters import Predicate
from .modifiers import ModificationInfo
from .statistics import ReadLengthStatistics
from .utils import reverse_complemented_sequence
        self.predicate = predicate

    def __repr__(self):
        return f"SingleEndFilter(writer={self.writer}, predicate={self.predicate})"

    def descriptive_identifier(self) -> str:
        return self.predicate.descriptive_identifier()

    def __call__(self, read, info: ModificationInfo) -> bool:
        if self.predicate.test(read, info):
            self.filtered += 1
            if self.writer is not None:
                self.writer.write(read)
            return DISCARD
        return KEEP


class PairedEndFilter(PairedEndStep):
    """
    A pipeline step that can filter paired-end reads, redirect them to a file, and counts
    how many read pairs were filtered.

    Different filtering styles are supported, differing by which of the
    two reads in a pair have to fulfill the filtering criterion.
    """

    def __init__(
        self,
        writer,
        predicate1: Predicate,

# Path: src/cutadapt/filters.py
# class Predicate(ABC):
#     @abstractmethod
#     def test(self, read, info: ModificationInfo) -> bool:
#         """
#         Return True if the filtering criterion matches.
#         """
# 
#     @classmethod
#     def descriptive_identifier(cls) -> str:
#         """
#         Return a short name for this predicate based on the class name such as "too_long",
#         "too_many_expected_errors".
#         This is used as identifier in the JSON report.
#         """
#         return "".join(
#             ("_" + ch.lower() if ch.isupper() else ch) for ch in cls.__name__
#         )[1:]
# 
# Path: src/cutadapt/modifiers.py
# class ModificationInfo:
#     """
#     An object of this class is created for each read that passes through the pipeline.
#     Any information (except the read itself) that needs to be passed from one modifier
#     to one later in the pipeline or from one modifier to the filters is recorded here.
#     """
# 
#     __slots__ = ["matches", "original_read", "cut_prefix", "cut_suffix", "is_rc"]
# 
#     def __init__(self, read):
#         self.matches = []  # type: List[Match]
#         self.original_read = read
#         self.cut_prefix = None
#         self.cut_suffix = None
#         self.is_rc = None
# 
#     def __repr__(self):
#         return (
#             "ModificationInfo("
#             f"matches={self.matches!r}, "
#             f"original_read={self.original_read}, "
#             f"cut_prefix={self.cut_prefix}, "
#             f"cut_suffix={self.cut_suffix}, "
#             f"is_rc={self.is_rc})"
#         )
# 
# Path: src/cutadapt/statistics.py
# class ReadLengthStatistics:
#     """
#     Keep track of the lengths of written reads or read pairs
#     """
# 
#     def __init__(self) -> None:
#         # It would be more natural to use a Counter, but a
#         # defaultdict is much faster
#         self._written_lengths1: DefaultDict[int, int] = defaultdict(int)
#         self._written_lengths2: DefaultDict[int, int] = defaultdict(int)
# 
#     def update(self, read) -> None:
#         """Add a single-end read to the statistics"""
#         self._written_lengths1[len(read)] += 1
# 
#     def update2(self, read1, read2) -> None:
#         """Add a paired-end read to the statistics"""
#         self._written_lengths1[len(read1)] += 1
#         self._written_lengths2[len(read2)] += 1
# 
#     def written_reads(self) -> int:
#         """Return number of written reads or read pairs"""
#         return sum(self._written_lengths1.values())
# 
#     def written_bp(self) -> Tuple[int, int]:
#         return (
#             self._compute_total_bp(self._written_lengths1),
#             self._compute_total_bp(self._written_lengths2),
#         )
# 
#     def written_lengths(self) -> Tuple[Counter, Counter]:
#         return (Counter(self._written_lengths1), Counter(self._written_lengths2))
# 
#     @staticmethod
#     def _compute_total_bp(counts: DefaultDict[int, int]) -> int:
#         return sum(length * count for length, count in counts.items())
# 
#     def __iadd__(self, other):
#         written_lengths1, written_lengths2 = other.written_lengths()
#         for length, count in written_lengths1.items():
#             self._written_lengths1[length] += count
#         for length, count in written_lengths2.items():
#             self._written_lengths2[length] += count
#         return self
# 
# Path: src/cutadapt/utils.py
# 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
#     )


# Path: src/cutadapt/steps.py
from abc import ABC, abstractmethod
from typing import Tuple, Dict, Optional, Any
from .filters import Predicate
from .modifiers import ModificationInfo
from .statistics import ReadLengthStatistics
from .utils import reverse_complemented_sequence

        self.predicate = predicate

    def __repr__(self):
        return f"SingleEndFilter(writer={self.writer}, predicate={self.predicate})"

    def descriptive_identifier(self) -> str:
        return self.predicate.descriptive_identifier()

    def __call__(self, read, info: ModificationInfo) -> bool:
        if self.predicate.test(read, info):
            self.filtered += 1
            if self.writer is not None:
                self.writer.write(read)
            return DISCARD
        return KEEP


class PairedEndFilter(PairedEndStep):
    """
    A pipeline step that can filter paired-end reads, redirect them to a file, and counts
    how many read pairs were filtered.

    Different filtering styles are supported, differing by which of the
    two reads in a pair have to fulfill the filtering criterion.
    """

    def __init__(
        self,
        writer,
        predicate1: Predicate,

        predicate2: Predicate,