#  Copyright (C) 2021 The authors of Py-ChemShell
#
#  This file is part of Py-ChemShell.
#
#  Py-ChemShell is free software: you can redistribute it and/or modify
#  it under the terms of the GNU Lesser General Public License as
#  published by the Free Software Foundation, either version 3 of the
#  License, or (at your option) any later version.
#
#  Py-ChemShell is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU Lesser General Public License for more details.
#
#  You should have received a copy of the GNU Lesser General Public
#  License along with Py-ChemShell.  If not, see
#  <http://www.gnu.org/licenses/>.

__author__ = 'You Lu <you.lu@stfc.ac.uk>'


from collections import defaultdict
from sys         import stdout
from numpy       import array, bytes_, char, issubdtype, ndarray, str_, string_
from ...data     import forcefield
from ...dl_py2f  import utils

def charmm2udff(rtf='', prm='', patch={}, output='', version='CHARMM', charmm=forcefield.getFFObj('charmm'), override_default=True):
    '''A CHARMM-to-DL_FIELD converter, saving to output file (.udff)'''

    from os          import path
    from string      import ascii_letters, punctuation
    from ...io.file  import parse
    from ...io.files import udff

    # analyse the CHARMM FF version
    variants = ['carb','cgenff','lipid','prot']
    variant  = 'prot'
    for v in variants:
        if v in str(version).lower():
            vartiant = v
    version_number = str(version).strip(ascii_letters).strip(punctuation)
    selectcases = { '19':'CHARMM19',
                    '22':'CHARMM22_prot',
                    '36':'CHARMM36_{}'.format(variant),
                    ''  :'CHARMM'
                  }
    try:
        version = selectcases[version_number]
    except KeyError:
        print('\n WARNING: unsupported CHARMM version {} specified. Using the default {} instead..'.format(version, 'CHARMM'))
        version = 'CHARMM'

    # output filename
    if not output:
        output = f'_{version}.udff'

    # ChemShell PRM files rather than CHARMM standard files
    try:
        builtin_prms = charmm.builtin_prms
    except:
        builtin_prms = []

    utils.fileutils.archiveFile(output)

    # convert single rtf/prm to list
    if issubclass(type(rtf), str):
        rtf = [rtf]
    if issubclass(type(prm), str):
        prm = [prm]

    # reorder parameter files to use the override_default option
    # place the ChemShell files in the end so that override_existing=True overwrites standard CHARMM data with ChemShell ones
    if override_default:
        prm = [ p for p in prm if p not in builtin_prms ] + [ p for p in prm if p in builtin_prms ]
    else:
        prm = [ p for p in prm if p in builtin_prms ] + [ p for p in prm if p not in builtin_prms ]

    # in case of empty list
    if rtf == ['']:
        print('\n ERROR: charmm2udff() requires at least one RTF filename')
        stdout.flush()
        exit(999)
    if prm == ['']:
        print('\n ERROR: charmm2udff() requires at least one RTF filename')
        stdout.flush()
        exit(999)

    # save data buffer in a dict
    dbuff = dict()

    dict_rename_patch = patch.get('rename', {})

    # loop over topology files
    for r in rtf:
        if not path.isfile(r):
            print("\n ERROR: charmm2udff() cannot find file {}\n".format(r))
            stdout.flush()
            exit(999)
        # topology (.rtf)
        parse(r, fmt='rtf', receiver=dbuff)
        # YL 09/08/2023: '+' signs which indicate connection with the succeeding residue is not  convert
        for k, v in dbuff.items():
            if type(v) is dict:
                if k in patch:
                    v.update({'patch':patch[k]})
                # YL 25/11/2023: get the information of the original resname from which the current one is renamed
                v.update({'renamed_from':dict_rename_patch.get(k, '')})
                for key, val in v.items():
                    if type(val) is ndarray:
                        # purely string/byte array
                        if issubdtype(val.dtype, str_) or issubdtype(val.dtype, string_) or issubdtype(val.dtype, bytes_):
                            if key != 'bond':
                                v.update({key:char.replace(val, '+', '-')})
                        # structured array
                        else:
                            # loop over fields
                            for name in val.dtype.names:
                                try:
                                    val[name] = char.replace(val[name], '+', '-')
