#include <core/properties.h>
#include <unordered_set>
#include <pugixml.hpp>
#include <scene_loader.h>
#include <core/fwd.h>
#include <core/logger.h>
#include <core/transform.h>
#include <render/scene.h>
#include "../bsdf/diffuse.h"
#include "../bsdf/roughconductor.h"
#include "../bsdf/roughdielectric.h"
#include "../bsdf/null.h"
#include <render/shape.h>
#include <render/scene.h>
#include "../emitter/area.h"
#include <medium/homogeneous.h>

NAMESPACE_BEGIN(psdr)

template <typename T>
int get_index(const std::vector<T *> &vec, const T *t)
{
	for (int i = 0; i < vec.size(); ++i)
		if (vec[i] == t)
			return i;
	PSDR_ASSERT_MSG(false, "get_index: not found");
	return -1;
}

// parse a vector
template <int length>
Eigen::Matrix<Float, length, 1> parse_vector(const char *str, bool allow_empty = false)
{
	Eigen::Matrix<Float, length, 1> result;
	int tot = 0;

	int i = 0;
	for (;;)
	{
		while (str[i] && strchr(", ", str[i]))
			++i;
		if (!str[i])
			break;

		int j = i + 1;
		while (str[j] && strchr(", ", str[j]) == nullptr)
			++j;

		PSDR_ASSERT(tot < length);
		result[tot++] = static_cast<Float>(atof(str + i));

		i = j;
	}

	if (tot < length)
	{
		if (allow_empty)
		{
			float value = tot ? result[tot - 1] : 0.0f;
			std::fill(result.data() + tot, result.data() + 3, value);
		}
		else
		{
			PSDR_ASSERT_MSG(false, std::string("Vector too short: [") + str + "]");
		}
	}

	return result;
}

// return a child node
inline static pugi::xml_node find_child_by_name(const pugi::xml_node &parent,
												const std::unordered_set<std::string> &names,
												bool allow_empty = false)
{
	PSDR_ASSERT(!names.empty());
	pugi::xml_node result = parent.find_child(
		[&](pugi::xml_node node)
		{
			return names.find(node.attribute("name").value()) != names.end();
		});
	PSDR_ASSERT_MSG(allow_empty || result, std::string("Missing child node: ") + *names.begin());
	return result;
}

static Matrix4x4 load_transform(const pugi::xml_node &parent)
{
	Matrix4x4 result = Matrix4x4::Identity();

	if (parent)
	{
		const char *node_name = parent.attribute("name").value();
		PSDR_ASSERT_MSG(strcmp(node_name, "to_world") == 0 || strcmp(node_name, "toWorld") == 0,
						std::string("Invalid transformation name: ") + node_name);

		for (auto node = parent.first_child(); node; node = node.next_sibling())
		{
			if (strcmp(node.name(), "translate") == 0)
			{
				Vector offset(
					node.attribute("x").as_float(0.0f),
					node.attribute("y").as_float(0.0f),
					node.attribute("z").as_float(0.0f));
				result = transform::translate(offset) * result;
			}
			else if (strcmp(node.name(), "rotate") == 0)
			{
				Vector axis(
					node.attribute("x").as_float(),
					node.attribute("y").as_float(),
					node.attribute("z").as_float());
				float angle = node.attribute("angle").as_float();
				result = transform::rotate(axis, angle) * result;
			}
			else if (strcmp(node.name(), "scale") == 0)
			{
				Vector scl(
					node.attribute("x").as_float(1.0f),
					node.attribute("y").as_float(1.0f),
					node.attribute("z").as_float(1.0f));
				result = transform::scale(scl) * result;
			}
			else if (strcmp(node.name(), "look_at") == 0 || strcmp(node.name(), "lookAt") == 0 ||
					 strcmp(node.name(), "lookat") == 0)
			{
				Vector origin = parse_vector<3>(node.attribute("origin").value()),
					   target = parse_vector<3>(node.attribute("target").value()),
					   up = parse_vector<3>(node.attribute("up").value());
				result = transform::look_at(origin, target, up) * result;
			}
			else if (strcmp(node.name(), "matrix") == 0)
			{
				auto buf = parse_vector<16>(node.attribute("value").value());
				result = Matrix4x4(buf.data()).transpose() * result;
			}
			else
			{
				PSDR_ASSERT_MSG(false, std::string("Unsupported transformation: ") + node.name());
			}
		}
	}

	return result;
}

