""" A brute forth algorithm for calling variant peptides from a GVF file. """
import copy
import sys
import argparse
import math
from typing import Iterable, List, Dict, Set, Tuple
from pathlib import Path
from itertools import combinations
from Bio import SeqUtils
from Bio.Seq import Seq
from moPepGen import gtf, seqvar, aa, dna, params, constant
from moPepGen.SeqFeature import FeatureLocation, SeqFeature
from moPepGen.cli import common as cli_common
from moPepGen.seqvar.VariantRecord import VariantRecord
from moPepGen.seqvar.VariantRecordPool import VariantRecordPool
from moPepGen.seqvar.VariantRecordWithCoordinate import VariantRecordWithCoordinate
from moPepGen.util.common import load_references


# pylint: disable=W0212
def parse_args(subparsers:argparse._SubParsersAction):
    """ parse command line arguments """
    parser:argparse.ArgumentParser = subparsers.add_parser(
        name='bruteForce',
        help='Call variant peptide with the brute force algorithm.'
    )
    parser.add_argument(
        '-i', '--input-gvf',
        type=Path,
        help='GVF file',
        nargs="+"
    )
    parser.add_argument(
        '-r', '--reference-dir',
        type=Path,
        help='Reference directory. Must contain genome.fa, annotation.gtf, and'
        ' proteome.fasta. The directory should be generated by'
        ' the downsampleReference command'
    )
    parser.add_argument(
        '-f', '--force',
        action='store_true',
        help='If not set, the program stops when there are more than 10'
        ' variants. When this flag is set, the program runs anyway. Noted that '
        ' the runtime is going to increase quickly after 10 variants.',
        default=False
    )
    parser.add_argument(
        '--variant-ids',
        type=str,
        help='List of variant labels.',
        nargs='*'
    )
    parser.add_argument(
        '--max-adjacent-as-mnv',
        type=int,
        help='Max number of adjacent SNPs or INDELs to be merged as a MNV.',
        default=2
    )
    parser.add_argument(
        '--selenocysteine-termination',
        action='store_true',
        help='Include peptides of selenoprotiens that the UGA is treated as '
        'termination instead of Sec.'
    )
    parser.add_argument(
        '--w2f-reassignment',
        action='store_true',
        help='Include peptides with W > F (Tryptophan to Phenylalanine) '
        'reassignment.'
    )
    cli_common.add_args_cleavage(parser)
    cli_common.add_args_debug_level(parser)

    parser.set_defaults(func=main)
    cli_common.print_help_if_missing_args(parser)
    return parser

def _parse_exclusion(exclusion) -> Tuple:
    """ Parse exclusion to values """
    start, ref, alt = exclusion.split('-')
    return int(start), ref, alt

def parse_variant_exclusion(exclusions:List[str]) -> Dict[Tuple,List[Tuple]]:
    """ Parse exclusion variants into """
    groups = {}
    for exclusion in exclusions:
        variant, targets = exclusion.split(':')
        targets = targets.split(',')
        variant = _parse_exclusion(variant)
        targets = [_parse_exclusion(target) for target in targets]
        groups[variant] = targets
    return groups


