# vim: set fileencoding=utf-8 :
#
# Copyright (C) 2012 Daniyal Kazempour
# University of Tuebingen
# Interfaculty Institute of Microbiology and Infection Medicine
# Div. of Microbiology/Biotechnology
#
# Copyright (C) 2012,2013 Kai Blin
# University of Tuebingen
# Interfaculty Institute of Microbiology and Infection Medicine
# Div. of Microbiology/Biotechnology
#
# Copyright (C) 2017 Marnix Medema
# Wageningen University
# Bioinformatics Group
#
# License: GNU Affero General Public License v3 or later
# A copy of GNU AGPL v3 should have been included in this software package in LICENSE.txt.
'''
More detailed lassopeptide analysis using HMMer-based leader peptide
cleavage sites prediction as well as prediction of number of  dissulfide
bridges, molcular mass and macrolactam ring.
'''

import logging
from Bio.SeqFeature import SeqFeature, FeatureLocation
from antismash import utils
from antismash.config import get_config as get_global_config
from antismash.generic_modules.genefinding.all_orfs import Orf, scan_orfs, sort_orfs, get_reverse_complement
import re
from helperlibs.wrappers.io import TemporaryFile
import os
from svm_lasso import svm_classify
from config import get_config as get_lasso_config


class Lassopeptide(object):
    '''
    Class to calculate and store lassopeptide information
    '''
    def __init__(self, start, end, score, rodeo_score):
        self.start = start
        self.end = end
        self.score = score
        self.rodeo_score = rodeo_score
        self._leader = ''
        self._lassotype = 'Class-II'
        self._core = ''
        self._weight = -1
        self._monoisotopic_weight = -1
        self._num_bridges = 0
        self._macrolactam = ''
        self._c_cut = ''


    @property
    def core(self):
        return self._core

    @core.setter
    def core(self, seq):
        self.core_analysis_monoisotopic = utils.RobustProteinAnalysis(seq, monoisotopic=True)
        self.core_analysis = utils.RobustProteinAnalysis(seq, monoisotopic=False)
        self._core = seq

    @property
    def leader(self):
        return self._leader

    @leader.setter
    def leader(self, seq):
        self._leader = seq

    @property
    def c_cut(self):
        return self._c_cut

    @c_cut.setter
    def c_cut(self, ccut):
        self._c_cut = ccut

    def __repr__(self):
        return "Lassopeptide(%s..%s, %s, %r, %r, %s, %s(%s), %s, %s)" % (self.start, self.end, self.score, self._lassotype, self._core, self._num_bridges, self._monoisotopic_weight, self._weight, self._macrolactam, self.c_cut)


    def _calculate_mw(self):
        '''
        (re)calculate the monoisotopic mass and molecular weight
        '''
        if not self._core:
            raise ValueError()

        mol_mass = self.core_analysis.molecular_weight()
            
        CC_mass = 0
        if self._num_bridges != 0:
             CC_mass = 2*self._num_bridges

        # dehydration indicative of cyclization     
        bond = 18.02
        self._weight = mol_mass + CC_mass - bond


        monoisotopic_mass = self.core_analysis_monoisotopic.molecular_weight()
        self._monoisotopic_weight = monoisotopic_mass + CC_mass - bond

        
    @property
    def monoisotopic_mass(self):
        '''
        function determines the weight of the core peptide
        '''
        if self._monoisotopic_weight > -1:
            return self._monoisotopic_weight

        self._calculate_mw()
        return self._monoisotopic_weight

    
    @property
    def molecular_weight(self):
        '''
        function determines the weight of the core peptide
        '''
        if self._weight > -1:
            return self._weight

        self._calculate_mw()
        return self._weight


    @property 
    def macrolactam(self):
        '''
        Predict the lassopeptide macrolactam ring
        '''
        
        if not self._core:
            raise ValueError()
        
        seq = self._core[6:9]
        for res in range(len(seq)):
            if seq[res] in ['E', 'D']:
                self._macrolactam = self._core[0:res+7]
                
        return self._macrolactam

    
    @property
    def number_bridges(self):
        '''
        Predict the lassopeptide number of disulfide bridges
        '''

        aas = self.core_analysis.count_amino_acids()
        if aas['C'] >= 2:
            self._num_bridges = 1
        if aas['C'] >= 4:
            self._num_bridges = 2
        return self._num_bridges

    
    @property
    def lasso_class(self):
        '''
        Predict the lassopeptide class based on disulfide bridges
        '''
        if not self._core:
            raise ValueError()

        if self._num_bridges == 1:
             self._lassotype = 'Class-III'
        if self._num_bridges == 2:
             self._lassotype = 'Class-I'
        return self._lassotype
    

