#include "GridLayers.h"
#include "UIGrid.h"
#include "UIEntity.h"
#include "PyColor.h"
#include "PyTexture.h"
#include "PyFOV.h"
#include <sstream>

// =============================================================================
// GridLayer base class
// =============================================================================

GridLayer::GridLayer(GridLayerType type, int z_index, int grid_x, int grid_y, UIGrid* parent)
    : type(type), z_index(z_index), grid_x(grid_x), grid_y(grid_y),
      parent_grid(parent), visible(true),
      chunks_x(0), chunks_y(0),
      cached_cell_width(0), cached_cell_height(0)
{
    initChunks();
}

void GridLayer::initChunks() {
    // Calculate chunk dimensions
    chunks_x = (grid_x + CHUNK_SIZE - 1) / CHUNK_SIZE;
    chunks_y = (grid_y + CHUNK_SIZE - 1) / CHUNK_SIZE;
    int total_chunks = chunks_x * chunks_y;

    // Initialize per-chunk tracking
    chunk_dirty.assign(total_chunks, true);  // All chunks start dirty
    chunk_texture_initialized.assign(total_chunks, false);
    chunk_textures.clear();
    chunk_textures.reserve(total_chunks);
    for (int i = 0; i < total_chunks; ++i) {
        chunk_textures.push_back(std::make_unique<sf::RenderTexture>());
    }
}

void GridLayer::markDirty() {
    // Mark ALL chunks as dirty
    std::fill(chunk_dirty.begin(), chunk_dirty.end(), true);
}

void GridLayer::markDirty(int cell_x, int cell_y) {
    // Mark only the specific chunk containing this cell
    if (cell_x < 0 || cell_x >= grid_x || cell_y < 0 || cell_y >= grid_y) return;

    int chunk_idx = getChunkIndex(cell_x, cell_y);
    if (chunk_idx >= 0 && chunk_idx < static_cast<int>(chunk_dirty.size())) {
        chunk_dirty[chunk_idx] = true;
    }
}

int GridLayer::getChunkIndex(int cell_x, int cell_y) const {
    int cx = cell_x / CHUNK_SIZE;
    int cy = cell_y / CHUNK_SIZE;
    return cy * chunks_x + cx;
}

void GridLayer::getChunkCoords(int cell_x, int cell_y, int& chunk_x, int& chunk_y) const {
    chunk_x = cell_x / CHUNK_SIZE;
    chunk_y = cell_y / CHUNK_SIZE;
}

void GridLayer::getChunkBounds(int chunk_x, int chunk_y, int& start_x, int& start_y, int& end_x, int& end_y) const {
    start_x = chunk_x * CHUNK_SIZE;
    start_y = chunk_y * CHUNK_SIZE;
    end_x = std::min(start_x + CHUNK_SIZE, grid_x);
    end_y = std::min(start_y + CHUNK_SIZE, grid_y);
}

void GridLayer::ensureChunkTexture(int chunk_idx, int cell_width, int cell_height) {
    if (chunk_idx < 0 || chunk_idx >= static_cast<int>(chunk_textures.size())) return;
    if (!chunk_textures[chunk_idx]) return;

    // Calculate chunk dimensions in cells
    int cx = chunk_idx % chunks_x;
    int cy = chunk_idx / chunks_x;
    int start_x, start_y, end_x, end_y;
    getChunkBounds(cx, cy, start_x, start_y, end_x, end_y);

    int chunk_width_cells = end_x - start_x;
    int chunk_height_cells = end_y - start_y;

    unsigned int required_width = chunk_width_cells * cell_width;
    unsigned int required_height = chunk_height_cells * cell_height;

    // Check if texture needs (re)creation
    if (chunk_texture_initialized[chunk_idx] &&
        chunk_textures[chunk_idx]->getSize().x == required_width &&
        chunk_textures[chunk_idx]->getSize().y == required_height &&
        cached_cell_width == cell_width &&
        cached_cell_height == cell_height) {
        return;  // Already properly sized
    }

    // Create the texture for this chunk
    if (!chunk_textures[chunk_idx]->create(required_width, required_height)) {
        chunk_texture_initialized[chunk_idx] = false;
        return;
    }

    chunk_texture_initialized[chunk_idx] = true;
    chunk_dirty[chunk_idx] = true;  // Force re-render after resize
    cached_cell_width = cell_width;
    cached_cell_height = cell_height;
}

// =============================================================================
// ColorLayer implementation
// =============================================================================

ColorLayer::ColorLayer(int z_index, int grid_x, int grid_y, UIGrid* parent)
    : GridLayer(GridLayerType::Color, z_index, grid_x, grid_y, parent),
      colors(grid_x * grid_y, sf::Color::Transparent),
      perspective_visible(255, 255, 200, 64),
      perspective_discovered(100, 100, 100, 128),
      perspective_unknown(0, 0, 0, 255),
      has_perspective(false)
{}

sf::Color& ColorLayer::at(int x, int y) {
    return colors[y * grid_x + x];
}

const sf::Color& ColorLayer::at(int x, int y) const {
    return colors[y * grid_x + x];
}

void ColorLayer::fill(const sf::Color& color) {
    std::fill(colors.begin(), colors.end(), color);
    markDirty();  // Mark ALL chunks for re-render
}

void ColorLayer::fillRect(int x, int y, int width, int height, const sf::Color& color) {
    // Clamp to valid bounds
    int x1 = std::max(0, x);
    int y1 = std::max(0, y);
    int x2 = std::min(grid_x, x + width);
    int y2 = std::min(grid_y, y + height);

    // Fill the rectangle
    for (int fy = y1; fy < y2; ++fy) {
        for (int fx = x1; fx < x2; ++fx) {
            colors[fy * grid_x + fx] = color;
        }
    }

    // Mark affected chunks dirty
    int chunk_x1 = x1 / CHUNK_SIZE;
    int chunk_y1 = y1 / CHUNK_SIZE;
    int chunk_x2 = (x2 - 1) / CHUNK_SIZE;
    int chunk_y2 = (y2 - 1) / CHUNK_SIZE;

    for (int cy = chunk_y1; cy <= chunk_y2; ++cy) {
        for (int cx = chunk_x1; cx <= chunk_x2; ++cx) {
            int idx = cy * chunks_x + cx;
            if (idx >= 0 && idx < static_cast<int>(chunk_dirty.size())) {
                chunk_dirty[idx] = true;
            }
        }
    }
}