class BruteForceVariantPeptideCaller():
    """ Variant peptide caller using the brute force algorithm. """
    def __init__(self, reference_data:params.ReferenceData=None,
            cleavage_params:params.CleavageParams=None,
            variant_pool:seqvar.VariantRecordPool=None,
            canonical_peptides=None, tx_id:str=None,
            tx_model:gtf.TranscriptAnnotationModel=None,
            tx_seq:dna.DNASeqRecordWithCoordinates=None, gene_seq:dna.DNASeqRecord=None,
            start_index:int=None, variant_peptides:Set[str]=None,
            w2f:bool=False, selenocysteine_termination:bool=False):
        """ Constructor """
        self.reference_data = reference_data
        self.cleavage_params = cleavage_params
        self.variant_pool = variant_pool or VariantRecordPool()
        self.canonical_peptides = canonical_peptides
        self.tx_id = tx_id
        self.tx_model = tx_model
        self.tx_seq = tx_seq
        self.gene_seq = gene_seq
        self.start_index = start_index
        self.variant_peptides = variant_peptides or set()
        self.selenocysteine_termination = selenocysteine_termination
        self.w2f = w2f

    def create_canonical_peptide_pool(self):
        """ Create canonical peptide pool. """
        proteome = self.reference_data.proteome
        par = self.cleavage_params
        self.canonical_peptides = proteome.create_unique_peptide_pool(
            anno=self.reference_data.anno,
            rule=par.enzyme,
            exception=par.exception,
            miscleavage=par.miscleavage,
            min_mw=par.min_mw,
            min_length=par.min_length,
            max_length=par.max_length
        )

    def get_gene_seq(self) -> dna.DNASeqRecord:
        """ Get the gene sequence and cache it if it is not already cached. """
        if self.gene_seq:
            return self.gene_seq
        gene_id = self.tx_model.gene_id
        gene_model = self.reference_data.anno.genes[gene_id]
        chrom = gene_model.chrom
        self.gene_seq = gene_model.get_gene_sequence(self.reference_data.genome[chrom])
        return self.gene_seq

    def load_relevant_variants(self, pool:VariantRecordPool):
        """ Load relevant variants. Relevant variants are those associated with
        the transcript of `self.tx_id`, or the donor transcript of any fusion. """
        if self.tx_id not in pool:
            return

        for variant in pool[self.tx_id].transcriptional:
            self.variant_pool.add_transcriptional_variant(variant)
        for variant in pool[self.tx_id].intronic:
            self.variant_pool.add_intronic_variant(variant)
        for variant in pool[self.tx_id].fusion:
            self.variant_pool.add_fusion_variant(variant)
            right_tx_id = variant.attrs['ACCEPTER_TRANSCRIPT_ID']
            if right_tx_id not in pool:
                continue
            for v in pool[right_tx_id].transcriptional:
                self.variant_pool.add_transcriptional_variant(v)
            for v in pool[right_tx_id].intronic:
                self.variant_pool.add_intronic_variant(v)
        for variant in pool[self.tx_id].circ_rna:
            self.variant_pool.add_circ_rna(variant)

    def get_variants(self, tx_id:str, start:int, end:int,
            variant_ids:Iterable[str]=None) -> List[seqvar.VariantRecord]:
        """ Load variant records associated with the particular transcript. """
        series = self.variant_pool[tx_id]
        variants = []
        for variant in series.transcriptional:
            if variant.location.start < start -1:
                continue
            if self.tx_model.is_mrna_end_nf() and variant.location.end <= end:
                continue
            if variant_ids:
                if variant.id not in variant_ids:
                    continue
            if variant.location.start == start - 1:
                variant.to_end_inclusion(self.tx_seq)
            variants.append(variant)
        return variants

    def get_start_index(self):
        """ Get the "start index" used for filtering variants. For noncoding
        transcript, the "start index" is set to the very beginning. """
        if self.tx_seq.orf:
            self.start_index = self.tx_seq.orf.start + 3
        else:
            self.start_index = 3

    def peptide_is_valid(self, peptide:str, denylist:List[str], check_canonical) -> bool:
        """ Check whether the peptide is valid """
        if check_canonical \
                and self.canonical_peptides \
                and peptide in self.canonical_peptides:
            return False
        if peptide in denylist:
            return False
        min_len = self.cleavage_params.min_length
        max_len = self.cleavage_params.max_length
        min_mw = self.cleavage_params.min_mw
        return min_len <= len(peptide) <= max_len \
            and SeqUtils.molecular_weight(peptide, 'protein') >= min_mw

    def is_stop_lost(self, variant:seqvar.VariantRecord, reading_frame_index:int) -> bool:
        """ Check whether the variant is a stop lost mutation. """
        orf_index = self.start_index % 3

        if self.tx_model.is_protein_coding:
            if orf_index != reading_frame_index:
                return False
            orf_end = self.tx_seq.orf.end
            stop_codon = FeatureLocation(orf_end, orf_end + 3)
            return variant.location.overlaps(stop_codon)

        i = variant.variant.location.start - (variant.location.start - orf_index) % 3
        while i < variant.location.end:
            if self.tx_seq.seq[i:i+3] in ['TAA', 'TAG', 'TGA']:
                return True
            i += 3
        return False

    def should_clip_trailing_nodes(self, variants:List[seqvar.VariantRecordWithCoordinate]):
        """ Checks whether the trailing nodes should be excluded """
        return any(v.variant.is_circ_rna() for v in variants) \
            or self.tx_model.is_mrna_end_nf()

    def get_effective_variants(self, lhs:int, rhs:int,
            lrange:Tuple[int,int], rrange:Tuple[int,int], cds_start:int,
            variants:List[seqvar.VariantRecordWithCoordinate],
            variants_stop_lost:List[Tuple[bool,bool,bool]],
            variants_stop_gain:List[Tuple[bool,bool,bool]],
            variants_silent_mutation:List[Tuple[bool,bool,bool]]
            ) -> List[VariantRecordWithCoordinate]:
        """ Check whether the given range of the transcript has any variant
        associated. """
        effective_variants:List[seqvar.VariantRecordWithCoordinate] = []
        offset = 0
        query = FeatureLocation(start=lhs, end=rhs)
        start_loc = FeatureLocation(start=cds_start, end=cds_start + 3)
        rf_index = cds_start % 3
        frames_shifted = 0
        upstream_indels:List[VariantRecordWithCoordinate] = []
        is_coding = self.tx_model.is_protein_coding \
            and not any(v.variant.is_circ_rna() for v in variants)
        for i, variant_coordinate in enumerate(variants):
            variant = variant_coordinate.variant
            loc = variant_coordinate.location
            if variant.type == 'Insertion':
                loc = FeatureLocation(start=loc.start+1, end=loc.end)
            if loc.start > rhs + 3:
                break
            is_start_gain = start_loc.overlaps(loc)
            is_frameshifting = cds_start < loc.start < lhs and variant.is_frameshifting()
            if cds_start < loc.start < lhs:
                frames_shifted = (frames_shifted + variant.frames_shifted()) % 3
                if variant.is_frameshifting() \
                        and not (variant.is_fusion() or variant.is_circ_rna()):
                    upstream_indels.append(variant_coordinate)
            is_cleavage_gain = (
                loc.overlaps(FeatureLocation(start=lrange[0], end=lrange[1]))
                or loc.overlaps(FeatureLocation(start=rrange[0], end=rrange[1]))
            ) \
                if cds_start != lhs \
                else loc.overlaps(FeatureLocation(start=rhs, end=rhs + 3))

            is_stop_lost = variants_stop_lost[i][cds_start % 3] \
                and cds_start < variants[i].location.start < rhs

            is_silent_mutation = variants_silent_mutation[i][cds_start % 3] \
                and variants[i].location.start > cds_start

            is_stop_gain = variants_stop_gain[i][cds_start % 3] \
                and int((variants[i].location.start - rf_index) / 3) \
                    == int((rhs - rf_index)/3)

            if (loc.overlaps(query) \
                        or is_start_gain \
                        or (is_frameshifting and not is_coding) \
                        or is_cleavage_gain \
                        or is_stop_lost \
                        or is_stop_gain ) \
                    and not is_silent_mutation:
                effective_variants.append(variant_coordinate)
            offset += len(variant.alt) - len(variant.ref)

        if frames_shifted > 0:
            for v in upstream_indels:
                if v.variant.is_frameshifting():
                    effective_variants.append(v)
        else:
            # If there are multiple frameshifts but the overall frames shifted is 0,
            # then checks if the reference sequence skipped has any stop codon.
            if len(upstream_indels) > 1:
                first_start = upstream_indels[0].variant.location.start
                last_end = upstream_indels[-1].variant.location.end
                rf_index = cds_start % 3
                if self.has_any_stop_codon_between(first_start, last_end, rf_index):
                    effective_variants.extend(upstream_indels)
        effective_variants.sort(key=lambda v: v.location)
        return effective_variants

    def has_any_stop_codon_between(self, start:int, end:int, rf_index:int) -> bool:
        """ Checks if there is any stop codon between a given range of the tx. """
        aa_start = math.floor((start - rf_index) / 3)
        aa_end = math.ceil((end - rf_index) / 3)
        aa_end = min(aa_end, math.floor((len(self.tx_seq) - rf_index)/3))
        aa_seq:Seq = self.tx_seq.seq[rf_index:].translate()[aa_start:aa_end]
        i = 0
        sec_sites = {int(x.start) for x in self.tx_seq.selenocysteine}
        while i > -1:
            i = aa_seq.find('*', i)
            if i == -1:
                return False
            if aa_start * 3 + rf_index + i * 3 not in sec_sites:
                return True
            i += 1
        return False

    @staticmethod
    def has_overlapping_variants(variants:List[VariantRecord]) -> bool:
        """ Checks if any variants overlap. """
        for i, left in enumerate(variants):
            if i == len(variants) - 1:
                continue
            for right in variants[i+1:]:
                if left.location.end >= right.location.start:
                    return True
        return False

    def has_any_invalid_variants_on_inserted_sequences(self,
            pool:seqvar.VariantRecordPool) -> bool:
        """ Checks if any variants carried by fusion or alt splicing. In valid
        variants are those not in the region of sequence introduced by fusion.
        For example, a variant of the donor transcript before the breakpoint is
        invalid.
        """
        alt_splices = [x for x in pool[self.tx_id].transcriptional if x.is_alternative_splicing()]
        if not pool[self.tx_id].fusion and not alt_splices:
            return False

        inserted_intronic_region:Dict[str, List[FeatureLocation]] = {}

        if pool[self.tx_id].fusion:
            fusion = pool[self.tx_id].fusion[0]
            left_insert_start = fusion.attrs['LEFT_INSERTION_START']
            left_insert_end = fusion.attrs['LEFT_INSERTION_END']
            right_insert_start = fusion.attrs['RIGHT_INSERTION_START']
            right_insert_end = fusion.attrs['RIGHT_INSERTION_END']
            right_tx_id = fusion.attrs['ACCEPTER_TRANSCRIPT_ID']
            right_tx_model = self.reference_data.anno.transcripts[right_tx_id]
            right_gene_id = right_tx_model.gene_id
            right_breakpoint_gene = fusion.get_accepter_position()
            right_breakpoint_tx = self.reference_data.anno.coordinate_gene_to_transcript(
                index=right_breakpoint_gene,
                gene=right_gene_id,
                transcript=right_tx_id
            )

            if right_tx_id in pool:
                alt_splices += [x for x in pool[right_tx_id].transcriptional
                    if x.is_alternative_splicing()]

            if any(x.location.end >= fusion.location.start
                    for x in pool[self.tx_id].transcriptional):
                return True

            if left_insert_start:
                loc = FeatureLocation(start=left_insert_start, end=left_insert_end)
                if self.tx_id not in inserted_intronic_region:
                    inserted_intronic_region[self.tx_id] = []
                inserted_intronic_region[self.tx_id].append(loc)

            if right_insert_start:
                loc = FeatureLocation(start=right_insert_start, end=right_insert_end)
                if right_tx_id not in inserted_intronic_region:
                    inserted_intronic_region[right_tx_id] = []
                inserted_intronic_region[right_tx_id].append(loc)

            if right_tx_id in pool \
                    and any(x.location.start <= right_breakpoint_tx
                        for x in pool[right_tx_id].transcriptional):
                return True

        for alt_splice in alt_splices:
            donor_start = alt_splice.attrs.get('DONOR_START')
            if not donor_start:
                continue
            donor_start = int(donor_start)
            donor_end = int(alt_splice.attrs['DONOR_END'])
            loc = FeatureLocation(start=donor_start, end=donor_end)
            if alt_splice.transcript_id not in inserted_intronic_region:
                inserted_intronic_region[alt_splice.transcript_id] = []
            inserted_intronic_region[alt_splice.transcript_id].append(loc)

        if not inserted_intronic_region:
            return False

        for tx_id in pool:
            if not pool[tx_id].intronic:
                continue
            if tx_id not in inserted_intronic_region:
                return True
            for v in pool[tx_id].intronic:
                if not any(x.start < v.location.start and x.end > v.location.end
                        for x in inserted_intronic_region[tx_id]):
                    return True
        return False

    @staticmethod
    def has_any_invalid_variant_on_circ() -> bool:
        """ Checks if any variants are invlid with circRNA. """
        return False

    def has_incompatible_variants(self, pool:seqvar.VariantRecordPool) -> bool:
        """ Whether there are incompatible variants, i.e. variants that overlap
        with each other. """
        # check if there is any variants
        if self.tx_id not in pool:
            return True

        if not pool[self.tx_id].transcriptional\
                and not pool[self.tx_id].circ_rna \
                and not pool[self.tx_id].fusion:
            return True

        # check if there are multiple novel transcript variants (fusion + circ)
        n_fusion = len(pool[self.tx_id].fusion)
        n_circ = len(pool[self.tx_id].circ_rna)
        n_alt_splice = len([x for x in pool[self.tx_id].transcriptional
            if x.type in constant.ALTERNATIVE_SPLICING_TYPES])

        orf = self.tx_seq.orf
        is_mrna_end_nf = self.tx_model.is_mrna_end_nf()
        start_index = orf.start + 3 if bool(orf) else 3

        if n_circ == 0:
            for variant in pool[self.tx_id].transcriptional:
                if variant.location.start < start_index:
                    return True
                if is_mrna_end_nf and orf is not None:
                    orf_end_trinuc = FeatureLocation(start=orf.end-3, end=orf.end)
                    if variant.location.overlaps(orf_end_trinuc):
                        return True

        for fusion in pool[self.tx_id].fusion:
            if fusion.location.start < start_index:
                return True
            accepter_tx_id = fusion.attrs['ACCEPTER_TRANSCRIPT_ID']
            if accepter_tx_id not in pool:
                continue
            n_alt_splice += len([x for x in pool[accepter_tx_id].transcriptional
                if x.type in constant.ALTERNATIVE_SPLICING_TYPES])
        if n_fusion + n_circ > 1:
            return True

        # not allowing any alternative splicing comb with fusion or circ
        if (n_fusion > 0 or n_circ > 0) and n_alt_splice > 0:
            return True

        if self.has_any_invalid_variants_on_inserted_sequences(pool):
            return True

        # if self.has_any_invalid_variant_on_circ(pool):
        #     return True

        for series in pool.data.values():
            if self.has_overlapping_variants(series.transcriptional):
                return True
            if self.has_overlapping_variants(series.intronic):
                return True

        if self.tx_model.is_mrna_end_nf():
            for variant in pool[self.tx_id].transcriptional:
                if variant.location.end >= self.tx_seq.orf.end:
                    return True
                if n_circ == 0 \
                        and variant.location.start >= self.tx_seq.orf.end - 3:
                    return True
        return False

    @staticmethod
    def find_prev_cds_start_same_frame(cds_start:int, cds_start_positions:List[int]):
        """ find he previous cds start site in the same reading frame. """
        if cds_start == 0:
            return -1
        reading_frame_index = cds_start % 3
        for site in cds_start_positions[::-1]:
            if site >= cds_start:
                continue
            if site % 3 == reading_frame_index:
                return site
        return -1

    def get_variant_sequence(self, seq:Seq, location:FeatureLocation,
            offset:int, variants:List[seqvar.VariantRecord],
            pool:seqvar.VariantRecordPool
            ) -> Tuple[Seq, List[seqvar.VariantRecordWithCoordinate]]:
        """ Get variant sequence. """
        var_seq = seq
        variant_coordinates = []
        local_offset = 0
        for variant in variants:
            if variant.is_alternative_splicing():
                if variant.type == 'Deletion':
                    start = variant.location.start + local_offset - location.start
                    end = variant.location.end + local_offset - location.start
                    loc = FeatureLocation(start=start, end=start + 1)
                    variant_coordinate = seqvar.VariantRecordWithCoordinate(
                        variant=variant,
                        location=loc
                    )

                    variant_coordinates.append(variant_coordinate)
                    alt_seq = var_seq[start:start+1]
                    local_offset = local_offset + len(alt_seq) - len(variant.location)
                    var_seq = var_seq[:start] + alt_seq + var_seq[end:]

                elif variant.type == 'Insertion':
                    start = variant.location.start + local_offset - location.start
                    end = variant.location.end + local_offset - location.start

                    gene_seq = self.get_gene_seq()
                    donor_start = variant.get_donor_start()
                    donor_end = variant.get_donor_end()
                    alt_seq = str(gene_seq.seq[donor_start:donor_end])
                    loc = FeatureLocation(start=donor_start, end=donor_end)
                    insert_variants = [x for x in pool[self.tx_id].intronic
                        if loc.is_superset(x.location)]
                    alt_seq, insert_variants = self.get_variant_sequence(
                        seq=alt_seq, location=loc, offset=start,
                        variants=insert_variants, pool=pool
                    )

                    variant_coordinate = seqvar.VariantRecordWithCoordinate(
                        variant=variant,
                        location=FeatureLocation(start=start, end=start + len(alt_seq) + 1)
                    )
                    variant_coordinates.append(variant_coordinate)
                    variant_coordinates += insert_variants
                    local_offset = local_offset + len(alt_seq) + 1 - len(variant.location)
                    var_seq = var_seq[:start+1] + alt_seq + var_seq[end:]

                elif variant.type == 'Substitution':
                    start = variant.location.start + local_offset - location.start
                    end = variant.location.end + local_offset - location.start

                    gene_seq = self.get_gene_seq()
                    donor_start = variant.get_donor_start()
                    donor_end = variant.get_donor_end()
                    alt_seq = str(gene_seq.seq[donor_start:donor_end])
                    loc = FeatureLocation(start=donor_start, end=donor_end)
                    insert_variants = [x for x in pool[self.tx_id].intronic
                        if loc.is_superset(x.location)]
                    alt_seq, insert_variants = self.get_variant_sequence(
                        seq=alt_seq, location=loc, offset=start,
                        variants=insert_variants, pool=pool
                    )

                    variant_coordinate = seqvar.VariantRecordWithCoordinate(
                        variant=variant,
                        location=FeatureLocation(start=start, end=start + len(alt_seq))
                    )
                    variant_coordinates.append(variant_coordinate)
                    variant_coordinates += insert_variants
                    local_offset = local_offset + len(alt_seq) - len(variant.location)
                    var_seq = var_seq[:start] + alt_seq + var_seq[end:]

            else:
                start = variant.location.start + local_offset - location.start
                end = variant.location.end + local_offset - location.start
                loc = FeatureLocation(
                    start=start + offset, end=start + len(variant.alt) + offset
                )
                variant_coordinate = seqvar.VariantRecordWithCoordinate(
                    variant=variant,
                    location=loc
                )

                variant_coordinates.append(variant_coordinate)
                local_offset = local_offset + len(variant.alt) - len(variant.ref)
                var_seq = var_seq[:start] + variant.alt + var_seq[end:]

        return var_seq, variant_coordinates

    def get_variant_sequence_circ_rna(self, seq:Seq, variants:seqvar.VariantRecordPool
            ) -> Tuple[Seq, List[seqvar.VariantRecordWithCoordinate]]:
        """ Get the variant sequence of circRNA """
        number_of_circ = len(variants[self.tx_id].circ_rna)
        if number_of_circ != 1:
            raise ValueError(
                f"Should have exactly 1 circRNA, but {number_of_circ} were found."
            )
        circ = variants[self.tx_id].circ_rna[0]
        var_seq = Seq('')
        vars_coord = []

        for fragment in circ.fragments:
            loc = FeatureLocation(
                start=int(fragment.location.start), end=int(fragment.location.end)
            )
            frag_loc = FeatureLocation(
                start=int(fragment.location.start) + 3, end=int(fragment.location.end)
            )
            fragment = SeqFeature(location=frag_loc, chrom=fragment.chrom,
                attributes=fragment.attributes)
            new_seq = seq[loc.start:loc.end]
            frag_vars = variants.filter_variants(
                tx_ids=[circ.transcript_id], exclude_type=constant.ALTERNATIVE_SPLICING_TYPES,
                intron=False, segments=[fragment]
            )
            frag_seq, frag_vars_coord = self.get_variant_sequence(
                seq=new_seq, location=loc, offset=len(var_seq),
                variants=frag_vars, pool=variants
            )
            var_seq += frag_seq
            vars_coord += frag_vars_coord

        location = FeatureLocation(
            seqname=circ.gene_id,
            start=min(x.location.start for x in circ.fragments),
            end=max(x.location.end for x in circ.fragments)
        )
        circ_var = seqvar.VariantRecord(
            location=location,
            ref=var_seq[0],
            alt='<circRNA>',
            _type='circRNA',
            _id=circ.id
        )

        vars_aloop = copy.deepcopy(vars_coord)
        seq_aloop = copy.deepcopy(var_seq)

        for _ in range(3):
            offset = len(var_seq)
            vars_extend = copy.deepcopy(vars_aloop)
            for variant in vars_extend:
                location = FeatureLocation(
                    seqname=variant.location.seqname,
                    start=variant.location.start + offset,
                    end=variant.location.end + offset
                )
                variant.location = location
            var_seq += seq_aloop
            vars_coord += vars_extend

        circ_var_coord = VariantRecordWithCoordinate(
            variant=circ_var, location=FeatureLocation(start=0, end=len(var_seq))
        )
        vars_coord.insert(0, circ_var_coord)

        return var_seq, vars_coord

    def get_variant_sequence_fusion(self, seq:Seq, variants:seqvar.VariantRecordPool
            ) -> Tuple[Seq, List[seqvar.VariantRecordWithCoordinate]]:
        """ Get variant sequence with fusion. """
        number_of_fusion = len(variants[self.tx_id].fusion)
        if not number_of_fusion == 1:
            raise ValueError(
                f"Should have exactly 1 fusion, but {number_of_fusion} were found."
            )
        fusion = variants[self.tx_id].fusion[0]
        var_seq = seq[:fusion.location.start]
        location = FeatureLocation(start=0, end=len(var_seq))
        var_seq, variant_coordinates = self.get_variant_sequence(
            seq=var_seq, location=location, offset=0,
            variants=variants[self.tx_id].transcriptional, pool=variants
        )

        left_insert_start = fusion.attrs['LEFT_INSERTION_START']
        left_insert_end = fusion.attrs['LEFT_INSERTION_END']
        right_insert_start = fusion.attrs['RIGHT_INSERTION_START']
        right_insert_end = fusion.attrs['RIGHT_INSERTION_END']
        right_tx_id = fusion.attrs['ACCEPTER_TRANSCRIPT_ID']
        right_gene_id = fusion.attrs['ACCEPTER_GENE_ID']

        additional_seq = Seq('')
        additional_variants:List[VariantRecordWithCoordinate] = []

        # left insertion
        if left_insert_start is not None:
            gene_seq = self.get_gene_seq()
            location = FeatureLocation(start=left_insert_start, end=left_insert_end)
            insert_seq = gene_seq.seq[left_insert_start:left_insert_end]
            insert_variants = [x for x in variants[self.tx_id].intronic
                if location.is_superset(x.location)]
            insert_seq, insert_variants = self.get_variant_sequence(
                seq=insert_seq, location=location,
                offset=len(var_seq) + len(additional_seq),
                variants=insert_variants, pool=variants
            )
            location=FeatureLocation(
                start=len(var_seq),
                end=len(var_seq) + len(insert_seq)
            )
            insert_fusion = VariantRecordWithCoordinate(
                variant=fusion, location=location
            )
            insert_variants.insert(0, insert_fusion)
            additional_seq += insert_seq
            additional_variants += insert_variants

        # right insertion
        if right_insert_start is not None:
            gene_model = self.reference_data.anno.genes[right_gene_id]
            chrom = gene_model.chrom
            gene_seq = gene_model.get_gene_sequence(self.reference_data.genome[chrom])
            location = FeatureLocation(start=right_insert_start, end=right_insert_end)
            insert_seq = gene_seq.seq[right_insert_start:right_insert_end]
            insert_seq, insert_variants = self.get_variant_sequence(
                seq=insert_seq, location=location,
                offset=len(var_seq) + len(additional_seq),
                variants=variants[right_tx_id].intronic if right_tx_id in variants else [],
                pool=variants
            )
            location=FeatureLocation(
                start=len(var_seq) + len(additional_seq),
                end=len(var_seq) + len(additional_seq) + len(insert_seq)
            )
            insert_fusion = VariantRecordWithCoordinate(
                variant=fusion, location=location
            )
            insert_variants.insert(0, insert_fusion)
            additional_seq += insert_seq
            additional_variants += insert_variants

        right_tx_model = self.reference_data.anno.transcripts[right_tx_id]
        accepter_chrom = right_tx_model.transcript.location.seqname
        breakpoint_gene = fusion.get_accepter_position()
        breakpoint_tx = self.reference_data.anno.coordinate_gene_to_transcript(
            index=breakpoint_gene,
            gene=right_gene_id,
            transcript=right_tx_id
        )
        accepter_seq = right_tx_model.get_transcript_sequence(
            self.reference_data.genome[accepter_chrom]
        )
        location = FeatureLocation(start=breakpoint_tx, end=len(accepter_seq))
        insert_seq = accepter_seq.seq[breakpoint_tx:]
        insert_seq, insert_variants = self.get_variant_sequence(
            seq=insert_seq, location=location,
            offset=len(var_seq) + len(additional_seq),
            variants=variants[right_tx_id].transcriptional if right_tx_id in variants else [],
            pool=variants
        )

        location = FeatureLocation(
            start=len(var_seq) + len(additional_seq),
            end=len(var_seq) + len(additional_seq) + len(insert_seq)
        )
        fusion_var = VariantRecordWithCoordinate(variant=fusion, location=location)
        insert_variants.insert(0, fusion_var)

        additional_seq += insert_seq
        additional_variants += insert_variants

        var_seq += additional_seq
        variant_coordinates += additional_variants

        return var_seq, variant_coordinates

    def get_variant_ref_seq(self, variant:seqvar.VariantRecord) -> Seq:
        """ Get the reference sequence of a variant """
        if variant.type in ['Deletion', 'Substitution']:
            return self.tx_seq.seq[variant.location.start:variant.location.end]
        return variant.ref

    def check_variant_effect(self, seq:str,
            variants:List[seqvar.VariantRecordWithCoordinate]
            ) -> List[Tuple[bool, bool, bool]]:
        """ Check the variant effects, including stop lost, stop gain, and
        silent mutation """
        stop_lost:List[Tuple[bool, bool, bool]] = []
        stop_gain:List[Tuple[bool, bool, bool]] = []
        silent_mutation:List[Tuple[bool, bool, bool]] = []
        for variant in variants:
            skip_stop_lost = False
            skip_stop_gain = False
            skip_silent_mutation =False
            stop_gain_i:List[bool] = []
            stop_lost_i:List[bool] = []
            silent_mutation_i:List[bool] = []

            if variant.variant.is_fusion() or variant.variant.is_circ_rna():
                stop_gain_i = (False, False, False)
                skip_stop_gain = True

            if variant.variant.is_fusion() \
                    or variant.variant.is_circ_rna() \
                    or variant.variant.transcript_id != self.tx_id \
                    or 'TRANSCRIPT_ID' in variant.variant.attrs:
                stop_lost_i = (False, False, False)
                skip_stop_lost = True

            if variant.variant.is_fusion() \
                    or variant.variant.is_circ_rna() \
                    or variant.variant.is_alternative_splicing()\
                    or variant.variant.is_frameshifting():
                silent_mutation_i = (False, False, False)
                skip_silent_mutation = True

            for i in range(3):
                lhs_offset = (variant.location.start - i) % 3
                alt_lhs = variant.location.start - lhs_offset
                var_seq = seq[alt_lhs:variant.location.end]
                n_carry_over = (3 - (len(var_seq) % 3)) % 3
                alt_rhs = min(len(seq), variant.location.end + n_carry_over)
                var_seq += seq[variant.location.end:alt_rhs]

                loc = variant.variant.location
                lhs = loc.start - lhs_offset
                rhs = loc.end + (3 - (loc.end - lhs) % 3) % 3
                rhs = min(len(self.tx_seq), rhs)
                ref_seq = self.tx_seq.seq[lhs:rhs]
                ref_rf_index = lhs % 3

                var_aa = Seq(var_seq).translate(to_stop=False)
                ref_aa = Seq(ref_seq).translate(to_stop=False)
                for sec in self.tx_seq.selenocysteine:
                    if lhs <= sec.start < sec.end <= rhs \
                            and (sec.start - lhs) % 3 == 0:
                        sec_aa = int((sec.start - lhs) / 3)
                        ref_aa = ref_aa[:sec_aa] + 'U' + ref_aa[sec_aa+1:]

                    # this is when the variant is a deletion, the position is
                    # after the 3rd nt of selenocysteine codon, the codon sequence
                    # is unchanged and the amino acid sequence should be replaced
                    # to U from *.
                    if variant.variant.is_deletion():
                        if not variant.variant.is_end_inclusion() \
                                and lhs_offset == 2 \
                                and alt_lhs == sec.start and sec.end == alt_rhs \
                                and (sec.start - alt_lhs) % 3 == 0:
                            sec_aa = int((sec.start - alt_lhs) / 3)
                            var_aa = var_aa[:sec_aa] + 'U' + var_aa[sec_aa+1:]

                if not skip_stop_lost:
                    is_stop_lost = '*' not in var_aa and '*' in ref_aa \
                        and (self.tx_seq.orf is None
                            or ref_rf_index == self.tx_seq.orf.start % 3)
                    stop_lost_i.append(is_stop_lost)

                sec_altering = any(loc.overlaps(sec) for sec in self.tx_seq.selenocysteine)

                if not skip_stop_gain:
                    is_stop_gain = var_aa == '' or var_aa.startswith('*') \
                        and (
                            not (ref_aa == '' or ref_aa.startswith('*'))
                            or sec_altering
                        )
                    stop_gain_i.append(is_stop_gain)

                if not skip_silent_mutation:
                    is_silent_mutation = ref_aa == var_aa and not sec_altering
                    silent_mutation_i.append(is_silent_mutation)
            stop_lost.append(tuple(stop_lost_i))
            stop_gain.append(tuple(stop_gain_i))
            silent_mutation.append(tuple(silent_mutation_i))
        return stop_lost, stop_gain, silent_mutation

    def get_sec_positions(self, variants:List[VariantRecordWithCoordinate]) -> List[int]:
        """ Get Sec positions in the altered sequence. """
        sec_iter = iter(self.tx_seq.selenocysteine)
        var_iter = iter(variants)
        sec_i = next(sec_iter, None)
        var_i = next(var_iter, None)

        sec_positions = []
        offset = 0
        fusion_breakpoint = None
        while sec_i:
            if var_i:
                # Not consider Sec after fusion breakpoint.
                if var_i.variant.is_fusion():
                    fusion_breakpoint = var_i.variant.location.start
                    var_i = None
                    continue
                if var_i.variant.location.end <= sec_i.start:
                    ref_len = var_i.variant.get_ref_len()
                    alt_len = var_i.variant.get_alt_len()
                    offset += (alt_len - ref_len)
                    var_i = next(var_iter, None)
                    continue

                if var_i.variant.type == 'Deletion':
                    # Because for Deletion, the sequence after the REF
                    # nucleotide is deleted so the first nucleotide is unchanged.
                    var_loc = FeatureLocation(
                        var_i.variant.location.start+1,
                        var_i.variant.location.end
                    )
                else:
                    var_loc = var_i.variant.location

                if var_loc.overlaps(sec_i):
                    sec_i = next(sec_iter, None)
                    continue
            if fusion_breakpoint and sec_i.end >= fusion_breakpoint:
                break
            sec_start = sec_i.start + offset
            sec_positions.append(sec_start)
            sec_i = next(sec_iter, None)
        return sec_positions

    def call_peptides_main(self, variants:seqvar.VariantRecordPool,
            denylist:Set[str], check_variants:bool, check_canonical:bool,
            selenocysteine_termination:bool=False, is_mrna_end_nf:bool=False):
        """ Call peptide main """
        variant_peptides = set()
        tx_model = self.tx_model
        tx_seq = self.tx_seq
        rule = self.cleavage_params.enzyme
        exception = self.cleavage_params.exception

        is_circ_rna = False
        is_fusion = False

        is_coding = tx_model.is_protein_coding

        if variants[self.tx_id].fusion:
            is_fusion = True
            seq, variant_coordinates = self.get_variant_sequence_fusion(
                seq=tx_seq.seq, variants=variants
            )
        elif variants[self.tx_id].circ_rna:
            is_circ_rna = True
            gene_seq = self.get_gene_seq()
            seq, variant_coordinates = self.get_variant_sequence_circ_rna(
                seq=gene_seq.seq, variants=variants
            )
        else:
            location = FeatureLocation(start=0, end=len(tx_seq.seq))
            seq, variant_coordinates = self.get_variant_sequence(
                seq=tx_seq.seq, location=location, offset=0,
                variants=variants[self.tx_id].transcriptional, pool=variants
            )

        sec_positions = [] if is_circ_rna else \
            self.get_sec_positions(variant_coordinates)
        variant_effects = self.check_variant_effect(str(seq), variant_coordinates)
        stop_lost, stop_gain, silent_mutation = variant_effects

        if not (is_coding and is_mrna_end_nf):
            cur_cds_end = len(seq)

        if not is_coding or is_circ_rna:
            alt_seq = dna.DNASeqRecord(seq)
            cds_start_positions = alt_seq.find_all_start_codons()
            if is_fusion:
                fusion_var = None
                # Get the last fusion, which should be the actual fusion
                # no insertions of intronic regions.
                for variant in reversed(variant_coordinates):
                    if variant.variant.is_fusion():
                        fusion_var = variant
                        break
                cds_start_positions = [x for x in cds_start_positions
                    if x <= fusion_var.location.start]
        else:
            cds_start = tx_seq.orf.start
            cds_start_positions:List[int] = [cds_start]

        cds_start_sites = set(cds_start_positions)


        for cds_start in cds_start_positions:
            cur_cds_end = len(seq) - (len(seq) - cds_start) % 3

            aa_seq = seq[cds_start:cur_cds_end].translate(to_stop=False)
            if not is_circ_rna:
                for sec_start in sec_positions:
                    if (sec_start - cds_start) % 3 == 0:
                        sec_start_aa = int((sec_start - cds_start) / 3)
                        aa_seq = aa_seq[:sec_start_aa] + 'U' + aa_seq[sec_start_aa+1:]
            stop_index = aa_seq.find('*')
            if stop_index > -1:
                aa_seq = aa_seq[:stop_index]
            aa_seq = aa.AminoAcidSeqRecord(seq=aa_seq)

            sites = aa_seq.find_all_enzymatic_cleave_sites_with_ranges(rule, exception)

            # Finding the next M, so peptides that starts from the next M should
            # be processed with the correct `cds_start`
            if (not is_coding or is_circ_rna):
                next_m = aa_seq.seq[1:].find('M') + 1
                if cds_start + next_m * 3 not in cds_start_sites:
                    next_m = 0
            else:
                next_m = 0

            sites.insert(0, (0, (0,1)))
            sites.append((len(aa_seq), (len(aa_seq), len(aa_seq))))
            for j, site in enumerate(sites[:-1]):
                lhs, lrange = site
                if 0 < next_m < lhs:
                    break
                last_m = aa_seq.seq[:lhs].rfind('M')
                if last_m > 0 and not is_coding:
                    actual_cds_start = cds_start + last_m * 3
                else:
                    actual_cds_start = cds_start

                # prev_cds_start = self.find_prev_cds_start_same_frame(
                #     cds_start=actual_cds_start,
                #     cds_start_positions=cds_start_positions
                # )

                for k in range(j + 1, min([j + 3, len(sites) - 1]) + 1):
                    rhs, rrange = sites[k]
                    tx_lhs = cds_start + lhs * 3
                    tx_rhs = cds_start + rhs * 3
                    if is_mrna_end_nf and tx_rhs + 3 > len(seq):
                        continue
                    peptide = aa_seq.seq[lhs:rhs]
                    is_in_denylist = str(peptide) in denylist \
                        and (not lhs == 0 or str(peptide[1:]) in denylist)
                    if is_in_denylist:
                        continue
                    tx_lrange = (cds_start + lrange[0] * 3, cds_start + lrange[1] * 3)
                    if k == len(sites) - 1 \
                            and tx_rhs + 3 <= len(seq) \
                            and seq[tx_rhs:tx_rhs + 3].translate() == '*':
                        tx_rrange = (tx_rhs, tx_rhs + 3)
                    else:
                        tx_rrange = (cds_start + rrange[0] * 3, cds_start + rrange[1] * 3)
                    if check_variants:
                        effective_variants = self.get_effective_variants(
                            lhs=tx_lhs, rhs=tx_rhs, lrange=tx_lrange, rrange=tx_rrange,
                            cds_start=actual_cds_start, variants=variant_coordinates,
                            variants_stop_lost=stop_lost, variants_stop_gain=stop_gain,
                            variants_silent_mutation=silent_mutation
                        )

                        if not effective_variants:
                            continue
                        if is_fusion and \
                                not any(v.variant.is_fusion() for v in effective_variants):
                            continue
                    else:
                        effective_variants = []

                    peptide_seqs = self.translational_modification(
                        peptide, lhs, tx_lhs, effective_variants, denylist,
                        check_variants, check_canonical, selenocysteine_termination
                    )
                    for peptide_seq in peptide_seqs:
                        variant_peptides.add(peptide_seq)
        return variant_peptides

    def translational_modification(self, seq:Seq, lhs:int, tx_lhs:int,
            effective_variants:List[VariantRecordWithCoordinate],
            denylist:List[str], check_variants:bool, check_canonical:bool,
            selenocysteine_termination:bool
            ) -> Iterable[str]:
        """ Apply any modification that could happen during translation. """
        candidates = []
        is_start = lhs == 0 and seq.startswith('M')
        if effective_variants or not check_variants:
            candidates.append(seq)
            if is_start:
                candidates.append(seq[1:])

        if selenocysteine_termination:
            k = 0
            while k > -1:
                k = seq.find('U', k)
                if k == -1:
                    break
                if not check_variants or \
                        any(v.location.start < tx_lhs + k * 3 for v in effective_variants):
                    candidates.append(seq[:k])
                    if is_start:
                        candidates.append(seq[1:k])
                k += 1

        # W > F
        if self.w2f:
            for candidate in copy.copy(candidates):
                w2f_indices = []
                i = 0
                while i > -1:
                    i = candidate.find('W', start=i)
                    if i > -1:
                        w2f_indices.append(i)
                        i += 1
                        if i > len(candidate):
                            break

                for k in range(1, len(w2f_indices) + 1):
                    for comb in combinations(w2f_indices, k):
                        seq_mod = candidate
                        for i in comb:
                            seq_mod_new = seq_mod[:i] + 'F'
                            if i + 1 < len(candidate):
                                seq_mod_new += seq_mod[i+1:]
                            seq_mod = seq_mod_new
                        candidates.append(seq_mod)

        for candidate in candidates:
            if self.peptide_is_valid(candidate, denylist, check_canonical):
                yield str(candidate)

    def generate_variant_comb(self, fusion:bool, circ_rna:bool
            ) -> Iterable[seqvar.VariantRecordPool]:
        """ Generate combination of variants. """
        variant_type_mapper:Dict[str, Tuple[seqvar.VariantRecord, str]] = {}
        start_index = self.tx_seq.orf.start + 3 if bool(self.tx_seq.orf) else 3
        for variant in self.variant_pool[self.tx_id].transcriptional:
            if variant.location.start == start_index - 1 \
                    and (variant.is_insertion() or variant.is_deletion()) \
                    and not variant.is_alternative_splicing():
                variant.to_end_inclusion(self.tx_seq)

            variant_type_mapper[variant.id] = (variant, 'transcriptional')
        for variant in self.variant_pool[self.tx_id].intronic:
            variant_type_mapper[variant.id] = (variant, 'intronic')
        if fusion:
            for variant in self.variant_pool[self.tx_id].fusion:
                if variant.location.start < start_index - 1:
                    continue
                variant_type_mapper[variant.id] = (variant, 'fusion')
                accepter_tx_id = variant.attrs['ACCEPTER_TRANSCRIPT_ID']
                if accepter_tx_id not in self.variant_pool:
                    continue
                accepter_var_series = self.variant_pool[accepter_tx_id]
                for accepter_var in accepter_var_series.transcriptional:
                    variant_type_mapper[accepter_var.id] = (accepter_var, 'transcriptional')
                for accepter_var in accepter_var_series.intronic:
                    variant_type_mapper[accepter_var.id] = (accepter_var, 'intronic')
        if circ_rna:
            for variant in self.variant_pool[self.tx_id].circ_rna:
                variant_type_mapper[variant.id] = (variant, 'circ_rna')

        all_variants = [v[0] for v in variant_type_mapper.values()]

        for i in range(len(all_variants)):
            for inds in combinations(range(len(all_variants)), i + 1):
                variants = [all_variants[i] for i in inds]
                if fusion \
                        and not any(variant_type_mapper[v.id][1] == 'fusion'
                                    for v in variants):
                    continue
                if circ_rna \
                        and not any(variant_type_mapper[v.id][1] == 'circ_rna'
                                    for v in variants):
                    continue
                pool = seqvar.VariantRecordPool()
                pool.anno = self.variant_pool.anno
                for variant in variants:
                    var_type = variant_type_mapper[variant.id][1]
                    tx_id = variant.transcript_id
                    if var_type == 'transcriptional':
                        pool.add_transcriptional_variant(variant, tx_id)
                    elif var_type == 'intronic':
                        pool.add_intronic_variant(variant, tx_id)
                    elif var_type == 'fusion':
                        pool.add_fusion_variant(variant, tx_id)
                    elif var_type == 'circ_rna':
                        pool.add_circ_rna(variant, tx_id)
                pool.sort()
                if self.has_incompatible_variants(pool):
                    continue
                yield pool


    def call_peptides(self):
        """ Call variant peptides """
        empty_pool = seqvar.VariantRecordPool()
        empty_pool[self.tx_id] = seqvar.TranscriptionalVariantSeries()
        denylist = self.call_peptides_main(
            variants=empty_pool, denylist=set(),
            check_variants=False, check_canonical=False,
            selenocysteine_termination=self.selenocysteine_termination,
            is_mrna_end_nf=False
        )
        main_peptides = set()

        for comb in self.generate_variant_comb(fusion=False, circ_rna=False):
            peptides = self.call_peptides_main(
                variants=comb, denylist=denylist,
                check_variants=True,  check_canonical=True,
                selenocysteine_termination=self.selenocysteine_termination,
                is_mrna_end_nf=self.tx_model.is_mrna_end_nf()
            )
            self.variant_peptides.update(peptides)
            main_peptides.update(peptides)

        for comb in self.generate_variant_comb(fusion=True, circ_rna=False):
            donor_tx_id = comb[self.tx_id].fusion[0].accepter_transcript_id
            donor_tx_model = self.reference_data.anno.transcripts[donor_tx_id]
            peptides = self.call_peptides_main(
                variants=comb, denylist=denylist,
                check_variants=True, check_canonical=True,
                is_mrna_end_nf=donor_tx_model.is_mrna_end_nf()
            )
            self.variant_peptides.update(peptides)

        denylist.update(main_peptides)
        for comb in self.generate_variant_comb(fusion=False, circ_rna=True):
            peptides = self.call_peptides_main(
                variants=comb, denylist=denylist,
                check_variants=True, check_canonical=True,
                is_mrna_end_nf=True
            )
            self.variant_peptides.update(peptides)

