"Get various variables"
import bpy
from zpy import Is, utils

# -------------------------------------------------------------------------
# region: Python Types
# -------------------------------------------------------------------------

def as_digit(text):
    try:
        return int(text)
    except:
        return float(text)

def as_float(src):
    return float(src)

def as_int(src):
    return int(Get.as_float(src))

def as_list(context, attr):
    "Returns the bpy.context.%attr% item or an empty list if it's blank"

    items = getattr(context, attr, None)
    if items is None:
        items = []

    return items

# endregion
# -------------------------------------------------------------------------

# -------------------------------------------------------------------------
# region: Debug
# -------------------------------------------------------------------------

def file(levels=0):
    import os
    from inspect import (currentframe, getframeinfo)
    cf = currentframe().f_back.f_back
    while levels:
        cff = cf.f_back
        if cff is None:
            break
        cf = cff
        levels -= 1

    return os.path.basename(getframeinfo(cf).filename)

def line(levels=0):
    from inspect import (currentframe, getframeinfo)

    cf = currentframe().f_back.f_back
    while levels:
        cff = cf.f_back
        if cff is None:
            break
        cf = cff
        levels -= 1

    return cf.f_lineno

def stack(level=0):
    from inspect import stack
    # 0 = current def
    # 2 is used to call from stack << debug << original file call

    return stack()[level][3]

# endregion
# -------------------------------------------------------------------------

# -------------------------------------------------------------------------
# region: Utils
# -------------------------------------------------------------------------

def action_fcurves(src, actions=None, raw_action=True):
    "Find actions used by a bone/object and return it's action + fcurves"

    fcurves = list()

    if actions is None:
        rig = src.id_data
        strips = Get.strips(rig, selected=False)
        anim = rig.animation_data
        an_act = getattr(anim, 'action', None) if raw_action else None
        actions = {an_act, *(s.action for s in strips)}

    for action in actions:
        if not action:
            continue
        for fc in action.fcurves:
            if any((
                    Is.object(src),
                    Is.posebone(src) and fc.data_path.find(
                        f'bones[\"{src.name}\"]') != -1,
            )):
                fcurves.append((action, fc))

    return fcurves

def active_strip(anim):
    active_strip = None

    for track in anim.nla_tracks:
        for strip in track.strips:
            if strip.active:
                return strip
            elif anim.use_tweak_mode and (strip.action == anim.action):
                active_strip = strip
                # strip isn't marked active but it likely is the correct one

    return active_strip

def animation_datas(src):
    anims = list()
    data = getattr(src, 'data', None)
    shapes = getattr(data, 'shape_keys', None)

    for x in (src, data, shapes):
        anim = getattr(x, 'animation_data', None)
        if anim:
            anims.append(anim)

    return anims

def collection_from_scene(collection, scene):
    """Find a collection from the scene or sub collections"""

    if isinstance(scene, bpy.types.Scene):
        scene = scene.collection

    if collection in scene.children.values():
        return scene
    else:
        for sub in scene.children:
            search = Get.collection_from_scene(collection, sub)
            if search:
                return search

def controls_from_driver(driver, report=None):
    bone_props = set()

    if hasattr(driver, 'driver'):
        driver = driver.driver

    for var in driver.variables:
        for tar in var.targets:
            # Should be exactly one target (no more, no less)

            control = tar.id
            prop = tar.data_path

            if not prop:
                continue

            is_bone = prop.startswith(('pose.bones', 'bones'))
            redo_obj = False

            if not hasattr(control, prop):
                redo_obj = True

            if is_bone or redo_obj:
                if tar.data_path.endswith('"]'):
                    control, prop = tar.data_path.rsplit('["', 1)
                    prop = '["' + prop
                else:
                    control, prop = tar.data_path.rsplit('.', 1)

            try:
                if is_bone or redo_obj:
                    control = tar.id.path_resolve(control)
                bone_props.add((control, prop))
            except:
                # invalid driver?
                if report:
                    report({'ERROR'}, ("Invalid driver:" + repr(driver)))

    return list(bone_props)

def constraint_target(con):
    if hasattr(con, 'target'):
        target = con.target
        subtarget = getattr(con, 'subtarget', None)
    elif hasattr(con, 'targets'):
        targets = list()
        for subcon in con.targets:
            target = subcon.target
            subtarget = subcon.subtarget

            if target and subtarget and hasattr(target, 'pose'):
                target = target.pose.bones.get(subtarget)
            targets.append(target)

        return targets
    else:
        target = subtarget = None

    if target and subtarget and hasattr(target, 'pose'):
        target = target.pose.bones.get(subtarget)

    return target

def bone_group(src, name=""):
    """
    Returns a named group inside rig.
    if src is a posebone, will return the bone's group instead.
        If the bone doesn't have a group, will try to return the named group instead.
    """

    group = None

    if Is.posebone(src):
        group = src.bone_group

    if not group:
        group = src.id_data.pose.bone_groups.get(name)

    return group

def copy(src):
    ""

    return type("Duplicate", (), {p: getattr(src, p) for p in dir(src)})

def distance(point1, point2) -> float:
    """Calculate distance between two points in 3D."""
    from math import sqrt
    return sqrt(sum(
        (x[1] - x[0]) ** 2
        for x in zip(point1, point2)
    ))

def driver(src, prop, index=-1):
    ""

    drivers = getattr(src.id_data.animation_data, 'drivers', None)
    if drivers:
        if hasattr(src, prop):
            prop = src.path_from_id(prop)
        driver = drivers.find(prop, index=index)
    else:
        driver = None

    return driver

def event(event=None):
    "returns a fake event if not supplied with a real one"

    if isinstance(event, bpy.types.Event):
        return event

    class event:
        __module__ = 'bpy.types'
        __slots__ = ()
        # bl_rna =  <bpy_struct, Struct("Event")>
        # rna_type =  <bpy_struct, Struct("Event")>
        alt = False
        ctrl = False
        oskey = False
        shift = False
        type = 'NONE'
        value = 'NOTHING'
        ascii = ''
        unicode = ''
        is_mouse_absolute = False
        is_tablet = False
        mouse_prev_x = 0
        mouse_prev_y = 0
        mouse_region_x = -1
        mouse_region_y = -1
        mouse_x = 0
        mouse_y = 0
        pressure = 1.0
        tilt = Vector((0.0, 0.0))

    return event

def fcurve(action, path, index=-1):
    """Find if a specified path exists in an action"""
    fcs = action.fcurves
    if index == -1:
        fc = fcs.find(path)
    else:
        fc = fcs.find(path, index=index)

    # if fc and index != -1 and fc.array_index != index:
        # for fc in fcs:
            # if fc.data_path == path and fc.array_index == index:
                # break
        # else:
            # fc = None

    return fc

def icon(*args):
    """
    args:
        name_to_search, bool
        Off_Icon, On_Icon, bool(s)
    """
    if len(args) > 2:
        off, on, *var = args
        value = all(var)
        return (off, on)[value]
    else:
        name, value = args
        name = name.lower()
        value = bool(value)

    if name in {'disclosure', 'disclosure_tri_right'}:
        icon = 'DISCLOSURE_TRI_' + ('RIGHT', 'DOWN')[value]
    else:
        icon = 'BLANK1'

    return icon

def icon_from_type(src):
    "return an Icon id for the specified item's type"

    if Is.object(src):
        if src.type == 'LIGHT_PROBE':
            icon = 'LIGHTPROBE_' + src.data.type
        else:
            icon = src.type + '_DATA'
    elif Is.bone(src) or Is.editbone(src) or Is.posebone(src):
        icon = 'BONE_DATA'
    else:
        icon = 'ERROR'
        utils.debug("Can't find icon type for ", src, type(src))

    return icon

def ik_chain(bone, constraint=None):
    "return list of parent bones connected to the bone through an IK chain"

    parents = list()

    if not (Is.posebone(bone) and bone.parent):
        return parents

    maxlen = len(bone.parent_recursive)

    def scan(constraint):
        if (constraint.type in ('IK', 'SPLINE_IK')):
            count = constraint.chain_count
            if (count == 0 or count > maxlen):
                parents.extend(bone.parent_recursive)
            else:
                if getattr(constraint, 'use_tail', True):
                    # Spline IK starts count from the bone with the constraint
                    chain_range = count - 1
                else:
                    chain_range = count
                for chain in range(chain_range):
                    parents.append(bone.parent_recursive[chain])

    if constraint is None:
        for constraint in bone.constraints:
            scan(constraint)
    else:
        scan(constraint)

    parents_sorted = list()
    if parents:
        for parent in bone.parent_recursive:
            if parent in parents:
                parents_sorted.append(parent)

    return parents_sorted

def macro(*ops, poll=None, **kargs):
    """Get an operator to run a sequence of operators in succession
        if they aren't cancelled"""

    idname = kargs.get('idname', '_macro_._macro_start_')
    # For defaults, insert ops as operator or bl_idname
    # For operators with props:
    # ops = (bl_idname, dict(prop=var, ))

    class MACRO(bpy.types.Macro):
        bl_idname = idname
        bl_label = "Start Macro"
        bl_options = {'MACRO'}

        @classmethod
        def poll(self, context):
            if poll:
                return poll(self, context)
            return True
    bpy.utils.register_class(MACRO)
    # bpy.macro = MACRO

    for op in ops:
        if Is.iterable(op) and not Is.string(op):
            (op, props) = (op[0], dict(*op[1:]))
        else:
            props = dict()

        if hasattr(op, 'bl_idname'):
            op = op.bl_idname

        if Is.string(op):
            if op.startswith('bpy.ops.'):
                op = eval(op)
            else:
                op = eval('bpy.ops.' + op)
        operator = MACRO.define(op.idname())
        for prop in props:
            setattr(operator.properties, prop, props[prop])

    return eval('bpy.ops.' + idname)

def matrix(src, local=False, basis=False, tail=False, copy=True):
    # editbone.     matrix
    # bone.         matrix, matrix_local
    # posebone.     matrix, matrix_basis, matrix_channel
    # object.       matrix_world, matrix_basis, matrix_local, matrix_parent_inverse

    if Is.posebone(src):
        if copy:
            matrix = utils.multiply_matrix(
                src.id_data.matrix_world, src.matrix)
        else:
            matrix = src.matrix

        if basis:
            matrix_local = src.matrix_basis
        else:
            matrix_local = src.bone.matrix_local
    elif Is.bone(src):
        matrix = src.matrix
        matrix_local = src.matrix_basis
    elif Is.editbone(src):
        matrix = matrix_local = src.matrix
    else:
        matrix = src.matrix_world
        if basis:
            matrix_local = src.matrix_basis
        else:
            matrix_local = src.matrix_local

    if copy:
        matrix = matrix.copy()
        matrix_local = matrix_local.copy()

    # scaled = False
    # if len(set(matrix.to_scale())) == 1:
    #     matrix *= matrix.to_scale()[0]
    #     scaled = True

    if (tail and hasattr(src, 'tail')):
        # matrix.translation += (src.tail - src.head)
        matrix.translation = matrix.Translation(src.tail).translation
        matrix_local.translation = \
            matrix_local.Translation(src.tail).translation
        # for (i, t) in enumerate(matrix.translation):
        #     v = (src.tail[i] - src.head[i]) * (
        #         1 if scaled else matrix.to_scale()[i])
        #     matrix.translation[i] += v

    if (basis or local):
        return matrix_local
    else:
        return matrix

