//! R-Tree Spatial Index Helpers
//!
//! Provides convenient wrappers for SQLite's R-Tree extension for spatial indexing.
//! R-Tree is compiled into SQLite by default with -DSQLITE_ENABLE_RTREE.
//!
//! R-Tree is useful for:
//! - Geographic data (latitude/longitude bounding boxes)
//! - 2D/3D spatial queries
//! - Range queries on multiple dimensions
//! - Game collision detection
//! - Any multi-dimensional range search

const std = @import("std");
const Database = @import("database.zig").Database;
const Statement = @import("statement.zig").Statement;
const Error = @import("errors.zig").Error;

/// A 2D bounding box (rectangle).
pub const BoundingBox2D = struct {
    min_x: f64,
    max_x: f64,
    min_y: f64,
    max_y: f64,

    /// Creates a bounding box from center point and size.
    pub fn fromCenter(center_x: f64, center_y: f64, width: f64, height: f64) BoundingBox2D {
        const half_w = width / 2.0;
        const half_h = height / 2.0;
        return .{
            .min_x = center_x - half_w,
            .max_x = center_x + half_w,
            .min_y = center_y - half_h,
            .max_y = center_y + half_h,
        };
    }

    /// Creates a bounding box from a point (zero-size box).
    pub fn fromPoint(x: f64, y: f64) BoundingBox2D {
        return .{
            .min_x = x,
            .max_x = x,
            .min_y = y,
            .max_y = y,
        };
    }

    /// Checks if this box intersects with another box.
    pub fn intersects(self: BoundingBox2D, other: BoundingBox2D) bool {
        return self.min_x <= other.max_x and
            self.max_x >= other.min_x and
            self.min_y <= other.max_y and
            self.max_y >= other.min_y;
    }

    /// Checks if this box contains a point.
    pub fn containsPoint(self: BoundingBox2D, x: f64, y: f64) bool {
        return x >= self.min_x and x <= self.max_x and
            y >= self.min_y and y <= self.max_y;
    }

    /// Checks if this box fully contains another box.
    pub fn contains(self: BoundingBox2D, other: BoundingBox2D) bool {
        return other.min_x >= self.min_x and
            other.max_x <= self.max_x and
            other.min_y >= self.min_y and
            other.max_y <= self.max_y;
    }

    /// Returns the area of the bounding box.
    pub fn area(self: BoundingBox2D) f64 {
        return (self.max_x - self.min_x) * (self.max_y - self.min_y);
    }

    /// Returns the center point of the bounding box.
    pub fn center(self: BoundingBox2D) struct { x: f64, y: f64 } {
        return .{
            .x = (self.min_x + self.max_x) / 2.0,
            .y = (self.min_y + self.max_y) / 2.0,
        };
    }

    /// Expands this box to include another box.
    pub fn expand(self: *BoundingBox2D, other: BoundingBox2D) void {
        self.min_x = @min(self.min_x, other.min_x);
        self.max_x = @max(self.max_x, other.max_x);
        self.min_y = @min(self.min_y, other.min_y);
        self.max_y = @max(self.max_y, other.max_y);
    }
};

/// A 3D bounding box.
pub const BoundingBox3D = struct {
    min_x: f64,
    max_x: f64,
    min_y: f64,
    max_y: f64,
    min_z: f64,
    max_z: f64,

    /// Creates a 3D bounding box from center point and size.
    pub fn fromCenter(center_x: f64, center_y: f64, center_z: f64, width: f64, height: f64, depth: f64) BoundingBox3D {
        const half_w = width / 2.0;
        const half_h = height / 2.0;
        const half_d = depth / 2.0;
        return .{
            .min_x = center_x - half_w,
            .max_x = center_x + half_w,
            .min_y = center_y - half_h,
            .max_y = center_y + half_h,
            .min_z = center_z - half_d,
            .max_z = center_z + half_d,
        };
    }

    /// Creates a bounding box from a point (zero-size box).
    pub fn fromPoint(x: f64, y: f64, z: f64) BoundingBox3D {
        return .{
            .min_x = x,
            .max_x = x,
            .min_y = y,
            .max_y = y,
            .min_z = z,
            .max_z = z,
        };
    }

    /// Checks if this box intersects with another box.
    pub fn intersects(self: BoundingBox3D, other: BoundingBox3D) bool {
        return self.min_x <= other.max_x and
            self.max_x >= other.min_x and
            self.min_y <= other.max_y and
            self.max_y >= other.min_y and
            self.min_z <= other.max_z and
            self.max_z >= other.min_z;
    }

    /// Returns the volume of the bounding box.
    pub fn volume(self: BoundingBox3D) f64 {
        return (self.max_x - self.min_x) * (self.max_y - self.min_y) * (self.max_z - self.min_z);
    }
};

