from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

import os
import sys
import ase
import glob
import shutil
import inspect
import warnings
import numpy as np

# 2-to-3 compatibility
try:
    import cPickle as pickle
except ImportError:
    import pickle
# More 2-to-3 compatibility, defining a wrapper function for inspect
# Internal imports
from ase import io as ase_io
from ase.build import niggli_reduce
from ase.calculators.singlepoint import SinglePointCalculator
from soprano import utils

utils.customize_warnings()

class _AllCaller(object):

    """_AllCaller class.

    A meta-object that serves the purpose of calling a function on all members
    of a list in a natural way.
    """

    def __init__(self, all_list, all_class=None):
        """Initialize the AllCaller with an 'all' list"""
        if all_class is None:
            self._class = all_list[0].__class__
        else:
            self._class = all_class
        if not all([x.__class__ is self._class for x in all_list]):
            raise ValueError(
                "Elements of list passed to an _AllCaller" " must be of the same type."
            )
        self._all = all_list
        # Now get a list of all members in common between these instances
        if len(self._all) > 0:
            self._instance_attrs = set.intersection(
                *[set(ins.__dict__.keys()) for ins in self._all]
            )

    def __getattr__(self, name):
        """Here's the magic of the class - when a method isn't found belonging
        to it, go looking for it in its ._all list..."""

        if hasattr(self._class, name):
            attr = getattr(self._class, name)
            # Is it a function?
            if not hasattr(attr, "__call__"):
                return np.array([getattr(x, name) for x in self._all])

            def iterfunc(*args, **kwargs):
                return np.array([getattr(x, name)(*args, **kwargs) for x in self._all])

            return iterfunc
        elif name in self._instance_attrs:
            # It's an instance attribute
            return np.array([getattr(x, name) for x in self._all])
        else:
            raise AttributeError(
                ("Not all '{0}' objects have attribute" " '{1}'").format(
                    self._class.__name__, name
                )
            )

    def map(self, f, *args, **kwargs):
        """Map a function to each element of the ._all list and return the
        results."""

        # First, check the signature
        if not hasattr(f, "__call__"):
            raise TypeError("Only functions can be mapped")

        nargs, nargs_def = utils.inspect_args(f)
        if nargs < 1 + len(args) + len(kwargs):
            # Function is invalid!
            raise ValueError("Invalid function passed to map")
        return [f(x, *args, **kwargs) for x in self._all]