static int load_sampler(const pugi::xml_node &node)
{
	return node.child("integer").attribute("value").as_int();
}

/*
 * <rfilter type="tent">
 *   <integer name="padding" value="0.5"/>
 * </rfilter>
 * <rfilter type="box"/>
 */
static ReconstructionFilter *load_filter(const pugi::xml_node &node)
{
	auto type = node.attribute("type").value();
	if (strcmp(type, "tent") == 0)
		return new TentFilter(node.child("integer")
								  ? node.child("integer").attribute("value").as_float(0.5f)
								  : 0.5f);
	else if (strcmp(type, "box") == 0)
		return new BoxFilter();
	else if (strcmp(type, "aniso") == 0)
		return new AnisotropicGaussianFilter();
	else
		PSDR_ASSERT_MSG(false, std::string("Unsupported reconstruction filter: ") + type);
}

static Properties load_filter1(const pugi::xml_node &node)
{
	auto type = node.attribute("type").value();
	if (strcmp(type, "tent") == 0)
		return Properties()
			.set<std::string>("type", "tent")
			.set<float>("padding", node.child("float").attribute("value").as_float(0.5f));
	else if (strcmp(type, "box") == 0)
		return Properties()
			.set<std::string>("type", "box");
	else if (strcmp(type, "aniso") == 0)
		return Properties()
			.set<std::string>("type", "aniso");
	else
		PSDR_ASSERT_MSG(false, std::string("Unsupported reconstruction filter: ") + type);
}

/*
 * <film type="hdrfilm">
 *     <integer name="width" value="256"/>
 *     <integer name="height" value="256"/>
 *     <rfilter type="gaussian"/>
 * 	   <integer name="crop_height" value="4" />
 *	   <integer name="crop_width" value="4" />
 *	   <integer name="crop_offset_x" value="16" />
 *	   <integer name="crop_offset_y" value="16" />
 * </film>
 */
static std::tuple<int, int, ReconstructionFilter *> load_film(const pugi::xml_node &node)
{
	pugi::xml_node child;
	child = find_child_by_name(node, {"width"});
	int width = child.attribute("value").as_int();
	child = find_child_by_name(node, {"height"});
	int height = child.attribute("value").as_int();

	/*
	 * <rfilter type="tent"/>
	 * <rfilter type="box"/>
	 */
	ReconstructionFilter *rfilter = node.child("rfilter")
										? load_filter(node.child("rfilter"))
										: new BoxFilter();

	/*
	 * <integer name="crop_height" value="4" />
	 * <integer name="crop_width" value="4" />
	 * <integer name="crop_offset_x" value="16" />
	 * <integer name="crop_offset_y" value="16" />
	 */
	int crop_height = find_child_by_name(node, {"crop_height"}).attribute("value").as_int();
	int crop_width = find_child_by_name(node, {"crop_width"}).attribute("value").as_int();
	int crop_offset_x = find_child_by_name(node, {"crop_offset_x"}).attribute("value").as_int();
	int crop_offset_y = find_child_by_name(node, {"crop_offset_y"}).attribute("value").as_int();

	return {width,
			height,
			rfilter};
}

__attribute__((optnone)) Properties load_film1(const pugi::xml_node &node)
{
	Properties props;
	pugi::xml_node child;
	child = find_child_by_name(node, {"width"});
	int width = child.attribute("value").as_int();
	child = find_child_by_name(node, {"height"});
	int height = child.attribute("value").as_int();
	props
		.set("width", width)
		.set("height", height);
	/*
	 * <rfilter type="tent"/>
	 * <rfilter type="box"/>
	 */

	if (node.child("rfilter"))
		props.set("rfilter", load_filter1(node.child("rfilter")));

	/*
	 * <integer name="crop_height" value="4" />
	 * <integer name="crop_width" value="4" />
	 * <integer name="crop_offset_x" value="16" />
	 * <integer name="crop_offset_y" value="16" />
	 */
	if (find_child_by_name(node, {"crop_height"}, true))
	{
		std::cout << "crop_height" << find_child_by_name(node, {"crop_height"}).attribute("value").as_int() << std::endl;
		props.set("crop_height", find_child_by_name(node, {"crop_height"}).attribute("value").as_int());
	}
	if (find_child_by_name(node, {"crop_width"}, true))
		props.set("crop_width", find_child_by_name(node, {"crop_width"}).attribute("value").as_int());
	if (find_child_by_name(node, {"crop_offset_x"}, true))
		props.set("offset_x", find_child_by_name(node, {"crop_offset_x"}).attribute("value").as_int());
	if (find_child_by_name(node, {"crop_offset_y"}, true))
		props.set("offset_y", find_child_by_name(node, {"crop_offset_y"}).attribute("value").as_int());
	return props;
}