void ColorLayer::drawFOV(int source_x, int source_y, int radius,
                         TCOD_fov_algorithm_t algorithm,
                         const sf::Color& visible_color,
                         const sf::Color& discovered_color,
                         const sf::Color& unknown_color) {
    // Need parent grid for TCOD map access
    if (!parent_grid) {
        return;  // Cannot compute FOV without parent grid
    }

    // Import UIGrid here to avoid circular dependency in header
    // parent_grid is already a UIGrid*, we can use its tcod_map directly
    // But we need to forward declare access to it...

    // Compute FOV on the parent grid
    parent_grid->computeFOV(source_x, source_y, radius, true, algorithm);

    // Paint cells based on visibility
    for (int cy = 0; cy < grid_y; ++cy) {
        for (int cx = 0; cx < grid_x; ++cx) {
            // Check if in FOV (visible right now)
            if (parent_grid->isInFOV(cx, cy)) {
                colors[cy * grid_x + cx] = visible_color;
            }
            // Check if previously discovered (current color != unknown)
            else if (colors[cy * grid_x + cx] != unknown_color) {
                colors[cy * grid_x + cx] = discovered_color;
            }
            // Otherwise leave as unknown (or set to unknown if first time)
            else {
                colors[cy * grid_x + cx] = unknown_color;
            }
        }
    }

    // Mark entire layer dirty
    markDirty();
}

void ColorLayer::applyPerspective(std::shared_ptr<UIEntity> entity,
                                   const sf::Color& visible,
                                   const sf::Color& discovered,
                                   const sf::Color& unknown) {
    perspective_entity = entity;
    perspective_visible = visible;
    perspective_discovered = discovered;
    perspective_unknown = unknown;
    has_perspective = true;

    // Initial draw based on entity's current position
    updatePerspective();
}

void ColorLayer::updatePerspective() {
    if (!has_perspective) return;

    auto entity = perspective_entity.lock();
    if (!entity) {
        // Entity was deleted, clear perspective
        has_perspective = false;
        return;
    }

    if (!parent_grid) return;

    // Get entity position and grid's FOV settings
    int source_x = static_cast<int>(entity->position.x);
    int source_y = static_cast<int>(entity->position.y);
    int radius = parent_grid->fov_radius;
    TCOD_fov_algorithm_t algorithm = parent_grid->fov_algorithm;

    // Use drawFOV with our stored colors
    drawFOV(source_x, source_y, radius, algorithm,
            perspective_visible, perspective_discovered, perspective_unknown);
}

void ColorLayer::clearPerspective() {
    perspective_entity.reset();
    has_perspective = false;
}

void ColorLayer::resize(int new_grid_x, int new_grid_y) {
    std::vector<sf::Color> new_colors(new_grid_x * new_grid_y, sf::Color::Transparent);

    // Copy existing data
    int copy_x = std::min(grid_x, new_grid_x);
    int copy_y = std::min(grid_y, new_grid_y);
    for (int y = 0; y < copy_y; ++y) {
        for (int x = 0; x < copy_x; ++x) {
            new_colors[y * new_grid_x + x] = colors[y * grid_x + x];
        }
    }

    colors = std::move(new_colors);
    grid_x = new_grid_x;
    grid_y = new_grid_y;

    // Reinitialize chunks for new dimensions
    initChunks();
}

// Render a single chunk to its cached texture
void ColorLayer::renderChunkToTexture(int chunk_x, int chunk_y, int cell_width, int cell_height) {
    int chunk_idx = chunk_y * chunks_x + chunk_x;
    if (chunk_idx < 0 || chunk_idx >= static_cast<int>(chunk_textures.size())) return;
    if (!chunk_textures[chunk_idx]) return;

    ensureChunkTexture(chunk_idx, cell_width, cell_height);
    if (!chunk_texture_initialized[chunk_idx]) return;

    // Get chunk bounds
    int start_x, start_y, end_x, end_y;
    getChunkBounds(chunk_x, chunk_y, start_x, start_y, end_x, end_y);

    chunk_textures[chunk_idx]->clear(sf::Color::Transparent);

    sf::RectangleShape rect;
    rect.setSize(sf::Vector2f(cell_width, cell_height));
    rect.setOutlineThickness(0);

    // Render only cells within this chunk (local coordinates in texture)
    for (int x = start_x; x < end_x; ++x) {
        for (int y = start_y; y < end_y; ++y) {
            const sf::Color& color = at(x, y);
            if (color.a == 0) continue;  // Skip fully transparent

            // Position relative to chunk origin
            rect.setPosition(sf::Vector2f((x - start_x) * cell_width, (y - start_y) * cell_height));
            rect.setFillColor(color);
            chunk_textures[chunk_idx]->draw(rect);
        }
    }

    chunk_textures[chunk_idx]->display();
    chunk_dirty[chunk_idx] = false;
}

// Legacy: render all chunks (used by fill, resize, etc.)
void ColorLayer::renderToTexture(int cell_width, int cell_height) {
    for (int cy = 0; cy < chunks_y; ++cy) {
        for (int cx = 0; cx < chunks_x; ++cx) {
            renderChunkToTexture(cx, cy, cell_width, cell_height);
        }
    }
}

void ColorLayer::render(sf::RenderTarget& target,
                       float left_spritepixels, float top_spritepixels,
                       int left_edge, int top_edge, int x_limit, int y_limit,
                       float zoom, int cell_width, int cell_height) {
    if (!visible) return;

    // Calculate visible chunk range
    int chunk_left = std::max(0, left_edge / CHUNK_SIZE);
    int chunk_top = std::max(0, top_edge / CHUNK_SIZE);
    int chunk_right = std::min(chunks_x - 1, (x_limit + CHUNK_SIZE - 1) / CHUNK_SIZE);
    int chunk_bottom = std::min(chunks_y - 1, (y_limit + CHUNK_SIZE - 1) / CHUNK_SIZE);

    // Iterate only over visible chunks
    for (int cy = chunk_top; cy <= chunk_bottom; ++cy) {
        for (int cx = chunk_left; cx <= chunk_right; ++cx) {
            int chunk_idx = cy * chunks_x + cx;

            // Re-render chunk only if dirty AND visible
            if (chunk_dirty[chunk_idx] || !chunk_texture_initialized[chunk_idx]) {
                renderChunkToTexture(cx, cy, cell_width, cell_height);
            }

            if (!chunk_texture_initialized[chunk_idx]) {
                // Fallback: direct rendering for this chunk
                int start_x, start_y, end_x, end_y;
                getChunkBounds(cx, cy, start_x, start_y, end_x, end_y);

                sf::RectangleShape rect;
                rect.setSize(sf::Vector2f(cell_width * zoom, cell_height * zoom));
                rect.setOutlineThickness(0);

                for (int x = start_x; x < end_x; ++x) {
                    for (int y = start_y; y < end_y; ++y) {
                        const sf::Color& color = at(x, y);
                        if (color.a == 0) continue;

                        auto pixel_pos = sf::Vector2f(
                            (x * cell_width - left_spritepixels) * zoom,
                            (y * cell_height - top_spritepixels) * zoom
                        );
                        rect.setPosition(pixel_pos);
                        rect.setFillColor(color);
                        target.draw(rect);
                    }
                }
                continue;
            }

            // Blit this chunk's texture to target
            int start_x, start_y, end_x, end_y;
            getChunkBounds(cx, cy, start_x, start_y, end_x, end_y);

            // Chunk position in world pixel coordinates
            float chunk_world_x = start_x * cell_width;
            float chunk_world_y = start_y * cell_height;

            // Position in target (accounting for viewport offset and zoom)
            float dest_x = (chunk_world_x - left_spritepixels) * zoom;
            float dest_y = (chunk_world_y - top_spritepixels) * zoom;

            sf::Sprite chunk_sprite(chunk_textures[chunk_idx]->getTexture());
            chunk_sprite.setPosition(sf::Vector2f(dest_x, dest_y));
            chunk_sprite.setScale(sf::Vector2f(zoom, zoom));

            target.draw(chunk_sprite);
        }
    }
}