class AtomsCollection(object):

    """AtomsCollection object.

    An AtomsCollection represents a group of ASE Atoms objects.
    It handles them together, can perform mass operations on them, and stores
    arrays of informations related to them.
    """

    def __init__(
        self,
        structures=[],
        info={},
        cell_reduce=False,
        progress=False,
        suppress_ase_warnings=True,
    ):
        """
        Initialize the AtomsCollection

        | Args:
        |    structures (list[str] or list[ase.Atoms]): list of file names or
        |                                               Atoms that will form
        |                                               the collection
        |    info (dict): dictionary of general information to attach
        |                 to this collection
        |    cell_reduce (bool): if True, perform a Niggli cell reduction on
        |                        all loaded structures
        |    progress (bool): visualize a progress bar for the loading process
        |    suppress_ase_warnings (bool): suppress annoying ASE warnings when
        |                                  loading files (default is True)
        """

        # Start by parsing out the structures
        self.structures = []

        if isinstance(structures, ase.Atoms):
            # Well, it's just one...
            structures = [structures]
        elif inspect.isgenerator(structures):
            # Let's unravel it
            iter_structs = structures
            structures = []
            for s in iter_structs:
                structures.append(s)

        if progress:
            sys.stdout.write("Loading collection...\n")
        s_n = len(structures)
        for s_i, struct in enumerate(structures):
            if progress:
                # Progress bar
                sys.stdout.write("\rLoading: {0}".format(utils.progbar(s_i + 1, s_n)))
            # Is it an Atoms object?
            if type(struct) is ase.Atoms:
                self.structures.append(ase.Atoms(struct))
                # Copy all arrays
                for k in struct.arrays.keys():
                    if not self.structures[-1].has(k):
                        self.structures[-1].new_array(k, struct.get_array(k))
                if struct.calc is not None:
                    # Prevents pointless attempts at re-calculating
                    self.structures[-1].calc._old_atoms = self.structures[-1]
            # Or is it a string?
            elif utils.is_string(struct):
                with utils.silence_stdio(suppress_ase_warnings, suppress_ase_warnings):
                    self.structures.append(ase_io.read(str(struct)))
                # If there's no name, give it the filename
                if "name" not in self.structures[-1].info:
                    self.structures[-1].info["name"] = utils.seedname(struct)
            else:
                raise TypeError(
                    "Structures must be Atoms objects or valid "
                    "file names,"
                    " not {0}".format(type(struct).__name__)
                )
            if cell_reduce:
                # Here we must keep the energy if it was present
                # We do this by hand because ASE has its good reasons
                # for severing the atoms-calculator connection when changing
                # the unit cell.
                try:
                    _E = self.structures[-1].calc.results["energy"]
                except (KeyError, AttributeError):
                    _E = None
                niggli_reduce(self.structures[-1])
                if _E is not None:
                    _calc = SinglePointCalculator(self.structures[-1], energy=_E)
                    self.structures[-1].set_calculator(_calc)

        if progress:
            sys.stdout.write("\nLoaded {0} structures\n".format(s_n))

        self._all = _AllCaller(self.structures, ase.Atoms)

        self._arrays = {}

        # Now assign the info
        if type(info) is not dict:
            raise TypeError("Info must be dict," " not {0}".format(type(info).__name__))
        else:
            self.info = info.copy()

    def __add__(self, other):
        """Addition of two collections brings a merging"""

        if not isinstance(other, self.__class__):
            raise TypeError(
                "'AtomsCollection' does not support operator +"
                " with object of type"
                " '{0}'".format(type(other).__name__)
            )

        # Create a common collection, join arrays where present, copy all info
        all_struct = list(self.structures)
        all_struct += list(other.structures)

        all_info = dict(self.info)
        for k in other.info:
            if k not in all_info:
                all_info[k] = other.info[k]
            else:
                all_info.pop(k)

        sum_struct = AtomsCollection(all_struct, all_info)

        # Now arrays
        for aname in self._arrays:
            # Grab the shape
            shape = self._arrays[aname].shape[1:]
            # Check if present in the other as well
            all_arr = list(self.get_array(aname))
            if aname in other._arrays:
                all_arr += list(other.get_array(aname))
            else:
                all_arr += [np.zeros(shape) * np.nan] * other.length
            sum_struct.set_array(aname, all_arr, shape=shape)

        for aname in other._arrays:
            # Can not be present in both
            if aname in self._arrays:
                continue
            # Grab the shape
            shape = other._arrays[aname].shape[1:]
            all_arr = list(other.get_array(aname))
            all_arr = [np.zeros(shape)] * self.length + all_arr
            sum_struct.set_array(aname, all_arr, shape=shape)

        return sum_struct

    def __iadd__(self, other):
        self = self + other
        return self

    def __getitem__(self, indices):
        """Allow sophisticated slicing"""

        if type(indices) is int:
            # Special case, a single element!
            indices = indices % len(self)
            indices = slice(indices, indices + 1)

        try:
            struct_slice = self.structures[indices]
        except TypeError:
            indices = np.array(indices)
            if indices.dtype == bool:
                indices = np.where(indices)[0]  # Support for bool arrays
            struct_slice = [self.structures[i] for i in indices]

        sliced = AtomsCollection(struct_slice, info=self.info)

        # Now to add the arrays
        for a in self._arrays:
            sliced.set_array(a, self._arrays[a][indices])

        return sliced

    def __iter__(self):
        return self.structures.__iter__()

    def __deepcopy__(self, memodict={}):
        """Protects against problems with infinite recursion in AllCaller"""

        dcopy = AtomsCollection(self.structures, self.info)
        for arr in self._arrays:
            dcopy.set_array(arr, self._arrays[arr])

        return dcopy

    def __len__(self):
        return self.length

    @property
    def length(self):
        return len(self.structures)

    @property
    def all(self):
        return self._all

    def set_array(self, name, a, dtype=None, shape=None, args={}):
        """Add or modify an array of data related to the Atoms objects
        in this collection.

        | Args:
        |   name (str): name of the array to operate on.
        |   a (np.ndarray or function<Atoms, \\*\\*kwargs>
        |                    => Any): the data to assign to the array (must
        |                             be same length as the collection) or
        |                             a function that takes an Atoms object
        |                             as the first argument and returns a
        |                             value. This will be mapped over the
        |                             structures to create the array.
        |   dtype (type): type to cast the values of the array to.
        |   shape (tuple [int]): shape of each entry of the array. Will be
        |                        checked if provided.
        |   args (dict): named arguments to pass to the function provided
        |                as a. Will be ignored if an array is passed instead.

        """

        # a can be an actual array or a function that operates on each
        # separate Atoms object and returns a value

        a = np.array(a, dtype)
        if a.shape == ():
            a = a.item()
            if hasattr(a, "__call__"):
                # It's a function
                a = np.array(self.all.map(a, **args), dtype)
            else:
                # Invalid
                raise TypeError(
                    "new_array requires to pass either an array"
                    " or a function taking an Atoms object as its"
                    " first argument and returning a value."
                )

        # Now check that the shape is valid
        if shape is not None:
            if shape == (1,):
                targ_shape = (self.length,)
            else:
                targ_shape = (self.length,) + shape
            if a.shape != targ_shape:
                raise ValueError("Array of invalid shape passed to new_array")
        else:
            if a.shape[0] != self.length:
                raise ValueError(
                    "Array passed to new_array should be"
                    " as long as the number of structures"
                )

        # And finally, assign
        self._arrays[name] = a

    def get_array(self, name, copy=True):
        """Get a copy of an array of given name (or a reference if copy=False)

        | Args:
        |   name (str): name of the array to retrieve.
        |   copy (bool): if the array should be copied or a reference should
        |                be returned instead.

        | Returns:
        |   array (np.ndarray): the requested array

        """

        if name not in self._arrays:
            raise ValueError("Array '{0}' does not exist".format(name))
        else:
            return np.array(self._arrays[name], copy=copy)

    def has(self, name):
        """Check if array of given name exists"""

        return name in self._arrays

    def set_calculators(self, calctype, labels=None, params={}):
        """Set an ASE calculator on each structure in the collection,
        and set said calculator's parameters.

        | Args:
        |   calctype (ASE Calculator type): the type of calculator
        |                                   to instantiate.
        |   labels (Optional[list[str]]): names to use for the calculators'
        |                                 files. If not present, random
        |                                 generated names are used.
        |   params (Optional[dict]): parameters of the calculator to set.

        """

        # First, a check
        from ase.calculators.general import Calculator as gCalculator
        from ase.calculators.calculator import Calculator as cCalculator
        from ase.calculators.calculator import FileIOCalculator as ioCalculator

        if (
            (gCalculator not in calctype.__bases__)
            and (cCalculator not in calctype.__bases__)
            and (ioCalculator not in calctype.__bases__)
        ):
            raise TypeError("calctype must be a type of ASE Calculator")

        if labels is not None and len(labels) != self.length:
            raise ValueError("labels must be long as the collection itself")

        # Then set it up
        for i, s in enumerate(self.structures):
            if labels is None:
                # First: do we have a name?
                if "name" in s.info:
                    label = s.info["name"]
                else:
                    label = "struct_{0}".format(i)
            else:
                label = labels[i]
            calc = calctype(atoms=s, label=str(label), **params)
            # To make sure...
            s.set_calculator(calc)

    def run_calculators(self, properties=None, system_changes=None):
        """Run all previously set ASE calculators.

        | Args:
        |   properties (list[str]): list of properties to calculate (depends
        |                           on type of Calculator used)
        |   system_changes (list[str]): list of changes to the structure
        |                               since the last calculation. Can be
        |                               any combination of these five:
        |                               'positions', 'numbers', 'cell',
        |                               'pbc', 'initial_charges' and
        |                               'initial_magmoms'.

        """

        # First, check if we even have those
        if any([c is None for c in self.all.calc]):
            raise RuntimeError("Not all structures in collection" " have a calculator")
        kwargs = {}
        if properties is not None:
            kwargs["properties"] = properties
        if system_changes is not None:
            kwargs["system_changes"] = system_changes
        self.all.map(lambda s: s.calc.calculate(atoms=s, **kwargs))

    def chunkify(self, chunk_size=None, chunk_n=None):
        """Split this collection into multiple collections based on either
        size or number of chunks.

        | Args:
        |   chunk_size (Optional[int]): maximum size of a generated chunk
        |   chunk_n (Optional[int]): number of chunks to generate

        | Returns:
        |   chunks (list[AtomsCollection]): a list of the generated chunks

        """

        if [chunk_size, chunk_n].count(None) != 1:
            raise RuntimeError(
                "Only one between chunk_size and chunk_n" "must be passed"
            )

        if chunk_size is not None and type(chunk_size) is not int:
            raise TypeError("chunk_size must be an int")
        if chunk_n is not None and type(chunk_n) is not int:
            raise TypeError("chunk_n must be an int")

        # Now determine the size
        if chunk_size is None:
            chunk_size = int(np.ceil(self.length * 1.0 / chunk_n))

        struct_chunks = [
            self.structures[i : i + chunk_size]
            for i in range(0, self.length, chunk_size)
        ]
        chunks = [AtomsCollection(s, self.info) for s in struct_chunks]

        # Now the arrays
        for aname in self._arrays:
            for i, c in enumerate(chunks):
                c.set_array(
                    aname, self._arrays[aname][i * chunk_size : i * (chunk_size + 1)]
                )

        return chunks

    def sorted_byarray(self, name, reverse=False):
        """Return a copy of this collection sorted by a given array.

        | Args:
        |   name (str): name of the array to use for the sorting
        |   reverse (Optional[bool]): reverse order of sorting (max to min)

        | Returns:
        |   sorted (AtomsCollection): a sorted copy of the collection

        """

        # First, check that we do have the array
        if not self.has(name):
            raise ValueError("Array '{0}' does not exist".format(name))

        arr_names = list(self._arrays.keys())
        data_block = zip(self.structures, *[self._arrays[n] for n in arr_names])
        key_i = arr_names.index(name)
        data_block = list(
            zip(*sorted(data_block, key=lambda x: x[key_i + 1], reverse=reverse))
        )

        sorted_copy = AtomsCollection(data_block[0], info=self.info)
        for ai, an in enumerate(arr_names):
            sorted_copy.set_array(an, data_block[ai + 1])

        return sorted_copy

    def filter(self, filter_func):
        """Return a collection composed only of the elements for which a given
        filter function returns True.

        | Args:
        |   filter_func (function<Atoms>
        |                       => bool): filter function. Should take an
        |                                 Atoms object and return a boolean

        | Returns:
        |   filtered (AtomsCollection): the filtered version of the collection

        """

        filter_slice = []

        for i, s in enumerate(self.structures):
            if filter_func(s):
                filter_slice.append(i)

        return self[filter_slice]

    def classify(self, classes):
        """Return a dictionary of collections based on the names of assigned
        classes.

        | Args:
        |   classes (np.ndarray): array of the class to which each structure
        |                         belongs. For example [1, 2, 1] will put the
        |                         first and third structures in class 1 and
        |                         the other in class 2. The classes can be any
        |                         hashable types, like int or str.

        | Returns:
        |   classified (dict): a dictionary using class names as keys and
        |                      sliced collections as values

        """

        classes = np.array(classes)
        classified = {k: self[np.where(classes == k)[0]] for k in set(classes)}

        return classified

    def save(self, filename):
        """Simply save a pickled copy to a given file path"""

        # Pickling doesn't deal well with the _AllCaller, so we get rid of it
        selfcopy = self[:]
        selfcopy._all = None

        with open(filename, "wb") as f:
            pickle.dump(selfcopy, f, protocol=2)

    @staticmethod
    def load(filename):
        """Load a pickled copy from a given file path"""

        with open(filename, "rb") as f:
            coll = pickle.load(f)

        if not isinstance(coll, AtomsCollection):
            raise ValueError("File does not contain an AtomsCollection" " object")
        # Restore the _AllCaller
        coll._all = _AllCaller(coll.structures, ase.Atoms)
        return coll

    @staticmethod
    def check_tree(path):
        """Checks if a path is a valid 'tree' format for a collection. This is
        any folder that satisfies the following conditions:

        - contains a .collection file storing metadata
        - contains a series of folders matching the list stored in the
          .collection file, and nothing else

        This function will return 0 if both conditions are satisfied, 1 if
        only the first is, 2 if no .collection file is found, and -1 if the
        folder itself doesn't exist.

        | Args:
        |   path (str): path to check for whether it matches or not the
        |               collection pattern

        | Returns:
        |   result (int): 0, 1 or 2 depending on the outcome of the checks
        """

        if not os.path.exists(path):
            return -1

        # Begin by checking whether there is a .collection file
        try:
            with open(os.path.join(path, ".collection"), "rb") as f:
                coll = pickle.load(f)
        except (IOError, UnicodeDecodeError):
            return 2  # No or invalid .collection file found

        # Check if the directories match
        dirlist = coll["dirlist"]
        dirs = glob.glob(os.path.join(path, "*"))
        all_dirs = all([os.path.isdir(d) for d in dirs])
        dirs = [os.path.relpath(d, path) for d in dirs]

        # Are they even all directories?
        if not all_dirs or set(dirlist) != set(dirs):
            return 1

        return 0

    def save_tree(
        self,
        path,
        save_format,
        name_root="structure",
        opt_args={},
        safety_check=3,
        suppress_ase_warnings=True,
    ):
        """Save the collection's structures as a series of folders, named like
        the structures, inside a given parent folder (that will be created if
        not present). Arrays and info are stored in a pickled .collection file
        which works as metadata for the whole directory tree.
        The files can be saved in a format of choice, or a function can be
        passed that will save them in a custom way. Only one collection can be
        saved per folder.

        | Args:
        |   path (str): folder path in which the collection should be saved.
        |   save_format (str or function): format in which the structures
        |                                  should be saved.
        |                                  If a string, it will be used as a
        |                                  file extension. If a function, it
        |                                  must take as arguments the
        |                                  structure (an ase.Atoms object)
        |                                  the save path (a string), and any
        |                                  additional arguments passed as
        |                                  opt_args, and take care of saving
        |                                  the required files.
        |   name_root (str): name prefix to be used for structures when a name
        |                    is not available in their info dictionary
        |   opt_args (dict): dictionary of additional arguments to pass to
        |                    either ase.io.write (if save_format is a string)
        |                    or to the save_format function.
        |   safety_check (int): how much care should be taken not to overwrite
        |                       potentially important data in path. Can be a
        |                       number from 0 to 3.
        |                       Here's the meaning of the codes:
        |
        |                       3 (default): always ask before overwriting an
        |                         existing folder that passes the check_tree
        |                         control, raise an exception otherwise;
        |                       2: overwite any folder that passes fully the
        |                          check_tree control, raise an exception
        |                          otherwise;
        |                       1: overwrite any folder that passes fully the
        |                          check_tree control, ask for user input
        |                          otherwise;
        |                       0 (DANGER - use at your own risk!): no checks,
        |                         always overwrite path.

        """

        check = AtomsCollection.check_tree(path)

        def ow_ask(path):
            return (
                utils.safe_input(
                    ("Folder {0} exists, " "overwrite (y/n)?").format(path)
                ).lower()
                == "y"
            )

        if check > -1:
            # The folder exists
            if check == 0:
                if safety_check >= 3:
                    # Ask for permission
                    perm = ow_ask(path)
                else:
                    perm = True
            else:
                if safety_check >= 2:
                    raise IOError(
                        (
                            "Trying to overwrite folder {0} which did"
                            " not pass check_tree control (result {1})"
                            " with safety_check level "
                            "{2}"
                        ).format(path, check, safety_check)
                    )
                elif safety_check == 1:
                    perm = ow_ask(path)
                else:
                    perm = True

            if not perm:
                print("Can not overwrite folder {0}, skipping...".format(path))

            shutil.rmtree(path)

        # Re-create folder
        os.mkdir(path)

        # Format type?
        is_ext = utils.is_string(save_format)
        is_func = hasattr(save_format, "__call__")
        if not (is_ext or is_func):
            raise ValueError("Invalid save_format passed to save_tree")

        dirlist = []
        for i, s in enumerate(self.structures):
            sname = s.info.get("name", "{0}_{1}".format(name_root, i + 1))
            fold = os.path.join(path, sname)
            try:
                os.mkdir(fold)
            except OSError:
                shutil.rmtree(fold)
                os.mkdir(fold)
            with utils.silence_stdio(suppress_ase_warnings, suppress_ase_warnings):
                if is_ext:
                    ase_io.write(
                        os.path.join(fold, sname + "." + save_format), s, **opt_args
                    )
                elif is_func:
                    save_format(s, fold, **opt_args)

            dirlist.append(sname)

        with open(os.path.join(path, ".collection"), "wb") as f:
            pickle.dump(
                {"dirlist": dirlist, "arrays": self._arrays, "info": self.info},
                f,
                protocol=2,
            )

    @staticmethod
    def load_tree(
        path,
        load_format,
        opt_args={},
        safety_check=3,
        tolerant=False,
        suppress_ase_warnings=True,
    ):
        """Load a collection's structures from a series of folders, named like
        the structures, inside a given parent folder, as created by save_tree.
        The files can be loaded from a format of choice, or a
        function can be passed that will load them in a custom way.

        | Args:
        |   path (str): folder path in which the collection should be saved.
        |   load_format (str or function): format from which the structures
        |                                  should be loaded.
        |                                  If a string, it will be used as a
        |                                  file extension. If a function, it
        |                                  must take as arguments the load
        |                                  path (a string) and any additional
        |                                  arguments passed as opt_args, and
        |                                  return the loaded structure as an
        |                                  ase.Atoms object.
        |   opt_args(dict): dictionary of additional arguments to pass to
        |                   either ase.io.read (if load_format is a string)
        |                   or to the load_format function.
        |   safety_check (int): how much care should be taken to verify the
        |                       folder that is being loaded. Can be a number
        |                       from 0 to 3.
        |                       Here's the meaning of the codes:
        |
        |                       3 (default): only load a folder if it passes
        |                         fully the check_tree control;
        |                       2: load any folder that has a valid
        |                          .collection file, but only the listed
        |                          subfolders;
        |                       1: load any folder that has a valid
        |                          .collection file, all subfolders. Array
        |                          data will be discarded;
        |                       0: no checks, try to load from all subfolders.
        |   tolerant (bool): if set to true, proceeds to load the
        |                    structures into an AtomsCollection, even
        |                    if some of the structures could not be
        |                    read.

        | Returns:
        |   coll (AtomsCollection): loaded collection

        """

        check = AtomsCollection.check_tree(path)

        if check == -1:
            raise IOError("Folder {0} does not exist".format(path))

        dirlist = []
        if check < 2:
            with open(os.path.join(path, ".collection"), "rb") as f:
                coll = pickle.load(f)
            if check == 1 and safety_check == 3:
                raise IOError(
                    ("Folder {0} is not a valid collection " "tree").format(path)
                )
            if safety_check >= 2:
                dirlist = coll["dirlist"]
            else:
                dirlist = [
                    os.path.relpath(d, path)
                    for d in glob.glob(os.path.join(path, "*"))
                    if os.path.isdir(d)
                ]
        else:
            if safety_check > 0:
                raise IOError(
                    ("Folder {0} is not a valid collection " "tree").format(path)
                )
            dirlist = [
                os.path.relpath(d, path)
                for d in glob.glob(os.path.join(path, "*"))
                if os.path.isdir(d)
            ]

        # Format type?
        is_ext = utils.is_string(load_format)
        is_func = hasattr(load_format, "__call__")
        if not (is_ext or is_func):
            raise ValueError("Invalid load_format passed to load_tree")

        structures = []
        for d in dirlist:
            try:
                if is_ext:
                    with utils.silence_stdio(
                        suppress_ase_warnings, suppress_ase_warnings
                    ):
                        s = ase_io.read(
                            os.path.join(path, d, d + "." + load_format), **opt_args
                        )
                elif is_func:
                    s = load_format(os.path.join(path, d), **opt_args)
                structures.append(s)
            except Exception as e:
                warnings.warn(str(e))

        if check < 2:
            info = coll["info"]
        else:
            info = {}

        percentage_failed = (1 - len(structures) / len(dirlist)) * 100

        if percentage_failed > 0:

            if percentage_failed == 100:
                raise IOError(
                    "{0:.2f}% of structures could not be loaded.".format(
                        percentage_failed
                    )
                )
                return
            elif not tolerant:
                raise IOError(
                    "{0:.2f}% of structures could not be loaded. Set"
                    " tolerant to True if you would still"
                    " like to load the remaining structures.".format(percentage_failed)
                )
                return
            else:
                percentage_success = 100 - percentage_failed
                info["percentage_loaded"] = percentage_success

                warnings.warn(
                    "{0:.2f}% of structures could not be loaded.".format(
                        percentage_failed
                    )
                )
        else:
            print("100% of structures loaded successfully.")

        loaded_coll = AtomsCollection(structures, info=info)

        if safety_check >= 2:
            arrays = coll["arrays"]
            for k, a in arrays.items():
                loaded_coll.set_array(k, a)

        return loaded_coll

