/*
 * Copyright (c) 2015, Vsevolod Stakhov
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *	 * Redistributions of source code must retain the above copyright
 *	   notice, this list of conditions and the following disclaimer.
 *	 * Redistributions in binary form must reproduce the above copyright
 *	   notice, this list of conditions and the following disclaimer in the
 *	   documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY AUTHOR ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL AUTHOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#pragma once
#include <string>
#include <vector>
#include <map>
#include <set>
#include <memory>
#include <iostream>
#include <tuple>

#include "ucl.h"

// C++11 API inspired by json11: https://github.com/dropbox/json11/

namespace ucl {

struct ucl_map_construct_t {};
constexpr ucl_map_construct_t ucl_map_construct = ucl_map_construct_t();
struct ucl_array_construct_t {};
constexpr ucl_array_construct_t ucl_array_construct = ucl_array_construct_t();

class Ucl final {
private:
	struct ucl_deleter {
		void operator()(ucl_object_t *obj)
		{
			ucl_object_unref(obj);
		}
	};

	static int
	append_char(unsigned char c, size_t nchars, void *ud)
	{
		std::string *out = reinterpret_cast<std::string *>(ud);

		out->append(nchars, (char) c);

		return nchars;
	}
	static int
	append_len(unsigned const char *str, size_t len, void *ud)
	{
		std::string *out = reinterpret_cast<std::string *>(ud);

		out->append((const char *) str, len);

		return len;
	}
	static int
	append_int(int64_t elt, void *ud)
	{
		std::string *out = reinterpret_cast<std::string *>(ud);
		auto nstr = std::to_string(elt);

		out->append(nstr);

		return nstr.size();
	}
	static int
	append_double(double elt, void *ud)
	{
		std::string *out = reinterpret_cast<std::string *>(ud);
		auto nstr = std::to_string(elt);

		out->append(nstr);

		return nstr.size();
	}

	static struct ucl_emitter_functions default_emit_funcs()
	{
		struct ucl_emitter_functions func = {
			Ucl::append_char,
			Ucl::append_len,
			Ucl::append_int,
			Ucl::append_double,
			nullptr,
			nullptr};

		return func;
	};

	static bool ucl_variable_getter(const unsigned char *data, size_t len,
									unsigned char ** /*replace*/, size_t * /*replace_len*/, bool *need_free, void *ud)
	{
		*need_free = false;

		auto vars = reinterpret_cast<std::set<std::string> *>(ud);
		if (vars && data && len != 0) {
			vars->emplace(data, data + len);
		}
		return false;
	}

	static bool ucl_variable_replacer(const unsigned char *data, size_t len,
									  unsigned char **replace, size_t *replace_len, bool *need_free, void *ud)
	{
		*need_free = false;

		auto replacer = reinterpret_cast<variable_replacer *>(ud);
		if (!replacer) {
			return false;
		}

		std::string var_name(data, data + len);
		if (!replacer->is_variable(var_name)) {
			return false;
		}

		std::string var_value = replacer->replace(var_name);
		if (var_value.empty()) {
			return false;
		}

		*replace = (unsigned char *) UCL_ALLOC(var_value.size());
		memcpy(*replace, var_value.data(), var_value.size());

		*replace_len = var_value.size();
		*need_free = true;

		return true;
	}

	template<typename C, typename P>
	static Ucl parse_with_strategy_function(C config_func, P parse_func, std::string &err)
	{
		auto parser = ucl_parser_new(UCL_PARSER_DEFAULT);

		config_func(parser);

		if (!parse_func(parser)) {
			const char *error = ucl_parser_get_error(parser);//Assigning here without checking result first causes a
			if (error != NULL) err.assign(error);            //	crash if ucl_parser_get_error returns NULL
			ucl_parser_free(parser);

			return nullptr;
		}

		auto obj = ucl_parser_get_object(parser);
		ucl_parser_free(parser);

		// Obj will handle ownership
		return Ucl(obj);
	}

	std::unique_ptr<ucl_object_t, ucl_deleter> obj;

