from __future__ import annotations

from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
from warnings import warn

import numpy as np
import pandas as pd
from numpy.polynomial.polynomial import polyfit, polyval
from numpy.typing import ArrayLike
from pandas import DataFrame
from scipy.interpolate import UnivariateSpline as USpline
from scipy.optimize import curve_fit
from typing_extensions import Literal

from empyricalRMT._constants import (
    DEFAULT_POLY_DEGREE,
    DEFAULT_SPLINE_DEGREE,
    DEFAULT_SPLINE_DEGREES,
    DEFAULT_SPLINE_SMOOTH,
    DEFAULT_SPLINE_SMOOTHS,
)
from empyricalRMT._types import fArr
from empyricalRMT.detrend import emd_detrend
from empyricalRMT.exponentials import gompertz

SPLINE_DICT = {3: "cubic", 4: "quartic", 5: "quintic"}

class SmoothMethod(Enum):
    Polynomial = "poly"
    Spline = "spline"
    Exponential = "exp"
    Gompertz = "gompertz"
    GOE = "goe"
    Poisson = "poisson"

    @classmethod
    def validate(cls, s: str | SmoothMethod) -> SmoothMethod:
        try:
            if isinstance(s, str):
                return cls(s)
            return s
        except Exception as e:
            values = [e.value for e in cls]
            raise ValueError(f"SmoothMethod must be one of {values}") from e

SmoothArg = Union[List[float], Literal["heuristic"]]

def _spline_name(i: int) -> str:
    return SPLINE_DICT[i] if SPLINE_DICT.get(i) is not None else f"deg{i}"

