    max_axis = len(arr.shape) - 1
    min_axis = 0
    return draw(
        one_of((just(default_axis), integers(min_value=min_axis, max_value=max_axis))))

import warnings
import sys
import numpy as np
import riptable as rt
from typing import Any, List, Optional, Tuple, Union
import hypothesis.extra.pandas as pdst
from hypothesis import assume
from hypothesis.strategies import (
    booleans,
    characters,
    complex_numbers,
    composite,
    floats,
    integers,
    just,
    lists,
    none,
    one_of,
    sampled_from,
    shared,
    slices,
    text,
    SearchStrategy,
)
from hypothesis.extra.numpy import (
    arrays,
    basic_indices,
    boolean_dtypes,
    byte_string_dtypes,
    complex_number_dtypes,
    datetime64_dtypes,
    floating_dtypes,
    integer_dtypes,
    timedelta64_dtypes,
    unicode_string_dtypes,
    unsigned_integer_dtypes,
)
from riptable.Utils.common import _dtypes_by_group, integer_range, dtypes_by_group
from riptable.Utils.rt_testing import name

_MAX_SHAPE_SIZE: int = 270_000  # maximum shape size (this is an arbitrary value >250k, which should put it above various length cutoffs we use)
_MAX_VALUE: int = 1_000_000  # numeric maximum and minimum magnitude value
_MAX_INT: int = sys.maxsize
_MAX_FLOAT: float = sys.float_info.max

@composite
def ndarray_shape_strategy(
    draw,
    min_rank: int = 0,
    max_rank: int = 2,
    max_shape_size: Optional[int] = None,
    max_size_multiplier: Optional[float] = None
):
    rank = draw(integers(min_value=min_rank, max_value=max_rank))

    if max_shape_size is None:
        # The max shape size is meant to be a maximum number of elements, so for higher-rank
        # arrays we need to use a smaller size-per-axis. This is only an upper bound, so for higher-rank
        # arrays this approach will typically generate arrays whose total element count is somewhat smaller
        # than the max size.
        max_shape_size = _MAX_SHAPE_SIZE if rank <= 1 else int(np.float_power(_MAX_SHAPE_SIZE, 1 / rank))

        # If a max size multiplier was specified, apply it here.
        if max_size_multiplier is not None:
            # Validate the parameter value; this check is implemented to disallow NaNs.
            if max_size_multiplier > 0.:
                # Round upwards to avoid any case where we round down to zero.
                max_shape_size = int(np.ceil(max_shape_size * max_size_multiplier))
            else:
                raise ValueError("The 'max_size_multiplier' argument must be a strictly-positive value.")

    if rank == 0:
        x_dim = y_dim = 1
    elif rank == 1:
        col_vector = draw(booleans())
        min_shape_size = 2
        if col_vector:
            x_dim = 1
            y_dim = draw(integers(min_value=min_shape_size, max_value=max_shape_size))
        else:
            x_dim = draw(integers(min_value=min_shape_size, max_value=max_shape_size))
            y_dim = 1
            if draw(booleans()):
                return (x_dim,)
    elif rank == 2:
        min_shape_size = 3
        x_dim = draw(integers(min_value=min_shape_size, max_value=max_shape_size))
        y_dim = draw(integers(min_value=min_shape_size, max_value=max_shape_size))
    else:
        raise ValueError(f"Unsupported rank value. (rank = {rank})")

    return x_dim, y_dim

@composite
def one_darray_shape_strategy(draw, max_shape_size: Optional[int] = None):
    x_dim = draw(integers(min_value=1, max_value=max_shape_size or _MAX_SHAPE_SIZE))
    return (x_dim,)

@composite
def ints_floats_datetimes_and_timedeltas(draw):
    dtypes = (
        unsigned_integer_dtypes(endianness="="),
        integer_dtypes(endianness="="),
        floating_dtypes(endianness="="),
        datetime64_dtypes(endianness="="),
        timedelta64_dtypes(endianness="="),
    )
    return draw(one_of(dtypes))

@composite
def ints_or_floats_example(draw):
    dtypes = (
        integers(),
        floats(),
    )
    return draw(one_of(dtypes))

@composite
def ints_floats_complex_or_booleans(draw):
    dtypes = (
        unsigned_integer_dtypes(endianness="="),
        integer_dtypes(endianness="="),
        floating_dtypes(endianness="="),
        boolean_dtypes(),
    )
    return draw(one_of(dtypes))

@composite
def ints_floats_and_complex_numbers(draw):
    dtypes = (
        integers(min_value=-40000000, max_value=40000000),
        floats(min_value=-40000000, max_value=40000000),
    )
    return draw(one_of(dtypes))

@composite
def ints_floats_or_complex_dtypes(draw):
    # Endianness needs to be specified for now, otherwise the byte-order may get flipped
    dtypes = (
        unsigned_integer_dtypes(endianness="="),
        integer_dtypes(endianness="="),
        floating_dtypes(endianness="="),
    )
    return draw(one_of(dtypes))

@composite
def ints_or_floats_dtypes(draw):
    # Endianness needs to be specified for now, otherwise the byte-order may get flipped
    # Half floats are not supported.
    dtypes = (
        unsigned_integer_dtypes(endianness="="),
        integer_dtypes(endianness="="),
        floating_dtypes(endianness="=", sizes=(32, 64)),
    )
    return draw(one_of(dtypes))

@composite
def same_structure_ndarrays(draw):
    shape = draw(ndarray_shape_strategy())
    # The datatypes for both arrays should be the same. Since ints_floats_datetimes_and_timedeltas() returns a strategy
    # rather than a draw(strategy()) a different value could be returned from the one_of. Shared prevents this and
    # ensures the same datatype is used for both arrays on each run.
    dtype = shared(ints_floats_datetimes_and_timedeltas())
    array1 = draw(arrays(dtype=dtype, shape=shape))
    array2 = draw(arrays(dtype=dtype, shape=shape))
    assert array1.dtype == array2.dtype
    assert array1.shape == array2.shape
    return array1, array2

@composite
def generate_array_and_where(draw, shape, dtype):
    arr = draw(arrays(shape=shape, dtype=dtype))
    where = draw(arrays(shape=arr.shape, dtype=boolean_dtypes()))
    return arr, where

@composite
def generate_ndarray_of_int_float_datetime_or_timedelta(draw):
    shape = draw(ndarray_shape_strategy())
    dtype = draw(ints_floats_datetimes_and_timedeltas())
    return draw(arrays(dtype=dtype, shape=shape))

@composite
def select_axis(draw, arr: np.ndarray, default_axis: Optional[int] = None):
    """Strategy for generating a random axis value based on the shape of an array."""

@composite
def generate_array(draw, shape, dtype, include_invalid: bool = True):
    if isinstance(dtype, SearchStrategy):
        dtype = draw(shared(dtype))
    arr = draw(
        arrays(
            dtype=dtype,
            shape=shape,
            elements=rt_element_strategy(dtype, include_invalid=include_invalid),
        )
    )
    return arr

@composite
def generate_array_and_axis(draw, shape, dtype, include_invalid: bool = True, default_axis: Optional[int] = None):
    arr = draw(generate_array(shape, dtype, include_invalid))
    axis = draw(select_axis(arr, default_axis=default_axis))
    return arr, axis

@composite
def floating_scalar(draw):
    scalar = draw(floats())
    scalars = (
        scalar,
        np.array(scalar),
        np.float64(scalar),
        np.float32(scalar),
    )
    return draw(sampled_from(scalars))

@composite
def start_stop_step_strategy(draw):
    dtype = ints_floats_and_complex_numbers()
    l = np.sort(draw(lists(dtype, min_size=3, max_size=3)))
    params = {
        'start': l[0],
        'stop': l[2],
        'step': l[1],
    }
    return params

@composite
def generate_tuples_of_arrays(
    draw,
    dtype_strategy=ints_floats_datetimes_and_timedeltas(),
    max_tuple_length=30,
    max_array_size=200,
    all_same_width=False,
    all_same_height=False,
):
    shape_type = draw(
        integers(min_value=0, max_value=4)
    )  # 0: (1,1), 1: (x,), 2: (x,1), 3: (1,x), 4: (x,y)
    if shape_type == 1 and all_same_height:
        shape_type = 3
    dtype = shared(dtype_strategy)
    starting_size = (
        draw(integers(min_value=2, max_value=max_array_size)),
        draw(integers(min_value=2, max_value=max_array_size)),
    )
    arr_list = []
    arr_length = draw(shared(integers(min_value=1, max_value=max_tuple_length)))
    if shape_type == 0:  # (1,1)
        for i in range(arr_length):
            arr_list.append(draw(arrays(dtype=dtype, shape=(1, 1))))
    elif shape_type == 1:  # (x,)
        if all_same_width:
            for i in range(arr_length):
                arr_list.append(draw(arrays(dtype=dtype, shape=(starting_size[1],))))
        else:
            for i in range(arr_length):
                x = draw(integers(min_value=1, max_value=max_array_size))
                arr_list.append(draw(arrays(dtype=dtype, shape=(x,))))
    elif shape_type == 2:  # (x,1)
        if all_same_height:
            for i in range(arr_length):
                arr_list.append(draw(arrays(dtype=dtype, shape=(starting_size[0], 1))))
        else:
            for i in range(arr_length):
                x = draw(integers(min_value=1, max_value=max_array_size))
                arr_list.append(draw(arrays(dtype=dtype, shape=(x, 1))))
    elif shape_type == 3:  # (1,x)
        if all_same_width:
            for i in range(arr_length):
                arr_list.append(draw(arrays(dtype=dtype, shape=(1, starting_size[1]))))
        else:
            for i in range(arr_length):
                x = draw(integers(min_value=1, max_value=max_array_size))
                arr_list.append(draw(arrays(dtype=dtype, shape=(1, x))))
    elif shape_type == 4:  # (x,y)
        if all_same_width and all_same_height:
            for i in range(arr_length):
                arr_list.append(
                    draw(
                        arrays(dtype=dtype, shape=(starting_size[0], starting_size[1]))
                    )
                )
        elif all_same_height:
            for i in range(arr_length):
                x = draw(integers(min_value=1, max_value=max_array_size))
                arr_list.append(draw(arrays(dtype=dtype, shape=(starting_size[0], x))))
        elif all_same_width:
            for i in range(arr_length):
                x = draw(integers(min_value=1, max_value=max_array_size))
                arr_list.append(draw(arrays(dtype=dtype, shape=(x, starting_size[1]))))
        else:
            for i in range(arr_length):
                x = draw(integers(min_value=1, max_value=max_array_size))
                y = draw(integers(min_value=1, max_value=max_array_size))
                arr_list.append(draw(arrays(dtype=dtype, shape=(x, y))))

    return tuple(arr_list)

@composite
def generate_array_axis_and_ddof(draw):
    arr, axis = draw(
        generate_array_and_axis(
            shape=draw(ndarray_shape_strategy()), dtype=draw(ints_or_floats_dtypes())
        )
    )
    N = np.count_nonzero(~np.isnan(arr), axis=axis)
    ddof = draw(integers(min_value=0, max_value=np.min(N)))
    return {
        'arr': arr,
        'axis': axis,
        'ddof': ddof,
    }