#                                    v.update({key:val})
                                except ValueError:
                                    pass
                                except TypeError:
                                    pass

    # loop over parameter files
    for p in prm:
        if not path.isfile(p):
            print("\n ERROR: charmm2udff() cannot find file {}\n".format(p))
            stdout.flush()
            exit(999)
        # parameter (.prm)
        # override_existing=True is default
        parse(p, fmt='prm', receiver=dbuff, override_existing=True)

    if not output.lower().endswith('udff'):
        print("\n charmm2udff() WARNING: output filename had better be of extension .udff to be used by DL_FIELD\n")

#    dbuff.update({'patch':patch})

    # default version is CHARMM36
    dict_data = {version:dbuff}
#    dict_data.update({'patch':patch})

    udff.write(output, dict_data, patch=patch)

    return dict_data


# now place holder only 
def amber2udff(prmtop='', output='', version='CHARMM', charmm=forcefield.getFFObj('charmm'), override_default=True):
    '''A CHARMM-to-DL_FIELD converter, saving to output file (.udff)'''

    return dict_data


# YL 10/2023
def patchRTF(input='', output='_patched.rtf', patch=defaultdict(list), rename={}, ff='charmm', natoms_foreign=1):
    '''Patch residues and return a dict containing instructions of patching to DL_FIELD'''

    from re          import match
    from numpy       import append, arange, array, char, delete, hstack, isin, ndarray, ravel, str_, string_, unique, vstack
    from ...data     import forcefield
    from ...io.files import rtf
    from ...dl_py2f  import utils

    # result dict
    dict_result = {}

    # fetch the FF data module
    ffObj = forcefield.getFFObj(ff)

    # residue renaming
    if len(rename):
        sbuff_renaming = '\n Renaming residues:'
    else:
        sbuff_renaming = ''

    dict_patch_to_data = {}
    if type(input) in [ list, tuple, ndarray ]:
        dict_rtf_inp = rtf.getResidueDicts(input, residues_only=False)
    elif utils.strutils.isStr(input):
        # NB: getResidueDicts() takes a list of paths
        dict_rtf_inp = rtf.getResidueDicts([input], residues_only=False)

    for filename_rtf, d in dict_rtf_inp.items():
        patches = [ p for p in list(d.keys()) if p not in ['residues','mass','decl','defa','auto'] ]
        for p in patches:
            dict_patch_to_data.update({p:d[p]})

    if patch:
        print('\n ---------------')
        print(' Patching RTF...')
        print(' ---------------')

    # this is a dict for all patches and their targets
    dict_resname_to_data = {}
    for pname, targets in patch.items():
        # dict of pairs of resname_target and newname
        if not type(targets) in [ list, ndarray, ndarray ]:
            print('\n Targets of a patch must be defined as a Python list (duplicates allowed), such as ["old_res1","old_res2","old_res1"]')
            continue

        # this is a dict for the current patch only
        dict_index_to_resname = {}
        for i_res, resname_target in enumerate(targets):
            if resname_target in ffObj.residues:
                rtfs, prms, nfounds = ffObj.getFilesOfResidues([resname_target], rtfs=[], prms=[])
                dict_rtf_to_patch = rtf.getResidueDicts(rtfs, residues_only=False)
                for filename_rtf in rtfs:
                    if resname_target not in dict_resname_to_data:
                        dict_residue_data = dict_rtf_to_patch[filename_rtf][resname_target]
                        dict_resname_to_data.update({resname_target:dict_residue_data})
                dict_index_to_resname.update({i_res:resname_target})
                print(f'\n Found \'{resname_target}\' in library. Applying patch \'{pname}\' to form new residue \'{rename.get(resname_target, "")}\'...')

        # delete items:
        to_deletes = dict_patch_to_data[pname].get('delete', array([], dtype=rtf.contents2dtypes['delete']))
        dict_atoms_deleted = defaultdict(list)
        if to_deletes.shape == ():
            to_deletes = to_deletes[None]
        for to_delete in to_deletes:
            entry = to_delete['entry'].upper()
            buffer = to_delete['buffer'].upper()
            # delete atoms
            if entry == 'ATOM':
                dbuff = defaultdict(list) # a buffer for printing
                for b in buffer.split():
                    m = match('[0-9]', b)
                    if m:
                        A = b[m.end():]
                        index_target = int(m.group()) - 1
                        resname_target = dict_index_to_resname.get(index_target, '')
                        dbuff[resname_target].append(A)
                        # for later use of excluding bonds/angles/dihedrals containing deleted atoms
                        dict_residue_data = dict_resname_to_data.get(resname_target, {})
                        dict_atoms_deleted[index_target].append(A)
                        abuff = dict_residue_data.get('atom', array([], dtype=rtf.contents2dtypes['atom']))
                        indices_to_delete = arange(abuff.size)[A==abuff['key']]
                        dict_residue_data.update({'atom':delete(abuff, indices_to_delete)})
                        # also delete data containing the deleted atom
                        selectcases = { 'bond':2, 'double':2, 'triple':2, 'angle':3, 'dihe':4, 'impr':4 }
                        for term in selectcases.keys():
                            data = dict_residue_data.get(term, array([], dtype='U'))
                            incr = selectcases[term]
                            L = [ data[i:][::incr] for i in range(incr) ]
                            abuff = list(zip(*L))
                            for d in abuff:
                                if A in d or '+'+A in d or '-'+A in d:
                                    D = ' '+' '.join([ str(index_target+1)+x.strip() for x in d ])
                                    if term.upper() in to_deletes['entry']:
                                        i_delete = to_deletes['entry'].tolist().index(term.upper())
                                        to_deletes[i_delete] = array([(term.upper(), to_deletes[i_delete]['buffer']+D)], dtype=to_deletes.dtype)
                                    else:
                                        to_deletes = hstack((to_deletes, array([(term.upper(), D)], dtype=to_deletes.dtype)))
                                    
                    # no match of digits
                    else:
                        print(f'\n WARNING: It is unclear from which residue the atom {b} should be deleted')
                for x, y in dbuff.items():
                    print(f'\n Deleting atom(s) from residue {x}:')
                    print(' '+' '.join(y))