def create_mnvs(pool:seqvar.VariantRecordPool, max_adjacent_as_mnv:int
        ) -> seqvar.VariantRecordPool:
    """ Create MNVs """
    for tx_id in pool.data.keys():
        mnvs = seqvar.find_mnvs_from_adjacent_variants(
            pool[tx_id].transcriptional,
            max_adjacent_as_mnv
        )
        pool[tx_id].transcriptional = sorted(pool[tx_id].transcriptional + mnvs)

        mnvs = seqvar.find_mnvs_from_adjacent_variants(
            pool[tx_id].intronic,
            max_adjacent_as_mnv
        )
        pool[tx_id].intronic = sorted(pool[tx_id].intronic + mnvs)
    return pool

def fix_indel_after_start_codon(pool:seqvar.VariantRecordPool,
        ref:params.ReferenceData) -> seqvar.VariantRecordPool:
    """ Fix indel variants that are right after the start codon by shifting
    it to end inclusion to be consistant with callVariant """
    for tx_id in pool.data.keys():
        tx_model = ref.anno.transcripts[tx_id]
        chrom = tx_model.transcript.chrom
        tx_seq = tx_model.get_transcript_sequence(ref.genome[chrom])

        if tx_seq.orf:
            start = tx_seq.orf.start
        else:
            start = 0
        for v in pool[tx_id].transcriptional:
            if v.location.start == start + 2 \
                    and (v.is_insertion() or v.is_deletion()) \
                    and not v.is_fusion() \
                    and not v.is_alternative_splicing():
                v.to_end_inclusion(tx_seq)
    return pool