// =============================================================================
// TileLayer implementation
// =============================================================================

TileLayer::TileLayer(int z_index, int grid_x, int grid_y, UIGrid* parent,
                     std::shared_ptr<PyTexture> texture)
    : GridLayer(GridLayerType::Tile, z_index, grid_x, grid_y, parent),
      tiles(grid_x * grid_y, -1),  // -1 = no tile
      texture(texture)
{}

int& TileLayer::at(int x, int y) {
    return tiles[y * grid_x + x];
}

int TileLayer::at(int x, int y) const {
    return tiles[y * grid_x + x];
}

void TileLayer::fill(int tile_index) {
    std::fill(tiles.begin(), tiles.end(), tile_index);
    markDirty();  // Mark ALL chunks for re-render
}

void TileLayer::fillRect(int x, int y, int width, int height, int tile_index) {
    // Clamp to valid bounds
    int x1 = std::max(0, x);
    int y1 = std::max(0, y);
    int x2 = std::min(grid_x, x + width);
    int y2 = std::min(grid_y, y + height);

    // Fill the rectangle
    for (int fy = y1; fy < y2; ++fy) {
        for (int fx = x1; fx < x2; ++fx) {
            tiles[fy * grid_x + fx] = tile_index;
        }
    }

    // Mark affected chunks dirty
    int chunk_x1 = x1 / CHUNK_SIZE;
    int chunk_y1 = y1 / CHUNK_SIZE;
    int chunk_x2 = (x2 - 1) / CHUNK_SIZE;
    int chunk_y2 = (y2 - 1) / CHUNK_SIZE;

    for (int cy = chunk_y1; cy <= chunk_y2; ++cy) {
        for (int cx = chunk_x1; cx <= chunk_x2; ++cx) {
            int idx = cy * chunks_x + cx;
            if (idx >= 0 && idx < static_cast<int>(chunk_dirty.size())) {
                chunk_dirty[idx] = true;
            }
        }
    }
}

void TileLayer::resize(int new_grid_x, int new_grid_y) {
    std::vector<int> new_tiles(new_grid_x * new_grid_y, -1);

    // Copy existing data
    int copy_x = std::min(grid_x, new_grid_x);
    int copy_y = std::min(grid_y, new_grid_y);
    for (int y = 0; y < copy_y; ++y) {
        for (int x = 0; x < copy_x; ++x) {
            new_tiles[y * new_grid_x + x] = tiles[y * grid_x + x];
        }
    }

    tiles = std::move(new_tiles);
    grid_x = new_grid_x;
    grid_y = new_grid_y;

    // Reinitialize chunks for new dimensions
    initChunks();
}

// Render a single chunk to its cached texture
void TileLayer::renderChunkToTexture(int chunk_x, int chunk_y, int cell_width, int cell_height) {
    if (!texture) return;

    int chunk_idx = chunk_y * chunks_x + chunk_x;
    if (chunk_idx < 0 || chunk_idx >= static_cast<int>(chunk_textures.size())) return;
    if (!chunk_textures[chunk_idx]) return;

    ensureChunkTexture(chunk_idx, cell_width, cell_height);
    if (!chunk_texture_initialized[chunk_idx]) return;

    // Get chunk bounds
    int start_x, start_y, end_x, end_y;
    getChunkBounds(chunk_x, chunk_y, start_x, start_y, end_x, end_y);

    chunk_textures[chunk_idx]->clear(sf::Color::Transparent);

    // Render only tiles within this chunk (local coordinates in texture)
    for (int x = start_x; x < end_x; ++x) {
        for (int y = start_y; y < end_y; ++y) {
            int tile_index = at(x, y);
            if (tile_index < 0) continue;  // No tile

            // Position relative to chunk origin
            auto pixel_pos = sf::Vector2f((x - start_x) * cell_width, (y - start_y) * cell_height);
            sf::Sprite sprite = texture->sprite(tile_index, pixel_pos, sf::Vector2f(1.0f, 1.0f));
            chunk_textures[chunk_idx]->draw(sprite);
        }
    }

    chunk_textures[chunk_idx]->display();
    chunk_dirty[chunk_idx] = false;
}

// Legacy: render all chunks (used by fill, resize, etc.)
void TileLayer::renderToTexture(int cell_width, int cell_height) {
    for (int cy = 0; cy < chunks_y; ++cy) {
        for (int cx = 0; cx < chunks_x; ++cx) {
            renderChunkToTexture(cx, cy, cell_width, cell_height);
        }
    }
}

