"""Functions to build a structure from a sequence in the alignment file.
   Usually called by setting automodel.generate_method."""

from modeller.alignment import alignment
from modeller.selection import selection

def _set_seq_id(mdl, aln):
    """Set the model's seq_id field using the most similar template.
       This is done automatically by transfer_xyz, but generate methods
       that don't use transfer_xyz to create the model from the templates
       will need to do this manually."""
    target = aln[-1]
    seq_id = 0.
    for known in aln[:-1]:
        seq_id = max(seq_id, known.get_sequence_identity(target))
    mdl.seq_id = seq_id

def generate_xyz(mdl, aln):
    """Build coordinates from scratch (ignore templates)"""

    mdl.read_top_par()
    mdl.create_topology(aln)
    mdl.build(initialize_xyz=True, build_method='3D_INTERPOLATION')
    mdl.make_valid_pdb_coordinates()
    _set_seq_id(mdl, aln)


def transfer_xyz(mdl, aln):
    """Build structure by copying equivalent coordinates from the templates"""

    # If initial malign3d was requested, orient the template structures but
    # then restore the original alignment
    if mdl.initial_malign3d:
        aln.clear()
        aln.append(file=mdl.alnfile, align_codes=mdl.knowns)
        aln.malign3d(fit=False, gap_penalties_3d=(0, 4))
        mdl.read_alignment(aln)
    mdl.read_top_par()
    mdl.create_topology(aln)
    mdl.transfer_xyz(aln, cluster_cut=-1.0)
    mdl.build(initialize_xyz=False, build_method='INTERNAL_COORDINATES')
    _rebuild_bad_rings(mdl)
    mdl.make_valid_pdb_coordinates()

def _detect_bad_ring(r, ring_atoms):
    """Return True if the given residue contains a bad phenyl ring.
       Given an ordered list of the 6 atoms in the ring, measure three atom
       pair distances across the ring. In an ideal structure, these should each
       be around 2.6-2.7 angstroms. If they're substantially less than this,
       mark the ring as 'bad'."""
    for i in range(3):
        try:
            a1 = r.atoms[ring_atoms[i]]
            a2 = r.atoms[ring_atoms[i + 3]]
        except KeyError:
            continue
        dx = a1.x - a2.x
        dy = a1.y - a2.y
        dz = a1.z - a2.z
        d = dx*dx + dy*dy + dz*dz
        if d < 5.0:
            return True

def _unbuild_ring(r, atoms):
    s = selection()
    for a in atoms:
        try:
            s.add(r.atoms[a])
        except KeyError:
            continue
    s.unbuild()

def _rebuild_bad_rings(mdl):
    """Rebuild the structure of any distorted phenyl ring, from internal
       coordinates. This is done because the optimizer sometimes has a hard
       time recovering the ideal geometry if given badly distorted rings."""
    rings = {'PHE': (('CG', 'CD1', 'CE1', 'CZ', 'CE2', 'CD2'),
                     ('HD1', 'HE1', 'HZ', 'HE2', 'HD2')),
             'TRP': (('CD2', 'CE3', 'CZ3', 'CH2', 'CZ2', 'CE2'),
                     ('HE3', 'HZ3', 'HH2', 'HZ2')),
             'TYR': (('CG', 'CD1', 'CE1', 'CZ', 'CE2', 'CD2'),
                     ('HD1', 'HE1', 'OH', 'HH', 'HE2', 'HD2'))}
    to_rebuild = []
    for r in mdl.residues:
        if r.name in rings:
            ring_atoms, extra_atoms = rings[r.name]
            if _detect_bad_ring(r, ring_atoms):
                to_rebuild.append(r)
                _unbuild_ring(r, ring_atoms + extra_atoms)
    if len(to_rebuild) > 0:
        print("The following %d residues contain 6-membered rings with "
              "poor geometries\nafter transfer from templates. Rebuilding "
              "rings from internal coordinates:\n   %s" \
              % (len(to_rebuild),
                 "\n   ".join([str(r) for r in to_rebuild])))
        mdl.build(initialize_xyz=False, build_method='INTERNAL_COORDINATES')

def read_xyz(mdl, aln):
    """Read in the initial structure from an existing file"""

    # Create two copies of the template sequence:
    a = alignment(mdl.env)
    a.append(file=mdl.alnfile, align_codes=[mdl.sequence]*2)

    # Use the initial model as the first structure in the alignment:
    a[0].prottyp = 'structureX'
    a[0].atom_file = a[0].code = mdl.my_inifile

    mdl.read_top_par()
    mdl.create_topology(aln)

    # Get model coordinates from the initial model, making sure that the
    # sequence is correct and any missing atoms are filled in:
    mdl.transfer_xyz(a, cluster_cut=-1.0)
    mdl.build(initialize_xyz=False, build_method='INTERNAL_COORDINATES')

    # Note that transfer_xyz will set mdl.seq_id to 100%, which is not what
    # we want. So calculate it correctly here.
    _set_seq_id(mdl, aln)