static Spectrum load_rgb(const pugi::xml_node &node)
{
	if (strcmp(node.name(), "float") == 0)
	{
		return Spectrum(node.attribute("value").as_double());
	}
	else if (strcmp(node.name(), "rgb") == 0 || strcmp(node.name(), "spectrum") == 0)
	{
		auto rgb = parse_vector<3>(node.attribute("value").value(), true);
		return Spectrum(rgb[0], rgb[1], rgb[2]);
	}
	else
	{
		PSDR_ASSERT_MSG(false, std::string("Unsupported RGB type: ") + node.name());
	}
}

static std::string parse_bitmap(const pugi::xml_node &node)
{
	const char *texture_type = node.attribute("type").value();
	PSDR_ASSERT_MSG(strcmp(texture_type, "bitmap") == 0, std::string("Unsupported texture type: ") + texture_type);

	const pugi::xml_node &fn_node = node.child("string");
	const char *tmp = fn_node.attribute("name").value(),
			   *file_name = fn_node.attribute("value").value();
	PSDR_ASSERT_MSG(strcmp(tmp, "filename") == 0 && file_name, "Failed to retrieve bitmap filename");
	return std::string(file_name);
}

static void load_texture(const pugi::xml_node &node, Bitmap &bitmap)
{
	if (strcmp(node.name(), "texture") == 0)
	{
		bitmap.load(parse_bitmap(node).c_str());
	}
	else
	{
		bitmap.fill(load_rgb(node));
	}
}

void SceneLoader::load_from_file(const char *file_name, Scene &scene)
{
	pugi::xml_document doc;
	PSDR_ASSERT_MSG(doc.load_file(file_name), "XML parsing failed");
	load_scene(doc, scene);
}

void SceneLoader::load_from_string(const char *scene_xml, Scene &scene)
{
	pugi::xml_document doc;
	PSDR_ASSERT_MSG(doc.load_string(scene_xml), "XML parsing failed");
	load_scene(doc, scene);
}

Scene SceneLoader::load_from_string1(const char *scene_xml)
{
	Scene scene;
	pugi::xml_document doc;
	PSDR_ASSERT_MSG(doc.load_string(scene_xml), "XML parsing failed");
	load_scene(doc, scene);
	return scene;
}

template <typename T>
void build_param_map(Scene::ParamMap &param_map, const std::vector<T *> arr, const char *name)
{
	for (size_t i = 0; i < arr.size(); ++i)
	{
		const T *obj = arr[i];

		std::stringstream oss1;
		oss1 << name << "[" << i << "]";
		param_map.insert(Scene::ParamMap::value_type(oss1.str(), obj));

		if (obj->m_id != "")
		{
			std::stringstream oss2;
			oss2 << name << "[id=" << obj->m_id << "]";

			bool is_new;
			std::tie(std::ignore, is_new) = param_map.insert(Scene::ParamMap::value_type(oss2.str(), obj));
			PSDR_ASSERT_MSG(is_new, std::string("Duplicate id: ") + obj->m_id);
		}
	}
}

