bl_info = {
    "name": "Batch Frame Render",
    "author": "Haiblender",
    "version": (1, 12, 0),
    "blender": (5, 2, 0),
    "location": "View3D > Sidebar > Batch Render",
    "description": "Render specific frames, markers, keyframes, or generated frame patterns with custom settings to the project's output location",
    "category": "Render",
}

import bpy
import os
from bpy.props import StringProperty, BoolProperty, IntProperty, EnumProperty
from bpy.types import Operator, Panel, PropertyGroup

# ============================================================================
# Helper Functions
# ============================================================================

def get_frame_list(frame_string):
    """Parses input string into sorted unique integers."""
    if not frame_string or not frame_string.strip():
        return []
    frames = []
    parts = frame_string.split(',')
    for part in parts:
        part = part.strip()
        if not part:
            continue
        try:
            if '-' in part:
                start, end = part.split('-', 1)
                start_frame = int(start.strip())
                end_frame = int(end.strip())
                frames.extend(range(start_frame, end_frame + 1))
            else:
                frames.append(int(part))
        except ValueError:
            continue
    return sorted(list(set(f for f in frames if f >= 0)))


def get_marker_frames(scene):
    """Get all timeline marker frames from the scene"""
    marker_frames = []

    if hasattr(scene, 'timeline_markers') and scene.timeline_markers:
        for marker in scene.timeline_markers:
            marker_frames.append(int(marker.frame))

    return sorted(list(set(marker_frames)))


def get_keyframe_frames(scene):
    """Get all keyframe frames from animated objects in the scene"""
    keyframe_frames = []

    for obj in scene.objects:
        if obj.animation_data and obj.animation_data.action:
            action = obj.animation_data.action
            for fcurve in action.fcurves:
                for keyframe in fcurve.keyframe_points:
                    keyframe_frames.append(int(keyframe.co[0]))

        if obj.animation_data and obj.animation_data.nla_tracks:
            for track in obj.animation_data.nla_tracks:
                for strip in track.strips:
                    if strip.action:
                        for fcurve in strip.action.fcurves:
                            for keyframe in fcurve.keyframe_points:
                                keyframe_frames.append(int(keyframe.co[0]))

    return sorted(list(set(keyframe_frames)))


def get_every_n_frames(scene, n):
    """Get every Nth frame across the timeline range"""
    if n is None or n < 1:
        n = 1
    start, end = scene.frame_start, scene.frame_end
    if end < start:
        return []
    return list(range(start, end + 1, n))


def get_source_frames(context):
    """Compute the raw (unfiltered) frame list for whichever Frame Source is active"""
    scene = context.scene
    props = scene.batch_render_props
    mode = props.frame_source

    if mode == 'MANUAL':
        return get_frame_list(props.frame_list)
    elif mode == 'KEYFRAMES':
        return get_keyframe_frames(scene)
    elif mode == 'MARKERS':
        return get_marker_frames(scene)
    elif mode == 'EVERY_N':
        return get_every_n_frames(scene, props.every_n_value)

    return []


def split_valid_invalid(scene, frames):
    """Split a frame list into (valid, invalid) based on the scene timeline range"""
    start, end = scene.frame_start, scene.frame_end
    valid = [f for f in frames if start <= f <= end]
    invalid = [f for f in frames if f < start or f > end]
    return valid, invalid


def get_scene_samples(scene, rd):
    """Return the current sample count for the active render engine"""
    if rd.engine == 'CYCLES':
        return scene.cycles.samples
    elif rd.engine in ('BLENDER_EEVEE', 'BLENDER_EEVEE_NEXT'):
        return scene.eevee.taa_render_samples
    return 0


def get_output_filepath(base_path, frame_number, file_format):
    """
    Construct the output filepath for a specific frame.
    Handles Blender's output path and adds frame number properly.
    """
    abs_path = bpy.path.abspath(base_path)

    ext_map = {
        'PNG': '.png',
        'JPEG': '.jpg',
        'OPEN_EXR': '.exr',
        'TIFF': '.tif',
        'BMP': '.bmp',
        'TARGA': '.tga',
        'TARGA_RAW': '.tga',
    }
    ext = ext_map.get(file_format, '.png')

    if abs_path.endswith(os.sep) or os.path.isdir(abs_path):
        output_path = os.path.join(abs_path, f"{frame_number:04d}{ext}")
    else:
        dir_path = os.path.dirname(abs_path)
        basename = os.path.basename(abs_path)
        name_without_ext = os.path.splitext(basename)[0]
        output_path = os.path.join(dir_path, f"{name_without_ext}_{frame_number:04d}{ext}")

    return output_path


