#!/usr/bin/env python
"""Extend an existing chain of nucleotides."""

# Copyright 2012 Kevin Keating
# 
# Licensed under the Educational Community License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the
# License. You may obtain a copy of the License at
# 
# http://www.osedu.org/licenses/ECL-2.0
# 
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an "AS IS"
# BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
# or implied. See the License for the specific language governing
# permissions and limitations under the License.

from coot import add_status_bar_text, clear_pending_picks, imol_refinement_map
from coot import user_defined_click_py as user_defined_click

from selectMapDialog import selectMapDialog
from citationPopup import createCitationPopup
from pseudoMolecule import PseudoMolecule
import rotamerize #we only need clearPendingRotamerize, but if we import that function directly into this namespace
    #we'll wind up with circular dependencies
from traceGui import TraceGui
from reportInputErrors import reportPDB2Error, reportInsCodeError, STANDARD_BASES, reportModifiedNucError


__menuItem = None #the "Add to terminal nts..." menuItem object
waitingForClicks = False #whether or not we are waiting for the user to pick an atom


def pickAtoms():
    """Prompt the user to pick an atoms to extend a stretch of nucleotides.  Alternatively, if
    we're already waiting for the user to pick an atom, then cancel the pending pick.
    
    ARGUMENTS:
        None
    RETURNS:
        None
    """
    
    #create the citation pop-up if necessary
    createCitationPopup()
    
    #make sure that we're not waiting on a pending rotamerize call
    rotamerize.clearPendingRotamerize()
    
    global waitingForClicks
    
    if waitingForClicks:
        #if we're already in the middle of an extend chain call, then cancel the pending call
        print "Rotamerize cancelled"
        add_status_bar_text("Extend chain cancelled")
        clear_pending_picks() #tell Coot to stop waiting for the user to click on atoms
        __setMenuToExtendChain()
        waitingForClicks = False
    
    else:
        #if we're not in the middle of a extend chain call, then start one
        
        #make sure that there is a refinement map set
        if imol_refinement_map() == -1:
            print "No refinement map set for RCrane extend chain"
            selectMapDialog(pickAtoms)
            return
        
        print "Click on a nucleotide to extend"
        add_status_bar_text("Pick a nucleotide [Ctrl Left-mouse rotates the view]...")
        __setMenuToCancel()
        waitingForClicks = True
        user_defined_click(1, extendChainFromAtomSpec)
    