// load sensor: <sensor type="perspective">...</sensor>
void SceneLoader::load_sensor(const pugi::xml_node &node, Scene &scene)
{
	Properties props;
	const char *sensor_type = node.attribute("type").value();

	// <film type="hdrfilm">...</film>
	const pugi::xml_node &film_node = node.child("film");

	// <sampler type="independent">...</sampler>
	const pugi::xml_node &sampler_node = node.child("sampler");

	int width, height;
	ReconstructionFilter *filter;
	if (scene.cameras.empty())
	{
		PSDR_ASSERT_MSG(film_node, "Missing film node");
		PSDR_ASSERT_MSG(sampler_node, "Missing sampler node");

		props.merge(load_film1(film_node));
		int spp = load_sampler(sampler_node);
		scene.m_render_options.num_samples = spp;
	}
	else
	{
		PSDR_ASSERT_MSG(!film_node, "Duplicate film node");
		PSDR_ASSERT_MSG(!sampler_node, "Duplicate sampler node");
	}

	// read transform, fov, near_clip, far_clip
	if (strcmp(sensor_type, "perspective") == 0)
	{
		// Perspective camera
		Matrix4x4 to_world = load_transform(node.child("transform"));
		props.set("to_world", to_world);
		float fov_x = find_child_by_name(node, {"fov"}).attribute("value").as_float();
		props.set("fov", fov_x);
		const pugi::xml_node &fov_axis_node = find_child_by_name(node, {"fov_axis", "fovAxis"}, true);
		if (fov_axis_node)
		{
			const char *fov_axis = fov_axis_node.attribute("value").value();
			if (strcmp(fov_axis, "x"))
			{
				PSDR_ASSERT_MSG(false, std::string("Unsupported fov-axis: ") + fov_axis);
			}
			props.set("fov_axis", fov_axis);
		}

		// const pugi::xml_node &near_node = find_child_by_name(node, {"near_clip", "nearClip"}, true);
		// float near = (near_node ? near_node.attribute("value").as_float(0.1f) : 0.1f);

		// const pugi::xml_node &far_node = find_child_by_name(node, {"far_clip", "farClip"}, true);
		// float far = (far_node ? far_node.attribute("value").as_float(1e4f) : 1e4f);

		if (scene.cameras.empty())
			scene.camera = Camera(props);
		scene.cameras.push_back(new Camera(props));
	}
	else
	{
		PSDR_ASSERT_MSG(false, std::string("Unsupported sensor: ") + sensor_type);
	}
}

void SceneLoader::load_scene(const pugi::xml_document &doc, Scene &scene)
{
	PSDR_ASSERT_MSG(scene.m_state == ESInit, "Scene is already loaded");

	const pugi::xml_node &root = doc.child("scene");

	// Load sensors
	for (auto node = root.child("sensor"); node; node = node.next_sibling("sensor"))
	{
		load_sensor(node, scene);
	}

	// Load BSDFs
	for (auto node = root.child("bsdf"); node; node = node.next_sibling("bsdf"))
	{
		load_bsdf(node, scene);
	}

	// // Load (env) emitter
	// for (auto node = root.child("emitter"); node; node = node.next_sibling("emitter"))
	// {
	// 	load_emitter(node, scene);
	// }

	// load medium
	for (auto node = root.child("medium"); node; node = node.next_sibling("medium"))
	{
		load_medium(node, scene);
	}
	// Load shapes
	for (auto node = root.child("shape"); node; node = node.next_sibling("shape"))
	{
		load_shape(node, scene);
	}

	// Build the parameter map
	build_param_map<Shape>(scene.m_param_map, scene.shape_list, "Shape");
	build_param_map<Emitter>(scene.m_param_map, scene.emitter_list, "Emitter");
	scene.m_param_map.insert(Scene::ParamMap::value_type("Camera", &scene.camera));

	scene.m_state = ESLoaded;
}

// TODO load envmap