def main(args):
    """ main """
    # Load genomic references
    anno, genome, proteome = load_references(
        path_anno=args.reference_dir/'annotation.gtf',
        path_genome=args.reference_dir/'genome.fasta',
        path_proteome=args.reference_dir/'proteome.fasta'
    )
    reference_data = params.ReferenceData(
        genome=genome,
        anno=anno,
        proteome=proteome,
        canonical_peptides=None
    )

    # load GVF files
    variant_pool = seqvar.VariantRecordPool()
    variant_pool.anno = reference_data.anno
    for path in args.input_gvf:
        with open(path) as handle:
            variant_pool.load_variants(
                handle=handle,
                anno=reference_data.anno,
                genome=reference_data.genome
            )
    variant_pool = fix_indel_after_start_codon(variant_pool, reference_data)
    variant_pool = create_mnvs(variant_pool, args.max_adjacent_as_mnv)
    variant_peptides:Set[str] = set()
    for tx_id in variant_pool.data.keys():
        caller = BruteForceVariantPeptideCaller()
        caller.variant_pool = variant_pool
        caller.reference_data = reference_data
        caller.tx_id = tx_id
        caller.tx_model = caller.reference_data.anno.transcripts[caller.tx_id]
        caller.tx_seq = caller.tx_model.get_transcript_sequence(
            caller.reference_data.genome[caller.tx_model.transcript.chrom]
        )
        caller.w2f = args.w2f_reassignment
        caller.selenocysteine_termination = args.selenocysteine_termination

        caller.cleavage_params = params.CleavageParams(
            enzyme=args.cleavage_rule,
            exception=args.cleavage_exception,
            miscleavage=int(args.miscleavage),
            min_mw=float(args.min_mw),
            min_length=args.min_length,
            max_length=args.max_length
        )

        caller.create_canonical_peptide_pool()
        caller.get_start_index()

        caller.call_peptides()

        variant_peptides.update(caller.variant_peptides)

    for peptide in sorted(variant_peptides):
        print(peptide, file=sys.stdout)