def extendChainFromAtomSpec(atomSpec):
    """Rotamerize a stretch of nucleotides
    
    ARGUMENTS:
        atomSpec - an atom spec specifying the start of the chain extension
    RETURNS:
        False if there is a problem with the selected nucleotide
        None otherwise
    """
    
    #stop waiting for the user to cancel the rotamerize
    __setMenuToExtendChain()
    global waitingForClicks
    waitingForClicks = False
    
    #print "In extendChainFromAtomSpec"
    #print "atomSpec =", atomSpec
    
    (mol, chain, resNum, insCode, atom, alt) = atomSpec[1:]
    atom = atom.strip()
    try:
        pseudoMol = PseudoMolecule(createFromMolecule = mol, chain = chain)
    except PseudoMoleculeError, err:
        #if creating the PseudoMolecule raises an error, then it means that molecule we clicked on contains
        #anisotropic temperature records and we're using a version of Coot that's older than rev 3631
        #so clear_and_update_molecule can't handle anisotropic records
        
        print err
        #add_status_bar_text(str(err))
        
        #Since this isn't an intuitive problem (i.e. it's far less obvious for the user than "you clicked on
        #two different chains"), we create a dialog box instead of a status bar message
        createRCraneErrorDialog(str(err) + "\n\nRotamerization canceled.")
        
        return False
    
    resNumFull = str(resNum) + str(insCode)
    resIndex = pseudoMol.resIndex(resNum)
    
    #figure out what direction to extend in
    connectedToNext = pseudoMol.connectedToNextFromIndex(resIndex)
    connectedToPrev = pseudoMol.connectedToPrevFromIndex(resIndex)
    curResIsPhosOnly = pseudoMol.isOnlyPhosGroupFromIndex(resIndex)
    
    nextResIsPhosOnly = False
    if connectedToNext:
        nextResIsPhosOnly = pseudoMol.isOnlyPhosGroupFromIndex(resIndex+1)
    
    
    extendDir = None #the direction to extend the chain in
    
    if curResIsPhosOnly:
        if connectedToPrev:
            #we're building on a 3' phosphate
            extendDir = 3
        else:
            #the selected residue is an isolated phosphate group, so we can't do anything
            print "Nucleotide " + resNumFull + " is an isolated phosphate group.  Cannot extend."
            add_status_bar_text("Nucleotide " + resNumFull + " is an isolated phosphate group.  Cannot extend.")
            return False
    elif nextResIsPhosOnly:
        if connectedToPrev:
            #we're at the 3' end of the chain and there is a 3' phosphate
            extendDir = 3
            resIndex += 1 #increment the resIndex so that it points to the real last residue of the segment
            curResIsPhosOnly = True
            resNumFull = pseudoMol.resNumFull(resIndex) #update the residue name in case we need to use it for later error messages
            
        else:
            #we're dealing with a single nt containing a 3' phosphate
            #we need to decide direction based on the atom clicked
            
            extendDir = __decideDirectionFromAtom(atom, resNumFull)
            if not extendDir: return False #if we couldn't determine a direction from the atom, then cancel
    
    elif connectedToNext and connectedToPrev:
        print "Nucleotide " + resNumFull + " is not at the end of a chain"
        add_status_bar_text("Nucleotide " + resNumFull + " is not at the end of a chain")
        return False
    elif connectedToNext:
        extendDir = 5
    elif connectedToPrev:
        extendDir = 3
    else:
        #we're dealing with a single nt not containing a 3' phosphate
        #we need to decide direction based on the atom clicked
        extendDir = __decideDirectionFromAtom(atom, resNumFull)
        if not extendDir: return False #if we couldn't determine a direction from the atom, then cancel
    
    
    
    #make sure that the residue we're going to extend doesn't have an insertion code
    #   (also check the next and previous residues)
    if pseudoMol.resNum(resIndex)[1] != "":
        reportInsCodeError(resNumFull)
        return False
    if connectedToNext and pseudoMol.resNum(resIndex+1)[1] != "":
        reportInsCodeError(pseudoMol.resNumFull(resIndex+1))
        return False
    if connectedToPrev and pseudoMol.resNum(resIndex-1)[1] != "":
        reportInsCodeError(pseudoMol.resNumFull(resIndex-1))
        return False
    
    #figure out which residue we should be checking for PDB2 naming and missing glycosidic bond atoms
    if curResIsPhosOnly:
        resIndexToCheck = resIndex - 1
        resNumFullToCheck = pseudoMol.resNumFull(resIndex - 1)
    else:
        resIndexToCheck = resIndex
        resNumFullToCheck = resNumFull
    
    #make sure that this nucleotide isn't mdofied
    resType = pseudoMol.resTypeFromIndex(resIndexToCheck)
    if resType not in STANDARD_BASES:
        reportModifiedNucError(resNumFullToCheck, resType)
        return False
    
    #make sure that the molecule (or at least the end of it we're going to be building on) uses PDB3 atom naming
    if pseudoMol.checkPDB2FromIndex(resIndexToCheck):
        reportPDB2Error(resNumFullToCheck)
        return False
    
    #the current residue must have a glycosidic bond
    baseType = pseudoMol.resTypeFromIndex(resIndexToCheck)
    if baseType == "A" or baseType== "G":
        glyN = "N9"
    else:
        #we already know that the base isn't modified because we checked above
        glyN = "N1"
    
    if pseudoMol.getAtomCoordsFromIndex("C1'", resIndexToCheck) is None:
        print "Nucleotide " + resNumFullToCheck + " does not have a C1' atom.  Cannot extend chain."
        add_status_bar_text("Nucleotide " + resNumFullToCheck + " does not have a C1' atom.  Cannot extend chain.")
        return False
    if pseudoMol.getAtomCoordsFromIndex(glyN, resIndexToCheck) is None:
        print "Nucleotide " + resNumFullToCheck + " does not have an " + glyN + " atom.  Cannot extend chain."
        add_status_bar_text("Nucleotide " + resNumFullToCheck + " does not have an " + glyN + " atom.  Cannot extend chain.")
        return False
    
    #make sure that this isn't an isolated nt without any phosphates
    curPhosCoords = pseudoMol.getPhosCoordsFromIndex(resIndex) #only used for sanity checking
    if not connectedToNext and not connectedToPrev and curPhosCoords is None:
        print "Nucleotide " + resNumFull + "is an isolated nucleotides without any phosphates.  Cannot extend chain."
        add_status_bar_text("Nucleotide " + resNumFull + "is an isolated nucleotides without any phosphates.  Cannot extend chain.")
        return False
    
    #make sure that we're not building 3'->5' on a nucleotide without a 3' phosphate (because we can't determine pseudotorsions w/o a 3' phos)
    if extendDir == 5 and not connectedToNext:
        print "Cannot extend nucleotide " + resNumFull + " in the 5' direction without a 3' phosphate."
        print "  Please add a 3' phosphate by extending the chain in the 3' direction."
        add_status_bar_text("Cannot extend nucleotide " + resNumFull + " in the 5' direction without a 3' phosphate.")
        return False
    
    print "About to extend the chain in the " + str(extendDir) + "' direction"
    
    #tell the PseudoMolecule object where to insert new residues
    pseudoMol.setResInsertionPoint(resIndex)
    
    #save the molecule state so we can restore it if we cancel the extend
    pseudoMol.saveMoleculeState()
    
    TraceGui(direction = extendDir, existingMolecule = pseudoMol, resIndexToExtend = resIndex)
    


