const std = @import("std");
const zphysics = @import("zphysics");
const core = @import("core");
const zm = core.zm;

const physicsCollider = @import("physicsCollider.zig");
pub const PhysicsCollider = physicsCollider.PhysicsCollider;

const physicsCharacter = @import("physicsCharacter.zig");
pub const PhysicsCharacter = physicsCharacter.PhysicsCharacter;

pub const ObjectLayers = struct {
    pub const non_moving: zphysics.ObjectLayer = 0;
    pub const moving: zphysics.ObjectLayer = 1;
    pub const len: u32 = 2;
};

pub const BroadPhaseLayers = struct {
    pub const non_moving: zphysics.BroadPhaseLayer = 0;
    pub const moving: zphysics.BroadPhaseLayer = 1;
    pub const len: u32 = 2;
};

pub const IgnoreFixedBodiesFilter = extern struct {
    __v: *const zphysics.BodyFilter.VTable = &vtable,

    filtered: [16]BodyId = undefined,
    len: usize = 0,

    const vtable = zphysics.BodyFilter.VTable{
        .shouldCollide = _shouldCollide,
        .shouldCollideLocked = _shouldCollideLocked,
    };

    pub fn addIgnoredBody(self: *@This(), bodyId: BodyId) !void {
        if (self.len == 16) {
            return error.OutOfMemory;
        }

        self.filtered[self.len] = bodyId;
        self.len += 1;
    }

    pub fn _shouldCollide(p: *const zphysics.BodyFilter, bodyId: *const BodyId) callconv(.C) bool {
        const self: *const @This() = @ptrCast(p);
        for (0..self.len) |i| {
            if (self.filtered[i] == bodyId.*) {
                return false;
            }
        }
        return true;
    }

    pub fn _shouldCollideLocked(p: *const zphysics.BodyFilter, body: *const Body) callconv(.C) bool {
        const self: *const @This() = @ptrCast(p);
        _ = self;
        _ = body;
        return true;
    }
};

const BroadPhaseLayerInterface = extern struct {
    usingnamespace zphysics.BroadPhaseLayerInterface.Methods(@This());
    __v: *const zphysics.BroadPhaseLayerInterface.VTable = &vtable,

    object_to_broad_phase: [ObjectLayers.len]zphysics.BroadPhaseLayer = undefined,

    const vtable = zphysics.BroadPhaseLayerInterface.VTable{
        .getNumBroadPhaseLayers = _getNumBroadPhaseLayers,
        .getBroadPhaseLayer = _getBroadPhaseLayer,
    };

    fn init() BroadPhaseLayerInterface {
        var layer_interface: BroadPhaseLayerInterface = .{};
        layer_interface.object_to_broad_phase[ObjectLayers.non_moving] = BroadPhaseLayers.non_moving;
        layer_interface.object_to_broad_phase[ObjectLayers.moving] = BroadPhaseLayers.moving;
        return layer_interface;
    }

    fn _getNumBroadPhaseLayers(_: *const zphysics.BroadPhaseLayerInterface) callconv(.C) u32 {
        return BroadPhaseLayers.len;
    }

    fn _getBroadPhaseLayer(
        interface_self: *const zphysics.BroadPhaseLayerInterface,
        object_layer: zphysics.ObjectLayer,
    ) callconv(.C) zphysics.BroadPhaseLayer {
        const self: *const BroadPhaseLayerInterface = @ptrCast(interface_self);
        return self.object_to_broad_phase[object_layer];
    }
};