def predict_cleavage_site(query_hmmfile, target_sequence, threshold):
    '''
    Function extracts from HMMER the start position, end position and score 
    of the HMM alignment
    '''
    hmmer_res = utils.run_hmmpfam2(query_hmmfile, target_sequence)
    resvec = [None, None, None]
    for res in hmmer_res:
        for hits in res:
            for hsp in hits:

                # when hmm includes 1st macrolactam residue: end-2
                if hsp.bitscore > threshold:
                    resvec = [hsp.query_start-1, hsp.query_end-1, hsp.bitscore]
                    return resvec
    return resvec

def run_cleavage_site_phmm(fasta, hmmer_profile, threshold):
    """Try to identify cleavage site using pHMM"""
    profile = utils.get_full_path(__file__, hmmer_profile)
    return predict_cleavage_site(profile, fasta, threshold)

def run_cleavage_site_regex(fasta):
    """Try to identify cleavage site using regular expressions"""
    #Regular expressions; try 1 first, then 2, etc.
    rex1 = re.compile('(Y[ARNDBCEQZGHILKMFPSTWYV]{2}P[ARNDBCEQZGHILKMFPSTWYV]L[ARNDBCEQZGHILKMFPSTWYV]{3}G[ARNDBCEQZGHILKMFPSTWYV]{5}T)') 
    rex2 = re.compile('(G[ARNDBCEQZGHILKMFPSTWYV]{5}T)') 
    rex3 = re.compile('(Y[ARNDBCEQZGHILKMFPSTWYV]{2}P[ARNDBCEQZGHILKMFPSTWYV]L)') 
    rex4 = re.compile('(Y[ARNDBCEQZGHILKMFPSTWYV]{2}P)') 

    #For each regular expression, check if there is a match that is <10 AA from the end
    if re.search(rex1,fasta) and len(re.split(rex1,fasta)[-1]) > 14:
        start, end = [m.span() for m in rex1.finditer(fasta)][-1]
        end -= 5
    elif re.search(rex2,fasta) and len(re.split(rex1,fasta)[-1]) > 14:
        start, end = [m.span() for m in rex2.finditer(fasta)][-1]
        end -= 5
    elif re.search(rex3,fasta) and len(re.split(rex1,fasta)[-1]) > 14:
        start, end = [m.span() for m in rex3.finditer(fasta)][-1]
        end += 5
    elif re.search(rex4,fasta) and len(re.split(rex1,fasta)[-1]) > 14:
        start, end = [m.span() for m in rex4.finditer(fasta)][-1]
        end += 7
    else:
        return [None, None, None]

    return start, end, 0

def distance_to_pfam(seq_record, query, hmmer_profiles):
    """Function to check how many nt a gene is away from a gene with one of a list of given Pfams"""
    nt = 40000 #maximum number of nucleotides distance to search
    #Get all CDS features in seq_record
    cds_features = utils.get_cds_features(seq_record)
    #Get all CDS features within <X nt distances
    close_cds_features = []
    distance = {}
    for cds in cds_features:
        if query.location.start - nt <= cds.location.start <= query.location.end + nt or \
           query.location.start - nt <= cds.location.end <= query.location.end + nt:
            close_cds_features.append(cds)
            distance[utils.get_gene_id(cds)] = min([abs(cds.location.start - query.location.end), \
             abs(cds.location.end - query.location.start), abs(cds.location.start - query.location.start), \
             abs(cds.location.end - query.location.end)])
    #For nearby CDS features, check if they have hits to the pHMM
    closest_distance = -1
    for cds in close_cds_features:
        if 'sec_met' in cds.qualifiers:
            for annotation in cds.qualifiers['sec_met']:
                if annotation.startswith('Domains detected: '):
                    for profile in hmmer_profiles:
                        if profile in annotation:
                            if closest_distance == -1 or distance[utils.get_gene_id(cds)] < closest_distance:
                                closest_distance = distance[utils.get_gene_id(cds)]
    return closest_distance