def matrix_local(src, mat=None):
    """Get and convert a matrix to local space for later re-use"""
    if mat is None:
        mat = Get.matrix(src)

    if Is.posebone(src):
        # from zpy import Set
        # Set.matrix(src, Get.matrix(src))
        # mat = Get.matrix(src, basis=True)

        # Setting the matrix updates it to the visual pose
        # Basis gets the applied pose

        # This would do that as well, without needing "apply":
        mat = src.id_data.convert_space(
            pose_bone=src, matrix=mat,
            from_space='POSE', to_space='LOCAL')
        "or so my notes said"
        # From Baker:

        # if visual keying:
        #     # Get the final transform of the bone in its own local space...
        #     matrix[name] = obj.convert_space(
        #         pose_bone=bone, matrix=bone.matrix,
        #         from_space=self.bone_matrix, to_space='LOCAL')
        # else:
        #     matrix = bone.matrix_basis.copy()
    else:
        mat = src.id_data.convert_space(
            pose_bone=None, matrix=mat,
            from_space='WORLD', to_space='LOCAL')

        # From Baker:
        # If deleting object parents, do matrix world

        # if (Global.do_parents_clear):
        #     trans['matrix'] = obj.matrix_world.copy()
        # else:
        #     parent = obj.parent
        #     matrix = obj.matrix_world
        #     trans['matrix'] = multiply_matrix(
        #         parent.matrix_world.inverted_safe(), matrix
        #         ) if parent else matrix.copy()

    return mat

def mode(object):

    if hasattr(object, 'mode'):
        return object.mode

def name(src):

    # orig = Constraint.get.original(src)
    # if orig:
    #     src = orig

    if Is.posebone(src):
        name = f"{src.id_data.name}.{src.name}"
    else:
        name = f"{src.name}"

    # if src.constraints_relative.is_new:
    #     name = name.rsplit('-', 1)[0]
    #     if name.endswith('-'):
    #         name = name.rsplit('-', 1)[0]

    return name

def nla_blend_name(anim_or_strip):

    if Is.nla_strip(anim_or_strip):
        blend_type = anim_or_strip.blend_type
    else:
        blend_type = anim_or_strip.action_blend_type

    if blend_type == 'REPLACE':
        return 'Base'
    else:
        return blend_type.replace('COMBINE', 'Layer').title()

def object_bone_from_string(target: "String or Object", subtarget=''):
    """Try to find an object an bone from text"""

    if Is.string(target):
        obj = bpy.data.objects.get(target)
    else:
        obj = target

    bone = None

    if Is.armature(obj) and subtarget:
        if (obj.mode == 'EDIT'):
            bones = obj.data.edit_bones
        else:
            bones = obj.pose.bones
        bone = bones.get(subtarget)

    return (obj, bone)

def reverse(src):
    "return a reversed list/string or opposite number"

    if Is.iterable(src):
        if type(src) is str:
            inverse = ""
        else:
            inverse = []
        for item in reversed(src):
            inverse += item
    elif type(src) is bool:
        inverse = not src
    elif type(src) in (int, float):
        inverse = src * -1
    else:
        inverse = src

    return inverse

def sorted_bones(selected):
    "Scan through selected bones to find order of bones in a a tree"

    selected = [b for b in selected
                if (Is.posebone(b) or Is.bone(b) or Is.editbone(b))]
    bones = list()

    while selected:
        for bone in list(selected):
            if (bone not in selected):
                # a previous bone was one of this bone's parents
                continue

            for parent in bone.parent_recursive:
                if parent in selected:
                    break
            else:
                bones.append(bone)
                selected.remove(bone)

                # Loop over children, which "may" result in less looping
                for child in bone.children_recursive:
                    if child in selected:
                        bones.append(child)
                        selected.remove(child)

    return bones

def sorted_chains(selected):
    "Scan through selected bones to find order of bones for spline_ik chains"

    chains = []
    for bone in selected:
        if not any((Is.posebone(bone), Is.bone(bone), Is.editbone(bone))):
            continue
        chain = []
        end = True
        for child in bone.children_recursive:
            if child in selected:
                end = False
                continue
        if end:
            for par in reversed(bone.parent_recursive):
                if par in selected:
                    chain.append(par)
            chain.append(bone)
            chains.append(chain)

    return chains

def strip_co_frame(strip, co=None, frame=None):
    "Try to convert the frame number between scene and action keyframes"

    action_start = strip.action_frame_start
    action_end = strip.action_frame_end
    strip_start = strip.frame_start
    strip_end = strip.frame_end
    # Repeat: store repeat then reset it to get the actual strip range
    # repeat = strip.repeat; scale = strip.scale; strip.repeat = 1; strip.scale = 1; strip_end = strip.frame_end

    end = utils.scale_range(
        strip_end, strip_start, strip_end,
        0, abs(strip_start - strip_end)
    )
    end /= strip.repeat
    end /= strip.scale
    end *= strip.scale
    end += strip_start

    # Restore repeat value
    # strip.repeat = repeat; strip.scale = scale

    if (co is not None):
        # target = relative strip
        value = utils.scale_range(co, action_start, action_end, strip_start, end)
    elif (frame is not None):
        # target = absolute frame
        value = utils.scale_range(frame, strip_start, end, action_start, action_end)

    # # for looping curve keyframes
    # while (value > end):
        # value -= abs(strip_start - end)

    return value

def strip_tracks(src, items=[], selected=True, top_down=True, active_first=True):
    """Return a class pack of strips and track + owner for object"""
    anim = getattr(src, 'animation_data', None)
    if not anim:
        return []

    has_strips = False
    active = False
    strips = []
    if top_down:
        tracks = reversed(anim.nla_tracks)
    else:
        tracks = anim.nla_tracks

    for track in tracks:
        for strip in track.strips:
            if any((
                    not (strip.select or not selected),
                    # strip.type == 'TRANSITION',
            )):
                continue
            has_strips = True
            active_strip = type('strip', (), dict(
                data=src,
                track=track,
                strip=strip,
            ))

            if strip.active and active_first:
                active = True
                strips.insert(0, active_strip)
                # strips.insert(0, strip)
            else:
                strips.append(active_strip)
                # strips.append(strip)
    if not has_strips:
        return []

    if active:
        # items = [*strips, *items]
        items.insert(0, [src, strips])
    else:
        # items = [*items, *strips]
        items.append([src, strips])

    return strips

def strips(src, items=[], selected=False, top_down=True, active_first=True):
    """Return all strips available from the specified object"""
    strips = []
    for item in Get.strip_tracks(
            src, items=items, selected=selected,
            top_down=top_down, active_first=active_first):
        strips.append(item.strip)

    return strips

def valid_op(*ops):
    """Validate whether or not an operator is in bpy.ops;
        if True, return operator"""

    valid = list()

    for op in ops:
        if not Is.string(op):
            if hasattr(op, 'bl_idname'):
                op = op.bl_idname
            else:
                continue

        try:
            exec(f'bpy.ops.{op}.get_rna_type()')
            valid.append(op)
        except:
            continue

    if len(ops) > 1:
        return valid
    elif valid:
        return valid[0]
    else:
        return None

def visible_armature_layers(bone, arm):
    """List of layer indexes that is visible and the Bone is on"""

    if Is.posebone(bone):
        bone = bone.bone
    if Is.armature(arm):
        arm = arm.data

    return [i for i, j in zip(bone.layers, arm.layers) if i and j]

# endregion
# -------------------------------------------------------------------------

# -------------------------------------------------------------------------
# region: Contextual:
# -------------------------------------------------------------------------

# def context(C=None):
    # import bpy

    # if C is None:
    #     context = bpy.context
    # elif isinstance(C, dict):
    #     context = type('', (Get.copy(bpy.context), bpy.types.Context), C)
    # else:
    #     context = type('', (Get.copy(C), bpy.types.Context,), {})

    # return context

def active(context, mode=None):
    "Return active object or bone, depending on the active mode"

    if mode is None:
        mode = context.mode

    # context_modes = (
        # 'OBJECT', 'SCULPT', 'POSE',
        # 'EDIT_ARMATURE', 'EDIT_MESH',
        # 'PAINT_WEIGHT', 'PAINT_TEXTURE', 'PAINT_VERTEX',
    # )
    # object_modes = (
        # 'OBJECT', 'SCULPT', 'POSE',
        # 'EDIT',
        # 'WEIGHT_PAINT', 'TEXTURE_PAINT', 'VERTEX_PAINT',
    # )

    if mode in ('OBJECT', 'EDIT_MESH', 'SCULPT', 'PAINT_WEIGHT', 'PAINT_TEXTURE', 'PAINT_VERTEX'):
        active = context.object
        if active is None:
            active = (Get.selected_objects(context) + [None])[0]
    elif mode in {'EDIT_ARMATURE', 'edit_bones'}:
        active = context.active_bone
        if active is None:
            active = (Get.selected_edit_bones(context) + [None])[0]
    elif mode in {'bones'}:
        active = context.active_bone
        if active is None:
            active = (Get.selected_bones(context) + [None])[0]
    elif mode == 'POSE':
        active = context.active_pose_bone
        if active is None:
            active = (Get.selected_pose_bones(context) + [None])[0]
    else:
        utils.debug(
            f"Error: zpy.Get.py > Get.active() > "
            f"does not account for this mode ({mode})"
        )
        active = None

    return active

def cursor(context):
    return context.scene.cursor

def frame_mapped(context, frame=None):
    """Get the time remapped original of the frame # or frame current"""
    scn = context.scene

    if frame is None:
        frame = scn.frame_current_final
    fmn = scn.render.frame_map_new
    fmo = scn.render.frame_map_old

    frame *= (fmn / fmo)
    # if fmo < fmn:
    # elif fmn < fmo:
        # frame *= (fmo / fmn)

    return frame

def frame_from_strip(context, anim_or_strip, frame=None, absolute=False):
    "Convert the keyframe.co inside scaled nla strip to timeline frame"

    if frame is None:
        frame = context.scene.frame_current_final

    anim = anim_or_strip.id_data.animation_data
    strip = anim_or_strip
    tweak = anim.use_tweak_mode
    is_strip = Is.nla_strip(strip)

    if (tweak or is_strip):
        if (tweak and is_strip and strip.active) and hasattr(anim, 'nla_tweak_strip_time_to_scene'):
            value = anim.nla_tweak_strip_time_to_scene(frame, invert=False)
        else:
            if not is_strip:
                strip = Get.active_strip(anim)
            value = Get.strip_co_frame(strip, co=frame)
    else:
        value = frame

    if absolute:
        return value
    else:
        return (round(value, 6))