void TileLayer::render(sf::RenderTarget& target,
                      float left_spritepixels, float top_spritepixels,
                      int left_edge, int top_edge, int x_limit, int y_limit,
                      float zoom, int cell_width, int cell_height) {
    if (!visible || !texture) return;

    // Calculate visible chunk range
    int chunk_left = std::max(0, left_edge / CHUNK_SIZE);
    int chunk_top = std::max(0, top_edge / CHUNK_SIZE);
    int chunk_right = std::min(chunks_x - 1, (x_limit + CHUNK_SIZE - 1) / CHUNK_SIZE);
    int chunk_bottom = std::min(chunks_y - 1, (y_limit + CHUNK_SIZE - 1) / CHUNK_SIZE);

    // Iterate only over visible chunks
    for (int cy = chunk_top; cy <= chunk_bottom; ++cy) {
        for (int cx = chunk_left; cx <= chunk_right; ++cx) {
            int chunk_idx = cy * chunks_x + cx;

            // Re-render chunk only if dirty AND visible
            if (chunk_dirty[chunk_idx] || !chunk_texture_initialized[chunk_idx]) {
                renderChunkToTexture(cx, cy, cell_width, cell_height);
            }

            if (!chunk_texture_initialized[chunk_idx]) {
                // Fallback: direct rendering for this chunk
                int start_x, start_y, end_x, end_y;
                getChunkBounds(cx, cy, start_x, start_y, end_x, end_y);

                for (int x = start_x; x < end_x; ++x) {
                    for (int y = start_y; y < end_y; ++y) {
                        int tile_index = at(x, y);
                        if (tile_index < 0) continue;

                        auto pixel_pos = sf::Vector2f(
                            (x * cell_width - left_spritepixels) * zoom,
                            (y * cell_height - top_spritepixels) * zoom
                        );
                        sf::Sprite sprite = texture->sprite(tile_index, pixel_pos, sf::Vector2f(zoom, zoom));
                        target.draw(sprite);
                    }
                }
                continue;
            }

            // Blit this chunk's texture to target
            int start_x, start_y, end_x, end_y;
            getChunkBounds(cx, cy, start_x, start_y, end_x, end_y);

            // Chunk position in world pixel coordinates
            float chunk_world_x = start_x * cell_width;
            float chunk_world_y = start_y * cell_height;

            // Position in target (accounting for viewport offset and zoom)
            float dest_x = (chunk_world_x - left_spritepixels) * zoom;
            float dest_y = (chunk_world_y - top_spritepixels) * zoom;

            sf::Sprite chunk_sprite(chunk_textures[chunk_idx]->getTexture());
            chunk_sprite.setPosition(sf::Vector2f(dest_x, dest_y));
            chunk_sprite.setScale(sf::Vector2f(zoom, zoom));

            target.draw(chunk_sprite);
        }
    }
}

// =============================================================================
// Python API - ColorLayer
// =============================================================================

PyMethodDef PyGridLayerAPI::ColorLayer_methods[] = {
    {"at", (PyCFunction)PyGridLayerAPI::ColorLayer_at, METH_VARARGS,
     "at(x, y) -> Color\n\nGet the color at cell position (x, y)."},
    {"set", (PyCFunction)PyGridLayerAPI::ColorLayer_set, METH_VARARGS,
     "set(x, y, color)\n\nSet the color at cell position (x, y)."},
    {"fill", (PyCFunction)PyGridLayerAPI::ColorLayer_fill, METH_VARARGS,
     "fill(color)\n\nFill the entire layer with the specified color."},
    {"fill_rect", (PyCFunction)PyGridLayerAPI::ColorLayer_fill_rect, METH_VARARGS | METH_KEYWORDS,
     "fill_rect(pos, size, color)\n\n"
     "Fill a rectangular region with a color.\n\n"
     "Args:\n"
     "    pos (tuple): Top-left corner as (x, y)\n"
     "    size (tuple): Dimensions as (width, height)\n"
     "    color: Color object or (r, g, b[, a]) tuple"},
    {"draw_fov", (PyCFunction)PyGridLayerAPI::ColorLayer_draw_fov, METH_VARARGS | METH_KEYWORDS,
     "draw_fov(source, radius=None, fov=None, visible=None, discovered=None, unknown=None)\n\n"
     "Paint cells based on field-of-view visibility from source position.\n\n"
     "Args:\n"
     "    source (tuple): FOV origin as (x, y)\n"
     "    radius (int): FOV radius. Default: grid's fov_radius\n"
     "    fov (FOV): FOV algorithm. Default: grid's fov setting\n"
     "    visible (Color): Color for currently visible cells\n"
     "    discovered (Color): Color for previously seen cells\n"
     "    unknown (Color): Color for never-seen cells\n\n"
     "Note: Layer must be attached to a grid for FOV calculation."},
    {"apply_perspective", (PyCFunction)PyGridLayerAPI::ColorLayer_apply_perspective, METH_VARARGS | METH_KEYWORDS,
     "apply_perspective(entity, visible=None, discovered=None, unknown=None)\n\n"
     "Bind this layer to an entity for automatic FOV updates.\n\n"
     "Args:\n"
     "    entity (Entity): The entity whose perspective to track\n"
     "    visible (Color): Color for currently visible cells\n"
     "    discovered (Color): Color for previously seen cells\n"
     "    unknown (Color): Color for never-seen cells\n\n"
     "After binding, call update_perspective() when the entity moves."},
    {"update_perspective", (PyCFunction)PyGridLayerAPI::ColorLayer_update_perspective, METH_NOARGS,
     "update_perspective()\n\n"
     "Redraw FOV based on the bound entity's current position.\n\n"
     "Call this after the entity moves to update the visibility layer."},
    {"clear_perspective", (PyCFunction)PyGridLayerAPI::ColorLayer_clear_perspective, METH_NOARGS,
     "clear_perspective()\n\n"
     "Remove the perspective binding from this layer."},
    {NULL}
};

PyGetSetDef PyGridLayerAPI::ColorLayer_getsetters[] = {
    {"z_index", (getter)PyGridLayerAPI::ColorLayer_get_z_index,
                (setter)PyGridLayerAPI::ColorLayer_set_z_index,
     "Layer z-order. Negative values render below entities.", NULL},
    {"visible", (getter)PyGridLayerAPI::ColorLayer_get_visible,
                (setter)PyGridLayerAPI::ColorLayer_set_visible,
     "Whether the layer is rendered.", NULL},
    {"grid_size", (getter)PyGridLayerAPI::ColorLayer_get_grid_size, NULL,
     "Layer dimensions as (width, height) tuple.", NULL},
    {NULL}
};

