import logging
from typing import Dict

from slither.solc_parsing.expressions.expression_parsing import parse_expression

from slither.core.variables.variable import Variable

from slither.solc_parsing.solidity_types.type_parsing import parse_type, UnknownType

from slither.core.solidity_types.elementary_type import (
    ElementaryType,
    NonElementaryType,
)
from slither.solc_parsing.exceptions import ParsingError

logger = logging.getLogger("VariableDeclarationSolcParsing")

class MultipleVariablesDeclaration(Exception):
    """
    This is raised on
    var (a,b) = ...
    It should occur only on local variable definition
    """

    pass

class VariableDeclarationSolc:
    def __init__(
        self, variable: Variable, variable_data: Dict
    ):  # pylint: disable=too-many-branches
        """
        A variable can be declared through a statement, or directly.
        If it is through a statement, the following children may contain
        the init value.
        It may be possible that the variable is declared through a statement,
        but the init value is declared at the VariableDeclaration children level
        """

        self._variable = variable
        self._was_analyzed = False
        self._elem_to_parse = None
        self._initializedNotParsed = None

        self._is_compact_ast = False

        self._reference_id = None

        if "nodeType" in variable_data:
            self._is_compact_ast = True
            nodeType = variable_data["nodeType"]
            if nodeType in [
                "VariableDeclarationStatement",
                "VariableDefinitionStatement",
            ]:
                if len(variable_data["declarations"]) > 1:
                    raise MultipleVariablesDeclaration
                init = None
                if "initialValue" in variable_data:
                    init = variable_data["initialValue"]
                self._init_from_declaration(variable_data["declarations"][0], init)
            elif nodeType == "VariableDeclaration":
                self._init_from_declaration(variable_data, variable_data.get("value", None))
            else:
                raise ParsingError("Incorrect variable declaration type {}".format(nodeType))

        else:
            nodeType = variable_data["name"]

            if nodeType in [
                "VariableDeclarationStatement",
                "VariableDefinitionStatement",
            ]:
                if len(variable_data["children"]) == 2:
                    init = variable_data["children"][1]
                elif len(variable_data["children"]) == 1:
                    init = None
                elif len(variable_data["children"]) > 2:
                    raise MultipleVariablesDeclaration
                else:
                    raise ParsingError(
                        "Variable declaration without children?" + str(variable_data)
                    )
                declaration = variable_data["children"][0]
                self._init_from_declaration(declaration, init)
            elif nodeType == "VariableDeclaration":
                self._init_from_declaration(variable_data, False)
            else:
                raise ParsingError("Incorrect variable declaration type {}".format(nodeType))

    @property
    def underlying_variable(self) -> Variable:
        return self._variable

    @property
    def reference_id(self) -> int:
        """
        Return the solc id. It can be compared with the referencedDeclaration attr
        Returns None if it was not parsed (legacy AST)
        """

    def _analyze_variable_attributes(self, attributes: Dict):
        if "visibility" in attributes:
            self._variable.visibility = attributes["visibility"]
        else:
            self._variable.visibility = "internal"

    def _init_from_declaration(self, var: Dict, init: bool):  # pylint: disable=too-many-branches
        if self._is_compact_ast:
            attributes = var
            self._typeName = attributes["typeDescriptions"]["typeString"]
        else:
            assert len(var["children"]) <= 2
            assert var["name"] == "VariableDeclaration"

            attributes = var["attributes"]
            self._typeName = attributes["type"]

        self._variable.name = attributes["name"]

        # Only for comapct ast format
        # the id can be used later if referencedDeclaration
        if "id" in var:
            self._reference_id = var["id"]

        if "constant" in attributes:
            self._variable.is_constant = attributes["constant"]

        if "mutability" in attributes:
            # Note: this checked is not needed if "constant" was already in attribute, but we keep it
            if attributes["mutability"] == "constant":
                self._variable.is_constant = True
            if attributes["mutability"] == "immutable":
                self._variable.is_immutable = True

        self._analyze_variable_attributes(attributes)

        if self._is_compact_ast:
            if var["typeName"]:
                self._elem_to_parse = var["typeName"]
            else:
                self._elem_to_parse = UnknownType(var["typeDescriptions"]["typeString"])
        else:
            if not var["children"]:
                # It happens on variable declared inside loop declaration
                try:
                    self._variable.type = ElementaryType(self._typeName)
                    self._elem_to_parse = None
                except NonElementaryType:
                    self._elem_to_parse = UnknownType(self._typeName)
            else:
                self._elem_to_parse = var["children"][0]

        if self._is_compact_ast:
            self._initializedNotParsed = init
            if init:
                self._variable.initialized = True
        else:
            if init:  # there are two way to init a var local in the AST
                assert len(var["children"]) <= 1
                self._variable.initialized = True
                self._initializedNotParsed = init
            elif len(var["children"]) in [0, 1]:
                self._variable.initialized = False
                self._initializedNotParsed = []
            else:
                assert len(var["children"]) == 2
                self._variable.initialized = True
                self._initializedNotParsed = var["children"][1]

    def analyze(self, caller_context):
        # Can be re-analyzed due to inheritance
        if self._was_analyzed:
            return
        self._was_analyzed = True

        if self._elem_to_parse:
            self._variable.type = parse_type(self._elem_to_parse, caller_context)
            self._elem_to_parse = None

        if self._variable.initialized:
            self._variable.expression = parse_expression(self._initializedNotParsed, caller_context)
            self._initializedNotParsed = None


    if isinstance(x, list) or isinstance(x, tuple):
        if len(x) == 0:
            raise Exception('can not determine dtype for empty list')
        x = x[0]

    if hasattr(x, 'dtype'):
        return x.dtype
    elif isinstance(x, float):
        return float
    elif isinstance(x, int):
        return int
    #elif isinstance(x, basestring):  ## don't try to guess correct string length; use object instead.
    else:
        return object

import numpy as np
from ...metaarray import MetaArray
from ...python2_3 import basestring, xrange

