/* SPDX-License-Identifier: LGPL-2.1-or-later */
/*
 * Copyright (C) 2022, Google Inc.
 *
 * libcamera YAML parsing helper
 */

#include "libcamera/internal/yaml_parser.h"

#include <charconv>
#include <errno.h>
#include <functional>
#include <limits>
#include <stdlib.h>

#include <libcamera/base/file.h>
#include <libcamera/base/log.h>

#include <yaml.h>

/**
 * \file yaml_parser.h
 * \brief A YAML parser helper
 */

namespace libcamera {

LOG_DEFINE_CATEGORY(YamlParser)

namespace {

/* Empty static YamlObject as a safe result for invalid operations */
static const YamlObject empty;

} /* namespace */

/**
 * \class YamlObject
 * \brief A class representing the tree structure of the YAML content
 *
 * The YamlObject class represents the tree structure of YAML content. A
 * YamlObject can be empty, a dictionary or list of YamlObjects, or a value if a
 * tree leaf.
 */

YamlObject::YamlObject()
	: type_(Type::Empty)
{
}

YamlObject::~YamlObject() = default;

/**
 * \fn YamlObject::isValue()
 * \brief Return whether the YamlObject is a value
 *
 * \return True if the YamlObject is a value, false otherwise
 */

/**
 * \fn YamlObject::isList()
 * \brief Return whether the YamlObject is a list
 *
 * \return True if the YamlObject is a list, false otherwise
 */

/**
 * \fn YamlObject::isDictionary()
 * \brief Return whether the YamlObject is a dictionary
 *
 * \return True if the YamlObject is a dictionary, false otherwise
 */

/**
 * \fn YamlObject::isEmpty()
 * \brief Return whether the YamlObject is an empty
 *
 * \return True if the YamlObject is empty, false otherwise
 */

/**
 * \fn YamlObject::operator bool()
 * \brief Return whether the YamlObject is a non-empty
 *
 * \return False if the YamlObject is empty, true otherwise
 */

/**
 * \fn YamlObject::size()
 * \brief Retrieve the number of elements in a dictionary or list YamlObject
 *
 * This function retrieves the size of the YamlObject, defined as the number of
 * child elements it contains. Only YamlObject instances of Dictionary or List
 * types have a size, calling this function on other types of instances is
 * invalid and results in undefined behaviour.
 *
 * \return The size of the YamlObject
 */
std::size_t YamlObject::size() const
{
	switch (type_) {
	case Type::Dictionary:
	case Type::List:
		return list_.size();
	default:
		return 0;
	}
}

/**
 * \fn template<typename T> YamlObject::get<T>() const
 * \brief Parse the YamlObject as a \a T value
 *
 * This function parses the value of the YamlObject as a \a T object, and
 * returns the value. If parsing fails (usually because the YamlObject doesn't
 * store a \a T value), std::nullopt is returned.
 *
 * \return The YamlObject value, or std::nullopt if parsing failed
 */

/**
 * \fn template<typename T, typename U> YamlObject::get<T>(U &&defaultValue) const
 * \brief Parse the YamlObject as a \a T value
 * \param[in] defaultValue The default value when failing to parse
 *
 * This function parses the value of the YamlObject as a \a T object, and
 * returns the value. If parsing fails (usually because the YamlObject doesn't
 * store a \a T value), the \a defaultValue is returned.
 *
 * \return The YamlObject value, or \a defaultValue if parsing failed
 */

#ifndef __DOXYGEN__

template<>
std::optional<bool>
YamlObject::Getter<bool>::get(const YamlObject &obj) const
{
	if (obj.type_ != Type::Value)
		return std::nullopt;

	if (obj.value_ == "true")
		return true;
	else if (obj.value_ == "false")
		return false;

	return std::nullopt;
}

template<typename T>
struct YamlObject::Getter<T, std::enable_if_t<
	std::is_same_v<int8_t, T> ||
	std::is_same_v<uint8_t, T> ||
	std::is_same_v<int16_t, T> ||
	std::is_same_v<uint16_t, T> ||
	std::is_same_v<int32_t, T> ||
	std::is_same_v<uint32_t, T>>>
{
	std::optional<T> get(const YamlObject &obj) const
	{
		if (obj.type_ != Type::Value)
			return std::nullopt;

		const std::string &str = obj.value_;
		T value;

		auto [ptr, ec] = std::from_chars(str.data(), str.data() + str.size(),
						 value);
		if (ptr != str.data() + str.size() || ec != std::errc())
			return std::nullopt;

		return value;
	}
};

template struct YamlObject::Getter<int8_t>;
template struct YamlObject::Getter<uint8_t>;
template struct YamlObject::Getter<int16_t>;
template struct YamlObject::Getter<uint16_t>;
template struct YamlObject::Getter<int32_t>;
template struct YamlObject::Getter<uint32_t>;

template<>
std::optional<float>
YamlObject::Getter<float>::get(const YamlObject &obj) const
{
	return obj.get<double>();
}

template<>
std::optional<double>
YamlObject::Getter<double>::get(const YamlObject &obj) const
{
	if (obj.type_ != Type::Value)
		return std::nullopt;

	if (obj.value_.empty())
		return std::nullopt;

	char *end;

	errno = 0;
	double value = utils::strtod(obj.value_.c_str(), &end);

	if ('\0' != *end || errno == ERANGE)
		return std::nullopt;

	return value;
}

template<>
std::optional<std::string>
YamlObject::Getter<std::string>::get(const YamlObject &obj) const
{
	if (obj.type_ != Type::Value)
		return std::nullopt;

	return obj.value_;
}

template<>
std::optional<Size>
YamlObject::Getter<Size>::get(const YamlObject &obj) const
{
	if (obj.type_ != Type::List)
		return std::nullopt;

	if (obj.list_.size() != 2)
		return std::nullopt;

	auto width = obj.list_[0].value->get<uint32_t>();
	if (!width)
		return std::nullopt;

	auto height = obj.list_[1].value->get<uint32_t>();
	if (!height)
		return std::nullopt;

	return Size(*width, *height);
}

#endif /* __DOXYGEN__ */

/**
 * \fn template<typename T> YamlObject::getList<T>() const
 * \brief Parse the YamlObject as a list of \a T
 *
 * This function parses the value of the YamlObject as a list of \a T objects,
 * and returns the value as a \a std::vector<T>. If parsing fails, std::nullopt
 * is returned.
 *
 * \return The YamlObject value as a std::vector<T>, or std::nullopt if parsing
 * failed
 */

#ifndef __DOXYGEN__

template<typename T,
	 std::enable_if_t<
		 std::is_same_v<bool, T> ||
		 std::is_same_v<float, T> ||
		 std::is_same_v<double, T> ||
		 std::is_same_v<int8_t, T> ||
		 std::is_same_v<uint8_t, T> ||
		 std::is_same_v<int16_t, T> ||
		 std::is_same_v<uint16_t, T> ||
		 std::is_same_v<int32_t, T> ||
		 std::is_same_v<uint32_t, T> ||
		 std::is_same_v<std::string, T> ||
		 std::is_same_v<Size, T>> *>
std::optional<std::vector<T>> YamlObject::getList() const
{
	if (type_ != Type::List)
		return std::nullopt;

	std::vector<T> values;
	values.reserve(list_.size());

	for (const YamlObject &entry : asList()) {
		const auto value = entry.get<T>();
		if (!value)
			return std::nullopt;
		values.emplace_back(*value);
	}

	return values;
}

template std::optional<std::vector<bool>> YamlObject::getList<bool>() const;
template std::optional<std::vector<float>> YamlObject::getList<float>() const;
template std::optional<std::vector<double>> YamlObject::getList<double>() const;
template std::optional<std::vector<int8_t>> YamlObject::getList<int8_t>() const;
template std::optional<std::vector<uint8_t>> YamlObject::getList<uint8_t>() const;
template std::optional<std::vector<int16_t>> YamlObject::getList<int16_t>() const;
template std::optional<std::vector<uint16_t>> YamlObject::getList<uint16_t>() const;
template std::optional<std::vector<int32_t>> YamlObject::getList<int32_t>() const;
template std::optional<std::vector<uint32_t>> YamlObject::getList<uint32_t>() const;
template std::optional<std::vector<std::string>> YamlObject::getList<std::string>() const;
template std::optional<std::vector<Size>> YamlObject::getList<Size>() const;

#endif /* __DOXYGEN__ */

/**
 * \fn YamlObject::asDict() const
 * \brief Wrap a dictionary YamlObject in an adapter that exposes iterators
 *
 * The YamlObject class doesn't directly implement iterators, as the iterator
 * type depends on whether the object is a Dictionary or List. This function
 * wraps a YamlObject of Dictionary type into an adapter that exposes
 * iterators, as well as begin() and end() functions, allowing usage of
 * range-based for loops with YamlObject. As YAML mappings are not ordered, the
 * iteration order is not specified.
 *
 * The iterator's value_type is a
 * <em>std::pair<const std::string &, const \ref YamlObject &></em>.
 *
 * If the YamlObject is not of Dictionary type, the returned adapter operates
 * as an empty container.
 *
 * \return An adapter of unspecified type compatible with range-based for loops
 */

/**
 * \fn YamlObject::asList() const
 * \brief Wrap a list YamlObject in an adapter that exposes iterators
 *
 * The YamlObject class doesn't directly implement iterators, as the iterator
 * type depends on whether the object is a Dictionary or List. This function
 * wraps a YamlObject of List type into an adapter that exposes iterators, as
 * well as begin() and end() functions, allowing usage of range-based for loops
 * with YamlObject. As YAML lists are ordered, the iteration order is identical
 * to the list order in the YAML data.
 *
 * The iterator's value_type is a <em>const YamlObject &</em>.
 *
 * If the YamlObject is not of List type, the returned adapter operates as an
 * empty container.
 *
 * \return An adapter of unspecified type compatible with range-based for loops
 */

/**
 * \fn YamlObject::operator[](std::size_t index) const
 * \brief Retrieve the element from list YamlObject by index
 *
 * This function retrieves an element of the YamlObject. Only YamlObject
 * instances of List type associate elements with index, calling this function
 * on other types of instances or with an invalid index results in an empty
 * object.
 *
 * \return The YamlObject as an element of the list
 */
const YamlObject &YamlObject::operator[](std::size_t index) const
{
	if (type_ != Type::List || index >= size())
		return empty;

	return *list_[index].value;
}

/**
 * \fn YamlObject::contains()
 * \brief Check if an element of a dictionary exists
 *
 * This function check if the YamlObject contains an element. Only YamlObject
 * instances of Dictionary type associate elements with names, calling this
 * function on other types of instances is invalid and results in undefined
 * behaviour.
 *
 * \return True if an element exists, false otherwise
 */
bool YamlObject::contains(std::string_view key) const
{
	return dictionary_.find(key) != dictionary_.end();
}

/**
 * \fn YamlObject::operator[](std::string_view key) const
 * \brief Retrieve a member by name from the dictionary
 *
 * This function retrieve a member of a YamlObject by name. Only YamlObject
 * instances of Dictionary type associate elements with names, calling this
 * function on other types of instances or with a nonexistent key results in an
 * empty object.
 *
 * \return The YamlObject corresponding to the \a key member
 */
const YamlObject &YamlObject::operator[](std::string_view key) const
{
	if (type_ != Type::Dictionary)
		return empty;

	auto iter = dictionary_.find(key);
	if (iter == dictionary_.end())
		return empty;

	return *iter->second;
}

#ifndef __DOXYGEN__

class YamlParserContext
{
public:
	YamlParserContext();
	~YamlParserContext();