def frame_to_strip(context, anim_or_strip, frame=None, absolute=False):
    "Convert the keyframe.co/frame to correct frame inside nla strip"

    if frame is None:
        frame = context.scene.frame_current_final

    anim = anim_or_strip.id_data.animation_data
    strip = anim_or_strip
    tweak = anim.use_tweak_mode
    is_strip = Is.nla_strip(strip)

    if (tweak or is_strip):
        if is_strip and strip.active and hasattr(anim, 'nla_tweak_strip_time_to_scene'):
            value = anim.nla_tweak_strip_time_to_scene(frame, invert=True)
        else:
            if not is_strip:
                strip = Get.active_strip(anim)
            value = Get.strip_co_frame(strip, frame=frame)
    else:
        # Null call, so just return scene frame
        value = frame

    if absolute:
        return value
    else:
        return (round(value, 6))

def frame_range(context):
    "return current playback range"

    scn = context.scene
    preview = ("_", "_preview_")[scn.use_preview_range]
    frame_start = eval(f'scn.frame{preview}start')
    frame_end = eval(f'scn.frame{preview}end')

    return (frame_start, frame_end)

def frame_range_from_nla(context, **kargs):
    "return current playback range of selected nla strips"
    # kargs:
    # objects=[]

    objects = kargs.get('objects', Get.objects(context=context))

    def get_sub_strips(strip, sub):
        """Loop through Meta Strips to find Action Strips"""
        nonlocal start, end
        if (getattr(sub, 'strips', None)):
            for sub_sub in sub.strips:
                get_sub_strips(sub, sub_sub)
                # "loop until no more sub-strips"
        elif (getattr(sub, 'action', None)):
            start = (start, strip.frame_start)[start is None]
            end = (end, strip.frame_end)[end is None]

            if (strip.frame_start < start):
                start = strip.frame_start
            if (strip.frame_end > end):
                end = strip.frame_end

    start = end = None
    for obj in objects:
        anim = obj.animation_data
        if anim is None:
            continue

        for track in anim.nla_tracks:
            for strip in track.strips:
                if (not strip.select):
                    continue
                get_sub_strips(strip, strip)

    frame_start = (start, frame_start)[start is None]
    frame_end = (end, frame_end)[end is None]

    return (frame_start, frame_end)

def in_scene(context, *scenes):
    "return list of objects in the scene(s)"

    objects = []
    if not scenes:
        scenes = [context.scene]

    for scene in scenes:
        objects.extend(scene.objects)

    return objects

def in_view(context, *views, **filters):
    """return list of objects/collections in the current view layer"""

    items = []

    if not views:
        views = [context.view_layer]

    if not filters:
        filters = dict(
            object=True,
            collection=False,
        )

    for view_layer in views:
        if filters.get('object'):
            items.extend(view_layer.objects)

        if filters.get('collection'):
            def colls(layer):
                for view in layer.children:
                    if view.is_visible:
                        items.append(view.collection)
                    colls(view)
            colls(view_layer.layer_collection)

    return items

def matrix_constraints(context, src, mat=None, force_constraints=[]):
    """Convert matrix so that it doesn't offset with the item's constraints"""

    from mathutils import Matrix

    mats_cons = list()

    mat_new = Get.matrix(src)
    if mat is None:
        mat = mat_new

    if Is.posebone(src):
        inv = src.id_data.matrix_world.inverted()
        mat = utils.multiply_matrix(inv, mat)
        mat_new = utils.multiply_matrix(inv, mat_new)

    for con in src.constraints:
        if not getattr(con, 'target', None) or \
                con.mute or \
                (con.influence == 0.0 and con not in force_constraints):
            continue
        else:
            pose = con.target.pose
            bone = None
            if pose and getattr(con, 'subtarget', None):
                bone = pose.bones.get(con.subtarget)

        if hasattr(con, 'inverse_matrix'):
            im = con.inverse_matrix
            imi = im.inverted()
            mat0 = utils.multiply_matrix(imi, mat)
            # The constraint with cleared inverse
            mat1 = mat.copy()
            # The goal pose without constraint
            con.inverse_matrix == utils.multiply_matrix(mat1, mat0.inverted())
            # The value set initially for the constraint

            mat_tar = con.target.matrix_world.inverted()
            if bone:
                mat_tar = utils.multiply_matrix(mat_tar, bone.matrix.inverted())

            mat_value = utils.multiply_matrix(imi, mat_tar)

            # # TODO: This get's the standard Child of, but it doesn't get
            # # a constraint without all 3 Rotation enabled

            # This is meant to give a list of matrices to cycle through
            # to try to find the correct mixture. What's wrong appears to
            # be that the target matrix from above needs to be altered,
            # and the current math is already valid
            # mat0 and mat
            # mats = (
            #     Matrix(),               # 0

            #     mat0,                   # 1
            #     mat1,                   # 2
            #     mat_new,                # 3
            #     mat_tar,                # 4
            #     im,                     # 5

            #     mat0.inverted(),        # 6
            #     mat1.inverted(),        # 7
            #     mat_new.inverted(),     # 8
            #     mat_tar.inverted(),     # 9
            #     imi,                    # 10
            # )
            # ac = [' ' + a for a in ['a', 'b', 'c', 'd', 'e', 'f', 'g']]
            # for a in ac:
            #     if src.get(a) is None:
            #         src[a] = 0

            # mat_value = multiply_matrix(
            #     *(
            #         mats[src[a]] for a in ac
            #     ),
            # )

            # Set influence of the result from constraint
            mat_influce = Matrix().lerp(mat_value, con.influence)
            mats_cons.append(mat_influce)
        elif getattr(con, 'use_offset', False):
            mats_cons.append(Matrix().lerp(
                utils.multiply_matrix(mat, mat_new.inverted()),
                con.influence))

    return utils.multiply_matrix(*mats_cons, mat)

def mirror_bone(bone):
    name = utils.flip_name(bone.name)
    if name == bone.name:
        return

    if Is.posebone(bone):
        return bone.id_data.pose.bones.get(name)
    elif Is.editbone(bone):
        return bone.id_data.edit_bones.get(name)
    elif Is.bone(bone):
        return bone.id_data.bones.get(name)

def objects(context, collection=None, link=False):
    """
    Return the active scene objects pointer list\\
    (for example, to set the active object)
    """

    objects = context.view_layer.objects  # active scene's objects (all)
    if link:
        # 2.8 can only link to collections
        objects = context.scene.collection.objects
        # active scene's objects without a collection
    if collection:
        objects = collection.objects

    return objects

def objects_nla(context):
    """Find available objects in the NLA / Dopesheet"""

    objects = []

    for obj in Get.objects(context):
        if Is.in_dopesheet(context, obj):
            objects.append(obj)

    return objects

def rig(context, src):
    """
    bone/editbone don't directly link to objects, just the armature.\\
    So, try to find an object by using their armature instead
    """

    is_armature = isinstance(src, bpy.types.Armature)

    if Is.posebone(src) or Is.armature(src.id_data):
        return src.id_data
    elif (is_armature, Is.bone(src) or Is.editbone(src)):
        for object in Get.objects(context):
            if (object.data == src.id_data):
                break
        else:
            object = None
        return object
    else:
        assert None, ("Could not find rig for", src)

def selected(context, src=None, mirror=False, mirror_x_only=True):
    """
    return selected objects or selected bones.
    If src is specified, return selected bones from it
    """

    mode = context.mode

    if mode in ('POSE', 'PAINT_WEIGHT'):
        selected = Get.selected_pose_bones(context, src)
        if mirror:
            for bone in selected:
                rig = bone.id_data
                if (rig.pose.use_mirror_x or not mirror_x_only):
                    flip = rig.pose.bones.get(utils.flip_name(bone.name))
                    if flip and (flip not in selected):
                        selected.append(flip)
    elif mode == 'EDIT_ARMATURE':
        selected = Get.selected_edit_bones(context, src)
        if mirror:
            for bone in selected:
                arm = bone.id_data
                if (arm.use_mirror_x or not mirror_x_only):
                    flip = arm.edit_bones.get(utils.flip_name(bone.name))
                    if flip and (flip not in selected):
                        selected.append(flip)
    else:
        selected = Get.selected_objects(context, True)

    return selected

def selected_bones(context, src=None):
    "Always return data bones as list, never as None"

    if src:
        bones = src.data.bones
        selected = [b for b in bones if Is.selected(b) and
        Is.visible(context, b)]
    else:
        selected = Get.as_list(context, 'selected_bones')

    return selected

def selected_edit_bones(context, src=None):
    "Always return edit bones as list, never as None"

    if src:
        bones = src.data.edit_bones
        selected = [b for b in bones if Is.selected(b) and
        Is.visible(context, b)]
    else:
        selected = Get.as_list(context, 'selected_editable_bones')

    return selected

def selected_pose_bones(context, src=None, force: "not needed, todelete" = False):
    "Always return pose bones as list, never as None"

    if src:
        selected = [b for b in src.pose.bones if Is.selected(b) and
        Is.visible(context, b)]
    # elif not force and context.mode not in ('POSE', 'PAINT_WEIGHT'):
        # selected = []
    else:
        selected = Get.as_list(context, 'selected_pose_bones')

    return selected

# Disabled because this is supposed to find the "Active object" only
# def selected_pose_bones_from_object(src=None, context=None):
    # if src:
    # selected = [b for b in Get.selected_pose_bones() if b.id_data == src]
    # else:
    # return Get.as_list('selected_pose_bones_from_active_object', context=context)

def selected_objects(context, all_objects=False):
    "Return selected objects if either they're in object mode or object is True"

    selected = [o for o in Get.as_list(context, 'selected_objects')
                if (all_objects) or (o.mode not in ('POSE', 'EDIT'))]

    return selected

def space(context, area):
    "return the active spacedata or a defaults if not found"

    space = getattr(context.space_data, area, None)

    if space is not None:
        pass
    elif area == 'dopesheet':
        space = type('defaults', (), dict(
            # Isolation
            show_hidden=False,
            show_missing_nla=True,
            show_only_selected=False,

            # Main Filter
            # None or a collection
            filter_collection=type('None', (), {'objects': []}),
            show_only_group_objects=False,  # 27
            # None or a group
            filter_group=type('None', (), {'objects': []}),
            filter_fcurve_name='',
            filter_text='',

            # Filters
            show_armatures=True,
            show_cache_files=True,
            show_cameras=True,
            show_curves=True,
            show_datablock_filters=False,
            show_expanded_summary=True,
            show_gpencil=True,
            show_gpencil_3d_only=False,
            show_lattices=True,
            show_lights=True,
            show_lamps=True,  # 27
            show_linestyles=True,
            show_materials=True,
            show_meshes=True,
            show_metaballs=True,
            show_modifiers=True,
            show_nodes=True,
            show_only_errors=False,
            show_only_matching_fcurves=False,  # 27
            show_particles=True,
            show_scenes=True,
            show_shapekeys=True,
            show_speakers=True,
            show_summary=False,
            show_textures=True,
            show_transforms=True,
            show_worlds=True,

            __doc__=None,
            __module__='bpy.types',
            __slots__=(),
            # bl_rna=< bpy_struct, Struct("DopeSheet") >,
            # rna_type=bpy.data.screens['Layout']...Struct,
            source=context.scene,  # 'source': bpy.data.scenes['Scene'],
            use_datablock_sort=True,
            use_filter_text=False,  # 27
            use_multi_word_filter=False
        ))

    return space