def downsample(data, n, axis=0, xvals='subsample'):
    """Downsample by averaging points together across axis.
    If multiple axes are specified, runs once per axis.
    If a metaArray is given, then the axis values can be either subsampled
    or downsampled to match.
    """
    ma = None
    if (hasattr(data, 'implements') and data.implements('MetaArray')):
        ma = data
        data = data.view(np.ndarray)

    if hasattr(axis, '__len__'):
        if not hasattr(n, '__len__'):
            n = [n]*len(axis)
        for i in range(len(axis)):
            data = downsample(data, n[i], axis[i])
        return data

    nPts = int(data.shape[axis] / n)
    s = list(data.shape)
    s[axis] = nPts
    s.insert(axis+1, n)
    sl = [slice(None)] * data.ndim
    sl[axis] = slice(0, nPts*n)
    d1 = data[tuple(sl)]
    #print d1.shape, s
    d1.shape = tuple(s)
    d2 = d1.mean(axis+1)

    if ma is None:
        return d2
    else:
        info = ma.infoCopy()
        if 'values' in info[axis]:
            if xvals == 'subsample':
                info[axis]['values'] = info[axis]['values'][::n][:nPts]
            elif xvals == 'downsample':
                info[axis]['values'] = downsample(info[axis]['values'], n)
        return MetaArray(d2, info=info)

def applyFilter(data, b, a, padding=100, bidir=True):
    """Apply a linear filter with coefficients a, b. Optionally pad the data before filtering
    and/or run the filter in both directions."""
    try:
        import scipy.signal
    except ImportError:
        raise Exception("applyFilter() requires the package scipy.signal.")

    d1 = data.view(np.ndarray)

    if padding > 0:
        d1 = np.hstack([d1[:padding], d1, d1[-padding:]])

    if bidir:
        d1 = scipy.signal.lfilter(b, a, scipy.signal.lfilter(b, a, d1)[::-1])[::-1]
    else:
        d1 = scipy.signal.lfilter(b, a, d1)

    if padding > 0:
        d1 = d1[padding:-padding]

    if (hasattr(data, 'implements') and data.implements('MetaArray')):
        return MetaArray(d1, info=data.infoCopy())
    else:
        return d1

def besselFilter(data, cutoff, order=1, dt=None, btype='low', bidir=True):
    """return data passed through bessel filter"""
    try:
        import scipy.signal
    except ImportError:
        raise Exception("besselFilter() requires the package scipy.signal.")

    if dt is None:
        try:
            tvals = data.xvals('Time')
            dt = (tvals[-1]-tvals[0]) / (len(tvals)-1)
        except:
            dt = 1.0

    b,a = scipy.signal.bessel(order, cutoff * dt, btype=btype)

    return applyFilter(data, b, a, bidir=bidir)

def butterworthFilter(data, wPass, wStop=None, gPass=2.0, gStop=20.0, order=1, dt=None, btype='low', bidir=True):
    """return data passed through bessel filter"""
    try:
        import scipy.signal
    except ImportError:
        raise Exception("butterworthFilter() requires the package scipy.signal.")

    if dt is None:
        try:
            tvals = data.xvals('Time')
            dt = (tvals[-1]-tvals[0]) / (len(tvals)-1)
        except:
            dt = 1.0

    if wStop is None:
        wStop = wPass * 2.0
    ord, Wn = scipy.signal.buttord(wPass*dt*2., wStop*dt*2., gPass, gStop)
    b,a = scipy.signal.butter(ord, Wn, btype=btype)

    return applyFilter(data, b, a, bidir=bidir)

def rollingSum(data, n):
    d1 = data.copy()
    d1[1:] += d1[:-1]  # integrate
    d2 = np.empty(len(d1) - n + 1, dtype=data.dtype)
    d2[0] = d1[n-1]  # copy first point
    d2[1:] = d1[n:] - d1[:-n]  # subtract
    return d2

def mode(data, bins=None):
    """Returns location max value from histogram."""
    if bins is None:
        bins = int(len(data)/10.)
        if bins < 2:
            bins = 2
    y, x = np.histogram(data, bins=bins)
    ind = np.argmax(y)
    mode = 0.5 * (x[ind] + x[ind+1])
    return mode

def modeFilter(data, window=500, step=None, bins=None):
    """Filter based on histogram-based mode function"""
    d1 = data.view(np.ndarray)
    vals = []
    l2 = int(window/2.)
    if step is None:
        step = l2
    i = 0
    while True:
        if i > len(data)-step:
            break
        vals.append(mode(d1[i:i+window], bins))
        i += step

    chunks = [np.linspace(vals[0], vals[0], l2)]
    for i in range(len(vals)-1):
        chunks.append(np.linspace(vals[i], vals[i+1], step))
    remain = len(data) - step*(len(vals)-1) - l2
    chunks.append(np.linspace(vals[-1], vals[-1], remain))
    d2 = np.hstack(chunks)

    if (hasattr(data, 'implements') and data.implements('MetaArray')):
        return MetaArray(d2, info=data.infoCopy())
    return d2

def denoise(data, radius=2, threshold=4):
    """Very simple noise removal function. Compares a point to surrounding points,
    replaces with nearby values if the difference is too large."""

    r2 = radius * 2
    d1 = data.view(np.ndarray)
    d2 = d1[radius:] - d1[:-radius] #a derivative
    #d4 = d2 - d3
    stdev = d2.std()
    #print "denoise: stdev of derivative:", stdev
    mask1 = d2 > stdev*threshold #where derivative is large and positive
    mask2 = d2 < -stdev*threshold #where derivative is large and negative
    maskpos = mask1[:-radius] * mask2[radius:] #both need to be true
    maskneg = mask1[radius:] * mask2[:-radius]
    mask = maskpos + maskneg
    d5 = np.where(mask, d1[:-r2], d1[radius:-radius]) #where both are true replace the value with the value from 2 points before
    d6 = np.empty(d1.shape, dtype=d1.dtype) #add points back to the ends
    d6[radius:-radius] = d5
    d6[:radius] = d1[:radius]
    d6[-radius:] = d1[-radius:]

    if (hasattr(data, 'implements') and data.implements('MetaArray')):
        return MetaArray(d6, info=data.infoCopy())
    return d6

def adaptiveDetrend(data, x=None, threshold=3.0):
    """Return the signal with baseline removed. Discards outliers from baseline measurement."""
    try:
        import scipy.signal
    except ImportError:
        raise Exception("adaptiveDetrend() requires the package scipy.signal.")

    if x is None:
        x = data.xvals(0)

    d = data.view(np.ndarray)

    d2 = scipy.signal.detrend(d)

    stdev = d2.std()
    mask = abs(d2) < stdev*threshold

    lr = scipy.stats.linregress(x[mask], d[mask])
    base = lr[1] + lr[0]*x
    d4 = d - base

    if (hasattr(data, 'implements') and data.implements('MetaArray')):
        return MetaArray(d4, info=data.infoCopy())
    return d4