if __name__ == "__main__":

    # Just load whatever was passed in the command line!

    if len(sys.argv) > 1:
        coll = AtomsCollection(sys.argv[1:], progress=True, cell_reduce=True)


from distancetime import getDurationGMAPS
from random import randint
from Node import Node
def calculate_score(nodes,hubs):
    '''
    The lower the distance from hub to event and the higher the weight of the event,
    the higher the score of the hub!

    Score of hub = sum(weight_of_surrounding_events/((distance_to_event)^2))
    '''
    hub_score={}
    for hub in hubs:
        hub_score[hub]=0
    print(hub_score)
    for node in nodes:
        if node.nearest_hub in hubs:
            source=str((node.nearest_hub).lat)+", "+str((node.nearest_hub).long)
            destination=str(node.lat)+", "+str(node.long)
            print("Node weight:",node.weight)
            print("Source:",source)
            print("Destination:",destination)
            print("Nearest hub:",node.nearest_hub)
            try:
                hub_score[node.nearest_hub]+=(node.weight/((getDurationGMAPS(source,destination))*2))
            except Exception as e:
                print("Warning")
                hub_score[node.nearest_hub]+=(node.weight/((node.nearest_hub).getDistance(node)))
    return hub_score

def place_random_hubs(minlat,maxlat,minlon,maxlon,number_to_try,existing_nodes,existing_hubs):

    def genNewNode():
        minlat,maxlat=347,351
        minlon,maxlon=323,340
        lat=randint(minlat,maxlat)/10 #determine new lat
        long=randint(minlon,maxlon)/10 #determine new long
        print(lat,long)
        new_hub=Node(lat,long,"hub",0)
        return new_hub

    highscore=0
    best_hub=None
    for x in range(number_to_try):
        new_hub=genNewNode()
        total_hubs=[new_hub]+existing_hubs
        for node in existing_nodes:
            node.determineNearestHub(total_hubs)
        new_score=calculate_score(existing_nodes,[new_hub])
        while new_score[new_hub]==0:
            new_hub=genNewNode()
            new_score=calculate_score(existing_nodes,[new_hub])
        if new_score[new_hub]>highscore:
            best_hub=new_hub
            highscore=new_score[new_hub]
    return best_hub


