#  ----------------------------------------------------------------------------
#  "THE BEER-WARE LICENSE" (Revision 42):
#  dkratzert@gmx.de> wrote this file.  As long as you retain
#  this notice you can do whatever you want with this stuff. If we meet some day,
#  and you think this stuff is worth it, you can buy me a beer in return.
#  Dr. Daniel Kratzert
#  ----------------------------------------------------------------------------
import os
import re
from collections import namedtuple
from collections.abc import Generator
from contextlib import suppress
from pathlib import Path
from typing import Any, Generator

import gemmi

from fastmolwidget.atoms import atoms
from fastmolwidget.tools import grouper, isnumeric

if hasattr(gemmi, 'set_leak_warnings'):
    gemmi.set_leak_warnings(False)

from gemmi.cif import as_string, Document, Loop, quote

adp = namedtuple('adp', ('label', 'U11', 'U22', 'U33', 'U23', 'U13', 'U12'))


class GemmiError(Exception):
    pass


class CifReader:
    """
    This class holds the content of a cif file, independent of the file parser used.
    """

    def __init__(self, file: Path | str, new_block: str = ''):
        """

        Args:
            file: CIF file to open
            new_block: Create a new block (new file) if a name is given. Otherwise, just
                       the existing document is opened.
        """
        self.chars_ok = True
        self._abs_hkl_details: dict[str, str] = {}
        if isinstance(file, str):
            self.fileobj = Path(file)
        elif isinstance(file, Path):
            self.fileobj = file
        else:
            raise TypeError('The file parameter must be string or Path object.')
        self.current_block = ''
        # I do this in small steps instead of gemmi.cif.read_file() in order to
        # leave out the check_for_missing_values. This way, gemmi reads cif files even
        # with missing values.
        if new_block:
            self.doc: Document = gemmi.cif.Document()
            self.doc.add_new_block(new_block)
        else:
            try:
                self.doc = self.read_file(self.fileobj)
            except Exception as e:
                raise
                raise GemmiError(e)
        # Starting with first block, but can use others with subsequent self._onload():
        self.block: gemmi.cif.Block = self.doc[0]
        self._on_load()

    @property
    def path_base(self) -> Path:
        """
        The absolute path of the current file without file name:
        /foo/bar/baz
        """
        return Path(self.doc.source).resolve().parent

    @property
    def filename(self) -> str:
        """
        The name of the current file without path:
        foo.cif
        """
        return Path(self.doc.source).name or self.fileobj.name

    def get_line_numbers_of_bad_characters(self, filepath: Path):
        line_numbers = []
        for num, line in enumerate(filepath.read_bytes().splitlines(keepends=True)):
            try:
                line.decode('ascii')
            except UnicodeDecodeError:
                line_numbers.append(num)
        return line_numbers

    @property
    def is_multi_cif(self) -> bool:
        return True if len(self.doc) > 1 else False

    def load_this_block(self, index: int) -> None:
        self.block = self.doc[index]
        self.current_block = self.block.name
        self._on_load()

    def load_block_by_name(self, blockname: str) -> None:
        self.block = self.doc.find_block(blockname)
        self.current_block = self.block.name
        self._on_load()

    def delete_block(self, index: int) -> None:
        del self.doc[index]
        self.block: gemmi.cif.Block = self.doc[0]

    def _on_load(self) -> None:
        # will not ok with non-ascii characters in the res file:
        self.chars_ok = True
        try:
            self.doc.check_for_duplicates()
        except RuntimeError as e:
            raise GemmiError(e)
        self.atomic_struct: gemmi.SmallStructure = gemmi.make_small_structure_from_block(self.block)
        # A dictionary to convert Atom names like 'C1_2' or 'Ga3' into Element names like 'C' or 'Ga'
        self._name2elements = dict(zip([x.upper() for x in self.block.find_loop('_atom_site_label')],
                                       [y.upper() for y in self.block.find_loop('_atom_site_type_symbol')],
                                       strict=False))

    def read_file(self, path: Path) -> gemmi.cif.Document:
        """
        Reads a cif file and returns a gemmi document object.
        """
        doc = gemmi.cif.Document()
        doc.source = str(path.resolve())
        doc.parse_file(str(path.resolve()))
        return doc

    def read_string(self, cif_string: str) -> gemmi.cif.Document:
        """
        Reads a cif file from a string and returns a gemmi cif docment.
        :param cif_string: cif as string
        :return: gemmi document
        """
        doc = gemmi.cif.Document()
        doc.parse_string(cif_string)
        return doc

    def cif_as_string(self, without_hkl=False) -> str:
        opt = gemmi.cif.WriteOptions()
        opt.align_pairs = 33
        if without_hkl:
            # return a copy, do not delete hkl from original:
            doc = gemmi.cif.Document()
            doc.parse_string(self.doc.as_string(options=opt))
            for block in doc:
                if block.find_pair_item('_shelx_hkl_file'):
                    block.find_pair_item('_shelx_hkl_file').erase()
                if block.find_pair_item('_shelx_fcf_file'):
                    block.find_pair_item('_shelx_fcf_file').erase()
            return doc.as_string(options=opt)
        else:
            return self.doc.as_string(options=opt)

    def __getitem__(self, item: str) -> str:
        """
        This method returns an empty string when the item value is '?'
        """
        if value := self.block.find_value(item):
            return as_string(value)
        else:
            return ''

    def __setitem__(self, key: str, value: str) -> None:
        """Set a key value pair of the current block.
        Values are automatically encoded from utf-8 and delimited.
        """
        self.set_pair_delimited(key, value)

    def __delitem__(self, key: str) -> None:
        with suppress(AttributeError):
            self.block.find_pair_item(key).erase()

    def __contains__(self, item) -> bool:
        return bool(self.__getitem__(item))

    def __str__(self) -> str:
        return (f"CIF file: {self.fileobj.resolve()!s}\n"
                f"{len(self.doc)} Block(s): {', '.join([x.name for x in self.doc])}\n"
                f"Contains SHELX res file: {True if self.res_file_data else False}\n"
                f"Has {self.natoms()} atoms"
                f", {self.nbonds()} bonds"
                f", {self.nangles()} angles")

    def file_is_there_and_writable(self) -> bool:
        return self.fileobj.exists() and self.fileobj.is_file() and os.access(self.fileobj, os.W_OK)

    def set_pair_delimited(self, key: str, txt: str):
        """
        Converts special characters to their markup counterparts.
        """
        # txt = utf8_to_str(txt)
        try:
            # bad hack to get the numbered values correct
            float(txt)
            self.block.set_pair(key, txt)
        except (TypeError, ValueError):
            # prevent _key '?' in cif:
            if txt == '?':
                self.block.set_pair(key, txt)
            else:
                self.block.set_pair(key, quote(txt))

    def save(self, filename: Path | None = None) -> None:
        """
        Saves the current cif file. It uses the '[basename]-finalcif.cif' suffix by default.
        :param filename:  Name to save cif file to if the default is not sufficient.
        """
        if self.is_empty():
            print(f'DBG> CIF file {filename} is empty.')
            return
        if not self.is_writable(filename):
            raise PermissionError(f'Failed to open {filename.resolve()} for writing: Operation not permitted')
        print('Saving CIF to', Path(filename).resolve())
        options = gemmi.cif.WriteOptions()
        options.prefer_pairs = False
        options.compact = False
        options.align_pairs = 33
        options.align_loops = 15
        options.misuse_hash = False
        self.doc.write_file(str(filename), options=options)

    def is_writable(self, filepath: Path | str) -> bool:
        filepath = Path(filepath)
        try:
            exist = filepath.exists()
            with open(filepath, 'wb+') as f:
                f.write(b'')
            if not exist:
                Path(filepath).unlink(missing_ok=True)
            return True
        except Exception:
            return False

    @property
    def res_file_data(self) -> str:
        try:
            if self['_shelx_res_file']:
                return self['_shelx_res_file']
            elif self['_iucr_refine_instructions_details']:
                return self['_iucr_refine_instructions_details']
            else:
                return ''
        except UnicodeDecodeError:
            # This is a fallback in case _shelx_res_file has non-ascii characters.
            print('File has non-ascii characters. Switching to compatible mode.')
            self.doc = self.read_string(self.fileobj.read_text(encoding='latin1', errors='ignore'))
            self.block = self.doc.sole_block()
            self.chars_ok = False
            return self.block.find_value('_shelx_res_file')

    @property
    def hkl_file(self) -> str:
        hkl_loop: Loop = self.get_loop('_diffrn_refln_index_h')
        if hkl_loop and hkl_loop.width() > 4:
            return self._hkl_from_cif_format(hkl_loop)
        else:
            # returns an empty string if no cif hkl was found:
            return self._hkl_from_shelx()

    def _hkl_from_cif_format(self, hkl_loop: Loop) -> str:
        hkl_list = []
        format_string = '{:>4}{:>4}{:>4}{:>8}{:>8}'
        if hkl_loop.width() == 6:
            format_string = '{:>4}{:>4}{:>4}{:>8}{:>8}{:>4}'
        for line in grouper(hkl_loop.values, hkl_loop.width()):
            hkl_list.append(format_string.format(*line[:6]))
        return '\n'.join(hkl_list)

    def _hkl_from_shelx(self) -> str:
        try:
            if self['_shelx_hkl_file']:
                return self['_shelx_hkl_file']
            elif self['_iucr_refine_reflections_details']:
                return self['_iucr_refine_reflections_details']
            elif self['_xd_hkl_file']:
                return self['_xd_hkl_file']
            else:
                return ''
        except Exception as e:
            print(f'No hkl data found in CIF!, {e}')
            return ''

    @property
    def fab_file(self):
        if self['_shelx_fab_file']:
            return self['_shelx_fab_file']
        return ''

    @property
    def hkl_file_without_foot(self) -> str:
        """Returns a hkl file with no content after the 0 0 0 reflection"""
        zero_reflection_position = self._find_line_of_000(self.hkl_file)
        if zero_reflection_position:
            return '\n'.join(self.hkl_file.splitlines(keepends=False)[:zero_reflection_position + 1])
        else:
            return self.hkl_file

    @staticmethod
    def _find_line_of_000(lines: str) -> int:
        pattern = re.compile(r'^\s+0\s+0\s+0\s+0.*')
        for num, line in enumerate(lines.splitlines(keepends=False)):
            found = pattern.match(line)
            if found and num > 0:
                return num
        return 0

    def normal_search(self, hkl) -> int:
        pattern = re.compile(r'\s+0\s+0\s+0\s+0')
        found = pattern.search(hkl)
        if found:
            zero_reflection_position = found.start()
        else:
            zero_reflection_position = 0
        return zero_reflection_position

    def reversed_search(self, hkl_splitted: list[str]) -> int:
        pattern = re.compile(r'\s+0\s+0\s+0\s+0')
        zero_reflection_position = 0
        for num, line in enumerate(reversed(hkl_splitted)):
            found = pattern.search(line)
            if num > 500:
                # A longer footer is not realistic
                break
            if found:
                zero_reflection_position = len(hkl_splitted) - num
        return zero_reflection_position

    def is_empty(self) -> bool:
        """
        Checks if the currebt CIF contains data, but this works only for single-CIFs!
        """
        if len(self.doc) < 2 and len(self.keys()) + len(self.loops) == 0:
            return True
        return False

    def keys(self) -> list[str]:
        """
        Returns a plain list of keys that are really in this CIF.
        """
        return [x.pair[0] for x in self.block if x.pair is not None]

    def values(self):
        """
        Returns a plain list of keys that are really in this CIF.
        """
        return [x.pair[1] for x in self.block if x.pair is not None]

    @property
    def loops(self) -> list[gemmi.cif.Loop]:
        """
        Returns a list of loops contained in the current block.
        """
        loops = []
        for b in self.block:
            if b.loop:
                loops.append(b.loop)
        return loops

    def add_loop_to_cif(self, loop_tags: list[str], row_values: list[str] | tuple[str]) -> gemmi.cif.Loop:
        gemmi_loop = self.init_loop(loop_tags)
        for row in list(grouper(row_values, len(loop_tags))):
            gemmi_loop.add_row(row)
        return gemmi_loop

    def add_loop_from_columns(self, loop_tags: list[str],
                              column_values: list[list[str]] | tuple[tuple[str]]) -> gemmi.cif.Loop:
        try:
            gemmi_loop = self.init_loop(loop_tags)
            gemmi_loop.set_all_values(column_values)
        except RuntimeError as err:
            raise Exception('Gemmi raised an exception') from err
        return gemmi_loop

    @property
    def n_loops(self) -> int:
        return len(self.loops)

    def get_loop(self, key_in_loop: str) -> gemmi.cif.Loop:
        return self.block.find_loop(key_in_loop).get_loop()

    def get_loop_column(self, key_in_loop: str) -> list:
        return [as_string(x) for x in self.block.find_loop(key_in_loop)]

    def init_loop(self, loop_keywords: list) -> gemmi.cif.Loop:
        return self.block.init_loop('', loop_keywords)

    @property
    def z_value(self) -> float:
        return self.atomic_struct.cell.volume / self.atomic_struct.cell.volume_per_image()

    def _spgr(self) -> gemmi.SpaceGroup:
        if self.symmops and self.symmops != ['']:
            symm_ops = self.symmops
        else:
            symm_ops = self.symmops_from_spgr
        return gemmi.find_spacegroup_by_ops(gemmi.GroupOps([gemmi.Op(o) for o in symm_ops]))

    @property
    def space_group(self) -> str:
        """
        Returns the space group from the symmetry operators.
        spgr.short_name() gives the short name.
        """
        try:
            return self._spgr().xhm()
        except (AttributeError, RuntimeError, ValueError):
            if self['_space_group_name_H-M_alt']:
                return gemmi.cif.as_string(self['_space_group_name_H-M_alt'])
            elif self['_symmetry_space_group_name_H-M']:
                return gemmi.cif.as_string(self['_symmetry_space_group_name_H-M'])
            else:
                return ''

    @property
    def symmops_from_spgr(self) -> list[str]:
        # _symmetry_space_group_name_Hall
        space_group = None
        if self['_space_group_name_H-M_alt']:
            space_group = self['_space_group_name_H-M_alt']
        if self['_symmetry_space_group_name_H-M']:
            space_group = self['_symmetry_space_group_name_H-M']
        if not space_group:
            return []
        ops = [op.triplet() for op in
               gemmi.find_spacegroup_by_name(gemmi.cif.as_string(space_group)).operations()]
        return ops

    @property
    def spgr_number_from_symmops(self) -> int:
        return self._spgr().number

    @property
    def spgr_number(self) -> int:
        if self['_space_group_IT_number'] and isnumeric(self['_space_group_IT_number']):
            return int(self['_space_group_IT_number'])
        elif self['_symmetry_Int_Tables_number'] and isnumeric(self['_symmetry_Int_Tables_number']):
            return int(self['_symmetry_Int_Tables_number'])
        else:
            return self.spgr_number_from_symmops

    @property
    def crystal_system(self) -> str:
        if not self._spgr():
            return ''
        return self._spgr().crystal_system_str()

    @property
    def hall_symbol(self) -> str:
        return self._spgr().hall

    @property
    def hkl_checksum_calcd(self) -> int:
        """
        Calculates the shelx checksum for the hkl file content of a cif file.
        """
        if self.hkl_file:
            return self.calc_checksum(self.hkl_file)
        else:
            return 0

    @property
    def res_checksum_calcd(self) -> int:
        """
        Calculates the shelx checksum for the res file content of a cif file.
        """
        if self.res_file_data:
            return self.calc_checksum(self.res_file_data)
        return 0

    @staticmethod
    def calc_checksum(input_str: str) -> int:
        """
        Calculates the shelx checksum of a cif file.
        The original algorithm was posted by Berthold Stöger on the Bruker Users Mailing list.
        """
        crc_sum = 0
        try:
            bytes_str = input_str.encode('cp1250', 'ignore')
        except Exception:
            bytes_str = input_str.encode('ascii', 'ignore')
        for char in bytes_str:
            # print(char)
            if char > 32:  # ascii 32 is space character
                crc_sum += char
        crc_sum %= 714025
        crc_sum = crc_sum * 1366 + 150889
        crc_sum %= 714025
        crc_sum %= 100000
        return crc_sum

    def rename_data_name(self, newname: str = ''):
        """
        Renames data_ tags to the newname. Also _vrf tags are renamed accordingly.
        http://journals.iucr.org/services/cif/checking/checkfaq.html
        """
        newname = ''.join([i for i in newname if i.isascii()])
        self.block.name = newname
        for item in self.block:
            if item.pair is not None:
                key, value = item.pair
                if key.startswith('_vrf'):
                    newkey = '_' + '_'.join(key.split('_')[1:3]) + '_' + newname
                    self.block.find_pair_item(key).erase()
                    self.block.set_pair(newkey, value)

    @property
    def symmops(self) -> list[str]:
        """
        Reads the symmops from the cif file.
        """
        xyz1 = self.block.find(["_symmetry_equiv_pos_as_xyz"])  # deprecated
        xyz2 = self.block.find(["_space_group_symop_operation_xyz"])  # New definition
        if xyz1:
            return [i.str(0) for i in xyz1]
        elif xyz2:
            return [i.str(0) for i in xyz2]
        else:
            return self.symmops_from_spgr

    @property
    def is_centrosymm(self) -> bool:
        """
        Whether a structure is centrosymmetric or not.
        """
        if not self.symmops or self.symmops == ['']:
            # Do not crash without symmops
            return False
        ops = gemmi.GroupOps([gemmi.Op(o) for o in self.symmops])
        return ops.is_centrosymmetric()

    def atoms(self, without_h: bool = False) -> Generator:
        """
        Atoms from the CIF where values are returned as string like in the CIF with esds.
        """
        labels = self.block.find_loop('_atom_site_label')
        types = self.block.find_loop('_atom_site_type_symbol') or [atoms.get_atomlabel(x) for x in labels]
        x = self.block.find_loop('_atom_site_fract_x')
        y = self.block.find_loop('_atom_site_fract_y')
        z = self.block.find_loop('_atom_site_fract_z')
        part = self.block.find_loop('_atom_site_disorder_group') or ('0',) * len(labels)
        occ = self.block.find_loop('_atom_site_occupancy') or ('1.000000',) * len(labels)
        u_eq = self.block.find_loop('_atom_site_U_iso_or_equiv') or ('.',) * len(labels)
        atom = namedtuple('atom', ('label', 'type', 'x', 'y', 'z', 'part', 'occ', 'u_eq'))
        for label, type, x, y, z, part, occ, u_eq in zip(labels, types, x, y, z,
                                                         part,
                                                         occ,
                                                         u_eq, strict=False):
            if without_h and self.ishydrogen(label):
                continue
            #         0    1   2  3  4   5   6     7
            # yield label, type, x, y, z, part, occ, ueq
            yield atom(label=label, type=type, x=x, y=y, z=z, part=part, occ=occ, u_eq=u_eq)

    @property
    def atoms_fract(self) -> Generator:
        """Atoms with numbers as float values without esd."""
        for at in self.atomic_struct.sites:
            yield [at.label, at.type_symbol, at.fract.x, at.fract.y, at.fract.z, at.disorder_group, at.occ, at.u_iso]

    @property
    def atoms_orth(self) -> Generator:
        atom = namedtuple('atom', ('label', 'type', 'x', 'y', 'z', 'part', 'occ', 'u_eq'))
        cell = self.atomic_struct.cell
        for at in self.atomic_struct.sites:
            x, y, z = at.orth(cell)
            yield atom(label=at.label, type=at.type_symbol, x=x, y=y, z=z,
                       part=at.disorder_group, occ=at.occ, u_eq=at.u_iso)

    def displacement_parameters(self) -> Generator[adp, Any, None]:
        """
        Yields the anisotropic displacement parameters.
        """
        labels = self.block.find_loop('_atom_site_aniso_label')
        u11 = self.block.find_loop('_atom_site_aniso_U_11')
        u22 = self.block.find_loop('_atom_site_aniso_U_22')
        u33 = self.block.find_loop('_atom_site_aniso_U_33')
        u23 = self.block.find_loop('_atom_site_aniso_U_23')
        u13 = self.block.find_loop('_atom_site_aniso_U_13')
        u12 = self.block.find_loop('_atom_site_aniso_U_12')
        for label, u11, u22, u33, u23, u13, u12 in zip(labels, u11, u22, u33, u23, u13, u12, strict=True):
            yield adp(label=label, U11=u11, U22=u22, U33=u33, U12=u12, U13=u13, U23=u23)

    @property
    def hydrogen_atoms_present(self) -> bool:
        for at in self.atomic_struct.sites:
            if at.type_symbol in ('H', 'D'):
                return True
        return False

    @property
    def disorder_present(self) -> bool:
        for at in self.atomic_struct.sites:
            if at.disorder_group:
                return True
        return False

    @property
    def cell(self):
        c = self.atomic_struct.cell
        cell = namedtuple('cell', 'a, b, c, alpha, beta, gamma, volume')
        return cell(c.a, c.b, c.c, c.alpha, c.beta, c.gamma, c.volume)

    def ishydrogen(self, label: str) -> bool:
        """
        Determines if an atom with the name 'label' is a hydrogen atom.
        """
        hydrogen = ('H', 'D')
        if self._iselement(label) in hydrogen:
            return True
        else:
            return False

    def checksymm(self, symm):
        """Add translation of 555 to symmetry elements without translation"""
        if len(symm) == 1 and symm not in {'.', '?'}:
            symm = symm + '_555'
        return symm

    def bonds(self, without_h: bool = False) -> Generator:
        """
        Yields a list of bonds in the cif file.
        """
        label1 = self.block.find_loop('_geom_bond_atom_site_label_1')
        label2 = self.block.find_loop('_geom_bond_atom_site_label_2')
        dist = self.block.find_loop('_geom_bond_distance')
        symm = self.block.find_loop('_geom_bond_site_symmetry_2')
        publ_loop = self.block.find_loop('_geom_bond_publ_flag') or len(label1) * ['?']
        bond = namedtuple('bond', ('label1', 'label2', 'dist', 'symm'))
        for label1, label2, dist, symm, publ in zip(label1, label2, dist, symm, publ_loop, strict=False):
            if (without_h and (self.ishydrogen(label1) or self.ishydrogen(label2))) or self.yes_not_set(publ):
                continue
            else:
                yield bond(label1=label1, label2=label2, dist=dist, symm=self.checksymm(symm))

    def bond_dist(self, pair: str) -> str | None:
        for p in self.bonds():
            if f'{p.label1}-{p.label2}' == pair:
                return p.dist
        return None

    def angle(self, atoms: str) -> str | None:
        for p in self.angles():
            if f'{p.label1}-{p.label2}-{p.label3}' == atoms:
                return p.angle_val
        return None

    def torsion(self, atoms: str) -> str | None:
        for p in self.torsion_angles():
            if f'{p.label1}-{p.label2}-{p.label3}-{p.label4}' == atoms:
                return p.torsang
        return None

    def angles(self, without_H: bool = False) -> Generator:
        label1 = self.block.find_loop('_geom_angle_atom_site_label_1')
        label2 = self.block.find_loop('_geom_angle_atom_site_label_2')
        label3 = self.block.find_loop('_geom_angle_atom_site_label_3')
        angle_val = self.block.find_loop('_geom_angle')
        symm1 = self.block.find_loop('_geom_angle_site_symmetry_1')
        symm2 = self.block.find_loop('_geom_angle_site_symmetry_3')
        publ_loop = self.block.find_loop('_geom_angle_publ_flag') or len(label1) * ['?']
        angle = namedtuple('angle', ('label1', 'label2', 'label3', 'angle_val', 'symm1', 'symm2'))
        for label1, label2, label3, angle_val, symm1, symm2, publ in (
            zip(label1, label2, label3, angle_val, symm1, symm2, publ_loop, strict=False)):
            if ((without_H and (self.ishydrogen(label1) or self.ishydrogen(label2) or self.ishydrogen(label3))) or
                self.yes_not_set(publ)):
                continue
            else:
                yield angle(label1=label1, label2=label2, label3=label3, angle_val=angle_val,
                            symm1=self.checksymm(symm1), symm2=self.checksymm(symm2))

    def _iselement(self, name: str) -> str:
        try:
            return self._name2elements[name.upper()]
        except KeyError:
            return atoms.get_atomlabel(name)

    def natoms(self, without_h: bool = False) -> int:
        return len(tuple(self.atoms(without_h)))

    def nbonds(self, without_h: bool = False) -> int:
        """
        Number of bonds in the cif object, with and without hydrogen atoms.
        """
        return len(tuple(self.bonds(without_h)))

    def nangles(self, without_h: bool = False) -> int:
        """
        Number of bond angles in the cif object, with and without hydrogen atoms.
        """
        return len(tuple(self.angles(without_h)))

    def ntorsion_angles(self, without_h: bool = False) -> int:
        """
        Number of torsion angles in the cif object, with and without hydrogen atoms.
        """
        return len(tuple(self.torsion_angles(without_h)))

    def torsion_angles(self, without_h: bool = False) -> Generator:
        label1 = self.block.find_loop('_geom_torsion_atom_site_label_1')
        label2 = self.block.find_loop('_geom_torsion_atom_site_label_2')
        label3 = self.block.find_loop('_geom_torsion_atom_site_label_3')
        label4 = self.block.find_loop('_geom_torsion_atom_site_label_4')
        torsang = self.block.find_loop('_geom_torsion')
        symm1 = self.block.find_loop('_geom_torsion_site_symmetry_1')
        symm2 = self.block.find_loop('_geom_torsion_site_symmetry_2')
        symm3 = self.block.find_loop('_geom_torsion_site_symmetry_3')
        symm4 = self.block.find_loop('_geom_torsion_site_symmetry_4')
        publ_loop = self.block.find_loop('_geom_torsion_publ_flag') or len(label1) * ['?']
        tors = namedtuple('Torsion',
                          ('label1', 'label2', 'label3', 'label4', 'torsang', 'symm1', 'symm2', 'symm3', 'symm4'))
        for label1, label2, label3, label4, torsang, symm1, symm2, symm3, symm4, publ in zip(label1, label2, label3,
                                                                                             label4,
                                                                                             torsang, symm1, symm2,
                                                                                             symm3,
                                                                                             symm4, publ_loop,
                                                                                             strict=False):
            if ((without_h and (self.ishydrogen(label1) or self.ishydrogen(label2)
                                or self.ishydrogen(label3) or self.ishydrogen(label3))) or self.yes_not_set(publ)):
                continue
            yield tors(label1=label1, label2=label2, label3=label3, label4=label4, torsang=torsang,
                       symm1=self.checksymm(symm1),
                       symm2=self.checksymm(symm2),
                       symm3=self.checksymm(symm3),
                       symm4=self.checksymm(symm4))

    def hydrogen_bonds(self) -> Generator:
        label_d = self.block.find_loop('_geom_hbond_atom_site_label_D')
        label_h = self.block.find_loop('_geom_hbond_atom_site_label_H')
        label_a = self.block.find_loop('_geom_hbond_atom_site_label_A')
        dist_dh = self.block.find_loop('_geom_hbond_distance_DH')
        dist_ha = self.block.find_loop('_geom_hbond_distance_HA')
        dist_da = self.block.find_loop('_geom_hbond_distance_DA')
        angle_dha = self.block.find_loop('_geom_hbond_angle_DHA')
        symm = self.block.find_loop('_geom_hbond_site_symmetry_A')
        publ_loop = self.block.find_loop('_geom_hbond_publ_flag') or len(label_d) * ['?']
        hydr = namedtuple('HydrogenBond', ('label_d', 'label_h', 'label_a', 'dist_dh', 'dist_ha', 'dist_da',
                                           'angle_dha', 'symm'))
        for label_d, label_h, label_a, dist_dh, dist_ha, dist_da, angle_dha, symm, publ in (
            zip(label_d, label_h, label_a, dist_dh, dist_ha, dist_da, angle_dha, symm, publ_loop, strict=False)):
            if self.yes_not_set(publ):
                continue
            yield hydr(label_d, label_h, label_a, dist_dh, dist_ha, dist_da, angle_dha, self.checksymm(symm))

    def yes_not_set(self, publ: str):
        return publ not in {'y', 'yes', '?', '.'} or publ in {'n', 'no'}

    def key_value_pairs(self) -> list[tuple[str, str]]:
        """
        Returns the key/value pairs of a cif file sorted by priority.
        """
        keys_without_values, keys_with_values = self.keys_with_essentials()
        return [*keys_without_values, ('These below are already in:', '---------------------'), *keys_with_values]

    def pairs(self):
        for item in self.block:
            if item.pair is not None:
                yield item.pair

    def _is_centrokey(self, key) -> bool:
        """
        Is True if the kurrent key is only valid
        for non-centrosymmetric structures
        """
        return self.is_centrosymm and key in non_centrosymm_keys

    def test_res_checksum(self) -> bool:
        """
        A method to check whether the checksums in the cif file fit to the content.
        """
        cif_res_ckecksum = 0
        res_checksum_calcd = self.res_checksum_calcd
        if res_checksum_calcd > 0:
            cif_res_ckecksum = self['_shelx_res_checksum'] or -1
            try:
                cif_res_ckecksum = int(cif_res_ckecksum)
            except ValueError:
                cif_res_ckecksum = -1
        if cif_res_ckecksum > 0 and cif_res_ckecksum != res_checksum_calcd:
            return False
        else:
            return True

    def test_hkl_checksum(self) -> bool:
        """
        A method to check whether the checksums in the cif file fit to the content.
        """
        cif_hkl_ckecksum = 0
        hkl_checksum_calcd = self.hkl_checksum_calcd
        if hkl_checksum_calcd > 0:
            cif_hkl_ckecksum = self['_shelx_hkl_checksum'] or -1
            try:
                cif_hkl_ckecksum = int(cif_hkl_ckecksum)
            except ValueError:
                cif_hkl_ckecksum = -1
        if cif_hkl_ckecksum > 0 and cif_hkl_ckecksum != hkl_checksum_calcd:
            return False
        else:
            return True