def histogramDetrend(data, window=500, bins=50, threshold=3.0, offsetOnly=False):
    """Linear detrend. Works by finding the most common value at the beginning and end of a trace, excluding outliers.
    If offsetOnly is True, then only the offset from the beginning of the trace is subtracted.
    """

    d1 = data.view(np.ndarray)
    d2 = [d1[:window], d1[-window:]]
    v = [0, 0]
    for i in [0, 1]:
        d3 = d2[i]
        stdev = d3.std()
        mask = abs(d3-np.median(d3)) < stdev*threshold
        d4 = d3[mask]
        y, x = np.histogram(d4, bins=bins)
        ind = np.argmax(y)
        v[i] = 0.5 * (x[ind] + x[ind+1])

    if offsetOnly:
        d3 = data.view(np.ndarray) - v[0]
    else:
        base = np.linspace(v[0], v[1], len(data))
        d3 = data.view(np.ndarray) - base

    if (hasattr(data, 'implements') and data.implements('MetaArray')):
        return MetaArray(d3, info=data.infoCopy())
    return d3

def concatenateColumns(data):
    """Returns a single record array with columns taken from the elements in data.
    data should be a list of elements, which can be either record arrays or tuples (name, type, data)
    """

    ## first determine dtype
    dtype = []
    names = set()
    maxLen = 0
    for element in data:
        if isinstance(element, np.ndarray):
            ## use existing columns
            for i in range(len(element.dtype)):
                name = element.dtype.names[i]
                dtype.append((name, element.dtype[i]))
            maxLen = max(maxLen, len(element))
        else:
            name, type, d = element
            if type is None:
                type = suggestDType(d)
            dtype.append((name, type))
            if isinstance(d, list) or isinstance(d, np.ndarray):
                maxLen = max(maxLen, len(d))
        if name in names:
            raise Exception('Name "%s" repeated' % name)
        names.add(name)

    ## create empty array
    out = np.empty(maxLen, dtype)

    ## fill columns
    for element in data:
        if isinstance(element, np.ndarray):
            for i in range(len(element.dtype)):
                name = element.dtype.names[i]
                try:
                    out[name] = element[name]
                except:
                    print("Column:", name)
                    print("Input shape:", element.shape, element.dtype)
                    print("Output shape:", out.shape, out.dtype)
                    raise
        else:
            name, type, d = element
            out[name] = d

    return out

def suggestDType(x):
    """Return a suitable dtype for x"""

def removePeriodic(data, f0=60.0, dt=None, harmonics=10, samples=4):
    if (hasattr(data, 'implements') and data.implements('MetaArray')):
        data1 = data.asarray()
        if dt is None:
            times = data.xvals('Time')
            dt = times[1]-times[0]
    else:
        data1 = data
        if dt is None:
            raise Exception('Must specify dt for this data')

    ft = np.fft.fft(data1)

    ## determine frequencies in fft data
    df = 1.0 / (len(data1) * dt)
    freqs = np.linspace(0.0, (len(ft)-1) * df, len(ft))

    ## flatten spikes at f0 and harmonics
    for i in xrange(1, harmonics + 2):
        f = f0 * i # target frequency

        ## determine index range to check for this frequency
        ind1 = int(np.floor(f / df))
        ind2 = int(np.ceil(f / df)) + (samples-1)
        if ind1 > len(ft)/2.:
            break
        mag = (abs(ft[ind1-1]) + abs(ft[ind2+1])) * 0.5
        for j in range(ind1, ind2+1):
            phase = np.angle(ft[j])   ## Must preserve the phase of each point, otherwise any transients in the trace might lead to large artifacts.
            re = mag * np.cos(phase)
            im = mag * np.sin(phase)
            ft[j] = re + im*1j
            ft[len(ft)-j] = re - im*1j

    data2 = np.fft.ifft(ft).real

    if (hasattr(data, 'implements') and data.implements('MetaArray')):
        return metaarray.MetaArray(data2, info=data.infoCopy())
    else:
        return data2


while True:
        if i > len(data)-step:
            break
        vals.append(mode(d1[i:i+window], bins))
        i += step

import numpy as np
from ...metaarray import MetaArray
from ...python2_3 import basestring, xrange

def downsample(data, n, axis=0, xvals='subsample'):
    """Downsample by averaging points together across axis.
    If multiple axes are specified, runs once per axis.
    If a metaArray is given, then the axis values can be either subsampled
    or downsampled to match.
    """
    ma = None
    if (hasattr(data, 'implements') and data.implements('MetaArray')):
        ma = data
        data = data.view(np.ndarray)

    if hasattr(axis, '__len__'):
        if not hasattr(n, '__len__'):
            n = [n]*len(axis)
        for i in range(len(axis)):
            data = downsample(data, n[i], axis[i])
        return data

    nPts = int(data.shape[axis] / n)
    s = list(data.shape)
    s[axis] = nPts
    s.insert(axis+1, n)
    sl = [slice(None)] * data.ndim
    sl[axis] = slice(0, nPts*n)
    d1 = data[tuple(sl)]
    #print d1.shape, s
    d1.shape = tuple(s)
    d2 = d1.mean(axis+1)

    if ma is None:
        return d2
    else:
        info = ma.infoCopy()
        if 'values' in info[axis]:
            if xvals == 'subsample':
                info[axis]['values'] = info[axis]['values'][::n][:nPts]
            elif xvals == 'downsample':
                info[axis]['values'] = downsample(info[axis]['values'], n)
        return MetaArray(d2, info=info)

def applyFilter(data, b, a, padding=100, bidir=True):
    """Apply a linear filter with coefficients a, b. Optionally pad the data before filtering
    and/or run the filter in both directions."""
    try:
        import scipy.signal
    except ImportError:
        raise Exception("applyFilter() requires the package scipy.signal.")

    d1 = data.view(np.ndarray)

    if padding > 0:
        d1 = np.hstack([d1[:padding], d1, d1[-padding:]])

    if bidir:
        d1 = scipy.signal.lfilter(b, a, scipy.signal.lfilter(b, a, d1)[::-1])[::-1]
    else:
        d1 = scipy.signal.lfilter(b, a, d1)

    if padding > 0:
        d1 = d1[padding:-padding]

    if (hasattr(data, 'implements') and data.implements('MetaArray')):
        return MetaArray(d1, info=data.infoCopy())
    else:
        return d1