class Smoother:
    def __init__(self, eigenvalues: ArrayLike):
        """Initialize a Smoother.

        Parameters
        ----------
        eigenvalues: ndarray
            Eigenvalues for fitting to the step function.
        """
        try:
            eigs = np.array(eigenvalues).astype(np.float64)
        except BaseException as e:
            raise ValueError("Could not convert eigenvalues into numpy array.") from e
        if len(eigs) != len(eigs.reshape((-1,))):
            raise ValueError("Input array must be one-dimensional.")
        eigs = eigs.flatten()
        self._eigs = np.sort(eigs)

    def fit(
        self,
        smoother: SmoothMethod = SmoothMethod.Polynomial,
        degree: int = DEFAULT_POLY_DEGREE,
        spline_smooth: float = DEFAULT_SPLINE_SMOOTH,
        detrend: bool = False,
        return_callable: bool = False,
    ) -> Tuple[fArr, fArr, Optional[Callable[[fArr], fArr]]]:
        """Computer the specified smoothing function values for a set of eigenvalues.

        Parameters
        ----------
        eigs: ndarray
            The sorted eigenvalues

        smoother: "poly" | "spline" | "gompertz" | lambda
            The type of smoothing function used to fit the step function

        degree: int
            The degree of the polynomial or spline

        spline_smooth: float
            The smoothing factors passed into scipy.interpolate.UnivariateSpline

        detrend: bool
            Whether or not to perform EMD detrending before returning the
            unfolded eigenvalues.

        return_callable: bool
            If true, return a function that closes over the fit parameters so
            that, e.g., additional values can be fit later.

        Returns
        -------
        unfolded: ndarray
            the unfolded eigenvalues

        steps: ndarray
            the step-function values
        """
        eigs = self._eigs
        # steps = _step_function_fast(eigs, eigs)
        steps = np.arange(0, len(eigs)) + 1
        self.__validate_args(smoother=smoother, degree=degree, spline_smooth=spline_smooth)

        if smoother is SmoothMethod.Polynomial:
            poly_coef = polyfit(eigs, steps, degree)
            unfolded = polyval(eigs, poly_coef)
            func = lambda x: polyval(x, poly_coef) if return_callable else None
            if detrend:
                unfolded = emd_detrend(unfolded)
            return unfolded, steps, func  # type: ignore

        if smoother is SmoothMethod.Spline:
            k = DEFAULT_SPLINE_DEGREE
            try:
                k = int(degree)
            except BaseException as e:
                print(ValueError("Cannot convert spline degree to int."))
                raise e
            if spline_smooth == "heuristic":
                s = len(eigs) * np.var(eigs, ddof=1)
                spline = USpline(eigs, steps, k=k, s=s)
            elif spline_smooth is not None:
                if not isinstance(spline_smooth, float):
                    raise ValueError("Spline smoothing factor must be a float")
                spline = USpline(eigs, steps, k=k, s=spline_smooth)
            else:
                raise ValueError(
                    "Unreachable: All possible spline_smooth arguments should have been handled."
                )
                spline = USpline(eigs, steps, k=k, s=spline_smooth)
            func = spline if return_callable else None
            unfolded = np.array(spline(eigs))
            if detrend:
                unfolded = emd_detrend(unfolded)
            return unfolded, steps, func  # type: ignore

        if smoother is SmoothMethod.Exponential:
            func = lambda t, a, b: a * np.exp(-b * t)  # type: ignore
            [a, b], cov = curve_fit(
                func,
                eigs,
                steps,
                p0=(len(eigs) / 2, 0.5),
            )
            unfolded = func(eigs, a, b)  # type: ignore
            if detrend:
                unfolded = emd_detrend(unfolded)
            return unfolded, steps, func  # type: ignore

        if smoother is SmoothMethod.Gompertz:
            # use steps[end] as guess for the asymptote, a, of gompertz curve
            [a, b, c], cov = curve_fit(gompertz, eigs, steps, p0=(steps[-1], 1, 1))
            func = lambda x: gompertz(x, a, b, c) if return_callable else None
            unfolded = gompertz(eigs, a, b, c)
            if detrend:
                unfolded = emd_detrend(unfolded)
            return unfolded, steps, func  # type: ignore
        raise RuntimeError("Unreachable!")

    def fit_all(
        self,
        poly_degrees: List[int] = [],
        spline_smooths: SmoothArg = [],
        spline_degrees: List[int] = DEFAULT_SPLINE_DEGREES,
        gompertz: bool = False,
        detrend: bool = False,
    ) -> Tuple[DataFrame, DataFrame, DataFrame, Dict[str, Callable]]:
        """unfold eigenvalues for all specified smoothers

        Parameters
        ----------
        poly_degrees: List[int]
            the polynomial degrees for which to compute fits.
            Default [3, 4, 5, 6, 7, 8, 9, 10, 11]

        spline_smooths: List[float] | "heuristic"
            If a list of floats, the smoothing factors, s, passed into
            scipy.interpolate.UnivariateSpline.
            If "heuristic", choose a set of smoothing factors scaled to the length of the
            eigenvalues, that, on GOE eigenvalues, tend to result in a range of fits
            varying from highly flexible (nearly interpolated) to about the flexibility of
            a cubic or quartic. As the number of eigenvalues starts to go below about 300,
            an increasing number of practically-identical, redundant splines will be fit
            with this option, and manual inspection or non-heuristic specification of
            spline smoothing factors is strongly recommended.

        spline_degrees: List[int]
            A list of ints determining the degrees of scipy.interpolate.UnivariateSpline
            fits. Default [3]

        Returns
        -------
        unfoldeds: DataFrame
            DataFrame of unfolded eigenvalues for each set of fit parameters, e.g. where
            each column contains a name indicating the fitting parameters, with the values
            of that column being the (sorted) unfolded eigenvalues.

        sqes: DataFrame
            DataFrame of mean-squared error of fits, where each column contains a name
            indicating the fitting parameters and smoother, with the values of
            the column being the mean of the squared residuals of the fit

        smoother_map: dict
            A dict of {col_name: closure} for accessing the fitted smoothers later.
        """
        # construct dataframes to hold all info
        col_names, unfoldeds, spacings, sqes = [], [], [], []
        smoother_map = {}
        for d in poly_degrees:
            col_name = f"poly_{d}"
            unfolded, steps, closure = self.fit(
                smoother=SmoothMethod.Polynomial, degree=d, return_callable=True, detrend=detrend
            )
            col_names.append(col_name)
            sqes.append(np.mean((unfolded - steps) ** 2))
            unfolded = np.sort(unfolded)  # Important!
            unfoldeds.append(unfolded)
            spacings.append(np.diff(unfolded))
            smoother_map[col_name] = closure

        if spline_smooths == "heuristic":
            for s in DEFAULT_SPLINE_SMOOTHS:
                for d in spline_degrees:
                    col_name = f"{_spline_name(d)}-spline_" "{:1.2f}_heuristic".format(s)
                    unfolded, steps, closure = self.fit(
                        smoother=SmoothMethod.Spline,
                        spline_smooth=len(self._eigs) ** s,
                        degree=d,
                        return_callable=True,
                        detrend=detrend,
                    )
                    col_names.append(col_name)
                    sqes.append(np.mean((unfolded - steps) ** 2))
                    unfolded = np.sort(unfolded)
                    unfoldeds.append(unfolded)
                    spacings.append(np.diff(unfolded))
                    smoother_map[col_name] = closure
        else:
            for s in spline_smooths:  # type: ignore
                for d in spline_degrees:
                    col_name = f"{_spline_name(d)}-spline_" "{:1.3f}".format(s)
                    unfolded, steps, closure = self.fit(
                        smoother=SmoothMethod.Spline,
                        spline_smooth=s,
                        degree=d,
                        return_callable=True,
                        detrend=detrend,
                    )
                    col_names.append(col_name)
                    sqes.append(np.mean((unfolded - steps) ** 2))
                    unfolded = np.sort(unfolded)
                    unfoldeds.append(unfolded)
                    spacings.append(np.diff(unfolded))
                    smoother_map[col_name] = closure
        if gompertz:
            unfolded, steps, closure = self.fit(
                smoother=SmoothMethod.Gompertz, return_callable=True, detrend=detrend
            )
            col_names.append("gompertz")
            sqes.append(np.mean((unfolded - steps) ** 2))
            unfolded = np.sort(unfolded)
            unfoldeds.append(unfolded)
            spacings.append(np.diff(unfolded))
            smoother_map["gompertz"] = closure
        unfoldeds = pd.DataFrame(data=unfoldeds, index=col_names).T
        spacings = pd.DataFrame(data=spacings, index=col_names).T
        sqes = pd.DataFrame(data=sqes, index=col_names).T
        return unfoldeds, spacings, sqes, smoother_map  # type: ignore

    @staticmethod
    def _get_smoother_names(
        poly_degrees: List[int],
        spline_smooths: SmoothArg,
        spline_degrees: List[int] = [3],
        gompertz: bool = True,
    ) -> List[str]:
        """If arguments are arrays, generate names (unique identifiers) for each smoother
        + smoother parameters. Otherwise, just return the name for indexing into the report.
        """

        col_names = []
        if isinstance(poly_degrees, list):
            for d in poly_degrees:
                col_names.append(f"poly_{d}")
        else:
            raise ValueError("poly_degrees must be a list of int values")

        if spline_smooths == "heuristic":
            for s in DEFAULT_SPLINE_SMOOTHS:
                if not isinstance(spline_degrees, list):
                    raise ValueError("spline_degrees must be a list of integer values")
                for deg in spline_degrees:
                    col_name = f"{_spline_name(deg)}-spline_" "{:1.3f}_heuristic".format(s)
                    col_names.append(col_name)
        else:
            try:
                spline_smooths = list(spline_smooths)  # type: ignore
            except Exception as e:
                raise ValueError(f"Error converting `spline_smooths` to list: {e}")
            if isinstance(spline_smooths, list):
                for s in spline_smooths:
                    if not isinstance(spline_degrees, list):
                        raise ValueError("spline_degrees must be a list of integer values")
                    for deg in spline_degrees:
                        col_name = f"{_spline_name(deg)}-spline_" "{:1.3f}".format(s)
                        col_names.append(col_name)
            else:
                raise ValueError("spline_smooths must be a list of float values")

        if gompertz is True:
            col_names.append("gompertz")
        return col_names

    def __validate_args(self, **kwargs: Any) -> None:
        """throw an error if smoother args are in any way invalid"""
        smoother = SmoothMethod.validate(kwargs.get("smoother"))  # type: ignore
        degree = kwargs.get("degree")
        spline_smooth = kwargs.get("spline_smooth")
        emd = kwargs.get("detrend")  # TODO: implement
        method = kwargs.get("method")

        if smoother is SmoothMethod.Polynomial:
            if degree is None:
                warn(
                    "No degree set for polynomial unfolding."
                    f"Will default to polynomial of degree {DEFAULT_POLY_DEGREE}.",
                    category=UserWarning,
                )
            if not isinstance(degree, int):
                raise ValueError("Polynomial degree must be of type `int`")
            if degree < 3:
                raise ValueError("Unfolding polynomial must have minimum degree 3.")
        elif smoother is SmoothMethod.Spline:
            spline_degree = degree
            if degree is None:
                warn(
                    "No degree set for spline unfolding. Will default to "
                    f"spline of degree {DEFAULT_SPLINE_DEGREE}.",
                    category=UserWarning,
                )
            if not isinstance(spline_degree, int) or spline_degree > 5:
                raise ValueError("Degree of spline must be an int <= 5")
            if spline_smooth is not None and spline_smooth != "heuristic":
                spline_smooth = float(spline_smooth)
        elif smoother in [
            SmoothMethod.Gompertz,
            SmoothMethod.Exponential,
            SmoothMethod.GOE,
            SmoothMethod.Poisson,
        ]:
            pass  # just allow this for now
        elif callable(smoother):
            # NOTE: above is not a great check, but probably good enough for our purposes
            # https://stackoverflow.com/questions/624926/how-do-i-detect-whether-a-python-variable-is-a-function#comment437753_624939
            raise NotImplementedError("Custom fit functions not currently implemented.")
        else:
            raise ValueError("Unrecognized smoother argument.")

        if emd is not None and not isinstance(emd, bool):
            raise ValueError("`detrend` can be only a boolean or undefined (None).")

        if method is None or method == "auto" or method == "manual":
            pass
        else:
            raise ValueError("`method` must be one of 'auto', 'manual', or 'None'")