def strip_influence(context, strip, frame=None):
    "Return influence of NLA Strip at a given frame (or current frame)"
    if frame is None:
        frame = context.scene.frame_current_final

    (start, end) = (strip.frame_start, strip.frame_end)
    start_in = (start + strip.blend_in)
    end_out = (end - strip.blend_out)

    if (strip.use_animated_influence):
        # Animated Influence
        influence = strip.fcurves.find('influence').evaluate(frame)
    elif (start_in < frame < end_out):
        # Regular Influence
        influence = 1.0
    elif (frame < start):
        # Before strip
        if (strip.extrapolation == 'HOLD') and (start == start_in):
            influence = 1.0
        else:
            influence = 0.0
    elif (end < frame):
        # After strip
        if (strip.extrapolation.startswith('HOLD')) and (end_out == end):
            influence = 1.0
        else:
            influence = 0.0
    elif (start <= frame <= start_in):
        # At Start or Blend In
        if start != start_in:
            influence = utils.scale_range(frame, start, start_in, 0, 1)
        else:
            influence = 1.0
    elif (end_out <= frame <= end):
        # At End or Blend Out
        if end_out != end:
            influence = utils.scale_range(frame, end_out, end, 1, 0)
        else:
            influence = 1.0
    else:
        # Unknown
        influence = None

    return influence

def strip_in_track(context, track, frame=None):
    "Find the currently evaluated strip in the track"

    if frame is None:
        frame = context.scene.frame_current_final
    right_of_strips = None
    active_strip = None
    for strip in track.strips:
        if (frame < strip.frame_start):
            if right_of_strips in {'HOLD', 'HOLD_FORWARD'}:
                # Previous strip extends forward, so use that
                strip = active_strip
            elif right_of_strips in {'NOTHING', None}:
                if (strip.extrapolation == 'HOLD'):
                    # This strip extends backwards
                    pass
                elif (strip.extrapolation in {'NOTHING', 'HOLD_FORWARD'}):
                    # Outside and between two strips. No effect.
                    strip = None
            break
        elif (strip.frame_start <= frame <= strip.frame_end):
            # Inside strip's range
            break
        elif (strip.frame_end < frame):
            # Set strip as active to compare to next strip
            right_of_strips = strip.extrapolation
            active_strip = strip
    else:
        if not active_strip:
            # Reached end of track, and no strip can be even considered
            return

    return strip

def strips_nla(context, selected=True, top_down=True, active_first=True):
    "Return all strips (class pack) available from the NLA editor"

    items = []
    args = dict(items=items, selected=selected,
                top_down=top_down, active_first=active_first)

    for _object_ in Get.objects_nla(context):
        obj = getattr(_object_, 'id_data', _object_)
        data = getattr(_object_, 'data', None)
        shapes = getattr(data, 'shape_keys', None)

        Get.strip_tracks(obj, **args)
        Get.strip_tracks(data, **args)
        Get.strip_tracks(shapes, **args)

    return items

def tail(context, bone):
    # ------ Bones
    if any((Is.posebone(bone), Is.bone(bone), Is.editbone(bone))):
        if (Is.posebone(bone)):
            obj = bone.id_data
        else:
            obj = Get.rig(context, bone)

        if obj is None:
            return
        else:
            mat = Get.matrix(obj)
            return utils.multiply_matrix(mat, bone.tail)

    # # ------ Mesh
    # vp = getattr(obj.data, 'polygons', None)
    # ve = getattr(obj.data, 'edges', None)
    # vt = getattr(obj.data, 'vertices', None)
    # if any((vp, ve, vt)):
    #     if vp and len(vp) > 1:
    #         co = vp[0].center
    #         for poly in vp:
    #             if poly.center != Vector():
    #                 co = poly.center
    #                 break
    #     elif ve:
    #         for edge in ve:
    #             co = Vector()
    #             for i in range(3):
    #                 for vert_i in edge.vertices:
    #                     co[i] += obj.data.vertices[vert_i].co[i]
    #                 co[i] /= len(edge.vertices)
    #             if co != Vector():
    #                 break
    #     elif vt:
    #         co = vt[-1].co
    #         for vert in [vt[-1], vt[0], *reversed(vt)]:
    #             if vert.co != Vector():
    #                 co = vert.co
    #                 break
    #     return multiply_matrix(obj.matrix_world, co)

    # # -------- All other
    # try:
    #     # Get object's dimensions; Maybe get tail for curves/metaballs/etc later
    #     co = Vector((0, obj.dimensions[1], 0))
    #     if self:
    #         self.report({'INFO'}, "Non-Mesh tail finding doesn't work")
    #     return multiply_matrix(obj.matrix_world, co)
    # except:
    #     return None

def value_from_nla_strips(context, src, path, index=-1, frame=None, selected=False, tweak=False):
    # Tries to actually read the strips
    # but it doesn't work with COMBINE quaternions

    anim = getattr(src.id_data, 'animation_data', None)
    if not anim:
        return

    if frame is None:
        frame = context.scene.frame_current_final

    if Is.posebone(src) and not path.startswith('pose.bones'):
        kpath = (f'pose.bones[\"{src.name}\"].' + path)
    else:
        kpath = path

    # Establish default
    if any((
            'default' is 1,
            path.endswith('scale'),
            path.endswith('rotation_quaternion') and index == 0,
            path.endswith('rotation_axis_angle') and index == 2,
    )):
        default = 1.0
    else:
        default = 0.0
    value = default

    default_quat = Quaternion()
    value_quat = default_quat

    first_strip = True
    for track in anim.nla_tracks:
        if track.mute:
            continue
        if tweak and anim.use_tweak_mode:
            for strip in track.strips:
                if strip.active:
                    break
            else:
                strip = Get.strip_in_track(track, frame=frame)
        else:
            strip = Get.strip_in_track(track, frame=frame)

        action = getattr(strip, 'action', None)
        if not action or strip.mute:
            # Either no strip evaluated or strip is meta strips (TODO)
            continue

        # Try to find property in fcurve
        if index == -1:
            fc = action.fcurves.get(kpath)
        else:
            for fc in action.fcurves:
                if fc.data_path == kpath and fc.array_index == index:
                    break
            else:
                fc = None
        if fc is None:
            if path.endswith('rotation_quaternion') and action.fcurves.get(kpath):
                pass
            else:
                continue

        if frame < strip.frame_start:
            frame_strip = strip.frame_start
        elif (frame > strip.frame_end):
            frame_strip = strip.frame_end
        else:
            frame_strip = frame

        if tweak and strip.active:
            ff = frame
            inf = 1.0
        else:
            ff = frame_strip
            inf = Get.strip_influence(context, strip, frame=frame_strip)

        next_quat = Quaternion()

        if path.endswith('rotation_quaternion'):
            for i in range(4):
                # Don't research curves for the already found curve
                if fc and i == index:
                    next = fc.evaluate(Get.frame_to_strip(strip, ff))
                    next_quat[i] = next
                    continue
                for afc in action.fcurves:
                    if afc.data_path == kpath and afc.array_index == i:
                        next = afc.evaluate(Get.frame_to_strip(strip, ff))
                        next_quat[i] = next
                        break
                else:
                    next_quat[i] = default_quat[i]

        if fc:
            next = fc.evaluate(Get.frame_to_strip(context, strip, ff))
        else:
            next = None

        if (first_strip):
            value = next
            value_quat = next_quat
            first_strip = False
        elif (strip.blend_type == 'COMBINE'):
            if (path.endswith('rotation_axis_angle') and index == 2):
                result = value + (next * inf)
            elif (path.endswith('rotation_quaternion') and index == 0):
                result = value + ((default - next) * inf)
                # result = value * pow(next, inf)
            elif path.endswith('scale'):
                result = value * (next * inf)
            else:
                # result = value - ((default - next) * inf)
                result = value + (next - default) * inf
            # utils.debug(f"{path}[{index}]\t{strip.name}")
            # utils.debug(f"\tValue\t{value}")
            # utils.debug(f"\tNext\t{next}")
            # utils.debug(f"\tResult\t{result}")
            # utils.debug(f'\tGoal\t{getattr(src, path)[index]}')
            value = result

            # "Quaternion blending is deferred until all sub-channel values are known."

            #     # next_val = abs(next - default) * (-1 if next < default else 1)
            #     # value += utils.lerp(0.0, next_val, inf)

            # if path.endswith('rotation_quaternion'):
            #     mix_mode = 'QUATERNION'  # return old value
            #     mix_mode = 'AXIS_ANGLE'  # return old_value + (value - base_value) * inf
            #     # value = nla_combine_value(mix_mode, default, value, next, inf)
            #     # nla_invert_combine_quaternion(value_quat, next_quat, inf, value_quat)
            #     nla_combine_quaternion(value_quat, next_quat, inf, value_quat)
            #     value = value_quat[index]
            # else:
            #     # mix_mode = 'ADD'  # pass
            #     mix_mode = 'AXIS_ANGLE'  # return old_value + (value - base_value) * inf
            #     # mix_mode = 'MULTIPLY'  # return old_value * pow(value / base_value, inf)

            #     value = nla_combine_value(mix_mode, default, value, next, inf)
        else:
            value = nla_blend_value(strip.blend_type, value, next, inf)
            # nla_combine_quaternion(value_quat, next_quat, inf, value_quat)

    return value

# endregion
# -------------------------------------------------------------------------

Get = type('', (), globals())


import torch
import pandas as pd
import numpy as np

from peptdeep.model.featurize import (
    get_batch_aa_indices,
    get_batch_mod_feature
)

from peptdeep.settings import model_const

import peptdeep.model.model_interface as model_interface
import peptdeep.model.building_block as building_block

mod_feature_size = len(model_const['mod_elements'])

IRT_PEPTIDE_DF = pd.DataFrame(
    [['LGGNEQVTR', 'RT-pep a', -24.92, '', ''],
    ['GAGSSEPVTGLDAK', 'RT-pep b', 0.00, '', ''],
    ['VEATFGVDESNAK', 'RT-pep c', 12.39, '', ''],
    ['YILAGVENSK', 'RT-pep d', 19.79, '', ''],
    ['TPVISGGPYEYR', 'RT-pep e', 28.71, '', ''],
    ['TPVITGAPYEYR', 'RT-pep f', 33.38, '', ''],
    ['DGLDAASYYAPVR', 'RT-pep g', 42.26, '', ''],
    ['ADVTPADFSEWSK', 'RT-pep h', 54.62, '', ''],
    ['GTFIIDPGGVIR', 'RT-pep i', 70.52, '', ''],
    ['GTFIIDPAAVIR', 'RT-pep k', 87.23, '', ''],
    ['LFLQFGAQGSPFLK', 'RT-pep l', 100.00, '', '']],
    columns=['sequence','pep_name','irt', 'mods', 'mod_sites']
)
IRT_PEPTIDE_DF['nAA'] = IRT_PEPTIDE_DF.sequence.str.len()

#legacy
irt_pep = IRT_PEPTIDE_DF