if __name__ == '__main__':
    # c = CifContainer('../41467_2015.cif')
    # c = CifContainer('test-data/p21c.cif')

    c = CifContainer('test-data/DK_Zucker2_0m.cif')
    c.load_this_block(len(c.doc) - 1)
    # pp(list(c.torsion_angles()))
    print(c.bond_dist('C1-C2'))
    # pp(c.hkl_extra_info)
    # print(c.hkl_file)
    # print(c.hkl_as_cif)
    # print(c.test_hkl_checksum())
    # s = Shelxfile()
    # print(c.res_file_data)
    # s.read_string(c.res_file_data)
    # print(s.hklf.n)

    # print(CifContainer('tests/examples/1979688.cif').hkl_as_cif[-250:])

    """
    import n#umpy as np
    mtz = gemmi.Mtz(with_base=True)
    data = [x.split() for x in c.hkl_file.splitlines()]
    mtz.add_column('I', 'I')
    mtz.add_column('SIGI', 'Q')
    mtz.set_data(data)
    mtz.set_cell_for_all(gemmi.UnitCell(c.cell.a, c.cell.b, c.cell.c, c.cell.alpha, c.cell.beta, c.cell.gamma))
    mtz.spacegroup = gemmi.find_spacegroup_by_name(c.space_group)
    mtz.update_reso()
    size = mtz.get_size_for_hkl()
    np.seterr(divide='ignore', invalid='ignore')
    ios =  intensity.array / sigma.array
    
    doc = gemmi.cif.read('tests/examples/1979688-finalcif.fcf')
    rblock = gemmi.hkl_cif_as_refln_block(doc[0])
    """