# ============================================================================
# Property Group
# ============================================================================

class BatchRenderProperties(PropertyGroup):
    # Frame list (Manual source)
    frame_list: StringProperty(
        name="Frames",
        description="Comma-separated list of frame numbers (e.g., 1,5,10-20,30)",
        default="1,10,20,30"
    )

    # Frame source mode
    frame_source: EnumProperty(
        name="Source",
        description="Where the frames to render come from",
        items=[
            ('MANUAL', "Manual (Custom)", "Use the frame numbers/ranges entered above"),
            ('KEYFRAMES', "Keyframes", "Use frames from animated object keyframes"),
            ('MARKERS', "Timeline Markers", "Use frames from the scene's timeline markers"),
            ('EVERY_N', "Every N Frames", "Use every Nth frame across the timeline"),
        ],
        default='MANUAL',
    )
    every_n_value: IntProperty(
        name="N",
        description="Render every Nth frame across the timeline",
        default=10,
        min=1,
        max=100000
    )
    # Rendering state
    is_rendering: BoolProperty(
        name="Is Rendering",
        default=False
    )
    current_frame_index: IntProperty(
        name="Current Index",
        default=0
    )
    total_frames: IntProperty(
        name="Total Frames",
        default=0
    )

    # Custom render settings
    use_custom_resolution: BoolProperty(
        name="Override Resolution",
        description="Use custom resolution instead of scene settings",
        default=False
    )
    custom_resolution_x: IntProperty(
        name="Resolution X",
        description="Custom render width",
        default=1920,
        min=4,
        max=65536
    )
    custom_resolution_y: IntProperty(
        name="Resolution Y",
        description="Custom render height",
        default=1080,
        min=4,
        max=65536
    )
    resolution_percentage: IntProperty(
        name="Resolution %",
        description="Percentage scale of the resolution",
        default=100,
        min=1,
        max=32767,
        subtype='PERCENTAGE'
    )

    # Samples
    use_custom_samples: BoolProperty(
        name="Override Samples",
        description="Use custom samples instead of scene settings",
        default=False
    )
    custom_samples: IntProperty(
        name="Samples",
        description="Number of samples for rendering",
        default=128,
        min=1,
        max=1000000
    )

    # Output format
    use_custom_format: BoolProperty(
        name="Override Output Format",
        description="Use custom output format instead of scene settings",
        default=False
    )
    custom_format: EnumProperty(
        name="Format",
        description="Output file format",
        items=[
            ('PNG', "PNG", "PNG image format"),
            ('JPEG', "JPEG", "JPEG image format"),
            ('OPEN_EXR', "OpenEXR", "OpenEXR image format"),
            ('TIFF', "TIFF", "TIFF image format"),
            ('BMP', "BMP", "BMP image format"),
            ('TARGA', "Targa", "Targa image format"),
        ],
        default='PNG'
    )

# ============================================================================
# Operators
# ============================================================================