@composite
def interpolation_data(draw):
    length = draw(integers(min_value=2, max_value=10000))
    min_xp = draw(integers())
    xp = np.linspace(min_xp, min_xp + length, length)
    x = np.random.uniform(min_xp - 10, min_xp + length + 10, 1000)
    if draw(booleans()):
        m = draw(floats(min_value=-_MAX_VALUE, max_value=_MAX_VALUE))
        b = draw(floats(min_value=-_MAX_VALUE, max_value=_MAX_VALUE))
        fp = m * xp + b
    else:
        m = draw(floats(min_value=-_MAX_VALUE, max_value=_MAX_VALUE))
        vx = draw(floats(min_value=-_MAX_VALUE, max_value=_MAX_VALUE))
        vy = draw(floats(min_value=-_MAX_VALUE, max_value=_MAX_VALUE))
        fp = m * (xp - vx) ** 2 + vy

    return {
        'x': x,
        'xp': xp,
        'fp': fp,
    }

@composite
def generate_array_axis_and_repeats_array(draw, dtype, shape):
    arr = draw(arrays(shape=shape, dtype=dtype))
    axes = list(range(-1, len(arr.shape)))
    axis = draw(sampled_from(tuple(axes)))

    if axis == -1:
        axis = None
        repeats_length = arr.size
    elif axis == 0:
        repeats_length = arr.shape[0]
    elif axis == 1:
        repeats_length = arr.shape[1]
    else:
        repeats_length = 0

    repeats = draw(
        lists(
            integers(min_value=0, max_value=10),
            min_size=repeats_length,
            max_size=repeats_length,
        )
    )
    return arr, axis, repeats

@composite
def generate_reshape_array_and_shape_strategy(draw, shape, dtype):
    arr = draw(arrays(shape=shape, dtype=dtype))

    if draw(booleans()):
        return arr, -1  # make it a 1D array

    new_dim = draw(integers(min_value=1, max_value=arr.size))
    assume(arr.size % new_dim == 0)
    if draw(booleans()):
        return arr, (new_dim, -1)
    return arr, (-1, new_dim)

@composite
def generate_sample_test_integers(draw, num_bits, signed):
    if signed:
        min_value = -(2 ** (num_bits - 1))
        max_value = 2 ** (num_bits - 1) - 1
    else:
        min_value = 0
        max_value = (2 ** num_bits) - 1

    l = (
        0,
        -1,  # FF..FF
        (2 ** num_bits) - 1,  # FF..FF, -1 for signed, max for unsigned
        2
        ** (
            num_bits - 1
        ),  # 80..00, smallest negative for signed, convert to 64 bits first to prevent overflow exceptions
        ~(np.int64(1) << (num_bits - 1)),  # 7F..FF, max for signed
        np.int64(1) << (num_bits - 1),  # 80..00, done differently
        draw(integers(min_value=min_value, max_value=max_value)),
    )
    return l

@composite
def generate_sample_test_floats(draw):
    l = (
        0,
        -0.0,
        1,
        -1,
        np.inf,
        -np.inf,
        np.nan,
        draw(floats(min_value=0, max_value=1)),  # [0, 1]
        draw(floats(min_value=1)),  # [1, inf]
        draw(floats(min_value=-1, max_value=0)),  # [-1, 0]
        draw(floats(max_value=1)),  # [-inf, 1]
        draw(integers(min_value=0)),  # Z & (-inf, 0]
        draw(integers(max_value=0)),  # Z & [0, inf)
    )

    return l

@composite
def one_of_supported_dtypes(draw):
    # A strategy that selects a dtype that riptable is known to handle.
    # little endian is not supported
    return one_of(
        boolean_dtypes(),
        integer_dtypes(endianness="=", sizes=(8, 32, 64)),
        unsigned_integer_dtypes(endianness="=", sizes=(8, 32, 64)),
        floating_dtypes(endianness="=", sizes=(32, 64)),
        byte_string_dtypes(endianness="="),
        unicode_string_dtypes(endianness="="),
        # the following dtypes are not supported
    )

@composite
def generate_lists(draw) -> Tuple[List[str], List[str]]:
    # A strategy that generates a tuple of two lists of the same length for text
    # of non-ASCII characters, nothing on the high end, that exclude control characters
    size = draw(shared(integers(min_value=1, max_value=100)))
    lst1 = draw(
        lists(
            text(
                characters(
                    min_codepoint=1,
                    max_codepoint=1000,
                    blacklist_categories=("Cc", "Cs"),
                )
            ),
            min_size=size,
            max_size=size,
            unique=True,
        )
    )
    lst2 = draw(
        lists(
            text(
                characters(
                    min_codepoint=1,
                    max_codepoint=1000,
                    blacklist_categories=("Cc", "Cs"),
                )
            ),
            min_size=size,
            max_size=size,
            unique=True,
        )
    )
    return (lst1, lst2)

@composite
def generate_ndarrays(draw):
    shape = draw(shared(one_darray_shape_strategy(max_shape_size=10)))
    dtype = draw(one_of_supported_dtypes())
    arr1 = draw(arrays(shape=shape, dtype=dtype, unique=True))
    arr2 = draw(arrays(shape=shape, dtype=dtype, unique=True))
    return (arr1, arr2)

@composite
def generate_list_ndarrays(draw):
    # A strategy that generates a tuple of two lists of the same length that
    # contain ndarrays of possibly different dtypes from the supported dtypes and
    # are the same shape.
    size = draw(shared(integers(min_value=1, max_value=100)))
    shape = draw(shared(one_darray_shape_strategy(max_shape_size=10)))
    dtype = draw(one_of_supported_dtypes())
    lst1 = draw(
        lists(
            arrays(shape=shape, dtype=dtype, unique=True), min_size=size, max_size=size
        )
    )
    lst2 = draw(
        lists(
            arrays(shape=shape, dtype=dtype, unique=True), min_size=size, max_size=size
        )
    )
    return (lst1, lst2)

def rt_element_strategy(
    dtype: Union[np.dtype, type], include_invalid: bool = True
) -> Optional[SearchStrategy[Union[int, float, complex]]]:
    """
    Given a numpy dtype and whether to include riptable invalid values, return a strategy that
    can be used in generating the set of elements when constructing a ndarray or FastArray.

    Parameters
    ----------
    dtype: numpy.dtype or type
        The numpy type or numpy dtype of elements to generate for the search strategy.
    include_invalid: bool, optional
        If True, include riptable specific invalid values, otherwise do not include them (default True).

    Returns
    -------
    SearchStrategy
        A strategy that can be used to limit the scope of elements to generate for an ndarray or FastArray.
        If the dtype is not supported, returns None and warns the dtype is unsupported.

    Examples
    --------
    Generate an example ndarray of dtype int8 with invalid values:

    >>> arrays(np.int8, (2, 3), elements=rt_element_strategy(np.dtype(np.int8)), include_invalid=True).example()
    array([[-8,  6,  3],
           [-6,  4,  -128]], dtype=int8)

    Note, the invalid value, -128, is permissible in this example.

    Generate an example ndarray of dtype int8 without invalid values:

    >>> arrays(np.int8, (2, 3), elements=rt_element_strategy(np.dtype(np.int8)), include_invalid=False).example()
    array([[-8,  6,  3],
           [-6,  4,  -127]], dtype=int8)

    Note that the invalid value, -128, will not show in this array.

    Show all possible integer, float, and complex SearchStrategies:

    >>> for dtype in _dtypes_by_group["AllInteger"] + _dtypes_by_group["Float"] + _dtypes_by_group["Complex"]:
    >>>     print(f"{dtype} with invalid: {repr(rt_element_strategy(dtype, include_invalid=True))}")
    >>>     print(f"{dtype} without invalid: {repr(rt_element_strategy(dtype, include_invalid=False))}")
    int8 with invalid: integers(min_value=-128, max_value=127)
    int8 without invalid: integers(min_value=-127, max_value=127)
    ...

    """
    # Attempt to create a dtype from type.
    if not isinstance(dtype, np.dtype):
        dtype = np.dtype(dtype)

    elements_strategy: SearchStrategy = none()
    if dtype in _dtypes_by_group["AllInteger"]:
        min_value, max_value = integer_range(dtype, include_invalid=include_invalid)
        elements_strategy = integers(min_value=min_value, max_value=max_value)
    elif dtype in _dtypes_by_group["Float"]:
        width = dtype.itemsize * 8
        dtype_info = np.finfo(dtype)
        if include_invalid:
            elements_strategy = floats(
                width=width, allow_nan=True, allow_infinity=True,
            )
        else:
            elements_strategy = floats(
                width=width, allow_nan=False, allow_infinity=True,
            )
    elif dtype in _dtypes_by_group["Complex"]:
        elements_strategy = complex_numbers(allow_nan=True, allow_infinity=True)
    else:
        warnings.warn(f"unhandled dtype '{dtype}'")
        return None
    return elements_strategy

# column strategies and helper utilities for Datasets
# get array columns (ndarray + fastarray)
# get struct columns (one of each)
# get dataset columns (copy of itself for two layer embedding)
# get categorical column (using our categorical strategy)
def column_by_dtypes(dtype_group: Optional[str] = "RiptableNumeric") -> List[pdst.column]:
    """Returns a list of columns from a dtype group for generation of primitive data types wrapped in columns for DataFrame strategies."""
    return [
        pdst.column(str(dtype), dtype=np.dtype(dtype))
        for dtype in set(dtypes_by_group[dtype_group])
    ]

def column_arrays(draw) -> List[Union[np.ndarray, rt.FastArray]]:
    """Returns a list of numpy ndarray and riptide FastArray wrapped in columns for DataFrame strategies."""
    # todo add strategy to generate FastArray to the return list
    arr = draw(
        generate_array(
            shape=ndarray_shape_strategy(),
            dtype=ints_or_floats_dtypes(),
            include_invalid=False,
        )
    )
    return [pdst.column(name(arr), elements=arr)]


from .logresponse import LogResponse
from .util import Util

__all__ = ['CreateEtlResponse', 'DeleteEtlResponse', 'GetEtlResponse', 'UpdateEtlResponse',
           'ListEtlsResponse', 'StopEtlResponse', 'StartEtlResponse']

class CreateEtlResponse(LogResponse):
    """ The response of the create_etl API from log.
    :type resp: dict
    :param resp: CreateEtlResponse HTTP response body
    :type header: dict
    :param header: CreateEtlResponse HTTP response header
    """

    def __init__(self, header, resp=''):
        LogResponse.__init__(self, header, resp)

    def log_print(self):
        print('CreateEtlResponse:')
        print('headers:', self.get_all_headers())

class DeleteEtlResponse(LogResponse):
    """ The response of the delete_etl API from log.
    :type resp: dict
    :param resp: DeleteEtlResponse HTTP response body
    :type header: dict
    :param header: DeleteEtlResponse HTTP response header
    """

    def __init__(self, header, resp=''):
        LogResponse.__init__(self, header, resp)

    def log_print(self):
        print('DeleteEtlResponse:')
        print('headers:', self.get_all_headers())

class GetEtlResponse(LogResponse):
    """ The response of the get_etl API from log.
    :type resp: dict
    :param resp: GetEtlResponse HTTP response body
    :type header: dict
    :param header: GetEtlResponse HTTP response header
    """

    def __init__(self, header, resp):
        LogResponse.__init__(self, header, resp)
        self.etl_config = resp

    def get_etl(self):
        """Get etl from the response
        :return: dict, etl
        """
        return self.etl_config

    def log_print(self):
        print('GetEtlResponse:')
        print('headers:', self.get_all_headers())
        print('etl: ', self.etl_config)