int PyGridLayerAPI::ColorLayer_init(PyColorLayerObject* self, PyObject* args, PyObject* kwds) {
    static const char* kwlist[] = {"z_index", "grid_size", NULL};
    int z_index = -1;
    PyObject* grid_size_obj = nullptr;
    int grid_x = 0, grid_y = 0;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iO", const_cast<char**>(kwlist),
                                     &z_index, &grid_size_obj)) {
        return -1;
    }

    // Parse grid_size if provided
    if (grid_size_obj && grid_size_obj != Py_None) {
        if (!PyTuple_Check(grid_size_obj) || PyTuple_Size(grid_size_obj) != 2) {
            PyErr_SetString(PyExc_TypeError, "grid_size must be a (width, height) tuple");
            return -1;
        }
        grid_x = PyLong_AsLong(PyTuple_GetItem(grid_size_obj, 0));
        grid_y = PyLong_AsLong(PyTuple_GetItem(grid_size_obj, 1));
        if (PyErr_Occurred()) return -1;
    }

    // Create the layer (will be attached to grid via add_layer)
    self->data = std::make_shared<ColorLayer>(z_index, grid_x, grid_y, nullptr);
    self->grid.reset();

    return 0;
}

PyObject* PyGridLayerAPI::ColorLayer_at(PyColorLayerObject* self, PyObject* args) {
    int x, y;
    if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
        return NULL;
    }

    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return NULL;
    }

    if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
        PyErr_SetString(PyExc_IndexError, "Cell coordinates out of bounds");
        return NULL;
    }

    const sf::Color& color = self->data->at(x, y);

    // Return as mcrfpy.Color
    auto* color_type = (PyTypeObject*)PyObject_GetAttrString(
        PyImport_ImportModule("mcrfpy"), "Color");
    if (!color_type) return NULL;

    PyColorObject* color_obj = (PyColorObject*)color_type->tp_alloc(color_type, 0);
    Py_DECREF(color_type);
    if (!color_obj) return NULL;

    color_obj->data = color;
    return (PyObject*)color_obj;
}

PyObject* PyGridLayerAPI::ColorLayer_set(PyColorLayerObject* self, PyObject* args) {
    int x, y;
    PyObject* color_obj;
    if (!PyArg_ParseTuple(args, "iiO", &x, &y, &color_obj)) {
        return NULL;
    }

    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return NULL;
    }

    if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
        PyErr_SetString(PyExc_IndexError, "Cell coordinates out of bounds");
        return NULL;
    }

    // Parse color
    sf::Color color;
    auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
    if (!mcrfpy_module) return NULL;

    auto* color_type = PyObject_GetAttrString(mcrfpy_module, "Color");
    Py_DECREF(mcrfpy_module);
    if (!color_type) return NULL;

    if (PyObject_IsInstance(color_obj, color_type)) {
        color = ((PyColorObject*)color_obj)->data;
    } else if (PyTuple_Check(color_obj)) {
        int r, g, b, a = 255;
        if (!PyArg_ParseTuple(color_obj, "iii|i", &r, &g, &b, &a)) {
            Py_DECREF(color_type);
            return NULL;
        }
        color = sf::Color(r, g, b, a);
    } else {
        Py_DECREF(color_type);
        PyErr_SetString(PyExc_TypeError, "color must be a Color object or (r, g, b[, a]) tuple");
        return NULL;
    }
    Py_DECREF(color_type);

    self->data->at(x, y) = color;
    self->data->markDirty(x, y);  // Mark only the affected chunk
    Py_RETURN_NONE;
}

PyObject* PyGridLayerAPI::ColorLayer_fill(PyColorLayerObject* self, PyObject* args) {
    PyObject* color_obj;
    if (!PyArg_ParseTuple(args, "O", &color_obj)) {
        return NULL;
    }

    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return NULL;
    }

    // Parse color
    sf::Color color;
    auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
    if (!mcrfpy_module) return NULL;

    auto* color_type = PyObject_GetAttrString(mcrfpy_module, "Color");
    Py_DECREF(mcrfpy_module);
    if (!color_type) return NULL;

    if (PyObject_IsInstance(color_obj, color_type)) {
        color = ((PyColorObject*)color_obj)->data;
    } else if (PyTuple_Check(color_obj)) {
        int r, g, b, a = 255;
        if (!PyArg_ParseTuple(color_obj, "iii|i", &r, &g, &b, &a)) {
            Py_DECREF(color_type);
            return NULL;
        }
        color = sf::Color(r, g, b, a);
    } else {
        Py_DECREF(color_type);
        PyErr_SetString(PyExc_TypeError, "color must be a Color object or (r, g, b[, a]) tuple");
        return NULL;
    }
    Py_DECREF(color_type);

    self->data->fill(color);
    Py_RETURN_NONE;
}

PyObject* PyGridLayerAPI::ColorLayer_fill_rect(PyColorLayerObject* self, PyObject* args, PyObject* kwds) {
    static const char* kwlist[] = {"pos", "size", "color", NULL};
    PyObject* pos_obj;
    PyObject* size_obj;
    PyObject* color_obj;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOO", const_cast<char**>(kwlist),
                                     &pos_obj, &size_obj, &color_obj)) {
        return NULL;
    }

    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return NULL;
    }

    // Parse pos
    int x, y;
    if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
        x = PyLong_AsLong(PyTuple_GetItem(pos_obj, 0));
        y = PyLong_AsLong(PyTuple_GetItem(pos_obj, 1));
        if (PyErr_Occurred()) return NULL;
    } else {
        PyErr_SetString(PyExc_TypeError, "pos must be a (x, y) tuple");
        return NULL;
    }

    // Parse size
    int width, height;
    if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
        width = PyLong_AsLong(PyTuple_GetItem(size_obj, 0));
        height = PyLong_AsLong(PyTuple_GetItem(size_obj, 1));
        if (PyErr_Occurred()) return NULL;
    } else {
        PyErr_SetString(PyExc_TypeError, "size must be a (width, height) tuple");
        return NULL;
    }

    // Parse color
    sf::Color color;
    auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
    if (!mcrfpy_module) return NULL;

    auto* color_type = PyObject_GetAttrString(mcrfpy_module, "Color");
    Py_DECREF(mcrfpy_module);
    if (!color_type) return NULL;

    if (PyObject_IsInstance(color_obj, color_type)) {
        color = ((PyColorObject*)color_obj)->data;
    } else if (PyTuple_Check(color_obj)) {
        int r, g, b, a = 255;
        if (!PyArg_ParseTuple(color_obj, "iii|i", &r, &g, &b, &a)) {
            Py_DECREF(color_type);
            return NULL;
        }
        color = sf::Color(r, g, b, a);
    } else {
        Py_DECREF(color_type);
        PyErr_SetString(PyExc_TypeError, "color must be a Color object or (r, g, b[, a]) tuple");
        return NULL;
    }
    Py_DECREF(color_type);

    self->data->fillRect(x, y, width, height, color);
    Py_RETURN_NONE;
}