# COMMENTS

# This is a one line comment.
# It is always ingnored by python.

# print("Hello World") # Comments can be placed after an actual code.

# It can also be used to make some lines of code inactive.
# print("Jython")
# print("Python")

# For multiline comments you can insert "#" at the beginning of each line
# But you can use the multi-line String syntax to do that too.
"""
This is because when a string is not assigned to a variable, python ignores it
and this makes it an improvised comment. Kinda ðŸ˜‰.
"""

#There are three types of python comments.
# 1. Single line comments or end-of-line comments.
# 2. Multiline comments or block comments.
# 3. Docstring.

# The first two are treated above.

# DOCSTRINGS
"""Docstring is a way of providing help documentation on a
python function, method, class or module."""
# They are made availble via the (.__doc__ ) module.
# print(sum.__doc__)

# Practising docstrings

# A function to return the the reversed form of a list.

def reverse_list(x: list) -> list:
    """Returns a list in the reversed order."""
    reversed_list = []
    for n in x:
        reversed_list.insert(0, n)
    return reversed_list

print(reverse_list.__doc__)
print(reverse_list([1, 2, 3]))


"""Core helper utilities."""
import collections

import numpy as np

import pyvista
from pyvista.core import _vtk_core as _vtk

from . import transformations
from .fileio import from_meshio, is_meshio_mesh

def wrap(dataset):
    """Wrap any given VTK data object to its appropriate PyVista data object.

    Other formats that are supported include:

    * 2D :class:`numpy.ndarray` of XYZ vertices
    * 3D :class:`numpy.ndarray` representing a volume. Values will be scalars.
    * 3D :class:`trimesh.Trimesh` mesh.
    * 3D :class:`meshio.Mesh` mesh.

    .. versionchanged:: 0.38.0
        If the passed object is already a wrapped PyVista object, then
        this is no-op and will return that object directly. In previous
        versions of PyVista, this would perform a shallow copy.

    Parameters
    ----------
    dataset : :class:`numpy.ndarray` | :class:`trimesh.Trimesh` | vtk.DataSet
        Dataset to wrap.

    Returns
    -------
    pyvista.DataSet
        The PyVista wrapped dataset.

    Examples
    --------
    Wrap a numpy array representing a random point cloud.

    >>> import numpy as np
    >>> import pyvista as pv
    >>> points = np.random.random((10, 3))
    >>> cloud = pv.wrap(points)
    >>> cloud
    PolyData (...)
      N Cells:    10
      N Points:   10
      N Strips:   0
      X Bounds:   ...
      Y Bounds:   ...
      Z Bounds:   ...
      N Arrays:   0

    Wrap a VTK object.

    >>> import pyvista as pv
    >>> import vtk
    >>> points = vtk.vtkPoints()
    >>> p = [1.0, 2.0, 3.0]
    >>> vertices = vtk.vtkCellArray()
    >>> pid = points.InsertNextPoint(p)
    >>> _ = vertices.InsertNextCell(1)
    >>> _ = vertices.InsertCellPoint(pid)
    >>> point = vtk.vtkPolyData()
    >>> _ = point.SetPoints(points)
    >>> _ = point.SetVerts(vertices)
    >>> mesh = pv.wrap(point)
    >>> mesh
    PolyData (...)
      N Cells:    1
      N Points:   1
      N Strips:   0
      X Bounds:   1.000e+00, 1.000e+00
      Y Bounds:   2.000e+00, 2.000e+00
      Z Bounds:   3.000e+00, 3.000e+00
      N Arrays:   0

    Wrap a Trimesh object.

    >>> import trimesh
    >>> import pyvista as pv
    >>> points = [[0, 0, 0], [0, 0, 1], [0, 1, 0]]
    >>> faces = [[0, 1, 2]]
    >>> tmesh = trimesh.Trimesh(points, faces=faces, process=False)
    >>> mesh = pv.wrap(tmesh)
    >>> mesh  # doctest:+SKIP
    PolyData (0x7fc55ff27ad0)
      N Cells:  1
      N Points: 3
      X Bounds: 0.000e+00, 0.000e+00
      Y Bounds: 0.000e+00, 1.000e+00
      Z Bounds: 0.000e+00, 1.000e+00
      N Arrays: 0

    """
    # Return if None
    if dataset is None:
        return

    if isinstance(dataset, tuple(pyvista._wrappers.values())):
        # Return object if it is already wrapped
        return dataset

    # Check if dataset is a numpy array.  We do this first since
    # pyvista_ndarray contains a VTK type that we don't want to
    # directly wrap.
    if isinstance(dataset, (np.ndarray, pyvista.pyvista_ndarray)):
        if dataset.ndim == 1 and dataset.shape[0] == 3:
            return pyvista.PolyData(dataset)
        if dataset.ndim > 1 and dataset.ndim < 3 and dataset.shape[1] == 3:
            return pyvista.PolyData(dataset)
        elif dataset.ndim == 3:
            mesh = pyvista.ImageData(dimensions=dataset.shape)
            if isinstance(dataset, pyvista.pyvista_ndarray):
                # this gets rid of pesky VTK reference since we're raveling this
                dataset = np.array(dataset, copy=False)
            mesh['values'] = dataset.ravel(order='F')
            mesh.active_scalars_name = 'values'
            return mesh
        else:
            raise NotImplementedError('NumPy array could not be wrapped pyvista.')

    # wrap VTK arrays as pyvista_ndarray
    if isinstance(dataset, _vtk.vtkDataArray):
        return pyvista.pyvista_ndarray(dataset)

    # Check if a dataset is a VTK type
    if hasattr(dataset, 'GetClassName'):
        key = dataset.GetClassName()
        try:
            return pyvista._wrappers[key](dataset)
        except KeyError:
            raise TypeError(f'VTK data type ({key}) is not currently supported by pyvista.')
        return

    # wrap meshio
    if is_meshio_mesh(dataset):
        return from_meshio(dataset)

    # wrap trimesh
    if dataset.__class__.__name__ == 'Trimesh':
        # trimesh doesn't pad faces
        n_face = dataset.faces.shape[0]
        faces = np.empty((n_face, 4), dataset.faces.dtype)
        faces[:, 1:] = dataset.faces
        faces[:, 0] = 3
        polydata = pyvista.PolyData(np.asarray(dataset.vertices), faces)
        # If the Trimesh object has uv, pass them to the PolyData
        if hasattr(dataset.visual, 'uv'):
            polydata.active_texture_coordinates = np.asarray(dataset.visual.uv)
        return polydata

    # otherwise, flag tell the user we can't wrap this object
    raise NotImplementedError(f'Unable to wrap ({type(dataset)}) into a pyvista type.')