def is_on_same_strand_as(seq_record, cluster, query, profile):
    """Check if a query CDS is on same strand as gene with pHMM hit"""
    cluster_cds_features = utils.get_cluster_cds_features(cluster, seq_record)
    for cds in cluster_cds_features:
        if 'sec_met' in cds.qualifiers:
            for annotation in cds.qualifiers['sec_met']:
                if annotation.startswith('Domains detected: '):
                    if profile in annotation and cds.strand == query.strand:
                        return True
    return False

def acquire_rodeo_heuristics(seq_record, cluster, query, leader, core):
    """Calculate heuristic scores for RODEO"""
    tabs = []
    score = 0
    #Calcd. lasso peptide mass (Da) (with Xs average out)
    core_analysis = utils.RobustProteinAnalysis(core , monoisotopic=True, invalid='average')
    tabs.append(float(core_analysis.molecular_weight()))

    score += 0

    #Distance to any biosynthetic protein (E, B, C)
    hmmer_profiles = ['PF13471', 'PF00733', 'PF05402']
    distance = distance_to_pfam(seq_record, query, hmmer_profiles)
    tabs.append(distance)
    #Within 500 nucleotides of any biosynthetic protein (E, B, C)	+1
    if distance < 500:
        score += 1
        tabs.append(1)
    else:
        tabs.append(0)
    #Within 150 nucleotides of any biosynthetic protein (E, B, C)	+1
    if distance < 150:
        score += 1
        tabs.append(1)
    else:
        tabs.append(0)
    #Greater than 1000 nucleotides from every biosynthetic protein (E, B, C)	-2
    if distance > 1000:
        score -= 2
        tabs.append(1)
    else:
        tabs.append(0)
    #Core region has 2 or 4 Cys residues	+1
    if core.count("C") in [2,4]:
        score += 1
        tabs.append(1)
    else:
        tabs.append(0)
    #Leader region is longer than core region	+2
    if len(leader) > len(core):
        score += 2
        tabs.append(1)
    else:
        tabs.append(0)
    #Core has 7 (Glu) or 8(Glu/Asp) or 9 (Asp) membered ring possible	+1
    if 'E' in core[6:8] or 'D' in core[7:9]:
        score += 1
        tabs.append(1)
    else:
        tabs.append(0)
    #Leader region contains GxxxxxT	+3
    if re.search('(G[ARNDBCEQZGHILKMFPSTWYV]{5}T)', leader) != None:
        score += 3
        tabs.append(1)
    else:
        tabs.append(0)
    #Core starts with G	+2
    if core.startswith("G"):
        score += 2
        tabs.append(1)
    else:
        tabs.append(0)
    #Peptide and lasso cyclase are on same strand	+1
    if is_on_same_strand_as(seq_record, cluster, query, 'PF00733'):
        score += 1
        tabs.append(1)
    else:
        tabs.append(0)
    #Leader/core region length ratio < 2 and > 0.5	+1
    if 0.5 <= float(len(leader)) / len(core) <= 2:
        score += 1
        tabs.append(1)
    else:
        tabs.append(0)
    #Core starts with Cys and has an even number of Cys	0
    if core.startswith("C") and core.count("C") % 2 == 0:
        score += 0
        tabs.append(1)
    else:
        tabs.append(0)
    #Core contains no Gly	-4
    if "G" not in core:
        score -= 4
        tabs.append(1)
    else:
        tabs.append(0)
    #Core has at least one aromatic residue	+1
    if set("FWY") & set(core) > 0:
        score += 1
        tabs.append(1)
    else:
        tabs.append(0)
    #Core has at least 2 aromatic residues	+2
    if sum([core.count(aa) for aa in list("FWY")]) >= 2:
        score += 2
        tabs.append(1)
    else:
        tabs.append(0)
    #Core has odd number of Cys	-2
    if core.count("C") % 2 != 0:
        score -= 2
        tabs.append(1)
    else:
        tabs.append(0)
    #Leader region contains Trp	-1
    if "W" in leader:
        score -= 1
        tabs.append(1)
    else:
        tabs.append(0)
    #Leader region contains Lys	+1
    if "K" in leader:
        score += 1
        tabs.append(1)
    else:
        tabs.append(0)
    #Leader region has Cys	-2
    if "C" in leader:
        score -= 2
        tabs.append(1)
    else:
        tabs.append(0)
    #Gene cluster does not contain PF13471	-2
    if distance_to_pfam(seq_record, query, ['PF13471']) == -1 or \
       distance_to_pfam(seq_record, query, ['PF13471']) > 10000:
        score -= 2
    #Peptide utilizes alternate start codon	-1
    if not str(query.extract(seq_record.seq)).startswith("ATG"):
        score -= 1
    return score, tabs