'''
919 · Meeting Rooms II
https://www.lintcode.com/problem/920
'''
import heapq
import random
from typing import List, Tuple

from test_tool import assert_value

class Solution:
    def minMeetingRooms(self, intervals: List[Tuple[int]]) -> int:
        cnt = []
        for s, e in intervals:
            heapq.heappush(cnt, (s, 'start'))
            heapq.heappush(cnt, (e, 'end'))
        res, curr = 0, 0
        while cnt:
            t, i = heapq.heappop(cnt)
            if i == 'start':
                curr += 1
            else:
                curr -= 1
            res = max(curr, res)
        return res

assert_value(2, Solution().minMeetingRooms, intervals=[(0, 30), (5, 10), (15, 20)])
assert_value(1, Solution().minMeetingRooms, intervals=[(2, 7)])


from typing import List

class Solution:
    # 气死了，中位数的方法怎么都搞不好 merge 那，先写个排序的，之后把中位数的补上
    def wiggleSort(self, nums: List[int]) -> None:
        tmp = sorted(nums)
        mid_index = int((len(nums) - 1) / 2)
        i, j, k = mid_index, len(nums) - 1, 0
        while i >= 0 and j > mid_index:
            nums[k] = tmp[i]
            nums[k+1] = tmp[j]
            k += 2
            i -= 1
            j -= 1
        if i == 0:
            nums[k] = tmp[i]

    # """
    # 大小不等的不满足条件，经过交换会产生效果，但是对于大小相等的元素不适用。
    #
    # 要求时间 O(n)，空间 O(1) 是真的好难想 ...
    #
    # 思路：利用快排的思想得到 nums 的中位数，然后把小于中位数的数放在 (i & 1) == 0 的位置，大于中位数的数放在 (i & 1) == 1 的位置。
    #
    # 在 merge 时的注意要点：要从尾开始取元素！
    # 如果从头取元素：4556 -> 4556，不会发生变化
    # 从尾取元素：4456 -> 5645，不会导致第二个元素和第三个元素相等
    # """
    # def wiggleSort(self, nums: List[int]) -> None:
    #     tmp = [0] * len(nums)
    #     for i in range(len(nums)):
    #         tmp[i] = nums[i]
    #     mid_index = self.mid_num(tmp)
    #     i, j, k = mid_index, len(nums)-1, 0
    #     while i >= 0 and j > mid_index:
    #         nums[k] = tmp[i]
    #         nums[k+1] = tmp[j]
    #         k += 2
    #         i -= 1
    #         j -= 1
    #     if i == 0:
    #         nums[k] = tmp[i]
    #
    # def mid_num(self, nums):
    #     N = len(nums)
    #     mid_index = int((N - 1) / 2)
    #     index = self.partition(nums, 0, N-1, mid_index)
    #     while index != mid_index:
    #         if index < mid_index:
    #             index = self.partition(nums, index+1, N-1, mid_index)
    #         else:
    #             index = self.partition(nums, 0, index-1, mid_index)
    #     return mid_index
    #
    # def partition(self, nums, lo, hi, pos):
    #     nums[lo], nums[pos] = nums[pos], nums[lo]
    #     i = lo
    #     for j in range(lo+1, hi+1):
    #         if nums[j] <= nums[lo]:
    #             i += 1
    #             nums[i], nums[j] = nums[j], nums[i]
    #     nums[lo], nums[i] = nums[i], nums[lo]
    #     return i

if __name__ == '__main__':
    s = Solution()
    # nums = [1, 2, 2, 1, 2, 1, 1, 1, 1, 2, 2, 2]
    # nums = [1, 2, 2, 1, 2, 1, 1, 1, 1, 2, 2]
    # nums = [1, 5, 1, 1, 6, 4]
    # WA points
    # nums = [1, 3, 2, 2, 3, 1]
    # nums = [4, 5, 5, 6]
    nums = [5, 3, 1, 2, 6, 7, 8, 5, 5]

    # print(s.partition(nums, 0, len(nums)-1, 0))
    # s.mid_num(nums)
    # print(nums)

    s.wiggleSort(nums)
    print(nums)


import base_net
import json

ADDRESS = "localhost"
PORT = 50000
LISTEN = 10

class ServerUi(base_net.Server):
    def __init__(self, *args):
        super().__init__(*args)

    def on_receive(self, client, data):
        print(data)
        _json = json.loads(data)
        self.send_all(_json["message"])

if __name__ == "__main__":
    server = ServerUi(ADDRESS, PORT, LISTEN)
    server.start()


from turtle import *

t = Turtle()
t.shape('turtle')
steps = 10

for figure in range(15):

    # тут я рисую квадраты
    for square in range(4):
        t.rt(90)
        t.fd(steps)

    # а тут я отступаю от каждого квадрата
    t.up()
    t.lt(45)
    t.fd(10)
    t.rt(45)
    t.down()

    # увеличиваю сторону
    steps += 15


import sys
import math
input = sys.stdin.readline

n = int(input())
s = input().rstrip()
c_cnt = 0
s_cnt = 0
for i in s:
    if i == "C":
        c_cnt += 1
    else:
        s_cnt += 1