def is_pyvista_dataset(obj):
    """Return ``True`` if the object is a PyVista wrapped dataset.

    Parameters
    ----------
    obj : Any
        Any object to test.

    Returns
    -------
    bool
        ``True`` when the object is a :class:`pyvista.DataSet`.

    """
    return isinstance(obj, (pyvista.DataSet, pyvista.MultiBlock))

def generate_plane(normal, origin):
    """Return a _vtk.vtkPlane.

    Parameters
    ----------
    normal : sequence[float]
        Three item sequence representing the normal of the plane.

    origin : sequence[float]
        Three item sequence representing the origin of the plane.

    Returns
    -------
    vtk.vtkPlane
        VTK plane.

    """
    plane = _vtk.vtkPlane()
    # NORMAL MUST HAVE MAGNITUDE OF 1
    normal = normal / np.linalg.norm(normal)
    plane.SetNormal(normal)
    plane.SetOrigin(origin)
    return plane

def axis_rotation(points, angle, inplace=False, deg=True, axis='z'):
    """Rotate points by angle about an axis.

    Parameters
    ----------
    points : numpy.ndarray
        Array of points with shape ``(N, 3)``.

    angle : float
        Rotation angle.

    inplace : bool, default: False
        Updates points in-place while returning nothing.

    deg : bool, default: True
        If ``True``, the angle is interpreted as degrees instead of
        radians.

    axis : str, default: "z"
        Name of axis to rotate about. Valid options are ``'x'``, ``'y'``,
        and ``'z'``.

    Returns
    -------
    numpy.ndarray
        Rotated points.

    Examples
    --------
    Rotate a set of points by 90 degrees about the x-axis in-place.

    >>> import numpy as np
    >>> import pyvista as pv
    >>> from pyvista import examples
    >>> points = examples.load_airplane().points
    >>> points_orig = points.copy()
    >>> pv.axis_rotation(points, 90, axis='x', deg=True, inplace=True)
    >>> assert np.all(np.isclose(points[:, 0], points_orig[:, 0]))
    >>> assert np.all(np.isclose(points[:, 1], -points_orig[:, 2]))
    >>> assert np.all(np.isclose(points[:, 2], points_orig[:, 1]))
    """
    axis = axis.lower()
    axis_to_vec = {'x': (1, 0, 0), 'y': (0, 1, 0), 'z': (0, 0, 1)}

    if axis not in axis_to_vec:
        raise ValueError('Invalid axis. Must be either "x", "y", or "z"')

    rot_mat = transformations.axis_angle_rotation(axis_to_vec[axis], angle, deg=deg)
    return transformations.apply_transformation_to_points(rot_mat, points, inplace=inplace)

def is_inside_bounds(point, bounds):
    """Check if a point is inside a set of bounds.

    This is implemented through recursion so that this is N-dimensional.

    Parameters
    ----------
    point : sequence[float]
        Three item cartesian point (i.e. ``[x, y, z]``).

    bounds : sequence[float]
        Six item bounds in the form of ``(xMin, xMax, yMin, yMax, zMin, zMax)``.

    Returns
    -------
    bool
        ``True`` when ``point`` is inside ``bounds``.

    """
    if isinstance(point, (int, float)):
        point = [point]
    if isinstance(point, (np.ndarray, collections.abc.Sequence)) and not isinstance(
        point, collections.deque
    ):
        if len(bounds) < 2 * len(point) or len(bounds) % 2 != 0:
            raise ValueError('Bounds mismatch point dimensionality')
        point = collections.deque(point)
        bounds = collections.deque(bounds)
        return is_inside_bounds(point, bounds)
    if not isinstance(point, collections.deque):
        raise TypeError(f'Unknown input data type ({type(point)}).')
    if len(point) < 1:
        return True
    p = point.popleft()
    lower, upper = bounds.popleft(), bounds.popleft()
    if lower <= p <= upper:
        return is_inside_bounds(point, bounds)
    return False


import math

from eth_hash.auto import keccak as keccak_256

class MerkleTree:
    """
    Class for build perfect binary trees, generating merkle roots and proof.
    """
    def __init__(self):
        self.hash_func = keccak_256
        self.nodes = []
        self.tree_levels = None
        self.is_ready = False

    @staticmethod
    def _to_hex(x):
        return x.hex()

    @staticmethod
    def _from_hex(x):
        return bytes.fromhex(x)

    def add_node(self, value):
        """
        Adds node to a tree. `value` has to be hex string
        """
        self.is_ready = False
        if isinstance(value, str):
            value = self._from_hex(value)
        self.nodes.append(value)

    def get_node(self, index):
        """
        Returns node hash as hex string.
        """
        return self._to_hex(self.nodes[index])

    @property
    def node_count(self):
        """
        Complete number of nodes in a tree.
        """
        return len(self.nodes)

    def _calc_next_level(self):
        if len(self.tree_levels[0]) % 2 == 1:
            raise ValueError('Level length cannot be odd')

        next_level = []
        for left_node, right_node in zip(self.tree_levels[0][0::2], self.tree_levels[0][1::2]):
            next_level.append(self.hash_func(left_node + right_node))
        self.tree_levels = [next_level, ] + self.tree_levels

    @staticmethod
    def _next_power_of_2(x):
        return 1 if x == 0 else 2 ** math.ceil(math.log2(x))

    def _fill_to_power_of_2(self):
        """
        Fills tree with extra nodes (with hashes of empty strings) to form a perfect binary tree.
        """
        next_power_of_2 = self._next_power_of_2(self.node_count)
        nodes_to_append = next_power_of_2 - self.node_count
        empty_str_hash = self.hash_func(self._from_hex(''))
        for i in range(nodes_to_append):
            self.add_node(empty_str_hash)

    def make(self):
        """
        Build a complete tree from given nodes.
        """
        self.is_ready = False
        self._fill_to_power_of_2()
        self.tree_levels = [self.nodes, ]
        while len(self.tree_levels[0]) > 1:
            self._calc_next_level()
        self.is_ready = True

    def get_root(self):
        """
        Returns merkle root for a complete tree.
        """
        if self.is_ready and self.tree_levels is not None:
            return self._to_hex(self.tree_levels[0][0])
        raise ValueError('The tree is not ready. Call `make` to build the tree.')

    def get_proof_for_hash(self, hash_value):
        """
        Returns proof for a given node hash value.
        """
        if isinstance(hash_value, str):
            hash_value = self._from_hex(hash_value)
        return self.get_proof(self.nodes.index(hash_value))

    def get_proof(self, index):
        """
        Returns proof for a given tree index.

        Proof example
        [
            {'right': 'c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470'},
            {'left': '49d03a195e239b52779866b33024210fc7dc66e9c2998975c0aa45c1702549d5'}
        ]
        """
        if self.tree_levels is None or not self.is_ready:
            raise ValueError('The tree is not ready. Call `make` to build the tree.')

        if index < 0 or index > self.node_count - 1:
            raise ValueError('Specify the correct index (from 0 to {}).'.format(self.node_count - 1))

        proof = []
        for level in range(len(self.tree_levels) - 1, 0, -1):
            is_right_node = index % 2
            sibling_index = index - 1 if is_right_node else index + 1
            index = int(index / 2)

            sibling_position = 'left' if is_right_node else 'right'
            sibling_value = self._to_hex(self.tree_levels[level][sibling_index])
            proof.append({sibling_position: sibling_value})

        return proof

    def validate_proof(self, proof, target_hash, merkle_root):
        """
        Validates that provided `target_hash` and `proof` form a merkle root equal to provided one.
        """

        if len(proof) == 0:
            raise ValueError('Proof is empty.')

        proof_hash = self._from_hex(target_hash)
        root_hash = self._from_hex(merkle_root)
        for p in proof:
            if 'left' in p:
                proof_value = self._from_hex(p['left']) + proof_hash
            elif 'right' in p:
                proof_value = proof_hash + self._from_hex(p['right'])
            else:
                raise ValueError('Inconsistent proof.')
            proof_hash = self.hash_func(proof_value)
        return proof_hash == root_hash