def besselFilter(data, cutoff, order=1, dt=None, btype='low', bidir=True):
    """return data passed through bessel filter"""
    try:
        import scipy.signal
    except ImportError:
        raise Exception("besselFilter() requires the package scipy.signal.")

    if dt is None:
        try:
            tvals = data.xvals('Time')
            dt = (tvals[-1]-tvals[0]) / (len(tvals)-1)
        except:
            dt = 1.0

    b,a = scipy.signal.bessel(order, cutoff * dt, btype=btype)

    return applyFilter(data, b, a, bidir=bidir)

def butterworthFilter(data, wPass, wStop=None, gPass=2.0, gStop=20.0, order=1, dt=None, btype='low', bidir=True):
    """return data passed through bessel filter"""
    try:
        import scipy.signal
    except ImportError:
        raise Exception("butterworthFilter() requires the package scipy.signal.")

    if dt is None:
        try:
            tvals = data.xvals('Time')
            dt = (tvals[-1]-tvals[0]) / (len(tvals)-1)
        except:
            dt = 1.0

    if wStop is None:
        wStop = wPass * 2.0
    ord, Wn = scipy.signal.buttord(wPass*dt*2., wStop*dt*2., gPass, gStop)
    b,a = scipy.signal.butter(ord, Wn, btype=btype)

    return applyFilter(data, b, a, bidir=bidir)

def rollingSum(data, n):
    d1 = data.copy()
    d1[1:] += d1[:-1]  # integrate
    d2 = np.empty(len(d1) - n + 1, dtype=data.dtype)
    d2[0] = d1[n-1]  # copy first point
    d2[1:] = d1[n:] - d1[:-n]  # subtract
    return d2

def mode(data, bins=None):
    """Returns location max value from histogram."""
    if bins is None:
        bins = int(len(data)/10.)
        if bins < 2:
            bins = 2
    y, x = np.histogram(data, bins=bins)
    ind = np.argmax(y)
    mode = 0.5 * (x[ind] + x[ind+1])
    return mode

def modeFilter(data, window=500, step=None, bins=None):
    """Filter based on histogram-based mode function"""
    d1 = data.view(np.ndarray)
    vals = []
    l2 = int(window/2.)
    if step is None:
        step = l2
    i = 0

    chunks = [np.linspace(vals[0], vals[0], l2)]
    for i in range(len(vals)-1):
        chunks.append(np.linspace(vals[i], vals[i+1], step))
    remain = len(data) - step*(len(vals)-1) - l2
    chunks.append(np.linspace(vals[-1], vals[-1], remain))
    d2 = np.hstack(chunks)

    if (hasattr(data, 'implements') and data.implements('MetaArray')):
        return MetaArray(d2, info=data.infoCopy())
    return d2

def denoise(data, radius=2, threshold=4):
    """Very simple noise removal function. Compares a point to surrounding points,
    replaces with nearby values if the difference is too large."""

    r2 = radius * 2
    d1 = data.view(np.ndarray)
    d2 = d1[radius:] - d1[:-radius] #a derivative
    #d4 = d2 - d3
    stdev = d2.std()
    #print "denoise: stdev of derivative:", stdev
    mask1 = d2 > stdev*threshold #where derivative is large and positive
    mask2 = d2 < -stdev*threshold #where derivative is large and negative
    maskpos = mask1[:-radius] * mask2[radius:] #both need to be true
    maskneg = mask1[radius:] * mask2[:-radius]
    mask = maskpos + maskneg
    d5 = np.where(mask, d1[:-r2], d1[radius:-radius]) #where both are true replace the value with the value from 2 points before
    d6 = np.empty(d1.shape, dtype=d1.dtype) #add points back to the ends
    d6[radius:-radius] = d5
    d6[:radius] = d1[:radius]
    d6[-radius:] = d1[-radius:]

    if (hasattr(data, 'implements') and data.implements('MetaArray')):
        return MetaArray(d6, info=data.infoCopy())
    return d6

def adaptiveDetrend(data, x=None, threshold=3.0):
    """Return the signal with baseline removed. Discards outliers from baseline measurement."""
    try:
        import scipy.signal
    except ImportError:
        raise Exception("adaptiveDetrend() requires the package scipy.signal.")

    if x is None:
        x = data.xvals(0)

    d = data.view(np.ndarray)

    d2 = scipy.signal.detrend(d)

    stdev = d2.std()
    mask = abs(d2) < stdev*threshold

    lr = scipy.stats.linregress(x[mask], d[mask])
    base = lr[1] + lr[0]*x
    d4 = d - base

    if (hasattr(data, 'implements') and data.implements('MetaArray')):
        return MetaArray(d4, info=data.infoCopy())
    return d4

def histogramDetrend(data, window=500, bins=50, threshold=3.0, offsetOnly=False):
    """Linear detrend. Works by finding the most common value at the beginning and end of a trace, excluding outliers.
    If offsetOnly is True, then only the offset from the beginning of the trace is subtracted.
    """

    d1 = data.view(np.ndarray)
    d2 = [d1[:window], d1[-window:]]
    v = [0, 0]
    for i in [0, 1]:
        d3 = d2[i]
        stdev = d3.std()
        mask = abs(d3-np.median(d3)) < stdev*threshold
        d4 = d3[mask]
        y, x = np.histogram(d4, bins=bins)
        ind = np.argmax(y)
        v[i] = 0.5 * (x[ind] + x[ind+1])

    if offsetOnly:
        d3 = data.view(np.ndarray) - v[0]
    else:
        base = np.linspace(v[0], v[1], len(data))
        d3 = data.view(np.ndarray) - base

    if (hasattr(data, 'implements') and data.implements('MetaArray')):
        return MetaArray(d3, info=data.infoCopy())
    return d3

