Returns a dict pdb_id -> chain(s) -> chain and SCOPe details.
            This is the main function for getting details for a PDB chain. If there is an associated SCOPe entry for this
            chain then this function returns the most information.
            internal_function_call is used to prevent potential infinite loops

def get_chain_details(self, pdb_id, chain = None, internal_function_call = False, pfam_scop_mapping = {}):
        ''' Returns a dict pdb_id -> chain(s) -> chain and SCOPe details.
            This is the main function for getting details for a PDB chain. If there is an associated SCOPe entry for this
            chain then this function returns the most information.
            internal_function_call is used to prevent potential infinite loops
        '''

        query = '''
            SELECT DISTINCT scop_node.id AS scop_node_id, scop_node.*, pdb_entry.code, pdb_chain_id, pdb_chain.chain, pdb_chain.is_polypeptide, pdb_entry.description AS ChainDescription, pdb_release.resolution
            FROM `link_pdb`
            INNER JOIN scop_node on node_id=scop_node.id
            INNER JOIN pdb_chain ON pdb_chain_id = pdb_chain.id
            INNER JOIN pdb_release ON pdb_release_id = pdb_release.id
            INNER JOIN pdb_entry ON pdb_entry_id = pdb_entry.id
            WHERE pdb_entry.code=%s'''
        if chain:
            query += ' AND pdb_chain.chain=%s'
            parameters=(pdb_id, chain)
        else:
            parameters = (pdb_id, )
        query += ' ORDER BY release_id DESC'

        results = self.execute_select(query, parameters = parameters)
        if not results:
            if self.fallback_on_failures and not internal_function_call:
                # Fallback - use any Pfam accession numbers associated with the chain to get partial information
                #            Note: this fallback has another fallback in case none of the Pfam entries exist in SCOPe
                searched_deeper = True
                return self.get_chain_details_by_pfam(pdb_id, chain)
            else:
                return None

        # I am making the assumption here that sids are consistent through releases i.e. that if d1aqt_1 is used in release
        # 3 then it will be used for any other releases where the domain is named
        sid_map = {}
        for r in results:
            sid = r['sid']
            c_id = r['chain']
            if not(sid_map.get(sid)) or sid_map[sid] == ' ':
                sid_map[sid] = c_id
        chain_to_sid_map = {}
        for k, v in sid_map.iteritems():
            chain_to_sid_map[v] = chain_to_sid_map.get(v, set())
            chain_to_sid_map[v].add(k)

        leaf_node_chains = set()
        searched_deeper = False
        if pdb_id and chain:
            leaf_node_chains.add(chain)
        else:
            pdb_chain_ids = self.get_list_of_pdb_chains(pdb_id)
            if pdb_chain_ids:
                leaf_node_chains = pdb_chain_ids
            else:
                return None

        leaf_nodes = {}
        for c in leaf_node_chains:
            if c in chain_to_sid_map:
                for sid in chain_to_sid_map[c]:
                    leaf_nodes[(c, sid)] = None


        # Only consider the most recent records
        for r in results:
            chain_id = r['chain']
            sid = r['sid']
            k = (chain_id, sid)
            if (not leaf_nodes.get(k)) or (r['release_id'] > leaf_nodes[k]['release_id']):
                leaf_nodes[k] = r

        # Older revisions of SCOPe have blank chain IDs for some records while newer revisions have the chain ID
        # The best solution to avoid redundant results seems to be to remove all blank chain records if at least one
        # more recent named chain exists. There could be some nasty cases - we only keep the most recent unnamed chain
        # but this may correspond to many chains if the PDB has multiple chains since we only look at the chain ID.
        # I think that it should be *unlikely* that we will have much if any bad behavior though.
        for k1, v2 in leaf_nodes.iteritems():
            if k1[0] == ' ':
                release_id_of_blank_record = leaf_nodes[k1]['release_id']
                for k2, v2 in leaf_nodes.iteritems():
                    if k2[0] != ' ':
                        assert(k2[0].isalpha() and len(k2[0]) == 1)
                        if v2['release_id'] > release_id_of_blank_record:
                            del leaf_nodes[k1] # we are modifying a structure while iterating over it but we break immediately afterwards
                            break

        d = {}
        for chain_sid_pair, details in leaf_nodes.iteritems():
            chain_id = chain_sid_pair[0]
            sid = chain_sid_pair[1]
            if sid.strip() == '':
                colortext.warning('FOUND AN EMPTY SID FIELD')
            assert(sid == details['sid'])

            # Get the details for all chains
            if details:

                if d.get('resolution'):
                    assert(d['resolution'] == details['resolution'])
                else:
                    d['resolution'] = details['resolution']

                d['chains'] = d.get('chains', {})
                if d['chains'].get(chain_id):
                    assert(d['chains'][chain_id]['is_polypeptide'] == details['is_polypeptide'])
                    assert(d['chains'][chain_id]['chain_description'] == details['ChainDescription'])
                else:
                    d['chains'][chain_id] = {}
                    d['chains'][chain_id]['is_polypeptide'] = details['is_polypeptide']
                    d['chains'][chain_id]['chain_description'] = details['ChainDescription']

                d['chains'][chain_id]['domains'] = d['chains'][chain_id].get('domains', {})
                domain_information = dict(
                    #pdb_id = details['code'],
                    #chain = details['chain'],
                    #is_polypeptide = details['is_polypeptide'],
                    #chain_description = details['ChainDescription'],
                    sunid = details['sunid'],
                    sccs = details['sccs'],
                    sid = details['sid'],
                    scop_release_id = details['release_id'],
                    SCOPe_sources = 'SCOPe',
                    SCOPe_search_fields = 'link_pdb.pdb_chain_id',
                    SCOPe_trust_level = 1
                )

                for k, v in sorted(self.levels.iteritems()):
                    domain_information[v] = None

                pfam = None
                level, parent_node_id = details['level_id'], details['parent_node_id']
                pfam = pfam or self.get_pfam_for_node(details['scop_node_id'])

                # Store the top-level description
                domain_information[self.levels[level]] = details['description']

                # Wind up the level hierarchy and retrieve the descriptions
                c = 0
                while level > 2:
                    parent_details = self.execute_select('SELECT * FROM scop_node WHERE id=%s', parameters = (parent_node_id,))
                    assert(len(parent_details) <= 1)
                    if parent_details:
                        parent_details = parent_details[0]
                        level, parent_node_id = parent_details['level_id'], parent_details['parent_node_id']
                        pfam = pfam or self.get_pfam_for_node(parent_details['id'])
                        domain_information[self.levels[level]] = parent_details['description']
                    else:
                        break
                    # This should never trigger but just in case...
                    c += 1
                    if c > 20:
                        raise Exception('There is a logical error in the script or database which may result in an infinite lookup loop.')
                domain_information['Pfam'] = pfam

                # Fill in the residue range data
                domain_information['pdbe_residue_range'] = None
                sifts_object = self.get_sifts(pdb_id)
                if sifts_object:
                    colortext.message(pdb_id)
                    region_mapping = sifts_object.region_mapping
                    ps_map = sifts_object.pfam_scop_mapping or {}
                    for k, v in ps_map.iteritems():
                        pfam_scop_mapping[k] = pfam_scop_mapping.get(k, set())
                        pfam_scop_mapping[k] = pfam_scop_mapping[k].union(v.get_matches('SCOP'))

                    residue_ranges = region_mapping.get(chain_id, {}).get('SCOP', {}).get(str(details['sunid']))
                    if residue_ranges:
                        residue_ranges = ', '.join(['%d-%d' % (t[0], t[1]) for t in residue_ranges])
                    domain_information['pdbe_residue_range'] = residue_ranges

                d['chains'][chain_id]['domains'][sid] = domain_information

            else:
                if self.fallback_on_failures and not(internal_function_call) and not(searched_deeper):
                    fallback_results = self.get_chain_details_by_pfam(pdb_id, chain_id)
                    if fallback_results and fallback_results.get(chain_id):
                        domain_information = fallback_results[chain_id]
        return d