import sublime

import sys
import threading

import hashlib

# Helper module
try:
    from .helper import H
except:
    from helper import H

# Settings variables
try:
    from . import settings as S
except:
    import settings as S

# DBGp protocol constants
try:
    from . import dbgp
except:
    import dbgp

try:
    from . import grld
except:
    import grld

# Config module
from .config import get_value

# Log module
from .log import debug, info

# Protocol module
from .protocol import ProtocolConnectionException

# Util module
from .util import get_real_path

# View module
from .view import DATA_CONTEXT, DATA_STACK, DATA_WATCH, DATA_COROUTINES, DATA_EVALUATE, TITLE_WINDOW_WATCH, generate_context_output, generate_stack_output, generate_coroutines_output, has_debug_view, render_regions, show_content, show_file, show_panel_content

ACTION_BREAK = "action_break"
ACTION_EVALUATE = "action_evaluate"
ACTION_EXECUTE = "action_execute"
ACTION_INIT = "action_init"
ACTION_REMOVE_BREAKPOINT = "action_remove_breakpoint"
ACTION_SET_BREAKPOINT = "action_set_breakpoint"
ACTION_STATUS = "action_status"
ACTION_USER_EXECUTE = "action_user_execute"
ACTION_WATCH = "action_watch"
ACTION_SET_CURRENT_STACK_LEVEL = "action_set_current_stack_level"
ACTION_SET_SELECTED_THREAD = "action_set_selected_thread"

def is_connected(show_status=False):
    """
    Check if client is connected to debugger engine.

    Keyword arguments:
    show_status -- Show message why client is not connected in status bar.
    """
    if S.PROTOCOL and S.PROTOCOL.connected:
        return True
    elif S.PROTOCOL and show_status:
        sublime.status_message('GRLD: Waiting for response from debugger engine.')
    elif show_status:
        sublime.status_message('GRLD: No GRLD session running.')
    return False

def is_execution_broken():
    return S.BREAKPOINT_ROW or S.BREAKPOINT_EXCEPTION

def connection_error(exception):
    """
    Template for showing error message on connection error/loss.

    Keyword arguments:
    message -- Exception/reason of connection error/loss.
    """
    info("Connection lost with debugger engine.")
    debug(message)
    # Reset connection

    with S.PROTOCOL as protocol:
        protocol.disconnect(exception)
        #protocol.clear()

    # Render breakpoint markers
    #render_regions()

def update_socket_loop():
    if not S.PROTOCOL:
        return

    try:
        with S.PROTOCOL as protocol:
            protocol.update()
    except ProtocolConnectionException as e:
        protocol.disconnect(e)
        return

    sublime.set_timeout(lambda: sublime.status_message('GRLD: Connected'), 0)
    sublime.set_timeout_async(update_socket_loop, 100)

