from typing import Any
import traceback
from pyrx import Ap, Db, Ed, Gs, Rx


def exceptTypes() -> list[Rx.RxClass]:
    """
    Defines a list of non-standalone sub-entity types that should be skipped
    """
    return [Db.SequenceEnd.desc(), Db.BlockBegin.desc(), Db.BlockEnd.desc(), Db.Vertex.desc()]


def eraseEntIf(ids: list[Db.ObjectId], lids: list[Db.ObjectId], zero_id: Db.ObjectId):
    """
    Iterates through a list of object IDs, filtering out excluded sub-entities
    and objects on Layer 0, and erases any entity belonging to the target layers.
    """
    tps = exceptTypes()
    for id in ids:
        # Skip sub-entities like vertices or block boundaries
        if id.objectClass() in tps:
            continue

        # Open the entity in read-only mode first to evaluate its metadata safely
        e = Db.Entity(id, Db.OpenMode.kForRead)

        # Guard rail: Explicitly skip and close if the entity resides on Layer 0
        if e.layerId() == zero_id:
            e.close()
            continue

        # If the entity belongs to one of our target layers, upgrade to write-access and erase it
        if e.layerId() in lids:
            e.upgradeOpen()
            e.erase()

        # Always close the object to release its database lock and prevent memory leaks
        e.close()


def eraseLayerIf(lids: list[Db.ObjectId], db: Db.Database, zero_id: Db.ObjectId):
    """
    Attempts to erase the specified layer records from the Layer Table,
    ensuring that the active layer and Layer 0 are completely protected.
    """
    clayer = db.clayer()  # Get the ID of the current active layer in the drawing
    for id in lids:
        # Guard rail: Prevent deletion of the active working layer
        if id == clayer:
            print(f"Skipping active Layer ID: {id}. Cannot delete the current layer.")
            continue

        # Guard rail: Prevent deletion of Layer 0 (AutoCAD system requirement)
        if id == zero_id:
            print("Skipping Layer 0. It cannot be deleted.")
            continue

        try:
            # Open the LayerTableRecord with write permissions to mark it as erased
            lay = Db.LayerTableRecord(id, Db.OpenMode.kForWrite)
            lay.erase()
            lay.close()
        except Exception as e:
            # Catch failures if a layer is locked, referenced by a block definition, or un-purgeable
            print(f"Could not purge Layer ID {id}: {e}")


def getLayerIds(names: list[str], db: Db.Database, zero_id: Db.ObjectId) -> list[Db.ObjectId]:
    """
    Queries the Layer Table and matches uppercase string names against the
    drawing database to collect corresponding Db.ObjectId targets.
    """
    ids = []
    # PyRx enables a clean tuple unpack (name, object_id) directly from db.layerTable()
    for name, id in db.layerTable():
        # Skip Layer 0 from selection list entirely
        if id == zero_id:
            continue

        # Match against our pre-normalized uppercase target name list
        if name.upper() in names:
            ids.append(id)
    return ids


@Ap.LispFunction()
def pylaydel(args: list[tuple[int, Any]]):
    """
    Exposes the 'pylaydel' command to the AutoCAD AutoLISP environment.
    Accepts a list of layer names as strings, erases all geometry on those layers,
    and purges the layer table definitions.
    """
    try:
        names = []
        db = Db.curDb()  # Get the current active drawing database context
        zero_id = db.layerZero()  # Keep a persistent handle on Layer 0's ID

        # Parse arguments passed from the AutoLISP environment
        for code, name in args:
            # Verify the LISP data type is a text string (kText)
            if code == Rx.LispType.kText:
                # Normalize string names to uppercase to enforce case-insensitive matching
                names.append(str(name).upper())

        if not names:
            print("No valid layer names provided.")
            return False

        # Retrieve all entity descriptor IDs across the global database index
        allids = db.objectIds(Db.Entity.desc())

        # Translate the input string names into actual database object IDs
        layerids = getLayerIds(names, db, zero_id)

        if not layerids:
            print("No matching deletable layers found in the current drawing.")
            return False

        # Run cleanup operations
        eraseEntIf(allids, layerids, zero_id)
        eraseLayerIf(layerids, db, zero_id)

        return True

    except Exception as err:
        # Catch and print python runtime stack traces back to the CAD command line/console
        traceback.print_exception(err)
        return False


# Example AutoLISP execution usage:
# (pylaydel '("1_1_WALLS" "1_CRP_WALLS"))