PyObject* PyGridLayerAPI::ColorLayer_draw_fov(PyColorLayerObject* self, PyObject* args, PyObject* kwds) {
    static const char* kwlist[] = {"source", "radius", "fov", "visible", "discovered", "unknown", NULL};
    PyObject* source_obj;
    int radius = -1;  // -1 means use grid's default
    PyObject* fov_obj = Py_None;
    PyObject* visible_obj = nullptr;
    PyObject* discovered_obj = nullptr;
    PyObject* unknown_obj = nullptr;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|iOOOO", const_cast<char**>(kwlist),
                                     &source_obj, &radius, &fov_obj, &visible_obj, &discovered_obj, &unknown_obj)) {
        return NULL;
    }

    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return NULL;
    }

    if (!self->grid) {
        PyErr_SetString(PyExc_RuntimeError, "Layer is not attached to a grid");
        return NULL;
    }

    // Parse source position
    int source_x, source_y;
    if (PyTuple_Check(source_obj) && PyTuple_Size(source_obj) == 2) {
        source_x = PyLong_AsLong(PyTuple_GetItem(source_obj, 0));
        source_y = PyLong_AsLong(PyTuple_GetItem(source_obj, 1));
        if (PyErr_Occurred()) return NULL;
    } else {
        PyErr_SetString(PyExc_TypeError, "source must be a (x, y) tuple");
        return NULL;
    }

    // Get radius from grid if not specified
    if (radius < 0) {
        radius = self->grid->fov_radius;
    }

    // Get FOV algorithm
    TCOD_fov_algorithm_t algorithm;
    bool was_none = false;
    if (!PyFOV::from_arg(fov_obj, &algorithm, &was_none)) {
        return NULL;
    }
    if (was_none) {
        algorithm = self->grid->fov_algorithm;
    }

    // Helper lambda to parse color
    auto parse_color = [](PyObject* obj, sf::Color& out, const sf::Color& default_val, const char* name) -> bool {
        if (!obj || obj == Py_None) {
            out = default_val;
            return true;
        }

        auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
        if (!mcrfpy_module) return false;

        auto* color_type = PyObject_GetAttrString(mcrfpy_module, "Color");
        Py_DECREF(mcrfpy_module);
        if (!color_type) return false;

        if (PyObject_IsInstance(obj, color_type)) {
            out = ((PyColorObject*)obj)->data;
            Py_DECREF(color_type);
            return true;
        } else if (PyTuple_Check(obj)) {
            int r, g, b, a = 255;
            if (!PyArg_ParseTuple(obj, "iii|i", &r, &g, &b, &a)) {
                Py_DECREF(color_type);
                return false;
            }
            out = sf::Color(r, g, b, a);
            Py_DECREF(color_type);
            return true;
        }

        Py_DECREF(color_type);
        PyErr_Format(PyExc_TypeError, "%s must be a Color object or (r, g, b[, a]) tuple", name);
        return false;
    };

    // Default colors for FOV visualization
    sf::Color visible_color(255, 255, 200, 64);   // Light yellow tint
    sf::Color discovered_color(128, 128, 128, 128); // Gray
    sf::Color unknown_color(0, 0, 0, 255);          // Black

    if (!parse_color(visible_obj, visible_color, visible_color, "visible")) return NULL;
    if (!parse_color(discovered_obj, discovered_color, discovered_color, "discovered")) return NULL;
    if (!parse_color(unknown_obj, unknown_color, unknown_color, "unknown")) return NULL;

    self->data->drawFOV(source_x, source_y, radius, algorithm, visible_color, discovered_color, unknown_color);
    Py_RETURN_NONE;
}

PyObject* PyGridLayerAPI::ColorLayer_apply_perspective(PyColorLayerObject* self, PyObject* args, PyObject* kwds) {
    static const char* kwlist[] = {"entity", "visible", "discovered", "unknown", NULL};
    PyObject* entity_obj;
    PyObject* visible_obj = nullptr;
    PyObject* discovered_obj = nullptr;
    PyObject* unknown_obj = nullptr;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "O|OOO", const_cast<char**>(kwlist),
                                     &entity_obj, &visible_obj, &discovered_obj, &unknown_obj)) {
        return NULL;
    }

    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return NULL;
    }

    if (!self->grid) {
        PyErr_SetString(PyExc_RuntimeError, "Layer is not attached to a grid");
        return NULL;
    }

    // Get the Entity type
    auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
    if (!mcrfpy_module) return NULL;

    auto* entity_type = PyObject_GetAttrString(mcrfpy_module, "Entity");
    Py_DECREF(mcrfpy_module);
    if (!entity_type) return NULL;

    if (!PyObject_IsInstance(entity_obj, entity_type)) {
        Py_DECREF(entity_type);
        PyErr_SetString(PyExc_TypeError, "entity must be an Entity object");
        return NULL;
    }
    Py_DECREF(entity_type);

    // Get the shared_ptr to the entity
    PyUIEntityObject* py_entity = (PyUIEntityObject*)entity_obj;
    if (!py_entity->data) {
        PyErr_SetString(PyExc_RuntimeError, "Entity has no data");
        return NULL;
    }

    // Helper lambda to parse color
    auto parse_color = [](PyObject* obj, sf::Color& out, const sf::Color& default_val, const char* name) -> bool {
        if (!obj || obj == Py_None) {
            out = default_val;
            return true;
        }

        auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
        if (!mcrfpy_module) return false;

        auto* color_type = PyObject_GetAttrString(mcrfpy_module, "Color");
        Py_DECREF(mcrfpy_module);
        if (!color_type) return false;

        if (PyObject_IsInstance(obj, color_type)) {
            out = ((PyColorObject*)obj)->data;
            Py_DECREF(color_type);
            return true;
        } else if (PyTuple_Check(obj)) {
            int r, g, b, a = 255;
            if (!PyArg_ParseTuple(obj, "iii|i", &r, &g, &b, &a)) {
                Py_DECREF(color_type);
                return false;
            }
            out = sf::Color(r, g, b, a);
            Py_DECREF(color_type);
            return true;
        }

        Py_DECREF(color_type);
        PyErr_Format(PyExc_TypeError, "%s must be a Color object or (r, g, b[, a]) tuple", name);
        return false;
    };

    // Parse colors with defaults
    sf::Color visible_color(255, 255, 200, 64);
    sf::Color discovered_color(100, 100, 100, 128);
    sf::Color unknown_color(0, 0, 0, 255);

    if (!parse_color(visible_obj, visible_color, visible_color, "visible")) return NULL;
    if (!parse_color(discovered_obj, discovered_color, discovered_color, "discovered")) return NULL;
    if (!parse_color(unknown_obj, unknown_color, unknown_color, "unknown")) return NULL;

    self->data->applyPerspective(py_entity->data, visible_color, discovered_color, unknown_color);
    Py_RETURN_NONE;
}