void SceneLoader::load_bsdf(const pugi::xml_node &node, Scene &scene)
{
	const char *bsdf_id = node.attribute("id").value();
	PSDR_ASSERT_MSG(bsdf_id && strcmp(bsdf_id, ""), "BSDF must have an id");

	BSDF *bsdf = nullptr;
	const char *bsdf_type = node.attribute("type").value();
	if (strcmp(bsdf_type, "diffuse") == 0)
	{
		// Diffuse BSDF
		auto refl_node = find_child_by_name(node, {"reflectance"});
		DiffuseBSDF *diffuse = new DiffuseBSDF();
		load_texture(refl_node, diffuse->reflectance);
		bsdf = diffuse;
	}
	else if (strcmp(bsdf_type, "roughconductor") == 0)
	{
		// roughconductor BSDF
		pugi::xml_node alpha = find_child_by_name(node, {"alpha"});
		pugi::xml_node eta = find_child_by_name(node, {"eta"});
		pugi::xml_node k = find_child_by_name(node, {"k"});
		bsdf = new RoughConductorBSDF(alpha.attribute("value").as_float(),
									  load_rgb(eta),
									  load_rgb(k));
	}
	else if (strcmp(bsdf_type, "roughdielectric") == 0)
	{
		// roughdielectric BSDF
		pugi::xml_node alpha = find_child_by_name(node, {"alpha"});
		pugi::xml_node intIOR = find_child_by_name(node, {"intIOR"});
		pugi::xml_node extIOR = find_child_by_name(node, {"extIOR"});
		pugi::xml_node specularReflectance = find_child_by_name(node, {"specularReflectance"}, true);
		pugi::xml_node specularTransmittance = find_child_by_name(node, {"specularTransmittance"}, true);
		Spectrum sR = specularReflectance ? load_rgb(specularReflectance) : Spectrum(1.0f);
		Spectrum sT = specularTransmittance ? load_rgb(specularTransmittance) : Spectrum(1.0f);
		bsdf = new RoughDielectricBSDF(alpha.attribute("value").as_float(),
									   intIOR.attribute("value").as_float(),
									   extIOR.attribute("value").as_float(),
									   sR, sT);
	}
	else if (strcmp(bsdf_type, "null") == 0)
	{
		bsdf = new NullBSDF();
	}
	else
	{
		PSDR_ASSERT_MSG(false, std::string("Unsupported BSDF: ") + bsdf_type);
	}

	bsdf->m_id = bsdf_id;
	scene.bsdf_list.push_back(bsdf);

	Scene::ParamMap &param_map = scene.m_param_map;

	std::stringstream oss1, oss2;
	oss1 << "BSDF[" << scene.bsdf_list.size() - 1 << "]";
	oss2 << "BSDF[id=" << bsdf_id << "]";
	param_map.insert(Scene::ParamMap::value_type(oss1.str(), bsdf));
	bool is_new;
	std::tie(std::ignore, is_new) = param_map.insert(Scene::ParamMap::value_type(oss2.str(), bsdf));
	PSDR_ASSERT_MSG(is_new, std::string("Duplicate BSDF id: ") + bsdf_id);
}

void SceneLoader::load_ref(const pugi::xml_node &node, Scene &scene, Shape &shape)
{
	for (auto ref_node = node.child("ref"); ref_node; ref_node = ref_node.next_sibling("ref"))
	{
		const char *id = ref_node.attribute("id").value();
		PSDR_ASSERT(id);
		const char *name = ref_node.attribute("name").value();
		if (strcmp(name, "") == 0)
		{
			std::stringstream oss;
			oss << "BSDF[id=" << id << "]";
			auto bsdf_info = scene.m_param_map.find(oss.str());
			PSDR_ASSERT_MSG(bsdf_info != scene.m_param_map.end(), std::string("Unknown BSDF id: ") + id);
			shape.bsdf_id = get_index(scene.bsdf_list,
									  dynamic_cast<const BSDF *>(bsdf_info->second));
		}
		else if (strcmp(name, "interior") == 0)
		{
			std::stringstream oss;
			oss << "Medium[id=" << id << "]";
			auto med_info = scene.m_param_map.find(oss.str());
			PSDR_ASSERT_MSG(med_info != scene.m_param_map.end(), std::string("Unknown Medium id: ") + id);
			shape.med_int_id = get_index(scene.medium_list,
										 dynamic_cast<const Medium *>(med_info->second));
		}
		else if (strcmp(name, "exterior") == 0)
		{
			std::stringstream oss;
			oss << "Medium[id=" << id << "]";
			auto med_info = scene.m_param_map.find(oss.str());
			PSDR_ASSERT_MSG(med_info != scene.m_param_map.end(), std::string("Unknown Medium id: ") + id);
			shape.med_ext_id = get_index(scene.medium_list,
										 dynamic_cast<const Medium *>(med_info->second));
		}
		else
		{
			PSDR_ASSERT_MSG(false, std::string("Unsupported ref: ") + name);
		}
	}
	PSDR_ASSERT_MSG(shape.bsdf_id != -1, "No BSDF");
}

