/*
** Copyright (C) 2022-2024 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
**
** This program is free software; you can redistribute it and/or modify it
** under the terms of the GNU General Public License as published by the
** Free Software Foundation; either version 3, or (at your option) any
** later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software Foundation,
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
**
*/


#include "mu-sexp.hh"
#include "mu-utils.hh"

#include <charconv>

using namespace Mu;

/* avoid unbounded recursion (i.e., stack overflow) for pathological input */
constexpr size_t MaxDepth{1024};

template<typename...T> static Mu::Error
parsing_error(size_t pos, fmt::format_string<T...> frm, T&&... args)
{
	const auto&& msg{fmt::format(frm, std::forward<T>(args)...)};
	return Mu::Error(Error::Code::Parsing, "{}: {}", pos, msg);
}

static size_t
skip_whitespace(const std::string& s, size_t pos)
{
	while (pos != s.size() && is_ascii_space(s[pos]))
		++pos;

	return pos;
}

static Result<Sexp> parse(const std::string& expr, size_t& pos, size_t depth);

static Result<Sexp>
parse_list(const std::string& expr, size_t& pos, size_t depth)
{
	if (expr[pos] != '(') // sanity check.
		return Err(parsing_error(pos, "expected: '(' but got '{}'", expr[pos]));

	if (depth >= MaxDepth)
		return Err(parsing_error(pos, "parentheses nested too deeply"));

	Sexp lst{};

	++pos;
	while (pos < expr.size() && expr[pos] != ')') {
		if (auto&& item = parse(expr, pos, depth + 1); item)
			lst.add(std::move(*item));
		else
			return Err(item.error());
	}

	if (pos >= expr.size())
		return Err(parsing_error(pos, "expected: ')'"));
	else if (expr[pos] != ')')
		return Err(parsing_error(pos, "expected: ')' but got '{}'", expr[pos]));

	++pos;
	return Ok(std::move(lst));
}

static Result<Sexp>
parse_string(const std::string& expr, size_t& pos)
{
	if (expr[pos] != '"') // sanity check.
		return Err(parsing_error(pos, "expected: '\"' but got '{}'", expr[pos]));

	bool        escape{};
	std::string str;
	for (++pos; pos != expr.size(); ++pos) {
		auto kar = expr[pos];
		if (escape) {
			// follow the elisp reader: '\n', '\t' are special,
			// any other escaped character stands for itself.
			switch (kar) {
			case 'n': str += '\n'; break;
			case 't': str += '\t'; break;
			default:  str += kar; break;
			}
			escape = false;
		} else if (kar == '\\')
			escape = true;
		else if (kar == '"')
			break;
		else
			str += kar;
	}

	if (escape || pos == expr.size())
		return Err(parsing_error(pos, "unterminated string '{}'", str));

	++pos;
	return Ok(Sexp{std::move(str)});
}


static Result<Sexp>
parse_integer(const std::string& expr, size_t& pos)
{
	if (!is_ascii_digit(expr[pos]) && expr[pos] != '-') // sanity check.
		return Err(parsing_error(pos, "expected: <digit> but got '{}'", expr[pos]));

	auto end{pos + (expr[pos] == '-' ? 1U : 0U)};
	while (end != expr.size() && is_ascii_digit(expr[end]))
		++end;

	Sexp::Number num{};
	const auto res{std::from_chars(expr.data() + pos, expr.data() + end, num)};
	if (res.ec != std::errc{} || res.ptr != expr.data() + end)
		return Err(parsing_error(pos, "invalid number '{}'",
					 expr.substr(pos, end - pos)));

	pos = end;
	return Ok(Sexp{num});
}

static Result<Sexp>
parse_symbol(const std::string& expr, size_t& pos)
{
	if (!is_ascii_alpha(expr[pos]) && expr[pos] != ':') // sanity check.
		return Err(parsing_error(pos, "expected: <alpha>|: but got '{}'", expr[pos]));

	const auto start{pos};
	for (++pos; pos != expr.size() &&
		     (is_ascii_alnum(expr[pos]) || expr[pos] == '-'); ++pos)
		;

	return Ok(Sexp{Sexp::Symbol{expr.substr(start, pos - start)}});
}

