"""
Sortiert Szenen-Objekte anhand ihrer Chunk-/Group-Nummer in Collections "block_XXX".

Erkannte Namensschemata:
    road_object_XXX_chunk_YYY
    object_XXX_group_YYY_part_ZZZ
    runtime_object_XXX_group_YYY_part_ZZZ
    sprite_object_XXX_group_YYY_part_ZZZ_item_AAA
    sprite_runtime_object_XXX_group_YYY_part_ZZZ_item_AAA
    road_chunk_XXX

Maßgeblich ist die Zahl direkt nach "object_" (XXX), egal welches Praefix
(road_, runtime_, sprite_runtime_ ...) davor steht. Fehlt der "object_"-Teil
komplett (z. B. road_chunk_XXX), zaehlt stattdessen die Zahl nach "chunk_".

Anwendung: in Blender im Scripting-Tab öffnen und "Run Script".
"""

import re

import bpy

# ---------------------------------------------------------------- Einstellungen

PARENT_NAME = "blocks"      # Sammel-Collection; None = direkt in die Szene
ONLY_SELECTED = False       # True = nur ausgewählte Objekte einsortieren
PAD = 3                     # Stellen der Nummer im Collection-Namen (block_007)
REMOVE_EMPTY = True         # leer gewordene Collections danach löschen

# ----------------------------------------------------------------------- Logik

NUM_RE = re.compile(r"object_(\d+)")
FALLBACK_RE = re.compile(r"chunk_(\d+)")


def block_id(name):
    """Objekt-Nummer XXX aus dem Objektnamen, sonst None.

    Ohne "object_"-Teil (road_chunk_XXX) greift die Chunk-Nummer.
    """
    match = NUM_RE.search(name) or FALLBACK_RE.search(name)
    return match.group(1).zfill(PAD) if match else None


def get_parent():
    scene_coll = bpy.context.scene.collection
    if not PARENT_NAME:
        return scene_coll
    parent = bpy.data.collections.get(PARENT_NAME)
    if parent is None:
        parent = bpy.data.collections.new(PARENT_NAME)
    if parent.name not in scene_coll.children:
        try:
            scene_coll.children.link(parent)
        except RuntimeError:
            pass  # haengt bereits woanders in der Szene
    return parent


def main():
    parent = get_parent()

    objects = (
        list(bpy.context.selected_objects)
        if ONLY_SELECTED
        else list(bpy.context.scene.objects)
    )

    buckets = {}
    skipped = []
    for ob in objects:
        key = block_id(ob.name)
        if key is None:
            skipped.append(ob.name)
            continue
        buckets.setdefault(key, []).append(ob)

    touched = set()
    for key in sorted(buckets):
        cname = "block_%s" % key
        coll = bpy.data.collections.get(cname)
        if coll is None:
            coll = bpy.data.collections.new(cname)
        if coll.name not in parent.children:
            try:
                parent.children.link(coll)
            except RuntimeError:
                pass

        for ob in buckets[key]:
            for old in list(ob.users_collection):
                if old is coll:
                    continue
                old.objects.unlink(ob)
                touched.add(old.name)
            if ob.name not in coll.objects:
                coll.objects.link(ob)

    removed = 0
    if REMOVE_EMPTY:
        for name in touched:
            coll = bpy.data.collections.get(name)
            if coll is None or coll is parent:
                continue
            if not coll.objects and not coll.children and not coll.name.startswith("block_"):
                bpy.data.collections.remove(coll)
                removed += 1

    print("-" * 60)
    print("%d Objekte in %d Collections einsortiert" % (
        sum(len(v) for v in buckets.values()), len(buckets)))
    if removed:
        print("%d leere Collections entfernt" % removed)
    if skipped:
        print("%d Objekte ohne Nummer uebersprungen:" % len(skipped))
        for name in skipped[:20]:
            print("   ", name)
        if len(skipped) > 20:
            print("    ... und %d weitere" % (len(skipped) - 20))
    print("-" * 60)


main()