class UpdateEtlResponse(LogResponse):
    """ The response of the update_etl API from log.
    :type resp: dict
    :param resp: UpdateEtlResponse HTTP response header
    :type header: dict
    :param header: UpdateEtlResponse HTTP response header
    """

    def __init__(self, header, resp=''):
        LogResponse.__init__(self, header, resp)

    def log_print(self):
        print('UpdateEtlResponse:')
        print('headers:', self.get_all_headers())

class StartEtlResponse(LogResponse):
    """ The response of the start_etl API from log.
    :type resp: dict
    :param resp: StartEtlResponse HTTP response body
    :type header: dict
    :param header: StartEtlResponse HTTP response header
    """

    def __init__(self, header, resp=''):
        LogResponse.__init__(self, header, resp)

    def log_print(self):
        print('StartEtlResponse:')
        print('headers:', self.get_all_headers())

class StopEtlResponse(LogResponse):
    """ The response of the stop_etl API from log.
    :type resp: dict
    :param resp: StopEtlResponse HTTP response body
    :type header: dict
    :param header: StopEtlResponse HTTP response header
    """

    def __init__(self, header, resp=''):
        LogResponse.__init__(self, header, resp)

    def log_print(self):
        print('StopEtlResponse:')
        print('headers:', self.get_all_headers())

class ListEtlsResponse(LogResponse):
    """ The response of the list_etls API from log.
    :type header: dict
    :param header: ListEtlsResponse HTTP response header
    :type resp: dict
    :param resp: ListEtlsResponse HTTP response body
    """

    def __init__(self, resp, header):
        LogResponse.__init__(self, header, resp)
        self.count = int(resp["count"])
        self.total = int(resp["total"])
        self.etls = Util.convert_unicode_to_str(resp.get("results", []))

    def get_etls(self):
        """Get all the etls from the response
        :return: list, all etls
        """
        return self.etls

    def get_count(self):
        """ Get count of etls from the response
        :return: int, the number of count etls from the response
        """

    def get_total(self):
        """Get total count of etls from the response
        :return: int, the number of total etls from the response
        """
        return self.total

    def log_print(self):
        print('ListEtlResponse:')
        print('headers:', self.get_all_headers())
        print('count:', str(self.count))
        print('total:', str(self.total))
        print('etls:', self.etls)


  if not block_specs:
    block_specs = SPINENET_BLOCK_SPECS
  logging.info('Building SpineNet block specs: %s', block_specs)
  return [BlockSpec(*b) for b in block_specs]

"""Implementation of SpineNet model.

X. Du, T-Y. Lin, P. Jin, G. Ghiasi, M. Tan, Y. Cui, Q. V. Le, X. Song
SpineNet: Learning Scale-Permuted Backbone for Recognition and Localization
http://example.com
"""
import math

# Import libraries
from absl import logging
import tensorflow as tf
from official.modeling import tf_utils
from official.vision.beta.modeling.backbones import factory
from official.vision.beta.modeling.layers import nn_blocks
from official.vision.beta.ops import spatial_transform_ops

layers = tf.keras.layers

FILTER_SIZE_MAP = {
    1: 32,
    2: 64,
    3: 128,
    4: 256,
    5: 256,
    6: 256,
    7: 256,
}

# The fixed SpineNet architecture discovered by NAS.
# Each element represents a specification of a building block:
SPINENET_BLOCK_SPECS = [
    (2, 'bottleneck', (0, 1), False),
    (4, 'residual', (0, 1), False),
    (3, 'bottleneck', (2, 3), False),
    (4, 'bottleneck', (2, 4), False),
    (6, 'residual', (3, 5), False),
    (4, 'bottleneck', (3, 5), False),
    (5, 'residual', (6, 7), False),
    (7, 'residual', (6, 8), False),
    (5, 'bottleneck', (8, 9), False),
    (5, 'bottleneck', (8, 10), False),
    (4, 'bottleneck', (5, 10), True),
    (3, 'bottleneck', (4, 10), True),
    (5, 'bottleneck', (7, 12), True),
    (7, 'bottleneck', (5, 14), True),
    (6, 'bottleneck', (12, 14), True),
]

SCALING_MAP = {
    '49S': {
        'endpoints_num_filters': 128,
        'filter_size_scale': 0.65,
        'resample_alpha': 0.5,
        'block_repeats': 1,
    },
    '49': {
        'endpoints_num_filters': 256,
        'filter_size_scale': 1.0,
        'resample_alpha': 0.5,
        'block_repeats': 1,
    },
    '96': {
        'endpoints_num_filters': 256,
        'filter_size_scale': 1.0,
        'resample_alpha': 0.5,
        'block_repeats': 2,
    },
    '143': {
        'endpoints_num_filters': 256,
        'filter_size_scale': 1.0,
        'resample_alpha': 1.0,
        'block_repeats': 3,
    },
    '190': {
        'endpoints_num_filters': 512,
        'filter_size_scale': 1.3,
        'resample_alpha': 1.0,
        'block_repeats': 4,
    },
}

class BlockSpec(object):
  """A container class that specifies the block configuration for SpineNet."""

  def __init__(self, level, block_fn, input_offsets, is_output):
    self.level = level
    self.block_fn = block_fn
    self.input_offsets = input_offsets
    self.is_output = is_output

def build_block_specs(block_specs=None):
  """Builds the list of BlockSpec objects for SpineNet."""

def get_stochastic_depth_rate(init_rate, i, n):
  """Get drop connect rate for the ith block."""
  if init_rate is not None:
    if init_rate < 0 or init_rate > 1:
      raise ValueError('Initial drop rate must be within 0 and 1.')
    dc_rate = init_rate * float(i + 1) / n
  else:
    dc_rate = None
  return dc_rate