static Result<Sexp>
parse(const std::string& expr, size_t& pos, size_t depth)
{
	pos = skip_whitespace(expr, pos);

	if (pos == expr.size())
		return Err(parsing_error(pos, "unexpected end of input"));

	const auto kar  = expr[pos];
	const auto sexp = std::invoke([&]() -> Result<Sexp> {
		if (kar == '(')
			return parse_list(expr, pos, depth);
		else if (kar == '"')
			return parse_string(expr, pos);
		else if (is_ascii_digit(kar) || kar == '-')
			return parse_integer(expr, pos);
		else if (is_ascii_alpha(kar) || kar == ':')
			return parse_symbol(expr, pos);
		else
			return Err(parsing_error(pos, "unexpected character '{}'", kar));
	});

	if (sexp)
		pos = skip_whitespace(expr, pos);

	return sexp;
}

Result<Sexp>
Sexp::parse(const std::string& expr)
{
	size_t pos{};
	auto res = ::parse(expr, pos, 0/*depth*/);
	if (!res)
		return res;
	else if (pos != expr.size())
		return Err(parsing_error(pos, "trailing data starting with '{}'", expr[pos]));
	else
		return res;
}

std::string
Sexp::to_string(Format fopts) const
{
	std::string str;
	to_string_into(str, fopts);

	return str;
}

void
Sexp::to_string_into(std::string& out, Format fopts) const
{
	if (listp()) {
		out += '(';
		bool first{true};
		for(auto&& elm: list()) {
			if (!first)
				out += ' ';
			elm.to_string_into(out, fopts);
			first = false;
		}
		out += ')';
		if (any_of(fopts & Format::SplitList))
			out += '\n';
	} else if (stringp())
		out += quote(string());
	else if (numberp())
		out += std::to_string(number());
	else if (symbolp())
		out += symbol().name;

	if (any_of(fopts & Format::TypeInfo)) {
		out += '<';
		out += Sexp::type_name(type());
		out += '>';
	}
}

// LCOV_EXCL_START

// convert emacs-timestamp (a list) to a unix-tstamp
static uint64_t
unix_tstamp(const Sexp& emacs_tstamp)
{
	if (!emacs_tstamp.listp() || emacs_tstamp.list().size() < 2)
		throw std::runtime_error("unexpected type");

	const auto& lst{emacs_tstamp.list()};
	return (lst.at(0).number() << 16) | lst.at(1).number();
}

std::string
Sexp::to_json_string(Format fopts) const
{
	std::string str;
	to_json_string_into(str, fopts);

	return str;
}

void
Sexp::to_json_string_into(std::string& out, Format fopts) const
{
	const auto sym_name=[&](const std::string& sym) {
		if (any_of(fopts & Format::NoColon) && sym[0] == ':')
			return sym.substr(1); // remove colon
		else
			return sym;
	};

	switch (type()) {
	case Type::List: {
		// property-lists become JSON objects
		if (plistp()) {
			out += '{';
			auto it{list().begin()};
			bool first{true};
			while (it != list().end()) {
				const auto key{it->symbol().name};
				if (!first)
					out += ',';
				out += quote(sym_name(key));
				out += ':';
				++it;
				const auto& propval{*it};
				propval.to_json_string_into(out, fopts);
				++it;
				first = false;
				// special-case: tstamp-fields also get a "unix" value,
				// which are easier to work with than the "emacs" timestamps
				if (key == ":date" || key == ":changed") {
					out += ',';
					out += quote(sym_name(key) + "-unix");
					out += ':';
					out += std::to_string(unix_tstamp(propval));
				}
			}
			out += '}';
			if (any_of(fopts & Format::SplitList))
				out += '\n';
		} else { // other lists become arrays.
			out += '[';
			bool first{true};
			for (auto&& child : list()) {
				if (!first)
					out += ", ";
				child.to_json_string_into(out, fopts);
				first = false;
			}
			out += ']';
			if (any_of(fopts & Format::SplitList))
				out += '\n';
		}
		break;
	}
	case Type::String:
		out += quote(string());
		break;
	case Type::Symbol:
		if (nilp())
			out += "false";
		else if (symbol() == "t")
			out += "true";
		else
			out += quote(symbol().name);
		break;
	case Type::Number:
		out += std::to_string(number());
		break;
	default:
		break;
	}
}