void SceneLoader::load_shape(const pugi::xml_node &node, Scene &scene)
{
	const char *mesh_id = node.attribute("id").value();

	const char *shape_type = node.attribute("type").value();
	Shape *shape = nullptr;
	if (strcmp(shape_type, "obj") == 0)
	{
		const pugi::xml_node &name_node = node.child("string");
		PSDR_ASSERT(strcmp(name_node.attribute("name").value(), "filename") == 0);

		// Load mesh
		const char *file_name = name_node.attribute("value").value();
		shape = new Shape();
		shape->load(file_name);
	}
	else
	{
		PSDR_ASSERT_MSG(false, std::string("Unsupported shape: ") + shape_type);
	}

	// handle ref : BSDF, Medium
	load_ref(node, scene, *shape);

	PSDR_ASSERT_MSG(!node.child("bsdf"), "BSDFs declared under shapes are not supported.");

	// Handle face normals
	bool use_face_normals = true;
	const pugi::xml_node &fn_node = find_child_by_name(node, {"face_normals", "faceNormals"}, true);
	if (fn_node)
		use_face_normals = (strcmp(fn_node.attribute("value").value(), "true") == 0);
	shape->m_use_face_normals = use_face_normals;

	if (mesh_id)
		shape->m_id = mesh_id;

	// Set transform <transform name="toWorld">...</transform>
	shape->m_to_world = load_transform(node.child("transform"));
	scene.shape_list.push_back(shape);

	// Set emitter
	const pugi::xml_node &emitter_node = node.child("emitter");
	if (emitter_node)
	{
		const char *emitter_type = emitter_node.attribute("type").value();
		PSDR_ASSERT_MSG(strcmp(emitter_type, "area") == 0, std::string("Unsupported emitter: ") + emitter_type);

		Emitter *emitter = new AreaLight(get_index(scene.shape_list, shape),
										 load_rgb(find_child_by_name(emitter_node, {"radiance"})));
		scene.emitter_list.push_back(emitter);
		shape->light_id = get_index(scene.emitter_list, emitter);
	}

	//! transform shape vertices
	for (auto &v : shape->vertices)
	{
		auto _v = shape->m_to_world * v.homogeneous();
		v = _v.head<3>() / _v.w();
	}
	shape->configure();
}

int SceneLoader::create_phase(const pugi::xml_node &node, Scene &scene)
{
	// default phase function
	if (!node)
	{
		scene.phase_list.push_back(new IsotropicPhaseFunction());
		return scene.phase_list.size() - 1;
	}

	const char *phase_type = node.attribute("type").value();
	if (strcmp(phase_type, "isotropic") == 0)
	{
		scene.phase_list.push_back(new IsotropicPhaseFunction());
		return scene.phase_list.size() - 1;
	}

	if (strcmp(phase_type, "hg") == 0)
	{
		pugi::xml_node g = find_child_by_name(node, {"g"});
		scene.phase_list.push_back(new HGPhaseFunction(g.attribute("value").as_float()));
		return scene.phase_list.size() - 1;
	}

	PSDR_ASSERT_MSG(false, std::string("Unsupported phase function: ") + phase_type);
	return -1;
}

void SceneLoader::load_medium(const pugi::xml_node &node, Scene &scene)
{
	const char *med_id = node.attribute("id").value();
	PSDR_ASSERT_MSG(med_id && strcmp(med_id, ""), "Medium must have an id");

	Medium *medium = nullptr;
	const char *med_type = node.attribute("type").value();

	if (strcmp(med_type, "homogeneous") == 0)
	{
		int id = create_phase(node.child("phase"), scene);
		medium = new Homogeneous(find_child_by_name(node, {"sigmaT"}).attribute("value").as_float(),
								 load_rgb(find_child_by_name(node, {"albedo"})),
								 id);
		scene.medium_list.push_back(medium);
	}
	else
	{
		PSDR_ASSERT_MSG(false, std::string("Unsupported Medium: ") + med_type);
	}

	Scene::ParamMap &param_map = scene.m_param_map;

	std::stringstream oss1, oss2;
	oss1 << "Medium[" << scene.medium_list.size() - 1 << "]";
	oss2 << "Medium[id=" << med_id << "]";
	param_map.insert(Scene::ParamMap::value_type(oss1.str(), medium));
	bool is_new;
	std::tie(std::ignore, is_new) = param_map.insert(Scene::ParamMap::value_type(oss2.str(), medium));
	DEBUG(param_map[oss2.str()]);
	DEBUG(param_map[oss1.str()]);
	PSDR_ASSERT_MSG(is_new, std::string("Duplicate Medium id: ") + med_id);
}

NAMESPACE_END(psdr)
