#include "headers.hpp"

#include "SimpleXML.hpp"
namespace SimpleXML
{

#if defined(_WIN32) && defined(_MSC_VER) && defined(_DEBUG)
#define STRICT 1
#define LEAN_AND_MEAN
#include <windows.h>
static const char *texts[] = {"ok", "bad argument(s)", "unexpected end of data", "unexpected symbol",
		"tag end mismatch", "depth overflow", "= missed", "' or \" missed"};
static const char *err_text(parseRet ret)
{
	static_assert(static_cast<size_t>(parseRet::COUNT) == _countof(texts), "parseRet mismatch");
	return texts[static_cast<size_t>(ret)];
}
#endif

static const auto bom_utf8 = "\xEF\xBB\xBF"_v;
static const auto xml_header = "<?xml "_v;
static const auto comment_begin = "<!--"_v;
static const auto cdata_begin = "<![CDATA["_v;
static const auto doctype_begin = "<!DOCTYPE"_v;

class ParseException
{
public:
	parseRet ret;
	const char *ps;
	explicit ParseException(parseRet _Ret, const char *_Pos) : ret(_Ret), ps(_Pos) {}
};

class View : public str_view
{
private:
	mutable str_view saved;

public:
	View(const char *_Beg, const char *_End) noexcept : str_view(_Beg, _End), saved(_Beg) {}

	void save_start(const int offset = 0) const noexcept { saved.ps = ps + offset; }
	void save_end(const int offset = 0) const noexcept { saved.pe = ps + offset; }
	const str_view &get_saved() const noexcept { return saved; }
	void move(const int offset) noexcept { ps += offset; }

	static inline bool space(const char c) noexcept
	{
		return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\0';
	}
	static inline bool Name(const char c) noexcept
	{
		return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_';
	}
	static inline bool name(const char c) noexcept
	{
		return Name(c) || (c >= '0' && c <= '9') || c == '-' || c == '.' || c == ':';
	}

	char skip_spaces() noexcept
	{
		while (ps < pe && space(*ps))
			++ps;
		return ps < pe ? *ps : '\0';
	}

	void check_end()
	{
		if (ps >= pe)
			throw ParseException(parseRet::UnexpectedEnd, ps);
	}

	char look_next() const noexcept { return ps < pe ? *ps : '\0'; }
	char get_next()
	{
		check_end();
		return *ps++;
	}
	void ensure_next(const char c)
	{
		if (c != get_next())
			throw ParseException(parseRet::UnexpectedChr, --ps);
	}
	void ensure_next(const str_view &v)
	{
		for (const char c : v)
			ensure_next(c);
	}

	bool is_next(const str_view &v, int offset = 0) const noexcept
	{
		const auto sz = v.size();
		const char *pb = ps + offset;
		//		return pb + sz <= pe && std::equal(pb, pb + sz, v.begin(), v.end());
		return pb + sz <= pe && std::equal(pb, pb + sz, v.begin());
	}

	char skip_name(const int offset = 0)
	{
		ps += offset;
		if (!Name(get_next()))
			throw ParseException(parseRet::UnexpectedChr, --ps);
		while (ps < pe && name(*ps))
			++ps;
		check_end();
		return *ps;
	}