class Model_RT_Bert(torch.nn.Module):
    """Transformer model for RT prediction"""
    def __init__(self,
        dropout = 0.1,
        nlayers = 4,
        hidden = 128,
        output_attentions=False,
        **kwargs,
    ):
        super().__init__()

        self.dropout = torch.nn.Dropout(dropout)

        self.input_nn = building_block.AATransformerEncoding(hidden)

        self._output_attentions = output_attentions

        self.hidden_nn = building_block.Hidden_HFace_Transformer(
            hidden, nlayers=nlayers, dropout=dropout,
            output_attentions=output_attentions
        )

        self.output_nn = torch.nn.Sequential(
            building_block.SeqAttentionSum(hidden),
            torch.nn.PReLU(),
            self.dropout,
            torch.nn.Linear(hidden, 1),
        )

    @property
    def output_attentions(self):
        return self._output_attentions

    @output_attentions.setter
    def output_attentions(self, val:bool):
        self._output_attentions = val
        self.hidden_nn.output_attentions = val

    def forward(self,
        aa_indices,
        mod_x,
    ):
        x = self.dropout(self.input_nn(
            aa_indices, mod_x
        ))

        hidden_x = self.hidden_nn(x)
        if self.output_attentions:
            self.attentions = hidden_x[1]
        else:
            self.attentions = None
        x = self.dropout(hidden_x[0]+x*0.2)

        return self.output_nn(x).squeeze(1)

class Model_RT_LSTM_CNN(torch.nn.Module):
    """CNN+LSTM model for RT prediction"""
    def __init__(self,
        dropout=0.2,
    ):
        super().__init__()

        self.dropout = torch.nn.Dropout(dropout)

        hidden = 256
        self.rt_encoder = building_block.Encoder_26AA_Mod_CNN_LSTM_AttnSum(
            hidden
        )

        self.rt_decoder = building_block.Decoder_Linear(
            hidden,
            1
        )

    def forward(self,
        aa_indices,
        mod_x,
    ):
        x = self.rt_encoder(aa_indices, mod_x)
        x = self.dropout(x)

        return self.rt_decoder(x).squeeze(1)
#legacy
Model_RT_LSTM = Model_RT_LSTM_CNN

class AlphaRTModel(model_interface.ModelInterface):
    """
    `ModelInterface` for RT models
    """
    def __init__(self,
        dropout=0.1,
        model_class:torch.nn.Module=Model_RT_LSTM_CNN, #model defined above
        device:str='gpu',
        **kwargs,
    ):
        super().__init__(device=device)
        self.model:Model_RT_LSTM_CNN = None
        self.build(
            model_class,
            dropout=dropout,
            **kwargs
        )
        self.target_column_to_predict = 'rt_pred'
        self.target_column_to_train = 'rt_norm'

    def _get_features_from_batch_df(self,
        batch_df: pd.DataFrame,
    ):
        return (
            self._get_26aa_indice_features(batch_df),
            self._get_mod_features(batch_df)
        )

    def add_irt_column_to_precursor_df(self,
        precursor_df: pd.DataFrame
    ):
        self.predict(IRT_PEPTIDE_DF)
        # simple linear regression
        rt_pred_mean = IRT_PEPTIDE_DF.rt_pred.mean()
        irt_mean = IRT_PEPTIDE_DF.irt.mean()
        x = IRT_PEPTIDE_DF.rt_pred.values - rt_pred_mean
        y = IRT_PEPTIDE_DF.irt.values - irt_mean
        slope = np.sum(x*y)/np.sum(x*x)
        intercept = irt_mean - slope*rt_pred_mean
        # end linear regression
        precursor_df['irt_pred'] = precursor_df.rt_pred*slope + intercept
        return precursor_df


# Definition for a binary tree node.
class TreeNode:
    def __init__(self, x):
        self.val = x
        self.left = None
        self.right = None

    def insert(self, x):

        if x is None:
            return

        if not self.val:
            self.val = x
            return

        if x < self.val and self.left is None:
            self.left = TreeNode(x)
            return
        if x < self.val and self.left is not None:
            self.left.insert(x)
            return
        if x > self.val and self.right is None:
            self.right = TreeNode(x)
            return
        if x > self.val and self.right is not None:
            self.right.insert(x)
            return

class Solution:
    def minDepth(self, root):
        """
        :type root: TreeNode
        :rtype: int
        """

        if root is None:
            return 0

        if root.left is None and root.right is None:
            return 1

        if root.left is None:
            return 1 + self.minDepth(root.right)

        if root.right is None:
            return 1 + self.minDepth(root.left)

        if root.right is not None and root.left is not None:
            l = self.minDepth(root.left)
            r = self.minDepth(root.right)
            if l > r:
                return 1 + r
            else:
                return 1 + l

if __name__ == '__main__':
    n = [3, 9, 20, None, None, 15, 7]

    root = TreeNode(n.pop(0))
    for i in n:
        root.insert(i)

    from pprint import pprint
    s = Solution().minDepth(root)

    pprint(s)


from datetime import datetime
import json
import logging
import os
import pytz
import re

from pydnp3 import opendnp3

DEFAULT_POINT_TOPIC = 'dnp3/point'
#DEFAULT_OUTSTATION_STATUS_TOPIC = 'mesa/outstation_status'
#DEFAULT_LOCAL_IP = "0.0.0.0"
DEFAULT_PORT = 20000

# PointDefinition.fcodes values:
DIRECT_OPERATE = 'direct_operate'       # This is actually DIRECT OPERATE / RESPONSE
SELECT = 'select'                       # This is actually SELECT / RESPONSE
OPERATE = 'operate'                     # This is actually OPERATE / RESPONSE

# Some PointDefinition.point_type values:
POINT_TYPE_ANALOG_INPUT = 'Analog Input'
POINT_TYPE_ANALOG_OUTPUT = 'Analog Output'
POINT_TYPE_BINARY_INPUT = 'Binary Input'
POINT_TYPE_BINARY_OUTPUT = 'Binary Output'

# Default event group and variation for each of these types, in case they weren't spec'd for a point in the data file.
EVENT_DEFAULTS_BY_POINT_TYPE = {
    POINT_TYPE_ANALOG_INPUT: {"group": 32, "variation": 3},
    POINT_TYPE_ANALOG_OUTPUT: {"group": 42, "variation": 3},
    POINT_TYPE_BINARY_INPUT: {"group": 2, "variation": 1},
    POINT_TYPE_BINARY_OUTPUT: {"group": 11, "variation": 1}
}

EVENT_CLASSES = {
    1: opendnp3.PointClass.Class1,
    2: opendnp3.PointClass.Class2,
    3: opendnp3.PointClass.Class3
}

GROUP_AND_VARIATIONS = {
    '1.1': opendnp3.StaticBinaryVariation.Group1Var1,
    '1.2': opendnp3.StaticBinaryVariation.Group1Var2,
    '2.1': opendnp3.EventBinaryVariation.Group2Var1,
    '2.2': opendnp3.EventBinaryVariation.Group2Var2,
    '2.3': opendnp3.EventBinaryVariation.Group2Var3,
    '3.2': opendnp3.StaticDoubleBinaryVariation.Group3Var2,
    '4.1': opendnp3.EventDoubleBinaryVariation.Group4Var1,
    '4.2': opendnp3.EventDoubleBinaryVariation.Group4Var2,
    '4.3': opendnp3.EventDoubleBinaryVariation.Group4Var3,
    '10.2': opendnp3.StaticBinaryOutputStatusVariation.Group10Var2,
    '11.1': opendnp3.EventBinaryOutputStatusVariation.Group11Var1,
    '11.2': opendnp3.EventBinaryOutputStatusVariation.Group11Var2,
    '20.1': opendnp3.StaticCounterVariation.Group20Var1,
    '20.2': opendnp3.StaticCounterVariation.Group20Var2,
    '20.5': opendnp3.StaticCounterVariation.Group20Var5,
    '20.6': opendnp3.StaticCounterVariation.Group20Var6,
    '21.1': opendnp3.StaticFrozenCounterVariation.Group21Var1,
    '21.2': opendnp3.StaticFrozenCounterVariation.Group21Var2,
    '21.5': opendnp3.StaticFrozenCounterVariation.Group21Var5,
    '21.6': opendnp3.StaticFrozenCounterVariation.Group21Var6,
    '21.9': opendnp3.StaticFrozenCounterVariation.Group21Var9,
    '21.10': opendnp3.StaticFrozenCounterVariation.Group21Var10,
    '22.1': opendnp3.EventCounterVariation.Group22Var1,
    '22.2': opendnp3.EventCounterVariation.Group22Var2,
    '22.5': opendnp3.EventCounterVariation.Group22Var5,
    '22.6': opendnp3.EventCounterVariation.Group22Var6,
    '23.1': opendnp3.EventFrozenCounterVariation.Group23Var1,
    '23.2': opendnp3.EventFrozenCounterVariation.Group23Var2,
    '23.5': opendnp3.EventFrozenCounterVariation.Group23Var5,
    '23.6': opendnp3.EventFrozenCounterVariation.Group23Var6,
    '30.1': opendnp3.StaticAnalogVariation.Group30Var1,
    '30.2': opendnp3.StaticAnalogVariation.Group30Var2,
    '30.3': opendnp3.StaticAnalogVariation.Group30Var3,
    '30.4': opendnp3.StaticAnalogVariation.Group30Var4,
    '30.5': opendnp3.StaticAnalogVariation.Group30Var5,
    '30.6': opendnp3.StaticAnalogVariation.Group30Var6,
    '32.1': opendnp3.EventAnalogVariation.Group32Var1,
    '32.2': opendnp3.EventAnalogVariation.Group32Var2,
    '32.3': opendnp3.EventAnalogVariation.Group32Var3,
    '32.4': opendnp3.EventAnalogVariation.Group32Var4,
    '32.5': opendnp3.EventAnalogVariation.Group32Var5,
    '32.6': opendnp3.EventAnalogVariation.Group32Var6,
    '32.7': opendnp3.EventAnalogVariation.Group32Var7,
    '32.8': opendnp3.EventAnalogVariation.Group32Var8,
    '40.1': opendnp3.StaticAnalogOutputStatusVariation.Group40Var1,
    '40.2': opendnp3.StaticAnalogOutputStatusVariation.Group40Var2,
    '40.3': opendnp3.StaticAnalogOutputStatusVariation.Group40Var3,
    '40.4': opendnp3.StaticAnalogOutputStatusVariation.Group40Var4,
    '42.1': opendnp3.EventAnalogOutputStatusVariation.Group42Var1,
    '42.2': opendnp3.EventAnalogOutputStatusVariation.Group42Var2,
    '42.3': opendnp3.EventAnalogOutputStatusVariation.Group42Var3,
    '42.4': opendnp3.EventAnalogOutputStatusVariation.Group42Var4,
    '42.5': opendnp3.EventAnalogOutputStatusVariation.Group42Var5,
    '42.6': opendnp3.EventAnalogOutputStatusVariation.Group42Var6,
    '42.7': opendnp3.EventAnalogOutputStatusVariation.Group42Var7,
    '42.8': opendnp3.EventAnalogOutputStatusVariation.Group42Var8,
    '50.4': opendnp3.StaticTimeAndIntervalVariation.Group50Var4,
    '121.1': opendnp3.StaticSecurityStatVariation.Group121Var1,
    '122.1': opendnp3.EventSecurityStatVariation.Group122Var1,
    '122.2': opendnp3.EventSecurityStatVariation.Group122Var2
}

