"""Parent class DataN."""
import os
import os.path
from warnings import warn
from typing import Union, NoReturn
from pycifstar import Data, to_data

from cryspy.A_functions_base.function_1_markdown import md_to_html
from cryspy.A_functions_base.function_1_objects import \
    get_functions_of_objet, get_table_html_for_variables

from cryspy.B_parent_classes.cl_1_item import ItemN
from cryspy.B_parent_classes.cl_2_loop import LoopN


class DataN(object):
    """Data container of loops and items."""

    def __repr__(self):
        """
        Magic method print() is redefined.

        Returns
        -------
        TYPE
            DESCRIPTION.

        """
        ls_out = [f"# Object '{self.get_name():}'"]
        for item in self.items:
            if isinstance(item, ItemN):
                ls_out.append(f"{4*' ':}.{item.get_name():}")
            else:
                ls_out.append(f"{4*' ':}.{item.get_name():} (loop)")
                
        method = self.methods_html()
        if method != "":
            ls_out.append(f"\n# Methods:\n{method:}\n")                
        return "\n".join(ls_out)

    def _repr_html_(self):
        """Representation in HTML format."""
        ls_html = [f"<h2>Object '{self.get_name():}'</h2>"]
        ls_html.append(self.attributes_to_html())


        ls_html.append(get_table_html_for_variables(self))

        report = self.report_html()
        if report != "":
            ls_html.append(f"<h2>Description </h2> {report:}")

        ls_html.append(f"<h2>Classes and methods</h2>")
        try:
            names = sorted([obj.__name__ for obj in self.CLASSES_MANDATORY])
            if len(names) != 0:
                ls_html.append("<b>Mandatory classes: </b>")
                ls_html.append(f"{', '.join(names):}.<br>")
        except AttributeError:
            pass
        try:
            names = sorted([obj.__name__ for obj in self.CLASSES_OPTIONAL])
            if len(names) != 0:
                ls_html.append("<b>Optional classes: </b>")
                ls_html.append(f"{', '.join(names):}.<br>")
        except AttributeError:
            pass

        method = self.methods_html()
        if method != "":
            ls_html.append(f"<b>Methods: </b> {method:}")

        return " ".join(ls_html)

    def methods_html(self):
        ls_html = [f".{func_name}" for func_name in
                   get_functions_of_objet(self)]
        return ", ".join(ls_html)+"."
    
    def attributes_to_html(self) -> str:
        """Representation of defined parameters in HTML format.
        """
        ls_html = ["<table>"]
        ls_html.append("<tr><th>Attribute</th><th> Note </th></tr>")
        items_sorted = sorted(self.items, key=lambda item: item.get_name())
        for item in items_sorted:
            item_type = item.__doc__.strip().split("\n")[0]
            ls_html.append(f"<tr><td>.{item.get_name():}</td>\
<td>{item_type:}</td></tr>")
        ls_html.append("</table>")
        return " ".join(ls_html)

    def __str__(self):
        """
        Magic method str() is redefined.

        Returns
        -------
        TYPE
            DESCRIPTION.

        """
        return self.to_cif()

    def __getattr__(self, name):
        """
        Magic method __getattr__ is slightly changed for special attributes.

        Parameters
        ----------
        name : TYPE
            DESCRIPTION.

        Raises
        ------
        AttributeError
            DESCRIPTION.

        Returns
        -------
        res : TYPE
            DESCRIPTION.

        """
        for item in self.items:
            if name.lower() == item.get_name():
                return item
        raise AttributeError(f"Attribute '{name:}' is not defined")

    def is_attribute(self, name):
        """Temporary construction.

        Better to use:

        try:
            obj = self.attribute_name
        except AttributeError as e:
            obj = ...
        """
        for item in self.items:
            if name.lower() == item.get_name():
                return True
        return False

    def __setattr__(self, name, value) -> NoReturn:
        """
        Rules to set attribute.

        Parameters
        ----------
        name : TYPE
            DESCRIPTION.
        value : TYPE
            DESCRIPTION.

        Returns
        -------
        NoReturn
            DESCRIPTION.

        """
        flag_items, flag_direct = False, True
        if name == "data_name":
            flag_direct = False
            val_new = str(value).strip()
        elif name == "items":
            flag_items = True
            self.add_items(value)
        else:
            cls_value = type(value)
            if cls_value in self.CLASSES:
                l_name = [item.get_name() for item in self.items]
                name_new = value.get_name()
                if name_new in l_name:
                    self.items.pop(l_name.index(name))
                self.items.append(value)
                flag_items, flag_direct = True, False
                if name_new != name:
                    warn(f"Access to variable by '{name_new:}'.", UserWarning)

        if flag_items:
            pass
        elif flag_direct:
            self.__dict__[name] = value
        else:
            self.__dict__[name] = val_new

    def add_items(self, items: list):
        """Add items."""
        l_name = [item.get_name() for item in items]
        s_name = set(l_name)

        if len(s_name) != len(l_name):
            warn("Double items were given.", UserWarning)
            items_unique = [items[l_name.index(name)] for name in s_name]
        else:
            items_unique = items
        l_ind_del = []
        for ind_item, item in enumerate(self.items):
            if item.get_name() in s_name:
                l_ind_del.append(ind_item)
        l_ind_del.reverse()
        for ind in l_ind_del:
            self.items.pop(ind)
        for item in items_unique:
            if isinstance(item, self.CLASSES):
                self.items.append(item)

    @classmethod
    def make_container(cls, cls_mandatory, cls_optional, prefix):
        """Create DataN object as a container for items."""
        if cls is not DataN:
            warn("The method 'make_container' is used only for DataN class.")
            return
        obj = cls()
        obj.__dict__["CLASSES_MANDATORY"] = cls_mandatory
        obj.__dict__["CLASSES_OPTIONAL"] = cls_optional
        obj.__dict__["CLASSES"] = cls_mandatory+cls_optional
        obj.__dict__["PREFIX"] = prefix
        obj.__dict__["D_DEFAULT"] = {}
        obj.__dict__["items"] = []
        obj.__dict__["data_name"] = ""
        return obj

    @classmethod
    def get_mandatory_attributes(cls, separator: str = "_"):
        """Get a list of mandatory attributes from mandatory classes."""
        l_res = []
        for cls_obj in cls.CLASSES_MANDATORY:
            if issubclass(cls_obj, ItemN):
                cls_item = cls_obj
            else: #LoopN
                cls_item = cls_obj.ITEM_CLASS
            l_res.extend([f"{cls_item.PREFIX:}{separator:}{name_cif:}" 
                              for name_cif in cls_item.ATTR_MANDATORY_CIF])
        return l_res

    def __getitem__(self, name: Union[int, str]):
        """
        Get item by index or predefined index.

        Parameters
        ----------
        name : TYPE
            DESCRIPTION.

        Returns
        -------
        TYPE
            DESCRIPTION.

        """
        if isinstance(name, int):
            return self.items[name]
        elif isinstance(name, str):
            for item in self.items:
                if name.lower() == item.get_name():
                    return item
        return None

    def get_name(self) -> str:
        """Name of object."""
        name = self.PREFIX
        data_name = self.data_name
        if data_name is not None:
            name = f"{name:}_{data_name:}"
        return name.lower()

    def get_variable_names(self) -> list:
        """
        Get names of variable as a list.

        (((#prefix, #NAME), (#prefix, #NAME), (#attribute, #index))

        Returns
        -------
        list
            List of names of variable.

        """
        prefix = self.PREFIX
        data_name = self.data_name
        l_var = []
        for item in self.items:
            l_var.extend(item.get_variable_names())
        l_var_out = [((prefix, data_name), ) + var for var in l_var]
        return l_var_out

    def is_variables(self) -> bool:
        """Define is there variables or not."""
        flag = False
        for item in self.items:
            if item.is_variables():
                flag = True
                break
        return flag

    def get_variable_by_name(self, name: tuple) -> Union[float, int, str]:
        """
        Get variable given by name.

        Parameters
        ----------
        name : tuple
            (((#prefix, #data_name), (#prefix, #loop_name),
               (#attribute, #index_item))

        Returns
        -------
        Union[float, int, str]
            DESCRIPTION.

        """
        prefix = self.PREFIX
        data_name = self.data_name

        prefix_d, prefix_n = name[0], name[1]

        if prefix_d != (prefix, data_name):
            return None

        name_sh = tuple(name[1:])
        for item in self.items:
            if isinstance(item, ItemN):
                prefix = item.PREFIX
            elif isinstance(item, LoopN):
                item_cls = item.ITEM_CLASS
                if item_cls is ItemN:
                    prefix = item[0].PREFIX
                else:
                    prefix = item_cls.PREFIX
            else:
                raise AttributeError(
                    f"Unknown type object '{type(item).__name__:}'")
            if prefix == prefix_n[0]:
                res = item.get_variable_by_name(name_sh)
                if res is not None:
                    return res
        return None

    def set_variable_by_name(self, name: tuple, value) -> NoReturn:
        """
        Set value to variable given by name.

        Parameters
        ----------
        name : tuple
            DESCRIPTION.
        value : TYPE
            DESCRIPTION.

        Returns
        -------
        NoReturn
            DESCRIPTION.

        """
        prefix = self.PREFIX
        data_name = self.data_name

        prefix_d, prefix_n = name[0], name[1]

        if prefix_d != (prefix, data_name):
            return

        name_sh = tuple(name[1:])
        for item in self.items:
            if isinstance(item, ItemN):
                prefix = item.PREFIX
            elif isinstance(item, LoopN):
                item_cls = item.ITEM_CLASS
                if item_cls is ItemN:
                    prefix = item[0].PREFIX
                else:
                    prefix = item_cls.PREFIX
            else:
                raise AttributeError(
                    f"Unknown type object '{type(item).__name__:}'")
            if prefix == prefix_n[0]:
                item.set_variable_by_name(name_sh, value)

    def is_defined(self) -> bool:
        """
        If all mandatory attributes is defined.

        Returns
        -------
        bool
            DESCRIPTION.

        """
        flag = True
        for item in self.items:
            if not(item.is_defined()):
                flag = False
                if isinstance(item, ItemN):
                    warn(f"{item.PREFIX:} is not fully described.",
                         UserWarning)
                    break
                elif isinstance(item, LoopN):
                    warn(f"{item.ITEM_CLASS.PREFIX:} is not fully described.",
                         UserWarning)
                    break
        if flag:
            cls_items = [type(item) for item in self.items]
            for cls_mand in self.CLASSES_MANDATORY:
                if not(cls_mand in cls_items):
                    flag = False
                    warn(f"The object of {cls_mand.__name__:} is not defined.",
                         UserWarning)
                    break
        return flag

    def form_object(self):
        """Form object."""
        pass


    def to_cif(self, separator="_") -> str:
        """Print information about object in string in STAR format.

        Arguments
        ---------
            prefix: prefix in front of label of attribute
            separator: separator between prefix and attribute ("_" or ".")
            flag: for undefined attribute "." will be printed
            flag_minimal if it's True the minimal set of object will be printed

        Returns
        -------
            A string in STAR/CIF format
        """
        ls_out = []
        if self.data_name is None:
            ls_out.append("data_\n")
        else:
            ls_out.append(f"data_{self.data_name:}\n")
        l_item = self.items
        l_s_itemn = [item.to_cif(separator=separator)+"\n"
                     for item in l_item if isinstance(item, ItemN)]
        l_s_loopn = [item.to_cif(separator=separator)+"\n"
                     for item in l_item if isinstance(item, LoopN)]
        if l_s_loopn != []:
            n_max_loop = max([len(_) for _ in l_s_loopn])
            if n_max_loop < 1000:
                n_max_loop = 1000
        else:
            n_max_loop = 10000
        l_n_max_item = [len(_) for _ in l_s_itemn]

        ls_out.extend([_1 for _1, _2 in zip(l_s_itemn, l_n_max_item)
                       if _2 <= n_max_loop])
        ls_out.extend([_ for _ in l_s_loopn])
        ls_out.extend([_1 for _1, _2 in zip(l_s_itemn, l_n_max_item)
                       if _2 > n_max_loop])
        return "\n".join(ls_out)

    @classmethod
    def from_cif(cls, string: str):
        """Generate object from string of CIF format."""
        cif_data = Data()
        flag = cif_data.take_from_string(string)

        cif_items = cif_data.items
        cif_loops = cif_data.loops

        items = []
        flag = True
        n_mandatory = len(cls.CLASSES_MANDATORY)
        for i_cls, cls_ in enumerate(cls.CLASSES):
            flag = i_cls >= n_mandatory
            if issubclass(cls_, ItemN):
                prefix_cls = cls_.PREFIX
                if cif_items.is_prefix(prefix_cls):
                    cif_items_prefix = cif_items[prefix_cls]
                    cif_string = str(cif_items_prefix)
                    obj_prefix = cls_.from_cif(cif_string)
                    if obj_prefix is not None:
                        items.append(obj_prefix)
                        flag = True
            elif issubclass(cls_, LoopN):
                prefix_cls = cls_.ITEM_CLASS.PREFIX
                for cif_loop in cif_loops:
                    if cif_loop.is_prefix("_"+prefix_cls):
                        cif_string = str(cif_loop)
                        obj_prefix = cls_.from_cif(cif_string)
                        if obj_prefix is not None:
                            items.append(obj_prefix)
                            flag = True
            if (not(flag)):
                warn(f"Mandatory class: '{cls_.__name__:}' is not given.",
                     UserWarning)
                break

        if not(flag):
            return None

        data_name = cif_data.name
        obj = cls(data_name=data_name, items=items)
        obj.form_object()
        return obj

    @classmethod
    def from_cif_file(cls, f_name: str):
        """Read from cif file."""
        if not(os.path.isfile(f_name)):
            raise UserWarning(f"File {f_name:} is not found.")
            return None
        str_from_cif = str(to_data(f_name))
        obj = cls.from_cif(str_from_cif)
        obj.file_input = f_name
        return obj

    def copy(self, data_name: str = ""):
        """Deep copy of object with new data name."""
        s_cif = self.to_cif()
        obj_new = type(self).from_cif(s_cif)
        obj_new.data_name = data_name
        return obj_new

    def report(self):
        return ""

    def report_html(self):
        return md_to_html(self.report())

    def plots(self):
        l_res = []
        for item in self.items:
            for plot in item.plots():
                if plot is not None:
                    l_res.append(plot)
        return l_res

    def fix_variables(self):
        """Fix variables."""
        for item in self.items:
            item.fix_variables()        

    def set_variable(self, name: str, index=None):
        """Set refinement for variable given by name.
        
        Index parameters is used only for objects given as a matrix.
        """
        name_sh = name.strip(".").lower()
        l_name = name_sh.split(".")
        name_1 = l_name[0]
        for item in self.items:
            if name_1 == item.get_name(): 
                if len(l_name) == 1:
                    attr_refs = []
                    if isinstance(item, ItemN):
                        attr_refs = item.ATTR_REF
                    elif isinstance(item, LoopN):
                        item_class = item.ITEM_CLASS
                        if item_class is ItemN:
                            if len(self.items) != 0:
                                attr_refs = item.items[0].ATTR_REF
                        else:
                            attr_refs = item_class.ATTR_REF
                    for attr_ref in attr_refs:
                        item.set_variable(attr_ref, index=index)
                        
                else:
                    item.set_variable(".".join(l_name[1:]), index=index)