PyObject* PyGridLayerAPI::ColorLayer_update_perspective(PyColorLayerObject* self, PyObject* args) {
    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return NULL;
    }

    if (!self->data->has_perspective) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no perspective binding. Call apply_perspective() first.");
        return NULL;
    }

    self->data->updatePerspective();
    Py_RETURN_NONE;
}

PyObject* PyGridLayerAPI::ColorLayer_clear_perspective(PyColorLayerObject* self, PyObject* args) {
    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return NULL;
    }

    self->data->clearPerspective();
    Py_RETURN_NONE;
}

PyObject* PyGridLayerAPI::ColorLayer_get_z_index(PyColorLayerObject* self, void* closure) {
    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return NULL;
    }
    return PyLong_FromLong(self->data->z_index);
}

int PyGridLayerAPI::ColorLayer_set_z_index(PyColorLayerObject* self, PyObject* value, void* closure) {
    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return -1;
    }
    long z = PyLong_AsLong(value);
    if (PyErr_Occurred()) return -1;
    self->data->z_index = z;
    // TODO: Trigger re-sort in parent grid
    return 0;
}

PyObject* PyGridLayerAPI::ColorLayer_get_visible(PyColorLayerObject* self, void* closure) {
    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return NULL;
    }
    return PyBool_FromLong(self->data->visible);
}

int PyGridLayerAPI::ColorLayer_set_visible(PyColorLayerObject* self, PyObject* value, void* closure) {
    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return -1;
    }
    int v = PyObject_IsTrue(value);
    if (v < 0) return -1;
    self->data->visible = v;
    return 0;
}

PyObject* PyGridLayerAPI::ColorLayer_get_grid_size(PyColorLayerObject* self, void* closure) {
    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return NULL;
    }
    return Py_BuildValue("(ii)", self->data->grid_x, self->data->grid_y);
}

PyObject* PyGridLayerAPI::ColorLayer_repr(PyColorLayerObject* self) {
    std::ostringstream ss;
    if (!self->data) {
        ss << "<ColorLayer (invalid)>";
    } else {
        ss << "<ColorLayer z_index=" << self->data->z_index
           << " size=(" << self->data->grid_x << "x" << self->data->grid_y << ")"
           << " visible=" << (self->data->visible ? "True" : "False") << ">";
    }
    return PyUnicode_FromString(ss.str().c_str());
}

// =============================================================================
// Python API - TileLayer
// =============================================================================

PyMethodDef PyGridLayerAPI::TileLayer_methods[] = {
    {"at", (PyCFunction)PyGridLayerAPI::TileLayer_at, METH_VARARGS,
     "at(x, y) -> int\n\nGet the tile index at cell position (x, y). Returns -1 if no tile."},
    {"set", (PyCFunction)PyGridLayerAPI::TileLayer_set, METH_VARARGS,
     "set(x, y, index)\n\nSet the tile index at cell position (x, y). Use -1 for no tile."},
    {"fill", (PyCFunction)PyGridLayerAPI::TileLayer_fill, METH_VARARGS,
     "fill(index)\n\nFill the entire layer with the specified tile index."},
    {"fill_rect", (PyCFunction)PyGridLayerAPI::TileLayer_fill_rect, METH_VARARGS | METH_KEYWORDS,
     "fill_rect(pos, size, index)\n\n"
     "Fill a rectangular region with a tile index.\n\n"
     "Args:\n"
     "    pos (tuple): Top-left corner as (x, y)\n"
     "    size (tuple): Dimensions as (width, height)\n"
     "    index (int): Tile index to fill with (-1 for no tile)"},
    {NULL}
};

PyGetSetDef PyGridLayerAPI::TileLayer_getsetters[] = {
    {"z_index", (getter)PyGridLayerAPI::TileLayer_get_z_index,
                (setter)PyGridLayerAPI::TileLayer_set_z_index,
     "Layer z-order. Negative values render below entities.", NULL},
    {"visible", (getter)PyGridLayerAPI::TileLayer_get_visible,
                (setter)PyGridLayerAPI::TileLayer_set_visible,
     "Whether the layer is rendered.", NULL},
    {"texture", (getter)PyGridLayerAPI::TileLayer_get_texture,
                (setter)PyGridLayerAPI::TileLayer_set_texture,
     "Texture atlas for tile sprites.", NULL},
    {"grid_size", (getter)PyGridLayerAPI::TileLayer_get_grid_size, NULL,
     "Layer dimensions as (width, height) tuple.", NULL},
    {NULL}
};

int PyGridLayerAPI::TileLayer_init(PyTileLayerObject* self, PyObject* args, PyObject* kwds) {
    static const char* kwlist[] = {"z_index", "texture", "grid_size", NULL};
    int z_index = -1;
    PyObject* texture_obj = nullptr;
    PyObject* grid_size_obj = nullptr;
    int grid_x = 0, grid_y = 0;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "|iOO", const_cast<char**>(kwlist),
                                     &z_index, &texture_obj, &grid_size_obj)) {
        return -1;
    }

    // Parse texture
    std::shared_ptr<PyTexture> texture;
    if (texture_obj && texture_obj != Py_None) {
        // Check if it's a PyTexture
        auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
        if (!mcrfpy_module) return -1;

        auto* texture_type = PyObject_GetAttrString(mcrfpy_module, "Texture");
        Py_DECREF(mcrfpy_module);
        if (!texture_type) return -1;

        if (PyObject_IsInstance(texture_obj, texture_type)) {
            texture = ((PyTextureObject*)texture_obj)->data;
        } else {
            Py_DECREF(texture_type);
            PyErr_SetString(PyExc_TypeError, "texture must be a Texture object");
            return -1;
        }
        Py_DECREF(texture_type);
    }

    // Parse grid_size if provided
    if (grid_size_obj && grid_size_obj != Py_None) {
        if (!PyTuple_Check(grid_size_obj) || PyTuple_Size(grid_size_obj) != 2) {
            PyErr_SetString(PyExc_TypeError, "grid_size must be a (width, height) tuple");
            return -1;
        }
        grid_x = PyLong_AsLong(PyTuple_GetItem(grid_size_obj, 0));
        grid_y = PyLong_AsLong(PyTuple_GetItem(grid_size_obj, 1));
        if (PyErr_Occurred()) return -1;
    }

    // Create the layer
    self->data = std::make_shared<TileLayer>(z_index, grid_x, grid_y, nullptr, texture);
    self->grid.reset();

    return 0;
}