class RENDER_OT_batch_frames(Operator):
    """Render the frames produced by the currently selected Frame Source"""
    bl_idname = "render.batch_frames"
    bl_label = "Batch Render Frames"
    bl_description = "Render all frames from the selected Frame Source with the configured settings. Press Esc to cancel"

    _timer = None
    _frames_to_render = []
    _current_index = 0
    _rendering = False
    _original_settings = {}
    _original_filepath = ""

    def modal(self, context, event):
        props = context.scene.batch_render_props

        # Esc key interrupts the batch render at any time
        if event.type == 'ESC' and event.value == 'PRESS':
            props.is_rendering = False
            self.report({'INFO'}, "Batch render interrupted (Esc)")
            return self.finish(context, cancelled=True)

        if event.type == 'TIMER':
            if not props.is_rendering:
                return self.finish(context, cancelled=True)

            if self._current_index >= len(self._frames_to_render):
                return self.finish(context, cancelled=False)

            if not self._rendering:
                self.render_next_frame(context)

        return {'PASS_THROUGH'}

    def execute(self, context):
        props = context.scene.batch_render_props
        scene = context.scene

        # Compute frames from the active Frame Source
        all_frames = get_source_frames(context)

        if not all_frames:
            self.report({'ERROR'}, "No frames found for the selected Frame Source")
            return {'CANCELLED'}

        # Get timeline range
        timeline_start = scene.frame_start
        timeline_end = scene.frame_end

        valid_frames, invalid_frames = split_valid_invalid(scene, all_frames)

        if invalid_frames:
            self.report({'WARNING'},
                       f"Frames {invalid_frames} are outside timeline ({timeline_start}-{timeline_end}) - skipped")

        if not valid_frames:
            self.report({'ERROR'},
                       f"No frames within timeline range ({timeline_start}-{timeline_end}) to render")
            return {'CANCELLED'}

        self._frames_to_render = valid_frames

        # Check output path
        if not scene.render.filepath:
            self.report({'ERROR'}, "Output path not set. Set it in Render Properties > Output")
            return {'CANCELLED'}

        # Store original filepath
        self._original_filepath = scene.render.filepath

        # Store original settings
        self.store_original_settings(context)

        # Apply custom settings if enabled
        self.apply_custom_settings(context)

        # Initialize rendering state
        self._current_index = 0
        self._rendering = False
        props.is_rendering = True
        props.current_frame_index = 0
        props.total_frames = len(self._frames_to_render)

        # Register handlers
        bpy.app.handlers.render_complete.append(self.render_complete_handler)
        bpy.app.handlers.render_cancel.append(self.render_cancel_handler)
        bpy.app.handlers.render_post.append(self.render_post_handler)

        # Start modal operation
        context.window_manager.modal_handler_add(self)
        self._timer = context.window_manager.event_timer_add(0.1, window=context.window)

        self.report({'INFO'}, f"Starting batch render of {len(self._frames_to_render)} frames")
        return {'RUNNING_MODAL'}

    def store_original_settings(self, context):
        """Store original render settings to restore later"""
        scene = context.scene
        rd = scene.render

        self._original_settings = {
            'resolution_x': rd.resolution_x,
            'resolution_y': rd.resolution_y,
            'resolution_percentage': rd.resolution_percentage,
            'file_format': rd.image_settings.file_format,
            'filepath': rd.filepath,
        }

        self._original_settings['samples'] = get_scene_samples(scene, rd)

    def apply_custom_settings(self, context):
        """Apply custom render settings if enabled"""
        props = context.scene.batch_render_props
        scene = context.scene
        rd = scene.render

        if props.use_custom_resolution:
            rd.resolution_x = props.custom_resolution_x
            rd.resolution_y = props.custom_resolution_y
            rd.resolution_percentage = props.resolution_percentage

        if props.use_custom_samples:
            if rd.engine == 'CYCLES':
                scene.cycles.samples = props.custom_samples
            elif rd.engine in ('BLENDER_EEVEE', 'BLENDER_EEVEE_NEXT'):
                scene.eevee.taa_render_samples = props.custom_samples

        if props.use_custom_format:
            rd.image_settings.file_format = props.custom_format

    def restore_original_settings(self, context):
        """Restore original render settings"""
        if not self._original_settings:
            return

        scene = context.scene
        rd = scene.render

        rd.resolution_x = self._original_settings['resolution_x']
        rd.resolution_y = self._original_settings['resolution_y']
        rd.resolution_percentage = self._original_settings['resolution_percentage']
        rd.image_settings.file_format = self._original_settings['file_format']
        rd.filepath = self._original_settings['filepath']

        if 'samples' in self._original_settings:
            if rd.engine == 'CYCLES':
                scene.cycles.samples = self._original_settings['samples']
            elif rd.engine in ('BLENDER_EEVEE', 'BLENDER_EEVEE_NEXT'):
                scene.eevee.taa_render_samples = self._original_settings['samples']

    def render_next_frame(self, context):
        """Render the next frame in the queue"""
        props = context.scene.batch_render_props
        scene = context.scene
        rd = scene.render

        frame = self._frames_to_render[self._current_index]

        props.current_frame_index = self._current_index + 1

        scene.frame_set(frame)

        current_format = rd.image_settings.file_format

        output_path = get_output_filepath(self._original_filepath, frame, current_format)

        rd.filepath = output_path

        output_dir = os.path.dirname(bpy.path.abspath(output_path))
        if output_dir and not os.path.exists(output_dir):
            try:
                os.makedirs(output_dir, exist_ok=True)
            except Exception as e:
                self.report({'ERROR'}, f"Failed to create output directory: {str(e)}")
                self._rendering = False
                self._current_index += 1
                return

        self._rendering = True

        try:
            bpy.ops.render.render('INVOKE_DEFAULT', write_still=True)
        except Exception as e:
            self.report({'ERROR'}, f"Failed to render frame {frame}: {str(e)}")
            self._rendering = False
            self._current_index += 1

    def render_complete_handler(self, scene, depsgraph=None):
        """Called when a frame finishes rendering successfully"""
        self._rendering = False
        self._current_index += 1

    def render_cancel_handler(self, scene, depsgraph=None):
        """Called when rendering is cancelled"""
        self._rendering = False

    def render_post_handler(self, scene, depsgraph=None):
        """Called after rendering completes (success or failure)"""
        pass

    def finish(self, context, cancelled=False):
        """Clean up and finish the batch render operation"""
        props = context.scene.batch_render_props

        if self._timer:
            context.window_manager.event_timer_remove(self._timer)
            self._timer = None

        if self.render_complete_handler in bpy.app.handlers.render_complete:
            bpy.app.handlers.render_complete.remove(self.render_complete_handler)
        if self.render_cancel_handler in bpy.app.handlers.render_cancel:
            bpy.app.handlers.render_cancel.remove(self.render_cancel_handler)
        if self.render_post_handler in bpy.app.handlers.render_post:
            bpy.app.handlers.render_post.remove(self.render_post_handler)

        self.restore_original_settings(context)

        props.is_rendering = False
        props.current_frame_index = 0

        output_location = os.path.dirname(bpy.path.abspath(self._original_filepath))

        if cancelled:
            self.report({'WARNING'}, f"Batch render cancelled. Rendered {self._current_index} of {len(self._frames_to_render)} frames")
        else:
            self.report({'INFO'}, f"Batch render complete! Successfully rendered {self._current_index} frame(s) to: {output_location}")

        return {'FINISHED'}