const ObjectVsBroadPhaseLayerFilter = extern struct {
    usingnamespace zphysics.ObjectVsBroadPhaseLayerFilter.Methods(@This());
    __v: *const zphysics.ObjectVsBroadPhaseLayerFilter.VTable = &vtable,

    const vtable = zphysics.ObjectVsBroadPhaseLayerFilter.VTable{ .shouldCollide = _shouldCollide };

    fn _shouldCollide(
        _: *const zphysics.ObjectVsBroadPhaseLayerFilter,
        object_layer: zphysics.ObjectLayer,
        broad_phase_layer: zphysics.BroadPhaseLayer,
    ) callconv(.C) bool {
        return switch (object_layer) {
            ObjectLayers.non_moving => broad_phase_layer == BroadPhaseLayers.moving,
            ObjectLayers.moving => true,
            else => unreachable,
        };
    }
};

const ObjectLayerPairFilter = extern struct {
    usingnamespace zphysics.ObjectLayerPairFilter.Methods(@This());
    __v: *const zphysics.ObjectLayerPairFilter.VTable = &vtable,

    const vtable = zphysics.ObjectLayerPairFilter.VTable{ .shouldCollide = _shouldCollide };

    fn _shouldCollide(
        _: *const zphysics.ObjectLayerPairFilter,
        a: zphysics.ObjectLayer,
        b: zphysics.ObjectLayer,
    ) callconv(.C) bool {
        return switch (a) {
            ObjectLayers.non_moving => b == ObjectLayers.moving,
            ObjectLayers.moving => true,
            else => unreachable,
        };
    }
};

pub const PhysicsRuntime = struct {
    allocator: std.mem.Allocator,

    bpli: BroadPhaseLayerInterface,
    ovbplf: ObjectVsBroadPhaseLayerFilter = .{},
    olpf: ObjectLayerPairFilter = .{},
    max_bodies: u32 = 8192,

    system: *zphysics.PhysicsSystem = undefined,

    shapes: std.AutoHashMapUnmanaged(u32, ShapeRef) = .{},

    updatePeriod: f64 = 1.0 / 60.0,
    timeSinceUpdate: f64 = 0.0,
    idToEntity: std.AutoHashMapUnmanaged(zphysics.BodyId, core.Entity) = .{},

    pub var NeonObjectTable: core.EngineObjectVTable = core.EngineObjectVTable.from(@This());

    pub fn registerBodyEntity(self: *@This(), bodyId: zphysics.BodyId, entity: core.Entity) !void {
        try self.idToEntity.put(self.allocator, bodyId, entity);
    }

    pub fn init(allocator: std.mem.Allocator) !*@This() {
        const self = try allocator.create(@This());

        self.* = .{
            .allocator = allocator,
            .bpli = BroadPhaseLayerInterface.init(),
        };

        const system = try zphysics.PhysicsSystem.create(
            @as(*const zphysics.BroadPhaseLayerInterface, @ptrCast(&self.bpli)),
            @as(*const zphysics.ObjectVsBroadPhaseLayerFilter, @ptrCast(&self.ovbplf)),
            @as(*const zphysics.ObjectLayerPairFilter, @ptrCast(&self.olpf)),
            .{
                .max_bodies = self.max_bodies,
                .max_body_pairs = self.max_bodies * 2,
                .max_contact_constraints = self.max_bodies * 2,
                .num_body_mutexes = 0,
            },
        );

        self.system = system;

        try core.defineComponent(PhysicsCharacter, self.allocator);
        try core.defineComponent(PhysicsCollider, self.allocator);

        return self;
    }

    pub fn tick(self: *@This(), dt: f64) void {
        // self.timeSinceUpdate += dt;

        // while (self.timeSinceUpdate > self.updatePeriod) {
        //     self.timeSinceUpdate -= self.updatePeriod;
        //     self.system.update(@floatCast(self.updatePeriod), .{}) catch unreachable;
        // }
        self.system.update(@floatCast(dt), .{}) catch unreachable;

        // todo.. interpolation kinda easy here.
        self.updateScenes(dt);
    }

    fn updateScenes(self: *@This(), dt: f64) void {
        const lockInterface = self.system.getBodyLockInterface();

        // update character controllers

        for (PhysicsCharacter.BaseContainer.list.items) |physChar| {
            physChar.update(dt);
        }

        // update physics colliders
        for (PhysicsCollider.BaseContainer.list.items) |collider| {
            if (collider.mobile == false) {
                continue;
            }

            if (collider.bodyId) |bodyId| {
                var readLock: zphysics.BodyLockRead = .{};
                readLock.lock(lockInterface, bodyId);
                defer readLock.unlock();

                if (readLock.body) |body| {
                    const scene = collider.entity.get(core.Scene).?;
                    scene.setPosition(core.Vectorf.fromArray(body.position));
                    scene.setRotation(.{ .quat = body.rotation });
                }
            }
        }
    }

    pub fn deinit(self: *@This()) void {
        const allocator = self.allocator;

        for (PhysicsCharacter.BaseContainer.list.items) |physChar| {
            physChar.deinit();
        }
        self.idToEntity.deinit(self.allocator);
        core.undefineComponent(PhysicsCharacter);
        core.undefineComponent(PhysicsCollider);
        self.shapes.deinit(self.allocator);
        self.system.destroy();
        allocator.destroy(self);
    }

    pub fn createShape(self: *@This(), name: *core.Name, settings: ShapeSettings) !void {
        const ref: ShapeRef = .{
            .settings = settings,
            .shape = try settings.createShape(),
        };
        try self.shapes.put(self.allocator, name.handle(), ref);
    }
};