# YL 26/11/2023: not in use
#                    # YL 26/11/2023: for later use in deleting atoms in the real fragment object
#                    abuff = array([], dtype='U')
#                    newname = rename.get(x, x)
#                    for z in y:
#                        abuff = hstack((abuff,array([f'# ATOM REMOVE {z}'], dtype='U')))
#                        dict_result.update({newname:abuff})

        for i_delete, to_delete in enumerate(to_deletes):
            entry = to_delete['entry'].upper()
            buffer = to_delete['buffer'].upper()
            # delete bonds
            if entry == 'BOND' or entry == 'DOUBLE' or entry == 'TRIPLE':
                lbuff = buffer.split()
                lbuff = list(zip(lbuff[::2],lbuff[1:][::2]))
                dbuff = defaultdict(list) # a buffer for printing
                for b in lbuff:
                    I, J = b[0], b[1]
                    m, n = match('[0-9]',I), match('[0-9]',J)
                    if m and n:
                        A, B = I[m.end():], J[n.end():]
                        if not (int(m.group())==int(n.group())):
                            continue
                        index_target = int(m.group()) - 1
                        resname_target = dict_index_to_resname.get(index_target, '')
                        dbuff[resname_target].append((A,B))
                        # get existing bonds
                        dict_residue_data = dict_resname_to_data.get(resname_target, {})
                        data = dict_residue_data.get('bond', array([], dtype='U'))
                        abuff = list(zip(data[::2],data[1:][::2]))
                        # exclude A-B and B-A
                        abuff = [ x for x in abuff if x != (A,B) and x != (B,A) ]