def concatenateColumns(data):
    """Returns a single record array with columns taken from the elements in data.
    data should be a list of elements, which can be either record arrays or tuples (name, type, data)
    """

    ## first determine dtype
    dtype = []
    names = set()
    maxLen = 0
    for element in data:
        if isinstance(element, np.ndarray):
            ## use existing columns
            for i in range(len(element.dtype)):
                name = element.dtype.names[i]
                dtype.append((name, element.dtype[i]))
            maxLen = max(maxLen, len(element))
        else:
            name, type, d = element
            if type is None:
                type = suggestDType(d)
            dtype.append((name, type))
            if isinstance(d, list) or isinstance(d, np.ndarray):
                maxLen = max(maxLen, len(d))
        if name in names:
            raise Exception('Name "%s" repeated' % name)
        names.add(name)

    ## create empty array
    out = np.empty(maxLen, dtype)

    ## fill columns
    for element in data:
        if isinstance(element, np.ndarray):
            for i in range(len(element.dtype)):
                name = element.dtype.names[i]
                try:
                    out[name] = element[name]
                except:
                    print("Column:", name)
                    print("Input shape:", element.shape, element.dtype)
                    print("Output shape:", out.shape, out.dtype)
                    raise
        else:
            name, type, d = element
            out[name] = d

    return out

def suggestDType(x):
    """Return a suitable dtype for x"""
    if isinstance(x, list) or isinstance(x, tuple):
        if len(x) == 0:
            raise Exception('can not determine dtype for empty list')
        x = x[0]

    if hasattr(x, 'dtype'):
        return x.dtype
    elif isinstance(x, float):
        return float
    elif isinstance(x, int):
        return int
    #elif isinstance(x, basestring):  ## don't try to guess correct string length; use object instead.
    else:
        return object

def removePeriodic(data, f0=60.0, dt=None, harmonics=10, samples=4):
    if (hasattr(data, 'implements') and data.implements('MetaArray')):
        data1 = data.asarray()
        if dt is None:
            times = data.xvals('Time')
            dt = times[1]-times[0]
    else:
        data1 = data
        if dt is None:
            raise Exception('Must specify dt for this data')

    ft = np.fft.fft(data1)

    ## determine frequencies in fft data
    df = 1.0 / (len(data1) * dt)
    freqs = np.linspace(0.0, (len(ft)-1) * df, len(ft))

    ## flatten spikes at f0 and harmonics
    for i in xrange(1, harmonics + 2):
        f = f0 * i # target frequency

        ## determine index range to check for this frequency
        ind1 = int(np.floor(f / df))
        ind2 = int(np.ceil(f / df)) + (samples-1)
        if ind1 > len(ft)/2.:
            break
        mag = (abs(ft[ind1-1]) + abs(ft[ind2+1])) * 0.5
        for j in range(ind1, ind2+1):
            phase = np.angle(ft[j])   ## Must preserve the phase of each point, otherwise any transients in the trace might lead to large artifacts.
            re = mag * np.cos(phase)
            im = mag * np.sin(phase)
            ft[j] = re + im*1j
            ft[len(ft)-j] = re - im*1j

    data2 = np.fft.ifft(ft).real

    if (hasattr(data, 'implements') and data.implements('MetaArray')):
        return metaarray.MetaArray(data2, info=data.infoCopy())
    else:
        return data2


from __future__ import (
    absolute_import,
    division,
    unicode_literals,
)

import abc
from copy import deepcopy
import logging
import math
import random
import re
import time
from typing import (
    Any,
    Dict,
    FrozenSet,
    Hashable,
    List,
    Optional,
    Tuple,
    Union,
    cast,
)

import attr
from pymetrics.instruments import (
    Counter,
    Histogram,
    Timer,
    TimerResolution,
)
from pymetrics.recorders.base import MetricsRecorder
from pymetrics.recorders.noop import noop_metrics
import redis
import six

from pysoa.common.logging import RecursivelyCensoredDictWrapper
from pysoa.common.serializer.base import Serializer
from pysoa.common.serializer.msgpack_serializer import MsgpackSerializer
from pysoa.common.transport.base import ReceivedMessage
from pysoa.common.transport.errors import (
    InvalidMessageError,
    MessageReceiveError,
    MessageReceiveTimeout,
    MessageSendError,
    MessageTooLarge,
)
from pysoa.common.transport.redis_gateway.backend.base import (
    BaseRedisClient,
    CannotGetConnectionError,
)
from pysoa.common.transport.redis_gateway.backend.sentinel import SentinelRedisClient
from pysoa.common.transport.redis_gateway.backend.standard import StandardRedisClient
from pysoa.common.transport.redis_gateway.constants import (
    DEFAULT_MAXIMUM_MESSAGE_BYTES_CLIENT,
    DEFAULT_MAXIMUM_MESSAGE_BYTES_SERVER,
    MINIMUM_CHUNKED_MESSAGE_BYTES,
    REDIS_BACKEND_TYPE_SENTINEL,
    REDIS_BACKEND_TYPES,
    ProtocolFeature,
    ProtocolVersion,
)
from pysoa.utils import dict_to_hashable

__all__ = (
    'RedisTransportCore',
    'RedisTransportClientCore',
    'RedisTransportServerCore',
)

_logger = logging.getLogger(__name__)
_oversized_message_logger = logging.getLogger('pysoa.transport.oversized_message')

def _valid_backend_type(_, __, value):
    if not value or value not in REDIS_BACKEND_TYPES:
        raise ValueError('backend_type must be one of {}, got {}'.format(REDIS_BACKEND_TYPES, value))

def _valid_chunk_threshold(_, __, value):
    if 0 <= value < MINIMUM_CHUNKED_MESSAGE_BYTES:
        # Negative values are fine; that just means "disabled."
        raise ValueError(
            'If chunk_messages_larger_than_bytes is enabled (non-negative), it must be >= {}, '
            'got {}'.format(MINIMUM_CHUNKED_MESSAGE_BYTES, value),
        )

_DEFAULT_METRICS_RECORDER = noop_metrics  # type: MetricsRecorder