print(math.ceil(c_cnt / (s_cnt + 1)))


import telebot
from config_cb import keys,TOKEN
from extensions_cb import ConversionException, CryptoConverter

bot = telebot.TeleBot(TOKEN)

@bot.message_handler(commands=['start', 'help'])
def help(message: telebot.types.Message):
    text = "Привет!\nЯ бот, который конвертирует валюты в режиме реального времени!\
    \nДля начала работы задай Боту следующие параметры в одну строку через пробел: \n<имя валюты> \
    \n<в какую валюту перевести> \
    <количество валюты> \
    \nНАПРИМЕР: доллар евро 1 \
    \nСписок доступных валют можно увидеть по команде:\n /values"
    bot.reply_to(message,text)

@bot.message_handler(commands=['values'])
def values(message: telebot.types.Message):
    text = "Я могу конвертировать такие валюты:"
    for key in keys.keys():
        text = '\n'.join((text,key ))
    bot.reply_to(message,text)

@bot.message_handler(content_types=['text', ])
def convert(message: telebot.types.Message):
    try:
        values = message.text.split(' ')

        if len(values) != 3:
            raise ConversionException('Ошибка в параметрах, нужно три!\nпопробуй снова!')

        quote, base, amount = map(lambda x: x.lower(), values) #Название валют можно вводить с любым регистром
        total_base = CryptoConverter.convert(quote, base, amount)
        counter = float(values[2])
        total_summ = total_base * counter #подсчитываем количество валюты
    except ConversionException as e:
        bot.reply_to(message, f'Ошибка пользователя.\n{e}')
    except Exception as e:
        bot.reply_to(message, f'Не удалось обработать команду\n{e}')
    else:
        text = f'Стоимость {amount} {quote} в {base} равен {total_summ}'
        bot.send_message(message.chat.id, text)

bot.polling()


import multiprocessing
import tensorflow_hub as hub;
import tensorflow as tf
import cv2
import numpy as np

PIXEL_SKIP = 1
WIDTH = 640
HEIGHT = 480
#  [nose, left eye, right eye, left ear, right ear, left shoulder, right shoulder, left elbow, right elbow, left wrist,
#  right wrist, left hip, right hip, left knee, right knee, left ankle, right ankle]
def get_keypoints(frame, keypoints, confidence_threshold):
    y, x, c = frame.shape
    shaped = np.squeeze(np.multiply(keypoints, [y,x,1]))
    right_wrist = shaped[9]
    ky, kx, kp_conf = right_wrist
    if(kp_conf > confidence_threshold):
        coords = convert_to_scale(kx, ky)
        return coords
    else:
        return None

def convert_to_scale(x, y):
    return ({'x' :  round(abs((x / 640)  - 1), 2) , 'y' : round(abs((y / 480)  -1), 2)})

def loop_through_people(frame, keypoints_with_scores,confidence_threshold):
    points = []
    for person in keypoints_with_scores:
        key_points_for_person = get_keypoints(frame, person, confidence_threshold)
        if(key_points_for_person != None):
            points.append(key_points_for_person)
    return points

def group_data(points):
    grouped_data = {}
    for d in points:
        key = None
        for k in grouped_data.keys():
            if abs(k[0] - d['x']) <= 0.02 and abs(k[1] - d['y']) <= 0.02:
                key = k
                break
        if key:
            grouped_data[key].append(d)
        else:
            grouped_data[(d['x'], d['y'])] = [d]
    result = list(grouped_data.values())
    return result

def run_stream(isRunning, queue):
    model = hub.load('https://tfhub.dev/google/movenet/multipose/lightning/1')
    movenet = model.signatures['serving_default']
    cap = cv2.VideoCapture('udp://127.0.0.1:1235', )
    fps = round(cap.get(cv2.CAP_PROP_FPS))
    hop = round(fps / 1)
    curr_frame = 0
    while cap.isOpened():
        ret,frame = cap.read()
        if curr_frame % hop == 0:
            img = frame.copy()
            img = tf.image.resize_with_pad(tf.expand_dims(img, axis=0), 192,256)
            input_image = tf.cast(img, dtype=tf.int32)
            results = movenet(input_image)
            keypoints_with_scores = results['output_0'].numpy()[:,:,:51].reshape((6,17,3))
            selected_points = loop_through_people(frame, keypoints_with_scores, 0.1)
            queue.put(selected_points)
        curr_frame += 1
        if isRunning == False:
            break
    cap.release()

class Posenet():

    def __init__(self):
        self.isRunning = False
        self.cluster_buffer_data = []
        self.window_size = 3
        self.queue = None
        self.process = None

    def run_algorithm(self):
        self.isRunning = True
        self.queue = multiprocessing.Queue()
        self.process = multiprocessing.Process(target=run_stream, args=(self.isRunning, self.queue,))
        self.process.start()

    def get_cluster_coords(self):
        return self.queue.get()

    def get_smoothed_cluster_coords(self):
        clusters = self.get_cluster_coords()
        return clusters
        # cluster_buffer_data = self.cluster_buffer_data
        # smoothed_cluster_centroids = []
        # if(len(clusters) == 0):
        #     for cluster in cluster_buffer_data:
        #         updated = update_cluster_buffer(None, cluster, self.window_size)
        #         smoothed_cluster_centroids.append(get_movement_value(updated))
        # else:
        #     for i in range(len(clusters)):
        #         current_cluster_centroid = clusters[i]
        #         current_cluster_buffer_data = {
        #             'x' : [],
        #             'y' : [],
        #             'sum_x' : 0,
        #             'sum_y' : 0
        #         }
        #         if(i+1 <= len(cluster_buffer_data)):
        #             current_cluster_buffer_data = cluster_buffer_data[i]
        #         updated_cluster_buffer = update_cluster_buffer(current_cluster_centroid, current_cluster_buffer_data, self.window_size)
        #         if(i+1 <= len(cluster_buffer_data)):
        #             self.cluster_buffer_data[i] = updated_cluster_buffer
        #         else:
        #             self.cluster_buffer_data.append(updated_cluster_buffer)
        #         smoothed_cluster_centroids.append(get_movement_value(updated_cluster_buffer))
        # return smoothed_cluster_centroids

def update_cluster_buffer(latest_point, cluster_buffer, window_size):
    if len( cluster_buffer['x']) == window_size or (latest_point == None and len( cluster_buffer['x']) > 0):
        cluster_buffer['sum_x'] -= cluster_buffer['x'].pop(0)
    if len(cluster_buffer['y']) == window_size or (latest_point == None and len( cluster_buffer['y']) > 0):
        cluster_buffer['sum_y'] -= cluster_buffer['y'].pop(0)

    if latest_point != None:
        x = latest_point['x']
        y = latest_point['y']
        cluster_buffer['x'].append(x)
        cluster_buffer['y'].append(y)
        cluster_buffer['sum_x'] += x
        cluster_buffer['sum_y'] += y

    return cluster_buffer

def get_movement_value(cluster_data):
        x = -2
        y = -2
        if len(cluster_data['x']) > 0:
            x =  cluster_data['sum_x'] / len(cluster_data['x'])
        if len( cluster_data['y']) > 0:
            y = cluster_data['sum_y'] / len( cluster_data['y'])
        return {'x' : x, 'y' : y}


from unittest.mock import Mock, patch

import pytest
import voluptuous
from homeassistant import data_entry_flow
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant

from custom_components.iolite import DOMAIN

@pytest.mark.asyncio
async def test_flow_show_form(hass: HomeAssistant) -> None:
    """Test that the setup form is served."""
    result = await hass.config_entries.flow.async_init(
        DOMAIN, context={"source": SOURCE_USER}
    )

    assert result["type"] == data_entry_flow.FlowResultType.FORM
    assert result["step_id"] == "user"

@pytest.mark.asyncio
@patch("custom_components.iolite.config_flow.validate_and_persist_auth")
async def test_flow_invalid_scan_interval_range(
    _validate_and_persist_auth: Mock, hass: HomeAssistant
) -> None:
    """Test that the scan interval range is invalid."""
    result = await hass.config_entries.flow.async_init(
        DOMAIN, context={"source": SOURCE_USER}
    )

    with pytest.raises(voluptuous.error.MultipleInvalid):
        await hass.config_entries.flow.async_configure(
            result["flow_id"], user_input={CONF_SCAN_INTERVAL: 10}
        )

    with pytest.raises(voluptuous.error.MultipleInvalid):
        await hass.config_entries.flow.async_configure(
            result["flow_id"], user_input={CONF_SCAN_INTERVAL: 125}
        )


