from phaser import residue_substitution
from phaser import residue_topology as topology
from phaser import rsam

from iotbx.pdb import amino_acid_codes

# Common macromolecule types
PROTEIN_THREE_LETTER = (
    amino_acid_codes.one_letter_given_three_letter_modified_aa.keys()
    + amino_acid_codes.one_letter_given_three_letter.keys()
    )

NUCLEIC_THREE_LETTER = (
    " DA",        # Adenine
    " DC",        # Cytosine
    " DG",        # Guanine
    " DT",        # Thymine
    )


PROTEIN_ONE_LETTER = (
    amino_acid_codes.one_letter_given_three_letter_modified_aa.values()
    + amino_acid_codes.one_letter_given_three_letter.values()
    )

NUCLEIC_ONE_LETTER = (
    "A",        # Adenine
    "C",        # Cytosine
    "G",        # Guanine
    "T",        # Thymine
    )

PROTEIN_MAINCHAIN_ATOMS = (
    " N  ",
    " CA ",
    " C  ",
    " O  ",
    " OXT",
    )

PROTEIN_COMMON_RESIDUE_SIDECHAIN_TOPOLOGY_FOR = {
    "A": topology.ALA,   # ALA
    "C": topology.CYS,   # CYS
    "D": topology.ASP,   # ASP
    "E": topology.GLU,   # GLU
    "F": topology.PHE,   # PHE
    "G": topology.GLY,   # GLY
    "H": topology.HIS,   # HIS
    "I": topology.ILE,   # ILE
    "K": topology.LYS,   # LYS
    "L": topology.LEU,   # LEU
    "M": topology.MET,   # MET
    "N": topology.ASN,   # ASN
    "P": topology.PRO,   # PRO
    "Q": topology.GLN,   # GLN
    "R": topology.ARG,   # ARG
    "S": topology.SER,   # SER
    "T": topology.THR,   # THR
    "V": topology.VAL,   # VAL
    "W": topology.TRP,   # TRP
    "Y": topology.TYR,   # TYR
    }

PROTEIN_UNKNOWN_TOPOLOGY = ( ( " CB ", () ), )

PROTEIN_SIDECHAIN_TOPOLOGIES = [
  PROTEIN_COMMON_RESIDUE_SIDECHAIN_TOPOLOGY_FOR.get( resname, PROTEIN_UNKNOWN_TOPOLOGY )
  for resname in PROTEIN_ONE_LETTER
  ]

PROTEIN_MAINCHAIN_VAN_DER_WAALS_RADII = (
    1.35, # N
    1.50, # CA
    1.50, # C
    1.34, # O
    1.34, # OXT
    )

PROTEIN_COMMON_SIDECHAIN_VAN_DER_WAALS_RADII_FOR = {
    "A": {
        " CB ": 1.70,
        },
    "C": {
        " CB ": 1.70,
        " SG ": 1.70,
        },
    "D": {
        " CB ": 1.70,
        " CG ": 1.70,
        " OD1": 1.35,
        " OD2": 1.35,
        },
    "E": {
        " CB ": 1.70,
        " CG ": 1.70,
        " CD ": 1.50,
        " OE1": 1.35,
        " OE2": 1.35,
        },
    "F": {
        " CB ": 1.70,
        " CG ": 1.50,
        " CD1": 1.50,
        " CD2": 1.50,
        " CE1": 1.50,
        " CE2": 1.50,
        " CZ ": 1.50,
        },
    "G": {},
    "H": {
        " CB ": 1.70,
        " CG ": 1.70,
        " ND1": 1.35,
        " CD2": 1.50,
        " CE1": 1.50,
        " NE2": 1.35,
        },
    "I": {
        " CB ": 1.70,
        " CG1": 1.70,
        " CG2": 1.70,
        " CD1": 1.70,
        },
    "K": {
        " CB ": 1.70,
        " CG ": 1.70,
        " CD ": 1.70,
        " CE ": 1.70,
        " NZ ": 1.35,
        },
    "L": {
        " CB ": 1.70,
        " CG ": 1.70,
        " CD1": 1.70,
        " CD2": 1.70,
        },
    "M": {
        " CB ": 1.70,
        " CG ": 1.70,
        " SD ": 1.80,
        " CE ": 1.70,
        },
    "N": {
        " CB ": 1.70,
        " CG ": 1.70,
        " OD1": 1.35,
        " ND2": 1.35,
        },
    "P": {
        " CB ": 1.70,
        " CG ": 1.70,
        " CD ": 1.45,
        },
    "Q": {
        " CB ": 1.70,
        " CG ": 1.70,
        " CD ": 1.50,
        " OE1": 1.35,
        " NE2": 1.35,
        },
    "R": {
        " CB ": 1.70,
        " CG ": 1.70,
        " CD ": 1.70,
        " NE ": 1.35,
        " CZ ": 1.50,
        " NH1": 1.35,
        " NH2": 1.35,
        },
    "S": {
        " CB ": 1.70,
        " OG ": 1.35,
        },
    "T": {
        " CB ": 1.70,
        " OG1": 1.35,
        " CG2": 1.70,
        },
    "V": {
        " CB ": 1.70,
        " CG1": 1.70,
        " CG2": 1.70,
        },
    "W": {
        " CB ": 1.70,
        " CG ": 1.50,
        " CD1": 1.50,
        " CD2": 1.50,
        " NE1": 1.35,
        " CE2": 1.50,
        " CE3": 1.50,
        " CZ2": 1.50,
        " CZ3": 1.50,
        " CH2": 1.50,
        },
    "Y": {
        " CB ": 1.70,
        " CG ": 1.50,
        " CD1": 1.50,
        " CD2": 1.50,
        " CE1": 1.50,
        " CE2": 1.50,
        " CZ ": 1.50,
        " OH ": 1.35,
        },    
    }