@tf.keras.utils.register_keras_serializable(package='Vision')
class SpineNet(tf.keras.Model):
  """Class to build SpineNet models."""

  def __init__(self,
               input_specs=tf.keras.layers.InputSpec(shape=[None, 640, 640, 3]),
               min_level=3,
               max_level=7,
               block_specs=build_block_specs(),
               endpoints_num_filters=256,
               resample_alpha=0.5,
               block_repeats=1,
               filter_size_scale=1.0,
               init_stochastic_depth_rate=0.0,
               kernel_initializer='VarianceScaling',
               kernel_regularizer=None,
               bias_regularizer=None,
               activation='relu',
               use_sync_bn=False,
               norm_momentum=0.99,
               norm_epsilon=0.001,
               **kwargs):
    """SpineNet model."""
    self._input_specs = input_specs
    self._min_level = min_level
    self._max_level = max_level
    self._block_specs = block_specs
    self._endpoints_num_filters = endpoints_num_filters
    self._resample_alpha = resample_alpha
    self._block_repeats = block_repeats
    self._filter_size_scale = filter_size_scale
    self._init_stochastic_depth_rate = init_stochastic_depth_rate
    self._kernel_initializer = kernel_initializer
    self._kernel_regularizer = kernel_regularizer
    self._bias_regularizer = bias_regularizer
    self._activation = activation
    self._use_sync_bn = use_sync_bn
    self._norm_momentum = norm_momentum
    self._norm_epsilon = norm_epsilon
    if activation == 'relu':
      self._activation_fn = tf.nn.relu
    elif activation == 'swish':
      self._activation_fn = tf.nn.swish
    else:
      raise ValueError('Activation {} not implemented.'.format(activation))
    self._init_block_fn = 'bottleneck'
    self._num_init_blocks = 2

    if use_sync_bn:
      self._norm = layers.experimental.SyncBatchNormalization
    else:
      self._norm = layers.BatchNormalization

    if tf.keras.backend.image_data_format() == 'channels_last':
      self._bn_axis = -1
    else:
      self._bn_axis = 1

    # Build SpineNet.
    inputs = tf.keras.Input(shape=input_specs.shape[1:])

    net = self._build_stem(inputs=inputs)
    net = self._build_scale_permuted_network(
        net=net, input_width=input_specs.shape[1])
    endpoints = self._build_endpoints(net=net)

    self._output_specs = {l: endpoints[l].get_shape() for l in endpoints}
    super(SpineNet, self).__init__(inputs=inputs, outputs=endpoints)

  def _block_group(self,
                   inputs,
                   filters,
                   strides,
                   block_fn_cand,
                   block_repeats=1,
                   stochastic_depth_drop_rate=None,
                   name='block_group'):
    """Creates one group of blocks for the SpineNet model."""
    block_fn_candidates = {
        'bottleneck': nn_blocks.BottleneckBlock,
        'residual': nn_blocks.ResidualBlock,
    }
    block_fn = block_fn_candidates[block_fn_cand]
    _, _, _, num_filters = inputs.get_shape().as_list()

    if block_fn_cand == 'bottleneck':
      use_projection = not (num_filters == (filters * 4) and strides == 1)
    else:
      use_projection = not (num_filters == filters and strides == 1)

    x = block_fn(
        filters=filters,
        strides=strides,
        use_projection=use_projection,
        stochastic_depth_drop_rate=stochastic_depth_drop_rate,
        kernel_initializer=self._kernel_initializer,
        kernel_regularizer=self._kernel_regularizer,
        bias_regularizer=self._bias_regularizer,
        activation=self._activation,
        use_sync_bn=self._use_sync_bn,
        norm_momentum=self._norm_momentum,
        norm_epsilon=self._norm_epsilon)(
            inputs)
    for _ in range(1, block_repeats):
      x = block_fn(
          filters=filters,
          strides=1,
          use_projection=False,
          stochastic_depth_drop_rate=stochastic_depth_drop_rate,
          kernel_initializer=self._kernel_initializer,
          kernel_regularizer=self._kernel_regularizer,
          bias_regularizer=self._bias_regularizer,
          activation=self._activation,
          use_sync_bn=self._use_sync_bn,
          norm_momentum=self._norm_momentum,
          norm_epsilon=self._norm_epsilon)(
              x)
    return tf.identity(x, name=name)

  def _build_stem(self, inputs):
    """Build SpineNet stem."""
    x = layers.Conv2D(
        filters=64,
        kernel_size=7,
        strides=2,
        use_bias=False,
        padding='same',
        kernel_initializer=self._kernel_initializer,
        kernel_regularizer=self._kernel_regularizer,
        bias_regularizer=self._bias_regularizer)(
            inputs)
    x = self._norm(
        axis=self._bn_axis,
        momentum=self._norm_momentum,
        epsilon=self._norm_epsilon)(
            x)
    x = tf_utils.get_activation(self._activation_fn)(x)
    x = layers.MaxPool2D(pool_size=3, strides=2, padding='same')(x)

    net = []
    # Build the initial level 2 blocks.
    for i in range(self._num_init_blocks):
      x = self._block_group(
          inputs=x,
          filters=int(FILTER_SIZE_MAP[2] * self._filter_size_scale),
          strides=1,
          block_fn_cand=self._init_block_fn,
          block_repeats=self._block_repeats,
          name='stem_block_{}'.format(i + 1))
      net.append(x)
    return net

  def _build_scale_permuted_network(self,
                                    net,
                                    input_width,
                                    weighted_fusion=False):
    """Build scale-permuted network."""
    net_sizes = [int(math.ceil(input_width / 2**2))] * len(net)
    net_block_fns = [self._init_block_fn] * len(net)
    num_outgoing_connections = [0] * len(net)

    endpoints = {}
    for i, block_spec in enumerate(self._block_specs):
      # Find out specs for the target block.
      target_width = int(math.ceil(input_width / 2**block_spec.level))
      target_num_filters = int(FILTER_SIZE_MAP[block_spec.level] *
                               self._filter_size_scale)
      target_block_fn = block_spec.block_fn

      # Resample then merge input0 and input1.
      parents = []
      input0 = block_spec.input_offsets[0]
      input1 = block_spec.input_offsets[1]

      x0 = self._resample_with_alpha(
          inputs=net[input0],
          input_width=net_sizes[input0],
          input_block_fn=net_block_fns[input0],
          target_width=target_width,
          target_num_filters=target_num_filters,
          target_block_fn=target_block_fn,
          alpha=self._resample_alpha)
      parents.append(x0)
      num_outgoing_connections[input0] += 1

      x1 = self._resample_with_alpha(
          inputs=net[input1],
          input_width=net_sizes[input1],
          input_block_fn=net_block_fns[input1],
          target_width=target_width,
          target_num_filters=target_num_filters,
          target_block_fn=target_block_fn,
          alpha=self._resample_alpha)
      parents.append(x1)
      num_outgoing_connections[input1] += 1

      # Merge 0 outdegree blocks to the output block.
      if block_spec.is_output:
        for j, (j_feat,
                j_connections) in enumerate(zip(net, num_outgoing_connections)):
          if j_connections == 0 and (j_feat.shape[2] == target_width and
                                     j_feat.shape[3] == x0.shape[3]):
            parents.append(j_feat)
            num_outgoing_connections[j] += 1

      if weighted_fusion:
        dtype = parents[0].dtype
        parent_weights = [
            tf.nn.relu(tf.cast(tf.Variable(1.0, name='block{}_fusion{}'.format(
                i, j)), dtype=dtype)) for j in range(len(parents))]
        weights_sum = tf.add_n(parent_weights)
        parents = [
            parents[i] * parent_weights[i] / (weights_sum + 0.0001)
            for i in range(len(parents))
        ]

      # Fuse all parent nodes then build a new block.
      x = tf_utils.get_activation(self._activation_fn)(tf.add_n(parents))
      x = self._block_group(
          inputs=x,
          filters=target_num_filters,
          strides=1,
          block_fn_cand=target_block_fn,
          block_repeats=self._block_repeats,
          stochastic_depth_drop_rate=get_stochastic_depth_rate(
              self._init_stochastic_depth_rate, i, len(self._block_specs)),
          name='scale_permuted_block_{}'.format(i + 1))

      net.append(x)
      net_sizes.append(target_width)
      net_block_fns.append(target_block_fn)
      num_outgoing_connections.append(0)

      # Save output feats.
      if block_spec.is_output:
        if block_spec.level in endpoints:
          raise ValueError('Duplicate feats found for output level {}.'.format(
              block_spec.level))
        if (block_spec.level < self._min_level or
            block_spec.level > self._max_level):
          raise ValueError('Output level is out of range [{}, {}]'.format(
              self._min_level, self._max_level))
        endpoints[str(block_spec.level)] = x

    return endpoints

  def _build_endpoints(self, net):
    """Match filter size for endpoints before sharing conv layers."""
    endpoints = {}
    for level in range(self._min_level, self._max_level + 1):
      x = layers.Conv2D(
          filters=self._endpoints_num_filters,
          kernel_size=1,
          strides=1,
          use_bias=False,
          kernel_initializer=self._kernel_initializer,
          kernel_regularizer=self._kernel_regularizer,
          bias_regularizer=self._bias_regularizer)(
              net[str(level)])
      x = self._norm(
          axis=self._bn_axis,
          momentum=self._norm_momentum,
          epsilon=self._norm_epsilon)(
              x)
      x = tf_utils.get_activation(self._activation_fn)(x)
      endpoints[str(level)] = x
    return endpoints

  def _resample_with_alpha(self,
                           inputs,
                           input_width,
                           input_block_fn,
                           target_width,
                           target_num_filters,
                           target_block_fn,
                           alpha=0.5):
    """Match resolution and feature dimension."""
    _, _, _, input_num_filters = inputs.get_shape().as_list()
    if input_block_fn == 'bottleneck':
      input_num_filters /= 4
    new_num_filters = int(input_num_filters * alpha)

    x = layers.Conv2D(
        filters=new_num_filters,
        kernel_size=1,
        strides=1,
        use_bias=False,
        kernel_initializer=self._kernel_initializer,
        kernel_regularizer=self._kernel_regularizer,
        bias_regularizer=self._bias_regularizer)(
            inputs)
    x = self._norm(
        axis=self._bn_axis,
        momentum=self._norm_momentum,
        epsilon=self._norm_epsilon)(
            x)
    x = tf_utils.get_activation(self._activation_fn)(x)

    # Spatial resampling.
    if input_width > target_width:
      x = layers.Conv2D(
          filters=new_num_filters,
          kernel_size=3,
          strides=2,
          padding='SAME',
          use_bias=False,
          kernel_initializer=self._kernel_initializer,
          kernel_regularizer=self._kernel_regularizer,
          bias_regularizer=self._bias_regularizer)(
              x)
      x = self._norm(
          axis=self._bn_axis,
          momentum=self._norm_momentum,
          epsilon=self._norm_epsilon)(
              x)
      x = tf_utils.get_activation(self._activation_fn)(x)
      input_width /= 2
      while input_width > target_width:
        x = layers.MaxPool2D(pool_size=3, strides=2, padding='SAME')(x)
        input_width /= 2
    elif input_width < target_width:
      scale = target_width // input_width
      x = spatial_transform_ops.nearest_upsampling(x, scale=scale)

    # Last 1x1 conv to match filter size.
    if target_block_fn == 'bottleneck':
      target_num_filters *= 4
    x = layers.Conv2D(
        filters=target_num_filters,
        kernel_size=1,
        strides=1,
        use_bias=False,
        kernel_initializer=self._kernel_initializer,
        kernel_regularizer=self._kernel_regularizer,
        bias_regularizer=self._bias_regularizer)(
            x)
    x = self._norm(
        axis=self._bn_axis,
        momentum=self._norm_momentum,
        epsilon=self._norm_epsilon)(
            x)
    return x

  def get_config(self):
    config_dict = {
        'min_level': self._min_level,
        'max_level': self._max_level,
        'endpoints_num_filters': self._endpoints_num_filters,
        'resample_alpha': self._resample_alpha,
        'block_repeats': self._block_repeats,
        'filter_size_scale': self._filter_size_scale,
        'init_stochastic_depth_rate': self._init_stochastic_depth_rate,
        'kernel_initializer': self._kernel_initializer,
        'kernel_regularizer': self._kernel_regularizer,
        'bias_regularizer': self._bias_regularizer,
        'activation': self._activation,
        'use_sync_bn': self._use_sync_bn,
        'norm_momentum': self._norm_momentum,
        'norm_epsilon': self._norm_epsilon
    }
    return config_dict

  @classmethod
  def from_config(cls, config, custom_objects=None):
    return cls(**config)

  @property
  def output_specs(self):
    """A dict of {level: TensorShape} pairs for the model output."""
    return self._output_specs

@factory.register_backbone_builder('spinenet')
def build_spinenet(
    input_specs: tf.keras.layers.InputSpec,
    model_config,
    l2_regularizer: tf.keras.regularizers.Regularizer = None) -> tf.keras.Model:
  """Builds ResNet 3d backbone from a config."""
  backbone_type = model_config.backbone.type
  backbone_cfg = model_config.backbone.get()
  norm_activation_config = model_config.norm_activation
  assert backbone_type == 'spinenet', (f'Inconsistent backbone type '
                                       f'{backbone_type}')

  model_id = backbone_cfg.model_id
  if model_id not in SCALING_MAP:
    raise ValueError(
        'SpineNet-{} is not a valid architecture.'.format(model_id))
  scaling_params = SCALING_MAP[model_id]

  return SpineNet(
      input_specs=input_specs,
      min_level=model_config.min_level,
      max_level=model_config.max_level,
      endpoints_num_filters=scaling_params['endpoints_num_filters'],
      resample_alpha=scaling_params['resample_alpha'],
      block_repeats=scaling_params['block_repeats'],
      filter_size_scale=scaling_params['filter_size_scale'],
      init_stochastic_depth_rate=backbone_cfg.stochastic_depth_drop_rate,
      kernel_regularizer=l2_regularizer,
      activation=norm_activation_config.activation,
      use_sync_bn=norm_activation_config.use_sync_bn,
      norm_momentum=norm_activation_config.norm_momentum,
      norm_epsilon=norm_activation_config.norm_epsilon)


while input_width > target_width:
        x = layers.MaxPool2D(pool_size=3, strides=2, padding='SAME')(x)
        input_width /= 2

"""Implementation of SpineNet model.

X. Du, T-Y. Lin, P. Jin, G. Ghiasi, M. Tan, Y. Cui, Q. V. Le, X. Song
SpineNet: Learning Scale-Permuted Backbone for Recognition and Localization
http://example.com
"""
import math

# Import libraries
from absl import logging
import tensorflow as tf
from official.modeling import tf_utils
from official.vision.beta.modeling.backbones import factory
from official.vision.beta.modeling.layers import nn_blocks
from official.vision.beta.ops import spatial_transform_ops

layers = tf.keras.layers

FILTER_SIZE_MAP = {
    1: 32,
    2: 64,
    3: 128,
    4: 256,
    5: 256,
    6: 256,
    7: 256,
}

# The fixed SpineNet architecture discovered by NAS.
# Each element represents a specification of a building block:
SPINENET_BLOCK_SPECS = [
    (2, 'bottleneck', (0, 1), False),
    (4, 'residual', (0, 1), False),
    (3, 'bottleneck', (2, 3), False),
    (4, 'bottleneck', (2, 4), False),
    (6, 'residual', (3, 5), False),
    (4, 'bottleneck', (3, 5), False),
    (5, 'residual', (6, 7), False),
    (7, 'residual', (6, 8), False),
    (5, 'bottleneck', (8, 9), False),
    (5, 'bottleneck', (8, 10), False),
    (4, 'bottleneck', (5, 10), True),
    (3, 'bottleneck', (4, 10), True),
    (5, 'bottleneck', (7, 12), True),
    (7, 'bottleneck', (5, 14), True),
    (6, 'bottleneck', (12, 14), True),
]

SCALING_MAP = {
    '49S': {
        'endpoints_num_filters': 128,
        'filter_size_scale': 0.65,
        'resample_alpha': 0.5,
        'block_repeats': 1,
    },
    '49': {
        'endpoints_num_filters': 256,
        'filter_size_scale': 1.0,
        'resample_alpha': 0.5,
        'block_repeats': 1,
    },
    '96': {
        'endpoints_num_filters': 256,
        'filter_size_scale': 1.0,
        'resample_alpha': 0.5,
        'block_repeats': 2,
    },
    '143': {
        'endpoints_num_filters': 256,
        'filter_size_scale': 1.0,
        'resample_alpha': 1.0,
        'block_repeats': 3,
    },
    '190': {
        'endpoints_num_filters': 512,
        'filter_size_scale': 1.3,
        'resample_alpha': 1.0,
        'block_repeats': 4,
    },
}

