# -*- coding: utf-8 -*-

# Copyright (c) 2022-2023: Mustafa Tekpinar
# This code is part of the prescott package and governed by its license.
# Please see the LICENSE.txt file included as part of this package.

import sys
import os
import time
import argparse
import re
import subprocess
import math
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
matplotlib.use('Agg')
import shutil
import glob

from prody import *
from scipy.stats import rankdata
from Bio import AlignIO, SeqIO

import biotite.structure.io as strucio
import biotite.application.dssp as dssp
import biotite.structure.io.pdb as pdb

import importlib.resources
from importlib.resources import files, as_file
import pandas as pd

def selectMaxFastaSequences(inputMSAfile, outputMSAfile, maxNumSeqs=40001):
    """
        Given a multiple sequence alignment file in FASTA format,
        select the top maxNumSeqs sequences and write them to outputMSAfile.
    
    Parameters
    ----------
	inputMSAfile: string 
        Name of the input multiple sequence alignment file in FASTA format.
    outputMSAfile: string
        Name of the output multiple sequence alignment file in FASTA format.
	maxNumSeqs: int
		Maximum number of sequences to be selected from the input MSA file.
        Default is 40000 + 1 (for query sequence) .

    Returns
    -------
	Nothing 
    """

    alignment = AlignIO.read(inputMSAfile, 'fasta')
    # print(alignment[0].seq)
    numSeqInMSA = len(alignment)

    # print("Number of sequences read: "+str(numSeqInMSA))

    with open(outputMSAfile, "w") as handle:
        SeqIO.write(alignment[0:maxNumSeqs], handle, "fasta")

#############The following part between # signs is moved from gemmeAnal.py to 
# make the code more simple.  
def extractQuerySeq(filename):
	"""
		Extract the query sequence from the input alignment
	"""

	fIN = open(filename,"r")
	lines = fIN.readlines()
	fIN.close()

	if lines[0][0]!=">":
		raise Exception('bad FASTA format')
	else:
		prot = re.compile("[^A-Z0-9a-z]").split(lines[0][1:])[0]
		fOUT = open(prot+".fasta","w")
		fOUT.write(">"+prot+"\n")
		seq=""
		i = 1
		while lines[i][0]!=">":
			seq = seq + lines[i].strip().strip(".").strip("-")
			fOUT.write(lines[i])
			i = i + 1
	fOUT.close()
	return prot,seq,i-1

# def getNbSeq(filename):
# 	#TODO: Remove bash dependency and count the sequences within Python!
# 	"""
# 		# Get the number of sequences in a multi-fasta file
		
# 	"""
# 	if filename!='':
# 		proc=subprocess.Popen("grep -c '^>' "+filename,stdout=subprocess.PIPE,shell=True)
# 		return int(proc.stdout.read())
# 	else:
# 		return 0

# def getNbSeqPython(filename):
#     #TODO: Remove bash dependency and count the sequences within Python!
#     """
#     	Get the number of sequences in a multi-fasta file using only Python functions.	
#     """
#     if ((filename != '') or (filename != None)):
#         alignment = AlignIO.read(filename, "fasta")
#         return int(len(alignment))
#     else:
#         print("ERROR: Could not find MSA file!")
#         return 0

def createPDB(prot,seq):
	"""
		If there is not a real PDB file for a given sequence,
		create a fake PDB containing only dummy CA atoms.  
	"""
	d = {'C': 'CYS', 'D': 'ASP', 'S': 'SER',  'Q': 'GLN', 'K': 'LYS',
	'I': 'ILE', 'P': 'PRO', 'T': 'THR', 'F': 'PHE', 'N': 'ASN', 
	'G': 'GLY',  'H': 'HIS', 'L': 'LEU', 'R': 'ARG', 'W': 'TRP', 
	'A': 'ALA', 'V': 'VAL', 'E': 'GLU', 'Y': 'TYR', 'M': 'MET'}

	fOUT = open(prot+'.pdb','w')
	i = 1
	for let in seq:
		if(let != '-'):
			fOUT.write('ATOM%7d  CA  %s A%4d      43.524  70.381  46.465  1.00   0.0\n'%(i,d[let.upper()],i))
			i += 1
		
	fOUT.close()
# def editConfJET(N):
# 	"""
# 		# Edit JET configuration file with correct number of Seqs & MSA
# 	"""
# 	reCode=subprocess.call("sed -i 's/results\t\t5000/results\t\t"+str(N)+"/' default.conf",shell=True)
# 	return(reCode)

def editConfJETpython(N):
    """
    	# Edit JET configuration file with correct number of Seqs & MSA
    """
    with open("default.conf", "r+" ) as file:
        fileContents = file.read()
        textPattern = re.compile( re.escape("results\t\t5000"))
        fileContents = textPattern.sub("results\t\t"+str(N), fileContents)
        file.seek( 0 )
        file.truncate()
        file.write(fileContents)

    #reCode=subprocess.call("sed -i 's/results\t\t5000/results\t\t"+str(N)+"/' default.conf",shell=True)
    return 0

def editDefaultConfChangeCVRC(CVRc):
    """
        Edit JET configuration file to modify cutoff radius 
        when calculating CV (circular variance). 

    """
    with open("default.conf", "r+" ) as file:
        fileContents = file.read()
        textPattern = re.compile( re.escape(">CV\nmax_dist        7.0"))
        fileContents = textPattern.sub(">CV\nmax_dist        {:.1f}".format(CVRc), fileContents)
        file.seek( 0 )
        file.truncate()
        file.write(fileContents)

    #reCode=subprocess.call("sed -i 's/results\t\t5000/results\t\t"+str(N)+"/' default.conf",shell=True)
    return 0

def minMaxNormalization(data):
    """
        Min-max normalization of a data array.
    """
    return (data - np.min(data)) / (np.max(data) - np.min(data))

# Run JET to compute TJET values
def launchJET(prot, retMet, bFile, fFile, pdbfile, chains, n, N, nl, CVRc):
    """
		Call JET2 and produce prot+"_jet.res" file.

		prot+"_jet.res" will be used in the following steps (in launchPred)
		to calculate independent and epistatic models.
		Ideally, this call to JET2 should be from Dockers or Singularity 
		because installing all requirements of JET2 is a pain in the ass!

	Parameters
    ----------
    prot: string ???
        Name of the protein ???
    retMet: string
		Retreival method of multiple sequence alignments file
		It can be 'input', 'local' or 'server'. Default is local.
	bFile: string 
        A multiple sequence alignment file obtained with psiblast.
		It is used only if the retMet (explained above) is input.
	fFile: string 
        A multiple sequence alignment file obtained with psiblast.
		It is used only if the retMet (explained above) is input.
	pdbfile: string 
        a Protein Data Bank file obtained with rcsb.org or any 
		computational method like alphafold.
		If it is None, only JET and PC scores are calculated.
		Otherwise, CV and other structural-dynamical features also
		can be calculated.
	chains: list
		A list of chains available in the pdb file.
		Most of the time, it is supposed to be just one! 
	n: int
		Number of JET2 iterations.
	N: int
		Default 40000
	nl: int
		Number of lines after > character in the query sequences file.
		It is obtained in extractQuerySeq() function.
    CVRc: float or None
        Cutoff radius for CV (Circular Variance) calculation. 

    Returns
    -------
	Nothing 
    """

    chainID = chains[0]

    # esgemme_path = os.path.expandvars ('$PRESCOTT_PATH')
    # shutil.copy2(os.path.join(esgemme_path, "default.conf"), os.getcwd())