class SocketHandler(threading.Thread):
    def __init__(self, action, **options):
        threading.Thread.__init__(self)
        self.action = action
        self.options = options
        self.current_thread = 'current' # thread currently running in Lua (since the last time execution stopped)
        self.selected_thread = 'current' # selected thread from UI
        self.current_stack_level = 1

    def get_option(self, option, default_value=None):
        if option in self.options.keys():
            return self.options[option]
        return default_value

    def run_command(self, command, args=None):
        if not isinstance(args, dict):
            args = {}
        self.timeout(lambda: self._run_command(command, args))

    def _run_command(self, command, args=None):
        try:
            sublime.active_window().run_command(command, args)
        except:
            # In case active_window() is not available
            pass

    def run_view_command(self, command, args=None):
        if not isinstance(args, dict):
            args = {}
        self.timeout(lambda: self._run_view_command)

    def _run_view_command(self, command, args=None):
        try:
            sublime.active_window().active_view().run_command(command, args)
        except:
            # In case there is no active_view() available
            pass

    def status_message(self, message):
        sublime.set_timeout(lambda: sublime.status_message(message), 100)

    def timeout(self, function):
        sublime.set_timeout(function, 0)

    def run(self):
        # Make sure an action is defined
        if not self.action:
            return
        try:
            S.SESSION_BUSY = True
            # Evaluate
            if self.action == ACTION_BREAK:
                self.execute_break()
            elif self.action == ACTION_EVALUATE:
                self.evaluate(self.get_option('expression'), self.get_option('view'))
            # Execute
            elif self.action == ACTION_EXECUTE:
                self.execute(self.get_option('command'))
            # Init
            elif self.action == ACTION_INIT:
                self.init()
            # Remove breakpoint
            elif self.action == ACTION_REMOVE_BREAKPOINT:
                self.remove_breakpoint(self.get_option('filename'), self.get_option('lineno'))
            # Set breakpoint
            elif self.action == ACTION_SET_BREAKPOINT:
                # TODO: support conditional bps
                active = True # self.get_option('expression')
                self.set_breakpoint(self.get_option('filename'), self.get_option('lineno'), active)
            # Status
            elif self.action == ACTION_STATUS:
                self.status()
            # User defined execute
            elif self.action == ACTION_USER_EXECUTE:
                self.user_execute(self.get_option('command'), self.get_option('args'))
            # Watch expression
            elif self.action == ACTION_WATCH:
                self.watch_expression()
            elif self.action == ACTION_SET_CURRENT_STACK_LEVEL:
                self.set_current_stack_level(self.get_option('stack_level'))
                pass
            elif self.action == ACTION_SET_SELECTED_THREAD :
                self.set_current_stack_level(self.get_option('selected_thread'))
                pass
        # Show dialog on connection error
        except ProtocolConnectionException as e:
            self.timeout(lambda: connection_error(e))
        finally:
            S.SESSION_BUSY = False

    def execute_break(self):
        with S.PROTOCOL as protocol:
            protocol.send('break', 'running')

    def evaluate(self, expression, view):
        if not expression or not is_connected():
            return
        # Send 'eval' command to debugger engine with code to evaluate
        response = self.evaluate_expression(expression, self.current_thread, self.current_stack_level)

        transformed_response = self.transform_grld_eval_response(response)
        response_str = generate_context_output(transformed_response, values_only=True, multiline=False)

        self.timeout(lambda: view.run_command('grld_update_evaluate_line_response', {'response': response_str}))

    def execute(self, command):
        # Do not execute if no command is set
        if not command or not is_connected():
            return

        # Reset previous breakpoint values
        S.BREAKPOINT_EXCEPTION = None
        S.BREAKPOINT_ROW = None
        S.CONTEXT_DATA.clear()
        self.watch_expression()

        # Send command to debugger engine
        with S.PROTOCOL as protocol:
            protocol.send(command)

        if command == grld.RUN:
            self.run_command('grld_layout')

        #self.timeout(lambda: render_regions())

    def get_value(self, grld_id):
        with S.PROTOCOL as protocol:
            protocol.send('getValue')
            protocol.send(grld_id)
            response = protocol.read()

        return response

    def is_table(self, variable):
        if type(variable) != dict: return False
        if 'value' not in variable: return False
        value = variable['value']
        if type(value) != dict: return False
        if 'type' not in value: return False

        return value['type'] == 'table'

    def transform_grld_table(self, table_variable, parent_table_refs):
        name = table_variable['name']
        table_values = table_variable['value']
        table_ref = table_values['short']
        id = table_values['id']
        type = table_values['type']
        assert type == 'table', "table_variable passed to transform_grld_table must of type 'table'"

        parent_table_refs.append(table_ref)

        table_children = self.get_value(id)
        transformed_children = {}
        for i, child in table_children.items():

            if self.is_table(child):
                child_table_ref = child['value']['short']
                if child_table_ref in parent_table_refs: # special value if table child is a reference to the table itself (avoid infinite recursion)
                    idx = parent_table_refs.index(child_table_ref)
                    num_tables_up_from_child = len(parent_table_refs) - idx - 1
                    if num_tables_up_from_child == 0:
                        description = '<circular reference to this table>'
                    else:
                        description = '<circular reference to a parent table {} levels up>'.format(num_tables_up_from_child)

                    transformed_children[i] =  {'name': child['name'], 'type':'table-ref', 'value': description}
                else:
                    transformed_children[i] = self.transform_grld_variable(child, parent_table_refs)
            else:
                transformed_children[i] = self.transform_grld_variable(child, parent_table_refs)

        return {'name': name, 'type': type, 'value': table_ref, 'numchildren': len(transformed_children.keys()), 'children': transformed_children}

    def transform_grld_variable(self, variable, parent_table_refs=None):
        name = variable['name']

        # if nothing is returned, GRLD returns {name = '<no result'>}
        if not 'value' in variable:
            return {'name': '', 'value': name, 'type': ''}

        if self.is_table(variable):
            return self.transform_grld_table(variable, parent_table_refs or []) #handle tables separately

        value = variable['value']
        if type(value) == dict:
            value_type = value['type']
            value = value['short']
        else:
            if type(value) == bool:
                value_type = 'boolean'
            elif type(value) == int or type(value) == float:
                value_type = 'number'
            elif type(value) == str:
                value_type = 'string'
            else:
                value_type = '?'

        return {'name': name, 'value': str(value), 'type': value_type}

    def transform_grld_eval_response(self, eval_response, scope=None):
        transformed = {}
        for i, var in eval_response.items():
            transformed_item = self.transform_grld_variable(var)

            if scope:
                name = "(%s) %s" % (scope, transformed_item['name'])
                transformed_item['name'] = name
            else:
                name = transformed_item['name']

            transformed[i] = transformed_item

        return transformed

    def transform_grld_context_response(self, context_response, scope):
        return self.transform_grld_eval_response(context_response, scope)

    def get_context_values(self, thread, stack_level):
        """
        Get variables in current context.
        """
        if not is_connected():
            return

        context = H.new_dictionary()
        try:
            # local variables
            #if get_value(S.KEY_SUPER_GLOBALS):
            with S.PROTOCOL as protocol:
                protocol.send("locals")
                protocol.send(thread)
                protocol.send(stack_level)
                response = protocol.read()
                properties = self.transform_grld_context_response(response, "local")
                context.update(properties)

                # upvalues
                protocol.send("upvalues")
                protocol.send(thread)
                protocol.send(stack_level)
                response = protocol.read()
                properties = self.transform_grld_context_response(response, "upvalue")
                context.update(properties)
        except ProtocolConnectionException as e:
            self.timeout(lambda: connection_error(e))

        # Store context variables in session
        S.CONTEXT_DATA = context

        return generate_context_output(context)

    def get_stack_values(self):
        """
        Get stack information for current context.
        """
        response = None
        if is_connected():
            try:
                with S.PROTOCOL as protocol:
                    # Get stack information
                    protocol.send("callstack")
                    protocol.send(self.current_thread)
                    response = protocol.read()
            except ProtocolConnectionException as e:
                self.timeout(lambda: connection_error(e))

        #response should be something like: {"1": {"name": <name of thing>, "namewhat": (global|local|method|field|''), "what": (Lua, C, main), "source": @<filename>, "line": line in file}}
        return response

    def evaluate_expression(self, expression, thread, stack_level):
        try:
            with S.PROTOCOL as protocol:
                protocol.send('evaluate')
                protocol.send('=' + expression) #TODO: should we always add '='?
                protocol.send(thread)
                protocol.send(stack_level)

                response = protocol.read()
        except ProtocolConnectionException as e:
            self.timeout(lambda: connection_error(e))

        return response

    def get_watch_values(self, thread, stack_level):
        """
        Evaluate all watch expressions in current context.
        """

        for index, item in enumerate(S.WATCH):
            # Reset value for watch expression
            S.WATCH[index]['value'] = None

            # Evaluate watch expression when connected to debugger engine
            if is_connected():
                if item['enabled']:
                    watch_value = self.evaluate_expression(item['expression'], thread, stack_level)

                    S.WATCH[index]['value'] = self.transform_grld_eval_response(watch_value)

    def init(self):
        if not is_connected():
            return

        with S.PROTOCOL as protocol:
            protocol.register_command_cb('break', (lambda filename, line: self.handle_break_command(filename, line)))
            protocol.register_command_cb('synchronize', (lambda: self.handle_synchronize_command()))
            # Connection initialization
            client_name = protocol.read()

            # # Set breakpoints for files
            for filename, breakpoint_data in S.BREAKPOINT.items():
                if breakpoint_data:
                    for lineno, bp in breakpoint_data.items():
                        if bp['enabled']:
                            # TODO: support conditional bps
                            active = True #bp['expression']
                            self.set_breakpoint(filename, lineno, active)
                            debug('breakpoint_set: ' + filename + ':' + lineno)

            if get_value(S.KEY_BREAK_ON_START):
                protocol.send("break", "running")

        update_socket_loop()

        #self.timeout(lambda: render_regions())

    def remove_breakpoint(self, filename, lineno):
        if not is_connected():
            return

        self.set_breakpoint(filename, lineno, False)

    def set_breakpoint(self, filename, lineno, active=True):
        if not filename or not lineno or not is_connected():
            return

        with S.PROTOCOL as protocol:
            # Get path of file on server
            fileuri = get_real_path(filename, True)
            # Set breakpoint
            protocol.send("setbreakpoint", "running")
            protocol.send({"source": fileuri, "line": int(lineno), "value": active}, "running")

        #self.timeout(lambda: render_regions())

    def set_exception(self, exception):
        if not is_connected():
            return

        with S.PROTOCOL as protocol:
            protocol.send(dbgp.BREAKPOINT_SET, t='exception', x='"%s"' % exception)
            response = protocol.read()

    def status(self):
        raise NotImplementedError('status is not implemented yet')
        #if not is_connected():
        #    return

        #with S.PROTOCOL as protocol:
        #    # Send 'status' command to debugger engine
        #    protocol.send(dbgp.STATUS)
        #    response = protocol.read()

        ## Show response in status bar
        #self.status_message("GRLD status: " + response.get(dbgp.ATTRIBUTE_REASON) + ' - ' + response.get(dbgp.ATTRIBUTE_STATUS))

    def user_execute(self, command, args=None):
        if not command or not is_connected():
            return

        with S.PROTOCOL as protocol:
            # Send command to debugger engine
            protocol.send(command, args)
            response = protocol.read(return_string=True)

        # Show response data in output panel
        self.timeout(lambda: show_panel_content(response))

    def watch_expression(self, thread=None, stack_level=None):
        if not thread:
            thread = self.current_thread

        if not stack_level:
            stack_level = self.current_stack_level

        # Evaluate watch expressions
        self.get_watch_values(thread, stack_level)
        # Show watch expression
        self.timeout(lambda: self._watch_expression(self.get_option('check_watch_view', False)))

    def _watch_expression(self, check_watch_view):
        # Do not show if we only want to show content when Watch view is not available
        if check_watch_view and not has_debug_view(TITLE_WINDOW_WATCH):
            return

        show_content(DATA_WATCH)

        #self.timeout(lambda: render_regions())

    def set_current_stack_level(self, stack_level):
        self.current_stack_level = stack_level

    def set_selected_thread(self, selected_thread):
        self.selected_thread = selected_thread

    def get_all_coroutines(self):
        with S.PROTOCOL as protocol:
            protocol.send('coroutines')
            response = protocol.read()

        count = len(response.keys())
        response[count + 1] = {'id': 'main'} # always include a 'main' coroutine. This is how GRLD references the main Lua thread.

        return response

    def update_current_thread(self, coroutines_dict):
        with S.PROTOCOL as protocol:
            protocol.send('currentthread')
            current_thread = protocol.read()

        # GRLD only passes back co-routines that are NOT 'main'. So, if current_thread is not in the list, then 'main' is the current thread.
        coroutine_ids = (coroutine_descriptor['id'] for coroutine_descriptor in coroutines_dict.values())
        if current_thread not in coroutine_ids:
            current_thread = 'main'

        self.current_thread = current_thread

    def update_contextual_data(self, thread, stack_level):
        # Context variables
        context = self.get_context_values(thread, stack_level)
        self.timeout(lambda: show_content(DATA_CONTEXT, context))

        # Watch expressions
        self.watch_expression(thread, stack_level)

        # Render breakpoint markers
        #self.timeout(lambda: render_regions())

    def handle_break_command(self, filename, line):
        S.SESSION_BUSY = True
        filename = get_real_path(filename)

        # Show debug/status output
        self.status_message('GRLD: Break')
        info('Break: ' + filename )
        # Store line number of breakpoint for displaying region marker
        S.BREAKPOINT_ROW = { 'filename': filename, 'lineno': str(line) }

        # Focus/Open file window view
        self.timeout(lambda: show_file(filename, line))

        coroutines_dict = self.get_all_coroutines()

        self.update_current_thread(coroutines_dict)

        coroutines_str = generate_coroutines_output(coroutines_dict, self.current_thread)
        self.timeout(lambda: show_content(DATA_COROUTINES, coroutines_str))

        # Stack history
        stack = self.get_stack_values()
        if stack:
            stack_levels = [int(level) for level in stack.keys()]
            min_stack_level =  min(stack_levels)
            self.current_stack_level = min_stack_level
            stack_str = generate_stack_output(stack)
        else:
            stack_str = H.unicode_string('[{level}] {filename}.{where}:{lineno}\n' \
                                        .format(level=0, where='{main}', lineno=1, filename=fileuri))

        self.timeout(lambda: show_content(DATA_STACK, stack_str))

        self.update_contextual_data(self.current_thread, self.current_stack_level)

        #self.timeout(lambda: render_regions())

        S.SESSION_BUSY = False

    def handle_synchronize_command(self):
        S.SESSION_BUSY = True

        with S.PROTOCOL as protocol:
            # synchronize expects us to return the # of active breakpoints (this is actually not implemented, we MUST return 0 here)
            protocol.send(0)

            # next we need to send the "breakOnConnection" value, this is configurable, but we'll just always return false for now
            protocol.send(False)

        S.SESSION_BUSY = False