from typing import List, Optional

from fastapi import APIRouter, Depends, HTTPException, Response, status
from sqlalchemy.orm import Session
from .. import models, schemas, oauth2
from ..database import get_db

router = APIRouter(
    prefix='/votes',
    tags=['Votes'] #groups commands in the swagger docs
)
def get_vote_by_id(user_id: int, post_id: int, db: Session = Depends(get_db)):

    post = db.query(models.Post).filter(models.Post.id == post_id).first()
    if not post:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Error applying vote, post with id: {post_id} does not exist")
    vote_query = db.query(models.Vote).filter(models.Vote.user_id == user_id, models.Vote.post_id == post_id)
    vote = vote_query.first()
    # if not vote:
    #     raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
    #                         detail=f'Post with id: {user_id} does not exist')
    return vote, vote_query

def checkIfUserIsCurrent(current_user_id: int, original_user_id: int):
    if current_user_id != original_user_id:
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
                            detail="Not authorized to perform requested action")

@router.get("/{id}", status_code=status.HTTP_200_OK, response_model=schemas.VoteResponse)
def get_votes(id:int, db: Session = Depends(get_db),current_user_id: int = Depends(oauth2.get_current_user)):
    vote, _ = get_vote_by_id(user_id = current_user_id, post_id = id, db=db)
    return vote

@router.post("/", status_code=status.HTTP_201_CREATED, response_model=schemas.VoteResponse)
def create_vote(vote: schemas.VoteCreate, db: Session = Depends(get_db), current_user_id: int = Depends(oauth2.get_current_user)):
    vote_out, existing_vote_query = get_vote_by_id(user_id=current_user_id, post_id=vote.post_id, db=db)
    if vote_out:
        if (vote_out.direction > 0 and vote.direction > 0) or (vote_out.direction < 0 and vote.direction < 0):
            vote.direction = 0
        existing_vote_query.update(vote.dict(), synchronize_session=False)
        db.commit()
    else:
        vote_out = models.Vote(**vote.dict())
        vote_out.user_id = current_user_id
        db.add(vote_out)
        db.commit()
        db.refresh(vote_out)
    return vote_out

@router.put("/", status_code=status.HTTP_201_CREATED, response_model=schemas.VoteResponse)
def update_vote(vote: schemas.VoteCreate, db: Session = Depends(get_db), current_user_id: int = Depends(oauth2.get_current_user)):
    vote_out, existing_vote_query = get_vote_by_id(user_id=current_user_id, post_id=vote.post_id, db=db)
    if vote_out:
        if (vote_out.direction > 0 and vote.direction > 0) or (vote_out.direction < 0 and vote.direction < 0):
            vote.direction = 0
        existing_vote_query.update(vote.dict(), synchronize_session=False)
        db.commit()
    else:
        raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Cannot update vote, vote does not exist")
    return vote_out

# @router.post("/", status_code=status.HTTP_201_CREATED, response_model=schemas.PostResponse)
# def create_posts(post: schemas.PostCreate, db: Session = Depends(get_db),
#                  current_user_id: int = Depends(oauth2.get_current_user)):
#     new_post = models.Post(**post.dict())
#     new_post.user_id=current_user_id
#     db.add(new_post)
#     db.commit()
#     db.refresh(new_post)
#     return new_post

# @router.get("/", response_model=List[schemas.PostResponse])
# def get_all_posts(db: Session = Depends(get_db),  user_id: Optional[int]=None, limit: int = 10, skip: int = 0, title: Optional[str] = ""):
#     filters = [(models.Post.user_id,user_id), (models.Post.title.contains,title)]

#     query = db.query(models.Post)
#     # for (model,value) in filters:
#     #     if value:
#     #         query = query.filter(model==value)
#     if user_id:
#         query = query.filter(models.Post.user_id==user_id)
#     if title:
#         query = query.filter(models.Post.title.ilike(title))

#     posts=query.limit(limit).offset(skip).all()

#     if len(posts) == 0:
#         raise HTTPException(status_code=status.HTTP_204_NO_CONTENT, detail="No posts found")
#     return posts

# @router.get("/{id}", response_model=schemas.PostResponse)
# def get_post(id: int, db: Session = Depends(get_db)):
#     post, _ = get_post_by_id(id, db)
#     return post

# @router.delete("/{id}", status_code=status.HTTP_204_NO_CONTENT)
# def delete_post(id: int, db: Session = Depends(get_db),
#                 current_user_id: int = Depends(oauth2.get_current_user)):
#     post,post_query = get_post_by_id(id, db)
#     checkIfUserIsCurrent(current_user_id,post.user_id)
#     post_query.delete(synchronize_session=False)
#     db.commit()
#     return Response(status_code=status.HTTP_204_NO_CONTENT)

# @router.put("/{id}", response_model=schemas.PostResponse) #), status_code=status.)
# def update_post(id: int, post: schemas.PostCreate, db: Session = Depends(get_db),
#                 current_user_id: int = Depends(oauth2.get_current_user)):
#     orig_post, orig_post_query= get_post_by_id(id, db)
#     checkIfUserIsCurrent(current_user_id,orig_post.user_id)
#     orig_post_query.update(post.dict(), synchronize_session=False)
#     db.commit()
#     return orig_post


import requests, math, emoji, apiKey, datetime
import tkinter as tk
import matplotlib.pyplot as plot
from tkinter import Frame, messagebox