class RENDER_OT_cancel_batch(Operator):
    """Stop the batch rendering process"""
    bl_idname = "render.cancel_batch"
    bl_label = "Stop Batch Render"
    bl_description = "Cancel the ongoing batch render operation"

    def execute(self, context):
        props = context.scene.batch_render_props

        if not props.is_rendering:
            self.report({'WARNING'}, "No batch render in progress")
            return {'CANCELLED'}

        props.is_rendering = False
        self.report({'INFO'}, "Batch render stopped by user")

        return {'FINISHED'}


class RENDER_OT_batch_copy_frames(Operator):
    """Copy the currently computed frame list to the clipboard"""
    bl_idname = "render.batch_copy_frames"
    bl_label = "Copy Frame List"
    bl_description = "Copy the frames from the current Frame Source to the clipboard"

    def execute(self, context):
        frames = get_source_frames(context)

        if not frames:
            self.report({'WARNING'}, "No frames to copy")
            return {'CANCELLED'}

        context.window_manager.clipboard = ",".join(str(f) for f in frames)
        self.report({'INFO'}, f"Copied {len(frames)} frame(s) to clipboard")

        return {'FINISHED'}


class RENDER_OT_batch_refresh_frames(Operator):
    """Recompute the current Frame Source and load the result into the Frames field"""
    bl_idname = "render.batch_refresh_frames"
    bl_label = "Refresh Frame List"
    bl_description = "Recompute frames from the current Frame Source and load them into the Frames field"

    def execute(self, context):
        scene = context.scene
        props = scene.batch_render_props

        frames = get_source_frames(context)

        if not frames:
            self.report({'WARNING'}, "No frames found for the current Frame Source")
            return {'CANCELLED'}

        props.frame_list = ",".join(str(f) for f in frames)
        props.frame_source = 'MANUAL'
        self.report({'INFO'}, f"Loaded {len(frames)} frame(s) into the Frames field")

        return {'FINISHED'}


# ============================================================================
# UI Panel
# ============================================================================