pub const ShapeSettings = union(enum(u8)) {
    convex: *zphysics.ConvexShapeSettings,
    box: *zphysics.BoxShapeSettings,
    sphere: *zphysics.SphereShapeSettings,
    triangle: *zphysics.TriangleShapeSettings,
    capsule: *zphysics.CapsuleShapeSettings,
    taperedCapsule: *zphysics.TaperedCapsuleShapeSettings,
    cylinder: *zphysics.CylinderShapeSettings,
    convexHull: *zphysics.ConvexHullShapeSettings,
    heightField: *zphysics.HeightFieldShapeSettings,
    mesh: *zphysics.MeshShapeSettings,
    decorated: *zphysics.DecoratedShapeSettings,
    compound: *zphysics.CompoundShapeSettings,

    pub fn release(self: @This()) void {
        switch (self) {
            .convex => |x| {
                x.release();
            },
            .box => |x| {
                x.release();
            },
            .sphere => |x| {
                x.release();
            },
            .triangle => |x| {
                x.release();
            },
            .capsule => |x| {
                x.release();
            },
            .taperedCapsule => |x| {
                x.release();
            },
            .cylinder => |x| {
                x.release();
            },
            .convexHull => |x| {
                x.release();
            },
            .heightField => |x| {
                x.release();
            },
            .mesh => |x| {
                x.release();
            },
            .decorated => |x| {
                x.release();
            },
            .compound => |x| {
                x.release();
            },
        }
    }

    pub fn createShape(self: @This()) !*zphysics.Shape {
        switch (self) {
            .convex => |x| {
                return try x.createShape();
            },
            .box => |x| {
                return try x.createShape();
            },
            .sphere => |x| {
                return try x.createShape();
            },
            .triangle => |x| {
                return try x.createShape();
            },
            .capsule => |x| {
                return try x.createShape();
            },
            .taperedCapsule => |x| {
                return try x.createShape();
            },
            .cylinder => |x| {
                return try x.createShape();
            },
            .convexHull => |x| {
                return try x.createShape();
            },
            .heightField => |x| {
                return try x.createShape();
            },
            .mesh => |x| {
                return try x.createShape();
            },
            .decorated => |x| {
                return try x.createShape();
            },
            .compound => |x| {
                return try x.createShape();
            },
        }
    }
};

pub const ShapeRef = struct {
    settings: ShapeSettings = undefined,
    shape: *zphysics.Shape = undefined,
};

const BodyId = zphysics.BodyId;
const Body = zphysics.Body;