def identify_lasso_motifs(leader, core):
    """Run FIMO to identify lasso peptide-specific motifs"""
    lasso_dir = os.path.dirname(os.path.abspath(__file__))
    with TemporaryFile() as tempfile:
        out_file = open(tempfile.name, "w")
        out_file.write(">query\n%s%s" % (leader, core))
        out_file.close()
        fimo_output = utils.run_fimo_simple(lasso_dir + os.sep + "lasso_motifs_meme.txt", tempfile.name)
    fimo_motifs = [int(line.partition("\t")[0]) for line in fimo_output.split("\n") if "\t" in line and line.partition("\t")[0].isdigit()]
    fimo_scores = {int(line.split("\t")[0]): float(line.split("\t")[5]) for line in fimo_output.split("\n") if "\t" in line and line.partition("\t")[0].isdigit()}
    #Calculate score
    motif_score = 0
    if 2 in fimo_motifs:
        motif_score += 4
    elif len(fimo_motifs) > 0:
        motif_score += 2
    else:
        motif_score += -1
    return fimo_motifs, motif_score, fimo_scores

def generate_rodeo_svm_csv(seq_record, cluster, query, leader, core, previously_gathered_tabs, fimo_motifs, fimo_scores):
    """Generates all the items for one candidate precursor peptide"""
    columns = []
    #Precursor Index
    columns.append(1)
    #classification
    columns.append(0)
    columns += previously_gathered_tabs
    #Cluster has PF00733?
    if distance_to_pfam(seq_record, query, ['PF00733']) == -1 or \
       distance_to_pfam(seq_record, query, ['PF00733']) > 10000:
        columns.append(0)
    else:
        columns.append(1)
    #Cluster has PF05402?
    if distance_to_pfam(seq_record, query, ['PF05402']) == -1 or \
       distance_to_pfam(seq_record, query, ['PF05402']) > 10000:
        columns.append(0)
    else:
        columns.append(1)
    #Cluster has PF13471?
    if distance_to_pfam(seq_record, query, ['PF13471']) == -1 or \
       distance_to_pfam(seq_record, query, ['PF13471']) > 10000:
        columns.append(0)
    else:
        columns.append(1)
    #Leader has LxxxxxT motif?
    if re.search('(L[ARNDBCEQZGHILKMFPSTWYV]{5}T)', leader) != None:
        columns.append(1)
    else:
        columns.append(0)
    #Core has adjacent identical aas (doubles)?
    if len([core[idx] for idx in range(len(core)) if len(core) > idx+1 and core[idx+1] == core[idx]]) > 0:
        columns.append(1)
    else:
        columns.append(0)
    #Core length (aa)
    columns.append(len(core))
    #Leader length (aa)
    columns.append(len(leader))
    #Precursor length (aa)
    columns.append(len(leader) + len(core))
    #Leader/core ratio
    columns.append(float(len(core)) / float(len(leader)))
    #Number of Pro in first 9 aa of core?
    columns.append(core[:9].count("P"))
    #Estimated core charge
    charge_dict = {"E": -1, "D": -1, "K": 1, "H": 1, "R": 1}
    columns.append(sum([charge_dict[aa] for aa in core if aa in charge_dict]))
    #Estimated leader charge
    columns.append(sum([charge_dict[aa] for aa in leader if aa in charge_dict]))
    #Estimated precursor charge
    columns.append(sum([charge_dict[aa] for aa in leader+core if aa in charge_dict]))
    #Absolute value of core charge
    columns.append(abs(sum([charge_dict[aa] for aa in core if aa in charge_dict])))
    #Absolute value of leader charge
    columns.append(abs(sum([charge_dict[aa] for aa in leader if aa in charge_dict])))
    #Absolute value of precursor charge
    columns.append(abs(sum([charge_dict[aa] for aa in leader+core if aa in charge_dict])))
    #Counts of AAs in leader
    columns += [leader.count(aa) for aa in "ARDNCQEGHILKMFPSTWYV"]
    #Aromatics in leader
    columns.append(sum([leader.count(aa) for aa in "FWY"]))
    #Neg charged in leader
    columns.append(sum([leader.count(aa) for aa in "DE"]))
    #Pos charged in leader
    columns.append(sum([leader.count(aa) for aa in "RK"]))
    #Charged in leader
    columns.append(sum([leader.count(aa) for aa in "RKDE"]))
    #Aliphatic in leader
    columns.append(sum([leader.count(aa) for aa in "GAVLMI"]))
    #Hydroxyl in leader
    columns.append(sum([leader.count(aa) for aa in "ST"]))
    #Counts of AAs in core
    columns += [core.count(aa) for aa in "ARDNCQEGHILKMFPSTWYV"]
    #Aromatics in core
    columns.append(sum([core.count(aa) for aa in "FWY"]))
    #Neg charged in core
    columns.append(sum([core.count(aa) for aa in "DE"]))
    #Pos charged in core
    columns.append(sum([core.count(aa) for aa in "RK"]))
    #Charged in core
    columns.append(sum([core.count(aa) for aa in "RKDE"]))
    #Aliphatic in core
    columns.append(sum([core.count(aa) for aa in "GAVLMI"]))
    #Hydroxyl in core
    columns.append(sum([core.count(aa) for aa in "ST"]))
    #Counts (0 or 1) of amino acids within first AA position of core sequence
    columns += [core[0].count(aa) for aa in "ARDNCQEGHILKMFPSTWYV"]
    #Counts of AAs in leader+core
    precursor = leader + core
    columns += [precursor.count(aa) for aa in "ARDNCQEGHILKMFPSTWYV"] #Temp to work with current training CSV
    #Aromatics in precursor
    columns.append(sum([precursor.count(aa) for aa in "FWY"]))
    #Neg charged in precursor
    columns.append(sum([precursor.count(aa) for aa in "DE"]))
    #Pos charged in precursor
    columns.append(sum([precursor.count(aa) for aa in "RK"]))
    #Charged in precursor
    columns.append(sum([precursor.count(aa) for aa in "RKDE"]))
    #Aliphatic in precursor
    columns.append(sum([precursor.count(aa) for aa in "GAVLMI"]))
    #Hydroxyl in precursor
    columns.append(sum([precursor.count(aa) for aa in "ST"]))
    #Motifs
    columns += [1 if motif in fimo_motifs else 0 for motif in range(1, 17)]
    #Total motifs hit
    columns.append(len(fimo_motifs))
    #Motif scores
    columns += [fimo_scores[motif] if motif in fimo_motifs else 0 for motif in range(1, 17)]
    #Sum of MEME scores
    columns.append(sum([fimo_scores[motif] if motif in fimo_motifs else 0 for motif in range(1, 17)]))
    #No Motifs?
    if len(fimo_motifs) == 0:
        columns.append(1)
    else:
        columns.append(0)
    #Alternate Start Codon?
    if not str(query.extract(seq_record.seq)).startswith("ATG"):
        columns.append(1)
    else:
        columns.append(0)
    return columns