	char find_tag_end()
	{
		char q = '\0';
		while (ps < pe && *ps != '<' && (q || *ps != '>')) {
			if (q) {
				if (q == *ps)
					q = '\0';
			} else {
				if (*ps == '"' || *ps == '\'')
					q = *ps;
			}
			++ps;
		}
		check_end();
		if (*ps != '>')
			throw ParseException(parseRet::UnexpectedChr, ps);
		else {
			++ps;
			return ps[-2];
		}
	}
};

parseRet parse(const char *xml, size_t size, const IParseCallback *cb)
{
	str_view tags[32]{};
	str_view body[_countof(tags)]{};

	const int topmost = static_cast<int>(_countof(tags) - 1);
	int top = 0 - 1;

	View s(xml, xml + size);
	auto res = parseRet::Ok;
#if defined(_WIN32) && defined(_MSC_VER) && defined(_DEBUG)
	bool attr = false;
#endif

	try {
		if (!xml || !size || !cb)
			throw ParseException(parseRet::BadArgs, xml);

		if (s.look_next() == bom_utf8.data()[0])	// [BOM]<?xml ... ?>
			s.ensure_next(bom_utf8);
		s.ensure_next(xml_header);
		if (s.find_tag_end() != '?')
			throw ParseException(parseRet::UnexpectedChr, s.data() - 2);

		for (;;) {
			if (s.get_next() != '<')
				continue;

			auto c = s.get_next();
			if (c == '!') {
				if (s.is_next(comment_begin, -2))	 // <!--...-->
					c = '-';
				else if (s.is_next(cdata_begin, -2))	//  <![CDATA[...]]>
					c = ']';
				else if (s.is_next(doctype_begin, -2)) {	// <!DOCTYPE ... > or \n]]>
					do {
						c = s.get_next();
					} while (c != '>' && c != '\n' && c != '\r');
					if (c != '>') {
						for (;;) {
							do {
								c = s.get_next();
							} while (c == '\n' || c == '\r');
							if (c == ']' && s.look_next() == '>') {
								s.move(+1);
								break;
							}
						}
					}
					continue;
				} else
					throw ParseException(parseRet::UnexpectedChr, s.data());
				s.move(static_cast<int>(c == '-' ? comment_begin.size() : cdata_begin.size()) - 2);
				for (;;) {
					while (s.get_next() != c) {
					}
					if (s.look_next() != c)
						continue;
					s.move(+1);
					if (s.look_next() != '>')
						continue;
					s.move(+1);
					break;
				}
				continue;
			}

			else if (c == '?') {	// <? ... ?>
				if (s.find_tag_end() != '?')
					throw ParseException(parseRet::UnexpectedChr, s.data() - 2);
				continue;
			}

			else if (c == '/') {	// </Tag>
				if (top < 0)
					throw ParseException(parseRet::BadEndTag, s.data() - 2);
				s.save_start();
				body[top].pe = s.data() - 2;
				auto t = s.skip_name();
				if (t != '>')
					throw ParseException(parseRet::UnexpectedChr, s.data());
				s.save_end();
				if (!(tags[top] == s.get_saved()))
					throw ParseException(parseRet::BadEndTag, s.data() - 2 - tags[top].size());
				auto r = cb->OnBody(top, tags, body[top]);
				--top;
				if (r == cbRet::Stop || top < 0)
					break;
				s.move(+1);
				continue;
			}

			else if (!s.Name(c)) {
				throw ParseException(parseRet::UnexpectedChr, s.data() - 1);
			}

			else {	  // <Tag> <Tag/> <Tag ...> <Tag .../>
				s.move(-1);
				if (top >= topmost)
					throw ParseException(parseRet::DepthOvflow, s.data());
				s.save_start();
				auto t = s.skip_name();
				s.save_end();
				tags[++top] = s.get_saved();
				s.save_start();
				s.move(+1);
				bool has_attr = false, simple_tag = false;
				if (t != '>') {
					if (t == '/') {
						s.ensure_next('>');
						simple_tag = true;
					} else if (s.space(t)) {
						has_attr = true;
						simple_tag = s.find_tag_end() == '/';
					} else
						throw ParseException(parseRet::UnexpectedChr, s.data() - 1);
				}
				body[top].ps = body[top].pe = s.data();

				s.save_end(simple_tag ? -2 : -1);
				auto all_attributes = s.get_saved();
				auto r = cb->OnTag(top, tags, all_attributes);
				if (has_attr && r != cbRet::Stop && r != cbRet::ContinueSkipAttr) {
					View a(all_attributes.begin(), all_attributes.end());
#if defined(_WIN32) && defined(_MSC_VER) && defined(_DEBUG)
					attr = true;
#endif
					for (;;) {
						if (!a.skip_spaces())
							break;

						a.save_start();
						auto n = a.skip_name();
						a.save_end();
						str_view attr_name = a.get_saved();

						n = a.skip_spaces();
						if (n != '=')
							throw ParseException(parseRet::EqualMissed, a.data());

						a.move(+1);
						n = a.skip_spaces();
						if (n != '"' && n != '\'')
							throw ParseException(parseRet::QuoteMissed, a.data());

						a.move(+1);
						a.save_start();
						while (a.get_next() != n) {
						}
						a.save_end(-1);
						str_view attr_value = a.get_saved();

						r = cb->OnAttr(top, tags, attr_name, attr_value);
						if (r == cbRet::Stop || r == cbRet::ContinueSkipAttr)
							break;
					}
#if defined(_WIN32) && defined(_MSC_VER) && defined(_DEBUG)
					attr = false;
#endif
				}
				if (r == cbRet::Stop)
					break;
				if (simple_tag) {
					r = cb->OnBody(top, tags, body[top]);
					--top;
					if (r == cbRet::Stop)
						break;
				}
			}
		}
	}

	catch (const ParseException &ex) {
#if defined(_WIN32) && defined(_MSC_VER) && defined(_DEBUG)
		char out[256];
		wsprintfA(out, "SimpleXML parser%s failed: off=%d err=%s\n", attr ? "(attr)" : "", (int)(ex.ps - xml),
				err_text(ex.ret));
		OutputDebugStringA(out);
#endif
		res = ex.ret;
	}

	return res;
}
}	 // namespace SimpleXML