/// Geographic coordinates (latitude/longitude).
pub const GeoCoord = struct {
    latitude: f64,
    longitude: f64,

    /// Creates a bounding box around this point with a given radius in degrees.
    pub fn boundingBox(self: GeoCoord, radius_degrees: f64) BoundingBox2D {
        return .{
            .min_x = self.longitude - radius_degrees,
            .max_x = self.longitude + radius_degrees,
            .min_y = self.latitude - radius_degrees,
            .max_y = self.latitude + radius_degrees,
        };
    }

    /// Approximate distance in kilometers to another coordinate (Haversine formula).
    pub fn distanceKm(self: GeoCoord, other: GeoCoord) f64 {
        const earth_radius_km = 6371.0;
        const lat1 = self.latitude * std.math.pi / 180.0;
        const lat2 = other.latitude * std.math.pi / 180.0;
        const delta_lat = (other.latitude - self.latitude) * std.math.pi / 180.0;
        const delta_lon = (other.longitude - self.longitude) * std.math.pi / 180.0;

        const a = std.math.sin(delta_lat / 2.0) * std.math.sin(delta_lat / 2.0) +
            std.math.cos(lat1) * std.math.cos(lat2) *
            std.math.sin(delta_lon / 2.0) * std.math.sin(delta_lon / 2.0);
        const c = 2.0 * std.math.atan2(std.math.sqrt(a), std.math.sqrt(1.0 - a));

        return earth_radius_km * c;
    }
};