@attr.s
@six.add_metaclass(abc.ABCMeta)
class RedisTransportCore(object):
    """Handles communication with Redis."""

    # The backend layer holds Redis connections and should be reused as much as possible to reduce Redis connections.
    # Given identical input settings, two given backend layer instances will operate identically, and so we cash using
    # input variables as a key. This applies even across services--backend layers have no service-specific code, so
    # a single backend can be used for multiple services if those services' backend settings are the same.
    _backend_layer_cache = {}  # type: Dict[Tuple[six.text_type, FrozenSet[Tuple[Hashable, ...]]], BaseRedisClient]

    SUPPORTED_HEADERS_RE = re.compile(
        b'\\s*(?P<header_name>content-type|chunk-count|chunk-id)\\s*:\\s*(?P<header_value>[a-zA-Z0-9_/.-]+)\\s*;',
    )

    backend_type = attr.ib(validator=_valid_backend_type)  # type: six.text_type

    backend_layer_kwargs = attr.ib(
        # Keyword args for the backend layer (Standard Redis and Sentinel Redis modes)
        default=attr.Factory(dict),
        validator=attr.validators.instance_of(dict),
    )  # type: Dict[six.text_type, Any]

    log_messages_larger_than_bytes = attr.ib(
        default=DEFAULT_MAXIMUM_MESSAGE_BYTES_CLIENT,
        converter=int,
    )  # type: int

    maximum_message_size_in_bytes = attr.ib(
        default=DEFAULT_MAXIMUM_MESSAGE_BYTES_CLIENT,
        converter=int,
    )  # type: int

    chunk_messages_larger_than_bytes = -1

    message_expiry_in_seconds = attr.ib(
        # How long after a message is sent before it's considered "expired" and not received by default, unless
        # overridden in the send_message argument `message_expiry_in_seconds`
        default=60,
        converter=int,
    )  # type: int

    metrics = attr.ib(
        default=_DEFAULT_METRICS_RECORDER,
        validator=attr.validators.instance_of(MetricsRecorder),  # type: ignore
    )  # type: MetricsRecorder

    queue_capacity = attr.ib(
        # The capacity for queues to which messages are sent
        default=10000,
        converter=int,
    )  # type: int

    queue_full_retries = attr.ib(
        # Number of times to retry when the send queue is full
        default=10,
        converter=int,
    )  # type: int

    receive_timeout_in_seconds = attr.ib(
        # How long to block when waiting to receive a message by default, unless overridden in the receive_message
        # argument `receive_timeout_in_seconds`
        default=5,
        converter=int,
    )  # type: int

    default_serializer_config = attr.ib(
        # Configuration for which serializer should be used by this transport
        default={'object': MsgpackSerializer, 'kwargs': {}},
    )  # type: Dict[six.text_type, Any]

    service_name = attr.ib(
        # Service name used for error messages
        default='',
        validator=attr.validators.instance_of(six.text_type),
    )  # type: six.text_type

    protocol_version = ProtocolVersion.VERSION_3

    EXPONENTIAL_BACK_OFF_FACTOR = 4.0
    QUEUE_NAME_PREFIX = 'pysoa:'
    GLOBAL_QUEUE_SPECIFIER = '!'

    def __attrs_post_init__(self):
        # set the hosts property after all attrs are validated
        if self.backend_layer_kwargs.get('hosts'):
            final_hosts = []
            for host in self.backend_layer_kwargs['hosts']:
                if isinstance(host, tuple) and len(host) == 2:
                    final_hosts.append(host)
                elif isinstance(host, six.string_types):
                    final_hosts.append((host, self.backend_layer_kwargs.get('redis_port', 6379)))
                else:
                    raise ValueError("connection_kwargs['hosts'] must be a list of tuples of (host, port), or strings")
            self.backend_layer_kwargs['hosts'] = final_hosts

        if self.backend_layer_kwargs.get('redis_db') is not None:
            self.backend_layer_kwargs.setdefault('connection_kwargs', {})['db'] = self.backend_layer_kwargs['redis_db']

        self.backend_layer_kwargs.pop('redis_db', None)
        self.backend_layer_kwargs.pop('redis_port', None)

        self._backend_layer = None  # type: Optional[BaseRedisClient]
        self._default_serializer = None  # type: Optional[Serializer]

    @property
    @abc.abstractmethod
    def is_server(self):  # type: () -> bool
        """
        Indicates whether this is a server or a client.
        """

    # noinspection PyAttributeOutsideInit
    @property
    def backend_layer(self):  # type: () -> BaseRedisClient
        if self._backend_layer is None:
            cache_key = (self.backend_type, dict_to_hashable(cast(Dict[Hashable, Any], self.backend_layer_kwargs)))
            if cache_key not in self._backend_layer_cache:
                with self._get_timer('backend.initialize'):
                    backend_layer_kwargs = deepcopy(self.backend_layer_kwargs)
                    if self.backend_type == REDIS_BACKEND_TYPE_SENTINEL:
                        self._backend_layer_cache[cache_key] = SentinelRedisClient(**backend_layer_kwargs)
                    else:
                        self._backend_layer_cache[cache_key] = StandardRedisClient(**backend_layer_kwargs)

            self._backend_layer = self._backend_layer_cache[cache_key]

        # Each time the backend layer is accessed, use _this_ transport's metrics recorder for the backend layer
        self._backend_layer.metrics_counter_getter = self._get_counter
        return self._backend_layer

    # noinspection PyAttributeOutsideInit
    @property
    def default_serializer(self):  # type: () -> Serializer
        if self._default_serializer is None:
            self._default_serializer = cast(
                Serializer,
                self.default_serializer_config['object'](**self.default_serializer_config.get('kwargs', {})),
            )

        return self._default_serializer

    def _get_redis_connection(self, for_send, queue_key):
        try:
            with self._get_timer('{}.get_redis_connection'.format('send' if for_send else 'receive')):
                return self.backend_layer.get_connection(queue_key)
        except CannotGetConnectionError as e:
            self._get_counter('{}.error.connection'.format('send' if for_send else 'receive')).increment()
            raise (MessageSendError if for_send else MessageReceiveError)('Cannot get connection: {}'.format(e.args[0]))

    def _serialize_check_and_chunk_message(
        self,
        protocol_version,  # type: ProtocolVersion
        message,  # type: Dict[six.text_type, Any]
        serializer,  # type: Serializer
    ):
        with self._get_timer('send.serialize'):
            serialized_message = serializer.dict_to_blob(message)

            message_size_in_bytes = len(serialized_message)
            self._get_histogram('send.message_size').set(message_size_in_bytes)

            if message_size_in_bytes > self.maximum_message_size_in_bytes:
                self._get_counter('send.error.message_too_large').increment()
                raise MessageTooLarge(message_size_in_bytes, 'Message exceeds maximum message size')

            if self.log_messages_larger_than_bytes and message_size_in_bytes > self.log_messages_larger_than_bytes:
                _oversized_message_logger.warning(
                    'Oversized message sent for PySOA service {}'.format(self.service_name),
                    extra={'data': {
                        'message': RecursivelyCensoredDictWrapper(message),
                        'serialized_length_in_bytes': message_size_in_bytes,
                        'threshold': self.log_messages_larger_than_bytes,
                    }},
                )

            content_type_header = 'content-type:{};'.format(serializer.mime_type).encode('utf-8')

            if self.is_server and 0 < self.chunk_messages_larger_than_bytes < message_size_in_bytes:
                # chunking is enabled on the server and the message is big enough to chunk
                if not ProtocolFeature.CHUNKED_RESPONSES.supported_in(protocol_version):
                    self._get_counter('send.error.message_too_large').increment()
                    raise MessageTooLarge(
                        message_size_in_bytes,
                        'Message exceeds chunking threshold but client does not support chunking',
                    )

                chunk_count = int(math.ceil(message_size_in_bytes / self.chunk_messages_larger_than_bytes))
                self._get_histogram('send.chunk_count').set(chunk_count)
                headers = protocol_version.prefix + content_type_header + (b'chunk-count:%d;' % (chunk_count, ))
                return [
                    headers + (b'chunk-id:%d;' % (i + 1, )) + serialized_message[
                         i * self.chunk_messages_larger_than_bytes:
                         (i + 1) * self.chunk_messages_larger_than_bytes
                    ]
                    for i in range(chunk_count)
                ]

            if ProtocolFeature.CONTENT_TYPE_HEADER.supported_in(protocol_version):
                serialized_message = content_type_header + serialized_message
            if ProtocolFeature.VERSION_MARKER.supported_in(protocol_version):
                serialized_message = protocol_version.prefix + serialized_message
            return [serialized_message]

    def send_message(
        self,
        queue_name,  # type: six.text_type
        request_id,  # type: int
        meta,  # type: Dict[six.text_type, Any]
        body,  # type: Dict[six.text_type, Any]
        message_expiry_in_seconds=None,  # type: Optional[int]
    ):
        """
        Send a message to the specified queue in Redis.

        :param queue_name: The name of the queue to which to send the message
        :param request_id: The message's request ID
        :param meta: The message meta information, if any (should be an empty dict if no metadata)
        :param body: The message body (should be a dict)
        :param message_expiry_in_seconds: The optional message expiry, which defaults to the setting with the same name

        :raise: InvalidMessageError, MessageTooLarge, MessageSendError
        """
        if request_id is None:
            raise InvalidMessageError('No request ID')

        if message_expiry_in_seconds:
            message_expiry = time.time() + message_expiry_in_seconds
            redis_expiry = message_expiry_in_seconds + 10
        else:
            message_expiry = time.time() + self.message_expiry_in_seconds
            redis_expiry = self.message_expiry_in_seconds

        meta['__expiry__'] = message_expiry
        protocol_version = meta.pop('protocol_version', self.protocol_version)  # type: ProtocolVersion

        message = {'request_id': request_id, 'meta': meta, 'body': body}

        messages_to_send = self._serialize_check_and_chunk_message(
            protocol_version,
            message,
            cast(Serializer, meta.pop('serializer', self.default_serializer)),
        )

        queue_key = self.QUEUE_NAME_PREFIX + queue_name

        connection = self._get_redis_connection(for_send=True, queue_key=queue_key)

        for message_to_send in messages_to_send:
            # Try at least once, up to queue_full_retries times, then error
            for i in range(-1, self.queue_full_retries):
                if i >= 0:
                    time.sleep((2 ** i + random.random()) / self.EXPONENTIAL_BACK_OFF_FACTOR)
                    self._get_counter('send.queue_full_retry').increment()
                    self._get_counter('send.queue_full_retry.retry_{}'.format(i + 1)).increment()
                try:
                    with self._get_timer('send.send_message_to_redis_queue'):
                        self.backend_layer.send_message_to_queue(
                            queue_key=queue_key,
                            message=message_to_send,
                            expiry=redis_expiry,
                            capacity=self.queue_capacity,
                            connection=connection,
                        )
                    break
                except redis.exceptions.ResponseError as e:
                    # The Lua script handles capacity checking and sends the "full" error back
                    if e.args[0] == 'queue full':
                        continue
                    if isinstance(self.backend_layer, SentinelRedisClient):
                        self.backend_layer.reset_clients()
                    self._get_counter('send.error.response').increment()
                    raise MessageSendError(
                        'Redis error sending message for service {}'.format(self.service_name), *e.args
                    )
                except Exception as e:
                    if isinstance(self.backend_layer, SentinelRedisClient):
                        self.backend_layer.reset_clients()
                    self._get_counter('send.error.unknown').increment()
                    _logger.exception('Unknown error sending message in Redis transport core')
                    raise MessageSendError(
                        'Unknown error sending message for service {}'.format(self.service_name),
                        six.text_type(type(e).__name__),
                        *e.args
                    )
            else:
                # The loop (number of retries) was exhausted; it was not terminated with break / successful send.
                self._get_counter('send.error.redis_queue_full').increment()
                raise MessageSendError(
                    'Redis queue {queue_name} was full after {retries} retries'.format(
                        queue_name=queue_name,
                        retries=self.queue_full_retries,
                    )
                )

    @classmethod
    def _extract_supported_headers(cls, serialized_message):
        headers = {}  # type: Dict[six.text_type, six.text_type]
        match = cls.SUPPORTED_HEADERS_RE.match(serialized_message)
        while match:
            headers[match.group('header_name').decode('utf-8')] = match.group('header_value').decode('utf-8')

            # We used to strip whitespace, here, from the serialized message after slicing it, but that caused
            # http://example.com, so we do not strip whitespace there anymore. It was never
            # really necessary, anyway. There should never be whitespace after the final semicolon, and the regex takes
            # care of whitespace within the headers themselves.
            serialized_message = serialized_message[match.end():]
            match = cls.SUPPORTED_HEADERS_RE.match(serialized_message)

        return headers, serialized_message

    def _receive_message(self, connection, queue_key, receive_timeout_in_seconds):
        try:
            # returns message or None if no new messages within timeout
            with self._get_timer('receive.pop_from_redis_queue'):
                result = connection.blpop([queue_key], timeout=receive_timeout_in_seconds)
            serialized_message = None  # type: Optional[six.binary_type]
            if result:
                serialized_message = cast(six.binary_type, result[1])
        except Exception as e:
            if isinstance(self.backend_layer, SentinelRedisClient):
                self.backend_layer.reset_clients()
            self._get_counter('receive.error.unknown').increment()
            raise MessageReceiveError(
                'Unknown error receiving message for service {}'.format(self.service_name),
                six.text_type(type(e).__name__),
                *e.args
            )

        if serialized_message is None:
            raise MessageReceiveTimeout('No message received for service {}'.format(self.service_name))

        return serialized_message

    def receive_message(self, queue_name, receive_timeout_in_seconds=None):
        """
        Receive a message from the specified queue in Redis.

        :param queue_name: The name of the queue to which to send the message
        :param receive_timeout_in_seconds: The optional timeout, which defaults to the setting with the same name

        :return: A tuple of request ID, message meta-information dict, and message body dict

        :raise: MessageReceiveError, MessageReceiveTimeout, InvalidMessageError
        """
        queue_key = self.QUEUE_NAME_PREFIX + queue_name
        receive_timeout_in_seconds = receive_timeout_in_seconds or self.receive_timeout_in_seconds

        connection = self._get_redis_connection(for_send=False, queue_key=queue_key)
        serialized_message = self._receive_message(connection, queue_key, receive_timeout_in_seconds)

        with self._get_timer('receive.deserialize') as deserialize_timer:
            protocol_version, serialized_message = ProtocolVersion.extract_version(serialized_message)
            headers, serialized_message = self._extract_supported_headers(serialized_message)

            serializer = self.default_serializer
            if 'content-type' in headers and headers['content-type'] in Serializer.all_supported_mime_types:
                serializer = Serializer.resolve_serializer(headers['content-type'])

        if 'chunk-count' in headers:
            if self.is_server:
                raise InvalidMessageError('Unsupported chunked request on server Redis backend')
            if 'chunk-id' not in headers:
                raise InvalidMessageError(
                    'Invalid chunked response missing chunk ID for service {}'.format(self.service_name),
                )

            chunk_headers = headers
            chunk_id, chunk_count = int(chunk_headers['chunk-id']), int(chunk_headers['chunk-count'])
            while chunk_id < chunk_count:
                expected_chunk = chunk_id + 1
                next_chunk = self._receive_message(connection, queue_key, receive_timeout_in_seconds)

                with deserialize_timer:
                    protocol_version, next_chunk = ProtocolVersion.extract_version(next_chunk)
                    chunk_headers, next_chunk = self._extract_supported_headers(next_chunk)
                    serialized_message += next_chunk

                if 'chunk-count' not in chunk_headers or 'chunk-id' not in chunk_headers:
                    raise InvalidMessageError(
                        'Invalid chunked response missing chunk headers expecting chunk {} of {} for service '
                        '{}.'.format(expected_chunk, chunk_count, self.service_name)
                    )
                if int(chunk_headers['chunk-count']) != chunk_count:
                    raise InvalidMessageError(
                        'Invalid chunked response has different chunk count {} expecting chunk {} of {} for service '
                        '{}.'.format(chunk_headers['chunk-count'], expected_chunk, chunk_count, self.service_name)
                    )
                if int(chunk_headers['chunk-id']) != expected_chunk:
                    raise InvalidMessageError(
                        'Invalid chunked response has incorrect chunk ID {} expected {} of {} for service '
                        '{}.'.format(chunk_headers['chunk-id'], expected_chunk, chunk_count, self.service_name)
                    )

                chunk_id, chunk_count = int(chunk_headers['chunk-id']), int(chunk_headers['chunk-count'])

        with deserialize_timer:
            message = serializer.blob_to_dict(serialized_message)
            message.setdefault('meta', {})['serializer'] = serializer
            message['meta']['protocol_version'] = protocol_version

        if self._is_message_expired(message):
            self._get_counter('receive.error.message_expired').increment()
            raise MessageReceiveTimeout('Message expired for service {}'.format(self.service_name))

        request_id = message.get('request_id')
        if request_id is None:
            self._get_counter('receive.error.no_request_id').increment()
            raise InvalidMessageError('No request ID for service {}'.format(self.service_name))

        return ReceivedMessage(request_id, message.get('meta', {}), message.get('body'))

    @staticmethod
    def _is_message_expired(message):  # type: (Dict[six.text_type, Any]) -> bool
        return message.get('meta', {}).get('__expiry__') and message['meta']['__expiry__'] < time.time()

    @abc.abstractmethod
    def _get_metric_name(self, name):  # type: (six.text_type) -> six.text_type
        """
        Get a suitable full metric name including appropriate client or server prefix.
        """

    def _get_counter(self, name):  # type: (six.text_type) -> Counter
        return self.metrics.counter(self._get_metric_name(name))

    def _get_histogram(self, name):  # type: (six.text_type) -> Histogram
        return self.metrics.histogram(self._get_metric_name(name))

    def _get_timer(self, name):  # type: (six.text_type) -> Timer
        return self.metrics.timer(self._get_metric_name(name), resolution=TimerResolution.MICROSECONDS)