def run_rodeo_svm(csv_columns):
    """Run RODEO SVM"""
    input_training_file = utils.get_full_path(__file__, 'svm_lasso' + os.sep + 'training_set.csv')         # the CSV containing the training set
    with TemporaryFile() as input_fitting_file:
        out_file = open(input_fitting_file.name, "w")
        out_file.write('Precursor Index,classification,Distance,Within 500 nt?,Within 150 nt?,Further than 1000 nt?,Core has 2 or 4 Cys?,Leader longer than core?,Plausible lasso ring?,Leader has GxxxxxT motif?,Core starts with G?,Core and BGC in same direction?,Raito leader/core < 2 and > 0.5,Core starts with Cys and even number of Cys?,No Gly in core?,Core has at least 1 aromatic aa?,Core has at least 2 aromatic aa?,Core has odd number of Cys?,Leader has Trp?,Leader has Lys?,Leader has Cys?,Cluster has PF00733?,Cluster has PF05402?,Cluster has PF13471?,Leader has LxxxxxT motif?,Core has adjacent identical aas (doubles)?,Core length (aa),Leader length (aa),Precursor length (aa),Leader/core ratio,Number of Pro in first 9 aa of core?,Estimated core charge,Estimated leader charge,Estimated precursor charge,Absolute value of core charge,Absolute value of leader charge,Absolute value of precursor charge,A,R,D,N,C,Q,E,G,H,I,L,K,M,F,P,S,T,W,Y,V,Aromatics,Neg charged,Pos charged,Charged,Aliphatic,Hydroxyl,A,R,D,N,C,Q,E,G,H,I,L,K,M,F,P,S,T,W,Y,V,Aromatics,Neg charged,Pos charged,Charged,Aliphatic,Hydroxyl,A,R,D,N,C,Q,E,G,H,I,L,K,M,F,P,S,T,W,Y,V,Aromatics,Neg charged,Pos charged,Charged,Aliphatic,Hydroxyl,Motif1?,Motif2?,Motif3?,Motif4?,Motif5?,Motif6?,Motif7?,Motif8?,Motif9?,Motif10?,Motif11?,Motif12?,Motif13?,Motif14?,Motif15?,Motif16?,Total motifs hit,"Score, Motif1","Score, Motif2","Score, Motif3","Score, Motif4","Score, Motif5","Score, Motif6","Score, Motif7","Score, Motif8","Score, Motif9","Score, Motif10","Score, Motif11","Score, Motif12","Score, Motif13","Score, Motif14","Score, Motif15","Score, Motif16",Sum of MEME scores,No Motifs?,Alternate Start Codon?\n')
        out_file.write(",".join([str(item) for item in csv_columns]))
        out_file.close()
        with TemporaryFile() as output_filename:
            svm_classify.classify_peptide(input_training_file, input_fitting_file.name, output_filename.name)
            output = open(output_filename.name, "r").read()
            if output.strip().partition(",")[2] == "1":
                return 10
            else:
                return 0