POINT_TYPES_BY_GROUP = {
    # Single-Bit Binary: See DNP3 spec, Section A.2-A.5 and Table 11-17
    1: POINT_TYPE_BINARY_INPUT,         # Binary Input (static): Reporting the present value of a single-bit binary object
    2: POINT_TYPE_BINARY_INPUT,         # Binary Input Event: Reporting single-bit binary input events and flag bit changes
    # Double-Bit Binary: See DNP3 spec, Section A.4 and Table 11-15
    3: 'Double Bit Binary',             # Double-Bit Binary Input (static): Reporting present state value
    4: 'Double Bit Binary',             # Double-Bit Binary Input Event: Reporting double-bit binary input events and flag bit changes
    # Binary Output: See DNP3 spec, Section A.6-A.9 and Table 11-12
    10: POINT_TYPE_BINARY_OUTPUT,       # Binary Output (static): Reporting the present output status
    11: POINT_TYPE_BINARY_OUTPUT,       # Binary Output Event: Reporting changes to the output status or flag bits
    12: POINT_TYPE_BINARY_OUTPUT,       # Binary Output Command: Issuing control commands
    13: POINT_TYPE_BINARY_OUTPUT,       # Binary Output Command Event: Reporting control command was issued regardless of its source
    # Counter: See DNP3 spec, Section A.10-A.13 and Table 11-13
    20: 'Counter',                      # Counter: Reporting the count value
    21: 'Counter',                      # Frozen Counter: Reporting the frozen count value or changed flag bits
    22: 'Counter',                      # Counter Event: Reporting counter events
    23: 'Counter',                      # Frozen Counter Event: Reporting frozen counter events
    # Analog Input: See DNP3 spec, Section A.14-A.18 and Table 11-9
    30: POINT_TYPE_ANALOG_INPUT,        # Analog Input (static): Reporting the present value
    31: POINT_TYPE_ANALOG_INPUT,        # Frozen Analog Input (static): Reporting the frozen value
    32: POINT_TYPE_ANALOG_INPUT,        # Analog Input Event: Reporting analog input events or changes to the flag bits
    33: POINT_TYPE_ANALOG_INPUT,        # Frozen Analog Input Event: Reporting frozen analog input events
    34: POINT_TYPE_ANALOG_INPUT,        # Analog Input Reporting Deadband (static): Reading and writing analog deadband values
    # Analog Output: See DNP3 spec, Section A.19-A.22 and Table 11-10
    40: POINT_TYPE_ANALOG_OUTPUT,       # Analog Output Status (static): Reporting present value of analog outputs
    41: POINT_TYPE_ANALOG_OUTPUT,       # Analog Output (command): Controlling analog output values
    42: POINT_TYPE_ANALOG_OUTPUT,       # Analog Output Event: Reporting changes to the analog output or flag bits
    43: POINT_TYPE_ANALOG_OUTPUT,       # Analog Output Command Event: Reporting output points being commanded from any source
    # Time and Date: See DNP3 spec, Section A.23-A.25
    50: 'Time And Date',
    51: 'Time And Date',                # Time and Date Common Time-of-Occurrence
    52: 'Time And Date',                # Time Delay
    # Class Objects: See DNP3 spec, Section A.26
    60: 'Class Objects',
    # File-Control: See DNP3 spec, Section A.27
    70: 'File-Control',
    # Information Objects: See DNP3 spec, Section A.28-A.31
    80: 'Internal Indications',
    81: 'Device Storage',
    82: 'Device Profile',
    83: 'Data Set Registration',
    # Data Set Objects: See DNP3 spec, Section A.32-A.35
    85: 'Data Set Prototype',
    86: 'Data Set Descriptor',
    87: 'Data Set',
    88: 'Data Set Event',
    # Application & Status of Operation Information Objects: See DNP3 spec, Section A.36-A.37
    90: 'Application',
    91: 'Status of Requested Operation',
    # Floating-Point (Obsolete): See DNP3 spec, Section A.38
    100: 'Floating-Point',
    # Numeric Static Objects: See DNP3 spec, Section A.39-A.40
    101: 'BCD',                         # Device-dependent values in Binary-Coded Decimal form (Table 11-11)
    102: 'Unsigned Integer',
    # Octet String: See DNP3 spec, Section A.41-A.42 and Table 11-16
    110: 'Octet String',                # To convey the present value
    111: 'Octet String',                # Reporting an octet string event
    # Virtual Terminal: See DNP3 spec, Section A.43-A.44 and Table 11-18
    112: 'Virtual Terminal',            # Conveying data to the command interpreter at the outstation
    113: 'Virtual Terminal',            # Conveying data from the command interpreter at the outstation
    # Security: See DNP3 spec, Section A.45
    120: 'Authentication',
    # Security Statistic: See DNP3 spec, Section A.46-A.47 and Table 11-20
    121: 'Security Statistic',          # Reporting the current value of the statistics
    122: 'Security Statistic'           # Reporting changes to the statistics
}

_log = logging.getLogger(__name__)

class DNP3Exception(Exception):
    """Raise exceptions that are specific to the DNP3 agent. No special exception behavior is needed at this time."""
    pass

class PointDefinitions(object):
    """In-memory repository of PointDefinitions."""

    def __init__(self, point_definitions_path=None):
        self._points = {}
        self._point_variation_dict = {}
        self._point_name_dict = {}
        if point_definitions_path:
            file_path = os.path.expandvars(os.path.expanduser(point_definitions_path))
            self.load_points_from_json_file(file_path)

    def __getitem__(self, name):
        """Return the PointDefinition associated with this name. Must be unique."""
        return self.get_point_named(name)

    def load_points_from_json_file(self, point_definitions_path):
        """Load and cache a dictionary of PointDefinitions, indexed by point_type and point index."""
        if point_definitions_path:
            try:
                file_path = os.path.expandvars(os.path.expanduser(point_definitions_path))
                _log.debug('Loading DNP3 point definitions from {}.'.format(file_path))
                with open(file_path, 'r') as f:
                    # Filter comments out of the file's contents before loading it as JSON.
                    self.load_points(json.loads(self.strip_comments(f.read())))
            except Exception as err:
                raise ValueError('Problem parsing {}. Error={}'.format(point_definitions_path, err))
        else:
            _log.debug('No point_definitions_path specified, loading no points')

    def strip_comments(self, raw_string):
        """
            Return a string with comments stripped.

            Both JavaScript-style comments (//... and /*...*/) and hash (#...) comments are removed.
            Thanks to VOLTTRON volttron/platform/agent/utils.py/strip_comments() for this logic.
        """
        def _repl(match):
            return match.group(1) or ''

        _comment_re = re.compile(r'((["\'])(?:\\?.)*?\2)|(/\*.*?\*/)|((?:#|//).*?(?=\n|$))', re.MULTILINE | re.DOTALL)
        return _comment_re.sub(_repl, raw_string)

    def load_points(self, point_definitions_json):
        """Load and cache a dictionary of PointDefinitions, indexed by point_type and point index."""
        try:
            self._points = {}           # If they're already loaded, force a reload.
            for element in point_definitions_json:
                # Load a PointDefinition (or subclass) from JSON, and add it to the dictionary of points.
                # If the point defines an array, load additional definitions for each interior point in the array.
                try:
                    if element.get('type', None) != 'array':
                        point_def = PointDefinition(element)
                        self.index_point(point_def)
                    else:
                        point_def = ArrayHeadPointDefinition(element)
                        self.index_point(point_def)
                        # Load a separate ArrayPointDefinition for each interior point in the array.
                        for pt in point_def.create_array_point_definitions(element):
                            self.index_point(pt)
                except ValueError as err:
                    raise DNP3Exception('Validation error for point with json: {}: {}'.format(element, err))
        except Exception as err:
            raise ValueError('Problem parsing PointDefinitions. Error={}'.format(err))
        _log.debug('Loaded {} PointDefinitions'.format(len(self.all_points())))

    def index_point(self, point_def):
        """Add a PointDefinition to the dictionary of points."""
        point_type_dict = self._points.setdefault(point_def.point_type, {})
        if point_def.index in point_type_dict:
            error_message = 'Discarding DNP3 duplicate {0} (conflicting {1})'
            raise DNP3Exception(error_message.format(point_def, point_type_dict[point_def.index]))
        else:
            point_type_dict[point_def.index] = point_def

    def _points_dictionary(self):
        """Return a (cached) dictionary of PointDefinitions, indexed by point_type and point index."""
        return self._points

    def for_group_and_index(self, group, index):
        return self._points_dictionary().get(PointDefinition.point_type_for_group(group), {}).get(index, None)

    def point_value_for_command(self, command_type, command, index, op_type):
        """
            A DNP3 Select or Operate was received from the master. Create and return a PointValue for its data.

        :param command_type: Either 'Select' or 'Operate'.
        :param command: A ControlRelayOutputBlock or else a wrapped data value (AnalogOutputInt16, etc.).
        :param index: DNP3 index of the payload's data definition.
        :param op_type: An OperateType, or None if command_type == 'Select'.
        :return: An instance of PointValue
        """
        function_code = command.functionCode if type(command) == opendnp3.ControlRelayOutputBlock else None
        point_type = POINT_TYPE_BINARY_OUTPUT if function_code else POINT_TYPE_ANALOG_OUTPUT
        point_def = self.for_point_type_and_index(point_type, index)
        # print(point_def)
        if not point_def:
            raise DNP3Exception('No DNP3 PointDefinition found for point type {0} and index {1}'.format(point_type,
                                                                                index))
        point_value = PointValue(command_type,
                                 function_code,
                                 command.value if not function_code else None,
                                 point_def,
                                 index,
                                 op_type)
        _log.debug('Received DNP3 {}'.format(point_value))
        return point_value

    def for_point_type_and_index(self, point_type, index):
        """
            Return a PointDefinition for a given data type and index.

        @param point_type: A point type (string).
        @param index: Unique integer index of the PointDefinition to be looked up.
        @return: A PointDefinition.
        """
        return self._points_dictionary().get(point_type, {}).get(index, None)

    def _points_by_variation(self):
        """Return a (cached) dictionary of PointDefinitions, indexed by group, variation and index."""
        if not self._point_variation_dict:
            for point_type, inner_dict in self._points_dictionary().items():
                for index, point_def in inner_dict.items():
                    if self._point_variation_dict.get(point_def.group, None):
                        self._point_variation_dict[point_def.group] = {}
                    if self._point_variation_dict[point_def.group].get(point_def.variation, None):
                        self._point_variation_dict[point_def.group][point_def.variation] = {}
                    self._point_variation_dict[point_def.group][point_def.variation][index] = point_def
        return self._point_variation_dict

    def point_for_variation_and_index(self, group, variation, index):
        """Return a PointDefinition for a given group, variation and index."""
        return self._points_by_variation().get(group, {}).get(variation, {}).get(index, None)

    def points_by_name(self):
        """Return a (cached) dictionary of PointDefinition lists, indexed by name."""
        if not self._point_name_dict:
            for point_type, inner_dict in self._points_dictionary().items():
                for index, point_def in inner_dict.items():
                    point_name = point_def.name
                    if point_name not in self._point_name_dict:
                        self._point_name_dict[point_name] = []
                    self._point_name_dict[point_name].append(point_def)
        return self._point_name_dict

    def point_named(self, name, index=None):
        """
            Return the PointDefinition with the indicated name and (optionally) index.

        :param name: (string) The point's name.
        :param index: (integer) An optional index value. If supplied, search for an array point at this DNP3 index.
        :return A PointDefinition, or None if no match.
        """
        point_def_list = self.points_by_name().get(name, None)
        if point_def_list is None:
            return None                     # No points with that name

        if index is not None:
            # Return the PointDefinition with a matching index.
            for pt in point_def_list:
                if pt.index == index:
                    return pt
            return None

        # In multi-element lists, give preference to the ArrayHeadPointDefinition.
        for pt in point_def_list:
            if pt.is_array_head_point:
                return pt
        return point_def_list[0]

    def get_point_named(self, name, index=None):
        """
            Return the PointDefinition with the indicated name and (optionally) index.
            Raise an exception if none found.

        :param name: (string) The point's name.
        :param index: (integer) An optional index value. If supplied, search for an array point at this DNP3 index.
        :return A PointDefinition.
        """
        point_def = self.point_named(name, index=index)
        if point_def is None:
            if index is not None:
                raise DNP3Exception('No point named {} with index {}'.format(name, index))
            else:
                raise DNP3Exception('No point named {}'.format(name))
        return point_def

    def all_points(self):
        """Return a flat list of all PointDefinitions."""
        point_list = []
        for inner_dict in self._points_dictionary().values():
            point_list.extend(inner_dict.values())
        return point_list

    def points_by_mrid(self):
        """Return a (cached) dictionary of PointDefinition lists, indexed by name."""
        if not self._point_mrid_dict:
            for point_type, inner_dict in self._points_dictionary().items():
                for point_def in inner_dict.items():
                    print(point_def)
                    point_mrid = point_def.measurement_id
                    # if point_mrid not in self._point_mrid_dict:
                    #     self._point_mrid_dict[point_mrid] = []
                    self._point_mrid_dict[point_mrid].append(point_def)
        return self._point_mrid_dict

    def all_point_names(self):
        return self.points_by_name().keys()

    def all_point_mrid(self):
        return self.points_by_mrid().keys()