class currConverterGUI:
    def __init__(self):
        # Set up the interface
        self.root = tk.Tk()

        rootWidth = 1080
        rootHeight = 720
        screenWidth = self.root.winfo_screenwidth()
        screenHeight = self.root.winfo_screenheight()

        xCoord = (screenWidth/2) - (rootWidth/2)
        yCoord = (screenHeight/2) - (rootHeight/2)

        self.root.geometry("{}x{}+{}+{}".format(rootWidth, rootHeight, int(xCoord), int(yCoord))) # Interface will appear in the middle of the user screen
        self.root.resizable(False, False)
        self.root.iconbitmap("images/favicon.ico")
        self.root.title("Currency Converter")

        #  Set up the frame for title, set up the title and subtitle for the program
        self.titleFrame = Frame(self.root)
        self.titleFrame.pack()

        self.title = tk.Label(self.titleFrame, text="Currency Converter", font=("Tahoma", 36))
        self.title.pack(padx=10, pady=40)

        self.subtitle = tk.Label(self.root, text="How much do you want to convert?", font=("Tahoma", 24))
        self.subtitle.pack(pady=25)

        # Set up the frame for labels
        self.label = Frame(self.root)
        self.label.pack()

        self.fromLabel = tk.Label(self.label, text="From: ", font=("Tahoma",18))
        self.fromLabel.pack(side=tk.LEFT, padx=140)

        self.toLabel = tk.Label(self.label, text="To: ", font=("Tahoma", 18))
        self.toLabel.pack(padx=140)

        # Set up the frame for user inputs
        self.currInput = Frame(self.root)
        self.currInput.pack()

        self.currAmtBefore = tk.Entry(self.currInput, width=15, font=("Tahoma", 18))
        self.currAmtBefore.pack(side=tk.LEFT, padx=10)

        self.currCodeBefore = tk.Entry(self.currInput, width=5, font=("Tahoma", 18))
        self.currCodeBefore.pack(side=tk.LEFT, padx=10)

        self.arrowBtn = tk.Button(self.currInput, text="⇆", width=2, command=self.swapCurrency, font=("Tahoma", 12))
        self.arrowBtn.pack(side=tk.LEFT, padx=5)

        self.displayAmt = tk.IntVar(self.root, 0) # Set one of the inputs to zero

        self.currAmtAfter = tk.Entry(self.currInput, width=15, textvariable=self.displayAmt, state="readonly", font=("Tahoma", 18))
        self.currAmtAfter.pack(side=tk.LEFT, padx=10)

        self.currCodeAfter = tk.Entry(self.currInput, width=5, font=("Arial", 18))
        self.currCodeAfter.pack(side=tk.LEFT, padx=10)

        # Set up the frame for "Convert" button
        self.btn = Frame(self.root)
        self.btn.pack()

        self.convertBtn = tk.Button(self.btn, text="Convert", command=self.convertCurrency, font=("Tahoma", 20))
        self.convertBtn.pack(pady=25)

        # Set up the frame for "days" input and "View Previous Conversion Rates" button
        self.displayPrevRate = Frame(self.root)
        self.displayPrevRate.pack()

        self.daysInput = tk.Entry(self.displayPrevRate, width=5, font=("Tahoma", 18))
        self.daysInput.pack(side=tk.LEFT, padx=10)

        self.text = tk.Label(self.displayPrevRate, text="Days", font=("Tahoma", 18))
        self.text.pack(side=tk.LEFT)

        self.viewPrevRateBtn = tk.Button(self.displayPrevRate, text="View Previous Conversion Rate", command=self.viewPreviousRate, font=("Tahoma", 16))
        self.viewPrevRateBtn.pack(padx=25, pady=20)

        # Set up the footer of the interface
        self.footerFrame = Frame(self.root)
        self.footerFrame.pack(side=tk.BOTTOM)

        self.clearAllBtn = tk.Button(self.footerFrame, text="Clear All", command=self.clearAll, font=("Tahoma", 18))
        self.clearAllBtn.pack()

        self.footerContent = tk.Message(self.footerFrame, text=emoji.emojize("Made With ❤ by Larry"), width=300, foreground="red", font=("Tahoma", 18))
        self.footerContent.pack(pady=50)

        # Prompts the user to confirm whether to close the program
        self.root.protocol("WM_DELETE_WINDOW", self.closeProgram)

        self.root.mainloop()

    def closeProgram(self):
        if messagebox.askyesno(title="Quit Program", message="Do you want to quit?"):
            self.root.destroy()

    def clearAll(self):
        self.currCodeBefore.delete(0, len(self.currCodeBefore.get()))
        self.currCodeAfter.delete(0, len(self.currCodeAfter.get()))
        self.currAmtBefore.delete(0, len(self.currAmtBefore.get()))
        self.daysInput.delete(0, len(self.daysInput.get()))

    def swapCurrency(self):
        temp = self.currCodeBefore.get()
        self.currCodeBefore.delete(0, len(self.currCodeBefore.get()))
        self.currCodeBefore.insert(0, self.currCodeAfter.get())
        self.currCodeAfter.delete(0, len(self.currCodeAfter.get()))
        self.currCodeAfter.insert(0, temp)

    def convertCurrency(self):
        self.currAmtAfter.config(state="normal")
        amtBefore = self.currAmtBefore.get()
        codeBefore = self.currCodeBefore.get().upper()
        codeAfter = self.currCodeAfter.get().upper()
        if not amtBefore or not codeBefore or not codeAfter:
            messagebox.showwarning(title="Error!", message="Please fill up the necessary fields")
            return

        response1 = requests.get(f"https://v6.exchangerate-api.com/v6/{apiKey.apiKey}/latest/{codeBefore.strip()}")
        try:
            jsonData1 = response1.json()
        except:
            messagebox.showwarning(title="Error!", message="Either one of the currency codes entered is invalid.")
            self.currAmtAfter.config(state="readonly")
            return

        if jsonData1["result"] == "error" or response1.status_code == "404":
            messagebox.showwarning(title="Error!", message="Either one of the currency codes entered is invalid.")
            self.currAmtAfter.config(state="readonly")
            return

        conversionRate = jsonData1["conversion_rates"][f"{codeAfter.strip()}"]
        try:
            total = int(amtBefore) * conversionRate
        except:
            messagebox.showwarning(title="Error!", message="Please enter a valid number.")
            self.currAmtAfter.config(state="readonly")
            return

        self.currAmtAfter.delete(0, len(str(total)))
        self.currAmtAfter.insert(0, "{:.2f}".format(total))
        self.currAmtAfter.config(state="readonly")

    def viewPreviousRate(self):
        amtOfDays = self.daysInput.get()
        codeBefore = self.currCodeBefore.get().upper()
        codeAfter = self.currCodeAfter.get().upper()

        if not amtOfDays or not codeBefore or not codeAfter:
            messagebox.showwarning(title="Error!", message="Please fill up the necessary fields.")
            return

        try:
            if int(amtOfDays) >= 365:
                messagebox.showwarning(title="Error!", message="Please refrain from requesting too much data.")
                return
        except:
            messagebox.showwarning(title="Error!", message="Please enter a valid number.")
            return

        # Structure user input before sending request to API
        prevDates = []
        for i in range(1, int(amtOfDays)+1):
            timestamp = str(datetime.datetime.today() - datetime.timedelta(days = i))
            date = timestamp[0:10]
            prevDates.append(date)

        # Retrieve and store converstion rate data according to each data from API
        prevConversionRates = []
        for dates in prevDates[::-1]:
            response2 = requests.get(f"https://theforexapi.com/api/{dates}/?base={codeBefore}&symbols={codeAfter}")
            if response2.status_code == "400":
                messagebox.showwarning(title="Error!", message="Invalid input(s).")
            jsonData2 = response2.json()
            try:
                conversionRate = jsonData2["rates"][f"{codeAfter}"]
            except:
                messagebox.showwarning(title="Error!", message="Either one of the currency codes entered is invalid.")
                return
            prevConversionRates.append(float(f"{conversionRate:.6f}"))

        monthDay = []
        for dates in prevDates[::-1]:
            monthDay.append(dates[5:10])

        # Set up algorithm to improve graph visualization by showing each rates after i days
        splitMonthDay = []
        splitConversionRates = []
        num = 1
        for i in range(2, 14):
            qiotient = int(amtOfDays) / i
            if qiotient > 15 and qiotient < 25:
                num = math.ceil(i)
                break

        if int(amtOfDays) >= 20:
            splitMonthDay = monthDay[::num]
            x = splitMonthDay
            splitConversionRates = prevConversionRates[::num]
            y = splitConversionRates
        else:
            x = monthDay
            y = prevConversionRates

        # Plot graph
        plot.plot(x, y)

        plot.xlabel("Date")
        plot.ylabel(f"{codeBefore} to {codeAfter}")

        plot.title(f"Graph of {codeBefore} to {codeAfter}")

        plot.show()

if __name__ == "__main__":
    currConverterGUI()


import dill as pickle

ent_tags = {'PER', 'LOC', 'ORG', 'MISC'}
def is_named_ent(yc):
    yes_ct = 0
    no_ct = 0
    for tag, count in yc.ent_types.items():
        if tag in ent_tags:
            yes_ct += count
        else:
            no_ct += count
    if yes_ct >= no_ct:
        return True
    return False

for year in range(1995, 2023):
    ycs = pickle.load(open(f'dumps/invalid_chars_filtered/{year}ycs', 'rb'))
    kept = {}
    for text, yc in ycs.items():
        if not is_named_ent(yc):
            kept[text] = yc
    with open(f'dumps/named_ents_filtered/{year}ycs', 'wb') as outfile:
        pickle.dump(kept, outfile)


import dash
import dash_core_components as dcc

import plotly.graph_objects as go

from dash.dependencies import Input, Output,State
from app import app
import numpy as np

import json
from io import BytesIO
import pandas as pd
from wordcloud import WordCloud
import base64
import dash_cytoscape as cyto
import dash_html_components as html
from sklearn.manifold import TSNE
from gensim.models import AuthorTopicModel

# temp_file = datapath("C:/Users/Subhashree/Documents/KMD_Thesis/Data Final/ATM/model")
#temp_file = datapath("C:/Users/Subhashree/Documents/KMD_Thesis/Complete Code Collection/Author Topic Model/model")
# Load a potentially pretrained model from disk.
model = AuthorTopicModel.load("model")

tsne = TSNE(n_components=2, random_state=0)
smallest_author = 5  # Ignore authors with documents less than this.
authors = [model.author2id[a] for a in model.author2id.keys() if len(model.author2doc[a]) >= smallest_author]
_ = tsne.fit_transform(model.state.gamma[authors, :])  # Result stored in tsne.embedding_

