#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,

bl_info = {
    "name": "Layer Management",
    "author": "Alfonso Annarumma, Bastien Montagne",
    "version": (1, 5, 3),
    "blender": (2, 7, 5),
    "location": "Toolshelf > Layers Tab",
    "warning": "",
    "description": "Display and Edit Layer Name",
    "wiki_url": "http://example.com",
    "category": "3D View",
}

import bpy
from bpy.types import Menu, Panel, UIList, PropertyGroup
from bpy.props import StringProperty, BoolProperty, IntProperty, CollectionProperty, BoolVectorProperty, PointerProperty
from bpy.app.handlers import persistent

EDIT_MODES = {'EDIT_MESH', 'EDIT_CURVE', 'EDIT_SURFACE', 'EDIT_METABALL', 'EDIT_TEXT', 'EDIT_ARMATURE'}

NUM_LAYERS = 20

FAKE_LAYER_GROUP = [True] * NUM_LAYERS

class NamedLayer(PropertyGroup):
    name = StringProperty(name="Layer Name")
    use_lock = BoolProperty(name="Lock Layer", default=False)
    use_render = BoolProperty(name="Render", default=True)
    use_object_select = BoolProperty(name="Object Select", default=True)
    use_wire = BoolProperty(name="Wire Layer", default=False)

class NamedLayers(PropertyGroup):
    layers = CollectionProperty(type=NamedLayer)
    use_hide_empty_layers = BoolProperty(name="Hide Empty Layer", default=False)
    use_extra_options = BoolProperty(name="Show Extra Options", default=True)
    use_layer_indices = BoolProperty(name="Show Layer Indices", default=False)
    use_classic = BoolProperty(name="Classic", default=False, description="Use a classic layer selection visibility")

    use_init = BoolProperty(default=True, options={'HIDDEN'})

# Stupid, but only solution currently is to use a handler to init that layers collection...
@persistent
def check_init_data(scene):
    namedlayers = scene.namedlayers
    if namedlayers.use_init:
        while namedlayers.layers:
            namedlayers.layers.remove(0)
        for i in range(NUM_LAYERS):
            layer = namedlayers.layers.add()
            layer.name = "Layer%.2d" % (i + 1)  # Blender use layer nums starting from 1, not 0.
        namedlayers.use_init = False

class LayerGroup(PropertyGroup):
    use_toggle = BoolProperty(name="", default=False)
    use_wire = BoolProperty(name="", default=False)
    use_lock = BoolProperty(name="", default=False)
    use_render = BoolProperty(name="", default=True)

    layers = BoolVectorProperty(name="Layers", default=([False] * NUM_LAYERS), size=NUM_LAYERS, subtype='LAYER')

class SCENE_OT_namedlayer_group_add(bpy.types.Operator):
    """Add and select a new layer group"""
    bl_idname = "scene.namedlayer_group_add"
    bl_label = "Add Layer Group"

    layers = BoolVectorProperty(name="Layers", default=([False] * NUM_LAYERS), size=NUM_LAYERS)

    @classmethod
    def poll(cls, context):
        return bool(context.scene)

    def execute(self, context):
        scene = context.scene
        layergroups = scene.layergroups
        layers = self.layers

        group_idx = len(layergroups)
        layer_group = layergroups.add()
        layer_group.name = "LayerGroup.%.3d" % group_idx
        layer_group.layers = layers
        scene.layergroups_index = group_idx

        return {'FINISHED'}

class SCENE_OT_namedlayer_group_remove(bpy.types.Operator):
    """Remove selected layer group"""
    bl_idname = "scene.namedlayer_group_remove"
    bl_label = "Remove Layer Group"

    group_idx = bpy.props.IntProperty()

    @classmethod
    def poll(cls, context):
        return bool(context.scene)

    def execute(self, context):
        scene = context.scene
        group_idx = self.group_idx

        scene.layergroups.remove(group_idx)
        if scene.layergroups_index > len(scene.layergroups) - 1:
            scene.layergroups_index = len(scene.layergroups) - 1

        return {'FINISHED'}

class SCENE_OT_namedlayer_toggle_visibility(bpy.types.Operator):
    """Show or hide given layer (shift to extend)"""
    bl_idname = "scene.namedlayer_toggle_visibility"
    bl_label = "Show/Hide Layer"

    layer_idx = IntProperty()
    group_idx = IntProperty()
    use_spacecheck = BoolProperty()
    extend = BoolProperty(options={'SKIP_SAVE'})

    @classmethod
    def poll(cls, context):
        return context.scene and (context.area.spaces.active.type == 'VIEW_3D')

    def execute(self, context):
        scene = context.scene
        layer_cont = context.area.spaces.active if self.use_spacecheck else context.scene
        layer_idx = self.layer_idx

        def lg_zero(scene):
            for group in scene.layergroups:
                group.use_toggle = False

        if layer_idx == -1:
            group_idx = self.group_idx
            layergroups = scene.layergroups[group_idx]
            group_layers = layergroups.layers
            layers = layer_cont.layers

            if layergroups.use_toggle:
                lg_zero(scene)
            else:
                lg_zero(scene)
                layer_cont.layers = [group_layer for group_layer, layer in zip(group_layers, layers)]
                layergroups.use_toggle = True
        else:
            if self.extend:
                layer_cont.layers[layer_idx] = not layer_cont.layers[layer_idx]
            else:
                layers = [False] * NUM_LAYERS
                layers[layer_idx] = True
                layer_cont.layers = layers
        return {'FINISHED'}

    def invoke(self, context, event):
        self.extend = event.shift
        return self.execute(context)

class SCENE_OT_namedlayer_toggle_render(bpy.types.Operator):
    """Render or not given layer (shift to extend)"""
    bl_idname = "scene.namedlayer_toggle_render"
    bl_label = "Render/Not Layer"

    layer_idx = IntProperty()
    group_idx = IntProperty()
    use_spacecheck = BoolProperty()
    extend = BoolProperty(options={'SKIP_SAVE'})

    @classmethod
    def poll(cls, context):
        return context.scene and (context.area.spaces.active.type == 'VIEW_3D')

    def execute(self, context):
        scene = context.scene
        layer_cont = context.area.spaces.active if self.use_spacecheck else context.scene
        layer_idx = self.layer_idx

        def obj_render(scene, layer, group_l, layer_idx, render):
            for obj in scene.objects:
                print(group_l, obj.layers[layer_idx], layer_idx)
                if obj.layers[layer_idx]:
                    obj.hide_render = not render
            layer.use_render = render

        def layer_render(group_layers, layer_cont, layergroup, render):
            for group_l, named_l, y in zip(group_layers, layer_cont.namedlayers.layers, range(20)):
                if group_l:
                    obj_render(scene, named_l, group_l, y, render)
            layergroup.use_render = render

        def lg_zero(scene):
            for group in scene.layergroups:
                group.use_render = True

        if layer_idx == -1:
            group_idx = self.group_idx
            layergroup = scene.layergroups[group_idx]
            group_layers = layergroup.layers
            layers = layer_cont.layers

            if layergroup.use_render:
                lg_zero(scene)
                layer_render(group_layers, layer_cont, layergroup, False)
            else:
                lg_zero(scene)
                layer_render(group_layers, layer_cont, layergroup, True)
        '''
        for objects in bpy.data.objects:
            for i,z in zip(objects.layers,range(20)):
                if i:
                    print(z,objects.name)
        else:
            if self.extend:
                layer_cont.layers[layer_idx] = not layer_cont.layers[layer_idx]
            else:
                layers = [False] * NUM_LAYERS
                layers[layer_idx] = True
                layer_cont.layers = layers
        '''
        return {'FINISHED'}

    def invoke(self, context, event):
        self.extend = event.shift
        return self.execute(context)

class SCENE_OT_namedlayer_move_to_layer(bpy.types.Operator):
    """Move selected objects to this Layer (shift to extend)"""
    bl_idname = "scene.namedlayer_move_to_layer"
    bl_label = "Move Objects To Layer"

    layer_idx = IntProperty()
    extend = BoolProperty(options={'SKIP_SAVE'})

    @classmethod
    def poll(cls, context):
        return context.scene

    def execute(self, context):
        layer_idx = self.layer_idx
        scene = context.scene

        # Cycle all objects in the layer
        for obj in scene.objects:
            if obj.select:
                # If object is in at least one of the scene's visible layers...
                if True in {ob_layer and sce_layer for ob_layer, sce_layer in zip(obj.layers, scene.layers)}:
                    if self.extend:
                        obj.layers[layer_idx] = not obj.layers[layer_idx]
                    else:
                        layer = [False] * NUM_LAYERS
                        layer[layer_idx] = True
                        obj.layers = layer
        return {'FINISHED'}

    def invoke(self, context, event):
        self.extend = event.shift
        return self.execute(context)

class SCENE_OT_namedlayer_toggle_wire(bpy.types.Operator):
    """Toggle all objects on this layer draw as wire"""
    bl_idname = "scene.namedlayer_toggle_wire"
    bl_label = "Toggle Objects Draw Wire"

    layer_idx = IntProperty()
    use_wire = BoolProperty()
    group_idx = IntProperty()

    @classmethod
    def poll(cls, context):
        return context.scene and (context.area.spaces.active.type == 'VIEW_3D')

    def execute(self, context):
        scene = context.scene
        layer_idx = self.layer_idx
        use_wire = self.use_wire

        view_3d = context.area.spaces.active

        # Check if layer have some thing
        if view_3d.layers_used[layer_idx] or layer_idx == -1:
            display = 'WIRE' if use_wire else 'TEXTURED'
            # Cycle all objects in the layer.
            for obj in context.scene.objects:
                if layer_idx == -1:
                    group_idx = self.group_idx
                    group_layers = scene.layergroups[group_idx].layers
                    layers = obj.layers
                    if True in {layer and group_layer for layer, group_layer in zip(layers, group_layers)}:
                        obj.draw_type = display
                        scene.layergroups[group_idx].use_wire = use_wire
                else:
                    if obj.layers[layer_idx]:
                        obj.draw_type = display
                        scene.namedlayers.layers[layer_idx].use_wire = use_wire

        return {'FINISHED'}

