# 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 lantipeptide analysis using HMMer-based leader peptide
cleavage site prediction as well as prediction of number of lanthionine
bridges and molcular mass.
'''

import logging
from Bio.SeqFeature import SeqFeature, FeatureLocation
from antismash.generic_modules.genefinding.all_orfs import Orf, scan_orfs, sort_orfs, get_reverse_complement
from antismash.specific_modules.lassopeptides.specific_analysis import distance_to_pfam, find_all_orfs
from antismash.generic_modules.hmm_detection import HmmSignature
import re
import os
from helperlibs.wrappers.io import TemporaryFile
from antismash import utils
from antismash.config import get_config as get_global_config
from svm_lanthi import svm_classify
from config import get_config as get_lanthi_config

known_precursor_domains = (
    'Antimicr18',
    'Gallidermin',
    'L_biotic_A',
    'lacticin_l',
    'leader_d',
    'leader_abc',
    'leader_eh',
    'mature_ha',
    'lacticin_mat',
    'mature_b',
    'mature_d',
    'mature_ab',
    'mature_a',
    'TIGR03731',
    'LD_lanti_pre',
    'strep_PEQAXS',
)

class Lantipeptide(object):
    '''
    Class to calculate and store lantipeptide information
    '''
    def __init__(self, start, end, score, rodeo_score, lantype):
        self.start = start
        self.end = end
        self.score = score
        self.rodeo_score = rodeo_score
        self.lantype = lantype
        self._leader = ''
        self._core = ''
        self._weight = -1
        self._monoisotopic_weight = -1
        self._alt_weights = []
        self._lan_bridges = -1
        self._aminovinyl = False
        self._chlorinated = False
        self._oxygenated = False
        self._lac = False

    @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

    def __repr__(self):
        return "Lantipeptide(%s..%s, %s, %r, %r, %s, %s(%s))" % (self.start, self.end, self.score, self.lantype, self._core, self._lan_bridges, self._monoisotopic_weight, self._weight)

    @property
    def number_of_lan_bridges(self):
        '''
        function determines the number of lanthionine bridges in the core peptide
        '''
        if self._lan_bridges > -1:
            return self._lan_bridges

        aas = self.core_analysis.count_amino_acids()
        no_cys = aas['C']
        no_thr_ser = aas['T'] + aas['S']
        self._lan_bridges = min(no_cys, no_thr_ser)
        if self._aminovinyl:
            self._lan_bridges -= 1
        return self._lan_bridges

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

        aas = self.core_analysis.count_amino_acids()
        no_thr_ser = aas['T'] + aas['S']

        mol_mass = self.core_analysis.molecular_weight()
        mods = 18.02 * no_thr_ser
        if self._aminovinyl:
            mods += 46
        if self._chlorinated:
            mods -= 34
        if self._oxygenated:
            mods -= 16
        if self._lac:
            mods -= 2
        self._weight = mol_mass - mods

        # every unbridged Ser or Thr might not be dehydrated
        self._alt_weights = []
        for i in range(1, no_thr_ser - aas['C'] + 1):
            self._alt_weights.append(self._weight + 18.02 * i)

        monoisotopic_mass = self.core_analysis_monoisotopic.molecular_weight()
        mods = 18 * no_thr_ser
        if self._aminovinyl:
            mods += 46
        if self._chlorinated:
            mods -= 34
        if self._oxygenated:
            mods -= 16
        if self._lac:
            mods -= 2
        self._monoisotopic_weight = monoisotopic_mass - mods

    @property
    def monoisotopic_mass(self):
        '''
        function determines the weight of the core peptide and substracts
        the weight which is reduced, due to dehydratation
        '''
        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 and substracts
        the weight which is reduced, due to dehydratation
        '''
        if self._weight > -1:
            return self._weight

        self._calculate_mw()
        return self._weight

    @property
    def alternative_weights(self):
        '''
        function determines the possible alternative weights assuming one or
        more of the Ser/Thr residues aren't dehydrated
        '''
        if self._alt_weights != []:
            return self._alt_weights

        self._calculate_mw()
        return self._alt_weights

    @property
    def aminovinyl_group(self):
        '''
        Check if lantipeptide contains an aminovinyl group
        '''
        return self._aminovinyl

    @aminovinyl_group.setter
    def aminovinyl_group(self, value):
        '''
        Define if lantipeptide contains an aminovinyl group and trigger
        recalculation of the molecular weight if needed
        '''
        self._aminovinyl = value
        if self._core:
            self._calculate_mw()
            # recalculate the number of lan bridges
            self._lan_bridges = -1

    @property
    def chlorinated(self):
        '''
        Check if lantipeptide is chlorinated
        '''
        return self._chlorinated

    @chlorinated.setter
    def chlorinated(self, value):
        '''
        Define if lantipeptide is chlorinated and trigger
        recalculation of the molecular weight if needed
        '''
        self._chlorinated = value
        if self._core:
            self._calculate_mw()

    @property
    def oxygenated(self):
        '''
        Check if lantipeptide is oxygenated
        '''
        return self._oxygenated

    @oxygenated.setter
    def oxygenated(self, value):
        '''
        Define if lantipeptide is oxygenated and trigger
        recalculation of the molecular weight if needed
        '''
        self._oxygenated = value
        if self._core:
            self._calculate_mw()

    @property
    def lactonated(self):
        '''
        Check if lantipeptide starts with a lactone
        '''
        return self._lac

    @lactonated.setter
    def lactonated(self, value):
        self._lac = value
        if self._core:
            self._calculate_mw()