class SessionException(Exception):
    pass


import numpy as np
from scipy.io import loadmat

def create_train_matrix(train_edge_data, num_nodes):
	x = np.zeros((num_nodes, num_nodes)).astype(np.float32)
	for i in range(len(train_edge_data)):
		x[train_edge_data[i][0], train_edge_data[i][1]] = 1.0
	return x

def load_neighbors(name, num_nodes, train_edge_data):
	# x = loadmat('../data/mat_versions/{}.mat'.format(name))
	# b = x['net']
	# b = b.toarray()
	b = create_train_matrix(train_edge_data, num_nodes)

	links = np.transpose(np.nonzero(b))
	# print(np.sum(b, axis=1))
	return links, np.sum(b, axis=1)
	# print(links.shape)
	neighbors = {}
	j = 0
	for i in range(num_nodes):
		if i in neighbors.keys():
			k = j
			if k>=links.shape[0]:
				break
			while(links[k, 0] == i):
				neighbors[i].append(links[k, 1])
				k += 1
				if(k >= links.shape[0]):
					break
			j = k

		else:
			neighbors[i] = []
			k = j
			if k>=links.shape[0]:
				break
			while(links[k, 0] == i):
				neighbors[i].append(links[k, 1])
				k += 1
				if(k >= links.shape[0]):
					break
			j = k
		if neighbors[i]==[]:
			neighbors[i]=[i]
	return neighbors