class SCENE_OT_namedlayer_lock_all(bpy.types.Operator):
    """Lock all objects on this layer"""
    bl_idname = "scene.namedlayer_lock_all"
    bl_label = "Lock Objects"

    layer_idx = IntProperty()
    use_lock = BoolProperty()
    group_idx = IntProperty()

    @classmethod
    def poll(cls, context):
        return context.scene and (context.area.spaces.active.type == 'VIEW_3D')

    def execute(self, context):
        scene = context.scene
        view_3d = context.area.spaces.active
        layer_idx = self.layer_idx
        group_idx = self.group_idx
        group_layers = FAKE_LAYER_GROUP if group_idx < 0 else scene.layergroups[group_idx].layers
        use_lock = self.use_lock

        # check if layer have some thing
        if layer_idx == -1 or view_3d.layers_used[layer_idx]:
            # Cycle all objects in the layer.
            for obj in context.scene.objects:
                if layer_idx == -1:
                    layers = obj.layers
                    if True in {layer and group_layer for layer, group_layer in zip(layers, group_layers)}:
                        obj.hide_select = not use_lock
                        obj.select = False
                        scene.layergroups[group_idx].use_lock = not use_lock
                else:
                    if obj.layers[layer_idx]:
                        obj.hide_select = not use_lock
                        obj.select = False
                        scene.namedlayers.layers[layer_idx].use_lock = not use_lock

        return {'FINISHED'}

class SCENE_OT_namedlayer_select_objects_by_layer(bpy.types.Operator):
    """Select all the objects on this Layer (shift for multi selection, ctrl to make active the last selected object)"""
    bl_idname = "scene.namedlayer_select_objects_by_layer"
    bl_label = "Select Objects In Layer"

    select_obj = BoolProperty()
    layer_idx = IntProperty()

    extend = BoolProperty(options={'SKIP_SAVE'})
    active = BoolProperty(options={'SKIP_SAVE'})

    @classmethod
    def poll(cls, context):
        return context.scene and (context.area.spaces.active.type == 'VIEW_3D')

    def execute(self, context):
        scene = context.scene
        view_3d = context.area.spaces.active
        select_obj = self.select_obj
        layer_idx = self.layer_idx

        not_all_selected = 0
        # check if layer have some thing
        if view_3d.layers_used[layer_idx]:
            objects = []
            for obj in context.scene.objects:
                if obj.layers[layer_idx]:
                    objects.append(obj)
                    not_all_selected -= 1
                    if self.active:
                        context.scene.objects.active = obj
                    if obj.select:
                        not_all_selected += 1
            if not not_all_selected:
                for obj in objects:
                    obj.select = False
            else:
                bpy.ops.object.select_by_layer(extend=self.extend, layers=layer_idx + 1)

        return {'FINISHED'}

    def invoke(self, context, event):
        self.extend = event.shift
        self.active = event.ctrl
        return self.execute(context)

class SCENE_OT_namedlayer_render(bpy.types.Operator):
    """Render all the objects on this Layer"""
    bl_idname = "scene.namedlayer_render"
    bl_label = "Render Objects In Layer"

    render_obj = BoolProperty()
    layer_idx = IntProperty()

    extend = BoolProperty(options={'SKIP_SAVE'})

    @classmethod
    def poll(cls, context):
        return context.scene and (context.area.spaces.active.type == 'VIEW_3D')

    def execute(self, context):
        scene = context.scene
        view_3d = context.area.spaces.active
        render_obj = self.render_obj
        layer_idx = self.layer_idx
        layer_cont = scene if view_3d.lock_camera_and_layers else view_3d
        #wtf
        namedlayer = layer_cont.namedlayers.layers[layer_cont.namedlayers['layers'][layer_idx]['name']]

        for obj in scene.objects:
            if obj.layers[layer_idx]:
                obj.hide_render = namedlayer.use_render
        namedlayer.use_render = not namedlayer.use_render

        return {'FINISHED'}

    def invoke(self, context, event):
        self.extend = event.shift
        self.active = event.ctrl
        return self.execute(context)

class SCENE_OT_namedlayer_show_all(bpy.types.Operator):
    """Show or hide all layers in the scene"""
    bl_idname = "scene.namedlayer_show_all"
    bl_label = "Select All Layers"

    show = BoolProperty()

    @classmethod
    def poll(cls, context):
        return context.scene and (context.area.spaces.active.type == 'VIEW_3D')

    def execute(self, context):
        scene = context.scene
        view_3d = context.area.spaces.active
        show = self.show
        active_layer = scene.active_layer

        # check for lock camera and layer is active
        layer_cont = scene if view_3d.lock_camera_and_layers else view_3d

        if show:
            layer_cont.layers[:] = [True] * NUM_LAYERS
            # Restore active layer (stupid, but Scene.active_layer is readonly).
            layer_cont.layers[active_layer] = False
            layer_cont.layers[active_layer] = True
        else:
            layers = [False] * NUM_LAYERS
            # Keep selection of active layer
            layers[active_layer] = True
            layer_cont.layers[:] = layers

        return {'FINISHED'}