ATOMS3P = frozenset(["C3'", "O3'"])
    #atoms that count as a 3' selection
ATOMS5P = frozenset(["P", "OP1", "OP2", "OP3", "O5'", "C5'", "C4'"])
    #atoms that count as a 5' selection
def __decideDirectionFromAtom(atomName, resNum):
    """Determine the direction to extend the chain based on the atom name (for use with isolated nuclotides)
    
    ARGUMENTS:
        atomName - the atom name
        resNum   - the residue number of the current nucleotide (used in reporting errors)
    RETURNS:
        False if the direction cannot be determined from the atom name
        3 or 5 otherwise
    EFFECTS:
        if the direction cannot be determined from the atom name, an error will be reported to the user
        (and False will be returned)
    """
    
    if atomName in ATOMS3P:
        return 3
    elif atomName in ATOMS5P:
        return 5
    else:
        print "Nucleotide " + resNum + " is an isolated nucleotide.  Please select either the O5' or"
        print "O3' atom to extend in the 5' or 3' direction, respectively."
        add_status_bar_text("Must select either O5' or O3' of isolated nucleotides.")
        return False


def storeMenuItem(menuItem, resetLabel = False):
    """ARGUMENTS:
        menuItem   - the gtk MenuItem to store
    OPTIONAL ARGUMENTS:
        resetLabel    - whether we should reset the menu label to "Extend chain..."
                        (and also clear all pending atom picks in Coot)
                        This is intended for use when we're reloading RCrane
                        Defaults to False
    RETURNS:
        None
    EFFECTS:
        stores menuItem in the module-level variable __menuItem
    """
    
    global __menuItem
    __menuItem = menuItem
    
    #if we're being called during an RCrane reload, then reset everything back to the appropriate starting position
    global waitingForClicks
    if resetLabel:
        waitingForClicks = False #this is redundant with module initialization, but can't hurt to do
                                 #in case this function is called in an unexpected way
        __setMenuToExtendChain()
        clear_pending_picks()


def __setMenuToExtendChain():
    """Set the menu entry labels to "Extend chain..."
    
    ARGUMENTS:
        None
    RETURNS:
        None
    """
    
    if __menuItem is not None:
        __menuItem.get_child().set_text("Extend chain...")


def __setMenuToCancel():
    """Set the menu entry label to "Cancel extend chain..."
    
    ARGUMENTS:
        None
    RETURNS:
        None
    """
    
    if __menuItem is not None:
        __menuItem.get_child().set_text("Cancel extend chain...")


def clearPendingExtendChain():
    """Cancel any pending extend chain call (intended to be called by rotamerize)
    
    ARGUMENTS:
        None
    RETURNS:
        True if there was a pending extend chain call
        False otherwise
    """
    
    global waitingForClicks
    
    if waitingForClicks:
        #if we're already in the middle of a extend chain call, then cancel the pending extend chain
        print "Extend chain cancelled"
        clear_pending_picks() #tell Coot to stop waiting for the user to click on atoms
        __setMenuToExtendChain()
        waitingForClicks = False
        return True
    else:
        return False
