// HARFANG(R) Copyright (C) 2021 Emmanuel Julien, NWNC HARFANG. Released under GPL/LGPL/Commercial Licence, see licence.txt for details.

#include "engine/geometry.h"
#include "engine/file_format.h"
#include "engine/mikktspace.h"

#include "foundation/file.h"
#include "foundation/file_rw_interface.h"
#include "foundation/format.h"
#include "foundation/log.h"
#include "foundation/math.h"
#include "foundation/pack_float.h"
#include "foundation/time.h"

#include <bgfx/bgfx.h>

#include <numeric>

namespace hg {

uint8_t GetModelBinaryFormatVersion() { return 2; }

size_t ComputeBindingCount(const Geometry &geo) {
	return std::accumulate(std::begin(geo.pol), std::end(geo.pol), 0, [](int v, const Geometry::Polygon &pol) { return v + pol.vtx_count; });
}

size_t ComputeTriangleCount(const Geometry &geo) {
	return std::accumulate(
		std::begin(geo.pol), std::end(geo.pol), 0, [](int v, const Geometry::Polygon &pol) { return v + (pol.vtx_count > 2 ? pol.vtx_count - 2 : 0); });
}

std::vector<uint32_t> ComputePolygonIndex(const Geometry &geo) {
	std::vector<uint32_t> out;
	out.reserve(geo.pol.size());

	uint32_t idx = 0;
	for (auto &pol : geo.pol) {
		out.push_back(idx);
		idx += pol.vtx_count;
	}
	return out;
}

std::vector<VertexToPolygon> ComputeVertexToPolygon(const Geometry &geo) {
	std::vector<VertexToPolygon> vtx_to_pol(geo.vtx.size());

	size_t tt = 0;
	for (auto &p : geo.pol) {
		for (auto j = 0; j < p.vtx_count; ++j)
			++vtx_to_pol[geo.binding[tt + j]].pol_count;
		tt += p.vtx_count;
	}

	for (auto j = 0; j < geo.vtx.size(); ++j) {
		vtx_to_pol[j].pol_index.resize(vtx_to_pol[j].pol_count);
		vtx_to_pol[j].vtx_index.resize(vtx_to_pol[j].pol_count);
		vtx_to_pol[j].pol_count = 0;
	}

	tt = 0;
	for (auto i = 0; i < geo.pol.size(); ++i) {
		for (auto j = 0; j < geo.pol[i].vtx_count; ++j) {
			const auto v = geo.binding[tt + j];
			vtx_to_pol[v].pol_index[vtx_to_pol[v].pol_count] = numeric_cast<uint32_t>(i);
			vtx_to_pol[v].vtx_index[vtx_to_pol[v].pol_count] = j;
			++vtx_to_pol[v].pol_count;
		}
		tt += geo.pol[i].vtx_count;
	}

	return vtx_to_pol;
}

std::vector<VertexToVertex> ComputeVertexToVertex(const Geometry &geo, const std::vector<VertexToPolygon> &vtx_to_pol) {
	std::vector<VertexToVertex> vtx_to_vtx(geo.vtx.size());

	static const int __VertexToVertexTempListSize = 1024;
	std::array<VertexToVertex::PolygonVertex, __VertexToVertexTempListSize> tmp_vtx_to_vtx;

	for (auto pass = 0; pass < 2; ++pass)
		for (auto v = 0; v < geo.vtx.size(); ++v) {
			vtx_to_vtx[v].vtx_count = 0;

			size_t vtx_vtx_count = 0, tt = 0;
			for (auto p = 0; p < vtx_to_pol[v].pol_count; ++p) {
				const uint32_t pol_index = vtx_to_pol[v].pol_index[p];

				const auto &pol = geo.pol[pol_index];

				int ci = 0;
				for (; ci < pol.vtx_count; ++ci)
					if (geo.binding[tt + ci] == v)
						break;

				for (auto _c = ci - 1; _c <= ci + 1; _c += 2) {
					int vtx_index = _c;
					if (vtx_index < 0)
						vtx_index += pol.vtx_count;
					if (vtx_index >= pol.vtx_count)
						vtx_index -= pol.vtx_count;

					// invalidate already registered candidate
					bool insert = true;
					for (auto nl = 0; nl < vtx_vtx_count; ++nl)
						if (tmp_vtx_to_vtx[nl].pol_index == pol_index && tmp_vtx_to_vtx[nl].vtx_index == uint32_t(vtx_index)) {
							insert = false;
							break;
						}

					if (insert) {
						if (vtx_vtx_count == __VertexToVertexTempListSize) {
							error("Temporary list exceeded, vertex to vertex LUT corrupted");
							vtx_vtx_count = __VertexToVertexTempListSize - 1;
						}

						tmp_vtx_to_vtx[vtx_vtx_count].pol_index = pol_index;
						tmp_vtx_to_vtx[vtx_vtx_count].vtx_index = vtx_index;

						if (pass == 1) {
							vtx_to_vtx[v].vtx[vtx_vtx_count].pol_index = pol_index;
							vtx_to_vtx[v].vtx[vtx_vtx_count].vtx_index = vtx_index;
						}

						++vtx_vtx_count;
					}
				}

				tt += pol.vtx_count;
			}

			// allocate vertex container for this vertex
			if (pass == 0)
				vtx_to_vtx[v].vtx.reserve(vtx_vtx_count);
		}

	return vtx_to_vtx;
}

std::vector<Vec3> ComputePolygonNormal(const Geometry &geo) {
	std::vector<Vec3> out(geo.pol.size());

	size_t tt = 0;
	for (auto c = 0; c < geo.pol.size(); ++c) {
		const auto &pol = geo.pol[c];

		if (pol.vtx_count > 2) {
			const auto va = geo.vtx[geo.binding[tt + 2]] - geo.vtx[geo.binding[tt + 0]], vb = geo.vtx[geo.binding[tt + 1]] - geo.vtx[geo.binding[tt + 0]];
			out[c] = Normalize(Cross(vb, va));
		} else {
			out[c] = {0, 0, 0};
		}

		tt += pol.vtx_count;
	}

	return out;
}

std::vector<Vec3> ComputeVertexNormal(const Geometry &geo, const std::vector<VertexToPolygon> &vtx_to_pol, float msa) {
	const auto pol_normal = ComputePolygonNormal(geo);

	std::vector<Vec3> out(geo.binding.size());

	msa = Cos(msa);

	size_t tt = 0;
	for (auto cp = 0, ttp = 0; cp < geo.pol.size(); ++cp) {
		const auto &pol = geo.pol[cp];

		for (auto cv = 0; cv < pol.vtx_count; ++cv) {
			const auto gv = geo.binding[tt + cv];

			auto normal = pol_normal[cp];
			for (auto cg = 0; cg < vtx_to_pol[gv].pol_count; ++cg) {
				const auto cc = vtx_to_pol[gv].pol_index[cg];

				if (cc != cp)
					if (Dot(pol_normal[cp], pol_normal[cc]) >= msa)
						normal += pol_normal[cc];
			}
			out[ttp++] = Normalize(normal);
		}

		tt += pol.vtx_count;
	}

	return out;
}

//
struct SMikkTSpaceContextData {
	const hg::Geometry &geo;
	const std::vector<Vec3> &nrm;

