// Copyright (C) 2025 Lars Müller
// For conditions of distribution and use, see copyright notice in irrlicht.h

#include "WeightBuffer.h"

#include <algorithm>
#include <numeric>

namespace scene {

void WeightBuffer::VertexWeights::addWeight(u16 joint_id, f32 weight)
{
	assert(weight >= 0.0f);
	auto min_weight = std::min_element(weights.begin(), weights.end());
	if (*min_weight > weight)
		return;

	*min_weight = weight;
	joint_ids[std::distance(weights.begin(), min_weight)] = joint_id;
}

void WeightBuffer::addWeight(u32 vertex_id, u16 joint_id, f32 weight)
{
	weights.at(vertex_id).addWeight(joint_id, weight);
}

void WeightBuffer::VertexWeights::skinVertex(core::vector3df &pos, core::vector3df &normal,
		const std::vector<core::matrix4> &joint_transforms) const
{
	f32 total_weight = 0.0f;
	core::vector3df skinned_pos;
	core::vector3df skinned_normal;
	for (u16 i = 0; i < MAX_WEIGHTS_PER_VERTEX; ++i) {
		const u16 joint_id = joint_ids[i];
		const f32 weight = weights[i];
		if (core::equals(weight, 0.0f))
			continue;

		const auto &transform = joint_transforms[joint_id];
		core::vector3df transformed_pos = pos;
		transform.transformVect(transformed_pos);
		skinned_pos += weight * transformed_pos;
		skinned_normal += weight * transform.rotateAndScaleVect(normal);
		total_weight += weight;
	}
	if (core::equals(total_weight, 0.0f))
		return;

	pos = skinned_pos;
	// Need to renormalize normal after potentially scaling
	normal = skinned_normal.normalize();
}

void WeightBuffer::skinVertex(u32 vertex_id, core::vector3df &pos, core::vector3df &normal,
		const std::vector<core::matrix4> &joint_transforms) const
{
	return weights[vertex_id].skinVertex(pos, normal, joint_transforms);
}

void WeightBuffer::skin(IVertexBuffer *dst,
		const std::vector<core::matrix4> &joint_transforms)
{
	assert(animated_vertices.has_value());
	assert(static_pose);
	for (u32 i = 0; i < animated_vertices->size(); ++i) {
		u32 vertex_id = (*animated_vertices)[i];
		auto pos = static_pose[i].pos;
		auto normal = static_pose[i].normal;
		skinVertex(vertex_id, pos, normal, joint_transforms);
		dst->getPosition(vertex_id) = pos;
		dst->getNormal(vertex_id) = normal;
	}
	if (!animated_vertices->empty())
		dst->setDirty();
}

void WeightBuffer::finalize()
{
	// Normalizes weights so that they sum to 1.0 per vertex,
	// stores which vertices are animated.
	assert(!animated_vertices.has_value());
	animated_vertices.emplace();
	for (u32 i = 0; i < getCount(); ++i) {
		auto &weights_i = weights[i].weights;
		f32 total_weight = std::accumulate(weights_i.begin(), weights_i.end(), 0.0f);
		if (core::equals(total_weight, 0.0f)) {
			std::fill(weights_i.begin(), weights_i.end(), 0.0f);
			continue;
		}
		animated_vertices->emplace_back(i);
		if (core::equals(total_weight, 1.0f))
			continue;
		for (auto &strength : weights_i)
			strength /= total_weight;
	}
	animated_vertices->shrink_to_fit();
}

void WeightBuffer::updateStaticPose(const IVertexBuffer *vbuf)
{
	if (!static_pose)
		static_pose = std::make_unique<VertexGeometry[]>(animated_vertices->size());
	for (size_t idx = 0; idx < animated_vertices->size(); ++idx) {
		u32 vertex_id = (*animated_vertices)[idx];
		static_pose[idx].pos = vbuf->getPosition(vertex_id);
		static_pose[idx].normal = vbuf->getNormal(vertex_id);
	}
}

void WeightBuffer::resetToStaticPose(IVertexBuffer *vbuf) const
{
	assert(animated_vertices.has_value());
	if (!static_pose)
		return;
	for (size_t idx = 0; idx < animated_vertices->size(); ++idx) {
		u32 vertex_id = (*animated_vertices)[idx];
		vbuf->getPosition(vertex_id) = static_pose[idx].pos;
		vbuf->getNormal(vertex_id) = static_pose[idx].normal;
	}
	if (!animated_vertices->empty())
		vbuf->setDirty();
}

} // end namespace scene