public:
	struct macro_handler_s {
		ucl_macro_handler handler;
		ucl_context_macro_handler ctx_handler;
	};

	struct macro_userdata_s {
		ucl_parser *parser;
		void *userdata;
	};

	class const_iterator {
	private:
		struct ucl_iter_deleter {
			void operator()(ucl_object_iter_t it)
			{
				ucl_object_iterate_free(it);
			}
		};
		std::shared_ptr<void> it;
		std::unique_ptr<Ucl> cur;

	public:
		typedef std::forward_iterator_tag iterator_category;

		const_iterator(const Ucl &obj)
		{
			it = std::shared_ptr<void>(ucl_object_iterate_new(obj.obj.get()),
									   ucl_iter_deleter());
			cur.reset(new Ucl(ucl_object_iterate_safe(it.get(), true)));
			if (!cur->obj) {
				it.reset();
				cur.reset();
			}
		}

		const_iterator()
		{
		}
		const_iterator(const const_iterator &other) = delete;
		const_iterator(const_iterator &&other) = default;
		~const_iterator()
		{
		}

		const_iterator &operator=(const const_iterator &other) = delete;
		const_iterator &operator=(const_iterator &&other) = default;

		bool operator==(const const_iterator &other) const
		{
			if (cur && other.cur) {
				return cur->obj.get() == other.cur->obj.get();
			}

			return !cur && !other.cur;
		}

		bool operator!=(const const_iterator &other) const
		{
			return !(*this == other);
		}

		const_iterator &operator++()
		{
			if (it) {
				cur.reset(new Ucl(ucl_object_iterate_safe(it.get(), true)));
			}

			if (cur && !cur->obj) {
				it.reset();
				cur.reset();
			}

			return *this;
		}

		const Ucl &operator*() const
		{
			return *cur;
		}
		const Ucl *operator->() const
		{
			return cur.get();
		}
	};

	struct variable_replacer {
		virtual ~variable_replacer()
		{
		}

		virtual bool is_variable(const std::string &str) const
		{
			return !str.empty();
		}

		virtual std::string replace(const std::string &var) const = 0;
	};

	// We grab ownership if get non-const ucl_object_t
	Ucl(ucl_object_t *other)
	{
		obj.reset(other);
	}

	// Shared ownership
	Ucl(const ucl_object_t *other)
	{
		obj.reset(ucl_object_ref(other));
	}

	Ucl(const Ucl &other)
	{
		obj.reset(ucl_object_ref(other.obj.get()));
	}

	Ucl(Ucl &&other)
	{
		obj.swap(other.obj);
	}

	Ucl() noexcept
	{
		obj.reset(ucl_object_typed_new(UCL_NULL));
	}
	Ucl(std::nullptr_t) noexcept
	{
		obj.reset(ucl_object_typed_new(UCL_NULL));
	}
	Ucl(double value)
	{
		obj.reset(ucl_object_typed_new(UCL_FLOAT));
		obj->value.dv = value;
	}
	Ucl(int64_t value)
	{
		obj.reset(ucl_object_typed_new(UCL_INT));
		obj->value.iv = value;
	}
	Ucl(bool value)
	{
		obj.reset(ucl_object_typed_new(UCL_BOOLEAN));
		obj->value.iv = static_cast<int64_t>(value);
	}
	Ucl(const std::string &value)
	{
		obj.reset(ucl_object_fromstring_common(value.data(), value.size(),
											   UCL_STRING_RAW));
	}
	Ucl(const char *value)
	{
		obj.reset(ucl_object_fromstring_common(value, 0, UCL_STRING_RAW));
	}

	// Implicit constructor: anything with a to_json() function.
	template<class T, class = decltype(&T::to_ucl)>
	Ucl(const T &t)
		: Ucl(t.to_ucl())
	{
	}

	// Implicit constructor: map-like objects (std::map, std::unordered_map, etc)
	template<class M, typename std::enable_if<
						  std::is_constructible<std::string, typename M::key_type>::value && std::is_constructible<Ucl, typename M::mapped_type>::value,
						  int>::type = 0>
	Ucl(const M &m)
	{
		obj.reset(ucl_object_typed_new(UCL_OBJECT));
		auto cobj = obj.get();

		for (const auto &e: m) {
			ucl_object_insert_key(cobj, ucl_object_ref(e.second.obj.get()),
								  e.first.data(), e.first.size(), true);
		}
	}

	// Implicit constructor: vector-like objects (std::list, std::vector, std::set, etc)
	template<class V, typename std::enable_if<
						  std::is_constructible<Ucl, typename V::value_type>::value,
						  int>::type = 0>
	Ucl(const V &v)
	{
		obj.reset(ucl_object_typed_new(UCL_ARRAY));
		auto cobj = obj.get();

		for (const auto &e: v) {
			ucl_array_append(cobj, ucl_object_ref(e.obj.get()));
		}
	}

	ucl_type_t type() const
	{
		if (obj) {
			return ucl_object_type(obj.get());
		}
		return UCL_NULL;
	}

	std::string key() const
	{
		std::string res;

		if (obj->key) {
			res.assign(obj->key, obj->keylen);
		}

		return res;
	}

	double number_value(const double default_val = 0.0) const
	{
		double res;

		if (ucl_object_todouble_safe(obj.get(), &res)) {
			return res;
		}

		return default_val;
	}

	int64_t int_value(const int64_t default_val = 0) const
	{
		int64_t res;

		if (ucl_object_toint_safe(obj.get(), &res)) {
			return res;
		}

		return default_val;
	}

	bool bool_value(const bool default_val = false) const
	{
		bool res;

		if (ucl_object_toboolean_safe(obj.get(), &res)) {
			return res;
		}

		return default_val;
	}

	std::string string_value(const std::string &default_val = "") const
	{
		const char *res = nullptr;

		if (ucl_object_tostring_safe(obj.get(), &res)) {
			return res;
		}

		return default_val;
	}

	std::string forced_string_value() const
	{
		return ucl_object_tostring_forced(obj.get());
	}

	size_t size() const
	{
		if (type() == UCL_ARRAY) {
			return ucl_array_size(obj.get());
		}

		return 0;
	}

	Ucl at(size_t i) const
	{
		if (type() == UCL_ARRAY) {
			return Ucl(ucl_array_find_index(obj.get(), i));
		}

		return Ucl(nullptr);
	}

	Ucl lookup(const std::string &key) const
	{
		if (type() == UCL_OBJECT) {
			return Ucl(ucl_object_lookup_len(obj.get(),
											 key.data(), key.size()));
		}

		return Ucl(nullptr);
	}

	inline Ucl operator[](size_t i) const
	{
		return at(i);
	}

	inline Ucl operator[](const std::string &key) const
	{
		return lookup(key);
	}
	// Serialize.
	void dump(std::string &out, ucl_emitter_t type = UCL_EMIT_JSON) const
	{
		struct ucl_emitter_functions cbdata;

		cbdata = Ucl::default_emit_funcs();
		cbdata.ud = reinterpret_cast<void *>(&out);

		ucl_object_emit_full(obj.get(), type, &cbdata, nullptr);
	}

	std::string dump(ucl_emitter_t type = UCL_EMIT_JSON) const
	{
		std::string out;

		dump(out, type);

		return out;
	}

	static Ucl parse(const std::string &in, std::string &err, enum ucl_duplicate_strategy duplicate_strategy = UCL_DUPLICATE_APPEND)
	{
		return parse(in, std::map<std::string, std::string>(), err, duplicate_strategy);
	}

	static Ucl parse(const std::string &in, const std::map<std::string, std::string> &vars,
					 std::string &err, enum ucl_duplicate_strategy duplicate_strategy = UCL_DUPLICATE_APPEND)
	{
		std::vector<std::tuple<std::string, macro_handler_s, void *>> emptyVector;
		return parse(in, vars, emptyVector, err, duplicate_strategy);
	}

	//Macro handler will receive a macro_userdata_s as void *ud
	static Ucl parse(const std::string &in,
					 std::vector<std::tuple<std::string /*name*/, macro_handler_s, void * /*userdata*/>> &macros,
					 std::string &err, enum ucl_duplicate_strategy duplicate_strategy = UCL_DUPLICATE_APPEND)
	{
		return parse(in, std::map<std::string, std::string>(), macros, err, duplicate_strategy);
	}

	//Macro handler will receive a macro_userdata_s as void *ud
	static Ucl parse(const std::string &in, const std::map<std::string, std::string> &vars,
					 std::vector<std::tuple<std::string /*name*/, macro_handler_s, void * /*userdata*/>> &macros,
					 std::string &err, enum ucl_duplicate_strategy duplicate_strategy = UCL_DUPLICATE_APPEND)
	{
		//Preserve macro_userdata_s memory for later use in parse_with_strategy_function()
		std::vector<macro_userdata_s> userdata_list;
		userdata_list.reserve(macros.size());
		auto config_func = [&userdata_list, &vars, &macros](ucl_parser *parser) {
			for (const auto &item: vars) {
				ucl_parser_register_variable(parser, item.first.c_str(), item.second.c_str());
			}
			for (auto &macro: macros) {
				userdata_list.push_back({parser, std::get<2>(macro)});
				if (std::get<1>(macro).handler != NULL) {
					ucl_parser_register_macro(parser,
											  std::get<0>(macro).c_str(),
											  std::get<1>(macro).handler,
											  reinterpret_cast<void *>(&userdata_list.back()));
				}
				else if (std::get<1>(macro).ctx_handler != NULL) {
					ucl_parser_register_context_macro(parser,
													  std::get<0>(macro).c_str(),
													  std::get<1>(macro).ctx_handler,
													  reinterpret_cast<void *>(&userdata_list.back()));
				}
			}
		};

		auto parse_func = [&in, &duplicate_strategy](struct ucl_parser *parser) -> bool {
			return ucl_parser_add_chunk_full(parser,
											 (unsigned char *) in.data(),
											 in.size(),
											 (unsigned int) ucl_parser_get_default_priority(parser),
											 duplicate_strategy,
											 UCL_PARSE_UCL);
		};

		return parse_with_strategy_function(config_func, parse_func, err);
	}

	static Ucl parse(const std::string &in, const variable_replacer &replacer,
					 std::string &err, enum ucl_duplicate_strategy duplicate_strategy = UCL_DUPLICATE_APPEND)
	{
		std::vector<std::tuple<std::string, macro_handler_s, void *>> emptyVector;
		return parse(in, replacer, emptyVector, err, duplicate_strategy);
	}

	//Macro handler will receive a macro_userdata_s as void *ud
	static Ucl parse(const std::string &in, const variable_replacer &replacer,
					 std::vector<std::tuple<std::string /*name*/, macro_handler_s, void * /*userdata*/>> &macros,
					 std::string &err, enum ucl_duplicate_strategy duplicate_strategy = UCL_DUPLICATE_APPEND)
	{
		//Preserve macro_userdata_s memory for later use in parse_with_strategy_function()
		std::vector<macro_userdata_s> userdata_list;
		userdata_list.reserve(macros.size());
		auto config_func = [&userdata_list, &replacer, &macros](ucl_parser *parser) {
			ucl_parser_set_variables_handler(parser, ucl_variable_replacer, &const_cast<variable_replacer &>(replacer));
			for (auto &macro: macros) {
				userdata_list.push_back({parser, std::get<2>(macro)});
				if (std::get<1>(macro).handler != NULL) {
					ucl_parser_register_macro(parser,
											  std::get<0>(macro).c_str(),
											  std::get<1>(macro).handler,
											  reinterpret_cast<void *>(&userdata_list.back()));
				}
				else if (std::get<1>(macro).ctx_handler != NULL) {
					ucl_parser_register_context_macro(parser,
													  std::get<0>(macro).c_str(),
													  std::get<1>(macro).ctx_handler,
													  reinterpret_cast<void *>(&userdata_list.back()));
				}
			}
		};

		auto parse_func = [&in, &duplicate_strategy](struct ucl_parser *parser) -> bool {
			return ucl_parser_add_chunk_full(parser,
											 (unsigned char *) in.data(),
											 in.size(),
											 (unsigned int) ucl_parser_get_default_priority(parser),
											 duplicate_strategy,
											 UCL_PARSE_UCL);
		};

		return parse_with_strategy_function(config_func, parse_func, err);
	}

	static Ucl parse(const char *in, std::string &err, enum ucl_duplicate_strategy duplicate_strategy = UCL_DUPLICATE_APPEND)
	{
		return parse(in, std::map<std::string, std::string>(), err, duplicate_strategy);
	}

	static Ucl parse(const char *in, const std::map<std::string, std::string> &vars, std::string &err)
	{
		if (!in) {
			err = "null input";
			return nullptr;
		}
		return parse(std::string(in), vars, err);
	}

	//Macro handler will receive a macro_userdata_s as void *ud
	static Ucl parse(const char *in,
					 std::vector<std::tuple<std::string /*name*/, macro_handler_s, void * /*userdata*/>> &macros,
					 std::string &err, enum ucl_duplicate_strategy duplicate_strategy = UCL_DUPLICATE_APPEND)
	{
		return parse(in, std::map<std::string, std::string>(), macros, err, duplicate_strategy);
	}

	//Macro handler will receive a macro_userdata_s as void *ud
	static Ucl parse(const char *in, const std::map<std::string, std::string> &vars,
					 std::vector<std::tuple<std::string /*name*/, macro_handler_s, void * /*userdata*/>> &macros,
					 std::string &err, enum ucl_duplicate_strategy duplicate_strategy = UCL_DUPLICATE_APPEND)
	{
		if (!in) {
			err = "null input";
			return nullptr;
		}
		return parse(std::string(in), vars, macros, err, duplicate_strategy);
	}

	static Ucl parse(const char *in, const variable_replacer &replacer,
					 std::string &err, enum ucl_duplicate_strategy duplicate_strategy = UCL_DUPLICATE_APPEND)
	{
		if (!in) {
			err = "null input";
			return nullptr;
		}
		return parse(std::string(in), replacer, err, duplicate_strategy);
	}

	//Macro handler will receive a macro_userdata_s as void *ud
	static Ucl parse(const char *in, const variable_replacer &replacer,
					 std::vector<std::tuple<std::string /*name*/, macro_handler_s, void * /*userdata*/>> &macros,
					 std::string &err, enum ucl_duplicate_strategy duplicate_strategy = UCL_DUPLICATE_APPEND)
	{
		if (!in) {
			err = "null input";
			return nullptr;
		}
		return parse(std::string(in), replacer, macros, err, duplicate_strategy);
	}

	static Ucl parse_from_file(const std::string &filename, std::string &err)
	{
		return parse_from_file(filename, std::map<std::string, std::string>(), err);
	}

	static Ucl parse_from_file(const std::string &filename, const std::map<std::string, std::string> &vars, std::string &err)
	{
		auto config_func = [&vars](ucl_parser *parser) {
			for (const auto &item: vars) {
				ucl_parser_register_variable(parser, item.first.c_str(), item.second.c_str());
			}
		};

		auto parse_func = [&filename](ucl_parser *parser) {
			return ucl_parser_add_file(parser, filename.c_str());
		};

		return parse_with_strategy_function(config_func, parse_func, err);
	}

	static Ucl parse_from_file(const std::string &filename, const variable_replacer &replacer, std::string &err)
	{
		auto config_func = [&replacer](ucl_parser *parser) {
			ucl_parser_set_variables_handler(parser, ucl_variable_replacer,
											 &const_cast<variable_replacer &>(replacer));
		};

		auto parse_func = [&filename](ucl_parser *parser) {
			return ucl_parser_add_file(parser, filename.c_str());
		};

		return parse_with_strategy_function(config_func, parse_func, err);
	}

	static std::vector<std::string> find_variable(const std::string &in)
	{
		auto parser = ucl_parser_new(UCL_PARSER_DEFAULT);

		std::set<std::string> vars;
		ucl_parser_set_variables_handler(parser, ucl_variable_getter, &vars);
		ucl_parser_add_chunk(parser, (const unsigned char *) in.data(), in.size());
		ucl_parser_free(parser);

		std::vector<std::string> result;
		std::move(vars.begin(), vars.end(), std::back_inserter(result));
		return result;
	}

	static std::vector<std::string> find_variable(const char *in)
	{
		if (!in) {
			return std::vector<std::string>();
		}
		return find_variable(std::string(in));
	}

	static std::vector<std::string> find_variable_from_file(const std::string &filename)
	{
		auto parser = ucl_parser_new(UCL_PARSER_DEFAULT);

		std::set<std::string> vars;
		ucl_parser_set_variables_handler(parser, ucl_variable_getter, &vars);
		ucl_parser_add_file(parser, filename.c_str());
		ucl_parser_free(parser);

		std::vector<std::string> result;
		std::move(vars.begin(), vars.end(), std::back_inserter(result));
		return result;
	}

	Ucl &operator=(Ucl rhs)
	{
		obj.swap(rhs.obj);
		return *this;
	}

	bool operator==(const Ucl &rhs) const
	{
		return ucl_object_compare(obj.get(), rhs.obj.get()) == 0;
	}
	bool operator<(const Ucl &rhs) const
	{
		return ucl_object_compare(obj.get(), rhs.obj.get()) < 0;
	}
	bool operator!=(const Ucl &rhs) const
	{
		return !(*this == rhs);
	}
	bool operator<=(const Ucl &rhs) const
	{
		return !(rhs < *this);
	}
	bool operator>(const Ucl &rhs) const
	{
		return (rhs < *this);
	}
	bool operator>=(const Ucl &rhs) const
	{
		return !(*this < rhs);
	}

	explicit operator bool() const
	{
		if (!obj || type() == UCL_NULL) {
			return false;
		}

		if (type() == UCL_BOOLEAN) {
			return bool_value();
		}

		return true;
	}

	const_iterator begin() const
	{
		return const_iterator(*this);
	}
	const_iterator cbegin() const
	{
		return const_iterator(*this);
	}
	const_iterator end() const
	{
		return const_iterator();
	}
	const_iterator cend() const
	{
		return const_iterator();
	}
};

};// namespace ucl