"""Parent class DataN."""
import os
import os.path
from warnings import warn
from typing import Union, NoReturn
from pycifstar import Data, to_data

from cryspy.A_functions_base.function_1_markdown import md_to_html
from cryspy.A_functions_base.function_1_objects import \
    get_functions_of_objet, get_table_html_for_variables

from cryspy.B_parent_classes.cl_1_item import ItemN
from cryspy.B_parent_classes.cl_2_loop import LoopN


class DataN(object):
    """Data container of loops and items."""

    def __repr__(self):
        """
        Magic method print() is redefined.

        Returns
        -------
        TYPE
            DESCRIPTION.

        """
        ls_out = [f"# Object '{self.get_name():}'"]
        for item in self.items:
            if isinstance(item, ItemN):
                ls_out.append(f"{4*' ':}.{item.get_name():}")
            else:
                ls_out.append(f"{4*' ':}.{item.get_name():} (loop)")
                
        method = self.methods_html()
        if method != "":
            ls_out.append(f"\n# Methods:\n{method:}\n")                
        return "\n".join(ls_out)

    def _repr_html_(self):
        """Representation in HTML format."""
        ls_html = [f"<h2>Object '{self.get_name():}'</h2>"]
        ls_html.append(self.attributes_to_html())


        ls_html.append(get_table_html_for_variables(self))

        report = self.report_html()
        if report != "":
            ls_html.append(f"<h2>Description </h2> {report:}")

        ls_html.append(f"<h2>Classes and methods</h2>")
        try:
            names = sorted([obj.__name__ for obj in self.CLASSES_MANDATORY])
            if len(names) != 0:
                ls_html.append("<b>Mandatory classes: </b>")
                ls_html.append(f"{', '.join(names):}.<br>")
        except AttributeError:
            pass
        try:
            names = sorted([obj.__name__ for obj in self.CLASSES_OPTIONAL])
            if len(names) != 0:
                ls_html.append("<b>Optional classes: </b>")
                ls_html.append(f"{', '.join(names):}.<br>")
        except AttributeError:
            pass

        method = self.methods_html()
        if method != "":
            ls_html.append(f"<b>Methods: </b> {method:}")

        return " ".join(ls_html)

    def methods_html(self):
        ls_html = [f".{func_name}" for func_name in
                   get_functions_of_objet(self)]
        return ", ".join(ls_html)+"."
    
    def attributes_to_html(self) -> str:
        """Representation of defined parameters in HTML format.
        """
        ls_html = ["<table>"]
        ls_html.append("<tr><th>Attribute</th><th> Note </th></tr>")
        items_sorted = sorted(self.items, key=lambda item: item.get_name())
        for item in items_sorted:
            item_type = item.__doc__.strip().split("\n")[0]
            ls_html.append(f"<tr><td>.{item.get_name():}</td>\
<td>{item_type:}</td></tr>")
        ls_html.append("</table>")
        return " ".join(ls_html)

    def __str__(self):
        """
        Magic method str() is redefined.

        Returns
        -------
        TYPE
            DESCRIPTION.

        """
        return self.to_cif()

    def __getattr__(self, name):
        """
        Magic method __getattr__ is slightly changed for special attributes.

        Parameters
        ----------
        name : TYPE
            DESCRIPTION.

        Raises
        ------
        AttributeError
            DESCRIPTION.

        Returns
        -------
        res : TYPE
            DESCRIPTION.

        """
        for item in self.items:
            if name.lower() == item.get_name():
                return item
        raise AttributeError(f"Attribute '{name:}' is not defined")

    def is_attribute(self, name):
        """Temporary construction.

        Better to use:

        try:
            obj = self.attribute_name
        except AttributeError as e:
            obj = ...
        """
        for item in self.items:
            if name.lower() == item.get_name():
                return True
        return False

    def __setattr__(self, name, value) -> NoReturn:
        """
        Rules to set attribute.

        Parameters
        ----------
        name : TYPE
            DESCRIPTION.
        value : TYPE
            DESCRIPTION.

        Returns
        -------
        NoReturn
            DESCRIPTION.

        """
        flag_items, flag_direct = False, True
        if name == "data_name":
            flag_direct = False
            val_new = str(value).strip()
        elif name == "items":
            flag_items = True
            self.add_items(value)
        else:
            cls_value = type(value)
            if cls_value in self.CLASSES:
                l_name = [item.get_name() for item in self.items]
                name_new = value.get_name()
                if name_new in l_name:
                    self.items.pop(l_name.index(name))
                self.items.append(value)
                flag_items, flag_direct = True, False
                if name_new != name:
                    warn(f"Access to variable by '{name_new:}'.", UserWarning)

        if flag_items:
            pass
        elif flag_direct:
            self.__dict__[name] = value
        else:
            self.__dict__[name] = val_new

    def add_items(self, items: list):
        """Add items."""
        l_name = [item.get_name() for item in items]
        s_name = set(l_name)

        if len(s_name) != len(l_name):
            warn("Double items were given.", UserWarning)
            items_unique = [items[l_name.index(name)] for name in s_name]
        else:
            items_unique = items
        l_ind_del = []
        for ind_item, item in enumerate(self.items):
            if item.get_name() in s_name:
                l_ind_del.append(ind_item)
        l_ind_del.reverse()
        for ind in l_ind_del:
            self.items.pop(ind)
        for item in items_unique:
            if isinstance(item, self.CLASSES):
                self.items.append(item)

    @classmethod
    def make_container(cls, cls_mandatory, cls_optional, prefix):
        """Create DataN object as a container for items."""
        if cls is not DataN:
            warn("The method 'make_container' is used only for DataN class.")
            return
        obj = cls()
        obj.__dict__["CLASSES_MANDATORY"] = cls_mandatory
        obj.__dict__["CLASSES_OPTIONAL"] = cls_optional
        obj.__dict__["CLASSES"] = cls_mandatory+cls_optional
        obj.__dict__["PREFIX"] = prefix
        obj.__dict__["D_DEFAULT"] = {}
        obj.__dict__["items"] = []
        obj.__dict__["data_name"] = ""
        return obj

    @classmethod
    def get_mandatory_attributes(cls, separator: str = "_"):
        """Get a list of mandatory attributes from mandatory classes."""
        l_res = []
        for cls_obj in cls.CLASSES_MANDATORY:
            if issubclass(cls_obj, ItemN):
                cls_item = cls_obj
            else: #LoopN
                cls_item = cls_obj.ITEM_CLASS
            l_res.extend([f"{cls_item.PREFIX:}{separator:}{name_cif:}" 
                              for name_cif in cls_item.ATTR_MANDATORY_CIF])
        return l_res

    def __getitem__(self, name: Union[int, str]):
        """
        Get item by index or predefined index.

        Parameters
        ----------
        name : TYPE
            DESCRIPTION.

        Returns
        -------
        TYPE
            DESCRIPTION.

        """
        if isinstance(name, int):
            return self.items[name]
        elif isinstance(name, str):
            for item in self.items:
                if name.lower() == item.get_name():
                    return item
        return None

    def get_name(self) -> str:
        """Name of object."""
        name = self.PREFIX
        data_name = self.data_name
        if data_name is not None:
            name = f"{name:}_{data_name:}"
        return name.lower()

    def get_variable_names(self) -> list:
        """
        Get names of variable as a list.

        (((#prefix, #NAME), (#prefix, #NAME), (#attribute, #index))

        Returns
        -------
        list
            List of names of variable.

        """
        prefix = self.PREFIX
        data_name = self.data_name
        l_var = []
        for item in self.items:
            l_var.extend(item.get_variable_names())
        l_var_out = [((prefix, data_name), ) + var for var in l_var]
        return l_var_out

    def is_variables(self) -> bool:
        """Define is there variables or not."""
        flag = False
        for item in self.items:
            if item.is_variables():
                flag = True
                break
        return flag

    def get_variable_by_name(self, name: tuple) -> Union[float, int, str]:
        """
        Get variable given by name.

        Parameters
        ----------
        name : tuple
            (((#prefix, #data_name), (#prefix, #loop_name),
               (#attribute, #index_item))

        Returns
        -------
        Union[float, int, str]
            DESCRIPTION.

        """
        prefix = self.PREFIX
        data_name = self.data_name

        prefix_d, prefix_n = name[0], name[1]

        if prefix_d != (prefix, data_name):
            return None

        name_sh = tuple(name[1:])
        for item in self.items:
            if isinstance(item, ItemN):
                prefix = item.PREFIX
            elif isinstance(item, LoopN):
                item_cls = item.ITEM_CLASS
                if item_cls is ItemN:
                    prefix = item[0].PREFIX
                else:
                    prefix = item_cls.PREFIX
            else:
                raise AttributeError(
                    f"Unknown type object '{type(item).__name__:}'")
            if prefix == prefix_n[0]:
                res = item.get_variable_by_name(name_sh)
                if res is not None:
                    return res
        return None

    def set_variable_by_name(self, name: tuple, value) -> NoReturn:
        """
        Set value to variable given by name.

        Parameters
        ----------
        name : tuple
            DESCRIPTION.
        value : TYPE
            DESCRIPTION.

        Returns
        -------
        NoReturn
            DESCRIPTION.

        """
        prefix = self.PREFIX
        data_name = self.data_name

        prefix_d, prefix_n = name[0], name[1]

        if prefix_d != (prefix, data_name):
            return

        name_sh = tuple(name[1:])
        for item in self.items:
            if isinstance(item, ItemN):
                prefix = item.PREFIX
            elif isinstance(item, LoopN):
                item_cls = item.ITEM_CLASS
                if item_cls is ItemN:
                    prefix = item[0].PREFIX
                else:
                    prefix = item_cls.PREFIX
            else:
                raise AttributeError(
                    f"Unknown type object '{type(item).__name__:}'")
            if prefix == prefix_n[0]:
                item.set_variable_by_name(name_sh, value)

    def is_defined(self) -> bool:
        """
        If all mandatory attributes is defined.

        Returns
        -------
        bool
            DESCRIPTION.

        """
        flag = True
        for item in self.items:
            if not(item.is_defined()):
                flag = False
                if isinstance(item, ItemN):
                    warn(f"{item.PREFIX:} is not fully described.",
                         UserWarning)
                    break
                elif isinstance(item, LoopN):
                    warn(f"{item.ITEM_CLASS.PREFIX:} is not fully described.",
                         UserWarning)
                    break
        if flag:
            cls_items = [type(item) for item in self.items]
            for cls_mand in self.CLASSES_MANDATORY:
                if not(cls_mand in cls_items):
                    flag = False
                    warn(f"The object of {cls_mand.__name__:} is not defined.",
                         UserWarning)
                    break
        return flag

    def form_object(self):
        """Form object."""
        pass


    def to_cif(self, separator="_") -> str:
        """Print information about object in string in STAR format.

        Arguments
        ---------
            prefix: prefix in front of label of attribute
            separator: separator between prefix and attribute ("_" or ".")
            flag: for undefined attribute "." will be printed
            flag_minimal if it's True the minimal set of object will be printed

        Returns
        -------
            A string in STAR/CIF format
        """
        ls_out = []
        if self.data_name is None:
            ls_out.append("data_\n")
        else:
            ls_out.append(f"data_{self.data_name:}\n")
        l_item = self.items
        l_s_itemn = [item.to_cif(separator=separator)+"\n"
                     for item in l_item if isinstance(item, ItemN)]
        l_s_loopn = [item.to_cif(separator=separator)+"\n"
                     for item in l_item if isinstance(item, LoopN)]
        if l_s_loopn != []:
            n_max_loop = max([len(_) for _ in l_s_loopn])
            if n_max_loop < 1000:
                n_max_loop = 1000
        else:
            n_max_loop = 10000
        l_n_max_item = [len(_) for _ in l_s_itemn]

        ls_out.extend([_1 for _1, _2 in zip(l_s_itemn, l_n_max_item)
                       if _2 <= n_max_loop])
        ls_out.extend([_ for _ in l_s_loopn])
        ls_out.extend([_1 for _1, _2 in zip(l_s_itemn, l_n_max_item)
                       if _2 > n_max_loop])
        return "\n".join(ls_out)

    @classmethod
    def from_cif(cls, string: str):
        """Generate object from string of CIF format."""
        cif_data = Data()
        flag = cif_data.take_from_string(string)

        cif_items = cif_data.items
        cif_loops = cif_data.loops

        items = []
        flag = True
        n_mandatory = len(cls.CLASSES_MANDATORY)
        for i_cls, cls_ in enumerate(cls.CLASSES):
            flag = i_cls >= n_mandatory
            if issubclass(cls_, ItemN):
                prefix_cls = cls_.PREFIX
                if cif_items.is_prefix(prefix_cls):
                    cif_items_prefix = cif_items[prefix_cls]
                    cif_string = str(cif_items_prefix)
                    obj_prefix = cls_.from_cif(cif_string)
                    if obj_prefix is not None:
                        items.append(obj_prefix)
                        flag = True
            elif issubclass(cls_, LoopN):
                prefix_cls = cls_.ITEM_CLASS.PREFIX
                for cif_loop in cif_loops:
                    if cif_loop.is_prefix("_"+prefix_cls):
                        cif_string = str(cif_loop)
                        obj_prefix = cls_.from_cif(cif_string)
                        if obj_prefix is not None:
                            items.append(obj_prefix)
                            flag = True
            if (not(flag)):
                warn(f"Mandatory class: '{cls_.__name__:}' is not given.",
                     UserWarning)
                break

        if not(flag):
            return None

        data_name = cif_data.name
        obj = cls(data_name=data_name, items=items)
        obj.form_object()
        return obj

    @classmethod
    def from_cif_file(cls, f_name: str):
        """Read from cif file."""
        if not(os.path.isfile(f_name)):
            raise UserWarning(f"File {f_name:} is not found.")
            return None
        str_from_cif = str(to_data(f_name))
        obj = cls.from_cif(str_from_cif)
        obj.file_input = f_name
        return obj

    def copy(self, data_name: str = ""):
        """Deep copy of object with new data name."""
        s_cif = self.to_cif()
        obj_new = type(self).from_cif(s_cif)
        obj_new.data_name = data_name
        return obj_new

    def report(self):
        return ""

    def report_html(self):
        return md_to_html(self.report())

    def plots(self):
        l_res = []
        for item in self.items:
            for plot in item.plots():
                if plot is not None:
                    l_res.append(plot)
        return l_res

    def fix_variables(self):
        """Fix variables."""
        for item in self.items:
            item.fix_variables()        

    def set_variable(self, name: str, index=None):
        """Set refinement for variable given by name.
        
        Index parameters is used only for objects given as a matrix.
        """
        name_sh = name.strip(".").lower()
        l_name = name_sh.split(".")
        name_1 = l_name[0]
        for item in self.items:
            if name_1 == item.get_name(): 
                if len(l_name) == 1:
                    attr_refs = []
                    if isinstance(item, ItemN):
                        attr_refs = item.ATTR_REF
                    elif isinstance(item, LoopN):
                        item_class = item.ITEM_CLASS
                        if item_class is ItemN:
                            if len(self.items) != 0:
                                attr_refs = item.items[0].ATTR_REF
                        else:
                            attr_refs = item_class.ATTR_REF
                    for attr_ref in attr_refs:
                        item.set_variable(attr_ref, index=index)
                        
                else:
                    item.set_variable(".".join(l_name[1:]), index=index)