author_names = [model.id2author[a] for a in authors]
author_sizes = [len(model.author2doc[a]) for a in author_names]
author_top_names = pd.DataFrame()
author_top_names['Names'] = author_names
author_top_names['sizes'] = author_sizes
author_top_names=author_top_names.sort_values(by='sizes', ascending=False)
author_top_names = author_top_names.head(100)
topic_labels = ['Topic 0', 'Topic 1', 'Topic 2', 'Topic 3',
                'Topic 4', 'Topic 5', 'Topic 6',
                'Topic 7', 'Topic 8', 'Topic 9', 'Topic 10']

def show_author(name):
    print('\n%s' % name)
    print('Docs:', model.author2doc[name])
    print('Topics:')
    print([(topic_labels[topic[0]], topic[1]) for topic in model[name]])

mark_r=['0','5','10','15','20']
mark_y=['0','5','10','15','20','25','30','35','40']
mark_g=['0','5','10','15','20','25','30','35','40','45','50','55','60']

def plot_wordcloud(data):
    d = {a: x for a, x in data.values}
    wc = WordCloud(background_color='white', width=500, height=350)
    wc.fit_words(d)
    return wc.to_image()
# Element of network
# -------------------------------------------
# df = pd.read_csv (r'C:\Users\Subhashree\Documents\KMD_Thesis\Data Final\ATM\ATMv2_filtered_data\top500similar\Result_Atm.csv')
df = pd.read_csv (r'ATM\Result_Atm.csv')
# source = df['Source_auth'].unique()
# target = df['Target_auth'].unique()
# arr = np.concatenate((source, target))
# arr = set(arr)
degrees= dict([(i,s) for i,s in zip(df.Target_auth, df.Size)])

ele = set()
for i in df.iterrows():
    ele.add((i[1]['Source_auth'],i[1]['Target_auth'],i[1]['Score'],i[1]['Common']))

nodes = [
    {
        'data': {'id': s, 'label': s, 'size':degrees[s]},
        'position': {'x': 20, 'y': -20}
    }
    for s in (degrees)
]

edges = [
    {'data': {'source': source, 'target': target,'width':score*2,'weight':common}}
    for source, target,score,common in (ele)
]

default_stylesheet = [
    {
        'selector': 'node',
        'style': {
            # 'background-color': '#BFD7B5',

            'label': 'data(label)',
            "width": "mapData(size, 0, 170, 10,200)",
            "height": "mapData(size, 0, 170,10,200)",
            'background-color':'F7DC6F', #'#5D6D7E', #F7DC6F,#2722AF',
            'background-opacity': '0.4'
        }
    },
    {
        'selector': 'edge',
        'style': {
            'line-color': '#A3C4BC',
            'width':'data(width)',
            'curve-style':'unbundled-bezier'
        }
    }
]

elements = nodes + edges
new_styles = [

    {
        'selector': '[weight >= 0]',
        'style': {
            'line-color': 'red'
        }
    },
    {
        'selector': '[weight >= 3 ]',
        'style': {
            'line-color': 'blue'
        }
    }
    ,
    {
        'selector': '[weight >= 15]',
        'style': {
            'line-color': 'green'
        }
    }
]
# -------------------------------------------
def layout():
    return html.Div(children=[
        html.Div([
            html.H3('Author Topic Model : Analysis and visualizations for author similarity, co-authorship links etc.',style={'textAlign': 'center'}),
        ], className = 'row',style={'backgroundColor': '#d2e4f7'}
        ),
        html.Div([
            dcc.Markdown('''
                * **Cytoscape Network Graph**: This is a author similarity network plot. The nodes are the authors,
                the edges from a node connect to the similar authors. The size of the node is proportional to the number of publications by the author.
                * Co-authorship Links: This is depicted by the color of the egde between two nodes.
                * Red: Co-aothored very few ( 0-2 )
                * Blue: Co-authored moderartely ( 3-15 )
                * Green: Co-authored largly ( >15 )
                * **Topic Distribution Donut Chart:** On clicking on a node, the donut chart shows the interest distribution of the selected author.
                * **Topic Content Word Cloud**: On clicking a topic on the donut chart, the top words consisting the topic is displayed in the form of a word cloud
                ''')
        ], className='row',),

        html.Div(children=[

            html.Div(children=[

                html.Div([
                    html.H4('Select the network layout'),
                    dcc.Dropdown(
                        id='dropdown-update-layout',
                        value='cose',
                        clearable=False,
                        options=[
                            {'label': name.capitalize(), 'value': name}
                            for name in ['grid', 'random', 'circle', 'cose', 'concentric']
                        ]
                    )
                ],className='two columns',
                    style={'width': '25%', 'display': 'inline-block'}),

        ],className='row',
          style={'borderBottom': 'thin lightgrey solid',
                 'backgroundColor': 'rgb(250, 250, 250)'}),

        html.Div(children=[
            cyto.Cytoscape(
                id='cytoscape-atm-nodes',
                stylesheet=default_stylesheet +new_styles,
                layout={'name': 'cose'},
                style={'width': '1500px','height':'1000px'},
                elements=elements
            )
        ],className='row',
          style={'borderBottom': 'thin lightgrey solid'}),

        ],className='row'),

        html.Div(children=[
            html.Div(children=[
                dcc.Graph(id='donut')
            ],style={'width': '30%', 'display': 'inline-block'},
                className= 'three columns'),

            html.Div(children=[
                html.Img(id="image_wc"),
                html.Div(id='dd-output-container')
            ],className='three columns',
                style={'width': '60%', 'display': 'inline-block'})

        ],className='row')

    ])

@app.callback(Output('cytoscape-atm-nodes', 'layout'),
              Input('dropdown-update-layout', 'value'))
def update_layout(layout):
    return {
        'name': layout,
        'animate': True
    }

@app.callback(
    dash.dependencies.Output('donut', 'figure'),
    [Input('cytoscape-atm-nodes', 'tapNodeData')])
def update_output(data):
    value=data['id']
    # print(value)
    df2 = pd.DataFrame(np.array([(topic_labels[topic[0]], topic[1]) for topic in model[value]]),
                       columns=['Topic_Name', 'Distribution'])
    res=[]
    for i in range(0,11):
        name = 'Topic '+ str(i)
        if name not in df2.values:
            res.append([name,''])
        else:
            res.append([name,float(df2.loc[df2['Topic_Name'] == name]['Distribution'].values[0])])
    resdf = pd.DataFrame(res, columns = ['Topic_Name', 'Distribution'])
    labels=resdf['Topic_Name']
    traces=[]
    traces.append(
        go.Pie(
            labels=labels,
            # values=df2['Distribution'],
            values=resdf['Distribution'],
            name=value,hole=.4,
            marker={'colors':['#C0392B', '#E74C3C', '#9B59B6', '#2980B9', '#16A085', '#F1C40F', '#E59866', '#0B5345', '#D2B4DE', '#F39C12','#5D6D7E']},
            hoverinfo="label+percent"
    )
                  )
    return {
        'data':traces,
        'layout':go.Layout(
            title_text="Interest distribution of author over topics",
            annotations=[dict(text=value, x=0.18, y=0.5, font_size=13, showarrow=False)]
        )
    }

@app.callback(dash.dependencies.Output('image_wc', 'src'),
              [dash.dependencies.Input('image_wc', 'id'),
               dash.dependencies.Input('donut','clickData')])
def make_image(b,clk_data):
    print(clk_data)
    x = json.dumps(clk_data)
    y = json.loads(x)
    m=y['points']
    n=json.dumps(m)
    j = json.loads(n)
    topic_lbl=j[0]['label']
    print(topic_lbl)
    # topic_wc = datapath("C:/Users/Subhashree/Documents/KMD_Thesis/Data Final/ATM/ATMv2_filtered_data/"+topic_lbl+".csv")
    #topic_wc = datapath(topic_lbl+".csv")
    ff = pd.read_csv ("ATM\\"+topic_lbl+".csv")
    ff_df = pd.DataFrame(ff)
    words = ff_df['word'].tolist()
    freq = ff_df['score'].tolist()
    dfm = pd.DataFrame({'word': words, 'freq': freq})
    img = BytesIO()
    plot_wordcloud(data=dfm).save(img, format='PNG')
    return 'data:image/png;base64,{}'.format(base64.b64encode(img.getvalue()).decode())