def run_rodeo(seq_record, cluster, query, leader, core):
    """Run RODEO heuristics + SVM to assess precursor peptide candidate"""
    rodeo_score = 0
    
    #Incorporate heuristic scores
    heuristic_score, gathered_tabs_for_csv = acquire_rodeo_heuristics(seq_record, cluster, query, leader, core)
    rodeo_score += heuristic_score

    fimo_motifs = []
    fimo_scores = {}
    motif_score = 0

    if not get_global_config().without_fimo and get_lasso_config().fimo_present:
        # Incorporate motif scores
        fimo_motifs, motif_score, fimo_scores = identify_lasso_motifs(leader, core)
    rodeo_score += motif_score

    #Incorporate SVM scores
    csv_columns = generate_rodeo_svm_csv(seq_record, cluster, query, leader, core, gathered_tabs_for_csv, fimo_motifs, fimo_scores)
    rodeo_score +=  run_rodeo_svm(csv_columns)

    if rodeo_score >= 15:
        return True, rodeo_score
    else:
        return False, rodeo_score


def determine_precursor_peptide_candidate(seq_record, cluster, query, query_sequence):
    """Identify precursor peptide candidates and split into two"""

    #Skip sequences with >100 AA
    if len(query_sequence) > 100 or len(query_sequence) < 20:
        return

    #Create FASTA sequence for feature under study
    lasso_a_fasta = ">%s\n%s" % (utils.get_gene_id(query), query_sequence)

    #Run sequence against pHMM; if positive, parse into a vector containing START, END and SCORE
    start, end, score = run_cleavage_site_phmm(lasso_a_fasta, 'precursor_2637.hmm', -20.00)

    #If no pHMM hit, try regular expression
    if score is None:
        start, end, score = run_cleavage_site_regex(lasso_a_fasta)
        if score is None or end > len(query_sequence) - 3:
            start, end, score = 0, len(query_sequence)/2 - 5, 0

    #Run RODEO to assess whether candidate precursor peptide is judged real
    rodeo_result = run_rodeo(seq_record, cluster, query, query_sequence[:end], query_sequence[end:])
    if rodeo_result[0] is False:
        #logging.debug('%r: No cleavage site predicted' % utils.get_gene_id(query))
        return
    else:
        lasso_peptide = Lassopeptide(start, end + 1, score, rodeo_result[1])

    #Determine the leader and core peptide
    lasso_peptide.leader = query_sequence[:end]
    lasso_peptide.core = query_sequence[end:]

    return lasso_peptide