class SCENE_PT_namedlayer_layers(bpy.types.Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'TOOLS'
    bl_label = "Layer Management"
    bl_options = {'DEFAULT_CLOSED'}
    bl_category = "Layers"

    @classmethod
    def poll(self, context):
        return ((getattr(context, "mode", 'EDIT_MESH') not in EDIT_MODES) and
                (context.area.spaces.active.type == 'VIEW_3D'))

    def draw(self, context):
        scene = context.scene
        view_3d = context.area.spaces.active
        actob = context.object
        namedlayers = scene.namedlayers
        use_extra = namedlayers.use_extra_options
        use_hide = namedlayers.use_hide_empty_layers
        use_indices = namedlayers.use_layer_indices
        use_classic = namedlayers.use_classic

        # Check for lock camera and layer is active
        if view_3d.lock_camera_and_layers:
            layer_cont = scene
            use_spacecheck = False
        else:
            layer_cont = view_3d
            use_spacecheck = True

        layout = self.layout
        row = layout.row()
        col = row.column()
        col.prop(view_3d, "lock_camera_and_layers", text="")
        # Check if there is a layer off
        show = (False in {layer for layer in layer_cont.layers})
        icon = 'RESTRICT_VIEW_ON' if show else 'RESTRICT_VIEW_OFF'
        col.operator("scene.namedlayer_show_all", emboss=False, icon=icon, text="").show = show

        col = row.column()
        col.prop(namedlayers, "use_classic")
        col.prop(namedlayers, "use_extra_options", text="Options")

        col = row.column()
        col.prop(namedlayers, "use_layer_indices", text="Indices")
        col.prop(namedlayers, "use_hide_empty_layers", text="Hide Empty")

        col = layout.column()
        for layer_idx in range(NUM_LAYERS):
            namedlayer = namedlayers.layers[layer_idx]
            is_layer_used = view_3d.layers_used[layer_idx]

            if (use_hide and not is_layer_used):
                # Hide unused layers and this one is unused, skip.
                continue

            row = col.row(align=True)

            # layer index
            if use_indices:
                sub = row.row(align=True)
                sub.alignment = 'LEFT'
                sub.label(text="%.2d." % (layer_idx + 1))

            # visualization
            icon = 'RESTRICT_VIEW_OFF' if layer_cont.layers[layer_idx] else 'RESTRICT_VIEW_ON'
            if use_classic:
                op = row.operator("scene.namedlayer_toggle_visibility", text="", icon=icon, emboss=True)
                op.layer_idx = layer_idx
                op.use_spacecheck = use_spacecheck
            else:
                row.prop(layer_cont, "layers", index=layer_idx, emboss=True, icon=icon, toggle=True, text="")

            icon = 'RESTRICT_RENDER_OFF' if namedlayer.use_render else 'RESTRICT_RENDER_ON'
            op = row.operator("scene.namedlayer_render", icon=icon, text="", emboss=True).layer_idx = layer_idx

            # Name (use special icon for active layer)
            icon = 'FILE_TICK' if (getattr(layer_cont, "active_layer", -1) == layer_idx) else 'NONE'
            row.prop(namedlayer, "name", text="", icon=icon)

            if use_extra:
                use_lock = namedlayer.use_lock

                # Select by type operator
                sub = row.column(align=True)
                sub.enabled = not use_lock
                sub.operator("scene.namedlayer_select_objects_by_layer", icon='RESTRICT_SELECT_OFF',
                             text="", emboss=True).layer_idx = layer_idx

                # Lock operator
                icon = 'LOCKED' if use_lock else 'UNLOCKED'
                op = row.operator("scene.namedlayer_lock_all", text="", emboss=True, icon=icon)
                op.layer_idx = layer_idx
                op.group_idx = -1
                op.use_lock = use_lock

                # Merge layer
                # check if layer has something
                has_active = (actob and actob.layers[layer_idx])
                icon = ('LAYER_ACTIVE' if has_active else 'LAYER_USED') if is_layer_used else 'RADIOBUT_OFF'
                row.operator("scene.namedlayer_move_to_layer", text="", emboss=True, icon=icon).layer_idx = layer_idx

                # Wire view
                use_wire = namedlayer.use_wire
                icon = 'WIRE' if use_wire else 'POTATO'
                op = row.operator("scene.namedlayer_toggle_wire", text="", emboss=True, icon=icon)
                op.layer_idx = layer_idx
                op.use_wire = not use_wire

            if not (layer_idx + 1) % 5:
                col.separator()

        if len(scene.objects) == 0:
            layout.label(text="No objects in scene")

class SCENE_UL_namedlayer_groups(UIList):
    def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
        layer_group = item

        # check for lock camera and layer is active
        view_3d = context.area.spaces.active  # Ensured it is a 'VIEW_3D' in panel's poll(), weak... :/
        use_spacecheck = False if view_3d.lock_camera_and_layers else True

        if self.layout_type in {'DEFAULT', 'COMPACT'}:
            layout.prop(layer_group, "name", text="", emboss=False)
            # lock operator
            use_lock = layer_group.use_lock
            icon = 'LOCKED' if use_lock else 'UNLOCKED'
            op = layout.operator("scene.namedlayer_lock_all", text="", emboss=False, icon=icon)
            op.use_lock = use_lock
            op.group_idx = index
            op.layer_idx = -1

            # view operator
            icon = 'RESTRICT_VIEW_OFF' if layer_group.use_toggle else 'RESTRICT_VIEW_ON'
            op = layout.operator("scene.namedlayer_toggle_visibility", text="", emboss=False, icon=icon)
            op.use_spacecheck = use_spacecheck
            op.group_idx = index
            op.layer_idx = -1

            # render operator
            icon = 'RESTRICT_RENDER_OFF' if layer_group.use_render else 'RESTRICT_RENDER_ON'
            op = layout.operator("scene.namedlayer_toggle_render", text="", emboss=False, icon=icon)
            op.use_spacecheck = use_spacecheck
            op.group_idx = index
            op.layer_idx = -1

            # wire operator
            use_wire = layer_group.use_wire
            icon = 'WIRE' if use_wire else 'POTATO'
            op = layout.operator("scene.namedlayer_toggle_wire", text="", emboss=False, icon=icon)
            op.use_wire = not use_wire
            op.group_idx = index
            op.layer_idx = -1

        elif self.layout_type in {'GRID'}:
            layout.alignment = 'CENTER'

class SCENE_PT_namedlayer_groups(bpy.types.Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'TOOLS'
    bl_category = "Layers"
    bl_label = "Layer Groups"
    bl_options = {'DEFAULT_CLOSED'}

    @classmethod
    def poll(self, context):
        return ((getattr(context, "mode", 'EDIT_MESH') not in EDIT_MODES) and
                (context.area.spaces.active.type == 'VIEW_3D'))

    def draw(self, context):
        scene = context.scene
        group_idx = scene.layergroups_index

        layout = self.layout
        row = layout.row()
        row.template_list("SCENE_UL_namedlayer_groups", "", scene, "layergroups", scene, "layergroups_index")

        col = row.column(align=True)
        col.operator("scene.namedlayer_group_add", icon='ZOOMIN', text="").layers = scene.layers
        col.operator("scene.namedlayer_group_remove", icon='ZOOMOUT', text="").group_idx = group_idx

        if bool(scene.layergroups):
            layout.prop(scene.layergroups[group_idx], "layers", text="", toggle=True)
            layout.prop(scene.layergroups[group_idx], "name", text="Name:")

def register():
    bpy.utils.register_module(__name__)
    bpy.types.Scene.layergroups = CollectionProperty(type=LayerGroup)
    # Unused, but this is needed for the TemplateList to work...
    bpy.types.Scene.layergroups

    bpy.types.Scene.namedlayers = PointerProperty(type=NamedLayers)
    bpy.app.handlers.scene_update_post.append(check_init_data)

def unregister():
    bpy.app.handlers.scene_update_post.remove(check_init_data)
    del bpy.types.Scene.layergroups
    del bpy.types.Scene.layergroups_index
    del bpy.types.Scene.namedlayers
    bpy.utils.unregister_module(__name__)

if __name__ == "__main__":
    register()


while namedlayers.layers:
            namedlayers.layers.remove(0)

#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software Foundation,

bl_info = {
    "name": "Layer Management",
    "author": "Alfonso Annarumma, Bastien Montagne",
    "version": (1, 5, 3),
    "blender": (2, 7, 5),
    "location": "Toolshelf > Layers Tab",
    "warning": "",
    "description": "Display and Edit Layer Name",
    "wiki_url": "http://example.com",
    "category": "3D View",
}

import bpy
from bpy.types import Menu, Panel, UIList, PropertyGroup
from bpy.props import StringProperty, BoolProperty, IntProperty, CollectionProperty, BoolVectorProperty, PointerProperty
from bpy.app.handlers import persistent

EDIT_MODES = {'EDIT_MESH', 'EDIT_CURVE', 'EDIT_SURFACE', 'EDIT_METABALL', 'EDIT_TEXT', 'EDIT_ARMATURE'}

NUM_LAYERS = 20

FAKE_LAYER_GROUP = [True] * NUM_LAYERS

class NamedLayer(PropertyGroup):
    name = StringProperty(name="Layer Name")
    use_lock = BoolProperty(name="Lock Layer", default=False)
    use_render = BoolProperty(name="Render", default=True)
    use_object_select = BoolProperty(name="Object Select", default=True)
    use_wire = BoolProperty(name="Wire Layer", default=False)

class NamedLayers(PropertyGroup):
    layers = CollectionProperty(type=NamedLayer)
    use_hide_empty_layers = BoolProperty(name="Hide Empty Layer", default=False)
    use_extra_options = BoolProperty(name="Show Extra Options", default=True)
    use_layer_indices = BoolProperty(name="Show Layer Indices", default=False)
    use_classic = BoolProperty(name="Classic", default=False, description="Use a classic layer selection visibility")

    use_init = BoolProperty(default=True, options={'HIDDEN'})

# Stupid, but only solution currently is to use a handler to init that layers collection...
@persistent
def check_init_data(scene):
    namedlayers = scene.namedlayers
    if namedlayers.use_init:

        for i in range(NUM_LAYERS):
            layer = namedlayers.layers.add()
            layer.name = "Layer%.2d" % (i + 1)  # Blender use layer nums starting from 1, not 0.
        namedlayers.use_init = False

class LayerGroup(PropertyGroup):
    use_toggle = BoolProperty(name="", default=False)
    use_wire = BoolProperty(name="", default=False)
    use_lock = BoolProperty(name="", default=False)
    use_render = BoolProperty(name="", default=True)

    layers = BoolVectorProperty(name="Layers", default=([False] * NUM_LAYERS), size=NUM_LAYERS, subtype='LAYER')

class SCENE_OT_namedlayer_group_add(bpy.types.Operator):
    """Add and select a new layer group"""
    bl_idname = "scene.namedlayer_group_add"
    bl_label = "Add Layer Group"

    layers = BoolVectorProperty(name="Layers", default=([False] * NUM_LAYERS), size=NUM_LAYERS)

    @classmethod
    def poll(cls, context):
        return bool(context.scene)

    def execute(self, context):
        scene = context.scene
        layergroups = scene.layergroups
        layers = self.layers

        group_idx = len(layergroups)
        layer_group = layergroups.add()
        layer_group.name = "LayerGroup.%.3d" % group_idx
        layer_group.layers = layers
        scene.layergroups_index = group_idx

        return {'FINISHED'}

class SCENE_OT_namedlayer_group_remove(bpy.types.Operator):
    """Remove selected layer group"""
    bl_idname = "scene.namedlayer_group_remove"
    bl_label = "Remove Layer Group"

    group_idx = bpy.props.IntProperty()

    @classmethod
    def poll(cls, context):
        return bool(context.scene)

    def execute(self, context):
        scene = context.scene
        group_idx = self.group_idx

        scene.layergroups.remove(group_idx)
        if scene.layergroups_index > len(scene.layergroups) - 1:
            scene.layergroups_index = len(scene.layergroups) - 1

        return {'FINISHED'}

class SCENE_OT_namedlayer_toggle_visibility(bpy.types.Operator):
    """Show or hide given layer (shift to extend)"""
    bl_idname = "scene.namedlayer_toggle_visibility"
    bl_label = "Show/Hide Layer"

    layer_idx = IntProperty()
    group_idx = IntProperty()
    use_spacecheck = BoolProperty()
    extend = BoolProperty(options={'SKIP_SAVE'})

    @classmethod
    def poll(cls, context):
        return context.scene and (context.area.spaces.active.type == 'VIEW_3D')

    def execute(self, context):
        scene = context.scene
        layer_cont = context.area.spaces.active if self.use_spacecheck else context.scene
        layer_idx = self.layer_idx

        def lg_zero(scene):
            for group in scene.layergroups:
                group.use_toggle = False

        if layer_idx == -1:
            group_idx = self.group_idx
            layergroups = scene.layergroups[group_idx]
            group_layers = layergroups.layers
            layers = layer_cont.layers

            if layergroups.use_toggle:
                lg_zero(scene)
            else:
                lg_zero(scene)
                layer_cont.layers = [group_layer for group_layer, layer in zip(group_layers, layers)]
                layergroups.use_toggle = True
        else:
            if self.extend:
                layer_cont.layers[layer_idx] = not layer_cont.layers[layer_idx]
            else:
                layers = [False] * NUM_LAYERS
                layers[layer_idx] = True
                layer_cont.layers = layers
        return {'FINISHED'}

    def invoke(self, context, event):
        self.extend = event.shift
        return self.execute(context)

class SCENE_OT_namedlayer_toggle_render(bpy.types.Operator):
    """Render or not given layer (shift to extend)"""
    bl_idname = "scene.namedlayer_toggle_render"
    bl_label = "Render/Not Layer"

    layer_idx = IntProperty()
    group_idx = IntProperty()
    use_spacecheck = BoolProperty()
    extend = BoolProperty(options={'SKIP_SAVE'})

    @classmethod
    def poll(cls, context):
        return context.scene and (context.area.spaces.active.type == 'VIEW_3D')

    def execute(self, context):
        scene = context.scene
        layer_cont = context.area.spaces.active if self.use_spacecheck else context.scene
        layer_idx = self.layer_idx

        def obj_render(scene, layer, group_l, layer_idx, render):
            for obj in scene.objects:
                print(group_l, obj.layers[layer_idx], layer_idx)
                if obj.layers[layer_idx]:
                    obj.hide_render = not render
            layer.use_render = render

        def layer_render(group_layers, layer_cont, layergroup, render):
            for group_l, named_l, y in zip(group_layers, layer_cont.namedlayers.layers, range(20)):
                if group_l:
                    obj_render(scene, named_l, group_l, y, render)
            layergroup.use_render = render

        def lg_zero(scene):
            for group in scene.layergroups:
                group.use_render = True

        if layer_idx == -1:
            group_idx = self.group_idx
            layergroup = scene.layergroups[group_idx]
            group_layers = layergroup.layers
            layers = layer_cont.layers

            if layergroup.use_render:
                lg_zero(scene)
                layer_render(group_layers, layer_cont, layergroup, False)
            else:
                lg_zero(scene)
                layer_render(group_layers, layer_cont, layergroup, True)
        '''
        for objects in bpy.data.objects:
            for i,z in zip(objects.layers,range(20)):
                if i:
                    print(z,objects.name)
        else:
            if self.extend:
                layer_cont.layers[layer_idx] = not layer_cont.layers[layer_idx]
            else:
                layers = [False] * NUM_LAYERS
                layers[layer_idx] = True
                layer_cont.layers = layers
        '''
        return {'FINISHED'}

    def invoke(self, context, event):
        self.extend = event.shift
        return self.execute(context)

class SCENE_OT_namedlayer_move_to_layer(bpy.types.Operator):
    """Move selected objects to this Layer (shift to extend)"""
    bl_idname = "scene.namedlayer_move_to_layer"
    bl_label = "Move Objects To Layer"

    layer_idx = IntProperty()
    extend = BoolProperty(options={'SKIP_SAVE'})

    @classmethod
    def poll(cls, context):
        return context.scene

    def execute(self, context):
        layer_idx = self.layer_idx
        scene = context.scene

        # Cycle all objects in the layer
        for obj in scene.objects:
            if obj.select:
                # If object is in at least one of the scene's visible layers...
                if True in {ob_layer and sce_layer for ob_layer, sce_layer in zip(obj.layers, scene.layers)}:
                    if self.extend:
                        obj.layers[layer_idx] = not obj.layers[layer_idx]
                    else:
                        layer = [False] * NUM_LAYERS
                        layer[layer_idx] = True
                        obj.layers = layer
        return {'FINISHED'}

    def invoke(self, context, event):
        self.extend = event.shift
        return self.execute(context)

class SCENE_OT_namedlayer_toggle_wire(bpy.types.Operator):
    """Toggle all objects on this layer draw as wire"""
    bl_idname = "scene.namedlayer_toggle_wire"
    bl_label = "Toggle Objects Draw Wire"

    layer_idx = IntProperty()
    use_wire = BoolProperty()
    group_idx = IntProperty()

    @classmethod
    def poll(cls, context):
        return context.scene and (context.area.spaces.active.type == 'VIEW_3D')

    def execute(self, context):
        scene = context.scene
        layer_idx = self.layer_idx
        use_wire = self.use_wire

        view_3d = context.area.spaces.active

        # Check if layer have some thing
        if view_3d.layers_used[layer_idx] or layer_idx == -1:
            display = 'WIRE' if use_wire else 'TEXTURED'
            # Cycle all objects in the layer.
            for obj in context.scene.objects:
                if layer_idx == -1:
                    group_idx = self.group_idx
                    group_layers = scene.layergroups[group_idx].layers
                    layers = obj.layers
                    if True in {layer and group_layer for layer, group_layer in zip(layers, group_layers)}:
                        obj.draw_type = display
                        scene.layergroups[group_idx].use_wire = use_wire
                else:
                    if obj.layers[layer_idx]:
                        obj.draw_type = display
                        scene.namedlayers.layers[layer_idx].use_wire = use_wire

        return {'FINISHED'}

class SCENE_OT_namedlayer_lock_all(bpy.types.Operator):
    """Lock all objects on this layer"""
    bl_idname = "scene.namedlayer_lock_all"
    bl_label = "Lock Objects"

    layer_idx = IntProperty()
    use_lock = BoolProperty()
    group_idx = IntProperty()

    @classmethod
    def poll(cls, context):
        return context.scene and (context.area.spaces.active.type == 'VIEW_3D')

    def execute(self, context):
        scene = context.scene
        view_3d = context.area.spaces.active
        layer_idx = self.layer_idx
        group_idx = self.group_idx
        group_layers = FAKE_LAYER_GROUP if group_idx < 0 else scene.layergroups[group_idx].layers
        use_lock = self.use_lock

        # check if layer have some thing
        if layer_idx == -1 or view_3d.layers_used[layer_idx]:
            # Cycle all objects in the layer.
            for obj in context.scene.objects:
                if layer_idx == -1:
                    layers = obj.layers
                    if True in {layer and group_layer for layer, group_layer in zip(layers, group_layers)}:
                        obj.hide_select = not use_lock
                        obj.select = False
                        scene.layergroups[group_idx].use_lock = not use_lock
                else:
                    if obj.layers[layer_idx]:
                        obj.hide_select = not use_lock
                        obj.select = False
                        scene.namedlayers.layers[layer_idx].use_lock = not use_lock

        return {'FINISHED'}

class SCENE_OT_namedlayer_select_objects_by_layer(bpy.types.Operator):
    """Select all the objects on this Layer (shift for multi selection, ctrl to make active the last selected object)"""
    bl_idname = "scene.namedlayer_select_objects_by_layer"
    bl_label = "Select Objects In Layer"

    select_obj = BoolProperty()
    layer_idx = IntProperty()

    extend = BoolProperty(options={'SKIP_SAVE'})
    active = BoolProperty(options={'SKIP_SAVE'})

    @classmethod
    def poll(cls, context):
        return context.scene and (context.area.spaces.active.type == 'VIEW_3D')

    def execute(self, context):
        scene = context.scene
        view_3d = context.area.spaces.active
        select_obj = self.select_obj
        layer_idx = self.layer_idx

        not_all_selected = 0
        # check if layer have some thing
        if view_3d.layers_used[layer_idx]:
            objects = []
            for obj in context.scene.objects:
                if obj.layers[layer_idx]:
                    objects.append(obj)
                    not_all_selected -= 1
                    if self.active:
                        context.scene.objects.active = obj
                    if obj.select:
                        not_all_selected += 1
            if not not_all_selected:
                for obj in objects:
                    obj.select = False
            else:
                bpy.ops.object.select_by_layer(extend=self.extend, layers=layer_idx + 1)

        return {'FINISHED'}

    def invoke(self, context, event):
        self.extend = event.shift
        self.active = event.ctrl
        return self.execute(context)

class SCENE_OT_namedlayer_render(bpy.types.Operator):
    """Render all the objects on this Layer"""
    bl_idname = "scene.namedlayer_render"
    bl_label = "Render Objects In Layer"

    render_obj = BoolProperty()
    layer_idx = IntProperty()

    extend = BoolProperty(options={'SKIP_SAVE'})

    @classmethod
    def poll(cls, context):
        return context.scene and (context.area.spaces.active.type == 'VIEW_3D')

    def execute(self, context):
        scene = context.scene
        view_3d = context.area.spaces.active
        render_obj = self.render_obj
        layer_idx = self.layer_idx
        layer_cont = scene if view_3d.lock_camera_and_layers else view_3d
        #wtf
        namedlayer = layer_cont.namedlayers.layers[layer_cont.namedlayers['layers'][layer_idx]['name']]

        for obj in scene.objects:
            if obj.layers[layer_idx]:
                obj.hide_render = namedlayer.use_render
        namedlayer.use_render = not namedlayer.use_render

        return {'FINISHED'}

    def invoke(self, context, event):
        self.extend = event.shift
        self.active = event.ctrl
        return self.execute(context)

class SCENE_OT_namedlayer_show_all(bpy.types.Operator):
    """Show or hide all layers in the scene"""
    bl_idname = "scene.namedlayer_show_all"
    bl_label = "Select All Layers"

    show = BoolProperty()

    @classmethod
    def poll(cls, context):
        return context.scene and (context.area.spaces.active.type == 'VIEW_3D')

    def execute(self, context):
        scene = context.scene
        view_3d = context.area.spaces.active
        show = self.show
        active_layer = scene.active_layer

        # check for lock camera and layer is active
        layer_cont = scene if view_3d.lock_camera_and_layers else view_3d

        if show:
            layer_cont.layers[:] = [True] * NUM_LAYERS
            # Restore active layer (stupid, but Scene.active_layer is readonly).
            layer_cont.layers[active_layer] = False
            layer_cont.layers[active_layer] = True
        else:
            layers = [False] * NUM_LAYERS
            # Keep selection of active layer
            layers[active_layer] = True
            layer_cont.layers[:] = layers

        return {'FINISHED'}

class SCENE_PT_namedlayer_layers(bpy.types.Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'TOOLS'
    bl_label = "Layer Management"
    bl_options = {'DEFAULT_CLOSED'}
    bl_category = "Layers"

    @classmethod
    def poll(self, context):
        return ((getattr(context, "mode", 'EDIT_MESH') not in EDIT_MODES) and
                (context.area.spaces.active.type == 'VIEW_3D'))

    def draw(self, context):
        scene = context.scene
        view_3d = context.area.spaces.active
        actob = context.object
        namedlayers = scene.namedlayers
        use_extra = namedlayers.use_extra_options
        use_hide = namedlayers.use_hide_empty_layers
        use_indices = namedlayers.use_layer_indices
        use_classic = namedlayers.use_classic

        # Check for lock camera and layer is active
        if view_3d.lock_camera_and_layers:
            layer_cont = scene
            use_spacecheck = False
        else:
            layer_cont = view_3d
            use_spacecheck = True

        layout = self.layout
        row = layout.row()
        col = row.column()
        col.prop(view_3d, "lock_camera_and_layers", text="")
        # Check if there is a layer off
        show = (False in {layer for layer in layer_cont.layers})
        icon = 'RESTRICT_VIEW_ON' if show else 'RESTRICT_VIEW_OFF'
        col.operator("scene.namedlayer_show_all", emboss=False, icon=icon, text="").show = show

        col = row.column()
        col.prop(namedlayers, "use_classic")
        col.prop(namedlayers, "use_extra_options", text="Options")

        col = row.column()
        col.prop(namedlayers, "use_layer_indices", text="Indices")
        col.prop(namedlayers, "use_hide_empty_layers", text="Hide Empty")

        col = layout.column()
        for layer_idx in range(NUM_LAYERS):
            namedlayer = namedlayers.layers[layer_idx]
            is_layer_used = view_3d.layers_used[layer_idx]

            if (use_hide and not is_layer_used):
                # Hide unused layers and this one is unused, skip.
                continue

            row = col.row(align=True)

            # layer index
            if use_indices:
                sub = row.row(align=True)
                sub.alignment = 'LEFT'
                sub.label(text="%.2d." % (layer_idx + 1))

            # visualization
            icon = 'RESTRICT_VIEW_OFF' if layer_cont.layers[layer_idx] else 'RESTRICT_VIEW_ON'
            if use_classic:
                op = row.operator("scene.namedlayer_toggle_visibility", text="", icon=icon, emboss=True)
                op.layer_idx = layer_idx
                op.use_spacecheck = use_spacecheck
            else:
                row.prop(layer_cont, "layers", index=layer_idx, emboss=True, icon=icon, toggle=True, text="")

            icon = 'RESTRICT_RENDER_OFF' if namedlayer.use_render else 'RESTRICT_RENDER_ON'
            op = row.operator("scene.namedlayer_render", icon=icon, text="", emboss=True).layer_idx = layer_idx

            # Name (use special icon for active layer)
            icon = 'FILE_TICK' if (getattr(layer_cont, "active_layer", -1) == layer_idx) else 'NONE'
            row.prop(namedlayer, "name", text="", icon=icon)

            if use_extra:
                use_lock = namedlayer.use_lock

                # Select by type operator
                sub = row.column(align=True)
                sub.enabled = not use_lock
                sub.operator("scene.namedlayer_select_objects_by_layer", icon='RESTRICT_SELECT_OFF',
                             text="", emboss=True).layer_idx = layer_idx

                # Lock operator
                icon = 'LOCKED' if use_lock else 'UNLOCKED'
                op = row.operator("scene.namedlayer_lock_all", text="", emboss=True, icon=icon)
                op.layer_idx = layer_idx
                op.group_idx = -1
                op.use_lock = use_lock

                # Merge layer
                # check if layer has something
                has_active = (actob and actob.layers[layer_idx])
                icon = ('LAYER_ACTIVE' if has_active else 'LAYER_USED') if is_layer_used else 'RADIOBUT_OFF'
                row.operator("scene.namedlayer_move_to_layer", text="", emboss=True, icon=icon).layer_idx = layer_idx

                # Wire view
                use_wire = namedlayer.use_wire
                icon = 'WIRE' if use_wire else 'POTATO'
                op = row.operator("scene.namedlayer_toggle_wire", text="", emboss=True, icon=icon)
                op.layer_idx = layer_idx
                op.use_wire = not use_wire

            if not (layer_idx + 1) % 5:
                col.separator()

        if len(scene.objects) == 0:
            layout.label(text="No objects in scene")

class SCENE_UL_namedlayer_groups(UIList):
    def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):
        layer_group = item

        # check for lock camera and layer is active
        view_3d = context.area.spaces.active  # Ensured it is a 'VIEW_3D' in panel's poll(), weak... :/
        use_spacecheck = False if view_3d.lock_camera_and_layers else True

        if self.layout_type in {'DEFAULT', 'COMPACT'}:
            layout.prop(layer_group, "name", text="", emboss=False)
            # lock operator
            use_lock = layer_group.use_lock
            icon = 'LOCKED' if use_lock else 'UNLOCKED'
            op = layout.operator("scene.namedlayer_lock_all", text="", emboss=False, icon=icon)
            op.use_lock = use_lock
            op.group_idx = index
            op.layer_idx = -1

            # view operator
            icon = 'RESTRICT_VIEW_OFF' if layer_group.use_toggle else 'RESTRICT_VIEW_ON'
            op = layout.operator("scene.namedlayer_toggle_visibility", text="", emboss=False, icon=icon)
            op.use_spacecheck = use_spacecheck
            op.group_idx = index
            op.layer_idx = -1

            # render operator
            icon = 'RESTRICT_RENDER_OFF' if layer_group.use_render else 'RESTRICT_RENDER_ON'
            op = layout.operator("scene.namedlayer_toggle_render", text="", emboss=False, icon=icon)
            op.use_spacecheck = use_spacecheck
            op.group_idx = index
            op.layer_idx = -1

            # wire operator
            use_wire = layer_group.use_wire
            icon = 'WIRE' if use_wire else 'POTATO'
            op = layout.operator("scene.namedlayer_toggle_wire", text="", emboss=False, icon=icon)
            op.use_wire = not use_wire
            op.group_idx = index
            op.layer_idx = -1

        elif self.layout_type in {'GRID'}:
            layout.alignment = 'CENTER'

class SCENE_PT_namedlayer_groups(bpy.types.Panel):
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'TOOLS'
    bl_category = "Layers"
    bl_label = "Layer Groups"
    bl_options = {'DEFAULT_CLOSED'}

    @classmethod
    def poll(self, context):
        return ((getattr(context, "mode", 'EDIT_MESH') not in EDIT_MODES) and
                (context.area.spaces.active.type == 'VIEW_3D'))

    def draw(self, context):
        scene = context.scene
        group_idx = scene.layergroups_index

        layout = self.layout
        row = layout.row()
        row.template_list("SCENE_UL_namedlayer_groups", "", scene, "layergroups", scene, "layergroups_index")

        col = row.column(align=True)
        col.operator("scene.namedlayer_group_add", icon='ZOOMIN', text="").layers = scene.layers
        col.operator("scene.namedlayer_group_remove", icon='ZOOMOUT', text="").group_idx = group_idx

        if bool(scene.layergroups):
            layout.prop(scene.layergroups[group_idx], "layers", text="", toggle=True)
            layout.prop(scene.layergroups[group_idx], "name", text="Name:")

def register():
    bpy.utils.register_module(__name__)
    bpy.types.Scene.layergroups = CollectionProperty(type=LayerGroup)
    # Unused, but this is needed for the TemplateList to work...
    bpy.types.Scene.layergroups_index = IntProperty(default=-1)
    bpy.types.Scene.namedlayers = PointerProperty(type=NamedLayers)
    bpy.app.handlers.scene_update_post.append(check_init_data)

def unregister():
    bpy.app.handlers.scene_update_post.remove(check_init_data)
    del bpy.types.Scene.layergroups
    del bpy.types.Scene.layergroups_index
    del bpy.types.Scene.namedlayers
    bpy.utils.unregister_module(__name__)

if __name__ == "__main__":
    register()


def _get_filename(response: Response) -> str:

import re
from os import path
from typing import Any

import requests
from requests.models import Response

from ..config import config

_user_agent = (
    "user-agent=Mozilla/5.0 (Windows NT 6.1; Win64; x64) "
    + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
)
_headers = {"User-Agent": _user_agent}

class Downloader:
    def get(self, url: str) -> Any:
        with requests.get(url, headers=_headers) as response:
            return response.json(strict=False)

    def download(self, url: str, filename: str) -> str:
        """Download the specified mod
        Returns:
            Filename of the downloaded and saved file
        """

        if config.pretend:
            return filename

        with requests.get(url, headers=_headers) as response:

            if len(filename) == 0:
                filename = Downloader._get_filename(response)

            if not filename.endswith(".jar"):
                filename += ".jar"

            filename = path.join(config.dir, filename)

            # Save file
            with open(filename, "wb") as file:
                file.write(response.content)

        return filename

    @staticmethod

content_disposition = response.headers.get("content-disposition")
        if not content_disposition:
            return ""

        filename = re.search(r"filename=(.+)", content_disposition)
        if not filename:
            return ""

        return filename.group(1)


with requests.get(url, headers=_headers) as response:
            return response.json(strict=False)

import re
from os import path
from typing import Any

import requests
from requests.models import Response

from ..config import config

_user_agent = (
    "user-agent=Mozilla/5.0 (Windows NT 6.1; Win64; x64) "
    + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
)
_headers = {"User-Agent": _user_agent}

class Downloader:
    def get(self, url: str) -> Any:

    def download(self, url: str, filename: str) -> str:
        """Download the specified mod
        Returns:
            Filename of the downloaded and saved file
        """

        if config.pretend:
            return filename

        with requests.get(url, headers=_headers) as response:

            if len(filename) == 0:
                filename = Downloader._get_filename(response)

            if not filename.endswith(".jar"):
                filename += ".jar"

            filename = path.join(config.dir, filename)

            # Save file
            with open(filename, "wb") as file:
                file.write(response.content)

        return filename

    @staticmethod
    def _get_filename(response: Response) -> str:
        content_disposition = response.headers.get("content-disposition")
        if not content_disposition:
            return ""

        filename = re.search(r"filename=(.+)", content_disposition)
        if not filename:
            return ""

        return filename.group(1)


import re
from os import path
from typing import Any

import requests
from requests.models import Response

from ..config import config

_user_agent = (
    "user-agent=Mozilla/5.0 (Windows NT 6.1; Win64; x64) "
    + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
)
_headers = {"User-Agent": _user_agent}

class Downloader:
    def get(self, url: str) -> Any:
        with requests.get(url, headers=_headers) as response:
            return response.json(strict=False)

    def download(self, url: str, filename: str) -> str:
        """Download the specified mod
        Returns:
            Filename of the downloaded and saved file
        """

        if config.pretend:
            return filename

        with requests.get(url, headers=_headers) as response:

            if len(filename) == 0:
                filename = Downloader._get_filename(response)

            if not filename.endswith(".jar"):
                filename += ".jar"

            filename = path.join(config.dir, filename)

            # Save file
            with open(filename, "wb") as file:
                file.write(response.content)

        return filename

    @staticmethod
    def _get_filename(response: Response) -> str:
        content_disposition = response.headers.get("content-disposition")
        if not content_disposition:
            return ""

        filename = re.search(r"filen

        if not filename:
            return ""

        return filename.group(1)


        if config.pretend:
            return filename

        with requests.get(url, headers=_headers) as response:

            if len(filename) == 0:
                filename = Downloader._get_filename(response)

            if not filename.endswith(".jar"):
                filename += ".jar"

            filename = path.join(config.dir, filename)

            # Save file
            with open(filename, "wb") as file:
                file.write(response.content)

        return filename

import re
from os import path
from typing import Any

import requests
from requests.models import Response

from ..config import config

_user_agent = (
    "user-agent=Mozilla/5.0 (Windows NT 6.1; Win64; x64) "
    + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
)
_headers = {"User-Agent": _user_agent}

class Downloader:
    def get(self, url: str) -> Any:
        with requests.get(url, headers=_headers) as response:
            return response.json(strict=False)

    def download(self, url: str, filename: str) -> str:
        """Download the specified mod
        Returns:
            Filename of the downloaded and saved file
        """

    @staticmethod
    def _get_filename(response: Response) -> str:
        content_disposition = response.headers.get("content-disposition")
        if not content_disposition:
            return ""

        filename = re.search(r"filename=(.+)", content_disposition)
        if not filename:
            return ""

        return filename.group(1)


import random
import math
import copy
import struct
import time

from flag import getflag, checktoken
import logger

GX = 25
GY = 39
INIT_LEVEL = 1
ELEM_PROB = .1
EVIL_PROB = .12
HEART_LIFE = 25
BURN_LIFE = 20
RESTORE_TIME = 10
GOAL_OF_LEVEL = lambda level: 300+int(level**1.5)*100
GOOD_TIME_S = 100

class GameOver(Exception):
    def __str__(self): return 'Game Over'

class YouWin(Exception):
    def __str__(self): return 'You Win'

class _FireStopped(Exception):
    pass

class CheckerMixin:
    @classmethod
    def _check(cls, value):
        for k, v in cls.__dict__.items():
            if not k.startswith('_') and v==value:
                return v
        raise RuntimeError('bad value %s for %s'%(value, cls))

class Elem(CheckerMixin):
    dirt = 1
    evildirt = 2
    empty = 3
    fire = 4
    heart = 5
    chunk1 = 6
    chunk2 = 7
    chunk3 = 8
    chunks = [6, 7, 8]
    player = 9

class Command(CheckerMixin):
    left = 1
    right = 2
    next = 3
    down = 4
    empty = 5

class Sync(CheckerMixin):
    sync_level = 1
    sync_status = 2
    sync_map = 3
    init_level = 4
    show_hud = 5
    tick_routine = 6

SALARY = {Elem.chunk1: 100, Elem.chunk2: 200, Elem.chunk3: 300}

class Fire:
    def __init__(self, game, y, x):
        self.left = False
        self.y = y
        self.x = x
        self.game = game

    def tick(self):
        _t = {Elem.dirt, Elem.evildirt, Elem.fire}

        if self.y<GY-1 and self.game.g[self.y+1][self.x]==Elem.empty: #go down
            self.game.g[self.y+1][self.x] = Elem.fire
            self.game.g[self.y][self.x] = Elem.empty
            self.y += 1
        elif self.y<GY-1 and self.game.g[self.y+1][self.x] in Elem.chunks+[Elem.heart]: #burn down
            self.game.new_fire(self.y+1,self.x)
        elif self.y<GY-1 and self.game.g[self.y+1][self.x]==Elem.player: #beat down
            if self.game.player.hurt(BURN_LIFE):
                 raise _FireStopped()
        elif 0<self.x<GX-1 and self.game.g[self.y][self.x+1] in _t and self.game.g[self.y][self.x-1] in _t: #do nothing A
            return
        elif (self.x==0 and self.game.g[self.y][1] in _t) or (self.x==GX-1 and self.game.g[self.y][GX-2] in _t): #do nothing B
            return
        else:
            if self.x==0:
                self.left = False
            if self.x==GX-1:
                self.left = True
            nextx = self.x-1 if self.left else self.x+1
            if self.game.g[self.y][nextx] in _t:
                self.left = not self.left
                nextx = self.x-1 if self.left else self.x+1

            if self.game.g[self.y][nextx]==Elem.empty: #go next
                self.game.g[self.y][nextx] = Elem.fire
                self.game.g[self.y][self.x] = Elem.empty
                self.x = nextx
            elif self.game.g[self.y][nextx] in Elem.chunks+[Elem.heart]: #burn next
                self.game.new_fire(self.y,nextx)
            elif self.game.g[self.y][nextx]==Elem.fire: #ourselves, just keep calm
                pass
            elif self.game.g[self.y][nextx]==Elem.player: #beat next
                if self.game.player.hurt(BURN_LIFE):
                    raise _FireStopped()
            else:
                raise RuntimeError('bad next elem: %s'%self.game.g[self.y][nextx])

class Player:
    def __init__(self, game, y, x):
        self.life = 100
        self.life_restore = 0
        self.life_waiting = False
        self.left = False
        self.command = Command.empty
        self.game = game
        self.y = y
        self.x = x

    def _move(self, y, x):
        def go():
            self.game.g[self.y][self.x] = Elem.empty
            self.game.g[y][x] = Elem.player
            self.y = y
            self.x = x

        if not (0<=x<GX and 0<=y<GY):
            return False
        if self.game.g[y][x]==Elem.empty:
            return go() or True
        elif self.game.g[y][x]==Elem.heart:
            self.life = min(self.life+HEART_LIFE,100)
            return go() or True
        elif self.game.g[y][x] in Elem.chunks:
            self.game.cur += SALARY[self.game.g[y][x]]
            if self.game.cur>=self.game.goal:
                raise YouWin()
            return go() or True
        elif self.game.g[y][x] == Elem.fire:
            for fire in self.game.fires:
                if fire.y==y and fire.x==x:
                    if self.hurt(BURN_LIFE):
                        self.game.g[y][x] = Elem.empty
                        self.game.fires.remove(fire)
                        return self._move(y, x)
                    else:
                        return False
            else:
                raise RuntimeError('cant find right fire')

    def _digg(self, y, x):
        if not (0<=x<GX and 0<=y<GY):
            return
        if self.game.g[y][x]==Elem.dirt:
            self.game.g[y][x] = Elem.empty
        elif self.game.g[y][x]==Elem.evildirt:
            self.game.new_fire(y,x)

    def tick(self):
        if self.life_restore:
            self.life_restore -= 1
        if not self.life_waiting:
            self.life -= 1
        self.life_waiting = not self.life_waiting
        if self.life<=0:
            raise GameOver()

        if not self._move(self.y+1, self.x):
            if self.command==Command.left:
                if self.left:
                    self._move(self.y, self.x-1)
                self.left = True
            elif self.command==Command.right:
                if not self.left:
                    self._move(self.y, self.x+1)
                self.left = False
            elif self.command==Command.next:
                self._digg(self.y, self.x+(-1 if self.left else 1))
            elif self.command==Command.down:
                self._digg(self.y, self.x+(-1 if self.left else 1))
                self._digg(self.y+1, self.x+(-1 if self.left else 1))
            self.command = Command.empty

if self.life_restore:
            return False
        else:
            self.life -= value
            self.life_restore = RESTORE_TIME
            if self.life<=0:
                raise GameOver()
            return True

class Game:
    def __init__(self, socket, token):
        self.g = None
        self.level = -1
        self.cur = 0
        self.goal = 0
        self.fires = set()
        self.player = Player(self, 0, 0)
        self.fire_ticked = False

        self.socket = socket
        self.synced_g = None
        self.token = token
        self.uid = checktoken(token)

    def wait_command(self):
        b = self.socket.recv(1)
        return Command._check(struct.unpack('!b', b)[0])

    def sync_level(self):
        self.socket.send(struct.pack('!b', Sync.sync_level))
        self.socket.send(struct.pack('!ii', self.level, self.goal))

    def sync_status(self):
        self.socket.send(struct.pack('!b', Sync.sync_status))
        self.socket.send(struct.pack('!iii?iii',
            self.cur,
            self.player.life, self.player.life_restore, self.player.left,
            self.player.y, self.player.x, self.player.command,
        ))

    def sync_map(self):
        buf = []
        for y, row in enumerate(self.g):
            for x, col in enumerate(row):
                if self.synced_g is None or self.synced_g[y][x]!=col:
                    buf.append(struct.pack('!bbb', y, x, col))

        if buf:
            self.socket.send(struct.pack('!b', Sync.sync_map))
            self.socket.send(b''.join(buf))

        self.synced_g = copy.deepcopy(self.g)

    def sync_init_level(self):
        self.socket.send(struct.pack('!b', Sync.init_level))

    def sync_show_hud(self, text):
        logger.write(self.uid, ['show_hud', text])
        self.socket.send(struct.pack('!b', Sync.show_hud))
        self.socket.send(text.encode('utf-8'))

    def sync_tick_routine(self):
        self.socket.send(struct.pack('!b', Sync.tick_routine))

    def start_controller(self):
        self.sync_show_hud('欢迎来到神秘小游戏\n完成第 15 级\n可以获得 Flag 1')
        self.sync_show_hud('完成第 15 级\n且总用时不超过 %ss\n可以获得 Flag 2'%GOOD_TIME_S)
        self.init_level(INIT_LEVEL)
        start_time = time.time()

        while True:
            try:
                self.player.command = self.wait_command()
                self.tick()
            except GameOver:
                self.sync_show_hud('您输了\n最高等级 %d'%self.level)
                self.init_level(INIT_LEVEL)
                start_time = time.time()
            except YouWin:
                time_used = int(time.time()-start_time)
                self.sync_show_hud('您赢了\n进入下一级\n总用时 %ds'%time_used)
                if self.level==15:
                    self.sync_show_hud('Flag 1:\n%s\n你记住了吗？'%getflag(self.token, 0))
                    if time_used<=GOOD_TIME_S:
                        self.sync_show_hud('Flag 2:\n%s\n你记住了吗？'%getflag(self.token, 1))
                self.init_level(self.level+1)

    def init_level(self,level):
        # init grid
        self.g = [[Elem.dirt if random.random()>EVIL_PROB else Elem.evildirt for y in range(GX)] for x in range(GY)]
        self.g[0][math.floor(GX/2)] = Elem.player
        self.g[0][math.floor(GX/2)-1] = Elem.empty
        self.g[0][math.floor(GX/2)+1] = Elem.empty

        # put goods
        available = [(y,x) for y in range(GY) for x in range(GX) if self.g[y][x]==Elem.dirt]
        for y, x in random.sample(available, math.ceil(GX*GY*ELEM_PROB)):
            self.g[y][x] = random.choice(Elem.chunks+[Elem.heart])

        # no goods below initial platform
        if self.g[1][math.floor(GX/2)-1]!=Elem.evildirt:
            self.g[1][math.floor(GX/2)-1] = Elem.dirt
        if self.g[1][math.floor(GX/2)]!=Elem.evildirt:
            self.g[1][math.floor(GX/2)] = Elem.dirt
        if self.g[1][math.floor(GX/2)+1]!=Elem.evildirt:
            self.g[1][math.floor(GX/2)+1] = Elem.dirt

        # init data
        self.level = level
        self.cur = 0
        self.goal = GOAL_OF_LEVEL(level)
        self.fires = set()
        self.player = Player(self, 0, math.floor(GX/2))

        # trigger client
        self.sync_level()
        self.sync_status()
        self.sync_map()
        self.sync_init_level()
        self.sync_show_hud('等级 %d：\n目标 $%d'%(self.level, self.goal))

    def tick_fire(self):
        for fire in sorted(self.fires, key=lambda this: this.y*GX+this.x):
            try:
                fire.tick()
            except _FireStopped:
                self.g[fire.y][fire.x] = Elem.empty
                self.fires.remove(fire)

    def tick_item(self):
        for y in range(GY-2, -1, -1):
            for x in range(GX):
                if self.g[y][x] in Elem.chunks+[Elem.heart]:
                    if self.g[y+1][x]==Elem.empty:
                        self.g[y+1][x] = self.g[y][x]
                        self.g[y][x] = Elem.empty
                    elif self.g[y+1][x]==Elem.player:
                        if self.g[y][x]==Elem.heart:
                            self.player.life = min(self.player.life+HEART_LIFE, 100)
                        else:
                            self.cur += SALARY[self.g[y][x]]
                            if self.cur>=self.goal:
                                raise YouWin()
                        self.g[y][x] = Elem.empty
                    elif self.g[y+1][x]==Elem.fire:
                        self.new_fire(y,x)

    def new_fire(self,y,x):
        self.g[y][x] = Elem.fire
        self.fires.add(Fire(self, y, x))

    def tick(self):
        self.tick_item()
        if self.fire_ticked:
            self.tick_fire()
        self.fire_ticked = not self.fire_ticked
        self.player.tick()

        self.sync_status()
        self.sync_map()
        self.sync_tick_routine()


import random
import math
import copy
import struct
import time

from flag import getflag, checktoken
import logger

GX = 25
GY = 39
INIT_LEVEL = 1
ELEM_PROB = .1
EVIL_PROB = .12
HEART_LIFE = 25
BURN_LIFE = 20
RESTORE_TIME = 10
GOAL_OF_LEVEL = lambda level: 300+int(level**1.5)*100
GOOD_TIME_S = 100

class GameOver(Exception):
    def __str__(self):

class YouWin(Exception):
    def __str__(self): return 'You Win'

class _FireStopped(Exception):
    pass

class CheckerMixin:
    @classmethod
    def _check(cls, value):
        for k, v in cls.__dict__.items():
            if not k.startswith('_') and v==value:
                return v
        raise RuntimeError('bad value %s for %s'%(value, cls))

class Elem(CheckerMixin):
    dirt = 1
    evildirt = 2
    empty = 3
    fire = 4
    heart = 5
    chunk1 = 6
    chunk2 = 7
    chunk3 = 8
    chunks = [6, 7, 8]
    player = 9

class Command(CheckerMixin):
    left = 1
    right = 2
    next = 3
    down = 4
    empty = 5

class Sync(CheckerMixin):
    sync_level = 1
    sync_status = 2
    sync_map = 3
    init_level = 4
    show_hud = 5
    tick_routine = 6

SALARY = {Elem.chunk1: 100, Elem.chunk2: 200, Elem.chunk3: 300}

class Fire:
    def __init__(self, game, y, x):
        self.left = False
        self.y = y
        self.x = x
        self.game = game

    def tick(self):
        _t = {Elem.dirt, Elem.evildirt, Elem.fire}

        if self.y<GY-1 and self.game.g[self.y+1][self.x]==Elem.empty: #go down
            self.game.g[self.y+1][self.x] = Elem.fire
            self.game.g[self.y][self.x] = Elem.empty
            self.y += 1
        elif self.y<GY-1 and self.game.g[self.y+1][self.x] in Elem.chunks+[Elem.heart]: #burn down
            self.game.new_fire(self.y+1,self.x)
        elif self.y<GY-1 and self.game.g[self.y+1][self.x]==Elem.player: #beat down
            if self.game.player.hurt(BURN_LIFE):
                 raise _FireStopped()
        elif 0<self.x<GX-1 and self.game.g[self.y][self.x+1] in _t and self.game.g[self.y][self.x-1] in _t: #do nothing A
            return
        elif (self.x==0 and self.game.g[self.y][1] in _t) or (self.x==GX-1 and self.game.g[self.y][GX-2] in _t): #do nothing B
            return
        else:
            if self.x==0:
                self.left = False
            if self.x==GX-1:
                self.left = True
            nextx = self.x-1 if self.left else self.x+1
            if self.game.g[self.y][nextx] in _t:
                self.left = not self.left
                nextx = self.x-1 if self.left else self.x+1

            if self.game.g[self.y][nextx]==Elem.empty: #go next
                self.game.g[self.y][nextx] = Elem.fire
                self.game.g[self.y][self.x] = Elem.empty
                self.x = nextx
            elif self.game.g[self.y][nextx] in Elem.chunks+[Elem.heart]: #burn next
                self.game.new_fire(self.y,nextx)
            elif self.game.g[self.y][nextx]==Elem.fire: #ourselves, just keep calm
                pass
            elif self.game.g[self.y][nextx]==Elem.player: #beat next
                if self.game.player.hurt(BURN_LIFE):
                    raise _FireStopped()
            else:
                raise RuntimeError('bad next elem: %s'%self.game.g[self.y][nextx])

class Player:
    def __init__(self, game, y, x):
        self.life = 100
        self.life_restore = 0
        self.life_waiting = False
        self.left = False
        self.command = Command.empty
        self.game = game
        self.y = y
        self.x = x

    def _move(self, y, x):
        def go():
            self.game.g[self.y][self.x] = Elem.empty
            self.game.g[y][x] = Elem.player
            self.y = y
            self.x = x

        if not (0<=x<GX and 0<=y<GY):
            return False
        if self.game.g[y][x]==Elem.empty:
            return go() or True
        elif self.game.g[y][x]==Elem.heart:
            self.life = min(self.life+HEART_LIFE,100)
            return go() or True
        elif self.game.g[y][x] in Elem.chunks:
            self.game.cur += SALARY[self.game.g[y][x]]
            if self.game.cur>=self.game.goal:
                raise YouWin()
            return go() or True
        elif self.game.g[y][x] == Elem.fire:
            for fire in self.game.fires:
                if fire.y==y and fire.x==x:
                    if self.hurt(BURN_LIFE):
                        self.game.g[y][x] = Elem.empty
                        self.game.fires.remove(fire)
                        return self._move(y, x)
                    else:
                        return False
            else:
                raise RuntimeError('cant find right fire')

    def _digg(self, y, x):
        if not (0<=x<GX and 0<=y<GY):
            return
        if self.game.g[y][x]==Elem.dirt:
            self.game.g[y][x] = Elem.empty
        elif self.game.g[y][x]==Elem.evildirt:
            self.game.new_fire(y,x)

    def tick(self):
        if self.life_restore:
            self.life_restore -= 1
        if not self.life_waiting:
            self.life -= 1
        self.life_waiting = not self.life_waiting
        if self.life<=0:
            raise GameOver()

        if not self._move(self.y+1, self.x):
            if self.command==Command.left:
                if self.left:
                    self._move(self.y, self.x-1)
                self.left = True
            elif self.command==Command.right:
                if not self.left:
                    self._move(self.y, self.x+1)
                self.left = False
            elif self.command==Command.next:
                self._digg(self.y, self.x+(-1 if self.left else 1))
            elif self.command==Command.down:
                self._digg(self.y, self.x+(-1 if self.left else 1))
                self._digg(self.y+1, self.x+(-1 if self.left else 1))
            self.command = Command.empty

    def hurt(self, value):
        if self.life_restore:
            return False
        else:
            self.life -= value
            self.life_restore = RESTORE_TIME
            if self.life<=0:
                raise GameOver()
            return True

class Game:
    def __init__(self, socket, token):
        self.g = None
        self.level = -1
        self.cur = 0
        self.goal = 0
        self.fires = set()
        self.player = Player(self, 0, 0)
        self.fire_ticked = False

        self.socket = socket
        self.synced_g = None
        self.token = token
        self.uid = checktoken(token)

    def wait_command(self):
        b = self.socket.recv(1)
        return Command._check(struct.unpack('!b', b)[0])

    def sync_level(self):
        self.socket.send(struct.pack('!b', Sync.sync_level))
        self.socket.send(struct.pack('!ii', self.level, self.goal))

    def sync_status(self):
        self.socket.send(struct.pack('!b', Sync.sync_status))
        self.socket.send(struct.pack('!iii?iii',
            self.cur,
            self.player.life, self.player.life_restore, self.player.left,
            self.player.y, self.player.x, self.player.command,
        ))

    def sync_map(self):
        buf = []
        for y, row in enumerate(self.g):
            for x, col in enumerate(row):
                if self.synced_g is None or self.synced_g[y][x]!=col:
                    buf.append(struct.pack('!bbb', y, x, col))

        if buf:
            self.socket.send(struct.pack('!b', Sync.sync_map))
            self.socket.send(b''.join(buf))

        self.synced_g = copy.deepcopy(self.g)

    def sync_init_level(self):
        self.socket.send(struct.pack('!b', Sync.init_level))

    def sync_show_hud(self, text):
        logger.write(self.uid, ['show_hud', text])
        self.socket.send(struct.pack('!b', Sync.show_hud))
        self.socket.send(text.encode('utf-8'))

    def sync_tick_routine(self):
        self.socket.send(struct.pack('!b', Sync.tick_routine))

    def start_controller(self):
        self.sync_show_hud('欢迎来到神秘小游戏\n完成第 15 级\n可以获得 Flag 1')
        self.sync_show_hud('完成第 15 级\n且总用时不超过 %ss\n可以获得 Flag 2'%GOOD_TIME_S)
        self.init_level(INIT_LEVEL)
        start_time = time.time()

        while True:
            try:
                self.player.command = self.wait_command()
                self.tick()
            except GameOver:
                self.sync_show_hud('您输了\n最高等级 %d'%self.level)
                self.init_level(INIT_LEVEL)
                start_time = time.time()
            except YouWin:
                time_used = int(time.time()-start_time)
                self.sync_show_hud('您赢了\n进入下一级\n总用时 %ds'%time_used)
                if self.level==15:
                    self.sync_show_hud('Flag 1:\n%s\n你记住了吗？'%getflag(self.token, 0))
                    if time_used<=GOOD_TIME_S:
                        self.sync_show_hud('Flag 2:\n%s\n你记住了吗？'%getflag(self.token, 1))
                self.init_level(self.level+1)

    def init_level(self,level):
        # init grid
        self.g = [[Elem.dirt if random.random()>EVIL_PROB else Elem.evildirt for y in range(GX)] for x in range(GY)]
        self.g[0][math.floor(GX/2)] = Elem.player
        self.g[0][math.floor(GX/2)-1] = Elem.empty
        self.g[0][math.floor(GX/2)+1] = Elem.empty

        # put goods
        available = [(y,x) for y in range(GY) for x in range(GX) if self.g[y][x]==Elem.dirt]
        for y, x in random.sample(available, math.ceil(GX*GY*ELEM_PROB)):
            self.g[y][x] = random.choice(Elem.chunks+[Elem.heart])

        # no goods below initial platform
        if self.g[1][math.floor(GX/2)-1]!=Elem.evildirt:
            self.g[1][math.floor(GX/2)-1] = Elem.dirt
        if self.g[1][math.floor(GX/2)]!=Elem.evildirt:
            self.g[1][math.floor(GX/2)] = Elem.dirt
        if self.g[1][math.floor(GX/2)+1]!=Elem.evildirt:
            self.g[1][math.floor(GX/2)+1] = Elem.dirt

        # init data
        self.level = level
        self.cur = 0
        self.goal = GOAL_OF_LEVEL(level)
        self.fires = set()
        self.player = Player(self, 0, math.floor(GX/2))

        # trigger client
        self.sync_level()
        self.sync_status()
        self.sync_map()
        self.sync_init_level()
        self.sync_show_hud('等级 %d：\n目标 $%d'%(self.level, self.goal))

    def tick_fire(self):
        for fire in sorted(self.fires, key=lambda this: this.y*GX+this.x):
            try:
                fire.tick()
            except _FireStopped:
                self.g[fire.y][fire.x] = Elem.empty
                self.fires.remove(fire)

    def tick_item(self):
        for y in range(GY-2, -1, -1):
            for x in range(GX):
                if self.g[y][x] in Elem.chunks+[Elem.heart]:
                    if self.g[y+1][x]==Elem.empty:
                        self.g[y+1][x] = self.g[y][x]
                        self.g[y][x] = Elem.empty
                    elif self.g[y+1][x]==Elem.player:
                        if self.g[y][x]==Elem.heart:
                            self.player.life = min(self.player.life+HEART_LIFE, 100)
                        else:
                            self.cur += SALARY[self.g[y][x]]
                            if self.cur>=self.goal:
                                raise YouWin()
                        self.g[y][x] = Elem.empty
                    elif self.g[y+1][x]==Elem.fire:
                        self.new_fire(y,x)

    def new_fire(self,y,x):
        self.g[y][x] = Elem.fire
        self.fires.add(Fire(self, y, x))

    def tick(self):
        self.tick_item()
        if self.fire_ticked:
            self.tick_fire()
        self.fire_ticked = not self.fire_ticked
        self.player.tick()

        self.sync_status()
        self.sync_map()
        self.sync_tick_routine()


import random
import math
import copy
import struct
import time

from flag import getflag, checktoken
import logger

GX = 25
GY = 39
INIT_LEVEL = 1
ELEM_PROB = .1
EVIL_PROB = .12
HEART_LIFE = 25
BURN_LIFE = 20
RESTORE_TIME = 10
GOAL_OF_LEVEL = lambda level: 300+int(level**1.5)*100
GOOD_TIME_S = 100

class GameOver(Exception):
    def __str__(self): return 'Game Over'

class YouWin(Exception):
    def __str__(self): return 'You Win'

class _FireStopped(Exception):
    pass

class CheckerMixin:
    @classmethod
    def _check(cls, value):
        for k, v in cls.__dict__.items():
            if not k.startswith('_') and v==value:
                return v
        raise RuntimeError('bad value %s for %s'%(value, cls))

class Elem(CheckerMixin):
    dirt = 1
    evildirt = 2
    empty = 3
    fire = 4
    heart = 5
    chunk1 = 6
    chunk2 = 7
    chunk3 = 8
    chunks = [6, 7, 8]
    player = 9

class Command(CheckerMixin):
    left = 1
    right = 2
    next = 3
    down = 4
    empty = 5

class Sync(CheckerMixin):
    sync_level = 1
    sync_status = 2
    sync_map = 3
    init_level = 4
    show_hud = 5
    tick_routine = 6

SALARY = {Elem.chunk1: 100, Elem.chunk2: 200, Elem.chunk3: 300}

class Fire:
    def __init__(self, game, y, x):
        self.left = False
        self.y = y
        self.x = x
        self.game = game

    def tick(self):
        _t = {Elem.dirt, Elem.evildirt, Elem.fire}

        if self.y<GY-1 and self.game.g[self.y+1][self.x]==Elem.empty: #go down
            self.game.g[self.y+1][self.x] = Elem.fire
            self.game.g[self.y][self.x] = Elem.empty
            self.y += 1
        elif self.y<GY-1 and self.game.g[self.y+1][self.x] in Elem.chunks+[Elem.heart]: #burn down
            self.game.new_fire(self.y+1,self.x)
        elif self.y<GY-1 and self.game.g[self.y+1][self.x]==Elem.player: #beat down
            if self.game.player.hurt(BURN_LIFE):
                 raise _FireStopped()
        elif 0<self.x<GX-1 and self.game.g[self.y][self.x+1] in _t and self.game.g[self.y][self.x-1] in _t: #do nothing A
            return
        elif (self.x==0 and self.game.g[self.y][1] in _t) or (self.x==GX-1 and self.game.g[self.y][GX-2] in _t): #do nothing B
            return
        else:
            if self.x==0:
                self.left = False
            if self.x==GX-1:
                self.left = True
            nextx = self.x-1 if self.left else self.x+1
            if self.game.g[self.y][nextx] in _t:
                self.left = not self.left
                nextx = self.x-1 if self.left else self.x+1

            if self.game.g[self.y][nextx]==Elem.empty: #go next
                self.game.g[self.y][nextx] = Elem.fire
                self.game.g[self.y][self.x] = Elem.empty
                self.x = nextx
            elif self.game.g[self.y][nextx] in Elem.chunks+[Elem.heart]: #burn next
                self.game.new_fire(self.y,nextx)
            elif self.game.g[self.y][nextx]==Elem.fire: #ourselves, just keep calm
                pass
            elif self.game.g[self.y][nextx]==Elem.player: #beat next
                if self.game.player.hurt(BURN_LIFE):
                    raise _FireStopped()
            else:
                raise RuntimeError('bad next elem: %s'%self.game.g[self.y][nextx])

class Player:
    def __init__(self, game, y, x):
        self.life = 100
        self.life_restore = 0
        self.life_waiting = False
        self.left = False
        self.command = Command.empty
        self.game = game
        self.y = y

    def _move(self, y, x):
        def go():
            self.game.g[self.y][self.x] = Elem.empty
            self.game.g[y][x] = Elem.player
            self.y = y
            self.x = x

        if not (0<=x<GX and 0<=y<GY):
            return False
        if self.game.g[y][x]==Elem.empty:
            return go() or True
        elif self.game.g[y][x]==Elem.heart:
            self.life = min(self.life+HEART_LIFE,100)
            return go() or True
        elif self.game.g[y][x] in Elem.chunks:
            self.game.cur += SALARY[self.game.g[y][x]]
            if self.game.cur>=self.game.goal:
                raise YouWin()
            return go() or True
        elif self.game.g[y][x] == Elem.fire:
            for fire in self.game.fires:
                if fire.y==y and fire.x==x:
                    if self.hurt(BURN_LIFE):
                        self.game.g[y][x] = Elem.empty
                        self.game.fires.remove(fire)
                        return self._move(y, x)
                    else:
                        return False
            else:
                raise RuntimeError('cant find right fire')

    def _digg(self, y, x):
        if not (0<=x<GX and 0<=y<GY):
            return
        if self.game.g[y][x]==Elem.dirt:
            self.game.g[y][x] = Elem.empty
        elif self.game.g[y][x]==Elem.evildirt:
            self.game.new_fire(y,x)

    def tick(self):
        if self.life_restore:
            self.life_restore -= 1
        if not self.life_waiting:
            self.life -= 1
        self.life_waiting = not self.life_waiting
        if self.life<=0:
            raise GameOver()

        if not self._move(self.y+1, self.x):
            if self.command==Command.left:
                if self.left:
                    self._move(self.y, self.x-1)
                self.left = True
            elif self.command==Command.right:
                if not self.left:
                    self._move(self.y, self.x+1)
                self.left = False
            elif self.command==Command.next:
                self._digg(self.y, self.x+(-1 if self.left else 1))
            elif self.command==Command.down:
                self._digg(self.y, self.x+(-1 if self.left else 1))
                self._digg(self.y+1, self.x+(-1 if self.left else 1))
            self.command = Command.empty

    def hurt(self, value):
        if self.life_restore:
            return False
        else:
            self.life -= value
            self.life_restore = RESTORE_TIME
            if self.life<=0:
                raise GameOver()
            return True

class Game:
    def __init__(self, socket, token):
        self.g = None
        self.level = -1
        self.cur = 0
        self.goal = 0
        self.fires = set()
        self.player = Player(self, 0, 0)
        self.fire_ticked = False

        self.socket = socket
        self.synced_g = None
        self.token = token
        self.uid = checktoken(token)

    def wait_command(self):
        b = self.socket.recv(1)
        return Command._check(struct.unpack('!b', b)[0])

    def sync_level(self):
        self.socket.send(struct.pack('!b', Sync.sync_level))
        self.socket.send(struct.pack('!ii', self.level, self.goal))

    def sync_status(self):
        self.socket.send(struct.pack('!b', Sync.sync_status))
        self.socket.send(struct.pack('!iii?iii',
            self.cur,
            self.player.life, self.player.life_restore, self.player.left,
            self.player.y, self.player.x, self.player.command,
        ))

    def sync_map(self):
        buf = []
        for y, row in enumerate(self.g):
            for x, col in enumerate(row):
                if self.synced_g is None or self.synced_g[y][x]!=col:
                    buf.append(struct.pack('!bbb', y, x, col))

        if buf:
            self.socket.send(struct.pack('!b', Sync.sync_map))
            self.socket.send(b''.join(buf))

        self.synced_g = copy.deepcopy(self.g)

    def sync_init_level(self):
        self.socket.send(struct.pack('!b', Sync.init_level))

    def sync_show_hud(self, text):
        logger.write(self.uid, ['show_hud', text])
        self.socket.send(struct.pack('!b', Sync.show_hud))
        self.socket.send(text.encode('utf-8'))

    def sync_tick_routine(self):
        self.socket.send(struct.pack('!b', Sync.tick_routine))

    def start_controller(self):
        self.sync_show_hud('欢迎来到神秘小游戏\n完成第 15 级\n可以获得 Flag 1')
        self.sync_show_hud('完成第 15 级\n且总用时不超过 %ss\n可以获得 Flag 2'%GOOD_TIME_S)
        self.init_level(INIT_LEVEL)
        start_time = time.time()

        while True:
            try:
                self.player.command = self.wait_command()
                self.tick()
            except GameOver:
                self.sync_show_hud('您输了\n最高等级 %d'%self.level)
                self.init_level(INIT_LEVEL)
                start_time = time.time()
            except YouWin:
                time_used = int(time.time()-start_time)
                self.sync_show_hud('您赢了\n进入下一级\n总用时 %ds'%time_used)
                if self.level==15:
                    self.sync_show_hud('Flag 1:\n%s\n你记住了吗？'%getflag(self.token, 0))
                    if time_used<=GOOD_TIME_S:
                        self.sync_show_hud('Flag 2:\n%s\n你记住了吗？'%getflag(self.token, 1))
                self.init_level(self.level+1)

    def init_level(self,level):
        # init grid
        self.g = [[Elem.dirt if random.random()>EVIL_PROB else Elem.evildirt for y in range(GX)] for x in range(GY)]
        self.g[0][math.floor(GX/2)] = Elem.player
        self.g[0][math.floor(GX/2)-1] = Elem.empty
        self.g[0][math.floor(GX/2)+1] = Elem.empty

        # put goods
        available = [(y,x) for y in range(GY) for x in range(GX) if self.g[y][x]==Elem.dirt]
        for y, x in random.sample(available, math.ceil(GX*GY*ELEM_PROB)):
            self.g[y][x] = random.choice(Elem.chunks+[Elem.heart])

        # no goods below initial platform
        if self.g[1][math.floor(GX/2)-1]!=Elem.evildirt:
            self.g[1][math.floor(GX/2)-1] = Elem.dirt
        if self.g[1][math.floor(GX/2)]!=Elem.evildirt:
            self.g[1][math.floor(GX/2)] = Elem.dirt
        if self.g[1][math.floor(GX/2)+1]!=Elem.evildirt:
            self.g[1][math.floor(GX/2)+1] = Elem.dirt

        # init data
        self.level = level
        self.cur = 0
        self.goal = GOAL_OF_LEVEL(level)
        self.fires = set()
        self.player = Player(self, 0, math.floor(GX/2))

        # trigger client
        self.sync_level()
        self.sync_status()
        self.sync_map()
        self.sync_init_level()
        self.sync_show_hud('等级 %d：\n目标 $%d'%(self.level, self.goal))

    def tick_fire(self):
        for fire in sorted(self.fires, key=lambda this: this.y*GX+this.x):
            try:
                fire.tick()
            except _FireStopped:
                self.g[fire.y][fire.x] = Elem.empty
                self.fires.remove(fire)

    def tick_item(self):
        for y in range(GY-2, -1, -1):
            for x in range(GX):
                if self.g[y][x] in Elem.chunks+[Elem.heart]:
                    if self.g[y+1][x]==Elem.empty:
                        self.g[y+1][x] = self.g[y][x]
                        self.g[y][x] = Elem.empty
                    elif self.g[y+1][x]==Elem.player:
                        if self.g[y][x]==Elem.heart:
                            self.player.life = min(self.player.life+HEART_LIFE, 100)
                        else:
                            self.cur += SALARY[self.g[y][x]]
                            if self.cur>=self.goal:
                                raise YouWin()
                        self.g[y][x] = Elem.empty
                    elif self.g[y+1][x]==Elem.fire:
                        self.new_fire(y,x)

    def new_fire(self,y,x):
        self.g[y][x] = Elem.fire
        self.fires.add(Fire(self, y, x))

    def tick(self):
        self.tick_item()
        if self.fire_ticked:
            self.tick_fire()
        self.fire_ticked = not self.fire_ticked
        self.player.tick()

        self.sync_status()
        self.sync_map()
        self.sync_tick_routine()