#include "yamlconfigprovider.h"
#include "yamlhelper.h"
#include "utils/scopeguard.h"
#include <experimental/filesystem>
#include <functional>
#include <map>
#include <stdexcept>

namespace fs = std::experimental::filesystem;
using namespace std::string_literals;

namespace myapp::configuration {

struct modbus_visitor_t
{
	explicit modbus_visitor_t(yaml_document_t* doc, config& cfg)
	  : doc_(doc)
	  , cfg_(cfg)
	{
	}

	struct devices_visitor_t
	{
		void operator()(yaml_node_pair_t const& pair)
		{
			auto device_name = get_scalar(doc_, pair.key);
			auto device_connection_string = get_scalar(doc_, pair.value);

			devs_.emplace(std::move(device_name), std::move(device_connection_string));
		}

		void operator()(yaml_node_item_t const&) { throw bad_document("sequence value not allowed"); }
		void operator()(yaml_char_t*, std::size_t) { throw bad_document("scalar value not allowed"); }

		explicit devices_visitor_t(yaml_document_t* doc, config::modbus_devices& devices)
		  : doc_(doc)
		  , devs_(devices)
		{
		}

		yaml_document_t* doc_;
		config::modbus_devices& devs_;
	};

	void operator()(yaml_node_pair_t const& pair)
	{
		auto connection_string = get_scalar(doc_, pair.key);
		config::modbus_devices devices;

		yaml_visit(devices_visitor_t{ doc_, devices }, ::yaml_document_get_node(doc_, pair.value));
		cfg_.modbus.emplace(std::move(connection_string), std::move(devices));
	}

	void operator()(yaml_node_item_t const&) { throw bad_document("sequence value not allowed"); }
	void operator()(yaml_char_t*, std::size_t) { throw bad_document("scalar value not allowed"); }

	yaml_document_t* doc_;
	config& cfg_;
};

struct persistence_visitor_t
{
    explicit persistence_visitor_t(yaml_document_t* doc, config& cfg)
      : doc_(doc)
      , cfg_(cfg)
    {
    }

    void operator()(yaml_node_pair_t const& pair)
    {
        auto key = get_scalar(doc_, pair.key);
        auto value = get_scalar(doc_, pair.value);

        if (key == u8"sqlite"s)
            cfg_.sqlite_persistence_connection_string = std::move(value);
        else if (key == u8"redis"s)
            cfg_.redis_persistence_connection_string = std::move(value);
        else
            throw bad_document("No such known persistence '"s + key + "'"s);
    }

    void operator()(yaml_node_item_t const&) { throw bad_document("sequence value not allowed"); }
    void operator()(yaml_char_t*, std::size_t) { throw bad_document("scalar value not allowed"); }

    yaml_document_t* doc_;
    config& cfg_;
};

struct root_visitor_t
{
	void operator()(yaml_node_pair_t const& pair)
	{
		auto key = get_scalar(doc_, pair.key);

		auto handler = node_handlers_.find(key);
		if (handler == end(node_handlers_))
			throw bad_document("Unexpected node '"s + key + "' at root"s);

		handler->second(::yaml_document_get_node(doc_, pair.value));
	}

	void operator()(yaml_node_item_t const&) { throw bad_document("sequence document not allowed"); }
	void operator()(yaml_char_t*, std::size_t) { throw bad_document("scalar document not allowed"); }

	explicit root_visitor_t(yaml_document_t* doc, config& cfg)
	  : doc_(doc)
	  , cfg_(cfg)
	{
		node_handlers_.emplace(u8"remote_console"s, [this](yaml_node_t* value)
		{
			cfg_.remote_console_unix_socket_path = get_scalar(value);
		});
		node_handlers_.emplace(u8"persistence"s, [this](yaml_node_t* value)
		{
            yaml_visit(persistence_visitor_t{ doc_, cfg_ }, value);
		});
		node_handlers_.emplace(u8"modbus"s, [this](yaml_node_t* value)
		{
			yaml_visit(modbus_visitor_t{ doc_, cfg_ }, value);
		});
	}

	yaml_document_t* doc_;
	config& cfg_;
	std::map<std::string, std::function<void(yaml_node_t*)>> node_handlers_;
};

static config
do_parse(yaml_parser_t* parser)
{
    config cfg{};

    for (auto i = 0 ; ; ++i)
    {
        if (i > 1)
            throw bad_document("multiple documents in config is not allowed");

        auto doc = yaml_resource<yaml_document_t>(
            std::bind(&::yaml_parser_load, parser, std::placeholders::_1),
            &::yaml_document_delete);

        auto root = ::yaml_document_get_root_node(&doc);
        if (root == nullptr)
        {
            if (i == 0)
                throw bad_document("config is empty");

            break;
        }

        yaml_visit(root_visitor_t{ &doc, cfg }, root);
    }

    return cfg;
}

config yaml_config_provider::from_string(std::string const& input)
{
    auto parser = yaml_resource<yaml_parser_t>(
        &::yaml_parser_initialize,
        &::yaml_parser_delete);

    ::yaml_parser_set_encoding(&parser, YAML_UTF8_ENCODING);

    ::yaml_parser_set_input_string(&parser,
        reinterpret_cast<::yaml_char_t const*>(input.c_str()), input.size());

    return do_parse(&parser);
}

config yaml_config_provider::from_file(std::string const& input)
{
    auto file = std::fopen(fs::u8path(input).c_str(), "r");
    if (!file)
        throw std::runtime_error("could not open config file '"s + input + "'"s);

    utils::scope_guard file_deleter([&file] { std::fclose(file); });

    auto parser = yaml_resource<yaml_parser_t>(
        &::yaml_parser_initialize,
        &::yaml_parser_delete);

    ::yaml_parser_set_encoding(&parser, YAML_UTF8_ENCODING);
    ::yaml_parser_set_input_file(&parser, file);

    return do_parse(&parser);
}

// void yaml_config_provider::validate()
// {
//     namespace fs = std::experimental::filesystem;
//     if (!cfg_.remote_console_unix_socket_path.empty() && !fs::u8path(cfg_.remote_console_unix_socket_path).is_absolute())
//         throw std::runtime_error("rcon_socket is not absolute");
// }

} /* namespace myapp::configuration */