#                        abuff = [ x for x in abuff if not any(char.find(x,'+')+1) and not any(char.find(x,'-')+1) ]
                        dict_residue_data.update({'bond':array(ravel(abuff), dtype=data.dtype)})
                    # no match of digits
                    else:
                        pass
                for x, y in dbuff.items():
                    print(f'\n Deleting bond(s) from residue {x}:')
                    print(' '+' '.join([ '-'.join(z) for z in y ]))
            # delete angles
            elif entry == 'ANGLE':
                lbuff = buffer.split()
                lbuff = list(zip(lbuff[::3],lbuff[1:][::3],lbuff[2:][::3]))
                dbuff = defaultdict(list) # a buffer for printing
                for b in lbuff:
                    I, J, K = b[0], b[1], b[2]
                    m, n, o = match('[0-9]',I), match('[0-9]',J), match('[0-9]',K)
                    if m and n and o:
                        A, B, C = I[m.end():], J[n.end():], K[o.end():]
                        if not (int(m.group())==int(n.group())==int(o.group())):
                            continue
                        index_target = int(m.group()) - 1
                        resname_target = dict_index_to_resname.get(index_target, '')
                        dbuff[resname_target].append((A,B,C))
                        # get existing angles
                        dict_residue_data = dict_resname_to_data.get(resname_target, {})
                        data = dict_residue_data.get('angle', array([], dtype='U'))
                        abuff = list(zip(data[::3],data[1:][::3],data[2:][::3]))
                        abuff = [ x for x in abuff if x != (A,B,C) and x != (C,B,A) ]
                        dict_residue_data.update({'angle':array(ravel(abuff), dtype=data.dtype)})
                    # no match of digits
                    else:
                        pass
                for x, y in dbuff.items():
                    print(f'\n Deleting angle(s) from residue {x}:')
                    print(' '+' '.join([ '-'.join(z) for z in y ]))
                    # YL 11/10/2023: angles to delete are treated as a special case because auto-generation of these angle terms after patching must be prevented!
                    #                and this information must be passed to UDFF for DL_FIELD
                    abuff = array([], dtype='U')
                    newname = rename.get(x, x)
                    for z in y:
                        abuff = hstack((abuff,array(['ANGLE REMOVE '+' '.join(z)], dtype='U')))
                        dict_result.update({newname:abuff})

            # delete dihedrals/impropers
            elif entry == 'DIHE' or entry == 'IMPR':
                lbuff = buffer.split()
                lbuff = list(zip(lbuff[::4],lbuff[1:][::4],lbuff[2:][::4],lbuff[3:][::4]))
                dbuff = defaultdict(list) # a buffer for printing
                for b in lbuff:
                    I, J, K, L = b[0], b[1], b[2], b[3]
                    m, n, o, p = match('[0-9]',I), match('[0-9]',J), match('[0-9]',K), match('[0-9]',L)
                    if m and n and o and p:
                        A, B, C, D = I[m.end():], J[n.end():], K[o.end():], L[p.end():]
                        mi, ni, oi, pi = int(m.group()), int(n.group()), int(o.group()), int(p.group())
                        if not (mi==ni==oi==pi):
                            for xi in unique([mi,ni,oi,pi]):
                                index_target = xi - 1
                                resname_target = dict_index_to_resname.get(index_target, '')
                                prefixes = [ '-' if yi != xi else '' for yi in [mi,ni,oi,pi] ]
                                dbuff[resname_target].append((prefixes[0]+A,prefixes[1]+B,prefixes[2]+C,prefixes[3]+D))
                            continue
                        index_target = int(m.group()) - 1
                        resname_target = dict_index_to_resname.get(index_target, '')
                        dbuff[resname_target].append((A,B,C,D))
                        # get existing dihedrals/impropers
                        dict_residue_data = dict_resname_to_data.get(resname_target, {})
                        data = dict_residue_data.get(entry.lower(), array([], dtype='U'))
                        abuff = list(zip(data[::4],data[1:][::4],data[2:][::4],data[3:][::4]))
                        # YL 05/10/2023: it's difficult to delete unwanted from a list by indexing
                        abuff = [ x for x in abuff if x != (A,B,C,D) and x != (D,C,B,A) ]
                        dict_residue_data.update({entry.lower():array(ravel(abuff), dtype=data.dtype)})
                    # no match of digits
                    else:
                        pass
                for x, y in dbuff.items():
                    print(f'\n Deleting dihedral/improper(s) from residue {x}:')
                    print(' '+' '.join([ '-'.join(z) for z in y ]))
                    # YL 11/10/2023: dihedral/impropers to delete are treated as a special case because auto-generation of these terms after patching must be prevented!
                    #                and this information must be passed to UDFF for DL_FIELD
                    newname = rename.get(x, x)
                    abuff = dict_result.get(newname, array([], dtype='U'))
                    for z in y:
                        abuff = hstack((abuff, array(['DIHEDRAL REMOVE '+' '.join(z)], dtype='U')))
                        dict_result.update({newname:abuff})

            else:
                pass

        # add/amend atoms
        atoms_to_add = dict_patch_to_data[pname]['atom']
        dbuff = defaultdict(list) # a buffer for printing
        for atom_to_add in atoms_to_add:
            m = match('[0-9]', atom_to_add['key'])
            if m:
                index_target = int(m.group()) - 1
                if not index_target in dict_index_to_resname:
                    print('\n WARNING: The number of target residues must match the leading index of terms to add')
                resname_target = dict_index_to_resname.get(index_target, '')
                dict_residue_data = dict_resname_to_data.get(resname_target, {})
                abuff = dict_residue_data.get('atom', array([], dtype=rtf.contents2dtypes['atom']))
                # remove the leading index
                atom_to_add['key'] = atom_to_add['key'][m.end():]
                # add missing atoms
                if not atom_to_add['key'] in abuff['key']:
                    dict_residue_data.update({'atom':append(abuff, atom_to_add)})
                # modify existing atoms
                else:
                    dbuff[resname_target].append([f'  {atom_to_add["key"]:<6s}  {atom_to_add["type"]:<6s}  {atom_to_add["charge"]:>9.6f}\n'])
                    abuff[abuff['key']==atom_to_add['key']] = atom_to_add
                    dict_residue_data.update({'atom':abuff})
        print()
        for x, y in dbuff.items():
            print(f' Modified existing atom(s) in {x:<s}:')
            print(f'  {"key":<6s}  {"type":<6s}  {"charge":<s}')
            print(''.join([ ''.join(z) for z in y ]))
        stdout.flush()

        # add bonds
        bonds_to_add = dict_patch_to_data[pname]['bond']
        doubles = dict_patch_to_data[pname].get('double', array([], dtype=bonds_to_add.dtype))
        triples = dict_patch_to_data[pname].get('triple', array([], dtype=bonds_to_add.dtype))
        # we merge bonds, doubles, and triples because DL_FIELD doesn't distinguish them
        bonds_to_add = hstack((bonds_to_add, doubles, triples))
        lbuff = list(zip(bonds_to_add[::2],bonds_to_add[1:][::2]))
        dbuff = defaultdict(list) # a buffer for printing
        for b in lbuff:
            I, J = b[0], b[1]
            m, n = match('[0-9]', I), match('[0-9]', J)
            if m and n:
                # bond as atom names
                A, B = I[m.end():], J[n.end():]
                l_iresidues = [ int(m.group()), int(n.group()) ]
                for i in l_iresidues:
                    index_target = i - 1
                    # in RTF bonds definition only "+" but no "-" is allowed
                    prefixes = [ '-' if x != i else '' for x in l_iresidues ]
                    # only allow `natoms_foreign` atoms from other residues 
                    if prefixes.count('-') > 0:
                        continue
                    pA, pB = prefixes[0]+A, prefixes[1]+B
                    resname_target = dict_index_to_resname.get(index_target, '')
                    # get existing bonds
                    dict_residue_data = dict_resname_to_data.get(resname_target, {})
                    data = dict_residue_data.get('bond', array([], dtype=bonds_to_add.dtype))
                    abuff = list(zip(data[::2],data[1:][::2]))
                    if (pA,pB) not in abuff and (pB,pA) not in abuff:
                        data = hstack((data, array([pA,pB])))
                        dbuff[resname_target].append((pA,pB))
                    # bond with deleted atoms should be excluded (there're repeated operations but i want to reuse the code)
                    abuff = list(zip(data[::2],data[1:][::2]))
                    bonds_valid = []
                    for bond in abuff:
                        if not any(isin(dict_atoms_deleted[index_target], bond)):
                            bonds_valid += [ bond[0], bond[1] ]
                    dict_residue_data['bond'] = array(bonds_valid, dtype=data.dtype)
                else:
                    pass
        for x, y in dbuff.items():
            print(f'\n Adding bond(s) to {x:>s}:')
            print(' '+' '.join([ '-'.join(z) for z in y ]))
        stdout.flush()

        # add angles
        angles_to_add = dict_patch_to_data[pname]['angle']
        lbuff = list(zip(angles_to_add[::3],angles_to_add[1:][::3],angles_to_add[2:][::3]))
        dbuff = defaultdict(list) # a buffer for printing
        for b in lbuff:
            I, J, K = b[0], b[1], b[2]
            m, n, o = match('[0-9]', I), match('[0-9]', J), match('[0-9]', K)
            if m and n and o:
                # angle as atom names
                A, B, C = I[m.end():], J[n.end():], K[o.end():]
                l_iresidues = [ int(m.group()), int(n.group()), int(o.group()) ]
                for i in l_iresidues:
                    index_target = i - 1
                    # in RTF bonds definition only "+" but no "-" is allowed
                    prefixes = [ '-' if x != i else '' for x in l_iresidues ]
                    # only allow `natoms_foreign` atoms from other residues 
                    if prefixes.count('-') > 0:
                        continue
                    pA, pB, pC = prefixes[0]+A, prefixes[1]+B, prefixes[2]+C
                    resname_target = dict_index_to_resname.get(index_target, '')
                    dict_residue_data = dict_resname_to_data.get(resname_target, {})
                    data = dict_residue_data.get('angle', array([], dtype=angles_to_add.dtype))
                    abuff = list(zip(data[::3],data[1:][::3],data[2:][::3]))
                    if (pA,pB,pC) not in abuff and (pC,pB,pA) not in abuff:
                        data = hstack((data, array([pA,pB,pC])))
                        dbuff[resname_target].append((pA,pB,pC))
                    # check again if angles to add don't contain deleted atoms
                    abuff = list(zip(data[::3],data[1:][::3],data[2:][::3]))
                    angles_valid = []
                    for angle in abuff:
                        if not any(isin(dict_atoms_deleted[index_target], angle)):
                            angles_valid += [ angle[0], angle[1], angle[2] ]
                    dict_residue_data['angle'] = array(angles_valid, dtype=data.dtype)
        for x, y in dbuff.items():
            print(f'\n Adding angle(s) to {x:>s}:')
            print(' '+' '.join([ '-'.join(z) for z in y ]))
        stdout.flush()

        # add dihedrals/impropers
        for entry in [ 'dihe', 'impr' ]:
            dihedrals_to_add = dict_patch_to_data[pname].get(entry, array([], dtype=rtf.contents2dtypes[entry]))
            try:
                lbuff = list(zip(dihedrals_to_add[::4],dihedrals_to_add[1:][::4],dihedrals_to_add[2:][::4],dihedrals_to_add[3:][::4]))
            except:
                lbuff = []
            dbuff = defaultdict(list) # a buffer for printing
            for b in lbuff:
                I, J, K, L = b[0], b[1], b[2], b[3]
                m, n, o, p = match('[0-9]', I), match('[0-9]', J), match('[0-9]', K), match('[0-9]', L)
                if m and n and o and p:
                    # dihedral/improper as atom names
                    A, B, C, D = I[m.end():], J[n.end():], K[o.end():], L[p.end():]
                    l_iresidues = [ int(m.group()), int(n.group()), int(o.group()), int(p.group()) ]
                    for i in l_iresidues:
                        index_target = i - 1
                        # DL_FIELD only recognises '-' (i.e., no '+')
                        prefixes = [ '-' if x != i else '' for x in l_iresidues ]
                        # only allow `natoms_foreign` atoms from other residues 
                        if prefixes.count('-') > natoms_foreign:
                            continue
                        pA, pB, pC, pD = prefixes[0]+A, prefixes[1]+B, prefixes[2]+C, prefixes[3]+D
                        resname_target = dict_index_to_resname.get(index_target, '')
                        dict_residue_data = dict_resname_to_data.get(resname_target, {})
                        data = dict_residue_data.get(entry, array([], dtype=dihedrals_to_add.dtype))
                        abuff = list(zip(data[::4],data[1:][::4],data[2:][::4],data[3:][::4]))
                        if (pA,pB,pC,pD) not in abuff and (pD,pC,pB,pA) not in abuff:
                            data = hstack((data, array([pA,pB,pC,pD])))
                            dbuff[resname_target].append((pA,pB,pC,pD))
                        # check again if dihedrals/impropers to add don't contain deleted atoms
                        abuff = list(zip(data[::4],data[1:][::4],data[2:][::4],data[3:][::4]))
                        dihedrals_valid = []
                        for dihedral in abuff:
                            if not any(isin(dict_atoms_deleted[index_target], dihedral)):
                                dihedrals_valid += [ dihedral[0], dihedral[1], dihedral[2], dihedral[3] ]
                        dict_residue_data[entry] = array(dihedrals_valid, dtype=data.dtype)
            for x, y in dbuff.items():
                print(f'\n Adding dihedral/improper(s) to {x:>s}:')
                print(' '+' '.join([ '-'.join(z) for z in y ]))
        stdout.flush()

    # rename residues and update the data dict according to the new resnames
    dict_result.update({'rename':{}})
    for pname, targets in patch.items():
        for i_res, resname_target in enumerate(targets):
            newname = rename.get(resname_target, '')
            if sbuff_renaming:
                sbuff_renaming += f'\n {resname_target:<6s} -> {newname:<6s}'
                # YL 25/11/2023: keep the renaming information for later use
                dict_result['rename'].update({newname:resname_target})
            dict_resname_to_data[newname] = dict_resname_to_data.pop(resname_target, dict_resname_to_data.get(newname, {}))

    # print a summary of renaming
    if sbuff_renaming:
        print(sbuff_renaming)

    stdout.flush()

    rtf.write(output, dict_resname_to_data)

    if patch:
        print('\n ----------------------')
        print(' Finished patching RTF.')
        print(' ----------------------\n')

    return dict_result