Sexp&
Sexp::del_prop(const std::string& pname)
{
	if (auto kill_it = find_prop(pname, begin(), end()); kill_it != cend())
		list().erase(kill_it, kill_it + 2);
	return *this;
}


Sexp::const_iterator
Sexp::find_prop(const std::string& s,
		Sexp::const_iterator b, Sexp::const_iterator e)  const
{
	for (auto&& it = b; it != e && it+1 != e; it += 2)
		if (it->symbolp() && it->symbol() == s)
			return it;
	return e;
}

Sexp::iterator
Sexp::find_prop(const std::string& s,
		Sexp::iterator b, Sexp::iterator e)
{
	for (auto&& it = b; it != e && it+1 != e; it += 2)
		if (it->symbolp() && it->symbol() == s)
			return it;
	return e;
}


bool
Sexp::plistp(Sexp::const_iterator b, Sexp::const_iterator e) const
{
	if (b == e)
		return true;
	else if (b + 1 == e)
		return false;
	else
		return b->symbolp() &&
			b->symbol().name[0] == ':' &&
			plistp(b + 2, e);
}


// LCOV_EXCL_STOP

#if BUILD_TESTS

#include "mu-test-utils.hh"

static void
test_list()
{
	{
		Sexp s;
		g_assert_true(s.listp());
		g_assert_true(s.to_string() == "()");
		g_assert_true(Sexp::type_name(s.type()) == "list");
		g_assert_true(s.empty());
	}

	{
		Sexp::List items = {
			Sexp("hello"),
			Sexp(123),
			Sexp::Symbol("world")
		};
		const Sexp s{std::move(items)};
		g_assert_false(s.empty());
		g_assert_cmpuint(s.size(),==,3);
		g_assert_true(s.to_string() == "(\"hello\" 123 world)");


		/* copy */
		Sexp s2 = s;
		g_assert_true(s2.to_string() == "(\"hello\" 123 world)");

		/* move */
		Sexp s3 = std::move(s2);
		g_assert_true(s3.to_string() == "(\"hello\" 123 world)");

		s3.clear();
		g_assert_true(s3.empty());
	}

}

static void
test_string()
{
	{
		Sexp s("hello");
		g_assert_true(s.stringp());
		g_assert_true(s.string()=="hello");
		g_assert_true(s.to_string()=="\"hello\"");
		g_assert_true(Sexp::type_name(s.type()) == "string");
	}

	{
		// Sexp s(std::string_view("hel\"lo"));
		// g_assert_true(s.is_string());
		// g_assert_cmpstr(s.string().c_str(),==,"hel\"lo");
		// g_assert_cmpstr(s.to_string().c_str(),==,"\"hel\\\"lo\"");
	}
}

static void
test_number()
{
	{
		Sexp s(123);
		g_assert_true(s.numberp());
		g_assert_cmpint(s.number(),==,123);
		g_assert_true(s.to_string() == "123");
		g_assert_true(Sexp::type_name(s.type()) == "number");
	}

	{
		Sexp s(true);
		g_assert_true(s.numberp());
		g_assert_cmpint(s.number(),==,1);
		g_assert_true(s.to_string()=="1");
	}
}

static void
test_symbol()
{
	{
		Sexp s{Sexp::Symbol("hello")};
		g_assert_true(s.symbolp());
		g_assert_true(s.symbol()=="hello");
		g_assert_true (s.to_string()=="hello");
		g_assert_true(Sexp::type_name(s.type()) == "symbol");
	}

	{
		Sexp s{"hello"_sym};
		g_assert_true(s.symbolp());
		g_assert_true(s.symbol()=="hello");
		g_assert_true (s.to_string()=="hello");
	}

}

static void
test_multi()
{
	Sexp s{"abc", 123, Sexp::Symbol{"def"}};
	g_assert_true(s.to_string() == "(\"abc\" 123 def)");
}


