local ffi = require("ffi")
local C = ffi.C
local core = require("pxl8.core")

local gfx3d = {}

local Camera3D = {}
Camera3D.__index = Camera3D

function Camera3D.new()
    local cam = C.pxl8_3d_camera_create()
    if cam == nil then
        return nil
    end
    return setmetatable({ _ptr = cam }, Camera3D)
end

function Camera3D:destroy()
    if self._ptr then
        C.pxl8_3d_camera_destroy(self._ptr)
        self._ptr = nil
    end
end

function Camera3D:get_forward()
    local v = C.pxl8_3d_camera_get_forward(self._ptr)
    return {v.x, v.y, v.z}
end

function Camera3D:get_position()
    local v = C.pxl8_3d_camera_get_position(self._ptr)
    return {v.x, v.y, v.z}
end

function Camera3D:get_right()
    local v = C.pxl8_3d_camera_get_right(self._ptr)
    return {v.x, v.y, v.z}
end

function Camera3D:get_up()
    local v = C.pxl8_3d_camera_get_up(self._ptr)
    return {v.x, v.y, v.z}
end

function Camera3D:get_view()
    return C.pxl8_3d_camera_get_view(self._ptr)
end

function Camera3D:get_projection()
    return C.pxl8_3d_camera_get_projection(self._ptr)
end

function Camera3D:lookat(eye, target, up)
    up = up or {0, 1, 0}
    local eye_vec = ffi.new("pxl8_vec3", {x = eye[1], y = eye[2], z = eye[3]})
    local target_vec = ffi.new("pxl8_vec3", {x = target[1], y = target[2], z = target[3]})
    local up_vec = ffi.new("pxl8_vec3", {x = up[1], y = up[2], z = up[3]})
    C.pxl8_3d_camera_lookat(self._ptr, eye_vec, target_vec, up_vec)
end

function Camera3D:set_perspective(fov, aspect, near, far)
    C.pxl8_3d_camera_set_perspective(self._ptr, fov, aspect, near, far)
end

function Camera3D:set_position(x, y, z)
    local pos = ffi.new("pxl8_vec3", {x = x, y = y, z = z})
    C.pxl8_3d_camera_set_position(self._ptr, pos)
end

function Camera3D:set_rotation(pitch, yaw, roll)
    C.pxl8_3d_camera_set_rotation(self._ptr, pitch, yaw or 0, roll or 0)
end

function Camera3D:update(dt)
    C.pxl8_3d_camera_update(self._ptr, dt)
end

function Camera3D:world_to_screen(x, y, z, width, height)
    local pos = ffi.new("pxl8_vec3", {x = x, y = y, z = z})
    local result = C.pxl8_3d_camera_world_to_screen(self._ptr, pos, width, height)
    if result.visible then
        return {x = result.x, y = result.y, depth = result.depth}
    end
    return nil
end

gfx3d.Camera3D = Camera3D

local Mesh = {}
Mesh.__index = Mesh

function Mesh.new(vertices, indices)
    local vertex_count = #vertices
    local index_count = #indices
    local mesh = C.pxl8_mesh_create(vertex_count, index_count)
    if mesh == nil then
        return nil
    end
    local self = setmetatable({ _ptr = mesh }, Mesh)
    for _, v in ipairs(vertices) do
        local vert = ffi.new("pxl8_vertex", {
            position = {x = v.x or 0, y = v.y or 0, z = v.z or 0},
            normal = {x = v.nx or 0, y = v.ny or 0, z = v.nz or 0},
            u = v.u or 0,
            v = v.v or 0,
            color = v.color or 0,
            light = v.light or 255,
        })
        C.pxl8_mesh_push_vertex(mesh, vert)
    end
    for i = 1, #indices, 3 do
        C.pxl8_mesh_push_triangle(mesh, indices[i], indices[i+1], indices[i+2])
    end
    return self
end

function Mesh:destroy()
    if self._ptr then
        C.pxl8_mesh_destroy(self._ptr)
        self._ptr = nil
    end
end

function Mesh:clear()
    if self._ptr then
        C.pxl8_mesh_clear(self._ptr)
    end
end

gfx3d.Mesh = Mesh

function gfx3d.draw_mesh(mesh, opts)
    opts = opts or {}
    local model = ffi.new("pxl8_mat4")
    local s = opts.scale or 1
    model.m[0] = s
    model.m[5] = s
    model.m[10] = s
    model.m[15] = 1
    if opts.x then model.m[12] = opts.x end
    if opts.y then model.m[13] = opts.y end
    if opts.z then model.m[14] = opts.z end
    local material = ffi.new("pxl8_gfx_material", {
        texture_id = opts.texture or 0,
        alpha = opts.alpha or 255,
        blend_mode = opts.blend_mode or 0,
        dither = opts.dither ~= false,
        double_sided = opts.double_sided or false,
        dynamic_lighting = opts.lighting or false,
        per_pixel = opts.per_pixel or false,
        vertex_color_passthrough = opts.passthrough or false,
        wireframe = opts.wireframe or false,
        emissive_intensity = opts.emissive or 0.0,
    })
    C.pxl8_3d_draw_mesh(core.gfx, mesh._ptr, model, material)
end

function gfx3d.begin_frame(camera, uniforms)
    uniforms = uniforms or {}
    local u = ffi.new("pxl8_3d_uniforms")

    u.ambient = uniforms.ambient or 0
    u.fog_color = uniforms.fog_color or 0
    u.fog_density = uniforms.fog_density or 0.0
    u.time = uniforms.time or 0.0

    if uniforms.celestial_dir then
        u.celestial_dir.x = uniforms.celestial_dir[1] or 0
        u.celestial_dir.y = uniforms.celestial_dir[2] or -1
        u.celestial_dir.z = uniforms.celestial_dir[3] or 0
    else
        u.celestial_dir.x = 0
        u.celestial_dir.y = -1
        u.celestial_dir.z = 0
    end
    u.celestial_intensity = uniforms.celestial_intensity or 0.0

    u.num_lights = 0
    if uniforms.lights then
        for i, light in ipairs(uniforms.lights) do
            if i > 16 then break end
            local idx = i - 1
            u.lights[idx].position.x = light.x or 0
            u.lights[idx].position.y = light.y or 0
            u.lights[idx].position.z = light.z or 0
            u.lights[idx].r = light.r or 255
            u.lights[idx].g = light.g or 255
            u.lights[idx].b = light.b or 255
            u.lights[idx].intensity = light.intensity or 255
            u.lights[idx].radius = light.radius or 100
            local radius_sq = u.lights[idx].radius * u.lights[idx].radius
            u.lights[idx].radius_sq = radius_sq
            u.lights[idx].inv_radius_sq = radius_sq > 0 and (1.0 / radius_sq) or 0
            u.num_lights = i
        end
    end

    C.pxl8_3d_begin_frame(core.gfx, camera._ptr, u)
end

function gfx3d.clear(color)
    C.pxl8_3d_clear(core.gfx, color or 0)
end

function gfx3d.clear_depth()
    C.pxl8_3d_clear_depth(core.gfx)
end

function gfx3d.draw_line(p0, p1, color)
    local vec0 = ffi.new("pxl8_vec3", {x = p0[1], y = p0[2], z = p0[3]})
    local vec1 = ffi.new("pxl8_vec3", {x = p1[1], y = p1[2], z = p1[3]})
    C.pxl8_3d_draw_line(core.gfx, vec0, vec1, color)
end

function gfx3d.end_frame()
    C.pxl8_3d_end_frame(core.gfx)
end

return gfx3d