class BlockSpec(object):
  """A container class that specifies the block configuration for SpineNet."""

  def __init__(self, level, block_fn, input_offsets, is_output):
    self.level = level
    self.block_fn = block_fn
    self.input_offsets = input_offsets
    self.is_output = is_output

def build_block_specs(block_specs=None):
  """Builds the list of BlockSpec objects for SpineNet."""
  if not block_specs:
    block_specs = SPINENET_BLOCK_SPECS
  logging.info('Building SpineNet block specs: %s', block_specs)
  return [BlockSpec(*b) for b in block_specs]

def get_stochastic_depth_rate(init_rate, i, n):
  """Get drop connect rate for the ith block."""
  if init_rate is not None:
    if init_rate < 0 or init_rate > 1:
      raise ValueError('Initial drop rate must be within 0 and 1.')
    dc_rate = init_rate * float(i + 1) / n
  else:
    dc_rate = None
  return dc_rate

@tf.keras.utils.register_keras_serializable(package='Vision')
class SpineNet(tf.keras.Model):
  """Class to build SpineNet models."""

  def __init__(self,
               input_specs=tf.keras.layers.InputSpec(shape=[None, 640, 640, 3]),
               min_level=3,
               max_level=7,
               block_specs=build_block_specs(),
               endpoints_num_filters=256,
               resample_alpha=0.5,
               block_repeats=1,
               filter_size_scale=1.0,
               init_stochastic_depth_rate=0.0,
               kernel_initializer='VarianceScaling',
               kernel_regularizer=None,
               bias_regularizer=None,
               activation='relu',
               use_sync_bn=False,
               norm_momentum=0.99,
               norm_epsilon=0.001,
               **kwargs):
    """SpineNet model."""
    self._input_specs = input_specs
    self._min_level = min_level
    self._max_level = max_level
    self._block_specs = block_specs
    self._endpoints_num_filters = endpoints_num_filters
    self._resample_alpha = resample_alpha
    self._block_repeats = block_repeats
    self._filter_size_scale = filter_size_scale
    self._init_stochastic_depth_rate = init_stochastic_depth_rate
    self._kernel_initializer = kernel_initializer
    self._kernel_regularizer = kernel_regularizer
    self._bias_regularizer = bias_regularizer
    self._activation = activation
    self._use_sync_bn = use_sync_bn
    self._norm_momentum = norm_momentum
    self._norm_epsilon = norm_epsilon
    if activation == 'relu':
      self._activation_fn = tf.nn.relu
    elif activation == 'swish':
      self._activation_fn = tf.nn.swish
    else:
      raise ValueError('Activation {} not implemented.'.format(activation))
    self._init_block_fn = 'bottleneck'
    self._num_init_blocks = 2

    if use_sync_bn:
      self._norm = layers.experimental.SyncBatchNormalization
    else:
      self._norm = layers.BatchNormalization

    if tf.keras.backend.image_data_format() == 'channels_last':
      self._bn_axis = -1
    else:
      self._bn_axis = 1

    # Build SpineNet.
    inputs = tf.keras.Input(shape=input_specs.shape[1:])

    net = self._build_stem(inputs=inputs)
    net = self._build_scale_permuted_network(
        net=net, input_width=input_specs.shape[1])
    endpoints = self._build_endpoints(net=net)

    self._output_specs = {l: endpoints[l].get_shape() for l in endpoints}
    super(SpineNet, self).__init__(inputs=inputs, outputs=endpoints)

  def _block_group(self,
                   inputs,
                   filters,
                   strides,
                   block_fn_cand,
                   block_repeats=1,
                   stochastic_depth_drop_rate=None,
                   name='block_group'):
    """Creates one group of blocks for the SpineNet model."""
    block_fn_candidates = {
        'bottleneck': nn_blocks.BottleneckBlock,
        'residual': nn_blocks.ResidualBlock,
    }
    block_fn = block_fn_candidates[block_fn_cand]
    _, _, _, num_filters = inputs.get_shape().as_list()

    if block_fn_cand == 'bottleneck':
      use_projection = not (num_filters == (filters * 4) and strides == 1)
    else:
      use_projection = not (num_filters == filters and strides == 1)

    x = block_fn(
        filters=filters,
        strides=strides,
        use_projection=use_projection,
        stochastic_depth_drop_rate=stochastic_depth_drop_rate,
        kernel_initializer=self._kernel_initializer,
        kernel_regularizer=self._kernel_regularizer,
        bias_regularizer=self._bias_regularizer,
        activation=self._activation,
        use_sync_bn=self._use_sync_bn,
        norm_momentum=self._norm_momentum,
        norm_epsilon=self._norm_epsilon)(
            inputs)
    for _ in range(1, block_repeats):
      x = block_fn(
          filters=filters,
          strides=1,
          use_projection=False,
          stochastic_depth_drop_rate=stochastic_depth_drop_rate,
          kernel_initializer=self._kernel_initializer,
          kernel_regularizer=self._kernel_regularizer,
          bias_regularizer=self._bias_regularizer,
          activation=self._activation,
          use_sync_bn=self._use_sync_bn,
          norm_momentum=self._norm_momentum,
          norm_epsilon=self._norm_epsilon)(
              x)
    return tf.identity(x, name=name)

  def _build_stem(self, inputs):
    """Build SpineNet stem."""
    x = layers.Conv2D(
        filters=64,
        kernel_size=7,
        strides=2,
        use_bias=False,
        padding='same',
        kernel_initializer=self._kernel_initializer,
        kernel_regularizer=self._kernel_regularizer,
        bias_regularizer=self._bias_regularizer)(
            inputs)
    x = self._norm(
        axis=self._bn_axis,
        momentum=self._norm_momentum,
        epsilon=self._norm_epsilon)(
            x)
    x = tf_utils.get_activation(self._activation_fn)(x)
    x = layers.MaxPool2D(pool_size=3, strides=2, padding='same')(x)

    net = []
    # Build the initial level 2 blocks.
    for i in range(self._num_init_blocks):
      x = self._block_group(
          inputs=x,
          filters=int(FILTER_SIZE_MAP[2] * self._filter_size_scale),
          strides=1,
          block_fn_cand=self._init_block_fn,
          block_repeats=self._block_repeats,
          name='stem_block_{}'.format(i + 1))
      net.append(x)
    return net

  def _build_scale_permuted_network(self,
                                    net,
                                    input_width,
                                    weighted_fusion=False):
    """Build scale-permuted network."""
    net_sizes = [int(math.ceil(input_width / 2**2))] * len(net)
    net_block_fns = [self._init_block_fn] * len(net)
    num_outgoing_connections = [0] * len(net)

    endpoints = {}
    for i, block_spec in enumerate(self._block_specs):
      # Find out specs for the target block.
      target_width = int(math.ceil(input_width / 2**block_spec.level))
      target_num_filters = int(FILTER_SIZE_MAP[block_spec.level] *
                               self._filter_size_scale)
      target_block_fn = block_spec.block_fn

      # Resample then merge input0 and input1.
      parents = []
      input0 = block_spec.input_offsets[0]
      input1 = block_spec.input_offsets[1]

      x0 = self._resample_with_alpha(
          inputs=net[input0],
          input_width=net_sizes[input0],
          input_block_fn=net_block_fns[input0],
          target_width=target_width,
          target_num_filters=target_num_filters,
          target_block_fn=target_block_fn,
          alpha=self._resample_alpha)
      parents.append(x0)
      num_outgoing_connections[input0] += 1

      x1 = self._resample_with_alpha(
          inputs=net[input1],
          input_width=net_sizes[input1],
          input_block_fn=net_block_fns[input1],
          target_width=target_width,
          target_num_filters=target_num_filters,
          target_block_fn=target_block_fn,
          alpha=self._resample_alpha)
      parents.append(x1)
      num_outgoing_connections[input1] += 1

      # Merge 0 outdegree blocks to the output block.
      if block_spec.is_output:
        for j, (j_feat,
                j_connections) in enumerate(zip(net, num_outgoing_connections)):
          if j_connections == 0 and (j_feat.shape[2] == target_width and
                                     j_feat.shape[3] == x0.shape[3]):
            parents.append(j_feat)
            num_outgoing_connections[j] += 1

      if weighted_fusion:
        dtype = parents[0].dtype
        parent_weights = [
            tf.nn.relu(tf.cast(tf.Variable(1.0, name='block{}_fusion{}'.format(
                i, j)), dtype=dtype)) for j in range(len(parents))]
        weights_sum = tf.add_n(parent_weights)
        parents = [
            parents[i] * parent_weights[i] / (weights_sum + 0.0001)
            for i in range(len(parents))
        ]

      # Fuse all parent nodes then build a new block.
      x = tf_utils.get_activation(self._activation_fn)(tf.add_n(parents))
      x = self._block_group(
          inputs=x,
          filters=target_num_filters,
          strides=1,
          block_fn_cand=target_block_fn,
          block_repeats=self._block_repeats,
          stochastic_depth_drop_rate=get_stochastic_depth_rate(
              self._init_stochastic_depth_rate, i, len(self._block_specs)),
          name='scale_permuted_block_{}'.format(i + 1))

      net.append(x)
      net_sizes.append(target_width)
      net_block_fns.append(target_block_fn)
      num_outgoing_connections.append(0)

      # Save output feats.
      if block_spec.is_output:
        if block_spec.level in endpoints:
          raise ValueError('Duplicate feats found for output level {}.'.format(
              block_spec.level))
        if (block_spec.level < self._min_level or
            block_spec.level > self._max_level):
          raise ValueError('Output level is out of range [{}, {}]'.format(
              self._min_level, self._max_level))
        endpoints[str(block_spec.level)] = x

    return endpoints

  def _build_endpoints(self, net):
    """Match filter size for endpoints before sharing conv layers."""
    endpoints = {}
    for level in range(self._min_level, self._max_level + 1):
      x = layers.Conv2D(
          filters=self._endpoints_num_filters,
          kernel_size=1,
          strides=1,
          use_bias=False,
          kernel_initializer=self._kernel_initializer,
          kernel_regularizer=self._kernel_regularizer,
          bias_regularizer=self._bias_regularizer)(
              net[str(level)])
      x = self._norm(
          axis=self._bn_axis,
          momentum=self._norm_momentum,
          epsilon=self._norm_epsilon)(
              x)
      x = tf_utils.get_activation(self._activation_fn)(x)
      endpoints[str(level)] = x
    return endpoints

  def _resample_with_alpha(self,
                           inputs,
                           input_width,
                           input_block_fn,
                           target_width,
                           target_num_filters,
                           target_block_fn,
                           alpha=0.5):
    """Match resolution and feature dimension."""
    _, _, _, input_num_filters = inputs.get_shape().as_list()
    if input_block_fn == 'bottleneck':
      input_num_filters /= 4
    new_num_filters = int(input_num_filters * alpha)

    x = layers.Conv2D(
        filters=new_num_filters,
        kernel_size=1,
        strides=1,
        use_bias=False,
        kernel_initializer=self._kernel_initializer,
        kernel_regularizer=self._kernel_regularizer,
        bias_regularizer=self._bias_regularizer)(
            inputs)
    x = self._norm(
        axis=self._bn_axis,
        momentum=self._norm_momentum,
        epsilon=self._norm_epsilon)(
            x)
    x = tf_utils.get_activation(self._activation_fn)(x)

    # Spatial resampling.
    if input_width > target_width:
      x = layers.Conv2D(
          filters=new_num_filters,
          kernel_size=3,
          strides=2,
          padding='SAME',
          use_bias=False,
          kernel_initializer=self._kernel_initializer,
          kernel_regularizer=self._kernel_regularizer,
          bias_regularizer=self._bias_regularizer)(
              x)
      x = self._norm(
          axis=self._bn_axis,
          momentum=self._norm_momentum,
          epsilon=self._norm_epsilon)(
              x)
      x = tf_utils.get_activation(self._activation_fn)(x)
      input_width /= 2

    elif input_width < target_width:
      scale = target_width // input_width
      x = spatial_transform_ops.nearest_upsampling(x, scale=scale)

    # Last 1x1 conv to match filter size.
    if target_block_fn == 'bottleneck':
      target_num_filters *= 4
    x = layers.Conv2D(
        filters=target_num_filters,
        kernel_size=1,
        strides=1,
        use_bias=False,
        kernel_initializer=self._kernel_initializer,
        kernel_regularizer=self._kernel_regularizer,
        bias_regularizer=self._bias_regularizer)(
            x)
    x = self._norm(
        axis=self._bn_axis,
        momentum=self._norm_momentum,
        epsilon=self._norm_epsilon)(
            x)
    return x

  def get_config(self):
    config_dict = {
        'min_level': self._min_level,
        'max_level': self._max_level,
        'endpoints_num_filters': self._endpoints_num_filters,
        'resample_alpha': self._resample_alpha,
        'block_repeats': self._block_repeats,
        'filter_size_scale': self._filter_size_scale,
        'init_stochastic_depth_rate': self._init_stochastic_depth_rate,
        'kernel_initializer': self._kernel_initializer,
        'kernel_regularizer': self._kernel_regularizer,
        'bias_regularizer': self._bias_regularizer,
        'activation': self._activation,
        'use_sync_bn': self._use_sync_bn,
        'norm_momentum': self._norm_momentum,
        'norm_epsilon': self._norm_epsilon
    }
    return config_dict

  @classmethod
  def from_config(cls, config, custom_objects=None):
    return cls(**config)

  @property
  def output_specs(self):
    """A dict of {level: TensorShape} pairs for the model output."""
    return self._output_specs