	std::vector<uint32_t> pol_idx;
	std::vector<Geometry::TangentFrame> &out;

	uint32_t uv_index{0};
};

static int MikkT_getNumFace(const SMikkTSpaceContext *pContext) {
	const auto data = reinterpret_cast<SMikkTSpaceContextData *>(pContext->m_pUserData);
	return int(data->geo.pol.size());
}

static int MikkT_getNumVerticesOfFace(const SMikkTSpaceContext *pContext, const int iFace) {
	const auto data = reinterpret_cast<SMikkTSpaceContextData *>(pContext->m_pUserData);
	return data->geo.pol[iFace].vtx_count;
}

static void MikkT_getPosition(const SMikkTSpaceContext *pContext, float fvPosOut[], const int iFace, const int iVert) {
	const auto data = reinterpret_cast<SMikkTSpaceContextData *>(pContext->m_pUserData);
	const auto &T = data->geo.vtx[data->geo.binding[data->pol_idx[iFace] + iVert]];

	fvPosOut[0] = T.x;
	fvPosOut[1] = T.y;
	fvPosOut[2] = T.z;
}

static void MikkT_getNormal(const SMikkTSpaceContext *pContext, float fvNormOut[], const int iFace, const int iVert) {
	const auto data = reinterpret_cast<SMikkTSpaceContextData *>(pContext->m_pUserData);
	const auto &N = data->nrm[data->pol_idx[iFace] + iVert];

	fvNormOut[0] = N.x;
	fvNormOut[1] = N.y;
	fvNormOut[2] = N.z;
}

static void MikkT_getTexCoord(const SMikkTSpaceContext *pContext, float fvTexcOut[], const int iFace, const int iVert) {
	const auto data = reinterpret_cast<SMikkTSpaceContextData *>(pContext->m_pUserData);

	if (data->uv_index < data->geo.uv.size()) {
		const auto &UV = data->geo.uv[data->uv_index][data->pol_idx[iFace] + iVert];
		fvTexcOut[0] = UV.x;
		fvTexcOut[1] = UV.y;
	} else {
		fvTexcOut[0] = 0.f;
		fvTexcOut[1] = 0.f;
	}
}

static void MikkT_setTSpaceBasic(const SMikkTSpaceContext *pContext, const float fvTangent[], const float fSign, const int iFace, const int iVert) {
	const auto data = reinterpret_cast<SMikkTSpaceContextData *>(pContext->m_pUserData);
	const auto i = data->pol_idx[iFace] + iVert;

	auto &F = data->out[i];

	F.T.x = fvTangent[0];
	F.T.y = fvTangent[1];
	F.T.z = fvTangent[2];

	F.B = Cross(data->nrm[i], F.T) * fSign;
}

std::vector<Geometry::TangentFrame> ComputeVertexTangent(const Geometry &geo, const std::vector<Vec3> &vtx_normal, uint32_t uv_index, float msa) {
	std::vector<Geometry::TangentFrame> out(geo.binding.size());

	SMikkTSpaceInterface itf = {
		MikkT_getNumFace, MikkT_getNumVerticesOfFace, MikkT_getPosition, MikkT_getNormal, MikkT_getTexCoord, MikkT_setTSpaceBasic, nullptr};

	SMikkTSpaceContextData data = {geo, vtx_normal, ComputePolygonIndex(geo), out, uv_index};
	SMikkTSpaceContext ctx = {&itf, &data};

	genTangSpace(&ctx, RadianToDegree(msa));

	return out;
}

//
void ReverseTangentFrame(Geometry &geo, bool T, bool B) {
	if (T)
		for (auto &v : geo.tangent)
			v.T = Reverse(v.T);

	if (B)
		for (auto &v : geo.tangent)
			v.B = Reverse(v.B);
}

//
void SmoothVertexColor(Geometry &geo, const std::vector<uint32_t> &pol_index, const std::vector<VertexToVertex> &vtx_to_vtx) {
	std::vector<Color> out(geo.color.size());

	size_t tt = 0;
	for (auto np = 0; np < geo.pol.size(); ++np) {
		const auto &pol = geo.pol[np];

		for (auto nv = 0; nv < pol.vtx_count; ++nv) {
			const size_t imv = geo.binding[tt + nv], iv = tt + nv;

			out[iv] = geo.color[iv] * 4.f;

			float nrgb = 4.f;
			for (auto nvv = 0; nvv < vtx_to_vtx[imv].vtx_count; ++nvv)
				if (geo.pol[vtx_to_vtx[imv].vtx[nvv].pol_index].material == geo.pol[np].material) {
					out[iv] += geo.color[pol_index[vtx_to_vtx[imv].vtx[nvv].pol_index] + vtx_to_vtx[imv].vtx[nvv].vtx_index];
					nrgb += 1.f;
				}

			out[iv] /= float(nrgb);
		}

		tt += pol.vtx_count;
	}

	geo.color = std::move(out);
}

//
bool Validate(const Geometry &geo) {
	const size_t binding_count = ComputeBindingCount(geo);

	int error_count = 0;

	const auto validation_error = [&](const char *msg) {
		error(msg);

		if (++error_count == 32) {
			error("Too many errors in geometry, aborting validation");
			return false;
		}

		return true;
	};

	if (geo.binding.size() != binding_count)
		if (!validation_error("Invalid polygon vertex index count"))
			return false;

	if (!geo.color.empty() && (geo.color.size() != binding_count))
		if (!validation_error("Invalid vertex color count"))
			return false;

	if (!geo.normal.empty() && (geo.normal.size() != binding_count))
		if (!validation_error("Invalid vertex normal count"))
			return false;

	if (!geo.tangent.empty() && (geo.tangent.size() != binding_count))
		if (!validation_error("Invalid tangent frame count"))
			return false;

	for (auto &uv : geo.uv)
		if (!uv.empty() && (uv.size() != binding_count))
			if (!validation_error("Invalid UV count"))
				return false;

	for (auto &vtx_idx : geo.binding)
		if (vtx_idx >= geo.vtx.size())
			if (!validation_error("Invalid reference to non-existing vertex"))
				return false;

	const auto bone_count = geo.bind_pose.size();

	for (const auto &s : geo.skin)
		for (auto i : s.index)
			if (i >= bone_count)
				if (!validation_error("Invalid reference to non-existing bone"))
					return false;

	return error_count == 0;
}

//
template <typename T> bool ReadStdVector(const Reader &ir, const Handle &h, std::vector<T> &v) {
	const auto size = Read<uint32_t>(ir, h);
	v.resize(size);
	return ir.read(h, v.data(), size * sizeof(T)) == size * sizeof(T);
}

Geometry LoadGeometry(const Reader &ir, const Handle &h, const char *name) {
	Geometry geo;

	if (!ir.is_valid(h)) {
		error(format("Cannot load model '%1', invalid file handle").arg(name));
		return geo;
	}

	if (Read<uint32_t>(ir, h) != HarfangMagic) {
		error(format("Cannot load model '%1', invalid magic marker").arg(name));
		return geo;
	}

	if (Read<uint8_t>(ir, h) != ModelMarker) {
		error(format("Cannot load model '%1', invalid model marker").arg(name));
		return geo;
	}

	/*
		version 0: initial
		version 1: add skin weights & bind pose
		version 2: added arbitrary number of bones
	*/
	const auto version = Read<uint32_t>(ir, h);
	if (version > 2) {
		error(format("Cannot load model '%1', unsupported version").arg(name));
		return geo;
	}

	ReadStdVector(ir, h, geo.vtx);
	ReadStdVector(ir, h, geo.pol);
	ReadStdVector(ir, h, geo.binding);

	ReadStdVector(ir, h, geo.normal);
	ReadStdVector(ir, h, geo.color);

	ReadStdVector(ir, h, geo.tangent);

	for (auto &uv : geo.uv)
		ReadStdVector(ir, h, uv);

	if (version > 0) {
		if (version == 1) {

			// backward compat
			struct Skin_v1 { // 8B
				uint8_t index[4];
				uint8_t weight[4];
			};

			std::vector<Skin_v1> skin1;
			ReadStdVector(ir, h, skin1);
			geo.skin.resize(skin1.size());
			for (size_t i = 0; i < skin1.size(); i++) {
				for (size_t j = 0; j < 4; j++) {
					geo.skin[i].index[j] = skin1[i].index[j];
					geo.skin[i].weight[j] = skin1[i].weight[j];
				}
			}
		} else {
			ReadStdVector(ir, h, geo.skin);
		}
		ReadStdVector(ir, h, geo.bind_pose);
	}
	return geo;
}

Geometry LoadGeometryFromFile(const char *path) { return LoadGeometry(g_file_reader, ScopedReadHandle(g_file_read_provider, path), path); }

template <typename T> void WriteStdVector(const Writer &iw, const Handle &h, const std::vector<T> &v) {
	Write(iw, h, uint32_t(v.size()));
	iw.write(h, v.data(), v.size() * sizeof(T));
}

bool SaveGeometry(const Writer &iw, const Handle &h, const Geometry &geo) {
	if (!iw.is_valid(h))
		return false;

	Write(iw, h, HarfangMagic);
	Write(iw, h, ModelMarker);

	/*
		version 0: initial
		version 1: add skin weights & bind pose
		version 2: added arbitrary number of bones
	*/
	Write<uint32_t>(iw, h, 2); // version

	WriteStdVector(iw, h, geo.vtx);
	WriteStdVector(iw, h, geo.pol);
	WriteStdVector(iw, h, geo.binding);

	WriteStdVector(iw, h, geo.normal);
	WriteStdVector(iw, h, geo.color);

	WriteStdVector(iw, h, geo.tangent);

	for (auto &uv : geo.uv)
		WriteStdVector(iw, h, uv);

	WriteStdVector(iw, h, geo.skin);
	WriteStdVector(iw, h, geo.bind_pose);
	return true;
}

bool SaveGeometryToFile(const char *path, const Geometry &geo) { return SaveGeometry(g_file_writer, ScopedWriteHandle(g_file_write_provider, path), geo); }

//
static Vertex PreparePolygonVertex(const Geometry &geo, size_t i_bind, size_t i_vtp, const std::map<uint16_t, uint16_t>& bone_map) {
	Vertex vtx = {};

	const auto i_vtx = geo.binding[i_bind + i_vtp];
	vtx.pos = geo.vtx[i_vtx];

	if (!geo.normal.empty())
		vtx.normal = geo.normal[i_bind + i_vtp];

	if (!geo.tangent.empty()) {
		vtx.tangent = geo.tangent[i_bind + i_vtp].T;
		vtx.binormal = geo.tangent[i_bind + i_vtp].B;
	}

	if (!geo.color.empty())
		vtx.color0 = geo.color[i_bind + i_vtp];

	for (auto i = 0; i < geo.uv.size(); ++i)
		if (!geo.uv[i].empty())
			*(&vtx.uv0 + i) = geo.uv[i][i_bind + i_vtp];

	if (!geo.skin.empty())
		for (int i = 0; i < 4; ++i) {
			auto bone_idx = geo.skin[i_vtx].index[i];
			auto bone_map_it = bone_map.find(bone_idx);
			__ASSERT__(bone_map_it != bone_map.end());
			vtx.index[i] = hg::numeric_cast<uint8_t>(bone_map_it->second);
			vtx.weight[i] = unpack_float(geo.skin[i_vtx].weight[i]);
		}

	return vtx;
}

static bgfx::VertexLayout GetGeometryVertexDeclaration(const hg::Geometry &geo) {
	bgfx::VertexLayout vs_decl;

	vs_decl.begin();
	vs_decl.add(bgfx::Attrib::Position, 3, bgfx::AttribType::Float);

	if (!geo.normal.empty())
		vs_decl.add(bgfx::Attrib::Normal, 3, bgfx::AttribType::Uint8, true, true);

	if (!geo.tangent.empty()) {
		vs_decl.add(bgfx::Attrib::Tangent, 3, bgfx::AttribType::Uint8, true, true);
		vs_decl.add(bgfx::Attrib::Bitangent, 3, bgfx::AttribType::Uint8, true, true);
	}

	if (!geo.color.empty())
		vs_decl.add(bgfx::Attrib::Color0, 3, bgfx::AttribType::Uint8, true);

	for (auto i = 0; i < geo.uv.size(); ++i)
		if (!geo.uv[i].empty())
			vs_decl.add(bgfx::Attrib::Enum(bgfx::Attrib::TexCoord0 + i), 2, bgfx::AttribType::Float);

	if (!geo.skin.empty()) {
		vs_decl.add(bgfx::Attrib::Indices, 4, bgfx::AttribType::Uint8, true, false);
		vs_decl.add(bgfx::Attrib::Weight, 4, bgfx::AttribType::Uint8, true, false);
	}

	vs_decl.end();
	return vs_decl;
}

uint8_t GetMaterialCount(const Geometry &geo) {
	uint8_t count = 0;
	for (auto &p : geo.pol)
		if (p.material >= count)
			count = p.material + 1;
	return count;
}

static void GeometryToModelBuilder(const Geometry &geo, ModelBuilder &builder) {
	const time_ns t = time_now();

	const uint8_t mat_count = GetMaterialCount(geo);

	for (auto i_mat = 0; i_mat < mat_count; ++i_mat) {
		size_t i_bind = 0;

		std::map<uint16_t, uint16_t> bone_map;
		std::vector<uint16_t> bone_indices_to_add;

		for (auto &pol : geo.pol) {
			if (pol.material == i_mat) {
				if (builder.GetCurrentListIndexCount() + (pol.vtx_count - 2) * 3 > 65535) {
					builder.EndList(i_mat);
					bone_map.clear();
				}

				bool bone_map_ready = false;

				if (!geo.skin.empty()) {
					while (!bone_map_ready) {

						// if the bones on this polygon don't fit our max bones,
						// we'll clear this flag and start again with a new list
						bone_map_ready = true;
						bone_indices_to_add.clear();

						for (auto i_vtp = 0; i_vtp < pol.vtx_count; ++i_vtp) {
							// make sure our bone_map contains those bones

							// the bone limit has to be < 255 as long as we have a uint8_t vtx format for bone indices
							static_assert(
								hg::max_skinned_model_matrix_count <= std::numeric_limits<uint8_t>::max() + 1, "max_skinned_model_matrix_count <= 256");

							const auto i_vtx = geo.binding[i_bind + i_vtp];

							bool need_new_list = false;
							for (int i = 0; i < 4; ++i) {
								auto bone_idx = geo.skin[i_vtx].index[i];
								if (bone_map.find(bone_idx) == bone_map.end()) {
									auto redir_idx = uint16_t(bone_map.size());
									if (redir_idx < hg::max_skinned_model_matrix_count) {
										bone_map[bone_idx] = redir_idx;
										bone_indices_to_add.push_back(bone_idx);
									} else {
										need_new_list = true;
										break;
									}
								}
							}

							if (need_new_list) {
								// too many bones, start a new list
								builder.EndList(i_mat);
								bone_map.clear();
								bone_map_ready = false;
								bone_indices_to_add.clear();
								break;
							}
						}

						for (auto bone_idx : bone_indices_to_add)
							builder.AddBoneIdx(bone_idx);
					}
				}

				for (auto i_vtp = 1; i_vtp < pol.vtx_count - 1; ++i_vtp) {
					const auto a = builder.AddVertex(PreparePolygonVertex(geo, i_bind, 0, bone_map));
					const auto b = builder.AddVertex(PreparePolygonVertex(geo, i_bind, i_vtp + 1, bone_map));
					const auto c = builder.AddVertex(PreparePolygonVertex(geo, i_bind, i_vtp, bone_map));
					builder.AddTriangle(a, b, c);
				}
			}

			i_bind += pol.vtx_count;
		}

		builder.EndList(i_mat);
		bone_map.clear();
	}

	log(format("Geometry to model builder took %1 ms").arg(time_to_ms(time_now() - t)));
}

//
Model GeometryToModel(const hg::Geometry &geo, ModelOptimisationLevel optimisation_level) {
	ModelBuilder builder;
	GeometryToModelBuilder(geo, builder);

	const auto vs_decl = GetGeometryVertexDeclaration(geo);
	Model model = builder.MakeModel(vs_decl, optimisation_level);

	model.bind_pose = geo.bind_pose; // copy bind pose over to model
	return model;
}

bool SaveGeometryModelToFile(const char *path, const hg::Geometry &geo, ModelOptimisationLevel optimisation_level) {
	ScopedFile file(OpenWrite(path));
	if (!file)
		return false;

	Write(file, HarfangMagic);
	Write(file, ModelMarker);

	/*
		version 0: initial
		version 1: add bind poses
		version 2: add indices size
	*/
	const auto version = GetModelBinaryFormatVersion();
	Write<uint8_t>(file, version); // version

	const auto decl = GetGeometryVertexDeclaration(geo);
	Write(file, decl); // write vertex declaration

	ModelBuilder builder;
	GeometryToModelBuilder(geo, builder);

	auto on_end_list = [](const bgfx::VertexLayout &, 
		const MinMax &minmax, const std::vector<uint32_t> &idx32, const std::vector<uint8_t> &vtx, 
		const std::vector<uint16_t> &bones_table, uint16_t mat, void *userdata) {
		const auto &file = *reinterpret_cast<File *>(userdata);

		uint8_t idx_type_size = 2;

		for (auto idx : idx32)
			if (idx > 65535) {
				idx_type_size = 4;
				break;
			}

		Write<uint8_t>(file, idx_type_size); // version 2: indices size in bytes

		size_t idx_size;

		if (idx_type_size == 4) {
			idx_size = idx32.size() * sizeof(uint32_t);
			Write(file, uint32_t(idx_size));
			Write(file, idx32.data(), idx_size); // 32 bit index buffer
		} else {
			std::vector<uint16_t> idx16(idx32.size());
			for (size_t i = 0; i < idx32.size(); ++i)
				idx16[i] = idx32[i];

			idx_size = idx16.size() * sizeof(uint16_t);
			Write(file, uint32_t(idx_size));
			Write(file, idx16.data(), idx_size); // 16 bit index buffer
		}

		const size_t vtx_size = vtx.size();
		Write(file, uint32_t(vtx_size));
		Write(file, vtx.data(), vtx_size); // vertex buffer

		Write(file, uint32_t(bones_table.size()));
		Write(file, bones_table.data(), bones_table.size() * sizeof(bones_table[0])); // bones table

		Write(file, minmax);
		Write(file, mat);

		hg::log(hg::format("Index size: %1, vertex size: %2").arg(idx_size).arg(vtx_size));
	};

	builder.Make(decl, on_end_list, &file.f, optimisation_level);

	Write<uint8_t>(file, 0); // EOLists

	Write(file, numeric_cast<uint32_t>(geo.bind_pose.size())); // version 1: add bind poses
	for (auto &mtx : geo.bind_pose)
		Write(file, mtx);

	return true;
}

} // namespace hg