#    esgemme_default_conf_path = importlib.resources.path('esgemme', 'default.conf')
#    print(esgemme_default_conf_path)
    source = files('prescott').joinpath('default.conf')
    with as_file(source) as prescott_default_conf_path:
        #third_party_api_requiring_file_system_path(eml)
        shutil.copy2(prescott_default_conf_path, os.getcwd())
    
    #If you specified a different cutoff radius for CV calculation, it will take effect here
    #by changing the default value in default.conf file. 
    if (CVRc != None):
        editDefaultConfChangeCVRC(CVRc)

    if retMet=="input":
        if bFile!='':
			#TODO: I think these two lines must be here as well but I am not sure. 
			# print(N)
			# editConfJETpython(N)

            if(bFile == prot+"_"+chainID+".psiblast"):
                shutil.copy2(bFile, bFile+".orig")
                shutil.copy2(bFile+".orig ", prot+"_"+chainID+".psiblast")
            else:
                shutil.copy2(bFile+" ", prot+"_"+chainID+".psiblast")

            if(pdbfile == None):
                jetcmd = "java -Xmx8192m -cp $JET2_PATH:$JET2_PATH/jet/extLibs/vecmath.jar jet.JET -c default.conf -i "+\
						prot+".pdb -o `pwd` -p J -r input -b "+prot+"_"+chainID+".psiblast -d chain -n "+n+" > "+prot+".out"
            else:
				# jetcmd = "java -Xmx8192m -cp $JET2_PATH:$JET2_PATH/jet/extLibs/vecmath.jar jet.JET -c default.conf -i "+\
				#  		prot+".pdb -o `pwd` -p AVJ   -r input -b "+prot+"_"+chainID+".psiblast -d chain -n "+n+" > "+prot+".out"
                jetcmd = "java -Xmx8192m -cp $JET2_PATH:$JET2_PATH/jet/extLibs/vecmath.jar jet.JET -c default.conf -i "+\
					    prot+".pdb -o `pwd` -p AVJCG -r input -f "+prot+"_"+chainID+".psiblast -d chain -n "+n+" -a 5"+" > "+prot+".out"
						#One can also add: -g 'trace,pc,cv,clusters,axs' 

        else:
            print(N)
            editConfJETpython(N)

            if(fFile == prot+"_"+chainID+".fasta"):
                shutil.copy2(fFile, fFile+".orig")
                #I think this subprocess call causes overwriting of the fasta file. 
                #subprocess.call("cp "+fFile+" "+prot+"_"+chainID+".fasta",shell=True)
                # grpcmd="grep -m "+str(int(N)+1)+" -A "+str(nl)+" '^>' "+fFile+".orig > "+prot+"_"+chainID+".fasta"
                # print("\nRunning:\n"+grpcmd)
                # subprocess.call(grpcmd,shell=True)
                selectMaxFastaSequences(fFile+".orig", prot+"_"+chainID+".fasta", maxNumSeqs=(int(N)+1))
            else:
                #subprocess.call("cp "+fFile+" "+prot+"_"+chainID+".fasta",shell=True)
                #grpcmd="grep -m "+str(int(N)+1)+" -A "+str(nl)+" '^>' "+fFile+" > "+prot+"_"+chainID+".fasta"
			
                #print("\nRunning:\n"+grpcmd)
                #subprocess.call(grpcmd,shell=True)
                selectMaxFastaSequences(fFile, prot+"_"+chainID+".fasta", maxNumSeqs=(int(N)+1))
            if(pdbfile == None):
                jetcmd = "java -Xmx8192m -cp $JET2_PATH:$JET2_PATH/jet/extLibs/vecmath.jar jet.JET -c default.conf -i "+\
                        prot+".pdb -o `pwd` -p J -r input -f "+prot+"_"+chainID+".fasta -d chain -n "+n+" > "+prot+".out"
                print("\nRunning command:\n"+jetcmd)
                reCode=subprocess.call(jetcmd,shell=True)
                if os.path.isfile(prot+"/"+prot+"_jet.res"):
                    os.rename(prot+"/"+prot+"_jet.res",prot+"_jet.res")
            else:
                # Calculate SC1
                jetcmd = "java -Xmx8192m -cp $JET2_PATH:$JET2_PATH/jet/extLibs/vecmath.jar jet.JET -c default.conf -i "+\
                        prot+".pdb -o `pwd` -p AVJCG -r input -f "+prot+"_"+chainID+".fasta -d chain -n "+n+" -a 3"+" > "+prot+".out"
                        #One can also add: -g 'trace,pc,cv,clusters,axs' 
                
                print("\nRunning JET2 with following arguments:\n"+jetcmd)
                reCode=subprocess.call(jetcmd,shell=True)
                if os.path.isfile(prot+"/"+prot+"_jet.res"):
                    os.rename(prot+"/"+prot+"_jet.res",prot+"_jet.res")
                
                dir_name = prot+"/"
                if os.path.isdir(dir_name):
                    for f in os.listdir(dir_name): 
                        f_path = os.path.join(dir_name, f)
                        if os.path.isfile(f_path):
                            os.remove(f_path)
                    os.rmdir(dir_name)


    else:
        if(pdbfile == None):
            jetcmd = "java -Xmx8192m -cp $JET2_PATH:$JET2_PATH/jet/extLibs/vecmath.jar jet.JET -c default.conf -i "+\
                    prot+".pdb -o `pwd` -p J -r "+retMet+" -d chain -n "+n+" > "+prot+".out"
        else:
            # jetcmd = "java -Xmx8192m -cp $JET2_PATH:$JET2_PATH/jet/extLibs/vecmath.jar jet.JET -c default.conf -i "+\
            #     	prot+".pdb -o `pwd` -p AVJ   -r "+retMet+" -d chain -n "+n+" > "+prot+".out"
            jetcmd = "java -Xmx8192m -cp $JET2_PATH:$JET2_PATH/jet/extLibs/vecmath.jar jet.JET -c default.conf -i "+\
                    prot+".pdb -o `pwd` -p AVJCG -r "+retMet+" -d chain -n "+n+" -a 5"+" > "+prot+".out"
        
        reCode=subprocess.call(jetcmd,shell=True)
        if os.path.isfile(prot+"/"+prot+"_jet.res"):
            os.rename(prot+"/"+prot+"_jet.res",prot+"_jet.res")
        
        dir_name = prot+"/"
        if os.path.isdir(dir_name):
            for f in os.listdir(dir_name): 
                f_path = os.path.join(dir_name, f)
                if os.path.isfile(f_path):
                    os.remove(f_path)
            os.rmdir(dir_name)
	# print("\nRunning:\n"+jetcmd)
	# reCode=subprocess.call(jetcmd,shell=True)
	# if os.path.isfile(prot+"/"+prot+"_jet.res"):
	# 	os.rename(prot+"/"+prot+"_jet.res",prot+"_jet.res")
    return(reCode)