class BasePointDefinition(object):
    """Abstract superclass for PointDefinition data holders."""

    def __init__(self, element_def):
        """Initialize an instance of the PointDefinition from a dictionary of point attributes."""
        self.name = str(element_def.get('name', ''))
        self.data_type = element_def.get('data_type', None)
        self.group = element_def.get('group', None)
        self.variation = element_def.get('variation', None)
        self.index = element_def.get('index', None)
        self.description = element_def.get('description', '')
        self.measurement_id = element_def.get('measurement_id', None)
        self.measurement_type = element_def.get('measurement_type', None)
        self.magnitude = element_def.get('magnitude', None)
        self.value = element_def.get('value', None)
        self.attribute = element_def.get('attribute', None)
        #Old, unused data
        self.scaling_multiplier = element_def.get('scaling_multiplier', 1)
        self.units = element_def.get('units', '')
        self.event_class = element_def.get('event_class', 2)
        self.event_group = element_def.get('event_group', None)
        self.event_variation = element_def.get('event_variation', None)
        self.selector_block_start = element_def.get('selector_block_start', None)
        self.selector_block_end = element_def.get('selector_block_end', None)
        self.save_on_write = element_def.get('save_on_write', None)
        self.type = element_def.get('type', None)

    @property
    def is_array_point(self):
        return False

    @property
    def is_array_head_point(self):
        return False

    @property
    def is_array(self):
        return self.is_array_point or self.is_array_head_point

    def validate_point(self):
        """A PointDefinition has been created. Perform a variety of validations on it."""
        if self.type is not None and self.type not in ['array', 'selector_block']:
            raise ValueError('Invalid type for {}: {}'.format(self.name, self.type))
        if self.group is None:
            raise ValueError('Missing group for {}'.format(self.name))
        if self.variation is None:
            raise ValueError('Missing variation for {}'.format(self.name))
        if self.index is None:
            raise ValueError('Missing index for {}'.format(self.name))

        # Use intelligent defaults for event_group and event_variation based on data type
        if self.event_group is None:
            if self.point_type in EVENT_DEFAULTS_BY_POINT_TYPE:
                self.event_group = EVENT_DEFAULTS_BY_POINT_TYPE[self.point_type]["group"]
            else:
                raise ValueError('Missing event group for {}'.format(self.name))
        if self.event_variation is None:
            if self.point_type in EVENT_DEFAULTS_BY_POINT_TYPE:
                self.event_variation = EVENT_DEFAULTS_BY_POINT_TYPE[self.point_type]["variation"]
            else:
                raise ValueError('Missing event variation for {}'.format(self.name))

        if self.is_selector_block:
            if self.selector_block_start is None:
                raise ValueError('Missing selector_block_end for block named {}'.format(self.name))
            if self.selector_block_end is None:
                raise ValueError('Missing selector_block_end for block named {}'.format(self.name))
            if self.selector_block_start > self.selector_block_end:
                raise ValueError('Selector block end index < start index for block named {}'.format(self.name))
        else:
            if self.selector_block_start is not None:
                raise ValueError('selector_block_start defined for non-selector-block point {}'.format(self.name))
            if self.selector_block_end is not None:
                raise ValueError('selector_block_end defined for non-selector-block point {}'.format(self.name))

    def as_json(self):
        """Return a json description of the PointDefinition."""
        point_json = {
            "name": self.name,
            "group": self.group,
            "variation": self.variation,
            "index": self.index,
            "scaling_multiplier": self.scaling_multiplier,
            "event_class": self.event_class
        }
        if self.type is not None:
            point_json["type"] = self.type
        if self.description is not None:
            point_json["description"] = self.description
        if self.units is not None:
            point_json["units"] = self.units
        if self.event_group is not None:
            point_json["event_group"] = self.event_group
        if self.event_variation is not None:
            point_json["event_variation"] = self.event_variation
        if self.selector_block_start is not None:
            point_json["selector_block_start"] = self.selector_block_start
        if self.selector_block_end is not None:
            point_json["selector_block_end"] = self.selector_block_end
        if self.save_on_write is not None:
            point_json["save_on_write"] = self.save_on_write
        return point_json

    def __str__(self):
        """Return a string description of the PointDefinition."""
        try:
            return '{0} {1} ({2}, index={3}, type={4})'.format(self.__class__.__name__,
                                                               self.name,
                                                               self.group_and_variation,
                                                               self.index,
                                                               self.point_type)
        except UnicodeEncodeError as err:
            _log.error('Unable to convert point definition to string, err = {}'.format(err))
            return ''

    @property
    def group_and_variation(self):
        """Return a string representation of the PointDefinition's group and variation."""
        return '{0}.{1}'.format(self.group, self.variation)

    @property
    def event_group_and_variation(self):
        """Return a string representation of the PointDefinition's event group and event variation."""
        return '{0}.{1}'.format(self.event_group, self.event_variation)

    @property
    def point_type(self):
        """Return the PointDefinition's point type, derived from its group (indexing is within point type)."""
        return self.point_type_for_group(self.group)

    @property
    def is_input(self):
        """Return True if the PointDefinition is a Binary or Analog input point (i.e., sent by the Outstation)."""
        return self.point_type in [POINT_TYPE_ANALOG_INPUT, POINT_TYPE_BINARY_INPUT]

    @property
    def is_output(self):
        """Return True if the PointDefinition is a Binary or Analog output point (i.e., sent by the Master)."""
        return self.point_type in [POINT_TYPE_ANALOG_OUTPUT, POINT_TYPE_BINARY_OUTPUT]

    @property
    def is_selector_block(self):
        return self.type == 'selector_block'

    @property
    def eclass(self):
        """Return the PointDefinition's event class, or the default (2) if no event class was defined for the point."""
        return EVENT_CLASSES.get(self.event_class, 2)

    @property
    def svariation(self):
        """Return the PointDefinition's group-and-variation enumerated type."""
        return GROUP_AND_VARIATIONS.get(self.group_and_variation)

    @property
    def evariation(self):
        """Return the PointDefinition's event group-and-variation enumerated type."""
        return GROUP_AND_VARIATIONS.get(self.event_group_and_variation)

    @classmethod
    def point_type_for_group(cls, group):
        """Return the point type for a group value."""
        ptype = POINT_TYPES_BY_GROUP.get(group, None)
        if ptype is None:
            _log.error('No DNP3 point type found for group {}'.format(group))
        return ptype

class PointDefinition(BasePointDefinition):
    """Data holder for an OpenDNP3 data element."""

    def __init__(self, element_def):
        """Initialize an instance of the PointDefinition from a dictionary of point attributes."""
        super(PointDefinition, self).__init__(element_def)
        self.validate_point()

    def validate_point(self):
        """A PointDefinition has been created. Perform a variety of validations on it."""
        super(PointDefinition, self).validate_point()
        if self.type is not None and self.type != 'selector_block':
            raise ValueError('Invalid type for {}: {}'.format(self.name, self.type))

class ArrayHeadPointDefinition(BasePointDefinition):
    """Data holder for an OpenDNP3 data element that is the head point in an array."""

    def __init__(self, json_element):
        """
            Initialize an ArrayPointDefinition instance.
            An ArrayPointDefinition defines an interior point (not the head point) in an array.

        :param json_element: A JSON dictionary of point attributes.
        """
        super(ArrayHeadPointDefinition, self).__init__(json_element)
        self.array_points = json_element.get('array_points', None)
        self.array_times_repeated = json_element.get('array_times_repeated', None)
        self.array_point_definitions = []         # Holds all ArrayPointDefinitions belonging to this array.
        self.validate_point()

    def validate_point(self):
        """An ArrayHeadPointDefinition has been created. Perform a variety of validations on it."""
        super(ArrayHeadPointDefinition, self).validate_point()
        if self.type != 'array':
            raise ValueError('Invalid type {} for array named {}'.format(self.type, self.name))
        if self.array_points is None:
            raise ValueError('Missing array_points for array named {}'.format(self.name))
        if self.array_times_repeated is None:
            raise ValueError('Missing array_times_repeated for array named {}'.format(self.name))

    @property
    def is_array_point(self):
        return True

    @property
    def is_array_head_point(self):
        return True

    def as_json(self):
        """Return a json description of the ArrayHeadPointDefinition."""
        point_json = super(ArrayHeadPointDefinition, self).as_json()
        # array_points has been excluded because it's not a simple data type. Is it needed in the json?
        # if self.array_points is not None:
        #     point_json["array_points"] = self.array_points
        if self.array_times_repeated is not None:
            point_json["array_times_repeated"] = self.array_times_repeated
        return point_json

    @property
    def array_last_index(self):
        """Calculate and return the array's last index value."""
        if self.is_array_head_point:
            return self.index + self.array_times_repeated * len(self.array_points) - 1
        else:
            return None

    def create_array_point_definitions(self, element):
        """Create a separate ArrayPointDefinition for each interior point in the array."""
        for row_number in range(self.array_times_repeated):
            for column_number, pt in enumerate(self.array_points):
                # The ArrayHeadPointDefinition is already defined -- don't create a redundant definition.
                if row_number > 0 or column_number > 0:
                    array_pt_def = ArrayPointDefinition(element, self, row_number, column_number, pt['name'])
                    self.array_point_definitions.append(array_pt_def)
        return self.array_point_definitions