class VIEW3D_PT_batch_render(Panel):
    """Panel for batch frame rendering in the 3D viewport sidebar"""
    bl_label = "Batch Frame Render"
    bl_idname = "VIEW3D_PT_batch_render"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'Batch Render'

    def draw_header_row(self, layout, number, title, icon):
        """Draw a numbered section header row (badge + title + help icon)"""
        row = layout.row(align=True)
        badge = row.row(align=True)
        badge.scale_x = 0.6
        badge.alignment = 'CENTER'
        badge.label(text=str(number))
        row.label(text=title, icon=icon)
        row.label(text="", icon='QUESTION')

    def draw(self, context):
        layout = self.layout
        scene = context.scene
        rd = scene.render
        props = scene.batch_render_props

        all_frames = get_source_frames(context)
        valid_frames, invalid_frames = split_valid_invalid(scene, all_frames)

        # ====================================================================
        # 1. FRAME SELECTION
        # ====================================================================
        box = layout.box()
        self.draw_header_row(box, 1, "Frame Selection", 'RENDER_ANIMATION')

        row = box.row(align=True)
        row.prop(props, "frame_list", text="Frames")
        row.operator("render.batch_copy_frames", text="", icon='COPYDOWN')
        row.operator("render.batch_refresh_frames", text="", icon='FILE_REFRESH')

        col = box.column(align=True)
        col.scale_y = 0.8
        col.label(text=f"Timeline Range: {scene.frame_start} - {scene.frame_end}", icon='TIME')

        status_row = box.row(align=True)

        valid_box = status_row.box()
        valid_box.label(text=f"Valid Frames: {len(valid_frames)}", icon='CHECKMARK')

        skip_box = status_row.box()
        if invalid_frames:
            skip_box.alert = True
        skip_box.label(text=f"Skipped: {len(invalid_frames)}", icon='ERROR' if invalid_frames else 'BLANK1')

        # ====================================================================
        # 2. FRAME SOURCE
        # ====================================================================
        box = layout.box()
        self.draw_header_row(box, 2, "Frame Source", 'OUTLINER_OB_CAMERA')

        box.prop(props, "frame_source", text="Source")

        info_col = box.column(align=True)
        info_col.scale_y = 0.8

        if props.frame_source == 'MANUAL':
            info_col.label(text="Enter frame numbers or ranges manually.")
            info_col.label(text="Examples: 1,5,10  or  1-10,20,30-40")
        elif props.frame_source == 'KEYFRAMES':
            info_col.label(text="Uses keyframes from animated objects in the scene.")
        elif props.frame_source == 'MARKERS':
            info_col.label(text="Uses this scene's timeline markers.")
        elif props.frame_source == 'EVERY_N':
            box.prop(props, "every_n_value", text="N")
            info_col.label(text=f"Renders every {props.every_n_value} frame(s) across the timeline.")

        # ====================================================================
        # 3. RENDER SETTINGS OVERRIDE
        # ====================================================================
        box = layout.box()
        self.draw_header_row(box, 3, "Render Settings Override", 'SETTINGS')

        # Resolution
        row = box.row(align=True)
        row.prop(props, "use_custom_resolution", text="")
        row.label(text="Override Resolution", icon='FULLSCREEN_ENTER')
        if not props.use_custom_resolution:
            row.label(text=f"{rd.resolution_x} x {rd.resolution_y} ({rd.resolution_percentage}%)")
        if props.use_custom_resolution:
            sub = box.column(align=True)
            r = sub.row(align=True)
            r.prop(props, "custom_resolution_x", text="X")
            r.prop(props, "custom_resolution_y", text="Y")
            sub.prop(props, "resolution_percentage", text="%", slider=True)

        box.separator()

        # Samples
        row = box.row(align=True)
        row.prop(props, "use_custom_samples", text="")
        row.label(text="Override Samples", icon='PARTICLE_DATA')
        if not props.use_custom_samples:
            row.label(text=f"{get_scene_samples(scene, rd)} Samples")
        if props.use_custom_samples:
            box.prop(props, "custom_samples", text="Samples")

        box.separator()

        # Output Format
        row = box.row(align=True)
        row.prop(props, "use_custom_format", text="")
        row.label(text="Override Output Format", icon='IMAGE_DATA')
        if not props.use_custom_format:
            row.label(text=f"{rd.image_settings.file_format}")
        if props.use_custom_format:
            box.prop(props, "custom_format", text="")

        # ====================================================================
        # 4. ACTIVE SETTINGS (FROM SCENE)
        # ====================================================================
        box = layout.box()
        self.draw_header_row(box, 4, "Active Settings (From Scene)", 'SCENE_DATA')

        if props.use_custom_resolution:
            disp_res = f"{props.custom_resolution_x} x {props.custom_resolution_y} ({props.resolution_percentage}%)"
        else:
            disp_res = f"{rd.resolution_x} x {rd.resolution_y} ({rd.resolution_percentage}%)"

        if props.use_custom_samples:
            disp_samples = str(props.custom_samples)
        else:
            disp_samples = str(get_scene_samples(scene, rd))

        disp_format = props.custom_format if props.use_custom_format else rd.image_settings.file_format

        if rd.filepath:
            out_path = bpy.path.abspath(rd.filepath)
            if len(out_path) > 28:
                out_path = "..." + out_path[-25:]
        else:
            out_path = "Not Set"

        grid = box.grid_flow(row_major=True, columns=2, even_columns=True, even_rows=False, align=True)
        grid.label(text=f"Resolution: {disp_res}", icon='IMAGE_DATA')
        grid.label(text=f"Samples: {disp_samples}", icon='PARTICLE_DATA')
        grid.label(text=f"Format: {disp_format}", icon='FILE_IMAGE')
        grid.label(text=f"Output: {out_path}", icon='FILE_FOLDER')

        box.label(text=f"Engine: {rd.engine}", icon='RENDER_RESULT')

        layout.separator()

        # ====================================================================
        # ACTION BUTTONS
        # ====================================================================
        if props.is_rendering:
            box = layout.box()
            box.label(text="Rendering in Progress...", icon='RENDER_STILL')

            progress_text = f"Frame {props.current_frame_index} of {props.total_frames}"
            box.label(text=progress_text)

            hint = box.row()
            hint.scale_y = 0.7
            hint.label(text="Press Esc to cancel", icon='EVENT_ESC')

            if props.total_frames > 0:
                progress = props.current_frame_index / props.total_frames
                row = box.row()
                row.scale_y = 1.5
                row.progress(factor=progress, text=f"{int(progress * 100)}%")

            layout.separator()

            row = layout.row()
            row.scale_y = 1.5
            row.operator("render.cancel_batch", text="Stop Batch Render", icon='CANCEL')
        else:
            # Batch Render button
            row = layout.row()
            row.scale_y = 1.8
            btn_text = f"Batch Render {len(valid_frames)} Frame{'s' if len(valid_frames) != 1 else ''}"
            row.operator("render.batch_frames", text=btn_text, icon='RENDER_ANIMATION')

            if not valid_frames or not rd.filepath:
                row.enabled = False

            sub = layout.row()
            sub.scale_y = 0.6
            sub.alignment = 'CENTER'
            sub.label(text="Render only the selected frames")

            layout.separator()

            # ================================================================
            # 5. QUICK RENDER
            # ================================================================
            box = layout.box()
            self.draw_header_row(box, 5, "Quick Render", 'RENDER_STILL')

            r = box.row()
            r.scale_y = 1.5
            r.operator("render.render", text="Render Current Frame", icon='RENDER_STILL').write_still = True

            sub2 = box.row()
            sub2.scale_y = 0.6
            sub2.alignment = 'CENTER'
            sub2.label(text="Render the current frame only")

            if not rd.filepath:
                layout.separator()
                warn = layout.box()
                warn.alert = True
                warn.label(text="No Output Path Set!", icon='ERROR')
                warn.label(text="Set it in: Render Properties > Output")

        # ====================================================================
        # STATUS FOOTER
        # ====================================================================
        layout.separator()
        footer = layout.row(align=True)
        footer.scale_y = 0.8
        if props.is_rendering:
            footer.label(text="Rendering", icon='RENDER_STILL')
        else:
            footer.label(text="Ready", icon='CHECKMARK')
        footer.label(text=f"{len(valid_frames)} Frames", icon='RENDER_ANIMATION')
        footer.label(text=f"{rd.engine}", icon='SCENE_DATA')


# ============================================================================
# Registration
# ============================================================================

classes = (
    BatchRenderProperties,
    RENDER_OT_batch_frames,
    RENDER_OT_cancel_batch,
    RENDER_OT_batch_copy_frames,
    RENDER_OT_batch_refresh_frames,
    VIEW3D_PT_batch_render,
)


def register():
    """Register addon classes and properties"""
    for cls in classes:
        bpy.utils.register_class(cls)

    bpy.types.Scene.batch_render_props = bpy.props.PointerProperty(type=BatchRenderProperties)


def unregister():
    """Unregister addon classes and properties"""
    del bpy.types.Scene.batch_render_props

    for cls in reversed(classes):
        bpy.utils.unregister_class(cls)


if __name__ == "__main__":
    register()