Parent class DataN. Data container of loops and items. Magic method print() is redefined.          Returns         -------         TYPE             DESCRIPTION. # Methods:\n{method:}\n") Representation in HTML format. Representation of defined parameters in HTML format. Magic method str() is redefined.          Returns         -------         TYPE             DESCRIPTION. Magic method __getattr__ is slightly changed for special attributes.          Parameters         ----------         name : TYPE             DESCRIPTION.          Raises         ------         AttributeError             DESCRIPTION.          Returns         -------         res : TYPE             DESCRIPTION. Temporary construction.          Better to use:          try:             obj = self.attribute_name         except AttributeError as e:             obj = ... Rules to set attribute.          Parameters         ----------         name : TYPE             DESCRIPTION.         value : TYPE             DESCRIPTION.          Returns         -------         NoReturn             DESCRIPTION. Add items. Create DataN object as a container for items. Get a list of mandatory attributes from mandatory classes. #LoopN Get item by index or predefined index.          Parameters         ----------         name : TYPE             DESCRIPTION.          Returns         -------         TYPE             DESCRIPTION. Name of object. Get names of variable as a list.          (((#prefix, #NAME), (#prefix, #NAME), (#attribute, #index))          Returns         -------         list             List of names of variable. Define is there variables or not. Get variable given by name.          Parameters         ----------         name : tuple             (((#prefix, #data_name), (#prefix, #loop_name),                (#attribute, #index_item))          Returns         -------         Union[float, int, str]             DESCRIPTION. Set value to variable given by name.          Parameters         ----------         name : tuple             DESCRIPTION.         value : TYPE             DESCRIPTION.          Returns         -------         NoReturn             DESCRIPTION. If all mandatory attributes is defined.          Returns         -------         bool             DESCRIPTION. Form object. Print information about object in string in STAR format.          Arguments         ---------             prefix: prefix in front of label of attribute             separator: separator between prefix and attribute ("_" or ".")             flag: for undefined attribute "." will be printed             flag_minimal if it's True the minimal set of object will be printed          Returns         -------             A string in STAR/CIF format Generate object from string of CIF format. Read from cif file. Deep copy of object with new data name. Fix variables. Set refinement for variable given by name.                  Index parameters is used only for objects given as a matrix.