text:
bl_info = {
    "name": "Sprytile Painter",
    "author": "Jeiel Aranal",
    # Final version number must be two numerals to support x.x.00
    "version": (0, 5, 20),
    "blender": (2, 80, 0),
    "description": "A utility for creating tile based low spec scenes with paint/map editor tools",
    "location": "View3D > UI panel > Sprytile",
    "wiki_url": "http://itch.sprytile.xyz",
    "tracker_url": "https://github.com/ChemiKhazi/Sprytile/issues",
    "category": "Paint"
}

# Put Sprytile directory is sys.path so modules can be loaded
import os
import sys
import inspect
cmd_subfolder = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile(inspect.currentframe()))[0]))
if cmd_subfolder not in sys.path:
    sys.path.insert(0, cmd_subfolder)

locals_list = locals()
if "bpy" in locals_list:
    from importlib import reload
    #reload(addon_updater_ops)
    reload(sprytile_gui)
    reload(sprytile_modal)
    reload(sprytile_panel)
    reload(sprytile_utils)
    reload(sprytile_uv)
    reload(tool_build)
    reload(tool_paint)
    reload(tool_fill)
else:
    from . import sprytile_gui, sprytile_modal, sprytile_panel, sprytile_utils, sprytile_uv
    from sprytile_tools import *

import bpy
import bpy.utils.previews
from bpy.app.handlers import persistent
#from . import addon_updater_ops
from bpy.utils.toolsystem import ToolDef
from bpy.props import *
import rna_keymap_ui

class SprytileSceneSettings(bpy.types.PropertyGroup):
    def set_normal(self, value):
        if "lock_normal" not in self.keys():
            self["lock_normal"] = False

        if self["lock_normal"] is True:
            return
        if self["normal_mode"] == value:
            self["lock_normal"] = not self["lock_normal"]
            return
        self["normal_mode"] = value
        self["lock_normal"] = True

        try:
            bpy.ops.sprytile.axis_update('INVOKE_REGION_WIN')
        except:
            pass

    def get_normal(self):
        if "normal_mode" not in self.keys():
            self["normal_mode"] = 3
        return self["normal_mode"]

    normal_mode : EnumProperty(
        items=[
            ("X", "X", "World X-Axis", 1),
            ("Y", "Y", "World Y-Axis", 2),
            ("Z", "Z", "World X-Axis", 3)
        ],
        name="Normal Mode",
        description="World axis tiles will be built on",
        default='Z',
        set=set_normal,
        get=get_normal
    )

    lock_normal : BoolProperty(
        name="Lock",
        description="Lock axis used to create tiles",
        default=False
    )

    snap_translate : BoolProperty(
        name="Snap Translate",
        description="Snap pixel translations to pixel grid",
        default=True
    )

    paint_mode : EnumProperty(
        items=[
            ("PAINT", "Paint", "Advanced UV paint tools", 1),
            ("MAKE_FACE", "Build", "Only create new faces", 3),
            ("SET_NORMAL", "Set Normal", "Select a normal to use for face creation", 2),
            ("FILL", "Fill", "Fill the work plane cursor", 4)
        ],
        name="Sprytile Paint Mode",
        description="Paint mode",
        default='MAKE_FACE'
    )

    work_layer : EnumProperty(
        items=[
            ("BASE", "Base", "Base layer", 1),
            ("DECAL_1", "Decal 1", "Decal layer 1", 2)
        ],
        name="Build Layer",
        description="Layer for creating new faces",
        default='BASE'
    )

    def set_layer(self, value):
        keys = self.keys()
        if "work_layer" not in keys:
            self["work_layer"] = 1

        current_value = self.get_layer()
        value = list(value)
        for idx in range(len(value)):
            if current_value[idx] and current_value[idx] & value[idx]:
                value[idx] = False

        for idx in range(len(value)):
            if value[idx]:
                self["work_layer"] = (idx + 1)
                break

    def get_layer(self):
        keys = self.keys()
        if "work_layer" not in keys:
            self["work_layer"] = 1

        out_value = [False, False]
        index_value_lookup = 1, 2
        set_idx = index_value_lookup.index(self["work_layer"])
        out_value[set_idx] = True
        return out_value

    set_work_layer : BoolVectorProperty(
        name="Work Layer",
        description="Layer for creating new faces",
        size=2,
        get=get_layer,
        set=set_layer
    )

    work_layer_mode : EnumProperty(
        items=[
            ("MESH_DECAL", "Mesh Decal", "Create an overlay mesh. More compatible but less performant.", 1),
            ("UV_DECAL", "UV Layer", "Use UV layers. More performant in engine but requires shader support.", 2)
        ],
        name="Mode",
        description="Method used for layering",
        default="MESH_DECAL"
    )

    mesh_decal_offset : FloatProperty(
        name="Decal Offset",
        description="Distance to offset mesh decal, to prevent z-fighting",
        default=0.002,
        min=0.001,
        max=0.2,
        precision=4,
        subtype='DISTANCE',
    )

    world_pixels : IntProperty(
        name="World Pixel Density",
        description="How many pixels are displayed in one world unit",
        subtype='PIXEL',
        default=32,
        min=8,
        max=2048
    )

    paint_normal_vector : FloatVectorProperty(
        name="Srpytile Last Paint Normal",
        description="Last saved painting normal used by Sprytile",
        subtype='DIRECTION',
        default=(0.0, 0.0, 1.0)
    )

    paint_up_vector : FloatVectorProperty(
        name="Sprytile Last Paint Up Vector",
        description="Last saved painting up vector used by Sprytile",
        subtype='DIRECTION',
        default=(0.0, 1.0, 0.0)
    )

    uv_flip_x : BoolProperty(
        name="Flip X",
        description="Flip tile horizontally",
        default=False
    )
    uv_flip_y : BoolProperty(
        name="Flip Y",
        description="Flip tile vertically",
        default=False
    )
    mesh_rotate : FloatProperty(
        name="Grid Rotation",
        description="Rotation of tile",
        subtype='ANGLE',
        unit='ROTATION',
        step=9000,
        precision=0,
        min=-6.28319,
        max=6.28319,
        default=0.0
    )

    cursor_snap : EnumProperty(
        items=[
            ('VERTEX', "Vertex", "Snap cursor to nearest vertex", "SNAP_GRID", 1),
            ('GRID', "Grid", "Snap cursor to grid", "SNAP_VERTEX", 2)
        ],
        name="Cursor snap mode",
        description="Sprytile cursor snap mode"
    )

    cursor_flow : BoolProperty(
        name="Cursor Flow",
        description="Cursor automatically follows mesh building",
        default=False
    )
    paint_align : EnumProperty(
        items=[
            ('TOP_LEFT', "Top Left", "", 1),
            ('TOP', "Top", "", 2),
            ('TOP_RIGHT', "Top Right", "", 3),
            ('LEFT', "Left", "", 4),
            ('CENTER', "Center", "", 5),
            ('RIGHT', "Right", "", 6),
            ('BOTTOM_LEFT', "Bottom Left", "", 7),
            ('BOTTOM', "Bottom", "", 8),
            ('BOTTOM_RIGHT', "Bottom Right", "", 9),
        ],
        name="Paint Align",
        description="Paint alignment mode",
        default='CENTER'
    )

    def set_align_toggle(self, value, row):
        prev_value = self.get_align_toggle(row)
        row_val = 0
        if row == 'top':
            row_val = 0
        elif row == 'middle':
            row_val = 3
        elif row == 'bottom':
            row_val = 6
        else:
            return
        col_val = 0
        if value[0] and prev_value[0] != value[0]:
            col_val = 1
        elif value[1] and prev_value[1] != value[1]:
            col_val = 2
        elif value[2] and prev_value[2] != value[2]:
            col_val = 3
        else:
            return
        self["paint_align"] = row_val + col_val

    def set_align_top(self, value):
        self.set_align_toggle(value, "top")

    def set_align_middle(self, value):
        self.set_align_toggle(value, "middle")

    def set_align_bottom(self, value):
        self.set_align_toggle(value, "bottom")

    def get_align_toggle(self, row):
        if "paint_align" not in self.keys():
            self["paint_align"] = 5
        align = self["paint_align"]
        if row == 'top':
            return align == 1, align == 2, align == 3
        if row == 'middle':
            return align == 4, align == 5, align == 6
        if row == 'bottom':
            return align == 7, align == 8, align == 9
        return False, False, False

    def get_align_top(self):
        return self.get_align_toggle("top")

    def get_align_middle(self):
        return self.get_align_toggle("middle")

    def get_align_bottom(self):
        return self.get_align_toggle("bottom")

    paint_align_top : BoolVectorProperty(
        name="Align",
        size=3,
        set=set_align_top,
        get=get_align_top
    )
    paint_align_middle : BoolVectorProperty(
        name="Align",
        size=3,
        set=set_align_middle,
        get=get_align_middle
    )
    paint_align_bottom : BoolVectorProperty(
        name="Align",
        size=3,
        set=set_align_bottom,
        get=get_align_bottom
    )

    paint_hinting : BoolProperty(
        name="Hinting",
        description="Selected edge is used as X axis for UV mapping."
    )
    paint_stretch_x : BoolProperty(
        name="Stretch X",
        description="Stretch face over X axis of tile"
    )
    paint_stretch_y : BoolProperty(
        name="Stretch Y",
        description="Stretch face over Y axis of tile"
    )
    paint_edge_snap : BoolProperty(
        name="Snap To Edge",
        description="Snap UV vertices to edges of tile when close enough.",
        default=True
    )
    edge_threshold : FloatProperty(
        name="Threshold",
        description="Ratio of UV tile near to edge to apply snap",
        min=0.01,
        max=0.5,
        soft_min=0.01,
        soft_max=0.5,
        default=0.35
    )
    paint_uv_snap : BoolProperty(
        name="UV Snap",
        default=True,
        description="Snap UV vertices to texture pixels"
    )

    is_snapping : BoolProperty(
        name="Is Cursor Snap",
        description="Is cursor snapping currently activated"
    )
    is_picking : BoolProperty(
        name="Is Tile Picking",
        description="Is tile picking currently activated"
    )
    has_selection : BoolProperty(
        name="Has selection",
        description="Is there a mesh element selected"
    )
    is_grid_translate : BoolProperty(
        name="Is Grid Translate",
        description="Grid translate operator is running"
    )
    show_extra : BoolProperty(
        name="Extra UV Grid Settings",
        default=False
    )
    show_overlay : BoolProperty(
        name="Show Grid Overlay",
        description="Show grid on tile selection UI",
        default=True
    )
    outline_preview : BoolProperty(
        name="Outline Preview",
        description="Draw an outline on tile placement preview",
        default=True
    )
    auto_merge : BoolProperty(
        name="Auto Merge",
        description="Automatically merge vertices when creating faces",
        default=True
    )
    auto_join : BoolProperty(
        name="Join Multi",
        description="Join multi tile faces when possible",
        default=False
    )

    allow_backface: bpy.props.BoolProperty(
        name="Backface",
        description="Should Sprytile work on backfaces",
        default=False,
    )

    def set_reload(self, value):
        self["auto_reload"] = value
        if value is True:
            bpy.ops.sprytile.reload_auto('INVOKE_REGION_WIN')

    def get_reload(self):
        if "auto_reload" not in self.keys():
            self["auto_reload"] = False
        return self["auto_reload"]

    auto_reload: bpy.props.BoolProperty(
        name="Auto",
        description="Automatically reload images every few seconds",
        default=False,
        set=set_reload,
        get=get_reload
    )

    fill_lock_transform : BoolProperty(
        name="Lock Transforms",
        description="Filled faces keep current rotations",
        default=False,
    )

    axis_plane_display : EnumProperty(
        items=[
            ('OFF', "Off", "Always Off", "RADIOBUT_OFF", 1),
            ('ON', "On", "Always On", "RADIOBUT_ON", 2),
            ('MIDDLE_MOUSE', "View", "Only when changing view", "CAMERA_DATA", 3)
        ],
        name="Work Plane Cursor",
        description="Display mode of Work Plane Cursor",
        default='MIDDLE_MOUSE'
    )

    axis_plane_settings : BoolProperty(
        name="Axis Plane Settings",
        description="Show Work Plane Cursor settings",
        default=False
    )

    axis_plane_size : IntVectorProperty(
        name="Plane Size",
        description="Size of the Work Plane Cursor",
        size=2,
        default=(2, 2),
        min=1,
        soft_min=1
    )

    axis_plane_color : FloatVectorProperty(
        name="Plane Color",
        description="Color Work Plane Cursor is drawn with",
        size=3,
        default=(0.7, 0.7, 0.7),
        subtype='COLOR'
    )

    fill_plane_size : IntVectorProperty(
        name="Fill Plane Size",
        description="Size of the Fill Plane",
        size=2,
        default=(10, 10),
        min=1,
        soft_min=1
    )