@factory.register_backbone_builder('spinenet')
def build_spinenet(
    input_specs: tf.keras.layers.InputSpec,
    model_config,
    l2_regularizer: tf.keras.regularizers.Regularizer = None) -> tf.keras.Model:
  """Builds ResNet 3d backbone from a config."""
  backbone_type = model_config.backbone.type
  backbone_cfg = model_config.backbone.get()
  norm_activation_config = model_config.norm_activation
  assert backbone_type == 'spinenet', (f'Inconsistent backbone type '
                                       f'{backbone_type}')

  model_id = backbone_cfg.model_id
  if model_id not in SCALING_MAP:
    raise ValueError(
        'SpineNet-{} is not a valid architecture.'.format(model_id))
  scaling_params = SCALING_MAP[model_id]

  return SpineNet(
      input_specs=input_specs,
      min_level=model_config.min_level,
      max_level=model_config.max_level,
      endpoints_num_filters=scaling_params['endpoints_num_filters'],
      resample_alpha=scaling_params['resample_alpha'],
      block_repeats=scaling_params['block_repeats'],
      filter_size_scale=scaling_params['filter_size_scale'],
      init_stochastic_depth_rate=backbone_cfg.stochastic_depth_drop_rate,
      kernel_regularizer=l2_regularizer,
      activation=norm_activation_config.activation,
      use_sync_bn=norm_activation_config.use_sync_bn,
      norm_momentum=norm_activation_config.norm_momentum,
      norm_epsilon=norm_activation_config.norm_epsilon)


'''Commandline scripts.

These scripts are called by the executables defined in setup.py.
'''

from __future__ import with_statement, print_function

import abc
import sys
from optparse import OptionParser

import rsa
import rsa.bigfile
import rsa.pkcs1

HASH_METHODS = sorted(rsa.pkcs1.HASH_METHODS.keys())

def keygen():
    '''Key generator.'''

    # Parse the CLI options
    parser = OptionParser(usage='usage: %prog [options] keysize',
            description='Generates a new RSA keypair of "keysize" bits.')

    parser.add_option('--pubout', type='string',
            help='Output filename for the public key. The public key is '
            'not saved if this option is not present. You can use '
            'pyrsa-priv2pub to create the public key file later.')

    parser.add_option('-o', '--out', type='string',
            help='Output filename for the private key. The key is '
            'written to stdout if this option is not present.')

    parser.add_option('--form',
            help='key format of the private and public keys - default PEM',
            choices=('PEM', 'DER'), default='PEM')

    (cli, cli_args) = parser.parse_args(sys.argv[1:])

    if len(cli_args) != 1:
        parser.print_help()
        raise SystemExit(1)

    try:
        keysize = int(cli_args[0])
    except ValueError:
        parser.print_help()
        print('Not a valid number: %s' % cli_args[0], file=sys.stderr)
        raise SystemExit(1)

    print('Generating %i-bit key' % keysize, file=sys.stderr)
    (pub_key, priv_key) = rsa.newkeys(keysize)

    # Save public key
    if cli.pubout:
        print('Writing public key to %s' % cli.pubout, file=sys.stderr)
        data = pub_key.save_pkcs1(format=cli.form)
        with open(cli.pubout, 'wb') as outfile:
            outfile.write(data)

    # Save private key
    data = priv_key.save_pkcs1(format=cli.form)

    if cli.out:
        print('Writing private key to %s' % cli.out, file=sys.stderr)
        with open(cli.out, 'wb') as outfile:
            outfile.write(data)
    else:
        print('Writing private key to stdout', file=sys.stderr)
        sys.stdout.write(data)

class CryptoOperation(object):
    '''CLI callable that operates with input, output, and a key.'''

    __metaclass__ = abc.ABCMeta

    keyname = 'public' # or 'private'
    usage = 'usage: %%prog [options] %(keyname)s_key'
    description = None
    operation = 'decrypt'
    operation_past = 'decrypted'
    operation_progressive = 'decrypting'
    input_help = 'Name of the file to %(operation)s. Reads from stdin if ' \
            'not specified.'
    output_help = 'Name of the file to write the %(operation_past)s file ' \
            'to. Written to stdout if this option is not present.'
    expected_cli_args = 1
    has_output = True

    key_class = rsa.PublicKey

    def __init__(self):
        self.usage = self.usage % self.__class__.__dict__
        self.input_help = self.input_help % self.__class__.__dict__
        self.output_help = self.output_help % self.__class__.__dict__

    @abc.abstractmethod
    def perform_operation(self, indata, key, cli_args=None):
        '''Performs the program's operation.

        Implement in a subclass.

        :returns: the data to write to the output.
        '''

    def __call__(self):
        '''Runs the program.'''

        (cli, cli_args) = self.parse_cli()

        key = self.read_key(cli_args[0], cli.keyform)

        indata = self.read_infile(cli.input)

        print(self.operation_progressive.title(), file=sys.stderr)
        outdata = self.perform_operation(indata, key, cli_args)

        if self.has_output:
            self.write_outfile(outdata, cli.output)

    def parse_cli(self):
        '''Parse the CLI options

        :returns: (cli_opts, cli_args)
        '''

        parser = OptionParser(usage=self.usage, description=self.description)

        parser.add_option('-i', '--input', type='string', help=self.input_help)

        if self.has_output:
            parser.add_option('-o', '--output', type='string', help=self.output_help)

        parser.add_option('--keyform',
                help='Key format of the %s key - default PEM' % self.keyname,
                choices=('PEM', 'DER'), default='PEM')

        (cli, cli_args) = parser.parse_args(sys.argv[1:])

        if len(cli_args) != self.expected_cli_args:
            parser.print_help()
            raise SystemExit(1)

        return (cli, cli_args)

    def read_key(self, filename, keyform):
        '''Reads a public or private key.'''

        print('Reading %s key from %s' % (self.keyname, filename), file=sys.stderr)
        with open(filename, 'rb') as keyfile:
            keydata = keyfile.read()

        return self.key_class.load_pkcs1(keydata, keyform)

    def read_infile(self, inname):
        '''Read the input file'''

        if inname:
            print('Reading input from %s' % inname, file=sys.stderr)
            with open(inname, 'rb') as infile:
                return infile.read()

        print('Reading input from stdin', file=sys.stderr)
        return sys.stdin.read()

    def write_outfile(self, outdata, outname):
        '''Write the output file'''

        if outname:
            print('Writing output to %s' % outname, file=sys.stderr)
            with open(outname, 'wb') as outfile:
                outfile.write(outdata)
        else:
            print('Writing output to stdout', file=sys.stderr)
            sys.stdout.write(outdata)

class EncryptOperation(CryptoOperation):
    '''Encrypts a file.'''

    keyname = 'public'
    description = ('Encrypts a file. The file must be shorter than the key '
            'length in order to be encrypted. For larger files, use the '
            'pyrsa-encrypt-bigfile command.')
    operation = 'encrypt'
    operation_past = 'encrypted'
    operation_progressive = 'encrypting'

    def perform_operation(self, indata, pub_key, cli_args=None):
        '''Encrypts files.'''

        return rsa.encrypt(indata, pub_key)

class DecryptOperation(CryptoOperation):
    '''Decrypts a file.'''

    keyname = 'private'
    description = ('Decrypts a file. The original file must be shorter than '
            'the key length in order to have been encrypted. For larger '
            'files, use the pyrsa-decrypt-bigfile command.')
    operation = 'decrypt'
    operation_past = 'decrypted'
    operation_progressive = 'decrypting'
    key_class = rsa.PrivateKey

    def perform_operation(self, indata, priv_key, cli_args=None):
        '''Decrypts files.'''

        return rsa.decrypt(indata, priv_key)

class SignOperation(CryptoOperation):
    '''Signs a file.'''

    keyname = 'private'
    usage = 'usage: %%prog [options] private_key hash_method'
    description = ('Signs a file, outputs the signature. Choose the hash '
            'method from %s' % ', '.join(HASH_METHODS))
    operation = 'sign'
    operation_past = 'signature'
    operation_progressive = 'Signing'
    key_class = rsa.PrivateKey
    expected_cli_args = 2

    output_help = ('Name of the file to write the signature to. Written '
            'to stdout if this option is not present.')

    def perform_operation(self, indata, priv_key, cli_args):
        '''Decrypts files.'''

        hash_method = cli_args[1]
        if hash_method not in HASH_METHODS:
            raise SystemExit('Invalid hash method, choose one of %s' %
                    ', '.join(HASH_METHODS))

        return rsa.sign(indata, priv_key, hash_method)