def find_all_orfs(seq_record, cluster):
    """Find all ORFs in gene cluster outside annotated CDS features"""
    # Get sequence just for the gene cluster
    fasta_seq = seq_record.seq[cluster.location.start:cluster.location.end]

    # Find orfs throughout the cluster
    forward_matches = scan_orfs(fasta_seq, 1, cluster.location.start)
    reverse_matches = scan_orfs(get_reverse_complement(fasta_seq), -1, cluster.location.start)
    all_orfs = forward_matches + reverse_matches

    orfnr = 1
    cluster_cds = utils.get_cluster_cds_features(cluster, seq_record)
    new_features = []

    for orf in sort_orfs(all_orfs):
        # Remove if overlaps with existing CDSs
        skip = False
        for cds in cluster_cds:
            if cds.location.start <= orf.start <= cds.location.end or \
               cds.location.start <= orf.stop <= cds.location.end or \
               orf.start <= cds.location.start <= orf.stop or \
               orf.start <= cds.location.end <= orf.stop:
                skip = True
                break
        if skip:
            continue
        loc = FeatureLocation(orf.start, orf.stop, strand=orf.direction)
        feature = SeqFeature(location=loc, id=str(orf), type="CDS",
                    qualifiers={'locus_tag': ['cluster_%s_allorf%s%s' % (utils.get_cluster_number(cluster), "0" * (3 - len(str(orfnr))), str(orfnr))]})
        feature.qualifiers['note'] = ["auto-all-orf"]
        feature.qualifiers['translation'] = [str(utils.get_aa_translation(seq_record, feature))]
        new_features.append(feature)
        orfnr += 1

    return new_features


def run_lassopred(seq_record, cluster, query):
    """General function to predict and analyse lasso peptides"""

    query_sequence = utils.get_aa_sequence(query, to_stop=True)

    #Run checks to determine whether an ORF encodes a precursor peptide
    result = determine_precursor_peptide_candidate(seq_record, cluster, query, query_sequence)
    if result is None:
        return

    #prediction of cleavage in C-terminal based on lasso's core sequence
    C_term_hmmer_profile = 'tail_cut.hmm'
    thresh_C_hit = -7.5

    aux = result.core[(len(result.core)/2):]
    core_a_fasta = ">%s\n%s" % (utils.get_gene_id(query),aux)
 
    profile_C = utils.get_full_path(__file__, C_term_hmmer_profile)
    hmmer_res_C = utils.run_hmmpfam2(profile_C, core_a_fasta)

    for res in hmmer_res_C:
        for hits in res:
            for seq in hits:
                if seq.bitscore > thresh_C_hit:
                    result.c_cut = aux[seq.query_start+1:]        
                    

    if result is None:
        logging.debug('%r: No C-terminal cleavage site predicted' % utils.get_gene_id(query))
        return


    if not 'sec_met' in query.qualifiers:
        query.qualifiers['sec_met'] = []


    if ";".join(query.qualifiers['sec_met']).find(';Kind: biosynthetic') < 0:
        query.qualifiers['sec_met'].append('Kind: biosynthetic')
        

    return result