class SprytileMaterialGridSettings(bpy.types.PropertyGroup):
    mat_id : StringProperty(
        name="Material Id",
        description="Name of the material this grid references",
        default=""
    )
    id : IntProperty(
        name="Grid ID",
        default=-1
    )
    name : StringProperty(
        name="Grid Name"
    )
    grid : IntVectorProperty(
        name="Size",
        description="Grid size, in pixels",
        min=1,
        size=2,
        subtype='XYZ',
        default=(32, 32)
    )

    def set_padding(self, value):
        current_padding = self.get_padding()
        if "grid" not in self.keys():
            self["grid"] = (32, 32)
        padding_delta = [ (value[0] - current_padding[0]) * 2, (value[1] - current_padding[1]) * 2]
        new_grid = [self["grid"][0] - padding_delta[0], self["grid"][1] - padding_delta[1]]
        if new_grid[0] < 1 or new_grid[1] < 1:
            return
        self["grid"] = (new_grid[0], new_grid[1])
        self["padding"] = value

    def get_padding(self):
        if "padding" not in self.keys():
            try:
                self["padding"] = (0, 0)
            except:
                return (0, 0)
        return self["padding"]

    padding : IntVectorProperty(
        name="Padding",
        description="Cell padding, in pixels",
        min=0,
        size=2,
        subtype='XYZ',
        default=(0, 0),
        set=set_padding,
        get=get_padding
    )

    margin : IntVectorProperty(
        name="Margin",
        description="Spacing between tiles (top, right, bottom, left)",
        min=0,
        size=4,
        subtype='XYZ',
        default=(0, 0, 0, 0)
    )
    offset : IntVectorProperty(
        name="Offset",
        description="Offset of the grid",
        subtype='TRANSLATION',
        size=2,
        default=(0, 0)
    )
    rotate : FloatProperty(
        name="UV Rotation",
        description="Rotation of UV grid",
        subtype='ANGLE',
        unit='ROTATION',
        default=0.0
    )
    tile_selection : IntVectorProperty(
        name="Tile Selection",
        size=4,
        default=(0, 0, 1, 1)
    )
    auto_pad : BoolProperty(
        name="Auto Pad",
        description="Apply a subpixel padding to tiles of this grid",
        default=True
    )
    auto_pad_offset : FloatProperty(
        name="Pad Offset",
        description="Subpixel padding amount around edges of tiles",
        default=0.05,
        min=0.05,
        max=0.20
    )

class SprytileMaterialData(bpy.types.PropertyGroup):

    def expanded_default(self):
        if 'is_expanded' not in self.keys():
            self['is_expanded'] = True

    def get_expanded(self):
        self.expanded_default()
        return self['is_expanded']

    def set_expanded(self, value):
        self.expanded_default()
        do_rebuild = self['is_expanded'] is not value
        self['is_expanded'] = value
        if do_rebuild:
            bpy.ops.sprytile.build_grid_list()

    mat_id : StringProperty(
        name="Material Id",
        description="Name of the material this grid references",
        default=""
    )
    is_expanded : BoolProperty(
        default=True,
        description="Toggle tile material",
        get=get_expanded,
        set=set_expanded
    )
    grids : CollectionProperty(type=SprytileMaterialGridSettings)

class SprytileGridDisplay(bpy.types.PropertyGroup):
    mat_id: bpy.props.StringProperty(default="")
    grid_id: bpy.props.IntProperty(default=-1)

    def get_mat_name(self):
        if self.mat_id == "":
            return ""
        data_idx = bpy.data.materials.find(self.mat_id)
        if data_idx < 0:
            return ""
        return bpy.data.materials[self.mat_id].name

    def set_mat_name(self, value):
        if self.mat_id == "":
            return
        data_idx = bpy.data.materials.find(self.mat_id)
        if data_idx < 0:
            return
        bpy.data.materials[self.mat_id].name = value
        bpy.ops.sprytile.validate_grids()

    def get_search_name(self):
        mat_name = self.get_mat_name()
        if mat_name:
            return mat_name

        return self.parent_mat_name

    mat_name: bpy.props.StringProperty(
        get=get_mat_name,
        set=set_mat_name
    )

    parent_mat_name : bpy.props.StringProperty(default="")
    parent_mat_id : bpy.props.StringProperty(default="")

    search_name : bpy.props.StringProperty(
        get=get_search_name,
        set=None
    )

class SprytileGridList(bpy.types.PropertyGroup):
    def get_idx(self):
        if "idx" not in self.keys():
            self["idx"] = 0
        return self["idx"]

    def set_idx(self, value):
        # If the selected index is a material entry
        # Move to next entry
        list_size = len(self.display)
        while value < (list_size - 1) and self.display[value].mat_id != "":
            value += 1
        value = max(0, min(len(self.display)-1, value))
        self["idx"] = value
        if value < 0 or value >= len(self.display):
            return
        # Set the object grid id to target grid
        target_entry = self.display[value]
        if target_entry.grid_id != -1:
            bpy.context.object.sprytile_gridid = target_entry.grid_id

    display: bpy.props.CollectionProperty(type=SprytileGridDisplay)
    idx: bpy.props.IntProperty(
        default=0,
        get=get_idx,
        set=set_idx
    )

class PROP_OP_SprytilePropsSetup(bpy.types.Operator):
    bl_idname = "sprytile.props_setup"
    bl_label = "Setup Sprytile data"

    def execute(self, context):
        return self.invoke(context, None)

    def invoke(self, context, event):
        self.props_setup()
        return {'FINISHED'}

    @staticmethod
    def props_setup():
        bpy.types.Scene.sprytile_data = bpy.props.PointerProperty(type=SprytileSceneSettings)
        bpy.types.Scene.sprytile_mats = bpy.props.CollectionProperty(type=SprytileMaterialData)

        bpy.types.Scene.sprytile_list = bpy.props.PointerProperty(type=SprytileGridList)

        bpy.types.Scene.sprytile_ui = bpy.props.PointerProperty(type=sprytile_gui.SprytileGuiData)

        bpy.types.Object.sprytile_gridid = IntProperty(
            name="Grid ID",
            description="Grid index used for object",
            default=-1
        )

class PROP_OP_SprytilePropsTeardown(bpy.types.Operator):
    bl_idname = "sprytile.props_teardown"
    bl_label = "Remove Sprytile data"
    bl_description = "WARNING: This will clear all Sprytile data, tile grids will be lost. Continue?"

    @classmethod
    def poll(cls, context):
        return True

    def execute(self, context):
        self.props_teardown()
        return {'FINISHED'}

    def invoke(self, context, event):
        return context.window_manager.invoke_confirm(self, event)

    def draw(self, context):
        layout = self.layout

    @staticmethod
    def props_teardown():
        del bpy.types.Scene.sprytile_data
        del bpy.types.Scene.sprytile_mats

        del bpy.types.Scene.sprytile_list

        del bpy.types.Scene.sprytile_ui

        del bpy.types.Object.sprytile_gridid

class SprytileAddonPreferences(bpy.types.AddonPreferences):
    bl_idname = __package__

    preview_transparency: bpy.props.FloatProperty(
        name="Preview Alpha",
        description="Transparency level of build preview cursor",
        default=0.8,
        min=0,
        max=1
    )

    auto_adjust_viewport_shading: bpy.props.BoolProperty(
        name="Automatically switch viewport to Material Preview mode",
        description="If enabled, viewport shading mode will change to Material Preview while using Sprytile tools",
        default=True,
    )

    auto_pixel_viewport: bpy.props.BoolProperty(
        name="Automatically setup pixel viewport",
        description="If enabled, loading a tileset will automatically setup the pixel viewport.\nDisable if you're not going for a flatshaded look",
        default=False,
    )

    default_pixel_density: bpy.props.IntProperty(
        name="Pixel Density",
        description="How many pixels are displayed in one world unit",
        default=32,
        min=8
    )

    default_grid: bpy.props.IntVectorProperty(
        name="Grid Size",
        description="Tileset grid size, in pixels",
        min=1,
        size=2,
        subtype='XYZ',
        default=(32, 32)
    )

    default_pad_offset: bpy.props.FloatProperty(
        name="Subpixel Padding",
        description="Default subpixel edge padding for tilesets",
        default=0.05,
        min=0.05,
        max=0.20
    )

    auto_grid_setup: bpy.props.BoolProperty(
        name="Automatically setup grid",
        description="If enabled, loading a tileset will set the grid size to the chosen pixel density.",
        default=True,
    )

    #def set_picker(self, value):
    #    if "tile_picker_key" not in self.keys():
    #        self["tile_picker_key"] = 1
    #    if "tile_sel_move_key" not in self.keys():
    #        self["tile_sel_move_key"] = 2
    #    if value != self["tile_sel_move_key"]:
    #        self["tile_picker_key"] = value

    #def get_picker(self):
    #    if "tile_picker_key" not in self.keys():
    #        self["tile_picker_key"] = 1
    #    return self["tile_picker_key"]

    #tile_picker_key: bpy.props.EnumProperty(
    #    items=[
    #        ("Alt", "Alt", "Press Alt to pick tiles", 1),
    #        ("Ctrl", "Ctrl", "Press Ctrl to pick tiles", 2),
    #        ("Shift", "Shift", "Press Shift to pick tiles", 3)
    #    ],
    #    name="Tile Picker Key",
    #    description="Key for using the tile picker eyedropper",
    #    default='Alt',
    #    set=set_picker,
    #    get=get_picker
    #)

    #def set_sel_move(self, value):
    #    if "tile_picker_key" not in self.keys():
    #        self["tile_picker_key"] = 1
    #    if "tile_sel_move_key" not in self.keys():
    #        self["tile_sel_move_key"] = 2
    #    if value != self["tile_picker_key"]:
    #        self["tile_sel_move_key"] = value

    #def get_sel_move(self):
    #    if "tile_sel_move_key" not in self.keys():
    #        self["tile_sel_move_key"] = 1
    #    return self["tile_sel_move_key"]

    #tile_sel_move_key: bpy.props.EnumProperty(
    #    items=[
    #        ("Alt", "Alt", "Press Alt to move tile selection", 1),
    #        ("Ctrl", "Ctrl", "Press Ctrl to move tile selection", 2),
    #        ("Shift", "Shift", "Press Shift to move tile selection", 3)
    #    ],
    #    name="Tile Selection Move Key",
    #    description="Key for moving the tile selection",
    #    default='Ctrl',
    #    set=set_sel_move,
    #    get=get_sel_move
    #)

    # addon updater preferences
    #auto_check_update: bpy.props.BoolProperty(
    #    name="Auto-check for Update",
    #    description="If enabled, auto-check for updates using an interval",
    #    default=False,
    #)
    #updater_intrval_months: bpy.props.IntProperty(
    #    name='Months',
    #    description="Number of months between checking for updates",
    #    default=0,
    #    min=0
    #)
    #updater_intrval_days: bpy.props.IntProperty(
    #    name='Days',
    #    description="Number of days between checking for updates",
    #    default=7,
    #    min=0,
    #)
    #updater_intrval_hours: bpy.props.IntProperty(
    #    name='Hours',
    #    description="Number of hours between checking for updates",
    #    default=0,
    #    min=0,
    #    max=23
    #)
    #updater_intrval_minutes: bpy.props.IntProperty(
    #    name='Minutes',
    #    description="Number of minutes between checking for updates",
    #    default=0,
    #    min=0,
    #    max=59
    #)

    def draw(self, context):
        layout = self.layout

        layout.prop(self, "preview_transparency")

        box = layout.box()

        box.label(text="Global Options")

        row = box.row()

        size_left_col = 0.3

        split = row.split(factor=size_left_col)
        col = split.column()
        col.label(text="Default Settings:")

        col = split.column(align=True)
        col.prop(self, "default_pixel_density")
        col.row().prop(self, "default_grid")
        col.prop(self, "default_pad_offset")

        row = box.row()
        split = row.split(factor=size_left_col)

        col = split.column()
        col.label(text="On Load Tileset:")

        col = split.column()
        col.prop(self, "auto_grid_setup")
        col.prop(self, "auto_pixel_viewport")

        row = box.row()
        split = row.split(factor=size_left_col)

        col = split.column()
        col.label(text="On Sprytile Edit:")

        col = split.column()
        col.prop(self, "auto_adjust_viewport_shading")

        #box = layout.box()
        #box.label(text = "Keyboard Shortcuts")
        #box.prop(self, "tile_picker_key")
        #box.prop(self, "tile_sel_move_key")

        #kc = bpy.context.window_manager.keyconfigs.user
        #km = kc.keymaps['Mesh']
        #kmi_idx = km.keymap_items.find('sprytile.modal_tool')
        #if kmi_idx >= 0:
        #    box.label(text="Tile Mode Shortcut")
        #    col = box.column()

        #    kmi = km.keymap_items[kmi_idx]
        #    km = km.active()
        #    col.context_pointer_set("keymap", km)
        #    rna_keymap_ui.draw_kmi([], kc, km, kmi, col, 0)

        #addon_updater_ops.update_settings_ui(self, context)