class CleavageSiteHit(object):
    def __init__(self, start, end, score, lantype):
        self.start = start
        self.end = end
        self.score = score
        self.lantype = lantype


def get_detected_domains(seq_record, cluster):
    
    found_domains = []
    #Gather biosynthetic domains
    for feature in utils.get_cluster_cds_features(cluster, seq_record):
      
        if not 'sec_met' in feature.qualifiers:
            continue

        for entry in feature.qualifiers['sec_met']:
            if entry.startswith('Domains detected:'):
                entry = entry[17:]
                domains = entry.split(';')
                for domain in domains:
                    found_domains.append(domain.split()[0])

    #Gather non-biosynthetic domains
    cluster_features = utils.get_cluster_cds_features(cluster, seq_record)
    cluster_fasta = utils.get_specific_multifasta(cluster_features)
    non_biosynthetic_hmms_by_id = run_non_biosynthetic_phmms(cluster_fasta)
    non_biosynthetic_hmms_found = []
    for id in non_biosynthetic_hmms_by_id.keys():
        hsps_found_for_this_id = non_biosynthetic_hmms_by_id[id]
        for hsp in hsps_found_for_this_id:
            if hsp.query_id not in non_biosynthetic_hmms_found:
                non_biosynthetic_hmms_found.append(hsp.query_id)
    found_domains += non_biosynthetic_hmms_found

    return found_domains

def run_non_biosynthetic_phmms(fasta):
    """Try to identify cleavage site using pHMM"""
    hmmdetails = [line.split("\t") for line in open(utils.get_full_path(__file__, "non_biosyn_hmms" + os.sep + "hmmdetails.txt"),"r").read().split("\n") if line.count("\t") == 3]
    _signature_profiles = [HmmSignature(details[0], details[1], int(details[2]), details[3]) for details in hmmdetails]
    non_biosynthetic_hmms_by_id = {}
    for sig in _signature_profiles:
        sig.path = utils.get_full_path(__file__, "non_biosyn_hmms" + os.sep + sig.path.rpartition(os.sep)[2])
        runresults = utils.run_hmmsearch(sig.path, fasta)
        for runresult in runresults:
            #Store result if it is above cut-off
            for hsp in runresult.hsps:
                if hsp.bitscore > sig.cutoff:
                    if not non_biosynthetic_hmms_by_id.has_key(hsp.hit_id):
                        non_biosynthetic_hmms_by_id[hsp.hit_id] = [hsp]
                    else:
                        non_biosynthetic_hmms_by_id[hsp.hit_id].append(hsp)
    return non_biosynthetic_hmms_by_id

def cds_has_domain(cds, query_domain):
    """Function to test whether a cds has a certain domain"""
    if not 'sec_met' in cds.qualifiers:
        return False
    for entry in cds.qualifiers['sec_met']:
        if 'Domains detected:' in entry:
            entry = entry.partition('Domains detected: ')[2]
            domains = entry.split(';')
            for domain in domains:
                domain = domain.split()[0]
                if domain == query_domain:
                    return True
    return False

def predict_cleavage_site(query_hmmfile, target_sequence, threshold=-100):
    '''
    Function extracts from HMMER the start position, end position and score 
    of the HMM alignment
    '''
    hmmer_res = utils.run_hmmpfam2(query_hmmfile, target_sequence)
    
    for res in hmmer_res:
        for hits in res:
            lanti_type = hits.description
            for hsp in hits:
                if hsp.bitscore > threshold:
                    return CleavageSiteHit(hsp.query_start - 1, hsp.query_end, hsp.bitscore, lanti_type)

    return None


def predict_class_from_gene_cluster(seq_record, cluster):
    '''
    Predict the lantipeptide class from the gene cluster
    '''
    found_domains = []
    for feature in utils.get_cds_features(seq_record):
        if feature.location.start < cluster.location.start or \
           feature.location.end > cluster.location.end:
            continue

        if not 'sec_met' in feature.qualifiers:
            continue

        for entry in feature.qualifiers['sec_met']:
            if entry.startswith('Domains detected:'):
                entry = entry[17:]
                domains = entry.split(';')
                for domain in domains:
                    found_domains.append(domain.split()[0])

    if 'Lant_dehyd_N' in found_domains or 'Lant_dehyd_C' in found_domains:
        return 'Class-I'
    if 'DUF4135' in found_domains:
        return 'Class-II'
    if 'Pkinase' in found_domains:
        # this could be class 3 or class 4, but as nobody has seen class 4
        # in vivo yet, we'll ignore that
        return 'Class-III'

    # Ok, no biosynthetic enzymes found, let's try the prepeptide
    if 'Gallidermin' in found_domains:
        return 'Class-I'

    return None