class VerifyOperation(CryptoOperation):
    '''Verify a signature.'''

    keyname = 'public'
    usage = 'usage: %%prog [options] public_key signature_file'
    description = ('Verifies a signature, exits with status 0 upon success, '
        'prints an error message and exits with status 1 upon error.')
    operation = 'verify'
    operation_past = 'verified'
    operation_progressive = 'Verifying'
    key_class = rsa.PublicKey
    expected_cli_args = 2
    has_output = False

    def perform_operation(self, indata, pub_key, cli_args):
        '''Decrypts files.'''

        signature_file = cli_args[1]

        with open(signature_file, 'rb') as sigfile:
            signature = sigfile.read()

        try:
            rsa.verify(indata, signature, pub_key)
        except rsa.VerificationError:
            raise SystemExit('Verification failed.')

        print('Verification OK', file=sys.stderr)

class BigfileOperation(CryptoOperation):
    '''CryptoOperation that doesn't read the entire file into memory.'''

    def __init__(self):
        CryptoOperation.__init__(self)

        self.file_objects = []

    def __del__(self):
        '''Closes any open file handles.'''

        for fobj in self.file_objects:
            fobj.close()

    def __call__(self):
        '''Runs the program.'''

        (cli, cli_args) = self.parse_cli()

        key = self.read_key(cli_args[0], cli.keyform)

        # Get the file handles
        infile = self.get_infile(cli.input)
        outfile = self.get_outfile(cli.output)

        # Call the operation
        print(self.operation_progressive.title(), file=sys.stderr)
        self.perform_operation(infile, outfile, key, cli_args)

    def get_infile(self, inname):
        '''Returns the input file object'''

        if inname:
            print('Reading input from %s' % inname, file=sys.stderr)
            fobj = open(inname, 'rb')
            self.file_objects.append(fobj)
        else:
            print('Reading input from stdin', file=sys.stderr)
            fobj = sys.stdin

        return fobj

    def get_outfile(self, outname):
        '''Returns the output file object'''

        if outname:
            print('Will write output to %s' % outname, file=sys.stderr)
            fobj = open(outname, 'wb')
            self.file_objects.append(fobj)
        else:
            print('Will write output to stdout', file=sys.stderr)
            fobj = sys.stdout

        return fobj

class EncryptBigfileOperation(BigfileOperation):
    '''Encrypts a file to VARBLOCK format.'''

    keyname = 'public'
    description = ('Encrypts a file to an encrypted VARBLOCK file. The file '
            'can be larger than the key length, but the output file is only '
            'compatible with Python-RSA.')
    operation = 'encrypt'
    operation_past = 'encrypted'
    operation_progressive = 'encrypting'

    def perform_operation(self, infile, outfile, pub_key, cli_args=None):
        '''Encrypts files to VARBLOCK.'''

        return rsa.bigfile.encrypt_bigfile(infile, outfile, pub_key)

class DecryptBigfileOperation(BigfileOperation):
    '''Decrypts a file in VARBLOCK format.'''

    keyname = 'private'
    description = ('Decrypts an encrypted VARBLOCK file that was encrypted '
            'with pyrsa-encrypt-bigfile')
    operation = 'decrypt'
    operation_past = 'decrypted'
    operation_progressive = 'decrypting'
    key_class = rsa.PrivateKey

    def perform_operation(self, infile, outfile, priv_key, cli_args=None):
        '''Decrypts a VARBLOCK file.'''

        return rsa.bigfile.decrypt_bigfile(infile, outfile, priv_key)

encrypt = EncryptOperation()
decrypt = DecryptOperation()
sign = SignOperation()
verify = VerifyOperation()
encrypt_bigfile = EncryptBigfileOperation()
decrypt_bigfile = DecryptBigfileOperation()


        raise NotImplementedError(
            'Must define log function to use this base class'
        )

## @package experiment_util
# Module caffe2.python.experiment_util
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals

import datetime
import time
import logging
import socket
import abc
import six

from collections import OrderedDict
from future.utils import viewkeys, viewvalues

'''
Utilities for logging experiment run stats, such as accuracy
and loss over time for different runs. Runtime arguments are stored
in the log.

Optionally, ModelTrainerLog calls out to a logger to log to
an external log destination.
'''

class ExternalLogger(object):
    six.add_metaclass(abc.ABCMeta)

    @abc.abstractmethod
    def set_runtime_args(self, runtime_args):
        """
            Set runtime arguments for the logger.
            runtime_args: dict of runtime arguments.
        """
        raise NotImplementedError(
            'Must define set_runtime_args function to use this base class'
        )

    @abc.abstractmethod
    def log(self, log_dict):
        """
            log a dict of key/values to an external destination
            log_dict: input dict
        """

class ModelTrainerLog():

    def __init__(self, expname, runtime_args, external_loggers=None):
        now = datetime.datetime.fromtimestamp(time.time())
        self.experiment_id = \
            "{}_{}".format(expname, now.strftime('%Y%m%d_%H%M%S'))
        self.filename = "{}.log".format(self.experiment_id)
        self.logstr("# %s" % str(runtime_args))
        self.headers = None
        self.start_time = time.time()
        self.last_time = self.start_time
        self.last_input_count = 0
        self.external_loggers = None

        if external_loggers is not None:
            self.external_loggers = external_loggers
            if not isinstance(runtime_args, dict):
                runtime_args = dict(vars(runtime_args))
            runtime_args['experiment_id'] = self.experiment_id
            runtime_args['hostname'] = socket.gethostname()
            for logger in self.external_loggers:
                logger.set_runtime_args(runtime_args)
        else:
            self.external_loggers = []

    def logstr(self, str):
        with open(self.filename, "a") as f:
            f.write(str + "\n")
            f.close()
        logging.getLogger("experiment_logger").info(str)

    def log(self, input_count, batch_count, additional_values):
        logdict = OrderedDict()
        delta_t = time.time() - self.last_time
        delta_count = input_count - self.last_input_count
        self.last_time = time.time()
        self.last_input_count = input_count

        logdict['time_spent'] = delta_t
        logdict['cumulative_time_spent'] = time.time() - self.start_time
        logdict['input_count'] = delta_count
        logdict['cumulative_input_count'] = input_count
        logdict['cumulative_batch_count'] = batch_count
        if delta_t > 0:
            logdict['inputs_per_sec'] = delta_count / delta_t
        else:
            logdict['inputs_per_sec'] = 0.0

        for k in sorted(viewkeys(additional_values)):
            logdict[k] = additional_values[k]

        # Write the headers if they are not written yet
        if self.headers is None:
            self.headers = list(viewkeys(logdict))
            self.logstr(",".join(self.headers))

        self.logstr(",".join(str(v) for v in viewvalues(logdict)))

        for logger in self.external_loggers:
            try:
                logger.log(logdict)
            except Exception as e:
                logging.warn(
                    "Failed to call ExternalLogger: {}".format(e), e)


    bid = bot.user.id
    present_emotes = []
    for reaction in msg.reactions:
        if reaction.emoji in GAME_EMOTES:
            present_emotes.append(reaction.emoji)
        async for emote_author in reaction.users():
            if emote_author.id != bid:
                await msg.remove_reaction(reaction.emoji, emote_author)
    for emote in GAME_EMOTES:
        if emote not in present_emotes:
            await msg.add_reaction(emote)

"""
Apex Sigma: The Database Giant Discord Bot.

"""

import secrets

import discord

from sigma.core.utilities.data_processing import user_avatar

CARDS = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'Jack', 'Queen', 'King', 'Ace']
GAME_EMOTES = ['ðµ', 'ð´', 'â«']

class BlackJack(object):
    def __init__(self, message):
        self.author = message.author
        self.channel = message.channel
        self.deck = self.make_deck()
        self.dealer_hand = self.make_hand()
        self.player_hand = self.make_hand()

    @staticmethod
    def make_deck():
        deck = CARDS * 4
        decks = deck * 6
        shuffled = []
        while decks:
            card = decks.pop(secrets.randbelow(len(decks)))
            shuffled.append(card)
        return shuffled

    def make_hand(self):
        card_a = self.deck.pop(secrets.randbelow(len(self.deck)))
        card_b = self.deck.pop(secrets.randbelow(len(self.deck)))
        return [card_a, card_b]

    @staticmethod
    def get_hand_value(hand, dealer=False):
        hand_value = 0
        non_aces = list(filter(lambda x: x != 'Ace', hand))
        for card in non_aces:
            if card.isdigit():
                hand_value += int(card)
            else:
                hand_value += 10
        aces = list(filter(lambda x: x == 'Ace', hand))
        while aces:
            if dealer and hand_value + (11 * len(aces)) == 17:
                aces.pop(0)
                hand_value += 1
            elif hand_value + (11 * len(aces)) > 21:
                aces.pop(0)
                hand_value += 1
            else:
                hand_value += (11 * len(aces))
                break
        return hand_value

    async def dealer_hit(self, game_msg):
        while self.get_hand_value(self.dealer_hand, True) < 17:
            card = self.deck.pop(secrets.randbelow(len(self.deck)))
            self.dealer_hand.append(card)
        return await send_game_msg(self.channel, game_msg, self.generate_embed(False))

    def check_dealer_bust(self):
        return self.get_hand_value(self.dealer_hand) > 21

    def check_bust(self):
        return self.get_hand_value(self.player_hand) > 21

    def check_push(self):
        player_hand_value = self.get_hand_value(self.player_hand)
        dealer_hand_value = self.get_hand_value(self.dealer_hand)
        return dealer_hand_value == player_hand_value

    def check_win(self):
        player_hand_value = self.get_hand_value(self.player_hand)
        dealer_hand_value = self.get_hand_value(self.dealer_hand)
        return dealer_hand_value > 21 or player_hand_value > dealer_hand_value

    def check_blackjack(self):
        return self.get_hand_value(self.player_hand) == 21

    def generate_embed(self, player_turn=True):
        embed = discord.Embed(color=0xDE2A42)
        embed.set_author(name=self.author.name, icon_url=user_avatar(self.author))
        if player_turn:
            dealer_str = f'Face Down, {self.dealer_hand[-1]}'
        else:
            dealer_str = ", ".join(self.dealer_hand)
        embed.description = f'**Dealer\'s Hand:** {dealer_str}\n'
        embed.description += f'**Your Hand:** {", ".join(self.player_hand)}'
        embed.set_footer(text='Emotes are hit, stand, double down.')
        return embed

    async def add_card(self, game_msg):
        card = self.deck.pop(secrets.randbelow(len(self.deck)))
        self.player_hand.append(card)
        return await send_game_msg(self.channel, game_msg, self.generate_embed())

async def send_game_msg(channel, game_msg, game_resp):
    """
    Edits the game message or resends if it an edit is not possible.
    :type channel: discord.TextChannel
    :type game_msg: discord.Message
    :type game_resp: discord.Embed
    :rtype: discord.Message
    """
    replaced = False
    if game_msg:
        try:
            await game_msg.edit(embed=game_resp)
        except discord.NotFound:
            game_msg = await channel.send(embed=game_resp)
            replaced = True
    else:
        game_msg = await channel.send(embed=game_resp)
        replaced = True
    if replaced:
        [await game_msg.add_reaction(emote) for emote in GAME_EMOTES]
    return game_msg

async def check_emotes(bot, msg):
    """
    Ensures only the correct reactions are present on the message.
    :type bot: sigma.core.sigma.ApexSigma
    :type msg: discord.Message
    """