@ToolDef.from_fn
def toolbar_build():
    icons_dir = os.path.join(os.path.dirname(__file__), "icons")

    return dict(
        idname="sprytile.tool_build",
        label="Sprytile Build",
        description=(
            "Make new tiles"
        ),
        icon=os.path.join(icons_dir, "sprytile.build_tool"),
        keymap=sprytile_modal.VIEW3D_OP_SprytileModalTool.tool_keymaps['MAKE_FACE'],
        widget="VIEW3D_GGT_sprytile_gui",
        cursor="KNIFE"
    )

@ToolDef.from_fn
def toolbar_paint():
    icons_dir = os.path.join(os.path.dirname(__file__), "icons")

    return dict(
        idname="sprytile.tool_paint",
        label="Sprytile Paint",
        description=(
            "Paint existing tiles/faces"
        ),
        icon=os.path.join(icons_dir, "sprytile.paint_tool"),
        keymap=sprytile_modal.VIEW3D_OP_SprytileModalTool.tool_keymaps['PAINT'],
        widget="VIEW3D_GGT_sprytile_gui",
        cursor="PAINT_BRUSH"
    )

@ToolDef.from_fn
def toolbar_fill():
    def draw_settings(context, layout, tool):
        pass

    icons_dir = os.path.join(os.path.dirname(__file__), "icons")

    return dict(
        idname="sprytile.tool_fill",
        label="Sprytile Fill",
        description=(
            "Fill existing tiles/faces"
        ),
        icon=os.path.join(icons_dir, "sprytile.fill_tool"),
        keymap=sprytile_modal.VIEW3D_OP_SprytileModalTool.tool_keymaps['FILL'],
        widget="VIEW3D_GGT_sprytile_gui",
        cursor="SCROLL_XY"
    )

def get_tool_list(space_type, context_mode):
    from bl_ui.space_toolsystem_common import ToolSelectPanelHelper
    cls = ToolSelectPanelHelper._tool_class_from_space_type(space_type)
    return cls._tools[context_mode]

def register_tools():
    tools = get_tool_list('VIEW_3D', 'EDIT_MESH')

    for index, tool in enumerate(tools, 1):
        if isinstance(tool, ToolDef) and tool.label == "Transform":
            break

    tools[:index] += None, toolbar_build, toolbar_paint, toolbar_fill

def unregister_tools():
    tools = get_tool_list('VIEW_3D', 'EDIT_MESH')

    index = tools.index(toolbar_build) - 1 # None
    tools.pop(index)
    tools.remove(toolbar_build)
    tools.remove(toolbar_paint)
    tools.remove(toolbar_fill)

def generate_tool_keymap(keyconfig, paint_mode):
    keymap = keyconfig.keymaps.new(name=sprytile_modal.VIEW3D_OP_SprytileModalTool.tool_keymaps[paint_mode], space_type='VIEW_3D', region_type='WINDOW')
    km_items = keymap.keymap_items
    km_items.new("sprytile.modal_tool", 'LEFTMOUSE', 'PRESS')
    km_items.new("sprytile.tile_picker", 'LEFT_ALT', 'PRESS')
    km_items.new("sprytile.rotate_left", 'Q', 'PRESS')
    km_items.new("sprytile.rotate_right", 'E', 'PRESS')
    km_items.new("sprytile.flip_x_toggle", 'Q', 'PRESS').shift = True
    km_items.new("sprytile.flip_y_toggle", 'E', 'PRESS').shift = True

    if paint_mode in {'MAKE_FACE', 'FILL'}:
        km_items.new("sprytile.snap_cursor", 'S', 'PRESS')
        km_items.new("sprytile.set_normal", 'N', 'PRESS')

    return keymap

def setup_keymap():
    km_default = sprytile_modal.VIEW3D_OP_SprytileModalTool.default_keymaps
    km_addon = sprytile_modal.VIEW3D_OP_SprytileModalTool.addon_keymaps
    win_mgr = bpy.context.window_manager
    key_config = win_mgr.keyconfigs.addon
    key_config_default = win_mgr.keyconfigs.default

    tools = ['MAKE_FACE', 'PAINT', 'FILL']

    for tool in tools:
        keymap = generate_tool_keymap(key_config, tool)
        km_addon.append(keymap)
        keymap =  key_config_default.keymaps.new(name=sprytile_modal.VIEW3D_OP_SprytileModalTool.tool_keymaps[tool], space_type='VIEW_3D', region_type='WINDOW')
        km_default.append(keymap)

def teardown_keymap():
    for keymap in sprytile_modal.VIEW3D_OP_SprytileModalTool.addon_keymaps:
        bpy.context.window_manager.keyconfigs.addon.keymaps.remove(keymap)
    sprytile_modal.VIEW3D_OP_SprytileModalTool.addon_keymaps.clear()

    for keymap in sprytile_modal.VIEW3D_OP_SprytileModalTool.default_keymaps:
        bpy.context.window_manager.keyconfigs.default.keymaps.remove(keymap)
    sprytile_modal.VIEW3D_OP_SprytileModalTool.default_keymaps.clear()

# module classes
classes = (
        SprytileSceneSettings,
        SprytileMaterialGridSettings,
        SprytileMaterialData,
        SprytileGridDisplay,
        SprytileGridList,
        PROP_OP_SprytilePropsSetup,
        PROP_OP_SprytilePropsTeardown,
        SprytileAddonPreferences,
)

# submodule
submodules = (
    sprytile_gui,
    sprytile_modal,
    sprytile_panel,
    sprytile_utils,
    sprytile_uv,
    tool_build,
    tool_paint,
    tool_fill,
)

@persistent
def sprytile_load_handler(dummy):
    sprytile_data = bpy.context.scene.sprytile_data

    # Turn on auto reload
    if sprytile_data.auto_reload:
        bpy.ops.sprytile.reload_auto('INVOKE_REGION_WIN')

    # Reasonable assumption that a file that does not have a path
    # is new, so setup the pixel density according to preferences
    if not bpy.data.filepath:
        addon_prefs = bpy.context.preferences.addons[__package__].preferences
        if addon_prefs:
            sprytile_data.world_pixels = addon_prefs.default_pixel_density

def register():
    #addon_updater_ops.register(bl_info)

    for cl in classes:
        bpy.utils.register_class(cl)

    for submod in submodules:
        submod.register()

    PROP_OP_SprytilePropsSetup.props_setup()
    register_tools()
    setup_keymap()

    bpy.app.handlers.load_post.append(sprytile_load_handler)

def unregister():
    teardown_keymap()
    unregister_tools()
    PROP_OP_SprytilePropsTeardown.props_teardown()

    for cl in classes:
        bpy.utils.unregister_class(cl)

    for submod in submodules:
        submod.unregister()

    # Unregister self from sys.path as well
    cmd_subfolder = os.path.realpath(os.path.abspath(os.path.split(inspect.getfile(inspect.currentframe()))[0]))
    if cmd_subfolder in sys.path:
        sys.path.remove(cmd_subfolder)

if __name__ == "__main__":
    register()


text:
Package.describe({
  name: "nova:subscribe",
  summary: "Subscribe to posts, users, etc. to be notified of new activity",
  version: "0.27.1-nova",
  git: "https://github.com/TelescopeJS/telescope-subscribe-to-posts.git"
});

Package.onUse(function (api) {

  api.versionsFrom("METEOR@1.0");

  api.use([
    'nova:core@0.27.1-nova',
    'nova:users@0.27.1-nova', // this dep is needed to check users permissions
    // dependencies on posts, categories are done with nested imports to reduce explicit dependencies
  ]);

  api.mainModule("lib/modules.js", ["client"]);
  api.mainModule("lib/modules.js", ["server"]);

});

redpajama_set_name: RedPajamaGithub


text:
.article-comment {
  margin-top: 20px;
  background: #FFF;
  padding: 20px;

}
.article-comment .user_info {
  margin-right: 20px;
  line-height: 30px;
  color: #999;
  overflow: hidden;
}
.article-comment .user_info a.user_name_a {
  display: block;
  float: left;
  margin-right: 50px;
}
.article-comment .user_info img {
  width: 30px;
  height: 30px;
  display: block;
  float: left;
  margin-right: 8px;
  border-radius: 50%;
}
.article-comment .user_info .user_name {
  display: inline-block;
  float: left;
}
.article-comment .user_info .create_time {
  display: inline-block;
  float: left;
  margin-right: 10px;
}
.article-comment .comment_content {
  font-size: 14px;
  margin-top: 10px;
  color: #777;
}

.article-comment .comment-ul li:last-child {
  border-bottom: none;
}
.article-comment .comment-ul li {
    margin: 20px 0;
    border-bottom: 1px solid #ccc;
    padding-bottom: 10px;
    position: relative;
}