class ArrayPointDefinition(BasePointDefinition):
    """Data holder for an OpenDNP3 data element that is interior to an array."""

    def __init__(self, json_element, base_point_def, row, column, array_element_name):
        """
            Initialize an ArrayPointDefinition instance.
            An ArrayPointDefinition defines an interior point (not the head point) in an array.

        :param json_element: A JSON dictionary of point attributes.
        :param base_point_def: The PointDefinition of the head point in the array.
        :param row: The point's row number in the array.
        :param column: The point's column number in the array.
        :param array_element_name: The point's column name in the array.
        """
        super(ArrayPointDefinition, self).__init__(json_element)
        self.base_point_def = base_point_def
        self.row = row
        self.column = column
        self.index = self.base_point_def.index + row * len(self.base_point_def.array_points) + column
        self.array_element_name = array_element_name
        self.validate_point()

    def validate_point(self):
        """An ArrayPointDefinition has been created. Perform a variety of validations on it."""
        super(ArrayPointDefinition, self).validate_point()
        if self.type != 'array':
            raise ValueError('Invalid type {} for array named {}'.format(self.type, self.name))
        if self.base_point_def is None:
            raise ValueError('Missing base point definition for array point named {}'.format(self.name))
        if self.row is None:
            raise ValueError('Missing row number for array point named {}'.format(self.name))
        if self.column is None:
            raise ValueError('Missing column number for array point named {}'.format(self.name))
        if self.index is None:
            raise ValueError('Missing index value for array point named {}'.format(self.name))
        if self.array_element_name is None:
            raise ValueError('Missing array element name for array point named {}'.format(self.name))

    @property
    def is_array_point(self):
        return True

    @property
    def is_array_head_point(self):
        return False

    def as_json(self):
        """Return a json description of the ArrayPointDefinition."""
        point_json = super(ArrayPointDefinition, self).as_json()
        if self.row is not None:
            point_json["row"] = self.row
        if self.row is not None:
            point_json["column"] = self.column
        if self.row is not None:
            point_json["array_element_name"] = self.array_element_name
        return point_json

class PointValue(object):
    """Data holder for a point value (DNP3 measurement or command) received by an outstation."""

    def __init__(self, command_type, function_code, value, point_def, index, op_type):
        """Initialize an instance of the PointValue."""
        # Don't rely on VOLTTRON utils in this code, which may run outside of VOLTTRON
        # self.when_received = utils.get_aware_utc_now()
        self.when_received = pytz.UTC.localize(datetime.utcnow())
        self.command_type = command_type
        self.function_code = function_code
        self.value = value
        self.point_def = point_def
        self.index = index
        self.op_type = op_type

    def __str__(self):
        """Return a string description of the PointValue."""
        str_desc = 'Point value {0} ({1}, {2}.{3}, {4})'
        return str_desc.format(self.value or self.function_code,
                               self.name,
                               self.point_def.group_and_variation,
                               self.index,
                               self.command_type)

    @property
    def name(self):
        """Return the name of the PointDefinition."""
        return self.point_def.name

    def unwrapped_value(self):
        """Unwrap the point's value, returning the sample data type (e.g. an integer, binary, etc. instance)."""
        if self.value is None:
            # For binary commands, send True if function_code is LATCH_ON, False otherwise
            return self.function_code == opendnp3.ControlCode.LATCH_ON
        else:
            return self.value

class PointArray(object):
    """Data holder for  Array."""

    def __init__(self, point_def):
        """
            The "points" variable is a dictionary of dictionaries:
                0: {
                    0: PointValue,
                    1: PointValue
                },
                1: {
                    0: PointValue,
                    1: PointValue
                }
                (etc.)
            It's stored as dictionaries indexed by index numbers, not as lists,
            because there's no guarantee that array elements will arrive in order.

        :param point_def: The PointDefinition of the array's head point.
        """
        _log.debug('New Array {} starting at {} with bounds ({}, {})'.format(point_def.name,
                                                                             point_def.index,
                                                                             point_def.index,
                                                                             point_def.array_last_index))
        self.point_def = point_def
        self.points = {}

    def __str__(self):
        return 'Array, points = {}'.format(self.points)

    def as_json(self):
        """
            Return a JSON representation of the PointArray:

                [
                    {name1: val1a, name2: val2a, ...},
                    {name1: val1b, name2: val2b, ...},
                    ...
                ]
        """
        names = [d['name'] for d in self.point_def.array_points]
        json_array = []
        for pt_dict_key in sorted(self.points):
            pt_dict = self.points[pt_dict_key]
            json_array.append({name: (pt_dict[i].value if i in pt_dict else None) for i, name in enumerate(names)})
        return json_array

    def contains_index(self, index):
        """Answer whether this Array contains the point index."""
        return self.point_def.index <= index <= self.point_def.array_last_index

    def add_point_value(self, point_value):
        """Add point_value to the Array's "points" dictionary."""
        point_def = point_value.point_def
        row = 0 if point_def.is_array_head_point else point_def.row
        col = 0 if point_def.is_array_head_point else point_def.column
        if row not in self.points:
            self.points[row] = {}
        self.points[row][col] = point_value


from pycatdetector.NeuralNet import NeuralNet
import mxnet as mx
import os

def test_main():
    nn = NeuralNet()
    dirname = os.path.join(os.curdir, 'pycatdetector', 'tests', 'images')
    print()
    for filename in os.listdir(dirname):
        fullpath = os.path.join(dirname, filename)
        with open(fullpath, 'rb') as fp:
            str_image = fp.read()
            image = mx.img.imdecode(str_image)
            labels = nn.get_scored_labels(0.5, nn.analyze(image))
            if len(labels) == 0:
                assert False
            for label in labels:
                print(fullpath + "=>" + str(labels))
                assert label['label'] == 'cat'

            labels = nn.get_scored_labels(0.5, nn.analyze(fullpath))

            if len(labels) == 0:
                assert False
            for label in labels:
                print(fullpath + "=>" + str(labels))
                assert label['label'] == 'cat'

    assert 'cat' in nn.get_classes()


def prime(n):
    tp=[]
    for i in range(2,n//2+1):
        if n%i==0:
            tp.append(i)
            f=1
            print("here",tp)
            break
    return tp

num=int(input("Enter Starting Number"))
num1=int(input("Enter Last Number"))
print("Twin Prime Numbers Are")

for i in range(num, num1):

    if prime(i)==0 and prime(i+2)==0:
    #return

        #tp.append(i)
        print(i,i+2);
        #print(tp)

#tp.append(i)
#print(tp)


import time
from typing import Optional
from scanner_lib.signature_validator import SignatureValidator
from scanner_lib.display_utils import QrCodeImageDrawer
from scanner_lib.id_storage import IdStorage
from scanner_lib.qr import decode_message, read, CameraCapture
from scanner_lib.event_characteristics import EventCharacteristics

class Scanner:
    """Main class which handles scanning of QR codes"""

    def __init__(self, camera_capture: CameraCapture,
                 validator: SignatureValidator,
                 qr_code_image_drawer: QrCodeImageDrawer,
                 storage: IdStorage) -> None:
        self.camera_capture = camera_capture
        self.validator = validator
        self.qr_code_image_drawer = qr_code_image_drawer
        self.storage = storage
        self.event_characteristics: Optional[EventCharacteristics] = None

    def process_frame(self, origin_time: float) -> None:
        if time.time() - origin_time > 1:
            return
        frame = self.camera_capture.get_frame()
        read_result = read(frame)
        if read_result is None:
            self.qr_code_image_drawer.show_frame_stored(frame)
            return
        payload, frame_points = read_result
        decode_result = decode_message(payload)
        if decode_result is None:
            self.qr_code_image_drawer.show_frame_denied(
                frame, ("Wrong format", frame_points))
            return
        if self.event_characteristics is not None and decode_result.event_name != self.event_characteristics.name:
            self.qr_code_image_drawer.show_frame_denied(
                frame, ("Wrong event name", frame_points))
            return
        if self.event_characteristics is not None and decode_result.event_date != self.event_characteristics.date:
            self.qr_code_image_drawer.show_frame_denied(
                frame, ("Wrong event date", frame_points))
            return
        is_verified = self.validator.verify_message(
            decode_result.encoded, decode_result.signature)
        if not is_verified:
            self.qr_code_image_drawer.show_frame_denied(
                frame, ("Invalid signature", frame_points))
            return
        if not self.storage.try_add_id(decode_result.ticket_id):
            self.qr_code_image_drawer.show_frame_denied(
                frame, ("Duplicate ID", frame_points))
            return
        self.qr_code_image_drawer.show_frame_verified(
            frame, (decode_result.event_name, decode_result.ticket_id, frame_points))

    def set_event_characteristics(self, event_characteristics: EventCharacteristics) -> None:
        self.event_characteristics = event_characteristics


import streamlit as st
from sumarizador import *
from textblob import TextBlob

emojiText = 'https://emojipedia-us.s3.dualstack.us-west-1.amazonaws.com/thumbs/120/google/274/scroll_1f4dc.png'

nltk.download('punkt')
nltk.download('stopwords')

def app():
    col1, col2 = st.beta_columns([6,2])
    col1.title('*Resumos de Textos & Tradutor*')
    col2.image(emojiText, width = 80)

    lang = st.radio('Digite o idioma do texto:', ['Português', 'Inglês'])
    lang_translate = st.radio('Digite o idioma que você deseja ler o texto:', ['Português', 'Inglês'])
    text = st.text_area('Precione Ctrl+Enter para resumir')

    if lang == 'Português':
        if len(text) != 0:
            st.subheader('Texto Resumido')
            if lang_translate == 'Inglês':
                blob = TextBlob(''.join(sumarizador(text, 'pt-br')))
                texto_traduzido = str(blob.translate(to='en'))
                st.write(texto_traduzido)
            elif lang_translate == 'Português':
                st.write(''.join(sumarizador(text, 'pt-br')))

    elif lang == 'Inglês':
        if len(text) != 0:
            st.subheader('Texto Resumido')
            if lang_translate == 'Português':
                blob = TextBlob(''.join(sumarizador(text, 'en')))
                texto_traduzido = str(blob.translate(to='pt'))
                st.write(texto_traduzido)
            elif lang_translate == 'Inglês':
                st.write(''.join(sumarizador(text, 'en')))


blink = True

def pressed ():
    global blink
    while True:
        if input.button_is_pressed(Button.A):
            blink = False
        basic.pause (500)

control.in_background(pressed)

while blink:
    led.toggle(0, 0)
    basic.pause(1000)


from collections import Counter
class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        start = 0
        end = 0
        count = Counter()
        max_length = 0
        while start < len(s):
            if end < len(s) and count[s[end]] == 0 and ((len(count) == 0) or (len(count) > 0 and count.most_common()[0][1] <= 1)):
                count.update(s[end])
                end += 1
                if (end -start) > max_length:
                    max_length = end - start
            else:
                count.subtract(s[start])
                start += 1
            #print(start, end, count)
        return max_length