PyObject* PyGridLayerAPI::TileLayer_at(PyTileLayerObject* self, PyObject* args) {
    int x, y;
    if (!PyArg_ParseTuple(args, "ii", &x, &y)) {
        return NULL;
    }

    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return NULL;
    }

    if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
        PyErr_SetString(PyExc_IndexError, "Cell coordinates out of bounds");
        return NULL;
    }

    return PyLong_FromLong(self->data->at(x, y));
}

PyObject* PyGridLayerAPI::TileLayer_set(PyTileLayerObject* self, PyObject* args) {
    int x, y, index;
    if (!PyArg_ParseTuple(args, "iii", &x, &y, &index)) {
        return NULL;
    }

    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return NULL;
    }

    if (x < 0 || x >= self->data->grid_x || y < 0 || y >= self->data->grid_y) {
        PyErr_SetString(PyExc_IndexError, "Cell coordinates out of bounds");
        return NULL;
    }

    self->data->at(x, y) = index;
    self->data->markDirty(x, y);  // Mark only the affected chunk
    Py_RETURN_NONE;
}

PyObject* PyGridLayerAPI::TileLayer_fill(PyTileLayerObject* self, PyObject* args) {
    int index;
    if (!PyArg_ParseTuple(args, "i", &index)) {
        return NULL;
    }

    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return NULL;
    }

    self->data->fill(index);
    Py_RETURN_NONE;
}

PyObject* PyGridLayerAPI::TileLayer_fill_rect(PyTileLayerObject* self, PyObject* args, PyObject* kwds) {
    static const char* kwlist[] = {"pos", "size", "index", NULL};
    PyObject* pos_obj;
    PyObject* size_obj;
    int tile_index;

    if (!PyArg_ParseTupleAndKeywords(args, kwds, "OOi", const_cast<char**>(kwlist),
                                     &pos_obj, &size_obj, &tile_index)) {
        return NULL;
    }

    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return NULL;
    }

    // Parse pos
    int x, y;
    if (PyTuple_Check(pos_obj) && PyTuple_Size(pos_obj) == 2) {
        x = PyLong_AsLong(PyTuple_GetItem(pos_obj, 0));
        y = PyLong_AsLong(PyTuple_GetItem(pos_obj, 1));
        if (PyErr_Occurred()) return NULL;
    } else {
        PyErr_SetString(PyExc_TypeError, "pos must be a (x, y) tuple");
        return NULL;
    }

    // Parse size
    int width, height;
    if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
        width = PyLong_AsLong(PyTuple_GetItem(size_obj, 0));
        height = PyLong_AsLong(PyTuple_GetItem(size_obj, 1));
        if (PyErr_Occurred()) return NULL;
    } else {
        PyErr_SetString(PyExc_TypeError, "size must be a (width, height) tuple");
        return NULL;
    }

    self->data->fillRect(x, y, width, height, tile_index);
    Py_RETURN_NONE;
}

PyObject* PyGridLayerAPI::TileLayer_get_z_index(PyTileLayerObject* self, void* closure) {
    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return NULL;
    }
    return PyLong_FromLong(self->data->z_index);
}

int PyGridLayerAPI::TileLayer_set_z_index(PyTileLayerObject* self, PyObject* value, void* closure) {
    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return -1;
    }
    long z = PyLong_AsLong(value);
    if (PyErr_Occurred()) return -1;
    self->data->z_index = z;
    // TODO: Trigger re-sort in parent grid
    return 0;
}

PyObject* PyGridLayerAPI::TileLayer_get_visible(PyTileLayerObject* self, void* closure) {
    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return NULL;
    }
    return PyBool_FromLong(self->data->visible);
}

int PyGridLayerAPI::TileLayer_set_visible(PyTileLayerObject* self, PyObject* value, void* closure) {
    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return -1;
    }
    int v = PyObject_IsTrue(value);
    if (v < 0) return -1;
    self->data->visible = v;
    return 0;
}

PyObject* PyGridLayerAPI::TileLayer_get_texture(PyTileLayerObject* self, void* closure) {
    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return NULL;
    }

    if (!self->data->texture) {
        Py_RETURN_NONE;
    }

    auto* texture_type = (PyTypeObject*)PyObject_GetAttrString(
        PyImport_ImportModule("mcrfpy"), "Texture");
    if (!texture_type) return NULL;

    PyTextureObject* tex_obj = (PyTextureObject*)texture_type->tp_alloc(texture_type, 0);
    Py_DECREF(texture_type);
    if (!tex_obj) return NULL;

    tex_obj->data = self->data->texture;
    return (PyObject*)tex_obj;
}

int PyGridLayerAPI::TileLayer_set_texture(PyTileLayerObject* self, PyObject* value, void* closure) {
    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return -1;
    }

    if (value == Py_None) {
        self->data->texture.reset();
        self->data->markDirty();  // Mark ALL chunks for re-render (texture change affects all)
        return 0;
    }

    auto* mcrfpy_module = PyImport_ImportModule("mcrfpy");
    if (!mcrfpy_module) return -1;

    auto* texture_type = PyObject_GetAttrString(mcrfpy_module, "Texture");
    Py_DECREF(mcrfpy_module);
    if (!texture_type) return -1;

    if (!PyObject_IsInstance(value, texture_type)) {
        Py_DECREF(texture_type);
        PyErr_SetString(PyExc_TypeError, "texture must be a Texture object or None");
        return -1;
    }
    Py_DECREF(texture_type);

    self->data->texture = ((PyTextureObject*)value)->data;
    self->data->markDirty();  // Mark ALL chunks for re-render (texture change affects all)
    return 0;
}

PyObject* PyGridLayerAPI::TileLayer_get_grid_size(PyTileLayerObject* self, void* closure) {
    if (!self->data) {
        PyErr_SetString(PyExc_RuntimeError, "Layer has no data");
        return NULL;
    }
    return Py_BuildValue("(ii)", self->data->grid_x, self->data->grid_y);
}

PyObject* PyGridLayerAPI::TileLayer_repr(PyTileLayerObject* self) {
    std::ostringstream ss;
    if (!self->data) {
        ss << "<TileLayer (invalid)>";
    } else {
        ss << "<TileLayer z_index=" << self->data->z_index
           << " size=(" << self->data->grid_x << "x" << self->data->grid_y << ")"
           << " visible=" << (self->data->visible ? "True" : "False")
           << " texture=" << (self->data->texture ? "set" : "None") << ">";
    }
    return PyUnicode_FromString(ss.str().c_str());
}