# neighbors = load_neighbors("Celegans", 297)
# print(neighbors)


import random
import numpy as np

def replace_yo(x):
    if "ё" not in x:
        return None
    return x.replace("ё", "е")

def rm_exclamation(x):
    if "!" not in x:
        return None
    return x.replace("!", "")

def add_exclamation(x):
    if "!" in x:
        return None
    if x[-1] == ".":
        return x[:-1] + "!"
    return x + "!"

def rm_question(x):
    if "?" not in x:
        return None
    return x.replace("?", "")

def fix_case(x):
    has_lower = any(ch.islower() for ch in x)
    if has_lower:
        return None
    return x.lower()

def concat_with_toxic(x, toxic_texts):
    toxic_comment = random.choice(toxic_texts)
    return " ".join((toxic_comment, x) if random.random() < 0.5 else (x, toxic_comment))

def concat_non_toxic(x, non_toxic_texts):
    non_toxic_comment = random.choice(non_toxic_texts)
    return " ".join((non_toxic_comment, x) if random.random() < 0.5 else (x, non_toxic_comment))

def add_toxic_words(x, toxic_words, num_words=3):
    words = x.split()
    for _ in range(num_words):
        toxic_word = random.choice(toxic_words)
        words.insert(random.randint(0, len(words)), toxic_word)
    return " ".join(words)

def toxic_words_transform(x, toxic_words, word_transform):
    words = x.split()
    toxic_words = set(toxic_words)
    bad_words_indices = []
    for i, word in enumerate(words):
        if word.lower() in toxic_words:
            bad_words_indices.append(i)
    if not bad_words_indices:
        return None
    for index in bad_words_indices:
        bad_word = words[index]
        transformed_word = word_transform(bad_word)
        if not transformed_word:
            continue
        words[index] = transformed_word
    return " ".join(words)

def mask_char(word):
    replace_chars = ("*", "@", "$", "#")
    char_position = random.randint(1, len(word)-1)
    replace_char = random.choice(replace_chars)
    return word[:char_position] + replace_char + word[char_position+1:]

def add_typos(text, typos=1):
    if len(text) <= 1:
        return None
    text = list(text)
    swaps = np.random.choice(len(text) - 1, typos)
    for swap in swaps:
        text[swap], text[swap + 1] = text[swap + 1], text[swap]
    return ''.join(text)

def form_transformations(toxic_texts=None, non_toxic_texts=None, toxic_words=None):
    transformations = [replace_yo, rm_exclamation, add_exclamation, rm_question, fix_case, add_typos]
    transformations = {func.__name__: func for func in transformations}
    if toxic_texts:
        transformations["concat_with_toxic"] = lambda x: concat_with_toxic(x, toxic_texts)
    if non_toxic_texts:
        transformations["concat_non_toxic"] = lambda x: concat_non_toxic(x, non_toxic_texts)
    if toxic_words:
        transformations["add_toxic_words"] = lambda x: add_toxic_words(x, toxic_words)
        transformations["toxic_words_mask_char"] = lambda x: toxic_words_transform(
            x, toxic_words, mask_char
        )
        transformations["toxic_words_add_typos"] = lambda x: toxic_words_transform(
            x, toxic_words, add_typos
        )
    return transformations


from os import path
import json
from .common import *

class VideoInfo:
    _file_name = 'videoinfo.json'

    def __init__(self, url=None, video_data=None):
        self.url = ''
        self.title = ''
        self.upload_time = ''
        self.description = ''
        self.duration = 0
        self.uploader = ''

        if url is not None:
            self.url = url

        if video_data is not None:
            self.title = video_data['title']
            self.upload_time = video_data['ctime']
            self.description = video_data['desc']
            self.duration = video_data['duration']
            self.uploader = video_data['owner']['name']

    def to_json(self):
        return json.dumps(self, default=obj_dict)

    @staticmethod
    def from_json(str):
        vi = VideoInfo()
        vi.__dict__ = json.loads(str)
        return vi

    def __str__(self):
        fmt = 'title: {0.title} uploader: {0.uploader} \ndescription: {0.description}'
        return fmt.format(self)

    def __repr__(self):
        return self.__str__()

class VideoSegmentInfo:
    _flv = 'flv'
    _mp4 = 'mp4'

    def __init__(self, durl, format: str):
        if format.find(self._flv) >= 0:
            self.format = self._flv
        else:
            self.format = self._mp4

        self.url = durl['url']
        self.length = durl['length']
        self.size = durl['size']
        self.order = durl['order']

    @property
    def file_name(self):
        return '%d.%s' % (self.order, self.format)

    def to_json(self):
        return json.dumps(self, default=obj_dict)

    @staticmethod
    def from_json(s):
        l = json.loads(s)
        return [VideoSegmentInfo(t, t['format']) for t in l]

    def __str__(self):
        fmt = 'format: {0.format} size: {1} length: {2[0]}m {2[1]}s'
        return fmt.format(self, size2str(self.size), divmod(self.length // 1000, 60))

    def __repr__(self):
        return self.__str__()


import sys

def main(file_arg):
    with open(file_arg) as f:
        for line in f:
            n = int(line.strip().split(' ')[0])
            sequence = [int(x) for x in line.strip().split(' ')[1:]]

            difference_sequence = []

            while (len(sequence) > 1):
                sequence[0] = abs(sequence[0] - sequence[1])
                difference_sequence.append(sequence[0])
                del(sequence[0])

            for x in range(1, n):
                if x not in difference_sequence:
                    print("Not jolly")
                    break
            else:
                print("Jolly")

if __name__ == '__main__':
    main(sys.argv[1])


class Setttings:
    """Uma classe para manipular as configurações do jogo"""
    def __init__(self):
        """Inicia as configurações do jogo"""
        # Configurações da tela
        self.screen_width = 1200
        self.screen_height = 900
        self.bg_color = (100, 230, 220)

        # Configurações da Nave
        self.ship_speed = 1.5
        self.ship_limit = 3

        # Configurações da munição
        self.bullet_speed = 2.0
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = (60, 60, 60)
        self.bullets_allowed = 3

        # Configurações das naves aliens
        self.alien_speed = 2.0
        self.fleet_drop_speed = 10

        # fleet_direction onde 1 representa ir para direita e -1 representa ir para esquerda
        self.fleet_direction = 1


import chess
import logging
from fastapi import FastAPI
from .api import api_router

logger = logging.getLogger(__name__)
logFormatter = logging.Formatter(
    "[%(asctime)s] [%(threadName)-12.12s] [%(name)s] [%(levelname)-5.5s]  %(message)s"
)
consoleHandler = logging.StreamHandler()
consoleHandler.setFormatter(logFormatter)
logger.addHandler(consoleHandler)
logger.setLevel(logging.DEBUG)
logging.getLogger().addHandler(consoleHandler)

app = FastAPI()

app.include_router(api_router)

@app.get("/")
async def board():
    return {"board": str(board)}