# Run Rscript to compute predictions
def launchPred(prot,inAli,mutFile, normWeightMode, alphabet):

    # I don't want to have a path dependency here. 
    # Removing this dependency makes the code more portable.
    # Therefore, I copy the R scripts that are saved as the 
    # package resources to the current working directory.
    source = files('prescott').joinpath('pred.R')
    with as_file(source) as prescott_pred_R_path:
        shutil.copy2(prescott_pred_R_path, os.getcwd())
    
    source = files('prescott').joinpath('computePred.R')
    with as_file(source) as prescott_computePred_R_path:
        shutil.copy2(prescott_computePred_R_path, os.getcwd())

    source = files('prescott').joinpath('alphabets/'+alphabet+'.txt')
    with as_file(source) as prescott_alphabet:
        shutil.copy2(prescott_alphabet, os.getcwd())
        
    if (mutFile!=''):
        # rcmd="Rscript --save $PRESCOTT_PATH/computePred.R "+prot+" "+inAli+" FALSE "+mutFile+" "+normWeightMode+" "+alphabet
        rcmd="Rscript --save computePred.R "+prot+" "+inAli+" FALSE "+mutFile+" "+normWeightMode+" "+alphabet
    else:
        # rcmd="Rscript --save $PRESCOTT_PATH/computePred.R "+prot+" "+inAli+" TRUE none "+normWeightMode+" "+alphabet
        rcmd="Rscript --save computePred.R "+prot+" "+inAli+" TRUE none "+normWeightMode+" "+alphabet

    print("\nRunning Rscript with following arguments: \n"+rcmd)
    reCode=subprocess.call(rcmd,shell=True)

	#Add plots here with gemmemore
    return(reCode)

# Remove temporary files
def cleanTheMess(prot,bFile,fFile, chainID, verbosity=False):

    if bFile!='':
        if bFile!=prot+"_"+chainID+".psiblast":
            os.remove(prot+"_"+chainID+".psiblast")
    else:
        if os.path.isfile(prot+"/"+prot+"_"+chainID+".psiblast"):
            os.rename(prot+"/"+prot+"_"+chainID+".psiblast",prot+"_"+chainID+".psiblast")
    if fFile!='':
        if fFile!=prot+"_"+chainID+".fasta":
            if os.path.isfile(prot+"_"+chainID+".fasta"):
                os.remove(prot+"_"+chainID+".fasta")		
    # if os.path.isfile(prot+"_jet.res"):
    #     os.remove(prot+"_jet.res")
    if os.path.isfile(prot+".pdb"):
        os.remove(prot+".pdb")
	
	# Get all files with suffix nwk
    treefiles = glob.glob('*.nwk')

    # Iterate over the list of files and remove individually
    for file in treefiles:
        os.remove(file)

    dir_name = prot+"/"
    if os.path.isdir(dir_name):
        for f in os.listdir(dir_name): 
            f_path = os.path.join(dir_name, f)
            if os.path.isfile(f_path):
                os.remove(f_path)
        os.rmdir(dir_name)

    if os.path.isfile("bin1.fasta"):
        os.remove("bin1.fasta")
        os.remove("bin2.fasta")
        os.remove("bin3.fasta")
        os.remove("bin4.fasta")
    
    if os.path.isfile("caracTest.dat"):
        os.remove("caracTest.dat")

    # if os.path.isfile("default.conf"):
    #     os.remove("default.conf")

    if verbosity == False:
        if os.path.isfile(prot+"_normPred_evolInd.txt"):
            os.remove(prot+"_normPred_evolInd.txt")
        if os.path.isfile(prot+"_normPred_evolEpi.txt"):
            os.remove(prot+"_normPred_evolEpi.txt")
       
    
###############################################################################
def rankSortProteinData(dataArray, inverted=True):
    """
        This function ranksorts protein data and converts it
        to values [0,1.0].
    
    Parameters:
    ----------
    dataArray: numpy array of arrays
               data read by numpy.loadtxt

    Returns:
    -------
    normalizedRankedDataArray: numpy array
    """
    normalizedRankedDataArray = rankdata(dataArray)/float(len(dataArray))

#    np.savetxt(outfile, np.array([dataArray, normalizedRankedDataArray]).T)
    if(inverted==True):
        return (1.0 - normalizedRankedDataArray)
    else:
        return (normalizedRankedDataArray)

# def calcPercentDFI(pdbfile, outfile, nmodes=None, attenuate="true", ranksorted="true"):
#     """
#         Calculate rank normalized dynamic flexibility index. 
#     """
    
#     #confProDy(auto_show=False)

#     structure = parsePDB(pdbfile)
#     calphas = structure.select('name CA')

#     anm = ANM('ANM')
#     anm.buildHessian(calphas)
#     if(nmodes==None):
#         anm.calcModes(n_modes='all')
#     else:
#         anm.calcModes(n_modes=int(nmodes))

#     dfi = calcDynamicFlexibilityIndex(anm, calphas, select='ca', norm=True)
#     percentDfi = rankSortProteinData(dfi)

#     if(attenuate.lower()=='true'):
#         percentDfi = attenuateEndPoints(percentDfi)

#     # if(outfile != None):
#     #     np.savetxt(outfile, percentDfi)
#     # else:
#     #     np.savetxt(outfile, dfi)

#     if(ranksorted.lower()=='true'):
#         return percentDfi
#     else:
#         return dfi

# def calcBfactors(pdbfile, outfile, nmodes=None, attenuate="true", 
#                 ranksorted="true", inverted=False):
#     """
#         Calculate rank normalized mean squared fluctuations,
#         which correlate well with Bfactors. 
#     """
    
#     #confProDy(auto_show=False)

#     structure = parsePDB(pdbfile)
#     calphas = structure.select('name CA')

#     anm = ANM('ANM')
#     anm.buildHessian(calphas)
#     if(nmodes==None):
#         anm.calcModes(n_modes='all')
#     else:
#         anm.calcModes(n_modes=int(nmodes))

#     bfactors = calcSqFlucts(anm)

#     percentBfactors = rankSortProteinData(bfactors, inverted=False)

#     if(attenuate.lower()=='true'):
#         percentBfactors = attenuateEndPoints(percentBfactors)

#     # if(outfile != None):
#     #     np.savetxt(outfile, percentBfactors)
#     # else:
#     #     np.savetxt(outfile, dfi)

#     if(ranksorted.lower()=='true'):
#         if(inverted==True):
#             return (1.0 - percentBfactors)
#         else:
#             return percentBfactors
#     else:
#         if(inverted==True):
#             return (np.max(bfactors) - bfactors)
#         else:
#             return bfactors

# def getBfactors(pdbfile, outfile, attenuate="true", \
#                 ranksorted="true",  inverted=True):
#     """
#         Calculate rank normalized dynamic flexibility index. 
#     """
    
#     #confProDy(auto_show=False)

#     structure = parsePDB(pdbfile)
#     calphas = structure.select('name CA')

#     bfactors = calphas.getBetas()

#     percentBfactors = rankSortProteinData(bfactors, inverted=False)

#     if(attenuate.lower()=='true'):
#         percentBfactors = attenuateEndPoints(percentBfactors)

#     # if(outfile != None):
#     #     np.savetxt(outfile, percentBfactors)
#     # else:
#     #     np.savetxt(outfile, dfi)

#     if(ranksorted.lower()=='true'):
#         if(inverted==True):
#             return (1.0 - percentBfactors)
#         else:
#             return percentBfactors
#     else:
#         if(inverted==True):
#             return (np.max(bfactors) - bfactors)
#         else:
#             return bfactors
    