	int init(File &file);
	int parseContent(YamlObject &yamlObject);

private:
	struct EventDeleter {
		void operator()(yaml_event_t *event) const
		{
			yaml_event_delete(event);
			delete event;
		}
	};
	using EventPtr = std::unique_ptr<yaml_event_t, EventDeleter>;

	static int yamlRead(void *data, unsigned char *buffer, size_t size,
			    size_t *sizeRead);

	EventPtr nextEvent();

	void readValue(std::string &value, EventPtr event);
	int parseDictionaryOrList(YamlObject::Type type,
				  const std::function<int(EventPtr event)> &parseItem);
	int parseNextYamlObject(YamlObject &yamlObject, EventPtr event);

	bool parserValid_;
	yaml_parser_t parser_;
};

/**
 * \class YamlParserContext
 * \brief Class for YamlParser parsing and context data
 *
 * The YamlParserContext class stores the internal yaml_parser_t and provides
 * helper functions to do event-based parsing for YAML files.
 */
YamlParserContext::YamlParserContext()
	: parserValid_(false)
{
}

/**
 * \class YamlParserContext
 * \brief Destructor of YamlParserContext
 */
YamlParserContext::~YamlParserContext()
{
	if (parserValid_) {
		yaml_parser_delete(&parser_);
		parserValid_ = false;
	}
}

/**
 * \fn YamlParserContext::init()
 * \brief Initialize a parser with an opened file for parsing
 * \param[in] fh The YAML file to parse
 *
 * Prior to parsing the YAML content, the YamlParserContext must be initialized
 * with a file to create an internal parser. The file needs to stay valid until
 * parsing completes.
 *
 * \return 0 on success or a negative error code otherwise
 * \retval -EINVAL The parser has failed to initialize
 */
int YamlParserContext::init(File &file)
{
	/* yaml_parser_initialize returns 1 when it succeededs */
	if (!yaml_parser_initialize(&parser_)) {
		LOG(YamlParser, Error) << "Failed to initialize YAML parser";
		return -EINVAL;
	}
	parserValid_ = true;
	yaml_parser_set_input(&parser_, &YamlParserContext::yamlRead, &file);

	return 0;
}

int YamlParserContext::yamlRead(void *data, unsigned char *buffer, size_t size,
				size_t *sizeRead)
{
	File *file = static_cast<File *>(data);

	Span<unsigned char> buf{ buffer, size };
	ssize_t ret = file->read(buf);
	if (ret < 0)
		return 0;

	*sizeRead = ret;
	return 1;
}

/**
 * \fn YamlParserContext::nextEvent()
 * \brief Get the next event
 *
 * Get the next event in the current YAML event stream, and return nullptr when
 * there is no more event.
 *
 * \return The next event on success or nullptr otherwise
 */
YamlParserContext::EventPtr YamlParserContext::nextEvent()
{
	EventPtr event(new yaml_event_t);

	/* yaml_parser_parse returns 1 when it succeeds */
	if (!yaml_parser_parse(&parser_, event.get())) {
		File *file = static_cast<File *>(parser_.read_handler_data);

		LOG(YamlParser, Error) << file->fileName() << ":"
				       << parser_.problem_mark.line << ":"
				       << parser_.problem_mark.column << " "
				       << parser_.problem << " "
				       << parser_.context;

		return nullptr;
	}

	return event;
}

/**
 * \fn YamlParserContext::parseContent()
 * \brief Parse the content of a YAML document
 * \param[in] yamlObject The result of YamlObject
 *
 * Check YAML start and end events of a YAML document, and parse the root object
 * of the YAML document into a YamlObject.
 *
 * \return 0 on success or a negative error code otherwise
 * \retval -EINVAL The parser has failed to validate end of a YAML file
 */
int YamlParserContext::parseContent(YamlObject &yamlObject)
{
	/* Check start of the YAML file. */
	EventPtr event = nextEvent();
	if (!event || event->type != YAML_STREAM_START_EVENT)
		return -EINVAL;

	event = nextEvent();
	if (!event || event->type != YAML_DOCUMENT_START_EVENT)
		return -EINVAL;

	/* Parse the root object. */
	event = nextEvent();
	if (parseNextYamlObject(yamlObject, std::move(event)))
		return -EINVAL;

	/* Check end of the YAML file. */
	event = nextEvent();
	if (!event || event->type != YAML_DOCUMENT_END_EVENT)
		return -EINVAL;

	event = nextEvent();
	if (!event || event->type != YAML_STREAM_END_EVENT)
		return -EINVAL;

	return 0;
}

/**
 * \fn YamlParserContext::readValue()
 * \brief Parse event scalar and fill its content into a string
 * \param[in] value The string reference to fill value
 *
 * A helper function to parse a scalar event as string. The caller needs to
 * guarantee the event is of scaler type.
 */
void YamlParserContext::readValue(std::string &value, EventPtr event)
{
	value.assign(reinterpret_cast<char *>(event->data.scalar.value),
		     event->data.scalar.length);
}

/**
 * \fn YamlParserContext::parseDictionaryOrList()
 * \brief A helper function to abstract the common part of parsing dictionary or list
 *
 * \param[in] isDictionary True for parsing a dictionary, and false for a list
 * \param[in] parseItem The callback to handle an item
 *
 * A helper function to abstract parsing an item from a dictionary or a list.
 * The differences of them in a YAML event stream are:
 *
 * 1. The start and end event types are different
 * 2. There is a leading scalar string as key in the items of a dictionary
 *
 * The caller should handle the leading key string in its callback parseItem
 * when it's a dictionary.
 *
 * \return 0 on success or a negative error code otherwise
 * \retval -EINVAL The parser is failed to initialize
 */
int YamlParserContext::parseDictionaryOrList(YamlObject::Type type,
					     const std::function<int(EventPtr event)> &parseItem)
{
	yaml_event_type_t endEventType = YAML_SEQUENCE_END_EVENT;
	if (type == YamlObject::Type::Dictionary)
		endEventType = YAML_MAPPING_END_EVENT;

	/*
	 * Add a safety counter to make sure we don't loop indefinitely in case
	 * the YAML file is malformed.
	 */
	for (unsigned int sentinel = 2000; sentinel; sentinel--) {
		auto evt = nextEvent();
		if (!evt)
			return -EINVAL;

		if (evt->type == endEventType)
			return 0;

		int ret = parseItem(std::move(evt));
		if (ret)
			return ret;
	}

	LOG(YamlParser, Error) << "The YAML file contains a List or Dictionary"
				  " whose size exceeds the parser's limit (1000)";

	return -EINVAL;
}

/**
 * \fn YamlParserContext::parseNextYamlObject()
 * \brief Parse next YAML event and read it as a YamlObject
 * \param[in] yamlObject The result of YamlObject
 * \param[in] event The leading event of the object
 *
 * Parse next YAML object separately as a value, list or dictionary.
 *
 * \return 0 on success or a negative error code otherwise
 * \retval -EINVAL Fail to parse the YAML file.
 */
int YamlParserContext::parseNextYamlObject(YamlObject &yamlObject, EventPtr event)
{
	if (!event)
		return -EINVAL;

	switch (event->type) {
	case YAML_SCALAR_EVENT:
		yamlObject.type_ = YamlObject::Type::Value;
		readValue(yamlObject.value_, std::move(event));
		return 0;

	case YAML_SEQUENCE_START_EVENT: {
		yamlObject.type_ = YamlObject::Type::List;
		auto &list = yamlObject.list_;
		auto handler = [this, &list](EventPtr evt) {
			list.emplace_back(std::string{}, std::make_unique<YamlObject>());
			return parseNextYamlObject(*list.back().value, std::move(evt));
		};
		return parseDictionaryOrList(YamlObject::Type::List, handler);
	}

	case YAML_MAPPING_START_EVENT: {
		yamlObject.type_ = YamlObject::Type::Dictionary;
		auto &list = yamlObject.list_;
		auto handler = [this, &list](EventPtr evtKey) {
			/* Parse key */
			if (evtKey->type != YAML_SCALAR_EVENT) {
				LOG(YamlParser, Error) << "Expect key at line: "
						       << evtKey->start_mark.line
						       << " column: "
						       << evtKey->start_mark.column;
				return -EINVAL;
			}

			std::string key;
			readValue(key, std::move(evtKey));

			/* Parse value */
			EventPtr evtValue = nextEvent();
			if (!evtValue)
				return -EINVAL;

			auto &elem = list.emplace_back(std::move(key),
						       std::make_unique<YamlObject>());
			return parseNextYamlObject(*elem.value, std::move(evtValue));
		};
		int ret = parseDictionaryOrList(YamlObject::Type::Dictionary, handler);
		if (ret)
			return ret;

		auto &dictionary = yamlObject.dictionary_;
		for (const auto &elem : list)
			dictionary.emplace(elem.key, elem.value.get());

		return 0;
	}

	default:
		LOG(YamlParser, Error) << "Invalid YAML file";
		return -EINVAL;
	}
}

#endif /* __DOXYGEN__ */

/**
 * \class YamlParser
 * \brief A helper class for parsing a YAML file
 *
 * The YamlParser class provides an easy interface to parse the contents of a
 * YAML file into a tree of YamlObject instances.
 *
 * Example usage:
 *
 * \code{.unparsed}
 *
 * name:
 * 	"John"
 * numbers:
 * 	- 1
 * 	- 2
 *
 * \endcode
 *
 * The following code illustrates how to parse the above YAML file:
 *
 * \code{.cpp}
 *
 * std::unique_ptr<YamlObject> root = YamlParser::parse(fh);
 * if (!root)
 *   return;
 *
 * if (!root->isDictionary())
 *   return;
 *
 * const YamlObject &name = (*root)["name"];
 * std::cout << name.get<std::string>("") << std::endl;
 *
 * const YamlObject &numbers = (*root)["numbers"];
 * if (!numbers.isList())
 *   return;
 *
 * for (std::size_t i = 0; i < numbers.size(); i++)
 *   std::cout << numbers[i].get<int32_t>(0) << std::endl;
 *
 * \endcode
 *
 * The YamlParser::parse() function takes an open FILE, parses its contents, and
 * returns a pointer to a YamlObject corresponding to the root node of the YAML
 * document.
 *
 * The parser preserves the order of items in the YAML file, for both lists and
 * dictionaries.
 */

/**
 * \brief Parse a YAML file as a YamlObject
 * \param[in] file The YAML file to parse
 *
 * The YamlParser::parse() function takes a file, parses its contents, and
 * returns a pointer to a YamlObject corresponding to the root node of the YAML
 * document.
 *
 * \return Pointer to result YamlObject on success or nullptr otherwise
 */
std::unique_ptr<YamlObject> YamlParser::parse(File &file)
{
	YamlParserContext context;

	if (context.init(file))
		return nullptr;

	std::unique_ptr<YamlObject> root(new YamlObject());

	if (context.parseContent(*root)) {
		LOG(YamlParser, Error)
			<< "Failed to parse YAML content from "
			<< file.fileName();
		return nullptr;
	}

	return root;
}

} /* namespace libcamera */