def result_vec_to_features(orig_feature, res_vec):
    start = orig_feature.location.start
    end = orig_feature.location.start + (res_vec.end * 3)
    strand = orig_feature.location.strand
    loc = FeatureLocation(start, end, strand=strand)
    leader_feature = SeqFeature(loc, type='CDS_motif')
    leader_feature.qualifiers['note'] = ['leader peptide']
    leader_feature.qualifiers['note'].append('lassopeptide')
    leader_feature.qualifiers['note'].append('predicted leader seq: %s' % res_vec.leader)
    leader_feature.qualifiers['locus_tag'] = [ utils.get_gene_id(orig_feature) ]
    start = end
    end = orig_feature.location.end
    loc = FeatureLocation(start, end, strand=strand)
    core_feature = SeqFeature(loc, type='CDS_motif')
    core_feature.qualifiers['note'] = ['core peptide']
    core_feature.qualifiers['note'].append('lassopeptide')
    core_feature.qualifiers['note'].append('monoisotopic mass: %0.1f' % res_vec.monoisotopic_mass)
    core_feature.qualifiers['note'].append('molecular weight: %0.1f' % res_vec.molecular_weight)
      
    #resets .. to recalculate weights considering C-terminal putative cut
    res_vec._weight = -1
    res_vec._monoisotopic_weight = -1
    oldcore = res_vec.core
    res_vec.core = oldcore[:(len(res_vec.core)-len(res_vec.c_cut))]

    core_feature.qualifiers['note'].append('monoisotopic mass_cut: %0.1f' % res_vec.monoisotopic_mass)
    core_feature.qualifiers['note'].append('molecular weight_cut: %0.1f' % res_vec.molecular_weight)
    
            
    core_feature.qualifiers['note'].append('number of bridges: %s' % res_vec.number_bridges)
    core_feature.qualifiers['note'].append('predicted core seq: %s' % res_vec.core)
    core_feature.qualifiers['note'].append('predicted class: %s' % res_vec.lasso_class)
    core_feature.qualifiers['note'].append('score: %0.2f' % res_vec.score)
    core_feature.qualifiers['note'].append('RODEO score: %i' % res_vec.rodeo_score)
    core_feature.qualifiers['note'].append('macrolactam: %s' %res_vec.macrolactam)
    core_feature.qualifiers['note'].append('putative cleaved off residues: %s' %res_vec.c_cut)
    core_feature.qualifiers['locus_tag'] = [ utils.get_gene_id(orig_feature) ]

    return [leader_feature, core_feature]


def specific_analysis(seq_record, options):

    clusters = utils.get_cluster_features(seq_record)
    for cluster in clusters:
        if 'product' not in cluster.qualifiers or \
           'lassopeptide' not in cluster.qualifiers['product'][0]:
            continue
        
        # Find candidate ORFs that are not yet annotated
        new_orfs = find_all_orfs(seq_record, cluster)
        
        # Get all CDS features to evaluate for RiPP-likeness, including the new features
        lasso_fs = utils.get_cluster_cds_features(cluster, seq_record) + new_orfs
      
        #Evaluate each candidate precursor peptide
        for lasso_f in lasso_fs:
            result_vec = run_lassopred(seq_record, cluster, lasso_f)
            if result_vec is None:
                continue

            new_features = result_vec_to_features(lasso_f, result_vec)
            if "allorf" in utils.get_gene_id(lasso_f):
                new_features.append(lasso_f)
            seq_record.features.extend(new_features)