def attenuateEndPoints(dfi):
    """
        Reduce DFI on N and C terminals with a sigmoid function.
    """

    debug = 0

    n = len(dfi)
    if(debug):
        #Check array lengths
        print(n/2)
        print(len(dfi))

    if(n%2 == 0):
        x1 = np.linspace(-0, 10, int(n/2))
        x2 = np.linspace(-10, 0, int(n/2))
    else:
        x1 = np.linspace(-0, 10, int((n-1)/2))
        x2 = np.linspace(-10, 0, int((n+1)/2))
    
    z1 = 1/(1 + np.exp(-2.50*x1))
    z2 = 1.0 - (1/(1 + np.exp(-2.50*x2)))


    z = np.append(z1, z2)

    if(debug):
        #Check array lengths
        print(len(z1))
        print(len(z2))
        print(len(z))
        #Plot the arrays
        plt.plot(z)
        plt.plot(dfi)
        plt.plot(dfi*z)
        plt.xlabel("x")
        plt.ylabel("Sigmoid(X)")
        plt.show()


    if(len(dfi) == len(z)):
        return (dfi*z)
    else:
        print("ERROR: Can not attenuate N and C terminal dfi signals!")
        sys.exit(-1)


alphabeticalAminoAcidsList = ['A', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'K', 'L',
                              'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'V', 'W', 'Y']
def parseGEMMEoutput(inputFile, verbose):
    """
        Parse normalized (I don't know how?!) GEMME output files: 
        Independent, Epistatic and Combined
        Return: Data File as a Numpy array, where each col contains 
        deleteriousness score for an amino acid.
        verbose is a boolean value
    """
    gemmeDataFile =open(inputFile, 'r')
    allLines = gemmeDataFile.readlines()
    gemmeDataFile.close()

    headerLine = allLines[0]
    if(verbose):
        print(headerLine)
    matrixData = []
    aaColumn = []
    for line in allLines[1:]:
        tempList = []
        data = line.split()
        for item in data:
            if item[0] == "\"":
                aaColumn.append(item)
            elif (item == 'NA'):
                tempList.append(0.0000000)
            else:
                tempList.append(float(item))
        matrixData.append(tempList)

    mutationsData = np.array(matrixData)
    # mutatedTransposed = mutationsData.T
    # return mutatedTransposed
    return mutationsData
def plotGEMMEmatrix(scanningMatrix, outFile, beg, end, \
                    colorMap = 'coolwarm', \
                    offSet=0, pixelType='square',
                    aaOrder="ACDEFGHIKLMNPQRSTVWY", \
                    sequence=None,\
                    interactive=False,\
                    isColorBarOn=False):
    """
        A function to plot deep mutational scanning matrices. 
  
    Parameters
    ----------
    scanningMatrix: numpy array of arrays
        Data matrix to plot

    outFile: string
        Name of the output image without file extension.
        Default file extension (png) is added by the program
    
    beg: int
        The first residue to use. It is used to select a subrange 
        of amino acids. It starts from 1.
    
    end: int
        The last residue to use. It is used to select a subrange 
        of amino acids.

    colorMap: matplotlib cmap
        Any colormap existing in matplotlib.
        Default is coolwarm. 

    offSet: int
        It is used to match the residue IDs in your PDB
        file with 0 based indices read from scanningMatrix matrix

    pixelType: string
        It can only have 'square' or 'rectangle' values.
        It is a matter of taste but I added it as an option.
        Default is 'square'

    sequence: string
        A fasta file of one letter amino acid codes from N terminal to C terminal. 

    interactive: bool
        If True, it will plot the map interactively. 

    isColorBarOn: bool
        If True, it will show a colorbar to show the numerical scale of
        the colors. Default is False. 

    Returns
    -------
    Nothing

    """

    #We subtract 1 from beg bc matrix indices starts from 0
    if(end == None):
        end = len(scanningMatrix[0])
        
    print("Beginning: "+str(beg))
    print("End      : "+str(end))
    #print(scanningMatrix)
    #np.reshape(scanningMatrix.flatten(), (20, 286))
    #print(len(scanningMatrix[0]))

    subMatrix = scanningMatrix[:, (beg-1):end]
    #print(subMatrix)

    ###############################################################################
    # Set plotting parameters
    nres_shown = len(subMatrix[0])
    # print(nres_shown)
    fig_height=8
    # figure proportions
    fig_width = fig_height/2  # inches
    fig_width *= nres_shown/20
    
    fig, ax = plt.subplots(figsize=(fig_width, fig_height))

    if (nres_shown >150):
        majorTics = 50
    else:
        majorTics = 20


    major_nums_x = np.arange(majorTics, len(subMatrix[0]), majorTics, dtype=int)
    #major_nums_x = major_nums_x -1
    major_nums_x = np.insert(major_nums_x, 0, 0)
    #print(major_nums_x)

    minor_nums_x = np.arange(10, len(subMatrix[0]), 10, dtype=int)
    #minor_nums_x = minor_nums_x - 1
    minor_nums_x = np.insert(minor_nums_x, 0, 0)
    #print(minor_nums_x)

    major_labels_x = major_nums_x + 1 + offSet

    major_nums_y = np.arange(0, 20, 1, dtype=int)
    major_labels_y = list(aaOrder)

    plt.xticks(major_nums_x, major_labels_x, size=28)
    ax.set_xticks(minor_nums_x, minor=True)
    
    plt.yticks(major_nums_y, major_labels_y, size=16)
    ax.set_yticklabels(major_labels_y, ha='left')
    ax.tick_params(axis='y', which='major', pad=30)

    
    #############################################################################
    if(pixelType=='square'):
        #For plotting square pixels
        img = plt.imshow(subMatrix, cmap=colorMap)
    elif(pixelType=='rectangle'):
        #For plotting rectangular pixels
        img = plt.imshow(subMatrix, cmap=colorMap, aspect=3.0)
    else:
        print("\nERROR: Unknown pixelType specified!\n")
        sys.exit(-1)
    
    #To make the colors consistent if there are submatrices.
    plt.clim(np.min(scanningMatrix), np.max(scanningMatrix)) 
    #plt.clim([-5.0, 5.0])
    if(sequence!=None):
        mySeqFile = SeqIO.read(sequence, 'fasta')
        #print(mySeqFile)
        #Convert aaOrder to a list.
        aaOrderList = list(aaOrder)
        for i in range (len(subMatrix[0])):
            j = beg-1+i
            # print(i, aaOrderList.index(sequence[i]))
            plt.scatter(i, aaOrderList.index(mySeqFile.seq[j].upper()), s=5, c='black', marker='o')
    
    if(isColorBarOn):
        from mpl_toolkits.axes_grid1 import make_axes_locatable
        divider = make_axes_locatable(ax)
        # print("Here I am!")
        # print(nres_shown)
        cax = divider.append_axes("right", size=fig_width/nres_shown, pad=0.2)
        # plt.clim([-10.0, 2.0])
        plt.colorbar(img, cax=cax)

    plt.tight_layout()
    plt.savefig(outFile+".png")
    if(interactive):
        plt.show()
    

    #plt.imsave('output.png', subMatrix)
    plt.close()

def check_argument_groups(parser, arg_dict, group, argument):
    """
    Check for use of arguments.
    Raise an error if the parser uses an argument that belongs to an argument
    group (i.e. mode) but the group flag is not used or if the argument is
    required in the argument group and it's not used.
    Notes:
    - argument and group should be strings and should start with - or --
    - group should be a flag with action 'store_true'
    - argument shouldn't be a flag, and should be None if it isn't used.
    >>> import argparse
    >>> parser = argparse.ArgumentParser()
    >>> parser.add_argument('--phylo', action='store_true')
    _StoreTrueAction(...)
    >>> parser.add_argument('--inseq')
    _StoreAction(...)
    >>> args = parser.parse_args(['--phylo', '--inseq', 'not_none'])
    >>> check_argument_groups(parser, vars(args), '--phylo', '--inseq', True)
    """
    c = 0
    for myArg in argument:
        arg_name = myArg.replace("-", "")
        if arg_dict[arg_name]!='':
            c = c+1
    group_name = group.replace("-", "")
    if arg_dict[group_name]=="input":
    	if c!=1:
            parser.error("escott requires " + str(argument) + \
                         " if " + group + " is set to input.")
    else:
        if c>0:
            parser.error("escott requires " + group +
                         " to be set to input if " + str(argument) + " is used.")
    return None
# def calculateSecondaryStructure(pdbfile, coilBreakerLength):
def calculateSecondaryStructure(pdbfile):
    """
        Calls dssp, calculates secondary structure and saves the results as a 
        text file with .dssp extention. The result contains a residue index and 
        dssp assignment for that amino acid on each line:
        0,H
        1,E
        2,E
        .
        .
        .
        100,C

        A structure file in PDB format is the main input.
    """

    #pdbfile: Name of the input pdb file
    #outfile=Name of outputfile that contain secondary structure information= pdbfile+".dssp"
    #for each residue in a new line
    array = strucio.load_structure(pdbfile)

    app = dssp.DsspApp(array)
    app.start()
    app.join()
    sse = app.get_sse()
    # print(sse)
    calphas = array[array.atom_name == "CA"]
    residList = (calphas.res_id)

    # #################################################################
    # # Check S or T residues that are between coils and convert them to coils.
    # # These S or Ts break the continuity of a coil and reduce coil length parameter,
    # # which is used later do select Tjet or Max score. 
    # # We have to do it in a loop because we change S or T to C. When you check ss in a 
    # # second round, you will capture new C(S|T)+S patterns. We do this until this pattern
    # # can not be found!
    # debug = True
    # myRegex = r'C(S|T){'+str(coilBreakerLength)+',6}C'
    # print(myRegex)
    # while(1):
    #     secondaryStructureString=""
    #     for item in sse:
    #         secondaryStructureString = secondaryStructureString + item

    #     # print(secondaryStructureString)
    #     #allMatcheSum = sum(1 for _ in re.finditer(r'C(S|T)+C', secondaryStructureString))
    #     allMatcheSum = sum(1 for _ in re.finditer(myRegex, secondaryStructureString))
    #     # allMatches = re.finditer(r'C(S|T)+C', secondaryStructureString)
    #     if(allMatcheSum==0):
    #         break
    #     else:
    #         for match in re.finditer(myRegex, secondaryStructureString):
    #             if(debug):
    #                 print(secondaryStructureString[match.span()[0]:match.span()[1]], match.span()[0], match.span()[1])
    #             for i in range(match.span()[0], match.span()[1]):
    #                 sse[i]='C'

    #         secondaryStructureString=""
    #         for item in sse:
    #             secondaryStructureString = secondaryStructureString + item
            
    #         #print(secondaryStructureString)
    #     # print(match.span())
    # #sys.exit(-1)
    # #################################################################
    with open(pdbfile+".dssp", 'w') as file:
        for j in range(len(sse)): 
            file.write(str(residList[j])+","+sse[j]+"\n")


def countCoilSegments(inputfile):
    """
        Count length of each coil segment and write it to outputfile.
    """

    df = pd.read_csv(inputfile, header=None)
    df.columns = ["pos", "ss"]

    # numRows = (len(df))
    # internalCounter = 0
    from itertools import groupby
    groups = groupby(df['ss'])
    result = [(label, sum(1 for _ in group)) for label, group in groups]
    #print(result)
    beg = 0
    end = 0
    outputfile=inputfile+".new"
    with open(outputfile, 'w') as file: 
        for item in result:
            beg = end
            end = end + item[1]
            #print(item[0], item[1])
            for i in range(beg, end):
                file.write("{},{},{}\n".format(df.iloc[i]['pos'], item[0], item[1]))
            # if(item[0]=='C'):
        #     print(item[1])

def parse_command_line():
    """
        Parse command line.
        It uses argparse to parse 'escott' command line arguments and returns the
        argparse parser.
    """
    parser = argparse.ArgumentParser(
        prog="escott",
        description="""
            PRESCOTT (Population awaRe Epistatic and StruCtural mOdel of muTational effecTs) 
            is a tool to predict mutational outcomes.
            """,
        epilog="""
            It has been developed at LCQB (Laboratory of Computational and
            Quantitative Biology), UMR 7238 CNRS, Sorbonne Université.
            If you use it, please cite:
            Coming soon...
            """,
    )

    parser.add_argument(
    	'input', metavar='aliFile', type=str,
        help='input alignment file in FASTA format (first sequence = ungapped query sequence)',
    )

    parser.add_argument(
        '-m', '--mutations',
        help='text file containing the mutations of interest. Each line of the '+\
             'file should contain a mutation (e.g. D136R) or combination of '+\
             'mutations separated by colons and ordered according to their '+\
             'positions in the sequence (e.g. D136R:V271A)',
        default=''
    )

    retMet_args = parser.add_argument_group('Conservation levels calculation', """
        Arguments used for computing the conservation levels.
        """)
    
    retMet_args.add_argument(
        '-n', '--nIter',
        help='number of iterations to compute the conservation levels',
        default='1'
    )

    retMet_args.add_argument(
        '-N', '--NSeqs',
        help='maximum number of sequences to compute the conservation levels',
        default='40000'
    )
        
    retMet_args.add_argument(
        '-r', '--retrievingMethod',
        help='mode to retrieve related sequences for conservation levels calculation (input, local or server)',
        default='input'
    )

    retMet_args.add_argument(
        '-b', '--blastFile',
        help='psiblast file containing related sequences',
        default=''
    )

    retMet_args.add_argument(
        '-f', '--fastaFile',
        help='fasta file containing related sequences',
        default=''
    )

    retMet_args.add_argument('--jetfile', dest='jetfile', type=str, \
        help="If a jet file is provided, it will skip JET2 calculation and use the precalculated JET2 data in that file. Default is None",
        required=False, default=None)

    parser.add_argument('--alphabet', dest='alphabet', type=str, \
        help="Which alphabet to use. Default is lw-i.7",
        required=False, default="lw-i.7")

    parser.add_argument('-p', '--pdbfile', dest='pdbfile', type=str, \
        help="If a pdb file is provided, it will skip fake pdb file production step and use that file. Default is None",
        required=False, default=None)
    
    parser.add_argument('--normweightmode', dest='normweightmode', type=str, \
        help="It can be one of these: tjet or sstjetormaxtwocomponent. Default is 'tjet'.",
        required=False, default="tjet")

    parser.add_argument('--verbose', dest='verbose', type=bool, \
        help="This argument controls amount of the output. Default is 'False'."+\
             "If it is set to 'True', it will print independent and epistatic"+\
             "score files as well combined combined score file.",
        required=False, default=False)
    
    parser.add_argument('--offset', dest='offset', type=int, \
        help="This argument is used when printing the output."+\
            "If you start from amino acid 50 of the original gene,"+\
            "specify --offset 49 to make the numbering start from 50.",
        required=False, default=0)

    parser.add_argument('--colormap', dest='colormap', type=str, \
        help="Select a colormap from standard matplotlib colormaps",
        required=False, default='turbo_r')
    
    parser.add_argument('--maxcoillength', dest='maxcoillength', type=int, \
        help="If coil length is > maxcoillength, it will use JET values.",
        required=False, default=5)
    
    parser.add_argument('--cvrc', dest='cvrc', type=float, \
        help="Cutoff radius for CV (Circular Variance) calculation. Default is 7.0 Angstrom in default.conf file.)",
        required=False, default=None)
    
    # parser.add_argument('--coilbreakerlength', dest='coilbreakerlength', type=int, \
    #     help="Length of coil breaking S or T residues between C(S|T)C pattern.",
    #     required=False, default=1)
    
    args = parser.parse_args()

    arg_dict = vars(args)

    #This part is for just to get rid of an unnecessary argument (-f fastafile.fasta)!
    if (args.fastaFile == ''):
        args.fastaFile = args.input

    if (args.fastaFile != args.input):
        print("@>ERROR: There is a problem Houston!")
        print("@>ERROR: Your  main input file (obligatory file) and the file you")
        print("@>ERROR: specify after -f (or --fastaFile) should be identical!")
        sys.exit(-1)

    check_argument_groups(parser, arg_dict, '--retrievingMethod', ['--blastFile','--fastaFile'])
 
    # Check flag arguments
    if args.input is None:
        parser.error("PRESCOTT requires an input alignment.")

    return args


def doit(inAli,mutFile,retMet,bFile,fFile,n,N, jetfile, pdbfile, normWeightMode,\
         alphabet, verbosity, offset, colormap, CVRc, maxCoilLength = 5):
    """
    Perfect explanation for a function: typing the function call exactly!
    doit is basically the main function in disguise!

    """
    debug = 0
    if(mutFile != ''):
        simple = False
    else:
        simple = True

    
    prot,seq,nl=extractQuerySeq(inAli)

    if(pdbfile == None):
        createPDB(prot,seq)
    else:
        if(os.path.exists(prot+".pdb")):
            shutil.move(prot+".pdb", prot+"_original.pdb")
            shutil.copy2(pdbfile, prot+".pdb")
        else:
            shutil.copy2(pdbfile, prot+".pdb")

    print("query protein: "+prot)
    
    if( (normWeightMode != 'tjet') and \
        (normWeightMode != 'cv') and \
        (normWeightMode != 'pc') and \
        (normWeightMode != 'max') and \
        (normWeightMode != 'tjetormax') and \
        (normWeightMode != 'sstjetormaxtwocomponent') and \
        (normWeightMode != 'sstjetormaxthirdchanged') and \
        (normWeightMode != 'sstjetormaxthirdpcstar') and \
        (normWeightMode != 'sstjetormax')):
        print("ERROR: normWeightMode can only be 'tjet', 'cv', 'pc', "+\
              "'max', 'tjetormax' or 'sstjetormax'!")
        sys.exit(-1)

    structure = parsePDB(prot+".pdb")
    calphas = structure.select('name CA')
    
    chains = list(set(calphas.getChids()))
    
    #print(chains)

    # pdb_file = pdb.PDBFile.read(prot+".pdb")
    # chains = list(set(pdb_file.get_chains()))
    if((jetfile) == None):
        #I intend to run JET2 completely externally!!
        #It is too much buggy and it has too many dependencies. 
        #Using it with a Docker or Singularity may be the best solution!
        print("computing conservation levels...")
        launchJET(prot,retMet,bFile,fFile, pdbfile, chains, n,N,nl, CVRc)
        print("done")
    else:
        print("using previously calculated JET2 data from "+jetfile+"...")
        
        #Doing this copy operation just due to a peculiar behaviour of JET2.
        #According to JET2, all input files must have the same name obtained 
        #from the fasta file right after the > symbol.
        #Weird but true!
        if(jetfile!=prot+"_jet.res"):
            shutil.copy2(jetfile, prot+"_jet.res")
        print("done")

    if((normWeightMode=='sstjetormax')):
        if (pdbfile == None):
            print("ERROR: There is not any pdb file.")
            sys.exit(-1)
        else:
            calculateSecondaryStructure(pdbfile)
            countCoilSegments(pdbfile+".dssp")

            df = pd.read_table(prot+"_jet.res", sep="\s+")

            df2 = pd.read_table(pdbfile+".dssp.new", header=None, sep=",")
            df2.columns = ['pos', 'ss', 'length']

            mergedRes = pd.merge(df, df2, on ='pos', right_index=False)

            if(debug):
                print(df['pos'])
                print(pdbfile+".dssp")
                print(os.getcwd())
                print(df2)            
                print(mergedRes)

            sstjetormaxList = []
            # maxCoilLength = 5
            print("WARNING: Max. coil length = {}".format(maxCoilLength))
            for index, row in mergedRes.iterrows():
                if(row['ss']=='C') and (row['length']>maxCoilLength):
                    sstjetormaxList.append(row['trace'])
                else:
                    maxVal = max([((row['trace']+row['pc'])/2.0), ((row['trace']+row['cv'])/2.0), ((row['cv']+row['pc'])/2.0)])
                    sstjetormaxList.append(maxVal)
            #sstjetormaxList=rankSortProteinData(sstjetormaxList, inverted=False)
            df['sstjetormax'] = sstjetormaxList
            df['sstjetormax'] = df['sstjetormax'].round(decimals = 4)
            df.to_csv(prot+"_jet.res", header=True, index=None, sep='\t', mode='w')
            #sys.exit(-1)

    if((normWeightMode=='sstjetormaxtwocomponent')):
        if (pdbfile == None):
            print("ERROR: There is not any pdb file.")
            sys.exit(-1)
        else:
            calculateSecondaryStructure(pdbfile)
            countCoilSegments(pdbfile+".dssp")

            df = pd.read_table(prot+"_jet.res", sep="\s+")

            df2 = pd.read_table(pdbfile+".dssp.new", header=None, sep=",")
            df2.columns = ['pos', 'ss', 'length']

            mergedRes = pd.merge(df, df2, on ='pos', right_index=False)

            if(debug):
                print(df['pos'])
                print(pdbfile+".dssp")
                print(os.getcwd())
                print(df2)            
                print(mergedRes)

            sstjetormaxList = []
            # maxCoilLength = 5
            print("WARNING: Max. coil length = {}".format(maxCoilLength))
            for index, row in mergedRes.iterrows():
                if(row['ss']=='C') and (row['length']>maxCoilLength):
                    sstjetormaxList.append(row['trace'])
                else:
                    maxVal = max([((row['trace']+row['pc'])/2.0), ((row['trace']+row['cv'])/2.0)])
                    sstjetormaxList.append(maxVal)
            #sstjetormaxList=rankSortProteinData(sstjetormaxList, inverted=False)
            df['sstjetormaxtwocomponent'] = sstjetormaxList
            df['sstjetormaxtwocomponent'] = df['sstjetormaxtwocomponent'].round(decimals = 4)
            df.to_csv(prot+"_jet.res", header=True, index=None, sep='\t', mode='w')
            #sys.exit(-1)

    if((normWeightMode=='sstjetormaxthirdchanged')):
        if (pdbfile == None):
            print("ERROR: There is not any pdb file.")
            sys.exit(-1)
        else:
            calculateSecondaryStructure(pdbfile)
            countCoilSegments(pdbfile+".dssp")

            df = pd.read_table(prot+"_jet.res", sep="\s+")

            df2 = pd.read_table(pdbfile+".dssp.new", header=None, sep=",")
            df2.columns = ['pos', 'ss', 'length']

            mergedRes = pd.merge(df, df2, on ='pos', right_index=False)

            if(debug):
                print(df['pos'])
                print(pdbfile+".dssp")
                print(os.getcwd())
                print(df2)            
                print(mergedRes)

            sstjetormaxList = []
            # maxCoilLength = 5
            print("WARNING: Max. coil length = {}".format(maxCoilLength))
            for index, row in mergedRes.iterrows():
                if(row['ss']=='C') and (row['length']>maxCoilLength):
                    sstjetormaxList.append(row['trace'])
                else:
                    maxVal = max([((row['trace']+row['pc'])/2.0), ((row['trace']+row['cv'])/2.0), ((row['trace']))])
                    sstjetormaxList.append(maxVal)
            #sstjetormaxList=rankSortProteinData(sstjetormaxList, inverted=False)
            df['sstjetormaxthirdchanged'] = sstjetormaxList
            df['sstjetormaxthirdchanged'] = df['sstjetormaxthirdchanged'].round(decimals = 4)
            df.to_csv(prot+"_jet.res", header=True, index=None, sep='\t', mode='w')
    if((normWeightMode=='sstjetormaxthirdpcstar')):
        if (pdbfile == None):
            print("ERROR: There is not any pdb file.")
            sys.exit(-1)
        else:
            from collections import OrderedDict
            pcStar = OrderedDict()
            pcStar={'A':0.38,
                    'V':0.54,
                    'L':0.45,
                    'I':0.60,
                    'P':0.18,
                    'F':0.50,
                    'W':0.27,
                    'M':0.40,
                    'G':0.36,
                    'S':0.23,
                    'T':0.24,
                    'C':0.45,
                    'Y':0.15,
                    'N':0.13,
                    'Q':0.07,
                    'D':0.15,
                    'E':0.18,
                    'K':0.04,
                    'R':0.02,
                    'H':0.17}
            oneLetter2ThreeLetters = {'C': 'CYS', 'D': 'ASP', 'S': 'SER',  'Q': 'GLN', 'K': 'LYS',
                                    'I': 'ILE', 'P': 'PRO', 'T': 'THR', 'F': 'PHE', 'N': 'ASN', 
                                    'G': 'GLY',  'H': 'HIS', 'L': 'LEU', 'R': 'ARG', 'W': 'TRP', 
                                    'A': 'ALA', 'V': 'VAL', 'E': 'GLU', 'Y': 'TYR', 'M': 'MET'}

            threeLetters2oneLetter = {v: k for k, v in oneLetter2ThreeLetters.items()}

            calculateSecondaryStructure(pdbfile)
            countCoilSegments(pdbfile+".dssp")

            df = pd.read_table(prot+"_jet.res", sep="\s+")
            pcstarList = []
            for index, row in df.iterrows():
                print(row['pos'], row['AA'])
                singleLetterCode = threeLetters2oneLetter[row['AA']]
                pcstarList.append(pcStar[singleLetterCode])
            import numpy as np
            df['pcstar'] = list(np.array(pcstarList)/(np.array(pcstarList).max()))

            print(df)

            df2 = pd.read_table(pdbfile+".dssp.new", header=None, sep=",")
            df2.columns = ['pos', 'ss', 'length']

            mergedRes = pd.merge(df, df2, on ='pos', right_index=False)

            if(debug):
                print(df['pos'])
                print(pdbfile+".dssp")
                print(os.getcwd())
                print(df2)            
                print(mergedRes)

            sstjetormaxList = []
            # maxCoilLength = 5
            print("WARNING: Max. coil length = {}".format(maxCoilLength))
            for index, row in mergedRes.iterrows():
                if(row['ss']=='C') and (row['length']>maxCoilLength):
                    sstjetormaxList.append(row['trace'])
                else:
                    maxVal = max([((row['trace']+row['pc'])/2.0), ((row['trace']+row['cv'])/2.0), (   (row['cv']+row['pcstar'])/2.0   )])
                    sstjetormaxList.append(maxVal)
            #sstjetormaxList=rankSortProteinData(sstjetormaxList, inverted=False)
            df['sstjetormaxthirdpcstar'] = sstjetormaxList
            df['sstjetormaxthirdpcstar'] = df['sstjetormaxthirdpcstar'].round(decimals = 4)

            df.to_csv(prot+"_jet.res", header=True, index=None, sep='\t', mode='w')
            #sys.exit(-1)

    # #If a real pdb file is given, calculate dfi for the residues. 
    # if(((normWeightMode=='tracemovingaverage'))):
    #     print("Calculating trace moving average per residue!")
    #     # tma = tracemovingaverage
    #     df = pd.read_csv(prot+"_jet.res", delimiter=r"\s+") 
    #     print(df.columns)
    #     # Get the window of series
    #     # of observations of specified window size
    #     window_size = 3
    #     windows = df['trace'].rolling(window_size)
        
    #     # Create a series of moving
    #     # averages of each window
    #     moving_averages = windows.mean()
    #     tma = moving_averages

    #     print(df)

    #     df['tracemovingaverage'] = tma.round(4)
    #     #The first two elements are just copied bc they don't exist due to the moving average. 
    #     df['tracemovingaverage'].iloc[0] = df['trace'].iloc[0].round(4)
    #     df['tracemovingaverage'].iloc[1] = df['trace'].iloc[1].round(4)
    #     df.to_csv(prot+"_jet.res", header=True, index=None, sep='\t', mode='w')

    #     print(df)
    # #If a real pdb file is given, calculate or get Bfactors for the residues. 
    # if(((normWeightMode=='bfactor') or (normWeightMode=='maxtracebfactor') or (normWeightMode=='maxbfactortrace'))):
    #     isCalc = True
    #     if (pdbfile == None):
    #         print("ERROR: There is not any pdb file to calculate or get Bfactors!")
    #         sys.exit(-1)
    #     else:        
    #         if (isCalc):
    #             print("Computing Bfactors from the user-provided pdb file using ANM with all modes.")
    #             #Original
    #             # bfactors = calcBfactors(pdbfile, outfile=None, nmodes=None, \
    #             #           attenuate="true", ranksorted="true", inverted=False)
    #             bfactors = calcBfactors(pdbfile, outfile=None, nmodes=10, \
    #                         attenuate="false", ranksorted="true", inverted=True)
    #         else:
    #             print("Getting Bfactors from the user-provided pdb file.")
    #             bfactors = getBfactors(pdbfile, outfile=None,\
    #                         attenuate="true", ranksorted="true", inverted=False)

    #         bfactors = bfactors
    #         df = pd.read_table(prot+"_jet.res")
    #         print(df)

    #         df['bfactor'] = bfactors.round(4)
    #         df.to_csv(prot+"_jet.res", header=True, index=None, sep='\t', mode='w')

    #         print(df)
        
    launchPred(prot, inAli, mutFile, normWeightMode, alphabet)

    #Do Python plotting here
    #TODO: Eventually, I will do the map plotting with a completely independent 
    #      module and call the module here! demust module was created!
    #TODO: Mark the original (wildtype) residue locations with a dot or something
    #      special to show the original amino acid.
    #TODO: You can even put letters on the top line like in EVmutation output. 
    

    if(simple):
        print("generating the plots...")
        #Check if the normalized data files were created. 
        if(os.path.exists(prot+"_normPred_evolEpi.txt") and verbosity==True):
            gemmeData = parseGEMMEoutput(prot+"_normPred_evolEpi.txt", verbose=False)
            sequenceLength = len(gemmeData[0])
            beginning = 1
            end = sequenceLength
            if(sequenceLength>500):
                rowLength = 500
                numberOfImageChunks = int(int(sequenceLength)/int(rowLength))                
                for i in range(numberOfImageChunks):
                    plotGEMMEmatrix(gemmeData, prot+"_normPred_evolEpi"+"_part_"+str(i+1), \
                        i*rowLength + beginning, \
                        (i+1)*rowLength + beginning -1,\
                        colorMap='Blues_r', offSet=i*rowLength + offset, pixelType='square',\
                        sequence=prot+".fasta", isColorBarOn=True)
                if(sequenceLength%rowLength != 0):
                    plotGEMMEmatrix(gemmeData, prot+"_normPred_evolEpi"+"_part_"+str(i+2), \
                        (i+1)*rowLength + beginning, \
                        end,\
                        colorMap='Blues_r', offSet=(i+1)*rowLength + offset, pixelType='square',\
                        sequence=prot+".fasta", isColorBarOn=True)
            else:
                plotGEMMEmatrix(gemmeData, prot+"_normPred_evolEpi", beginning, end,\
                        colorMap='Blues_r', offSet=offset, pixelType='square',\
                        sequence=prot+".fasta", isColorBarOn=True)
        
        #Check if the normalized data files were created. 
        if(os.path.exists(prot+"_normPred_evolInd.txt") and verbosity==True):
            gemmeData = parseGEMMEoutput(prot+"_normPred_evolInd.txt", verbose=False)
            sequenceLength = len(gemmeData[0])
            beginning = 1
            end = sequenceLength
            if(sequenceLength>500):
                rowLength = 500
                numberOfImageChunks = int(int(sequenceLength)/int(rowLength))                
                for i in range(numberOfImageChunks):
                    plotGEMMEmatrix(gemmeData, prot+"_normPred_evolInd"+"_part_"+str(i+1), \
                        i*rowLength + beginning, \
                        (i+1)*rowLength + beginning -1,\
                        colorMap='Greens_r', offSet=i*rowLength + offset, pixelType='square',\
                        sequence=prot+".fasta", isColorBarOn=True)
                if(sequenceLength%rowLength != 0):
                    plotGEMMEmatrix(gemmeData, prot+"_normPred_evolInd"+"_part_"+str(i+2), \
                        (i+1)*rowLength + beginning, \
                        end,\
                        colorMap='Greens_r', offSet=(i+1)*rowLength + offset, pixelType='square',\
                        sequence=prot+".fasta", isColorBarOn=True)
            else:
                plotGEMMEmatrix(gemmeData, prot+"_normPred_evolInd",beginning, end,\
                        colorMap='Greens_r', offSet=offset, pixelType='square',\
                        sequence=prot+".fasta", isColorBarOn=True)

        #Check if the normalized data files were created. 
        if(os.path.exists(prot+"_normPred_evolCombi.txt")):        
            gemmeData = parseGEMMEoutput(prot+"_normPred_evolCombi.txt", verbose=False)
            sequenceLength = len(gemmeData[0])
            beginning = 1
            end = sequenceLength
            if(sequenceLength>500):
                rowLength = 500
                numberOfImageChunks = int(int(sequenceLength)/int(rowLength))                
                for i in range(numberOfImageChunks):
                    plotGEMMEmatrix(gemmeData, prot+"_normPred_evolCombi"+"_part_"+str(i+1), \
                        i*rowLength + beginning, \
                        (i+1)*rowLength + beginning -1,\
                        colorMap=colormap, offSet=i*rowLength + offset, pixelType='square',\
                        sequence=prot+".fasta", isColorBarOn=True)
                if(sequenceLength%rowLength != 0):
                    plotGEMMEmatrix(gemmeData, prot+"_normPred_evolCombi"+"_part_"+str(i+2), \
                        (i+1)*rowLength + beginning, \
                        end,\
                        colorMap=colormap, offSet=(i+1)*rowLength + offset, pixelType='square',\
                        sequence=prot+".fasta", isColorBarOn=True)
                    
            else:
                plotGEMMEmatrix(gemmeData, prot+"_normPred_evolCombi",beginning, end,\
                        colorMap=colormap, offSet=offset, pixelType='square',\
                        sequence=prot+".fasta", interactive=False, isColorBarOn=True)

            #Convert standard combined output to (1 minus) ranksorted transposed format to increase
            #legibility and interpretibility.
            gemmeDF = pd.read_table(prot+"_normPred_evolCombi.txt", sep="\s+")
            # print(gemmeDF)
            ######################################################################
            # Convert the DataFrame to a NumPy matrix
            from scipy.stats import rankdata
            import numpy as np
            #matrix = gemmeDF.to_numpy()
            maxValue =np.nanmax(gemmeDF.to_numpy())
            # print(maxValue)
            matrix = gemmeDF.to_numpy(na_value=maxValue)
            # Use rankdata to get the ranks for all values in the matrix
            ranks = 1.0 - rankdata(matrix, method='average')/len(matrix.flatten())

            # Reshape the ranks back to the original matrix shape
            ranks_matrix = ranks.reshape(matrix.shape)

            # Convert the ranks matrix back to a DataFrame
            df_ranks = pd.DataFrame(ranks_matrix, columns=gemmeDF.columns, index=gemmeDF.index)
            # df_ranks.set_index(gemmeDF.index.tolist(), inplace=True)
            # print(df_ranks)

            gemmeDFtrans = df_ranks.T
            ######################################################################

            # gemmeDFtrans = gemmeDF.T

            # print(gemmeDF)
            gemmeDFtrans.columns = gemmeDFtrans.columns.str.upper()
            aaAndPosition = []
            oldNamesList = gemmeDF.columns.tolist()
            for i in range(len(list(seq))):
                aaAndPosition.append(list(seq)[i]+str(i+1+offset))
            # gemmeDFtrans = pd.DataFrame(gemmeDFtrans, index=aaAndPosition)
            gemmeDFtrans.rename(index = dict(map(lambda i,j : (i,j) , oldNamesList,aaAndPosition)), inplace=True)
            # gemmeDFtrans.set_axis(aaAndPosition)
            # gemmeDFtrans.rename(index=aaAndPosition, inplace=True)
            # gemmeDFtrans['pos'] = aaAndPosition
            #print(df['pos'])
            gemmeDFtrans.to_csv(prot+"_normPred_evolCombiTransposedRanksorted.csv", float_format='%.2f', na_rep='NaN')
            #sys.exit(-1)

        else:
            print("ERROR: "+prot+"_normPred_evolCombi.txt file does not exist!")
            print("       Can not generate "+prot+"_normPred_evolCombi.png file!")
        print("done")

    cleanTheMess(prot,bFile,fFile, chainID=chains[0], verbosity=verbosity)

def main():
    """
    Main function (and yes, I know. The name and the function documentation are ingenious :). 
    """
    tic = time.perf_counter()

    args = parse_command_line()

    # If a structure file is provided, utilize structural information. 
    if(args.pdbfile==None):
        args.normweightmode='tjet'
        print("Using '{}' as normalization weight.".format(args.normweightmode))
    else:
        args.normweightmode='sstjetormaxtwocomponent'
        print("Using '{}' as normalization weight.".format(args.normweightmode))

    doit(args.input, args.mutations, args.retrievingMethod, args.blastFile,\
         args.fastaFile, args.nIter,args.NSeqs, args.jetfile, args.pdbfile,\
         args.normweightmode, args.alphabet, args.verbose, args.offset, \
         args.colormap, args.cvrc, maxCoilLength=args.maxcoillength)

    toc = time.perf_counter()
    
    print(f"ESCOTT computation finished in {toc - tic:0.4f} seconds!")

if (__name__ == '__main__'):
    main()
    