PROTEIN_UNKNOWN_SIDECHAIN_VAN_DER_WAALS_RADII = {}

PROTEIN_SIDECHAIN_VAN_DER_WAALS_RADII = [
  PROTEIN_COMMON_SIDECHAIN_VAN_DER_WAALS_RADII_FOR.get( resname, PROTEIN_UNKNOWN_SIDECHAIN_VAN_DER_WAALS_RADII )
  for resname in PROTEIN_ONE_LETTER
  ]

PROTEIN_COMMON_RESIDUE_WEIGHT_FOR = {
    "A": 89.09,        # Ala
    "C": 121.16,        # Cys
    "D": 133.10,        # Asp
    "E": 147.13,        # Glu
    "F": 165.19,        # Phe
    "G": 75.07,        # Gly
    "H": 155.15,        # His
    "I": 131.17,        # Ile
    "K": 146.19,        # Lys
    "L": 131.17,        # Leu
    "M": 149.21,        # Met
    "N": 132.12,        # Asn
    "P": 115.13,        # Pro
    "Q": 146.14,        # Gln
    "R": 174.20,        # Arg
    "S": 105.09,        # Ser
    "T": 119.12,        # Thr
    "V": 117.15,        # Val
    "W": 204.23,        # Trp
    "Y": 181.19,        # Tyr
    }

PROTEIN_UNKNOWN_RESIDUE_WEIGHT = 0

PROTEIN_RESIDUE_WEIGHTS = [
  PROTEIN_COMMON_RESIDUE_WEIGHT_FOR.get( resname, PROTEIN_UNKNOWN_RESIDUE_WEIGHT )
  for resname in PROTEIN_ONE_LETTER
  ]

PROTEIN_COMMON_RESIDUE_H_COUNT_FOR = {
    "A": 7,        # Ala
    "C": 7,        # Cys
    "D": 7,        # Asp
    "E": 9,        # Glu
    "F": 11,        # Phe
    "G": 5,        # Gly
    "H": 9,        # His
    "I": 13,        # Ile
    "K": 14,        # Lys
    "L": 13,        # Leu
    "M": 11,        # Met
    "N": 8,        # Asn
    "P": 9,        # Pro
    "Q": 10,        # Gln
    "R": 14,        # Arg
    "S": 7,        # Ser
    "T": 9,        # Thr
    "V": 11,        # Val
    "W": 12,        # Trp
    "Y": 11,        # Tyr
    }

PROTEIN_UNKNOWN_RESIDUE_H_COUNT = 0

PROTEIN_RESIDUE_H_COUNTS = [
  PROTEIN_COMMON_RESIDUE_H_COUNT_FOR.get( resname, PROTEIN_UNKNOWN_RESIDUE_H_COUNT )
  for resname in PROTEIN_ONE_LETTER
  ]


class TopologyCache(object):
    """
    Caches topology-related instances for quick access
    """
    
    def __init__(self):
        
        self.sidechain_atom_for = {}
        self.mainchain_distance_for = {}
        
        
    def get_sidechain_atom_for(self, sidechain): 
            
        return self.sidechain_atom_for.setdefault(
            sidechain,
            topology.SidechainAtom( topology = sidechain )
            )
        
        
    def get_mainchain_distance_for(self, sidechain):
        
        return self.mainchain_distance_for.setdefault(
            sidechain,
            topology.MainchainDistance(
                topology = sidechain,
                names = self.get_sidechain_atom_for( sidechain = sidechain ).names
                )
            )
        
        
    def get_mapping_between(self, source, target):
        
        return topology.SidechainCorrespondence( source = source, target = target )