def _convert_protocol_version(value):  # type: (Union[ProtocolVersion, int]) -> ProtocolVersion
    if isinstance(value, ProtocolVersion):
        return value
    return ProtocolVersion(value)

@attr.s
class RedisTransportClientCore(RedisTransportCore):
    protocol_version = attr.ib(
        default=ProtocolVersion.VERSION_3,
        converter=_convert_protocol_version,
    )  # type: ProtocolVersion

    @property
    def is_server(self):  # type: () -> bool
        return False

    def _get_metric_name(self, name):  # type: (six.text_type) -> six.text_type
        return 'client.transport.redis_gateway.{name}'.format(name=name)

@attr.s
class RedisTransportServerCore(RedisTransportCore):
    log_messages_larger_than_bytes = attr.ib(
        default=DEFAULT_MAXIMUM_MESSAGE_BYTES_SERVER,
        converter=int,
    )  # type: int

    maximum_message_size_in_bytes = attr.ib(
        default=DEFAULT_MAXIMUM_MESSAGE_BYTES_SERVER,
        converter=int,
    )  # type: int

    chunk_messages_larger_than_bytes = attr.ib(
        default=-1,
        converter=int,
        validator=_valid_chunk_threshold,
    )  # type: int

    def __attrs_post_init__(self):
        super(RedisTransportServerCore, self).__attrs_post_init__()

        if self.maximum_message_size_in_bytes < self.chunk_messages_larger_than_bytes * 5:
            raise ValueError(
                'If chunk_messages_larger_than_bytes is enabled (non-negative), maximum_message_size_in_bytes must '
                'be at least 5 times larger to allow for multiple chunks to be sent.',
            )

    @property
    def is_server(self):  # type: () -> bool
        return True

    def _get_metric_name(self, name):  # type: (six.text_type) -> six.text_type
        return 'server.transport.redis_gateway.{name}'.format(name=name)