Engineering domain: Operating System Design

## Task Description
In this task, you will be provided with a filesystem image. You are required to develop a separate program that implements a different filesystem operation (e.g., create, read, update, delete)—to modify the corresponding sections of that image. Your objective is to ensure that the program correctly performs its assigned operation on the intended part of the filesystem image while preserving its overall integrity.

Here is the class definition for your reference:

from dataclasses import dataclass, field
from typing import List, Dict, Optional

@dataclass
class SuperBlock:
    """File system superblock, stores metadata."""
    block_size: int             # Size of each block (bytes)
    total_blocks: int           # Total number of blocks
    inode_count: int            # Total number of inodes
    free_block_bitmap: List[bool]  # Free block bitmap, True = free

@dataclass
class Inode:
    """Inode structure, stores file/directory metadata."""
    ino: int                    # Inode number
    is_dir: bool                # Whether this inode is a directory
    size: int                   # File size (bytes)
    direct_blocks: List[int]    # List of direct block indices
    # Optional: add indirect block pointers, multi-level indexing, permissions, timestamps, etc.

@dataclass
class DirEntry:
    """Directory entry: maps a filename to an inode."""
    name: str
    inode: int

@dataclass
class FileSystemImage:
    """Complete file system image structure."""
    superblock: SuperBlock
    inodes: Dict[int, Inode] = field(default_factory=dict)
    # Directory tree: inode -> list of entries in that directory; valid only for is_dir=True inodes
    directories: Dict[int, List[DirEntry]] = field(default_factory=dict)
    # Data block storage area: index corresponds to block number, content is bytes
    data_blocks: List[Optional[bytes]] = field(default_factory=list)

    def __post_init__(self):
        # Initialize data block storage area
        if not self.data_blocks:
            self.data_blocks = [None] * self.superblock.total_blocks

    def allocate_block(self) -> int:
        """Allocate a block from the free bitmap and return its index; raise if none available."""
        for idx, free in enumerate(self.superblock.free_block_bitmap):
            if free:
                self.superblock.free_block_bitmap[idx] = False
                self.data_blocks[idx] = b''  # Initialize with empty content
                return idx
        raise RuntimeError("No free blocks available")

    def free_block(self, block_idx: int):
        """Free the specified block index."""
        if 0 <= block_idx < self.superblock.total_blocks:
            self.superblock.free_block_bitmap[block_idx] = True
            self.data_blocks[block_idx] = None
        else:
            raise IndexError("Block index out of range")

    def create_inode(self, is_dir: bool) -> Inode:
        """Create a new inode and return it."""
        new_ino = len(self.inodes) + 1
        if new_ino > self.superblock.inode_count:
            raise RuntimeError("No free inodes")
        inode = Inode(
            ino=new_ino,
            is_dir=is_dir,
            size=0,
            direct_blocks=[]
        )
        self.inodes[new_ino] = inode
        if is_dir:
            self.directories[new_ino] = []
        return inode

    def add_dir_entry(self, dir_ino: int, name: str, inode: int):
        """Add a directory entry to the directory with inode dir_ino."""
        if dir_ino not in self.directories:
            raise RuntimeError("Not a directory inode")
        self.directories[dir_ino].append(DirEntry(name=name, inode=inode))


### Task
def delete(fs_img: FileSystemImage, path: str) -> FileSystemImage:
    """
    Delete the file or directory at the given path within the FileSystemImage.
    Return the updated FileSystemImage so that its inodes, directory entries,
    and data blocks can be inspected for correctness.

    Parameters:
    - fs_img: the FileSystemImage instance containing inodes, directories,
              and data blocks.
    - path:   an absolute or relative path to the file or directory to delete.

    Returns:
    - The same FileSystemImage instance (`fs_img`), mutated to reflect the
      removal of the entry and reclamation of resources.

    Behavior:
    1. Split `path` into `parent_path` and `name`:
       e.g. `/a/b/c` → `parent_path='/a/b'`, `name='c'`.
    2. Look up `parent_path`:
       - If not found, raise `FileNotFoundError`.
       - If found but not a directory, raise `NotADirectoryError`.
    3. In the parent directory’s entries, locate an entry whose `name` matches:
       - If none found, raise `FileNotFoundError`.
       - Determine its `inode` number.
    4. Inspect the target inode:
       - If it is a directory and is **not empty**, raise `OSError("Directory not empty")`.
       - If it is a directory and empty, remove its entry list from `fs_img.directories`.
       - If it is a file, proceed to free its data blocks:
         - For each block index in `inode.direct_blocks`, call `fs_img.free_block(index)`.
    5. Remove the inode from `fs_img.inodes`.
    6. Remove the directory entry from the parent’s `fs_img.directories[parent_inode.ino]`.
    7. Return the mutated `fs_img`.

    Exceptions:
    - `FileNotFoundError` if the path or parent directory does not exist.
    - `NotADirectoryError` if the parent exists but is not a directory.
    - `FileExistsError` is **not** used here; instead:
      - `OSError("Directory not empty")` if attempting to delete a non-empty directory.
    """
    # Your implementation here…