# Class to encapsulate macromolecule-specific information
class MacromoleculeType(object):
    """
    Provides access to macromolecule-specific data
    """
    
    def __init__(
        self,
        name,
        one_letter_codes,
        three_letter_codes,
        molecular_weights,
        hydrogen_counts,
        consecutivity,
        mainchain_atom_names,
        mainchain_atom_radii,
        sidechain_topologies,
        sidechain_atom_radii_dicts,
        secondary_structure_analyser,
        similarity_matrix_suite,
        min_length,
        sidechain_completion = [],
        unknown_one_letter = "X",
        unknown_three_letter = "UNK",
        unknown_molecular_weight = 0,
        unknown_hydrogen_count = 0,
        unknown_sidechain_topology = (),
        unknown_atom_radius = 1.70,
        condensation_weight_loss = 0,
        specific_volume = None,
        ):
        
        self.name = name
        self.one_letter_codes = one_letter_codes
        self.three_letter_codes = three_letter_codes
        
        self.molecular_weights = molecular_weights
        self.hydrogen_counts = hydrogen_counts
        self.condensation_weight_loss = condensation_weight_loss
        self.specific_volume = specific_volume
        
        self.consecutivity = consecutivity
        
        self.mainchain_atom_names = mainchain_atom_names 
        self.mainchain_atom_radii = mainchain_atom_radii
        self.sidechain_topologies = sidechain_topologies
        self.sidechain_atom_radii_dicts = sidechain_atom_radii_dicts
        
        self.secondary_structure_analyser = secondary_structure_analyser
        self.similarity_matrix_suite = similarity_matrix_suite
        self.min_length = min_length
        self.sidechain_completion = sidechain_completion
        
        self.unknown_one_letter = unknown_one_letter
        self.unknown_three_letter = unknown_three_letter
        self.unknown_molecular_weight = unknown_molecular_weight
        self.unknown_hydrogen_count = unknown_hydrogen_count
        self.unknown_sidechain_topology = unknown_sidechain_topology
        self.unknown_atom_radius = unknown_atom_radius
        
        # Topology cache
        self.topology_cache = TopologyCache()
        
        
    def one_letter_for(self):
        
        return dict( zip( self.three_letter_codes, self.one_letter_codes ) )
    
    
    def three_letter_for(self):
        
        return dict( zip( self.one_letter_codes, self.three_letter_codes ) )
        

    def recognize_chain_type(self, chain, confidence = 0.9):
        
        residues = chain.residue_groups()
        
        if len( residues ) < self.min_length:
            return False
        
        all_count = len( residues )
        known_codes = set( self.three_letter_codes )
        
        known_count = len(
            [ rg for rg in residues if rg.atom_groups()[0].resname in known_codes ]
            )
        
        return confidence * all_count <= known_count
    
    
PROTEIN = MacromoleculeType(
    name = "protein",
    one_letter_codes = PROTEIN_ONE_LETTER,
    three_letter_codes = PROTEIN_THREE_LETTER,
    molecular_weights = PROTEIN_RESIDUE_WEIGHTS,
    hydrogen_counts = PROTEIN_RESIDUE_H_COUNTS,
    consecutivity = rsam.StructuralConsecutivity(
        pre_bond_atom = PROTEIN_MAINCHAIN_ATOMS[2],
        post_bond_atom = PROTEIN_MAINCHAIN_ATOMS[0],
        distance_cutoff = 1.70
        ),
    mainchain_atom_names = PROTEIN_MAINCHAIN_ATOMS,
    mainchain_atom_radii = PROTEIN_MAINCHAIN_VAN_DER_WAALS_RADII,
    sidechain_topologies = PROTEIN_SIDECHAIN_TOPOLOGIES,
    sidechain_atom_radii_dicts = PROTEIN_SIDECHAIN_VAN_DER_WAALS_RADII,
    secondary_structure_analyser = rsam.DSSPInterface,
    similarity_matrix_suite = residue_substitution.protein,
    min_length = 2,
    sidechain_completion = [ rsam.ProteinSidechainCBCompletion ],
    unknown_molecular_weight = PROTEIN_UNKNOWN_RESIDUE_WEIGHT,
    unknown_hydrogen_count = PROTEIN_UNKNOWN_RESIDUE_H_COUNT,
    unknown_sidechain_topology = PROTEIN_UNKNOWN_TOPOLOGY,
    condensation_weight_loss = 18,
    specific_volume = 0.74,
    )

KNOWN = [ PROTEIN ]

SOLVENT = MacromoleculeType(
    name = "solvent",
    one_letter_codes = ( "X", "X", "X" ),
    three_letter_codes = ( "HOH", "WAT", "0WO" ),
    molecular_weights = (),
    hydrogen_counts = (),
    consecutivity = lambda left, right: False,
    mainchain_atom_names = (),
    mainchain_atom_radii = (),
    sidechain_topologies = (),
    sidechain_atom_radii_dicts = (),
    secondary_structure_analyser = None,
    similarity_matrix_suite = residue_substitution.unknown,
    min_length = 0
    )

UNKNOWN = MacromoleculeType(
    name = "unknown",
    one_letter_codes = (),
    three_letter_codes = (),
    molecular_weights = (),
    hydrogen_counts = (),
    consecutivity = lambda left, right: False,
    mainchain_atom_names = (),
    mainchain_atom_radii = (),
    sidechain_topologies = (),
    sidechain_atom_radii_dicts = (),
    secondary_structure_analyser = None,
    similarity_matrix_suite = residue_substitution.unknown,
    min_length = 0
    )

    
def determine(chain, confidence = 0.9):
    
    for mmt in KNOWN:
        if mmt.recognize_chain_type( chain = chain, confidence = confidence ):
            return mmt
        
    return UNKNOWN


