const std = @import("std");
const Allocator = std.mem.Allocator;

const flecs = @import("./main.zig");
const c = flecs.c;
const err = @import("./error.zig");
const util = @import("./util.zig");

const Id = @import("./id.zig").Id;
const Lookup = @import("./main.zig").Lookup;
const Path = @import("./path.zig").Path;
const World = @import("./world.zig").World;

pub const EntityError = error{
    /// Id is `0`.
    IsNone,
    /// Id is not valid.
    IsInvalid,
    /// Id is not an `Entity`.
    IsNotEntity,
    /// Generation doesn't match.
    GenMismatch,
    /// Entity is not alive.
    IsNotAlive,
};

/// An `Entity` is an `Id` which represents a "thing" within the world. This
/// could be a traditional game object, but is also able to represent other
/// concepts such as component types, relationship types, systems, modules,
/// resources, and anything else you can make fit.
///
/// Each `Entity` can have a number of `Id`s added to it, which could be
/// components (such as `Position`), zero-size tags (such as `Disabled`) or
/// relationship `Pair`s (such as `.{ ChildOf, parent }`).
///
/// Entities can be created using `World.entity()` and deleted with `delete()`.
/// When an entity is deleted it is no longer considered "alive". A world can
/// contain up to 4 billion alive entities.
///
/// An `Id` can be converted to an `Entity` using `Id.asEntity()` (fails if
/// the id isn't an entity), and back again with `asId()` (always succeeds).
pub fn Entity(comptime ctx: anytype) type {
    return struct {
        world: *World(ctx),
        raw: c.ecs_entity_t,

        const Self = @This();

        /// Returns an `Entity` for the specified world and raw entity id.
        ///
        /// No safety checks are done, so if you pass an invalid id, an
        /// invalid `Entity` might be returned that could cause panics if
        /// passed to other API functions without ensuring their validity.
        pub fn fromRaw(world: *World(ctx), raw: c.ecs_entity_t) Self {
            return .{ .world = world, .raw = raw };
        }

        pub const Config = struct {
            // Set to modify an existing entity.
            id: ?Self = null,
            /// Set to specify the entity's name.
            name: ?[:0]const u8 = null,
            /// Set to specify the entity's symbol.
            symbol: ?[:0]const u8 = null,
            /// Whether to use a small id when generating one.
            /// These are typically reserved for components.
            use_low_id: bool = false,
            /// Set to specify the parent to create the entity under.
            parent: ?Self = null,
            /// Set to specify the `Path` under which to create the entity.
            path: ?Path = null,
        };

        /// Creates or modifies an `Entity` in the specified `World`.
        ///
        /// When the settings in `config` refer to an existing entity, it is
        /// modified. When any of these settings conflict, such as when an
        /// existing entity `.id` is specified but its `.name` doesn't match,
        /// an error is returned.
        ///
        /// `add` is a tuple that specifies `Id`s added to the entity.
        /// For example: `.{ Position, Velocity, .{ ChildOf, parent } }`.
        ///
        /// When adding components, they are just default-initialized.
        /// To set their values you'll need to use `set(...)` on the
        /// `Entity` returned from this function.
        pub fn init(world: *World(ctx), config: Config, add: anytype) !Self {
            const meta = @typeInfo(@TypeOf(add));
            if (meta != .Struct or (meta.Struct.is_tuple == false and meta.Struct.fields.len > 0))
                @compileError("Expected tuple or empty struct, got '" ++ @typeName(@TypeOf(add)) ++ "'");
            if (meta.Struct.fields.len > c.FLECS_ID_DESC_MAX)
                @compileError("Adding more than FLECS_ID_DESC_MAX ids");

            var id = if (config.id) |i| i.raw else null;
            var name = config.name;
            var scope = if (config.parent) |p| p.raw else null;

            if (config.path) |path| {
                if (path.absolute) {
                    // Specifying parent with an absolute path is invalid.
                    if (config.parent != null) return error.ParentMismatch;
                    scope = 0;
                }

                // Ensure that if `id` or `name` is used alongside path,
                // their values do actually match. Also sets these so they
                // can be passed to the `ecs_entity_desc_t` struct.
                switch (path.parts[path.parts.len - 1]) {
                    .id => |path_id| {
                        if (path_id == 0) return error.InvalidParameter;
                        if (id) |i| if (path_id != i) return error.IdMismatch;
                        id = path_id;
                    },
                    .name => |path_name| {
                        if (name) |n| if (!std.mem.eql(u8, path_name, n)) return error.NameMismatch;
                        name = path_name;
                    },
                }

                // Ensure the parent entities specified in `path` exist, or create them.
                for (path.parts[0..(path.parts.len - 1)]) |part| {
                    const parent = scope orelse c.ecs_get_scope(world.raw);
                    switch (part) {
                        .id => |i| {
                            const found = c.ecs_get_alive(world.raw, i);
                            if (found == 0) return error.EntityNotFound;
                            if (parent != c.ecs_get_parent(world.raw, found)) return error.ParentMismatch;
                            scope = found;
                        },
                        .name => |n| {
                            const found = c.ecs_lookup_child(world.raw, parent, n.ptr);
                            if (found == 0) {
                                var desc = std.mem.zeroInit(c.ecs_entity_desc_t, .{ .sep = "".ptr, .name = n.ptr });
                                desc.add[0] = c.ecs_pair(c.EcsChildOf, parent);
                                scope = c.ecs_entity_init(world.raw, &desc);
                                if (scope == 0) return err.getLastErrorOrUnknown();
                            } else scope = found;
                        },
                    }
                }
            }

            // Set the scope to create the entity under (its parent).
            // This avoids using up an id for a `ChildOf` relationship.
            const previous = if (scope) |s| world.setScope(s) else null;
            defer _ = if (scope != null) world.setScope(previous);

            var desc = std.mem.zeroInit(c.ecs_entity_desc_t, .{
                .sep = "".ptr, // Disable tokenization.
                .id = if (id) |i| i else 0,
                .name = if (name) |n| n.ptr else null,
                .symbol = if (config.symbol) |s| s.ptr else null,
                .use_low_id = config.use_low_id,
            });

            inline for (add, 0..) |a, i|
                desc.add[i] = util.anyToId(ctx, a);

            for (desc.add[0..add.len]) |i|
                if (c.ecs_id_is_pair(i) and c.ECS_PAIR_FIRST(i) == c.EcsChildOf)
                    if (config.parent != null or config.path != null)
                        return error.FoundChildOf;

            const result = c.ecs_entity_init(world.raw, &desc);
            if (result == 0) return err.getLastErrorOrUnknown();
            return Self.fromRaw(world, result);
        }

        /// Ensures this `Entity` is valid, returning an error otherwise.
        /// Invalid entities may cause panics when used with API functions.
        ///
        /// An `Entity` is valid if ..
        /// - .. it is not `0`.
        /// - .. it does not have dead bits set.
        /// - .. it does not have any `Id` flags set.
        /// - .. no entity with this id is alive, but this entity's generation is `0`.
        /// - .. an entity with this exact id and generation is alive in the world.
        pub fn ensureValid(self: Self) !void {
            // `0` is not a valid entity id.
            if (self.raw == 0) return EntityError.IsNone;
            // Entity ids should not contain data in dead zone bits.
            const VALID_BITS_MASK: c.ecs_entity_t = 0xFF00FFFFFFFFFFFF;
            if ((self.raw & ~VALID_BITS_MASK) != 0) return EntityError.IsInvalid;
            // Entity ids should not contain `Id` flag bits.
            if ((self.raw & c.ECS_ID_FLAGS_MASK) != 0) return EntityError.IsNotEntity;

            // Get the currently alive entity entity with the same id.
            // If entity is alive, the generation has to match.
            // If entity is not alive, the generation has to be `0`.
            const entity = c.ecs_get_alive(self.world.raw, self.raw);
            if (c.ECS_GENERATION(entity) != c.ECS_GENERATION(self.raw))
                return EntityError.GenMismatch;
        }

        /// Ensures this `Entity` is alive in this world, returning an error if not.
        pub fn ensureAlive(self: Self) !void {
            if (!c.ecs_is_alive(self.world.raw, self.raw)) return EntityError.IsNotAlive;
        }

        pub fn asId(self: Self) Id(ctx) {
            return @bitCast(self);
        }

        pub fn getEntityId(self: Self) u32 {
            return @intCast(self.raw & c.ECS_ENTITY_MASK);
        }

        pub fn getGeneration(self: Self) u16 {
            return @intCast(c.ECS_GENERATION(self.raw));
        }

        /// Deletes this `Entity` and all of its components.
        pub fn delete(self: Self) void {
            c.ecs_delete(self.world.raw, self.raw);
        }

        /// Returns the full, absolute `Path` of this `Entity`.
        /// The entity is assumed to be alive.
        /// See also: `Path.fromEntity(...)`.
        pub fn getPath(self: Self, alloc: Allocator) !Path {
            return Path.fromEntity(ctx, null, self, alloc);
        }

        /// Gets the name of this `Entity`, or `null` if none.
        ///
        /// The returned string is owned by Flecs and may only be valid while
        /// the entity is alive and its name doesn't change.
        pub fn getName(self: Self) ?[:0]const u8 {
            const result = c.ecs_get_name(self.world.raw, self.raw);
            return std.mem.sliceTo(result, 0);
        }

        /// Gets the symbol of this `Entity`, or `null` if none.
        ///
        /// The returned string is owned by Flecs and may only be valid while
        /// the entity is alive and its symbol doesn't change.
        pub fn getSymbol(self: Self) ?[:0]const u8 {
            const result = c.ecs_get_symbol(self.world.raw, self.raw);
            return std.mem.sliceTo(result, 0);
        }

        /// Sets the name of this `Entity` to the specified value, if any.
        ///
        /// Flecs does not take ownership of the provided value.
        pub fn setName(self: Self, value: ?[*:0]const u8) void {
            _ = c.ecs_set_name(self.world.raw, self.raw, value);
        }

        /// Sets the symbol of this `Entity` to the specified value, if any.
        ///
        /// Flecs does not take ownership of the provided value.
        pub fn setSymbol(self: Self, value: ?[*:0]const u8) void {
            _ = c.ecs_set_symbol(self.world.raw, self.raw, value);
        }

        /// Gets the parent of this `Entity`, or `null` if it has none.
        pub fn getParent(self: Self) ?Self {
            const result = c.ecs_get_parent(self.world.raw, self.raw);
            return if (result != 0) fromRaw(self.world, result) else null;
        }

        /// Returns whether this `Entity` has the specified value.
        ///
        /// `value` must be convertible to an id. See also: `util.anyToId(...)`.
        pub fn has(self: Self, value: anytype) bool {
            const id = util.anyToId(ctx, value);
            return c.ecs_has_id(self.world.raw, self.raw, id);
        }

        pub fn get(self: Self, comptime T: type) ?*const T {
            const id = Lookup(ctx, T).id;
            return @alignCast(@ptrCast(c.ecs_get_id(self.world.raw, self.raw, id)));
        }

        pub fn set(self: Self, comptime T: type, value: T) void {
            const id = Lookup(ctx, T).id;
            _ = c.ecs_set_id(self.world.raw, self.raw, id, @sizeOf(T), &value);
        }
    };
}