.article-comment .edit-comment {
  overflow: hidden;
}
.article-comment .comment-textarea {
    overflow: hidden;
    resize: none;
    height: 100px;
    border-radius: 5px;
    border: 1px solid #ccc;
    width: 580px;
    padding: 15px;
}
.article-comment .comment-submit {
    float: right;
  margin-right: 10px;
    padding: 12px 25px;
    background-color: #0078d7;
    color: #fff;
    border-radius: 5px;
    margin-top: 10px;
}
.article-comment .comment-submit:hover {
    background-color: #0c8ef5;
}
.article-comment .comment-submit.disabled {
  background-color: #838383;
}
.article-comment .load-more {
  display: none;
  line-height: 29px;
  text-align: center;
  background: #f8f8f9;
  background-color: #f1f1f2;
  background-image: -moz-linear-gradient(top,#f8f8f9,#e6e6e8);
  background-image: -ms-linear-gradient(top,#f8f8f9,#e6e6e8);
  background-image: -webkit-gradient(linear,0 0,0 100%,from(#f8f8f9),to(#e6e6e8));
  background-image: -webkit-linear-gradient(top,#f8f8f9,#e6e6e8);
  background-image: -o-linear-gradient(top,#f8f8f9,#e6e6e8);
  background-image: linear-gradient(top,#f8f8f9,#e6e6e8);
  background-repeat: repeat-x;
  text-shadow: 0 1px 0 #fff;
  border: 1px solid #bbb;
  color: #626262!important;
  box-shadow: 0 1px 0 #fff inset,0 1px 0 rgba(0,0,0,.1);
  border-radius: 4px;
  margin-bottom: 20px;
}
.article-comment .remove-comment {
    position: absolute;
    right: 10px;
    top: 10px;
}


text:
SYNONYM

#### According to
The Catalogue of Life, 3rd January 2011

#### Published in
Fl. austral. 2:414. 1864

#### Original name
null

### Remarks
null


text:
layout: page
title: Griffith Family Reunion
date: 2016-05-24
author: Jacqueline Chang
tags: weekly links, java
status: published
summary: Integer interdum lobortis velit venenatis tristique.
banner: images/banner/leisure-02.jpg
booking:
  startDate: 11/06/2017
  endDate: 11/07/2017
  ctyhocn: BOWMDHX
  groupCode: GFR
published: true
---
Etiam nisl elit, tristique quis interdum in, dignissim nec odio. Sed est est, pulvinar vitae nulla a, aliquam consequat eros. Sed tincidunt lobortis tellus ut ultrices. Maecenas est mauris, aliquam non vestibulum non, suscipit a risus. Praesent porttitor nunc in risus commodo eleifend. Integer interdum lorem sed eleifend molestie. Vestibulum nec massa non elit egestas vulputate. Donec sit amet turpis mattis, egestas ligula feugiat, cursus metus. Pellentesque pulvinar pretium elementum. Duis faucibus, magna vel consectetur euismod, nibh ipsum sagittis felis, ac mattis nisl ex sed erat. Pellentesque orci ex, semper ac mattis a, accumsan vitae felis. Nunc lorem mi, semper at velit a, interdum ornare nisi. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.

* Nunc cursus mauris non dui ullamcorper, vel congue arcu scelerisque.

Etiam malesuada, tellus sit amet congue molestie, tortor lectus convallis orci, consectetur sollicitudin sem urna nec purus. Sed tristique ante lorem, nec tempor libero vulputate at. Ut efficitur ante eu fringilla commodo. Vestibulum arcu lacus, lobortis in tellus nec, blandit aliquet nisi. Mauris molestie est quam, interdum pretium felis suscipit vitae. Vivamus pulvinar ac ligula vitae feugiat. Fusce rhoncus lorem a tortor ultrices, sit amet pretium sem semper. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.


text:
package br.com.petshopplus.safety;

import java.io.Serializable;

import javax.enterprise.context.SessionScoped;
import javax.inject.Named;

import br.com.petshopplus.model.Funcionario;
import br.com.petshopplus.model.Login;

@SessionScoped
@Named("session")
public class Session implements Serializable{

	/**
	 *
	 */
	private static final long serialVersionUID = -6689661661871422801L;
	private static Login login;
	private static Funcionario funcionario;
	private static boolean logado = false;

	public static void setSession(Login l) {
		login = l;
		funcionario = l.getFuncionario();
		logado = true;
	}

	public static Login getLogin(){
		return login;
	}

	public static Funcionario getFuncionario(){
		return funcionario;
	}

	public static boolean isLogado() {
		return logado;
	}

	public static void logout() {
		login = null;
		funcionario = null;
		logado = false;
	}
}


text:
namespace Microsoft.Data.OData.Atom
{
    #region Namespaces
    using System;
    #endregion Namespaces

    /// <summary>
    /// Atom metadata description for a link.
    /// </summary>
    public sealed class AtomLinkMetadata : ODataAnnotatable
    {
        /// <summary>The IRI value coming from EPM.</summary>
        /// <remarks>We use AtomLinkMetadata class to hold navigation links, association links etc.
        /// They convert Href property to string based on baseURI and whether Href is absolute or not.
        /// Also we do not want to rely on validation done by Uri class, so we are holding EPM values
        /// mapped to link/@href on a separate field.
        /// </remarks>
        private string hrefFromEpm;

        /// <summary>
        /// Constructor.
        /// </summary>
        public AtomLinkMetadata()
        {
        }

        /// <summary>
        /// Copy constructor.
        /// </summary>
        /// <param name="other">The <see cref="AtomLinkMetadata"/> instance to copy the values from; can be null.</param>
        internal AtomLinkMetadata(AtomLinkMetadata other)
        {
            DebugUtils.CheckNoExternalCallers();

            if (other == null)
            {
                return;
            }

            this.Relation = other.Relation;
            this.Href = other.Href;
            this.HrefLang = other.HrefLang;
            this.Title = other.Title;
            this.MediaType = other.MediaType;
            this.Length = other.Length;
            this.hrefFromEpm = other.hrefFromEpm;
        }

        /// <summary>
        /// The IRI of the link.
        /// </summary>
        public Uri Href
        {
            get;
            set;
        }

        /// <summary>
        /// The link's relation type.
        /// </summary>
        public string Relation
        {
            // TODO: should we check that the string is valid
            // The value of "rel" MUST be a string that is non-empty and matches
            // either the "isegment-nz-nc" or the "IRI" production in [RFC3987].
            get;
            set;
        }

        /// <summary>
        /// Media type of the data returned by the link.
        /// </summary>
        public string MediaType
        {
            // TODO: ckerer - validation
            // atomMediaType = xsd:string { pattern = ".+/.+" }
            get;
            set;
        }

        /// <summary>
        /// The language tag (e.g., en-US) of the resource pointed to by the link.
        /// </summary>
        public string HrefLang
        {
            // TODO: ckerer - validation
            // atomLanguageTag = xsd:string {
            //     pattern = "[A-Za-z]{1,8}(-[A-Za-z0-9]{1,8})*"
            //   }
            get;
            set;
        }

        /// <summary>
        /// A human-readable description of the link.
        /// </summary>
        public string Title
        {
            get;
            set;
        }

        /// <summary>
        /// A hint at the length of the content returned from the link.
        /// </summary>
        public int? Length
        {
            get;
            set;
        }
    }
}


text:
package com.github.chrisbanes.photoview;

import android.content.Context;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.widget.ImageView;

/**
 * A zoomable {@link ImageView}. See {@link PhotoViewAttacher} for most of the details on how the zooming
 * is accomplished
 */
public class EHPhotoView extends android.support.v7.widget.AppCompatImageView {

    private EHPhotoViewAttacher attacher;

    public EHPhotoView(Context context) {
        this(context, null);
    }

    public EHPhotoView(Context context, AttributeSet attr) {
        this(context, attr, 0);
    }

    public EHPhotoView(Context context, AttributeSet attr, int defStyle) {
        super(context, attr, defStyle);
        init();
    }

    private void init() {
        attacher = new EHPhotoViewAttacher(this);
        //We always pose as a Matrix scale type, though we can change to another scale type
        //via the attacher
        super.setScaleType(ScaleType.MATRIX);
    }

    /**
     * Get the current {@link PhotoViewAttacher} for this view. Be wary of holding on to references
     * to this attacher, as it has a reference to this view, which, if a reference is held in the
     * wrong place, can cause memory leaks.
     *
     * @return the attacher.
     */
    public EHPhotoViewAttacher getAttacher() {
        return attacher;
    }

    @Override
    public ScaleType getScaleType() {
        return attacher.getScaleType();
    }

    @Override
    public Matrix getImageMatrix() {
        return attacher.getImageMatrix();
    }

    @Override
    public void setOnLongClickListener(OnLongClickListener l) {
        attacher.setOnLongClickListener(l);
    }

    @Override
    public void setOnClickListener(OnClickListener l) {
        attacher.setOnClickListener(l);
    }

    @Override
    public void setScaleType(ScaleType scaleType) {
        if (attacher != null) {
            attacher.setScaleType(scaleType);
        }
    }

    @Override
    public void setImageDrawable(Drawable drawable) {
        super.setImageDrawable(drawable);
        // setImageBitmap calls through to this method
        if (attacher != null) {
            attacher.update();
        }
    }

    @Override
    public void setImageResource(int resId) {
        super.setImageResource(resId);
        if (attacher != null) {
            attacher.update();
        }
    }

    @Override
    public void setImageURI(Uri uri) {
        super.setImageURI(uri);
        if (attacher != null) {
            attacher.update();
        }
    }

    @Override
    protected boolean setFrame(int l, int t, int r, int b) {
        boolean changed = super.setFrame(l, t, r, b);
        if (changed) {
            attacher.update();
        }
        return changed;
    }

    public void setRotationTo(float rotationDegree) {
        attacher.setRotationTo(rotationDegree);
    }

    public void setRotationBy(float rotationDegree) {
        attacher.setRotationBy(rotationDegree);
    }

    public boolean isZoomEnabled() {
        return attacher.isZoomEnabled();
    }

    public RectF getDisplayRect() {
        return attacher.getDisplayRect();
    }

    public void getDisplayMatrix(Matrix matrix) {
        attacher.getDisplayMatrix(matrix);
    }

    public boolean setDisplayMatrix(Matrix finalRectangle) {
        return attacher.setDisplayMatrix(finalRectangle);
    }

    public float getMinimumScale() {
        return attacher.getMinimumScale();
    }

    public float getMediumScale() {
        return attacher.getMediumScale();
    }

    public float getMaximumScale() {
        return attacher.getMaximumScale();
    }

    public float getScale() {
        return attacher.getScale();
    }

    public void setAllowParentInterceptOnEdge(boolean allow) {
        attacher.setAllowParentInterceptOnEdge(allow);
    }

    public void setMinimumScale(float minimumScale) {
        attacher.setMinimumScale(minimumScale);
    }

    public void setMediumScale(float mediumScale) {
        attacher.setMediumScale(mediumScale);
    }

    public void setMaximumScale(float maximumScale) {
        attacher.setMaximumScale(maximumScale);
    }

    public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) {
        attacher.setScaleLevels(minimumScale, mediumScale, maximumScale);
    }

    public void setOnMatrixChangeListener(OnMatrixChangedListener listener) {
        attacher.setOnMatrixChangeListener(listener);
    }

    public void setOnPhotoTapListener(OnPhotoTapListener listener) {
        attacher.setOnPhotoTapListener(listener);
    }

    public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener listener) {
        attacher.setOnOutsidePhotoTapListener(listener);
    }

    public void setScale(float scale) {
        attacher.setScale(scale);
    }

    public void setScale(float scale, boolean animate) {
        attacher.setScale(scale, animate);
    }

    public void setScale(float scale, float focalX, float focalY, boolean animate) {
        attacher.setScale(scale, focalX, focalY, animate);
    }

    public void setZoomable(boolean zoomable) {
        attacher.setZoomable(zoomable);
    }

    public void setZoomTransitionDuration(int milliseconds) {
        attacher.setZoomTransitionDuration(milliseconds);
    }

    public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener newOnDoubleTapListener) {
        attacher.setOnDoubleTapListener(newOnDoubleTapListener);
    }

    public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangedListener) {
        attacher.setOnScaleChangeListener(onScaleChangedListener);
    }

    public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) {
        attacher.setOnSingleFlingListener(onSingleFlingListener);
    }
}


text:
Dustbowl API
=============

.. currentmodule:: dustbowl.api
.. autoclass:: Component
.. autoclass:: ExtensionPoint
.. autoclass:: implements
.. autoclass:: Interface
.. autoclass:: IShellCommandProvider
   :members:
.. autoclass:: IShellConsoleObjectProvider
   :members:


text:
nuget pack JobToolkit.Repository.Oracle.csproj -Prop Configuration=Release
pause


text:
/*! \file aggie.cpp
 *  \copydoc aggie.hpp
 */

#include "aggie.hpp"
#include "main.h"
#include "stringutils.hpp"
#include "config.hpp"

#include "json/json.h"

#include <fstream>
#include <sstream>
#include <algorithm>

/*! \brief Constructor
 *
 */
aggie::aggie()
	: message_listener_running(false),
	  supervisor_thread(0),
	  clients_filename_(""),
	  pm_(NULL),
	  pm_url(""),
	  received_a_pm_message(false),
	  last_received_pm_message(0),
	  sent_a_pm_message(false),
	  last_sent_pm_message(0),
	  pm_connected_time(0),
	  running(false),
	  stop_main_loop(false),
	  new_data_from_client(false),
	  new_configs(false),
	  new_connections(false),
	  previous_client_count(0)
{
	last_received_pm_message = timers.add_stopwatch();
	last_sent_pm_message = timers.add_stopwatch();
	pm_connected_time = timers.add_stopwatch();
	::pthread_mutex_init(&mutex_pm_queue, NULL);
	::pthread_mutex_init(&mutex_client_queue, NULL);
	::pthread_cond_init(&cond_message_received, NULL);
	::pthread_mutex_init(&mutex_message_received, NULL);
	::pthread_cond_init(&cond_main_action, NULL);
	::pthread_mutex_init(&mutex_main_action, NULL);
}

/*! \brief Controlled termination of the class.
 * \return Always returns #resulthandler::OK
 */
RH aggie::shutdown()
{
	RH result;
	result.set_ok();

	disconnect_pm();
	disconnect_clients();
	delete_clients();

	::pthread_mutex_destroy(&mutex_pm_queue);
	::pthread_mutex_destroy(&mutex_client_queue);
	::pthread_cond_destroy(&cond_message_received);
	::pthread_mutex_destroy(&mutex_message_received);
	return(result);
}

/*! \brief Reads list of clients from file and adds them to the system.
 *
 * The file must contain one client entry per line, with each entry
 * given as "IP-or-hostname port", e.g. "192.168.3.44 4002"
 * (without the quotes).
 *
 * \param clients_filename Filename of textfile
 * \return Always returns #resulthandler::OK
 */
RH aggie::add_clients(std::string clients_filename)
{
	RH result;
	result.set_ok();

	clients_filename_ = clients_filename;

	vout(VOUT_DEBUG) << "Reading clients from file \"" << clients_filename << "\"" << std::endlc;
	vout(VOUT_DEBUG) << " - ";

	std::ifstream file(clients_filename.c_str());
    std::string str;
    while (std::getline(file, str))
    {
    	trim(str);
    	if ((str.length() > 0) && (str.substr(0,1) != "#"))
    	{
			if (clients.size() > 0)
			{
				vout(VOUT_DEBUG) << ", ";
			}
			wclient *c = new wclient(str);
			clients.push_back(c);
			vout(VOUT_DEBUG) << c->host() << ":" << c->port();
    	}
    }
    if (clients.size() == 0)
    {
    	vout(VOUT_DEBUG) << "none found";
    }
   	vout(VOUT_DEBUG) << std::endlc;

   	file.close();

	return(result);
}

/*! \brief Reads list of clients from file and adds them to the system.
 *
 * The file must contain one client entry per line, with each entry
 * given as "IP-or-hostname port", e.g. "192.168.3.44 4002"
 * (without the quotes).
 *
 * \return Always returns #resulthandler::OK
 */
RH aggie::add_clients()
{
	return(add_clients(clients_filename_));
}

/*! \brief Removes all clients from memory.
 *
 * \return Always returns #resulthandler::OK
 */
RH aggie::delete_clients()
{
	RH result;
	result.set_ok();

	std::vector<wclient*>::iterator clients_itr = clients.begin();
	while (clients_itr != clients.end())
	{
		vout(VOUT_DEBUG) << "Deleting client " << (*clients_itr)->text() << std::endlc;
		delete (*clients_itr)->socket;
		delete *clients_itr;
		clients.erase(clients_itr);
	}

	return(result);
}

/*! \brief Tries to connect to all clients.
 *
 * #add_clients() must have been called first to load list of clients
 * into memory.
 *
 * \return #resulthandler::OK, or #resulthandler::NOT_OK if no clients were connected
 */
RH aggie::connect_clients()
{
	RH result;
	result.set_ok();

	unsigned connected_clients = 0;

	std::vector<wclient*>::iterator clients_itr = clients.begin();
	while (clients_itr != clients.end())
	{
		(*clients_itr)->socket->set_destination((*clients_itr)->host());
		(*clients_itr)->socket->set_port((*clients_itr)->port());
		vout(VOUT_INFO) << "Connecting to client " << (*clients_itr)->text() << " ... ";
		vout(VOUT_INFO).flush();
		(*clients_itr)->socket->connect();
		if ((*clients_itr)->socket->connected())
		{
			vout(VOUT_INFO) << "OK" << std::endlc;
			(*clients_itr)->socket->start_tcpsocket_client_listener(::client_listener);
			connected_clients += 1;
		}
		else
		{
			vout(VOUT_INFO) << ansi::red << "Failed" << std::endlc;
		}
		clients_itr += 1;
	}

	if (connected_clients == 0)
	{
		result.set_not_ok();
	}

	return(result);
}

/*! \brief Disconnects all clients.
 *
 * \return Always returns #resulthandler::OK
 */
RH aggie::disconnect_clients()
{
	RH result;
	result.set_ok();

	std::vector<wclient*>::iterator clients_itr = clients.begin();
	while (clients_itr != clients.end())
	{
		if ((*clients_itr)->socket->connected())
		{
			(*clients_itr)->socket->disconnect();
			if (!(*clients_itr)->socket->connected())
			{
				vout(VOUT_VERBOSE) << "Disconnected client " << (*clients_itr)->text() << std::endlc;
			}
			else
			{
				vout(VOUT_VERBOSE) << ansi::red << "Could not disconnect client " << (*clients_itr)->text() << std::endlc;
			}
		}
		clients_itr += 1;
	}

	return result;
}

wclient *aggie::find_client(tcpsocket *socket)
{
	wclient *found_client = NULL;
	std::vector<wclient*>::iterator clients_itr = clients.begin();
	while (clients_itr != clients.end())
	{
		if ((*clients_itr)->socket == socket)
		{
			found_client = (*clients_itr);
			break;
		}
		clients_itr += 1;
	}

	return(found_client);
}

/*! \brief Message handler for incoming client messages.
 *
 * \param listener Pointer to the tcpsocket instance
 *        message was receive on
 * \param data Incoming message
 */
void aggie::client_listener(tcpsocket *listener, std::string data)
{
	wclient *c = find_client(listener);
	if (c == NULL)
	{
		// We've received a client message from a client we have
		// no knowledge of. Should be impossible, but the impossible
		// happens way more often than you'd think...
		vout(VOUT_ERROR) << "Received message from unknown client: " << data << std::endlc;
	}
	else
	{
		c->received_message = true;
		//std::cout << "stopwatch " << c->last_received_message << ": pre-restart: " << timers.get_stopwatch_elapsed_time_in_ms(c->last_received_message);
		timers.restart_stopwatch(c->last_received_message);
		//std::cout << " post-restart: " << timers.get_stopwatch_elapsed_time_in_ms(c->last_received_message) << std::endl;
		if (message_listener_running)
		{
			::pthread_mutex_lock(&mutex_client_queue);
			msgqueue_client_in.push(client_message(c, data));
			::pthread_mutex_unlock(&mutex_client_queue);
			::pthread_cond_signal(&cond_message_received);
		}
		vout(VOUT_VERBOSEST) << ansi::cyan << "-> from client " << c->text() << ": " << data << std::endlc;
	}
}

/*! \brief Message handler for incoming PM-messages.
 *
 * \param listener Pointer to the websocket instance that communicates
 *                 with Presentation Manager
 * \param data Incoming message
 */
void aggie::pm_listener(websocket *listener, std::string data)
{
	received_a_pm_message = true;
	timers.restart_stopwatch(last_received_pm_message);
	::pthread_mutex_lock(&mutex_pm_queue);
	msgqueue_pm_in.push(data);
	::pthread_mutex_unlock(&mutex_pm_queue);
	::pthread_cond_signal(&cond_message_received);
	vout(VOUT_VERBOSEST) << ansi::cyan << "-> from PM: " << data << std::endlc;
}

/*! \brief Starts the websocket listener that dispatches incoming PM-messages.
 *
 * \param callback Pointer to the message handler function.
 * \return Always returns OK
 */
RH aggie::start_pm_listener(websocket::websocket_callback callback)
{
	return pm_->start_websocket_client_listener(callback);
}

/*! \brief Establishes a websocket connection to the presentation manager.
 * \param url URL of the PM.
 * \return #resulthandler::OK or #AGGIE_ERROR_INVALID_PM_URL.
 */
RH aggie::connect_pm(std::string url)
{
	RH result;
	result.set_ok();

	char host[128];
	int port;
	char path[128];

	if (url.length() == 0)
	{
		// Use previous specified url
		url = pm_url;
	}
	if (url.substr(0, 5) != "ws://")
	{
		url = "ws://" + url;
	}

	if (url.size() >= 128)
	{
		result.set_not_ok(AGGIE_ERROR_INVALID_PM_URL);
		return(result);
	}

	else if (sscanf(url.c_str(), "ws://%[^:/]:%d/%s", host, &port, path) == 3)
	{
	}
	else if (sscanf(url.c_str(), "ws://%[^:/]/%s", host, path) == 2)
	{
		port = 80;
	}
	else if (sscanf(url.c_str(), "ws://%[^:/]:%d", host, &port) == 2)
	{
		path[0] = '\0';
	}
	else if (sscanf(url.c_str(), "ws://%[^:/]", host) == 1)
	{
		port = 80;
		path[0] = '\0';
	}
	else
	{
		result.set_not_ok(AGGIE_ERROR_INVALID_PM_URL);
		return(result);
	}

	result = connect_pm(host, port, path);

	return(result);
}

/*! \brief Establishes a websocket connection to the presentation manager.
 * \param destination IP or hostname of PM
 * \param port Port number to connect to
 * \param path The path-part of the remote url
 * \return #resulthandler::OK or #resulthandler::NOT_OK
 */
RH aggie::connect_pm(std::string destination, unsigned port, std::string path)
{
	RH result;
	result.set_ok();

	disconnect_pm();

	if (path.length() != 0)
	{
		path = "/" + path;
	}

	pm_url = "ws://" + destination + ":" + int_to_string(port) + path;
	pm_ = new websocket(destination, port, path);
	result = pm_->connect();
	if (result.is_ok())
	{
		timers.restart_stopwatch(pm_connected_time);
		vout(VOUT_INFO) << "Connected to presentation manager " << pm_->url() << std::endlc;
	}
	else
	{
		pm_connected_time = 0;
		result.set_exitstatus(format_string("Failed to connect to presentation manager %s", pm_->url().c_str()));
	}

	return(result);
}

/*! \brief Disconnects from the presentation manager.
 * \return Always returns #resulthandler::OK
 */
RH aggie::disconnect_pm()
{
	RH result;
	result.set_ok();

	if (pm_ != NULL)
	{
		if (pm_->connected())
		{
			pm_->stop_websocket_client_listener();
			vout(VOUT_INFO) << "Disconnected from presentation manager " << pm_url << std::endlc;
		}
		pm_->disconnect();
		delete pm_;
		pm_ = NULL;
	}

	return(result);
}

/*! \brief Sends a message to the presentation manager.
 * \param data Message to send
 * \return resulthandler::OK if all is OK, otherwise either
 *         #SOCKET_ERROR_NOT_CONNECTED or #SOCKET_ERROR.

 */
RH aggie::send_pm(std::string data)
{
	sent_a_pm_message = true;
	timers.restart_stopwatch(last_sent_pm_message);
	return pm_->send(data);
}

/*! \brief Processes incoming messages from clients.
 *
 * Runs in it's own thread, and sleeps until the #client_listener wakes it up.
 */
void aggie::thread_entry()
{
	int condwait;

	message_listener_running = true;
	std::string current_dataset = "";

	// Dispatch loop
	while (message_listener_running)
	{
		condwait = pthread_cond_wait(&cond_message_received, &mutex_message_received);
		if (condwait == 0)
		{
			if (::pthread_mutex_trylock(&mutex_pm_queue) == 0)
			{
				// We have incoming PM messages. Deal with them.
				std::string msg = "";
				while (!msgqueue_pm_in.empty())
				{
					msg = msgqueue_pm_in.front();
					vout(VOUT_VERBOSE) << "From PM: " << msg << std::endlc;
					msgqueue_pm_in.pop();
				}
				::pthread_mutex_unlock(&mutex_pm_queue);
			}
			if (::pthread_mutex_trylock(&mutex_client_queue) == 0)
			{
				// We have incoming client messages. Deal with them.
				while (!msgqueue_client_in.empty())
				{
					client_message c = msgqueue_client_in.front();
					msgqueue_client_in.pop();
					wclient *client = c.get_client();
					if (client == NULL)
					{
						vout(VOUT_ERROR) << "Invalid client" << std::endlc;
						continue;
					}
//					vout(VOUT_VERBOSER) << "From client " << c.get_client()->text() <<  ": " << c.message() << std::endlc;
					std::string msg = c.message();
					std::replace(msg.begin(), msg.end(), '\t', ' '); // replace \t with space for easier parsing
					std::istringstream iss (msg);
					std::string token;
					iss >> token;
					int msg_id;
					string_to_int(token, msg_id);
//std::cout << "token = " << token << "    msg_id = " << msg_id << std::endl;
					if (msg_id == IPCSERVER_REPLY_BUSY)
					{
//						client->socket->disconnect();
						vout(VOUT_VERBOSE) << "Client " << client->host_and_port() << " is busy. Disconnecting." << std::endlc;
					}
					else if (msg_id == IPCSERVER_REPLY_HELP)
					{
						// Extract data columns
						client->data_column.clear(); // Erase whatever columns we had before
						while (!iss.eof())
						{
							iss >> token;
							client->data_column.push_back(token);
						}
					}
					else if (msg_id == IPCSERVER_REPLY_COMMAND_OUTPUT)
					{
						::pthread_mutex_lock(&client->request_command_mutex);
						if (client->request_command.size() > 0)
						{
							current_dataset = client->request_command.front();
						}
						::pthread_mutex_unlock(&client->request_command_mutex);
						struct wclient::client_node cn;
						struct wclient::configuration config;
						struct wclient::connection connection;
						int column_index = 0;
						while (!iss.eof())
						{
							iss >> token;
							std::string field = client->data_column[column_index];
//std::cout << ansi::green << "  " << "field = " << field<< std::endlc;
//std::cout << ansi::green << "  " << "token = " << token<< std::endlc;
							if (current_dataset == "list cn")
							{
								if (client->client_nodes_list_finished)
								{
									vout(VOUT_DEBUG2) << "Clearing client node list from client " << client->host_and_port() << std::endlc;
									client->client_nodes.clear();
									client->client_nodes_list_finished = false;
								}
								if (field == "ID") string_to_unsigned(token, cn.id);
								if (field == "AGE") string_to_unsigned(token, cn.age);
								if (field == "CR") string_to_unsigned(token, cn.cr);
								if (field == "LAT") string_to_double(token, cn.lat);
								if (field == "LON") string_to_double(token, cn.lon);
								if (field == "P2P_IP") cn.p2p_ip.set_host_and_port(token);
								if (field == "RADAC_IP") cn.radac_ip.set_host_and_port(token);
							}
							if (current_dataset == "list connections")
							{
								if (client->connection_list_finished)
								{
									vout(VOUT_DEBUG2) << "Clearing connection list from client " << client->host_and_port() << std::endlc;
									client->connections.clear();
									client->connection_list_finished = false;
								}
								if (field == "DIR") connection.dir = token;
								if (field == "PEER_ID") string_to_unsigned(token, connection.peer_id);
								if (field == "PEER_IP") connection.peer_ip.set_host_and_port(token);
							}
							if (current_dataset == "list configs")
							{
								if (client->config_list_finished)
								{
									vout(VOUT_DEBUG2) << "Clearing configuration list from client " << client->host_and_port() << std::endlc;
									client->configs.clear();
									client->config_list_finished = false;
								}
								if (field == "ID") string_to_unsigned(token, config.id);
								if (field == "AGE") string_to_unsigned(token, config.age);
								if (field == "SRC_IP") config.src_ip.set_host_and_port(token);
								if (field == "CONFIG") config.config = token;
							}
							column_index += 1;
						}
						// Complete line has been read; now store it:
						if (current_dataset == "list cn")
						{
							client->client_nodes.push_back(cn);
							vout(VOUT_DEBUG2) << client->host_and_port() << ": Added new client_node entry:" << std::endlc;
							vout(VOUT_DEBUG2) << " - ID       = " << cn.id << std::endlc;
							vout(VOUT_DEBUG2) << " - AGE      = " << cn.age << std::endlc;
							vout(VOUT_DEBUG2) << " - CR       = " << cn.cr << std::endlc;
							vout(VOUT_DEBUG2) << " - LAT      = " << cn.lat << std::endlc;
							vout(VOUT_DEBUG2) << " - LON      = " << cn.lon << std::endlc;
							vout(VOUT_DEBUG2) << " - P2P_IP   = " << cn.p2p_ip.host_and_port() << std::endlc;
							vout(VOUT_DEBUG2) << " - RADAC_IP = " << cn.radac_ip.host_and_port() << std::endlc;
							vout(VOUT_DEBUG2) << " New client_node count = " << client->client_nodes.size() << std::endlc;
						}
						if (current_dataset == "list connections")
						{
							client->connections.push_back(connection);
							vout(VOUT_DEBUG2) << client->host_and_port() << ": Added new connection entry:" << std::endlc;
							vout(VOUT_DEBUG2) << " - DIR      = " << connection.dir << std::endlc;
							vout(VOUT_DEBUG2) << " - PEER_ID  = " << connection.peer_id << std::endlc;
							vout(VOUT_DEBUG2) << " - PEER_IP  = " << connection.peer_ip.host_and_port() << std::endlc;
							vout(VOUT_DEBUG2) << " New connection count = " << client->connections.size() << std::endlc;
						}
						if (current_dataset == "list configs")
						{
							client->configs.push_back(config);
							vout(VOUT_DEBUG2) << client->host_and_port() << ": Added new configuration entry:" << std::endlc;
							vout(VOUT_DEBUG2) << " - ID       = " << config.id << std::endlc;
							vout(VOUT_DEBUG2) << " - AGE      = " << config.age << std::endlc;
							vout(VOUT_DEBUG2) << " - SRC_IP   = " << config.src_ip.host_and_port() << std::endlc;
							vout(VOUT_DEBUG2) << " - CONFIG   = " << config.config << std::endlc;
							vout(VOUT_DEBUG2) << " New configuration count = " << client->configs.size() << std::endlc;
						}
					}
					if (msg_id == IPCSERVER_REPLY_READY)
					{
						// Finished current data set
						vout(VOUT_DEBUG2) << "Finished current current_dataset \"" << current_dataset << "\" from client " << client->host_and_port() << std::endlc;

						::pthread_mutex_lock(&client->request_command_mutex);
						if (client->request_command.size() > 0)
						{
//std::cout << ansi::green << "  " << "current_dataset = " << current_dataset << std::endlc;
//std::cout << "About to pop..." << std::endl;
							client->request_command.pop();
//std::cout << "Popped" << std::endl;
						}
						::pthread_mutex_unlock(&client->request_command_mutex);
						if (current_dataset == "list cn")
						{
							client->client_nodes_list_finished = true;
							client->data_changed = true;
							new_data_from_client = true;
							vout(VOUT_DEBUG2) << "Received all client nodes from client " << client->host_and_port() << std::endlc;
						}
						if (current_dataset == "list connections")
						{
							client->connection_list_finished = true;
							client->data_changed = true;
							new_data_from_client = true;
							vout(VOUT_DEBUG2) << "Received all connections from client " << client->host_and_port() << std::endlc;
						}
						if (current_dataset == "list configs")
						{
							client->config_list_finished = true;
							client->data_changed = true;
							new_data_from_client = true;
							vout(VOUT_DEBUG2) << "Received all configs from client " << client->host_and_port() << std::endlc;
						}
//						std::cout << "Received 200" << std::endlc;
//std::cout << "[3] request_command size = " << client->request_command.size() << std::endlc;
					}

				}
				::pthread_mutex_unlock(&mutex_client_queue);
			}
		}
	}
	vout(VOUT_DEBUG) << "[aggie] exiting listener thread" << std::endlc;
}

/*! \brief Starts the message listener \ref thread_entry() "thread" that
 * processes incoming messages from clients.
 */
void aggie::start_message_listener()
{
	run();
	while (!message_listener_running); // Wait til message listener thread has been started properly
	vout(VOUT_DEBUG) << "[aggie] listener thread started" << std::endlc;

}

/*! \brief Requests information from clients.
 *
 * \param info_command Request \ref CLIENT_COMMANDS "command string" to send to the clients
 *
 */
void aggie::get_info_from_clients(std::string info_command)
{
	std::vector<wclient*>::iterator itr = clients.begin();
	while (itr != clients.end())
	{
		// How long to wait before discarding a previous request:
		unsigned timeout = (config::client_poll_interval_sec * 1000) / 2; // Always discard before new poll
		if (timeout == 0) timeout = 1000;  // One second by default
		if (timers.get_stopwatch_elapsed_time_in_ms((*itr)->last_received_message) > timeout)
		{
			if ((*itr)->request_command.size() > 0)
			{
				(*itr)->request_command.pop();
				vout(VOUT_DEBUG) << "Too long since last reply from " << (*itr)->host_and_port() << " - removing last request. request queue size is now " << (*itr)->request_command.size() << std::endlc;
			}
		}
		if (((*itr)->socket->connected()) && ((*itr)->send_command(info_command).is_not_ok()))
		{
			vout(VOUT_ERROR) << "Error in connection to " << (*itr)->host_and_port() << " - forcing disconnect" << std::endlc;
			(*itr)->socket->disconnect();
		}
		sleep_ms(50);
		itr += 1;
	}
}

void aggie::get_info_from_clients()
{
	if (clients.size() > 0)
	{
		vout(VOUT_VERBOSER) << "Polling clients" << std::endlc;
		get_info_from_clients(GET_CLIENT_NODES);
		get_info_from_clients(GET_CONFIGS);
		get_info_from_clients(GET_CONNECTIONS);
	}
}

void aggie::send_info_to_pm(wclient *client)
{
	Json::Value root;
	std::string host = client->host();
	std::string port = client->port();
	std::string cn("cn");
	std::vector<struct wclient::client_node>::iterator itr = client->client_nodes.begin();
	while(itr != client->client_nodes.end())
	{
		std::string id = int_to_string(itr->id);
		root[host][port][cn][id]["age"]      = itr->age;
		root[host][port][cn][id]["cr"]       = itr->cr;
		root[host][port][cn][id]["lat"]      = itr->lat;
		root[host][port][cn][id]["lon"]      = itr->lon;
		root[host][port][cn][id]["p2pip"]    = itr->p2p_ip.host_and_port();
		root[host][port][cn][id]["radac_ip"] = itr->radac_ip.host_and_port();
		itr += 1;
	}
	send_pm(root.toStyledString());
}

void aggie::send_client_nodes_to_pm()
{
	Json::Value root;
	Json::Value data;

	std::set<struct wclient::client_node, wclient::compare>::iterator itr = aggregated_cn_list.begin();
	while(itr != aggregated_cn_list.end())
	{
		Json::Value entry;
		std::string lat;
		std::string lon;
		double_to_string(itr->lat, lat);
		double_to_string(itr->lon, lon);
		entry["unitId"]     = itr->id;
		entry["unitPos"]    = lat + " " + lon;
		entry["unitSymbol"] = "SFGPICU---Exxx";
		entry["unitEnum"]   = "";
		entry["unitAlt"]    = 0.0;
		entry["unitSpeed"]  = 0.0;
		data.append(entry);
		itr++;
	}
	root["data"] = data;
	vout(VOUT_DEBUG) << "Sending client nodes to PM:" << std::endl << root.toStyledString() << std::endlc;
	send_pm(root.toStyledString());
}

void aggie::add_to_aggregated_list(wclient *client)
{
	std::vector<struct wclient::client_node>::iterator itr = client->client_nodes.begin();
	while(itr != client->client_nodes.end())
	{
		aggregated_cn_list.insert(*itr);
		itr += 1;
	}
}

std::vector<std::string> aggie::get_cn_list()
{
	std::vector<std::string> cn_list;

	std::set<struct wclient::client_node, wclient::compare>::iterator itr = aggregated_cn_list.begin();
	while(itr != aggregated_cn_list.end())
	{
		std::stringstream cn;
		cn << "ID: " << itr->id << " age=" << itr->age << " cr=" << itr->cr << " lat/lon=" << itr->lat << "/" << itr->lon << " p2p-ip=" << itr->p2p_ip.host_and_port() << " radac-ip=" << itr->radac_ip.host_and_port();
		cn_list.push_back(cn.str());
		itr++;
	}

	return(cn_list);
}

/*! \brief Main application loop.
 *
 * This is where all it all happens after everything has been set up.
 *
 *
 * \return Always returns resulthandler::OK
 */
RH aggie::start()
{
	RH result;
	result.set_ok();

	int poll_intervall_counter = config::client_poll_interval_sec * 1000;

	sleep_ms(CLIENTS_CN_POLL_INITIAL_DELAY_MS);
	get_info_from_clients();

	running = true;
	while (!stop_main_loop)
	{
		if (new_data_from_client)
		{
			// New list of client nodes has been received from one or more of the clients.
			// This information must be aggregated and transmitted to the PM.
			aggregated_cn_list.clear();
			std::vector<wclient*>::iterator clients_itr = clients.begin();
			while (clients_itr != clients.end())
			{
//				if ((*clients_itr)->data_changed)
//				{
					//send_info_to_pm(*clients_itr);
					add_to_aggregated_list(*clients_itr);
					(*clients_itr)->data_changed= false;
//				}
				clients_itr += 1;
			}
			new_data_from_client = false;
			if (previous_client_count != aggregated_cn_list.size())
			{
				vout(VOUT_INFO) << "Total client count: " << aggregated_cn_list.size() << std::endlc;
				previous_client_count = aggregated_cn_list.size();
			}
			send_client_nodes_to_pm();
		}

		unsigned long sleep_delay_ms = 500;
		sleep_ms(sleep_delay_ms);

		if (config::client_poll_interval_sec > 0)
		{
			// Perform repolling of clients at specified interval if requested on command line
			if (poll_intervall_counter <= 0)
			{
				vout(VOUT_VERBOSEST) << "Repolling clients after " << config::client_poll_interval_sec << " second" << (config::client_poll_interval_sec > 1 ? "s" : "") << std::endlc;
				get_info_from_clients(GET_CLIENT_NODES);
				get_info_from_clients(GET_CONFIGS);
				get_info_from_clients(GET_CONNECTIONS);
				poll_intervall_counter = config::client_poll_interval_sec * 1000;
			}
			poll_intervall_counter -= sleep_delay_ms;
		}

	}

	vout(VOUT_DEBUG) << "[aggie] leaving main thread loop" << std::endlc;

	running = false;
	return(result);
}

/*! \brief Stops the main application loop.
 *
 * \return Always returns resulthandler::OK
 */
RH aggie::stop()
{
	RH result;
	result.set_ok();

	if (running || message_listener_running)
	{
		vout(VOUT_DEBUG) << "Stopping aggie" << std::endlc;

		if (message_listener_running)
		{
			message_listener_running = false;
			::pthread_cond_signal(&cond_message_received); // Must "fake" this to wake the thread
			wait();
		}
		if (running)
		{
			stop_main_loop = true;
//		while (running); // Wait for main loop top finish
		}
	}

	return(result);
}

/*!\brief Returns an array of strings with current status.
 *
 * \return Status strings
 */
std::vector<std::string> aggie::status()
{
	std::vector<std::string> status;
	if ((pm_ == NULL) || (!pm_->connected()))
	{
		status.push_back("Not connected to PM");
	}
	else
	{
		status.push_back(format_string("Connected to PM @ %s for %ld seconds", pm_->url().c_str(),
		                 timers.get_stopwatch_elapsed_time_in_ms(pm_connected_time).value() / 1000));
	}
	status.push_back(format_string("Last message sent to PM: %s%s",
	                 (sent_a_pm_message ? int_to_string(timers.get_stopwatch_elapsed_time_in_ms(last_sent_pm_message).value() / 1000).c_str() : "never"),
                     (sent_a_pm_message ? " seconds ago" : "")));
	status.push_back(format_string("Last message received from PM: %s%s",
	                 (received_a_pm_message ? int_to_string(timers.get_stopwatch_elapsed_time_in_ms(last_received_pm_message).value() / 1000).c_str() : "never"),
	                 (received_a_pm_message ? " seconds ago" : "")));

	int clients_connected = 0;
	std::vector<wclient*>::iterator clients_itr = clients.begin();
	while (clients_itr != clients.end())
	{
		if ((*clients_itr)->socket->connected())
		{
			clients_connected += 1;
		}
		clients_itr += 1;
	}
	status.push_back(format_string("Clients connected: %d of %d", clients_connected, clients.size()));
	return(status);
}

/*!\brief Returns an array of strings with current status of specified client.
 *
 * \return Status strings
 */
std::vector<std::string> aggie::client_status(wclient *c)
{
//	std::cout << "stopwatch " << c->last_received_message << ": " << timers.get_stopwatch_elapsed_time_in_ms(c->last_received_message) << std::endl;
	std::vector<std::string> status;
	status.push_back(format_string("Client %s:%s is %sconnected", c->host().c_str(), c->port().c_str(),
	                 (c->socket->connected() ? "" : "not ")));
	status.push_back(format_string(" - Last message sent: %s%s",
	                 (c->sent_message ? int_to_string(timers.get_stopwatch_elapsed_time_in_ms(c->last_sent_message).value() / 1000).c_str() : "never"),
                     (c->sent_message ? " seconds ago" : "")));
	status.push_back(format_string(" - Last message received: %s%s",
	                 (c->received_message ? int_to_string(timers.get_stopwatch_elapsed_time_in_ms(c->last_received_message).value() / 1000).c_str() : "never"),
	                 (c->received_message ? " seconds ago" : "")));

	return(status);
}

/*!\brief Returns an array of strings with current status of all clients.
 *
 * \return Status strings
 */
std::vector<std::string> aggie::client_status()
{
	std::vector<std::string> status;
	std::vector<wclient*>::iterator clients_itr = clients.begin();
	while (clients_itr != clients.end())
	{
		std::vector<std::string> c_status;
		c_status = client_status(*clients_itr);
		status.insert(status.begin(), c_status.begin(), c_status.end()); // Much unnecessary copying here; perhaps there's a better way?
		clients_itr += 1;
	}

	return(status);
}

/*!\brief Returns an array of strings with current status of specified client.
 *
 * \return Status strings
 */
std::vector<std::string> aggie::client_status(std::string host, std::string port)
{
	wclient *c = NULL;
	std::vector<wclient*>::iterator clients_itr = clients.begin();
	while (clients_itr != clients.end())
	{
		if (((*clients_itr)->host() == host) && ((*clients_itr)->port() == port))
		{
			c = *clients_itr;
			break;
		}
		clients_itr += 1;
	}
	std::vector<std::string> status;
	if (c == NULL)
	{
		status.push_back(format_string("Client %s:%s not found", host.c_str(), port.c_str()));
	}
	else
	{
		status = client_status(c);
	}

	return(status);
}


text:
<?xml version="1.0"?>
<typesystem package="com.trolltech.qt.webkit">
<rejection class="WebCore"/>
<suppress-warning text="WARNING(MetaJavaBuilder) :: skipping field 'QWebPluginFactory_Plugin::mimeTypes' with unmatched type 'QList<MimeType>'"/>
    <namespace-type name="WebCore"/>

    <enum-type name="QWebSettings::FontFamily"/>
    <enum-type name="QWebSettings::FontSize"/>
    <enum-type name="QWebSettings::WebGraphic"/>
    <enum-type name="QWebSettings::WebAttribute">
        <reject-enum-value name="LocalStorageEnabled"/>
    </enum-type>
  <enum-type name="QWebSettings::ThirdPartyCookiePolicy"/>
  <enum-type name="QWebPage::VisibilityState"/>
  <enum-type name="QWebFrame::ValueOwnership"/>
  <enum-type name="QWebSecurityOrigin::SubdomainSetting"/>

  <enum-type name="QWebFrame::RenderLayer" flags="QWebFrame::RenderLayers"/>

    <enum-type name="QWebPage::Feature"/>
    <enum-type name="QWebPage::PermissionPolicy"/>
    <enum-type name="QWebPage::Extension"/>
    <enum-type name="QWebPage::NavigationType"/>
    <enum-type name="QWebPage::WebAction"/>
    <enum-type name="QWebPage::WebWindowType"/>
    <enum-type name="QWebPage::FindFlag" flags="QWebPage::FindFlags"/>
    <enum-type name="QWebPage::LinkDelegationPolicy"/>
    <enum-type name="QWebPluginFactory::Extension"/>
    <enum-type name="QWebElement::StyleResolveStrategy"/>
    <enum-type name="QWebHistory::HistoryStateVersion">
        <reject-enum-value name="DefaultHistoryVersion"/>
    </enum-type>
    <enum-type name="QWebPage::ErrorDomain"/>

    <value-type name="QWebElementCollection"/>

    <object-type name="QGraphicsWebView"/>
    <object-type name="QWebView">
    </object-type>
    <object-type name="QWebFrame">
        <modify-function signature="event(QEvent*)" remove="all"/>
    </object-type>
    <object-type name="QWebPage::ExtensionOption"/>
    <object-type name="QWebPage::ChooseMultipleFilesExtensionOption"/>
    <object-type name="QWebPage::ExtensionReturn"/>
    <object-type name="QWebPage::ChooseMultipleFilesExtensionReturn"/>
    <object-type name="QWebHistory">
      <modify-function signature="saveState(QWebHistory::HistoryStateVersion)const">
      </modify-function>
    </object-type>
    <object-type name="QWebSettings"/>
    <object-type name="QWebPage">
    </object-type>

    <value-type name="QWebHitTestResult">
      <modify-function signature="operator=(QWebHitTestResult)" remove="all"/>
    </value-type>

    <value-type name="QWebHistoryItem">
        <modify-function signature="operator=(QWebHistoryItem)" remove="all"/>
    </value-type>

    <object-type name="QWebHistoryInterface">
    </object-type>

    <object-type name="QWebPluginFactory"/>
    <object-type name="QWebPluginDatabase"/>
    <object-type name="QWebInspector"/>

    <value-type name="QWebPluginInfo"/>
    <value-type name="QWebElement"/>
    <value-type name="QWebPluginFactory::Plugin"/>
    <value-type name="QWebPluginFactory::MimeType"/>
    <value-type name="QWebSecurityOrigin">
    </value-type>
    <value-type name="QWebDatabase">
    </value-type>
    <object-type name="QWebPluginFactory::ExtensionOption"/>
    <object-type name="QWebPluginFactory::ExtensionReturn"/>
    <object-type name="QWebPage::ErrorPageExtensionOption"/>
    <object-type name="QWebPage::ErrorPageExtensionReturn"/>

    <suppress-warning text="WARNING(MetaJavaBuilder) :: skipping function 'QWebFrame::QWebFrame', unmatched parameter type 'QWebFrameData*'"/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: skipping function 'QWebSettings::QWebSettings', unmatched parameter type 'WebCore::Settings*'"/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: skipping function 'QWebPluginInfo::QWebPluginInfo', unmatched parameter type 'WebCore::PluginPackage*'"/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: skipping function 'QWebElement::enclosingElement', unmatched parameter type 'WebCore::Node*'"/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: skipping function 'QWebElement::QWebElement', unmatched parameter type 'WebCore::Node*'"/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: skipping function 'QWebElement::QWebElement', unmatched parameter type 'WebCore::Element*'"/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: skipping field 'QWebPluginFactory_Plugin::mimeTypes' with unmatched type 'QList<MimeType>'"/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: Class 'QWebPluginInfo' has equals operators but no qHash() function. Hashcode of objects will consistently be 0."/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: Class 'QWebPluginFactory_MimeType' has equals operators but no qHash() function. Hashcode of objects will consistently be 0."/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: Class 'QWebElement' has equals operators but no qHash() function. Hashcode of objects will consistently be 0."/>

    <!-- Needed to avoid warnings on compilers that don't support webkit -->
    <suppress-warning text="WARNING(MetaJavaBuilder) :: type 'QWebPluginFactory::Plugin' is specified in typesystem, but not defined. This could potentially lead to compilation errors."/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: type 'QWebPluginFactory::ExtensionReturn' is specified in typesystem, but not defined. This could potentially lead to compilation errors."/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: type 'QWebPluginFactory::MimeType' is specified in typesystem, but not defined. This could potentially lead to compilation errors."/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: namespace 'com.trolltech.qt.webkit.QWebPage' for enum 'Extension' is not declared"/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: type 'QWebPluginFactory::ExtensionOption' is specified in typesystem, but not defined. This could potentially lead to compilation errors."/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: namespace 'com.trolltech.qt.webkit.QWebPluginFactory' for enum 'Extension' is not declared"/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: type 'QWebPluginFactory' is specified in typesystem, but not defined. This could potentially lead to compilation errors."/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: namespace 'com.trolltech.qt.webkit.QWebPage' for enum 'NavigationRequestResponse' is not declared"/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: namespace 'com.trolltech.qt.webkit.QWebSettings' for enum 'WebAttribute' is not declared"/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: type 'QWebSettings' is specified in typesystem, but not defined. This could potentially lead to compilation errors."/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: type 'QWebHistoryItem' is specified in typesystem, but not defined. This could potentially lead to compilation errors."/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: type 'QWebHistoryInterface' is specified in typesystem, but not defined. This could potentially lead to compilation errors."/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: namespace 'com.trolltech.qt.webkit.QWebPage' for enum 'WebAction' is not declared"/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: namespace 'com.trolltech.qt.webkit.QWebSettings' for enum 'FontSize' is not declared"/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: namespace 'com.trolltech.qt.webkit.QWebSettings' for enum 'WebGraphic' is not declared"/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: namespace 'com.trolltech.qt.webkit.QWebPage' for enum 'NavigationType' is not declared"/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: namespace 'com.trolltech.qt.webkit.QWebSettings' for enum 'FontType' is not declared"/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: type 'QWebFrame' is specified in typesystem, but not defined. This could potentially lead to compilation errors."/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: type 'QWebHistory' is specified in typesystem, but not defined. This could potentially lead to compilation errors."/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: type 'QWebView' is specified in typesystem, but not defined. This could potentially lead to compilation errors."/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: type 'QWebPage' is specified in typesystem, but not defined. This could potentially lead to compilation errors."/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: type 'QWebHitTestResult' is specified in typesystem, but not defined. This could potentially lead to compilation errors."/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: namespace 'com.trolltech.qt.webkit.QWebPage' for enum 'FindFlag' is not declared"/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: namespace 'com.trolltech.qt.webkit.QWebPage' for enum 'LinkDelegationPolicy' is not declared"/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: namespace 'com.trolltech.qt.webkit.QWebSettings' for enum 'FontFamily' is not declared"/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: namespace 'com.trolltech.qt.webkit.QWebPage' for enum 'WebWindowType' is not declared"/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: skipping function 'QWebFrame::addToJavaScriptWindowObject', unmatched parameter type 'QScriptEngine::ValueOwnership'"/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: type 'QWebDatabase' is specified in typesystem, but not defined. This could potentially lead to compilation errors."/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: type 'QWebPage::ExtensionReturn' is specified in typesystem, but not defined. This could potentially lead to compilation errors."/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: type 'QWebPage::ExtensionOption' is specified in typesystem, but not defined. This could potentially lead to compilation errors."/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: type 'QWebPage::ChooseMultipleFilesExtensionOption' is specified in typesystem, but not defined. This could potentially lead to compilation errors."/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: type 'QWebSecurityOrigin' is specified in typesystem, but not defined. This could potentially lead to compilation errors."/>
    <suppress-warning text="WARNING(MetaJavaBuilder) :: type 'QWebPage::ChooseMultipleFilesExtensionReturn' is specified in typesystem, but not defined. This could potentially lead to compilation errors."/>

</typesystem>


text:
using System;
using System.Collections.Generic;
using System.Text;

namespace Andersc.CodeLib.Common
{
    public enum DateTimeUnit
    {
        /// <summary>
        /// one in a million of a second. abbr: mics.
        /// </summary>
        //MicroSecond

        /// <summary>
        /// one in a thousand of a second. abbr: mils.
        /// </summary>
        MilliSecond,
        /// <summary>
        /// abbr: s.
        /// </summary>
        Second,
        /// <summary>
        /// abbr: mi
        /// </summary>
        Minute,
        /// <summary>
        /// abbr: h
        /// </summary>
        Hour,
        /// <summary>
        /// abbr: d
        /// </summary>
        Day,
        /// <summary>
        /// abbr: w
        /// </summary>
        Week,
        /// <summary>
        /// abbr: m
        /// </summary>
        Month,
        /// <summary>
        /// abbr: y
        /// </summary>
        Year
    }

    public class DateTimeHelper
    {
        public const int DaysOfWeek = 7;

        /// <summary>
        /// Get a datetime unit by unit string.
        /// </summary>
        /// <param name="unit"></param>
        /// <returns>corresponding unit.</returns>
        /// <exception cref="ArgumentException">if the unit argument is null or empty.</exception>
        /// <exception cref="Exception">if the unit string is invalid.</exception>
        public static DateTimeUnit GetDateTimeUnit(string unit)
        {
            if (string.IsNullOrEmpty(unit))
            {
                throw new ArgumentException("unit must not be null or empty.");
            }

            switch (unit.ToLower())
            {
                case "y":
                case "year":
                    return DateTimeUnit.Year;
                case "m":
                case "month":
                    return DateTimeUnit.Month;
                case "w":
                case "week":
                    return DateTimeUnit.Week;
                case "d":
                case "day":
                    return DateTimeUnit.Day;
                case "h":
                case "hour":
                    return DateTimeUnit.Hour;
                case "mi":
                case "minute":
                    return DateTimeUnit.Minute;
                case "s":
                case "second":
                    return DateTimeUnit.Second;
                case "mils":
                case "millisecond":
                    return DateTimeUnit.MilliSecond;
                default:
                    throw new ArgumentException("invalid unit string.");
            }
        }

        /// <summary>
        /// Gets the date time unit.
        /// </summary>
        /// <param name="original">The original.</param>
        /// <param name="unit">The unit.</param>
        /// <param name="amount">The amount.</param>
        /// <returns></returns>
        public static DateTime AddDateTime(DateTime original, DateTimeUnit unit, double amount)
        {
            DateTime newDateTime = original;
            switch (unit)
            {
                case DateTimeUnit.Year:
                    newDateTime = newDateTime.AddYears((int)amount);
                    break;
                case DateTimeUnit.Month:
                    newDateTime = newDateTime.AddMonths((int)amount);
                    break;
                case DateTimeUnit.Week:
                    newDateTime = newDateTime.AddDays(DaysOfWeek * amount);
                    break;
                case DateTimeUnit.Day:
                    newDateTime = newDateTime.AddDays(amount);
                    break;
                case DateTimeUnit.Hour:
                    newDateTime = newDateTime.AddHours(amount);
                    break;
                case DateTimeUnit.Minute:
                    newDateTime = newDateTime.AddMinutes(amount);
                    break;
                case DateTimeUnit.Second:
                    newDateTime = newDateTime.AddSeconds(amount);
                    break;
                case DateTimeUnit.MilliSecond:
                    newDateTime = newDateTime.AddMilliseconds(amount);
                    break;
                default:
                    break;
            }

            return newDateTime;
        }
    }
}


text:
package org.apache.samza.test.operator;

import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.samza.Partition;
import org.apache.samza.system.StreamSpec;
import org.apache.samza.system.SystemStreamMetadata;
import org.apache.samza.system.SystemStreamMetadata.SystemStreamPartitionMetadata;
import org.apache.samza.system.kafka.KafkaSystemAdmin;
import org.apache.samza.util.ExponentialSleepStrategy;
import org.junit.Assert;
import org.junit.Test;

import java.util.Collections;
import java.util.List;

/**
 * Test driver for {@link RepartitionJoinWindowApp}.
 */
public class TestRepartitionJoinWindowApp extends StreamApplicationIntegrationTestHarness {

  void initializeTopics(String input1, String input2, String output) {
    // create topics
    createTopic(input1, 2);
    createTopic(input2, 2);
    createTopic(output, 1);

    // create events for the following user activity.
    // userId: (viewId, pageId, (adIds))
    // u1: (v1, p1, (a1)), (v2, p2, (a3))
    // u2: (v3, p1, (a1)), (v4, p3, (a5))
    produceMessage(input1, 0, "p1", "{\"viewId\":\"v1\",\"pageId\":\"p1\",\"userId\":\"u1\"}");
    produceMessage(input1, 1, "p2", "{\"viewId\":\"v2\",\"pageId\":\"p2\",\"userId\":\"u1\"}");
    produceMessage(input1, 0, "p1", "{\"viewId\":\"v3\",\"pageId\":\"p1\",\"userId\":\"u2\"}");
    produceMessage(input1, 1, "p3", "{\"viewId\":\"v4\",\"pageId\":\"p3\",\"userId\":\"u2\"}");

    produceMessage(input2, 0, "a1", "{\"viewId\":\"v1\",\"adId\":\"a1\"}");
    produceMessage(input2, 0, "a3", "{\"viewId\":\"v2\",\"adId\":\"a3\"}");
    produceMessage(input2, 0, "a1", "{\"viewId\":\"v3\",\"adId\":\"a1\"}");
    produceMessage(input2, 0, "a5", "{\"viewId\":\"v4\",\"adId\":\"a5\"}");
  }

  @Test
  public void testRepartitionJoinWindowAppWithoutDeletionOnCommit() throws Exception {
    String inputTopicName1 = "page-views";
    String inputTopicName2 = "ad-clicks";
    String outputTopicName = "user-ad-click-counts";

    KafkaSystemAdmin.deleteMessagesCalled_$eq(false);

    initializeTopics(inputTopicName1, inputTopicName2, outputTopicName);

    // run the application
    RepartitionJoinWindowApp app = new RepartitionJoinWindowApp();
    String appName = "UserPageAdClickCounter";
    Map<String, String> configs = new HashMap<>();
    configs.put("systems.kafka.samza.delete.committed.messages", "false");
    configs.put(RepartitionJoinWindowApp.INPUT_TOPIC_NAME_1_PROP, inputTopicName1);
    configs.put(RepartitionJoinWindowApp.INPUT_TOPIC_NAME_2_PROP, inputTopicName2);
    configs.put(RepartitionJoinWindowApp.OUTPUT_TOPIC_NAME_PROP, outputTopicName);

    runApplication(app, appName, configs);

    // consume and validate result
    List<ConsumerRecord<String, String>> messages = consumeMessages(Collections.singletonList(outputTopicName), 2);
    Assert.assertEquals(2, messages.size());

    Assert.assertFalse(KafkaSystemAdmin.deleteMessagesCalled());
  }

  @Test
  public void testRepartitionJoinWindowAppAndDeleteMessagesOnCommit() throws Exception {
    String inputTopicName1 = "page-views2";
    String inputTopicName2 = "ad-clicks2";
    String outputTopicName = "user-ad-click-counts2";

    initializeTopics(inputTopicName1, inputTopicName2, outputTopicName);

    // run the application
    RepartitionJoinWindowApp app = new RepartitionJoinWindowApp();
    final String appName = "UserPageAdClickCounter2";
    Map<String, String> configs = new HashMap<>();
    configs.put("systems.kafka.samza.delete.committed.messages", "true");
    configs.put(RepartitionJoinWindowApp.INPUT_TOPIC_NAME_1_PROP, inputTopicName1);
    configs.put(RepartitionJoinWindowApp.INPUT_TOPIC_NAME_2_PROP, inputTopicName2);
    configs.put(RepartitionJoinWindowApp.OUTPUT_TOPIC_NAME_PROP, outputTopicName);

    runApplication(app, appName, configs);

    // consume and validate result
    List<ConsumerRecord<String, String>> messages = consumeMessages(Collections.singletonList(outputTopicName), 2);
    Assert.assertEquals(2, messages.size());

    for (ConsumerRecord<String, String> message : messages) {
      String key = message.key();
      String value = message.value();
      Assert.assertTrue(key.equals("u1") || key.equals("u2"));
      Assert.assertEquals("2", value);
    }

    // Verify that messages in the intermediate stream will be deleted in 10 seconds
    long startTimeMs = System.currentTimeMillis();
    for (StreamSpec spec: runner.getExecutionPlan(app).getIntermediateStreams()) {
      long remainingMessageNum = -1;

      while (remainingMessageNum != 0 && System.currentTimeMillis() - startTimeMs < 10000) {
        remainingMessageNum = 0;
        SystemStreamMetadata metadatas = systemAdmin.getSystemStreamMetadata(
            new HashSet<>(Arrays.asList(spec.getPhysicalName())), new ExponentialSleepStrategy.Mock(3)
        ).get(spec.getPhysicalName()).get();

        for (Map.Entry<Partition, SystemStreamPartitionMetadata> entry : metadatas.getSystemStreamPartitionMetadata().entrySet()) {
          SystemStreamPartitionMetadata metadata = entry.getValue();
          remainingMessageNum += Long.parseLong(metadata.getUpcomingOffset()) - Long.parseLong(metadata.getOldestOffset());
        }
      }
      Assert.assertEquals(0, remainingMessageNum);
    }

  }

  @Test
  public void testBroadcastApp() {
    String inputTopicName1 = "page-views";
    String inputTopicName2 = "ad-clicks";
    String outputTopicName = "user-ad-click-counts";
    Map<String, String> configs = new HashMap<>();
    configs.put(BroadcastAssertApp.INPUT_TOPIC_NAME_PROP, inputTopicName1);

    initializeTopics(inputTopicName1, inputTopicName2, outputTopicName);
    runApplication(new BroadcastAssertApp(), "BroadcastTest", configs);
  }
}