async def set_blackjack_cd(cmd, pld):
    """
    :param cmd: The command object referenced in the command.
    :type cmd: sigma.core.mechanics.command.SigmaCommand
    :param pld: The payload with execution data and details.
    :type pld: sigma.core.mechanics.payload.CommandPayload
    """
    upgrade_file = await cmd.bot.db.get_profile(pld.msg.author.id, 'upgrades') or {}
    base_cooldown = 60
    stamina = upgrade_file.get('casino', 0)
    cooldown = int(base_cooldown - ((base_cooldown / 100) * ((stamina * 0.5) / (1.25 + (0.01 * stamina)))))
    if cooldown < 12:
        cooldown = 12
    await cmd.bot.cool_down.set_cooldown(cmd.name, pld.msg.author, cooldown)


"""
Base class for seq2seq models.
"""

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

import tensorflow as tf

from texar.models.model_base import ModelBase
from texar.losses.mle_losses import sequence_sparse_softmax_cross_entropy
from texar.data.data.paired_text_data import PairedTextData
from texar.core.optimization import get_train_op
from texar import HParams
from texar.utils import utils
from texar.utils.variables import collect_trainable_variables

__all__ = [
    "Seq2seqBase"
]

class Seq2seqBase(ModelBase):
    """Base class inherited by all seq2seq model classes.
    """

    def __init__(self, data_hparams ,hparams=None):
        ModelBase.__init__(self, hparams)

        self._data_hparams = HParams(data_hparams,
                                     PairedTextData.default_hparams())

        self._src_vocab = None
        self._tgt_vocab = None
        self._src_embedder = None
        self._tgt_embedder = None
        self._connector = None
        self._encoder = None
        self._decoder = None

    @staticmethod
    def default_hparams():
        """Returns a dictionary of hyperparameters with default values.
        """
        hparams = ModelBase.default_hparams()
        hparams.update({
            "name": "seq2seq",
            "source_embedder": "WordEmbedder",
            "source_embedder_hparams": {},
            "target_embedder": "WordEmbedder",
            "target_embedder_hparams": {},
            "embedder_share": True,
            "embedder_hparams_share": True,
            "encoder": "UnidirectionalRNNEncoder",
            "encoder_hparams": {},
            "decoder": "BasicRNNDecoder",
            "decoder_hparams": {},
            "decoding_strategy_train": "train_greedy",
            "decoding_strategy_infer": "infer_greedy",
            "beam_search_width": 0,
            "connector": "MLPTransformConnector",
            "connector_hparams": {},
            "optimization": {}
        })
        return hparams

    def _build_vocab(self):
        self._src_vocab, self._tgt_vocab = PairedTextData.make_vocab(
            self._data_hparams.source_dataset,
            self._data_hparams.target_dataset)

    def _build_embedders(self):
        kwargs = {
            "vocab_size": self._src_vocab.size,
            "hparams": self._hparams.source_embedder_hparams.todict()
        }
        self._src_embedder = utils.check_or_get_instance(
            self._hparams.source_embedder, kwargs,
            ["texar.modules", "texar.custom"])

        if self._hparams.embedder_share:
            self._tgt_embedder = self._src_embedder
        else:
            kwargs = {
                "vocab_size": self._tgt_vocab.size,
            }
            if self._hparams.embedder_hparams_share:
                kwargs["hparams"] = \
                        self._hparams.source_embedder_hparams.todict()
            else:
                kwargs["hparams"] = \
                        self._hparams.target_embedder_hparams.todict()
            self._tgt_embedder = utils.check_or_get_instance(
                self._hparams.target_embedder, kwargs,
                ["texar.modules", "texar.custom"])

    def _build_encoder(self):
        kwargs = {
            "hparams": self._hparams.encoder_hparams.todict()
        }
        self._encoder = utils.check_or_get_instance(
            self._hparams.encoder, kwargs,
            ["texar.modules", "texar.custom"])

    def _build_decoder(self):
        raise NotImplementedError

    def _build_connector(self):
        kwargs = {
            "output_size": self._decoder.state_size,
            "hparams": self._hparams.connector_hparams.todict()
        }
        self._connector = utils.check_or_get_instance(
            self._hparams.connector, kwargs,
            ["texar.modules", "texar.custom"])

    def get_loss(self, decoder_results, features, labels):
        """Computes the training loss.
        """
        return sequence_sparse_softmax_cross_entropy(
            labels=labels['target_text_ids'][:, 1:],
            logits=decoder_results['outputs'].logits,
            sequence_length=decoder_results['sequence_length'])

    def _get_predictions(self, decoder_results, features, labels, loss=None):
        raise NotImplementedError

    def _get_train_op(self, loss):
        varlist = collect_trainable_variables(
            [self._src_embedder, self._tgt_embedder, self._encoder,
             self._connector, self._decoder])
        return get_train_op(
            loss, variables=varlist, hparams=self._hparams.optimization)

    def _get_eval_metric_ops(self, decoder_results, features, labels):
        return None

    def embed_source(self, features, labels, mode):
        """Embeds the inputs.
        """
        raise NotImplementedError

    def embed_target(self, features, labels, mode):
        """Embeds the target inputs. Used in training.
        """
        raise NotImplementedError

    def encode(self, features, labels, mode):
        """Encodes the inputs.
        """

    def _connect(self, encoder_results, features, labels, mode):
        """Transforms encoder final state into decoder initial state.
        """
        raise NotImplementedError

    def decode(self, encoder_results, features, labels, mode):
        """Decodes.
        """
        raise NotImplementedError

    def _build(self, features, labels, params, mode, config=None):
        self._build_vocab()
        self._build_embedders()
        self._build_encoder()
        self._build_decoder()
        self._build_connector()

        encoder_results = self.encode(features, labels, mode)
        decoder_results = self.decode(encoder_results, features, labels, mode)

        loss, train_op, preds, eval_metric_ops = None, None, None, None
        if mode == tf.estimator.ModeKeys.PREDICT:
            preds = self._get_predictions(decoder_results, features, labels)
        else:
            loss = self.get_loss(decoder_results, features, labels)

            if mode == tf.estimator.ModeKeys.TRAIN:
                train_op = self._get_train_op(loss)
            if mode == tf.estimator.ModeKeys.EVAL:
                eval_metric_ops = self._get_eval_metric_ops(
                    decoder_results, features, labels)

            preds = self._get_predictions(decoder_results, features, labels,
                                          loss)

        return tf.estimator.EstimatorSpec(
            mode=mode,
            predictions=preds,
            loss=loss,
            train_op=train_op,
            eval_metric_ops=eval_metric_ops)

    def get_input_fn(self, mode, hparams=None):
        """Creates an input function `input_fn` that provides input data
        for the model in an :tf_main:`Estimator <estimator/Estimator>`.
        See, e.g., :tf_main:`tf.estimator.train_and_evaluate
        <estimator/train_and_evaluate>`.

        Args:
            mode: One of members in
                :tf_main:`tf.estimator.ModeKeys <estimator/ModeKeys>`.
            hparams: A `dict` or an :class:`~texar.hparams.HParams` instance
                containing the hyperparameters of
                :class:`~texar.data.PairedTextData`. See
                :meth:`~texar.data.PairedTextData.default_hparams` for the
                the structure and default values of the hyperparameters.

        Returns:
            An input function that returns a tuple `(features, labels)`
            when called.
        """
        def _input_fn():
            data = PairedTextData(hparams)

            iterator = data.dataset.make_initializable_iterator()
            tf.add_to_collection(tf.GraphKeys.TABLE_INITIALIZERS,
                                 iterator.initializer)

            batch = iterator.get_next()

            features, labels = {}, {}
            for key, value in batch.items():
                if key.startswith('source_'):
                    features[key] = value
                else:
                    labels[key] = value
            return features, labels

        return _input_fn


        lens = len(nums)
        dp = [0 for _ in range(lens)]
        if lens == 0: return 0
        if lens == 1: return nums[0]
        if lens == 2: return max(nums[0], nums[1])
        dp[0], dp[1] = nums[0], max(nums[0], nums[1])
        for i in range(2, lens, 1):
            dp[i] = max(dp[i-2] + nums[i], dp[i-1])

        return dp[-1]

'''
You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed. All houses at this place are arranged in a circle. That means the first house is the neighbor of the last one. Meanwhile, adjacent houses have security system connected and it will automatically contact the police if two adjacent houses were broken into on the same night.

Given a list of non-negative integers representing the amount of money of each house, determine the maximum amount of money you can rob tonight without alerting the police.

Example 1:

Input: [2,3,2]
Output: 3
Explanation: You cannot rob house 1 (money = 2) and then rob house 3 (money = 2),
             because they are adjacent houses.
Example 2:

Input: [1,2,3,1]
Output: 4
Explanation: Rob house 1 (money = 1) and then rob house 3 (money = 3).
             Total amount you can rob = 1 + 3 = 4.
'''

# 213. House Robber II
# 动态规划

class Solution:
    def rob(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        if not nums:
            return 0
        if len(nums) <= 2:
            return max(nums)
        nums_1 = nums[:-1]
        nums_2 = nums[1:]
        def dp_f(nums):
            lens = len(nums)
            dp = [0 for _ in range(lens)]
            dp[0], dp[1] = nums[0], max(nums[0], nums[1])
            for i in range(2, lens, 1):
                dp[i] = max(dp[i-2] + nums[i], dp[i-1])
            return dp[-1]
        return max(dp_f(nums_1), dp_f(nums_2))

class Solution2(object):
    def rob(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """

nums = [2,3,2]
test = Solution()
res = test.rob(nums)
print(res)


    ClassWithUnsafeInit._NowSafeOrigInit = ClassWithUnsafeInit.__init__
    module._NowSafeClassWithUnsafeInit = ClassWithUnsafeInit
    ClassWithUnsafeInit = _NowSafeNewClassWithUnsafeInit
    ClassWithUnsafeInitClass = _NowSafeNewClassWithUnsafeInit

"""HACK: Make module.ClassWithUnsafeInit safe."""

import module

def enable_safety():
    """HACK: Make ClassWithUnsafeInit safe.

    module.ClassWithUnsafeInit does something unsafe.

    Move the old ClassWithUnsafeInit.__init__ method out of the way--name it
    _NowSafeOrigInit.  Move the old ClassWithUnsafeInit class out of the
    way--name it _NowSafeClassWithUnsafeInit.  Create a new function,
    _NowSafeNewClassWithUnsafeInit, in place of the ClassWithUnsafeInit class
    that instantiates _NowSafeClassWithUnsafeInit and then calls
    _NowSafeClassWithUnsafeInit._NowSafeOrigInit (thanks go to Paul Abrams for
    this second part of the hack).  Last of all, ClassWithUnsafeInit has a
    ClassWithUnsafeInitClass attribute.  Set this to
    _NowSafeNewClassWithUnsafeInit.  I accept the fact that there's probably a
    special place in programmer hell for me (and Paul).

    This must be called exactly once, and it must be called before
    ClassWithUnsafeInit is used for the first time.

    """

def _NowSafeNewClassWithUnsafeInit(*args, **kargs):
    fs = module._NowSafeClassWithUnsafeInit()
    # Do whatever you need here.
    fs._NowSafeOrigInit(*args, **kargs)
    return fs