static void
test_add()
{
	{
		Sexp s{"abc", 123};
		s.add("def"_sym);
		g_assert_true(s.to_string() == "(\"abc\" 123 def)");
	}
}

static void
test_add_multi()
{
	{
		Sexp s{"abc", 123};
		s.add("def"_sym, 456, Sexp{"boo", 2});
		g_assert_true(s.to_string() == "(\"abc\" 123 def 456 (\"boo\" 2))");
	}

		{
		Sexp s{"abc", 123};
		Sexp t{"boo", 2};
		s.add("def"_sym, 456, t);
		g_assert_true(s.to_string() == "(\"abc\" 123 def 456 (\"boo\" 2))");
	}

}

static void
test_plist()
{
	Sexp s;
	s.put_props("hello", "world"_sym, "foo", 123, "bar"_sym, "cuux");
	g_assert_true(s.to_string() == R"((hello world foo 123 bar "cuux"))");

	s.put_props("hello", 12345);
	g_assert_true(s.to_string() == R"((foo 123 bar "cuux" hello 12345))");
}


static void
check_parse(const std::string& expr, const std::string& expected)
{
	auto sexp = Sexp::parse(expr);
	assert_valid_result(sexp);
	assert_equal(to_string(*sexp), expected);
}

static void
test_parser()
{
	check_parse(":foo-123", ":foo-123");
	check_parse("foo", "foo");
	check_parse(R"(12345)", "12345");
	check_parse(R"(-12345)", "-12345");
	check_parse(R"((123 bar "cuux"))", "(123 bar \"cuux\")");

	check_parse(R"("foo\"bar\"cuux")", "\"foo\\\"bar\\\"cuux\"");

	check_parse(R"("foo
bar")",
		    "\"foo\nbar\"");

	// escapes: '\n'/'\t' are special; \\ and unknown escapes
	// stand for the escaped character itself
	check_parse(R"("hello\nworld")", "\"hello\nworld\"");
	check_parse(R"("tab\there")", "\"tab\there\"");
	check_parse(R"("back\\slash")", "\"back\\\\slash\"");
	check_parse(R"("what\qever")", "\"whatqever\"");

	// full int64 range
	check_parse("4294967296", "4294967296");
	check_parse("9223372036854775807", "9223372036854775807");
	check_parse("-9223372036854775808", "-9223372036854775808");

	// \r\n is whitespace, too
	check_parse("(foo\r\nbar)", "(foo bar)");
}

static void
test_parser_fail()
{
	g_assert_false(!!Sexp::parse("\""));
	g_assert_false(!!Sexp::parse("123abc"));
	g_assert_false(!!Sexp::parse("("));
	g_assert_false(!!Sexp::parse(")"));
	g_assert_false(!!Sexp::parse("(hello (boo))))"));

	// numbers that don't fit an int64, or aren't numbers at all
	g_assert_false(!!Sexp::parse("99999999999999999999"));
	g_assert_false(!!Sexp::parse("-"));

	// trailing backslash
	g_assert_false(!!Sexp::parse(R"("foo\)"));

	// deeply-nested input gives an error, not a stack overflow
	std::string deep(100000, '(');
	deep += "a";
	deep.append(100000, ')');
	g_assert_false(!!Sexp::parse(deep));

	g_assert_true(Sexp::type_name(static_cast<Sexp::Type>(-1)) == "<error>");
}


int
main(int argc, char* argv[])
try {
	mu_test_init(&argc, &argv);

	g_test_add_func("/sexp/list", test_list);
	g_test_add_func("/sexp/string", test_string);
	g_test_add_func("/sexp/number", test_number);
	g_test_add_func("/sexp/symbol", test_symbol);
	g_test_add_func("/sexp/multi",  test_multi);
	g_test_add_func("/sexp/add",  test_add);
	g_test_add_func("/sexp/add-multi",  test_add_multi);
	g_test_add_func("/sexp/plist",  test_plist);
	g_test_add_func("/sexp/parser", test_parser);
	g_test_add_func("/sexp/parser-fail", test_parser_fail);

	return g_test_run();

} catch (const std::runtime_error& re) {
	mu_printerrln("{}", re.what());
	return 1;
}


#endif /*BUILD_TESTS*/