thresh_dict = {
        'Class-I' : -15,
        'Class-II' : -7.3,
        'Class-III' : 3.5,
    }

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 identify_lanthi_motifs(leader, core):
    """Run FIMO to identify lanthipeptide-specific motifs"""
    lanthi_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(lanthi_dir + os.sep + "lanthi_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()}
    return fimo_motifs, fimo_scores

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('F?LD')
    rex2 = re.compile('[LF]?LQ')

    #For 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]) > 10:
        start, end = [m.span() for m in rex1.finditer(fasta)][-1]
        end += 16
    elif re.search(rex2,fasta) and len(re.split(rex2,fasta)[-1]) > 10:
        start, end = [m.span() for m in rex2.finditer(fasta)][-1]
        end += 15
    else:
        return [None, None, None]
    return start, end, 0

def run_rodeo_svm(csv_columns):
    """Run RODEO SVM"""
    input_training_file = utils.get_full_path(__file__, 'svm_lanthi' + 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('PK,Classification,F?LD,S????C,T????C,S?????C,T?????C,Within 500 nt?,Cluster contains PF04738,Cluster contains PF05147,Cluster LACKS PF04738,Cluster LACKS PF05147,Cluster contains PF14028,Cluster contains PF00082,Cluster contains PF03412,Cluster contains PF00005,Cluster contains PF02624,Cluster contains PF00899,Cluster contains PF02052,Cluster contains PF08130,Precursor mass < 4000,Core mass < 2000,Peptide hits cl03420 (Gallidermin),Peptide hits TIGR03731 (lantibio_gallid),Peptide hits cl22812 (lanti_SCO0268),Peptide hits TIGR04363 (LD_lanti_pre),Peptide hits cl06940 (Antimicrobial18),Peptide hits PF02052 (Gallidermin),Peptide hits PF08130 (Antimicrobial18),Precursor peptide mass (unmodified),Leader peptide mass (unmodified),Core peptide mass (unmodified),Length of Leader,Length of Core,Length of precursor,Leader / core ratio,Core >= 35,Has repeating C motifs (not in last 3 residues),Leader > 4 neg charge motifs,Leader net neg charge,Leader FxLD,C-terminal CC,core DGCGxTC motif,core SFNS motif,core SxxLC motif,core CTxGC motif,core TPGC motif,core SFNS?C,Core Cys <3,Core Cys <2,No Core Cys residues,No Core Ser residues,No Core Thr residues,LS max >4,LS max <3,LS 4-membered ring >2,LS 5-membered ring >2,LS 6-membered ring,LS 7-membered ring,LS 8-membered ring,MEME/FIMO motif,LS max ring number,LS lan4,LS lan5,LS lan6,LS lan7,LS lan8,Ratio of Cys to sum of Ser/Thr,Ratio of Cys/Ser/Thr to len of core,Log10 MEME/FIMO score,log10 MEME motif 1,MEME motif 2,MEME motif 3,MEME motif 4,MEME motif 5,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\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, domains):
    """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, domains)
    rodeo_score += heuristic_score

    fimo_motifs = []
    fimo_scores = {}

    if not get_global_config().without_fimo and get_lanthi_config().fimo_present:
        # Find motifs
        fimo_motifs, fimo_scores = identify_lanthi_motifs(leader, core)

    #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 >= 14:
        return True, rodeo_score
    else:
        return False, rodeo_score

def lanscout(seq):
    #nis = 'ITSISLCTPGCKTGALMGCNMKTATCHCSIHVSK'
    #define lanthionine ring with a c-terminal Cys
    lanlower = 2
    lanupper = 6
    lan = '(?=([T|S].{%d,%d}C))' % (lanlower, lanupper)
    db = []
    strings = []
    numringlist = []
    seq = [num for elem in seq for num in elem]
    for n in range(0, len(seq)):
        core = str(seq[n][:])
        strings.append(core)
        totrings = re.compile(lan, re.I).findall(core)
        size = []
        for i in range(0, len(totrings)):
            size.append(len(totrings[i]))
        db.append(size)
        numrings = len(totrings)
        numringlist.append(numrings)

    profile = []
    for i in range(0,len(db)):
        #print db[i]
        temp = []
        for j in range(lanlower+2,lanupper+3):
            temp.append(db[i].count(j))
        profile.append(temp)

    for i in range(0,len(profile)):
        #profile[i] = str(profile[i])
        profile[i]=str(profile[i]).strip('[]')
    return numringlist, strings, profile

def acquire_rodeo_heuristics(seq_record, cluster, query, leader, core, domains):
    """Calculate heuristic scores for RODEO"""
    tabs = []
    score = 0
    precursor = leader + core
    #Leader peptide contains FxLD motif
    if re.search('F[ARNDBCEQZGHILKMFPSTWYV]LD', leader) != None:
        score += 2
        tabs.append(1)
    else:
        tabs.append(0)
    #Core residue position of Sx4C motif
    if re.search('S[ARNDBCEQZGHILKMFPSTWYV]{4}C', core) != None:
        tabs.append(re.search('S[ARNDBCEQZGHILKMFPSTWYV]{4}C', core).span()[0])
    else:
        tabs.append(0)
    #Core residue position of Tx4C motif
    if re.search('T[ARNDBCEQZGHILKMFPSTWYV]{4}C', core) != None:
        tabs.append(re.search('T[ARNDBCEQZGHILKMFPSTWYV]{4}C', core).span()[0])
    else:
        tabs.append(0)
    #Core residue position of Sx5C motif
    if re.search('S[ARNDBCEQZGHILKMFPSTWYV]{5}C', core) != None:
        tabs.append(re.search('S[ARNDBCEQZGHILKMFPSTWYV]{5}C', core).span()[0])
    else:
        tabs.append(0)
    #Core residue position of Tx5C motif
    if re.search('T[ARNDBCEQZGHILKMFPSTWYV]{5}C', core) != None:
        tabs.append(re.search('T[ARNDBCEQZGHILKMFPSTWYV]{5}C', core).span()[0])
    else:
        tabs.append(0)
    #Precursor is within 500 nt?
    hmmer_profiles = ['LANC_like', 'Lant_dehyd_C']
    distance = distance_to_pfam(seq_record, query, hmmer_profiles)
    if distance < 500:
        score += 1
        tabs.append(1)
    else:
        tabs.append(0)
    #Cluster contains LanB dehydratase domain (PF04738)
    if "Lant_dehyd_C" in domains:
        score += 2
        tabs.append(1)
    else:
        tabs.append(0)
    #Cluster contains Lan C cyclase domain (PF05147)
    if "LANC_like" in domains:
        score += 2
        tabs.append(1)
    else:
        tabs.append(0)
    #Cluster LACKS LanB dehydratase domain (PF04738)
    if "Lant_dehyd_C" not in domains:
        score -= 2
        tabs.append(1)
    else:
        tabs.append(0)
    #Cluster LACKS Lan C cyclase domain (PF05147)
    if "LANC_like" not in domains:
        score -= 2
        tabs.append(1)
    else:
        tabs.append(0)
    #Cluster contains LanB dehydratase elimination C-terminal domain (PF14028)
    if "PF14028" in domains:
        score += 2
        tabs.append(1)
    else:
        tabs.append(0)
    #Cluster contains S8 peptidase subtilase (PF00082)
    if "Peptidase_S8" in domains:
        score += 1
        tabs.append(1)
    else:
        tabs.append(0)
    #Cluster contains C39 peptidase (PF03412)
    if "Peptidase_C39" in domains:
        score += 1
        tabs.append(1)
    else:
        tabs.append(0)
    #Cluster contains ABC transporter (PF00005)
    if "PF00005" in domains:
        score += 1
        tabs.append(1)
    else:
        tabs.append(0)
    #Cluster contains YcaO-like protein (PF02624)
    if "YcaO" in domains:
        score -= 4
        tabs.append(1)
    else:
        tabs.append(0)
    #Cluster contains ThiF-like protein (PF00899)
    if "ThiF" in domains:
        score -= 4
        tabs.append(1)
    else:
        tabs.append(0)
    #Cluster contains PF02052 (Gallidermin)
    if "Gallidermin" in domains or "mature_a" in domains or "mature_b" in domains or "matura_ab" in domains:
        tabs.append(1)
    else:
        tabs.append(0)
    #Cluster contains PF8130
    if "Antimicr18" in domains:
        tabs.append(1)
    else:
        tabs.append(0)
    #Precursor peptide mass < 4000 Da
    precursor_analysis = utils.RobustProteinAnalysis(precursor, monoisotopic=True, invalid='average')
    if precursor_analysis.molecular_weight() < 4000:
        score -= 3
        tabs.append(1)
    else:
        tabs.append(0)
    #Core peptide mass < 2000 Da
    core_analysis = utils.RobustProteinAnalysis(core, monoisotopic=True, invalid='average')
    if core_analysis.molecular_weight() < 2000:
        score -= 3
        tabs.append(1)
    else:
        tabs.append(0)
    # Precursor peptide pHMMs below:
    precursor_hit = False
    #Precursor peptide hits gallidermin superfamily (cl03420) HMM
    if cds_has_domain(query, "TIGR03731") or cds_has_domain(query, "Gallidermin"):
        precursor_hit = True
        tabs.append(1)
    else:
        tabs.append(0)
    #Precursor peptide hits lantibio_gallid (TIGR03731) HMM
    if cds_has_domain(query, "TIGR03731"):
        precursor_hit = True
        tabs.append(1)
    else:
        tabs.append(0)
    #Precursor peptide hits lanti_SCO0268 superfamily (cl22812) HMM
    if cds_has_domain(query, "TIGR04451") or cds_has_domain(query, "strep_PEQAXS"):
        precursor_hit = True
        tabs.append(1)
    else:
        tabs.append(0)
    #Precursor peptide hits LD_lanti_pre (TIGR04363) HMM
    if cds_has_domain(query, "LD_lanti_pre"):
        precursor_hit = True
        tabs.append(1)
    else:
        tabs.append(0)
    #Precursor peptide hits Antimicrobial18 (cl06940) HMM
    if cds_has_domain(query, "Antimicr18"):
        precursor_hit = True
        tabs.append(1)
    else:
        tabs.append(0)
    #Precursor peptide hits gallidermin (PF02052) HMM
    if cds_has_domain(query, "Gallidermin") or cds_has_domain(query, "mature_a") or cds_has_domain(query, "mature_ab") or cds_has_domain(query, "mature_b"):
        precursor_hit = True
        tabs.append(1)
    else:
        tabs.append(0)
    #; precursor peptide hits Antimicrobial18 (PF08130) HMM
    if cds_has_domain(query, "Antimicr18"):
        precursor_hit = True
        tabs.append(1)
    else:
        tabs.append(0)

    if precursor_hit:
        score += 3

    # Precursor peptide mass (unmodified)
    precursor_analysis = utils.RobustProteinAnalysis(precursor, monoisotopic=True, invalid='average')
    tabs.append(float(precursor_analysis.molecular_weight()))

    # Unmodified leader peptide mass
    leader_analysis = utils.RobustProteinAnalysis(leader, monoisotopic=True, invalid='average')
    tabs.append(float(leader_analysis.molecular_weight()))

    # Unmodified core peptide mass
    core_analysis = utils.RobustProteinAnalysis(core, monoisotopic=True, invalid='average')
    tabs.append(float(core_analysis.molecular_weight()))

    #Length of leader peptide
    tabs.append(len(leader))
    #Length of core peptide
    tabs.append(len(core))
    #Length of precursor peptide
    tabs.append(len(precursor))
    #Ratio of length of leader peptide / length of core peptide
    tabs.append(float(len(leader) / float(len(core))))
    #Core peptide ≥ 35 residues
    if len(core) >= 35:
        score += 1
        tabs.append(1)
    else:
        tabs.append(0)
    #Core peptide contains CC motif (not in last 3 residues)
    if re.search('CC', core[:-3]) != None:
        score -= 3
        tabs.append(1)
    else:
        tabs.append(0)
    #Leader peptide has > 4 negatively charge motifs
    if sum([leader.count(aa) for aa in "DE"]) > 4:
        score += 1
        tabs.append(1)
    else:
        tabs.append(0)
    #Leader peptide has net negative charge
    charge_dict = {"E": -1, "D": -1, "K": 1, "R": 1}
    if sum([charge_dict[aa] for aa in leader if aa in charge_dict]) < 0:
        score += 1
        tabs.append(1)
    else:
        tabs.append(0)
    #Leader residue position of FxLD motif
    if re.search('F[ARNDBCEQZGHILKMFPSTWYV]LD', leader) != None:
        tabs.append(re.search('F[ARNDBCEQZGHILKMFPSTWYV]LD', leader).span()[0])
    else:
        tabs.append(0)
    #Core peptide contains C-terminal CC (within last 3 residues)
    if re.search('CC', core[-3:]) != None:
        score += 2
        tabs.append(1)
    else:
        tabs.append(0)
    #Core peptide contains DGCGxTC / SFNS / SxxLC / CTxGC / TPGC / SFNSxC motifs
    motifs = (('DGCG[ARNDBCEQZGHILKMFPSTWYV]TC', 2), ('SFNS', 2), \
       ('S[ARNDBCEQZGHILKMFPSTWYV]{2}LC', 2),('CT[ARNDBCEQZGHILKMFPSTWYV]{1}GC', 1), \
       ('TPGC', 1), ('SFNS[ARNDBCEQZGHILKMFPSTWYV]C', 1))
    for motif in motifs:
        if re.search(motif[0], core) != None:
            score += motif[1]
            tabs.append(1)
        else:
            tabs.append(0)
    #Core peptide contains < 2 or < 3 Cys
    if core.count("C") < 2:
        score -= 6
        tabs += [1,1]
    elif core.count("C") < 3:
        score -= 3
        tabs += [1,0]
    else:
        tabs += [0,0]
    #No Cys/Ser/Thr in core peptide
    for aa, penalty in [("C", -10), ("S", -4), ("T", -4)]:
        if aa not in core:
            score += penalty
            tabs.append(1)
        else:
            tabs.append(0)
    #Lanthionine regex maximum ring number > 4
    numringlist, strings, profile = lanscout([[core]])
    if numringlist[0] > 4:
        score += 2
        tabs.append(1)
    else:
        tabs.append(0)
    #Lanthionine regex maximum ring number < 3
    if numringlist[0] < 3:
        score -= 2
        tabs.append(1)
    else:
        tabs.append(0)
    #Lanthionine regex 4-membered ring/5-membered ring/6-membered ring/7-membered ring/8-membered ring
    scores = [2, 2, 2, 2, 1]
    scorepos = 0
    for ringsize in profile[0].split(", ")[:2]:
        if ringsize != "0" and ringsize != "1" and ringsize != "2":
            score += scores[scorepos]
            tabs.append(1)
        else:
            tabs.append(0)
        scorepos += 1
    for ringsize in profile[0].split(", ")[2:]:
        if ringsize != "0":
            score += scores[scorepos]
            tabs.append(1)
        else:
            tabs.append(0)
        scorepos += 1
    return score, tabs

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"""
    precursor = leader + core
    columns = []
    #Precursor Index
    columns.append(1)
    #classification
    columns.append(0)
    columns += previously_gathered_tabs
    #Lanthionine regex maximum ring number
    numringlist, strings, profile = lanscout([[core]])
    columns.append(numringlist[0])
    #Lanthionine regex 4-membered ring count
    columns.append(int(profile[0].split(", ")[0]))
    #Lanthionine regex 5-membered ring count
    columns.append(int(profile[0].split(", ")[1]))
    #Lanthionine regex 6-membered ring count
    columns.append(int(profile[0].split(", ")[2]))
    #Lanthionine regex 7-membered ring count
    columns.append(int(profile[0].split(", ")[3]))
    #Lanthionine regex 8-membered ring count
    columns.append(int(profile[0].split(", ")[4]))
    #Ratio of number of Cys in core peptide to sum of Ser/Thr in core peptide
    if "S" in core or "T" in core:
        columns.append(core.count("C") / float(core.count("S") + core.count("T")))
    else:
        columns.append(1.0)
    #Ratio of number of Cys/Ser/Thr to length of core peptide
    columns.append(float(core.count("S") + core.count("T") + core.count("C")) / len(core))
    #Estimated core charge at neutral pH
    #charge_dict = {"E": -1, "D": -1, "K": 1, "R": 1}
    #columns.append(sum([charge_dict[aa] for aa in core if aa in charge_dict]))
    #Estimated leader charge at neutral pH
    #columns.append(sum([charge_dict[aa] for aa in leader if aa in charge_dict]))
    #Estimated precursor charge at neutral pH
    #columns.append(sum([charge_dict[aa] for aa in precursor if aa in charge_dict]))
    #Absolute value of core charge at neutral pH
    #columns.append(abs(sum([charge_dict[aa] for aa in core if aa in charge_dict])))
    #Absolute value of leader charge at neutral pH
    #columns.append(abs(sum([charge_dict[aa] for aa in leader if aa in charge_dict])))
    #Absolute value of precursor charge at neutral pH
    #columns.append(abs(sum([charge_dict[aa] for aa in leader+core if aa in charge_dict])))
    #log10 p-value MEME motif 1
    if 1 in fimo_motifs:
        columns.append(fimo_scores[1])
    else:
        columns.append(0)
    #log10 p-value MEME motif 2
    if 2 in fimo_motifs:
        columns.append(fimo_scores[2])
    else:
        columns.append(0)
    #log10 p-value MEME motif 3
    if 3 in fimo_motifs:
        columns.append(fimo_scores[3])
    else:
        columns.append(0)
    #log10 p-value MEME motif 4
    if 4 in fimo_motifs:
        columns.append(fimo_scores[4])
    else:
        columns.append(0)
    #log10 p-value MEME motif 5
    if 5 in fimo_motifs:
        columns.append(fimo_scores[5])
    else:
        columns.append(0)
    #Number in leader of each amino acid
    columns += [leader.count(aa) for aa in "ARDNCQEGHILKMFPSTWYV"]
    #Number in leader of each amino acid type (aromatic, aliphatic, hydroxyl, basic, acidic)
    columns.append(sum([leader.count(aa) for aa in "FWY"]))
    columns.append(sum([leader.count(aa) for aa in "DE"]))
    columns.append(sum([leader.count(aa) for aa in "RK"]))
    columns.append(sum([leader.count(aa) for aa in "RKDE"]))
    columns.append(sum([leader.count(aa) for aa in "GAVLMI"]))
    columns.append(sum([leader.count(aa) for aa in "ST"]))
    #Number in core of each amino acid
    columns += [core.count(aa) for aa in "ARDNCQEGHILKMFPSTWYV"]
    #Number in core of each amino acid type (aromatic, aliphatic, hydroxyl, basic, acidic)
    columns.append(sum([core.count(aa) for aa in "FWY"]))
    columns.append(sum([core.count(aa) for aa in "DE"]))
    columns.append(sum([core.count(aa) for aa in "RK"]))
    columns.append(sum([core.count(aa) for aa in "RKDE"]))
    columns.append(sum([core.count(aa) for aa in "GAVLMI"]))
    columns.append(sum([core.count(aa) for aa in "ST"]))
    #Number in entire precursor of each amino acid
    columns += [precursor.count(aa) for aa in "ARDNCQEGHILKMFPSTWYV"]
    #Number in entire precursor of each amino acid type (aromatic, aliphatic, hydroxyl, basic, acidic)
    columns.append(sum([precursor.count(aa) for aa in "FWY"]))
    columns.append(sum([precursor.count(aa) for aa in "DE"]))
    columns.append(sum([precursor.count(aa) for aa in "RK"]))
    columns.append(sum([precursor.count(aa) for aa in "RKDE"]))
    columns.append(sum([precursor.count(aa) for aa in "GAVLMI"]))
    columns.append(sum([precursor.count(aa) for aa in "ST"]))
    return columns

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

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

    #Create FASTA sequence for feature under study
    lan_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
    cleavage_result = run_cleavage_site_phmm(lan_a_fasta, hmmer_profile, -15.0)

    if cleavage_result is not None and cleavage_result.end <= len(query_sequence) - 8 :
        start = cleavage_result.start
        end = cleavage_result.end
        score = cleavage_result.score
        lanti_type = cleavage_result.lantype
    else:
        # If no pHMM hit, try regular expression
        start, end, score = run_cleavage_site_regex(lan_a_fasta)
        if score is None or end > len(query_sequence) - 8:
            start, end, score = 0, int(len(query_sequence)*0.50), 0
        lanti_type = "lantipeptide"

    #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:], domains)
    if rodeo_result[0] is False:
        #logging.debug('%r: No cleavage site predicted' % utils.get_gene_id(query))
        return
    else:
        lanthipeptide = Lantipeptide(start, end, score, rodeo_result[1], lanti_type)

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

    return lanthipeptide

def run_lantipred(seq_record, cluster, query, lant_class, domains):
    hmmer_profiles = {'Class-I': 'class1.hmm',
                      'Class-II':'class2.hmm',
                      'Class-III': 'class3.hmm', }

    query_sequence = utils.get_aa_sequence(query, to_stop=True)
    lan_a_fasta = ">%s\n%s" % (utils.get_gene_id(query), query_sequence)

    if lant_class in ("Class-II", "Class-III"):
        profile = utils.get_full_path(__file__, hmmer_profiles[lant_class])
        cleavage_result = predict_cleavage_site(profile, lan_a_fasta)

        if cleavage_result is None:
            logging.debug('%s: No cleavage site predicted.', utils.get_gene_id(query))
            return

        if thresh_dict[lant_class] > cleavage_result.score:
            logging.debug('%r: Score %0.2f below threshold %0.2f for class %r',
                          utils.get_gene_id(query), cleavage_result.score,
                           thresh_dict[lant_class], lant_class)
            return

        result = Lantipeptide(cleavage_result.start, cleavage_result.end, cleavage_result.score, "N/A", lant_class)
        result.leader = query_sequence[:result.end]
        result.core = query_sequence[result.end:]

    else:
        result = determine_precursor_peptide_candidate(seq_record, cluster, query, query_sequence, domains, hmmer_profiles[lant_class])
        if result is None:
            return


    #extract now (that class is known and thus the END component) the core peptide
    if result.core.find('C') < 0:
        logging.debug('%r: No Cysteine residues found in core, false positive' %
                      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 find_lan_a_features(seq_record, cluster):
    lan_a_features = []
    for feature in utils.get_cds_features(seq_record):
        if feature.location.start < cluster.location.start or \
           feature.location.end > cluster.location.end:
            continue

        aa_seq = utils.get_aa_sequence(feature)
        if len(aa_seq) < 80:
            lan_a_features.append(feature)
            continue

        if not 'sec_met' in feature.qualifiers:
            continue

        domain = None
        for entry in feature.qualifiers['sec_met']:
            if entry.startswith('Domains detected:'):
                domain = entry.split()[2]
                break

        if domain is None:
            continue

        if domain not in known_precursor_domains:
            continue

        lan_a_features.append(feature)

    return lan_a_features


def find_flavoprotein(seq_record, cluster):
    "Look for an epiD-like flavoprotein responsible for aminovinylcystein"
    for feature in utils.get_cds_features(seq_record):
        if feature.location.start < cluster.location.start or \
           feature.location.end > cluster.location.end:
            continue

        if not 'sec_met' in feature.qualifiers:
            continue

        domain = None
        for entry in feature.qualifiers['sec_met']:
            if entry.startswith('Domains detected:'):
                domain = entry.split()[2]
                break

        if domain is None:
            continue

        if domain in 'Flavoprotein':
            return True

    return False


def find_halogenase(seq_record, cluster):
    "Look for a halogenase"
    #return False
    for feature in utils.get_cds_features(seq_record):
        if feature.location.start < cluster.location.start or \
           feature.location.end > cluster.location.end:
            continue

        if not 'sec_met' in feature.qualifiers:
            continue

        domain = None
        for entry in feature.qualifiers['sec_met']:
            if entry.startswith('Domains detected:'):
                domain = entry.split()[2]
                break

        if domain is None:
            continue

        if domain in 'Trp_halogenase':
            return True

    return False


def find_p450_oxygenase(seq_record, cluster):
    "Look for a p450 oxygenase"
    #return False
    for feature in utils.get_cds_features(seq_record):
        if feature.location.start < cluster.location.start or \
           feature.location.end > cluster.location.end:
            continue

        if not 'sec_met' in feature.qualifiers:
            continue

        domain = None
        for entry in feature.qualifiers['sec_met']:
            if entry.startswith('Domains detected:'):
                domain = entry.split()[2]
                break

        if domain is None:
            continue

        if domain in 'p450':
            return True

    return False


def find_short_chain_dehydrogenase(seq_record, cluster):
    "Look for an eciO-like short-chain dehydrogenase responsible for N-terminal lactone"
    for feature in utils.get_cds_features(seq_record):
        if feature.location.start < cluster.location.start or \
           feature.location.end > cluster.location.end:
            continue

        if not 'sec_met' in feature.qualifiers:
            continue

        domain = None
        for entry in feature.qualifiers['sec_met']:
            if entry.startswith('Domains detected:'):
                domain = entry.split()[2]
                break

        if domain is None:
            continue

        if domain in ('adh_short', 'adh_short_C2'):
            return True

    return False


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('lantipeptide')
    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('lantipeptide')
    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)
    if res_vec.alternative_weights:
        weights = map(lambda x: "%0.1f" % x, res_vec.alternative_weights)
        core_feature.qualifiers['note'].append('alternative weights: %s' % "; ".join(weights))
    core_feature.qualifiers['note'].append('number of bridges: %s' % res_vec.number_of_lan_bridges)
    core_feature.qualifiers['note'].append('predicted core seq: %s' % res_vec.core)
    core_feature.qualifiers['note'].append('predicted class: %s' % res_vec.lantype)
    core_feature.qualifiers['note'].append('score: %0.2f' % res_vec.score)
    core_feature.qualifiers['note'].append('RODEO score: %s' % str(res_vec.rodeo_score))
    if res_vec.aminovinyl_group:
        core_feature.qualifiers['note'].append('predicted additional modification: AviCys')
    if res_vec.chlorinated:
        core_feature.qualifiers['note'].append('predicted additional modification: Cl')
    if res_vec.oxygenated:
        core_feature.qualifiers['note'].append('predicted additional modification: OH')
    if res_vec.lactonated:
        core_feature.qualifiers['note'].append('predicted additional modification: Lac')
    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 \
           'lantipeptide' not in cluster.qualifiers['product'][0]:
            continue

        lan_as = find_lan_a_features(seq_record, cluster)

        #Find candidate ORFs that are not yet annotated
        for orf in find_all_orfs(seq_record, cluster):
            aa_seq = utils.get_aa_sequence(orf)
            if len(aa_seq) < 80:
                lan_as.append(orf)

        domains = get_detected_domains(seq_record, cluster)
        flavoprotein_found = find_flavoprotein(seq_record, cluster)
        halogenase_found = find_halogenase(seq_record, cluster)
        oxygenase_found = find_p450_oxygenase(seq_record, cluster)
        dehydrogenase_found = find_short_chain_dehydrogenase(seq_record, cluster)
        lant_class = predict_class_from_gene_cluster(seq_record, cluster)
        if lant_class is None:
            return
        for lan_a in lan_as:
            result_vec = run_lantipred(seq_record, cluster, lan_a, lant_class, domains)
            if result_vec is None:
                continue
            if flavoprotein_found:
                result_vec.aminovinyl_group = True
            if halogenase_found:
                result_vec.chlorinated = True
            if oxygenase_found:
                result_vec.oxygenated = True
            if dehydrogenase_found and result_vec.core.startswith('S'):
                result_vec.lactonated = True

            new_features = result_vec_to_features(lan_a, result_vec)
            if "allorf" in utils.get_gene_id(lan_a):
                new_features.append(lan_a)
                if lan_a.location.start < cluster.location.start:
                    cluster.location = FeatureLocation(lan_a.location.start, cluster.location.end)
                if lan_a.location.end > cluster.location.end:
                    cluster.location = FeatureLocation(cluster.location.start, lan_a.location.end)
            seq_record.features.extend(new_features)