/// R-Tree helper functions for a database connection.
pub const RTree = struct {
    db: *Database,
    allocator: std.mem.Allocator,

    const Self = @This();

    /// Creates a new R-Tree helper bound to a database connection.
    pub fn init(db: *Database, allocator: std.mem.Allocator) Self {
        return .{ .db = db, .allocator = allocator };
    }

    // ========================================================================
    // Table Management
    // ========================================================================

    /// Creates a 2D R-Tree virtual table.
    ///
    /// Example:
    /// ```zig
    /// var rtree = RTree.init(&db, allocator);
    /// try rtree.createTable2D("locations", "min_x", "max_x", "min_y", "max_y");
    /// ```
    pub fn createTable2D(
        self: *Self,
        table_name: []const u8,
        min_x_name: []const u8,
        max_x_name: []const u8,
        min_y_name: []const u8,
        max_y_name: []const u8,
    ) !void {
        const sql = try std.fmt.allocPrint(
            self.allocator,
            "CREATE VIRTUAL TABLE {s} USING rtree(id, {s}, {s}, {s}, {s})\x00",
            .{ table_name, min_x_name, max_x_name, min_y_name, max_y_name },
        );
        defer self.allocator.free(sql);
        try self.db.exec(sql[0 .. sql.len - 1 :0]);
    }

    /// Creates a 3D R-Tree virtual table.
    pub fn createTable3D(
        self: *Self,
        table_name: []const u8,
        min_x_name: []const u8,
        max_x_name: []const u8,
        min_y_name: []const u8,
        max_y_name: []const u8,
        min_z_name: []const u8,
        max_z_name: []const u8,
    ) !void {
        const sql = try std.fmt.allocPrint(
            self.allocator,
            "CREATE VIRTUAL TABLE {s} USING rtree(id, {s}, {s}, {s}, {s}, {s}, {s})\x00",
            .{ table_name, min_x_name, max_x_name, min_y_name, max_y_name, min_z_name, max_z_name },
        );
        defer self.allocator.free(sql);
        try self.db.exec(sql[0 .. sql.len - 1 :0]);
    }

    /// Creates a simple 2D R-Tree with default column names.
    pub fn createSimpleTable2D(self: *Self, table_name: []const u8) !void {
        try self.createTable2D(table_name, "min_x", "max_x", "min_y", "max_y");
    }

    /// Creates a simple 3D R-Tree with default column names.
    pub fn createSimpleTable3D(self: *Self, table_name: []const u8) !void {
        try self.createTable3D(table_name, "min_x", "max_x", "min_y", "max_y", "min_z", "max_z");
    }

    /// Creates a geographic R-Tree (using latitude/longitude).
    pub fn createGeoTable(self: *Self, table_name: []const u8) !void {
        try self.createTable2D(table_name, "min_lon", "max_lon", "min_lat", "max_lat");
    }

    /// Drops an R-Tree table.
    pub fn dropTable(self: *Self, table_name: []const u8) !void {
        const sql = try std.fmt.allocPrint(self.allocator, "DROP TABLE IF EXISTS {s}\x00", .{table_name});
        defer self.allocator.free(sql);
        try self.db.exec(sql[0 .. sql.len - 1 :0]);
    }

    // ========================================================================
    // Insert Operations
    // ========================================================================

    /// Inserts a 2D bounding box into an R-Tree.
    pub fn insert2D(self: *Self, table_name: []const u8, id: i64, box: BoundingBox2D) !void {
        const sql = try std.fmt.allocPrint(
            self.allocator,
            "INSERT INTO {s} VALUES (?, ?, ?, ?, ?)\x00",
            .{table_name},
        );
        defer self.allocator.free(sql);

        var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
        defer stmt.finalize();

        try stmt.bindInt(1, id);
        try stmt.bindFloat(2, box.min_x);
        try stmt.bindFloat(3, box.max_x);
        try stmt.bindFloat(4, box.min_y);
        try stmt.bindFloat(5, box.max_y);

        _ = try stmt.step();
    }

    /// Inserts a 3D bounding box into an R-Tree.
    pub fn insert3D(self: *Self, table_name: []const u8, id: i64, box: BoundingBox3D) !void {
        const sql = try std.fmt.allocPrint(
            self.allocator,
            "INSERT INTO {s} VALUES (?, ?, ?, ?, ?, ?, ?)\x00",
            .{table_name},
        );
        defer self.allocator.free(sql);

        var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
        defer stmt.finalize();

        try stmt.bindInt(1, id);
        try stmt.bindFloat(2, box.min_x);
        try stmt.bindFloat(3, box.max_x);
        try stmt.bindFloat(4, box.min_y);
        try stmt.bindFloat(5, box.max_y);
        try stmt.bindFloat(6, box.min_z);
        try stmt.bindFloat(7, box.max_z);

        _ = try stmt.step();
    }

    /// Inserts a point (as a zero-size bounding box) into a 2D R-Tree.
    pub fn insertPoint2D(self: *Self, table_name: []const u8, id: i64, x: f64, y: f64) !void {
        try self.insert2D(table_name, id, BoundingBox2D.fromPoint(x, y));
    }

    /// Inserts a geographic coordinate into a geo R-Tree.
    pub fn insertGeo(self: *Self, table_name: []const u8, id: i64, coord: GeoCoord) !void {
        try self.insert2D(table_name, id, .{
            .min_x = coord.longitude,
            .max_x = coord.longitude,
            .min_y = coord.latitude,
            .max_y = coord.latitude,
        });
    }

    // ========================================================================
    // Update Operations
    // ========================================================================

    /// Updates a 2D bounding box in an R-Tree.
    pub fn update2D(self: *Self, table_name: []const u8, id: i64, box: BoundingBox2D) !void {
        const sql = try std.fmt.allocPrint(
            self.allocator,
            "UPDATE {s} SET min_x=?, max_x=?, min_y=?, max_y=? WHERE id=?\x00",
            .{table_name},
        );
        defer self.allocator.free(sql);

        var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
        defer stmt.finalize();

        try stmt.bindFloat(1, box.min_x);
        try stmt.bindFloat(2, box.max_x);
        try stmt.bindFloat(3, box.min_y);
        try stmt.bindFloat(4, box.max_y);
        try stmt.bindInt(5, id);

        _ = try stmt.step();
    }

    /// Deletes an entry from an R-Tree.
    pub fn delete(self: *Self, table_name: []const u8, id: i64) !void {
        const sql = try std.fmt.allocPrint(
            self.allocator,
            "DELETE FROM {s} WHERE id=?\x00",
            .{table_name},
        );
        defer self.allocator.free(sql);

        var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
        defer stmt.finalize();

        try stmt.bindInt(1, id);
        _ = try stmt.step();
    }

    // ========================================================================
    // Query Operations
    // ========================================================================

    /// Queries for all entries that intersect with a bounding box.
    /// Returns a prepared statement with results: id, min_x, max_x, min_y, max_y
    pub fn queryIntersects2D(self: *Self, table_name: []const u8, box: BoundingBox2D) !Statement {
        const sql = try std.fmt.allocPrint(
            self.allocator,
            "SELECT * FROM {s} WHERE min_x <= ? AND max_x >= ? AND min_y <= ? AND max_y >= ?\x00",
            .{table_name},
        );
        defer self.allocator.free(sql);

        var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
        errdefer stmt.finalize();

        try stmt.bindFloat(1, box.max_x);
        try stmt.bindFloat(2, box.min_x);
        try stmt.bindFloat(3, box.max_y);
        try stmt.bindFloat(4, box.min_y);

        return stmt;
    }

    /// Queries for all entries contained within a bounding box.
    pub fn queryContained2D(self: *Self, table_name: []const u8, box: BoundingBox2D) !Statement {
        const sql = try std.fmt.allocPrint(
            self.allocator,
            "SELECT * FROM {s} WHERE min_x >= ? AND max_x <= ? AND min_y >= ? AND max_y <= ?\x00",
            .{table_name},
        );
        defer self.allocator.free(sql);

        var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
        errdefer stmt.finalize();

        try stmt.bindFloat(1, box.min_x);
        try stmt.bindFloat(2, box.max_x);
        try stmt.bindFloat(3, box.min_y);
        try stmt.bindFloat(4, box.max_y);

        return stmt;
    }

    /// Queries for all entries near a point (within a given radius).
    pub fn queryNearPoint2D(self: *Self, table_name: []const u8, x: f64, y: f64, radius: f64) !Statement {
        const box = BoundingBox2D{
            .min_x = x - radius,
            .max_x = x + radius,
            .min_y = y - radius,
            .max_y = y + radius,
        };
        return self.queryIntersects2D(table_name, box);
    }

    /// Queries for all entries near a geographic coordinate.
    /// radius is in degrees (approximately: 1 degree ≈ 111km at equator)
    pub fn queryNearGeo(self: *Self, table_name: []const u8, coord: GeoCoord, radius_degrees: f64) !Statement {
        return self.queryNearPoint2D(table_name, coord.longitude, coord.latitude, radius_degrees);
    }

    /// Gets all entry IDs that intersect with a bounding box.
    pub fn getIntersectingIds2D(self: *Self, table_name: []const u8, box: BoundingBox2D) ![]i64 {
        var stmt = try self.queryIntersects2D(table_name, box);
        defer stmt.finalize();

        var ids: std.ArrayListUnmanaged(i64) = .empty;
        errdefer ids.deinit(self.allocator);

        while (try stmt.step()) {
            try ids.append(self.allocator, stmt.columnInt(0));
        }

        return ids.toOwnedSlice(self.allocator);
    }

    /// Frees an array of IDs returned by getIntersectingIds2D.
    pub fn freeIds(self: *Self, ids: []i64) void {
        self.allocator.free(ids);
    }

    /// Counts entries that intersect with a bounding box.
    pub fn countIntersects2D(self: *Self, table_name: []const u8, box: BoundingBox2D) !i64 {
        const sql = try std.fmt.allocPrint(
            self.allocator,
            "SELECT COUNT(*) FROM {s} WHERE min_x <= ? AND max_x >= ? AND min_y <= ? AND max_y >= ?\x00",
            .{table_name},
        );
        defer self.allocator.free(sql);

        var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
        defer stmt.finalize();

        try stmt.bindFloat(1, box.max_x);
        try stmt.bindFloat(2, box.min_x);
        try stmt.bindFloat(3, box.max_y);
        try stmt.bindFloat(4, box.min_y);

        if (try stmt.step()) {
            return stmt.columnInt(0);
        }
        return 0;
    }

    /// Gets the total count of entries in an R-Tree.
    pub fn count(self: *Self, table_name: []const u8) !i64 {
        const sql = try std.fmt.allocPrint(
            self.allocator,
            "SELECT COUNT(*) FROM {s}\x00",
            .{table_name},
        );
        defer self.allocator.free(sql);

        var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
        defer stmt.finalize();

        if (try stmt.step()) {
            return stmt.columnInt(0);
        }
        return 0;
    }

    /// Gets the bounding box that encompasses all entries in the R-Tree.
    pub fn getBounds2D(self: *Self, table_name: []const u8) !?BoundingBox2D {
        const sql = try std.fmt.allocPrint(
            self.allocator,
            "SELECT MIN(min_x), MAX(max_x), MIN(min_y), MAX(max_y) FROM {s}\x00",
            .{table_name},
        );
        defer self.allocator.free(sql);

        var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
        defer stmt.finalize();

        if (try stmt.step()) {
            if (stmt.columnIsNull(0)) return null;
            return BoundingBox2D{
                .min_x = stmt.columnFloat(0),
                .max_x = stmt.columnFloat(1),
                .min_y = stmt.columnFloat(2),
                .max_y = stmt.columnFloat(3),
            };
        }
        return null;
    }

    // ========================================================================
    // Join Operations (R-Tree with regular tables)
    // ========================================================================

    /// Performs a spatial join between an R-Tree and a regular table.
    /// Returns entries from both tables where the R-Tree bbox intersects the query box.
    ///
    /// Example:
    /// ```zig
    /// var stmt = try rtree.spatialJoin2D(
    ///     "locations",           // R-Tree table
    ///     "places",              // Regular table
    ///     "id",                  // Join column in places
    ///     &.{ "name", "type" },  // Columns to select from places
    ///     query_box,
    /// );
    /// ```
    pub fn spatialJoin2D(
        self: *Self,
        rtree_table: []const u8,
        data_table: []const u8,
        join_column: []const u8,
        select_columns: []const []const u8,
        box: BoundingBox2D,
    ) !Statement {
        // Build select columns
        var col_buf: [2048]u8 = undefined;
        var col_len: usize = 0;

        @memcpy(col_buf[col_len..][0..4], "r.id");
        col_len += 4;

        for (select_columns) |col| {
            @memcpy(col_buf[col_len..][0..4], ", d.");
            col_len += 4;
            @memcpy(col_buf[col_len..][0..col.len], col);
            col_len += col.len;
        }

        const sql = try std.fmt.allocPrint(
            self.allocator,
            "SELECT {s} FROM {s} r JOIN {s} d ON r.id = d.{s} WHERE r.min_x <= ? AND r.max_x >= ? AND r.min_y <= ? AND r.max_y >= ?\x00",
            .{ col_buf[0..col_len], rtree_table, data_table, join_column },
        );
        defer self.allocator.free(sql);

        var stmt = try self.db.prepare(sql[0 .. sql.len - 1 :0]);
        errdefer stmt.finalize();

        try stmt.bindFloat(1, box.max_x);
        try stmt.bindFloat(2, box.min_x);
        try stmt.bindFloat(3, box.max_y);
        try stmt.bindFloat(4, box.min_y);

        return stmt;
    }
};
