#include <boost/asio/io_context.hpp>
#include <boost/asio/signal_set.hpp>
#include <sdbusplus/asio/connection.hpp>
#include <sdbusplus/asio/object_server.hpp>
#include <sdbusplus/asio/property.hpp>
#include <sdbusplus/bus.hpp>
#include <sdbusplus/unpack_properties.hpp>

#include <iostream>

const std::string demoServiceName = "demo.service";
const std::string demoObjectPath = "/xyz/demo";
const std::string demoInterfaceName = "xyz.demo";
const std::string propertyGrettingName = "Greetings";
const std::string propertyGoodbyesName = "Goodbyes";
const std::string propertyValueName = "Value";

class Application
{
  public:
    Application(sdbusplus::asio::connection& bus,
                sdbusplus::asio::object_server& objServer) :
        bus_(bus),
        objServer_(objServer)
    {
        demo_ = objServer_.add_unique_interface(demoObjectPath,
                                                demoInterfaceName);

        demo_->register_property_r<std::string>(
            propertyGrettingName, sdbusplus::vtable::property_::const_,
            [this](const auto&) { return greetings_; });

        demo_->register_property_rw<std::string>(
            propertyGoodbyesName, sdbusplus::vtable::property_::emits_change,
            [this](const auto& newPropertyValue, const auto&) {
            goodbyes_ = newPropertyValue;
            return true;
        },
            [this](const auto&) { return goodbyes_; });

        demo_->register_property_r<uint32_t>(
            propertyValueName, sdbusplus::vtable::property_::const_,
            [](const auto& value) -> uint32_t { return value; });

        demo_->initialize();
    }

    uint32_t fatalErrors() const
    {
        return fatalErrors_;
    }

    auto logSystemErrorCode(boost::system::error_code ec)
    {
        std::cerr << "Error: " << ec << "\n";
        ++fatalErrors_;
    }

    void logException(const std::exception& e)
    {
        std::cerr << "Error: " << e.what() << "\n";
        ++fatalErrors_;
    }

    void logUnpackError(const sdbusplus::UnpackErrorReason reason,
                        const std::string& property)
    {
        std::cerr << "UnpackError: " << static_cast<int>(reason) << ", "
                  << property << "\n";
        ++fatalErrors_;
    }

    void logExpectedException(
        const sdbusplus::exception::UnpackPropertyError& error)
    {
        std::cout << "As expected " << error.what() << "\n";
    }

    void asyncGetAllPropertiesStringTypeOnly()
    {
        sdbusplus::asio::getAllProperties(
            bus_, demoServiceName, demoObjectPath, demoInterfaceName,
            [this](const boost::system::error_code ec,
                   const std::vector<std::pair<
                       std::string, std::variant<std::monostate, std::string>>>&
                       properties) -> void {
            if (ec)
            {
                logSystemErrorCode(ec);
                return;
            }
            {
                const std::string* greetings = nullptr;
                const std::string* goodbyes = nullptr;
                const bool success = sdbusplus::unpackPropertiesNoThrow(
                    [this](const sdbusplus::UnpackErrorReason reason,
                           const std::string& property) {
                    logUnpackError(reason, property);
                },
                    properties, propertyGrettingName, greetings,
                    propertyGoodbyesName, goodbyes);

                if (success)
                {
                    std::cout << "value of greetings: " << *greetings << "\n";
                    std::cout << "value of goodbyes: " << *goodbyes << "\n";
                }
                else
                {
                    ++fatalErrors_;
                }
            }

            try
            {
                std::string value;
                sdbusplus::unpackProperties(properties, propertyValueName,
                                            value);

                std::cerr << "Error: it should fail because of "
                             "not matched type\n";
                ++fatalErrors_;
            }
            catch (const sdbusplus::exception::UnpackPropertyError& error)
            {
                logExpectedException(error);
            }
        });
    }

    void asyncGetAllProperties()
    {
        sdbusplus::asio::getAllProperties(
            bus_, demoServiceName, demoObjectPath, demoInterfaceName,
            [this](const boost::system::error_code ec,
                   const std::vector<std::pair<
                       std::string,
                       std::variant<std::monostate, std::string, uint32_t>>>&
                       properties) -> void {
            if (ec)
            {
                logSystemErrorCode(ec);
                return;
            }
            try
            {
                std::string greetings;
                std::string goodbyes;
                uint32_t value = 0u;
                sdbusplus::unpackProperties(properties, propertyGrettingName,
                                            greetings, propertyGoodbyesName,
                                            goodbyes, propertyValueName, value);

                std::cout << "value of greetings: " << greetings << "\n";
                std::cout << "value of goodbyes: " << goodbyes << "\n";
                std::cout << "value of value: " << value << "\n";
            }
            catch (const sdbusplus::exception::UnpackPropertyError& error)
            {
                logException(error);
            }

            try
            {
                std::string unknownProperty;
                sdbusplus::unpackProperties(properties, "UnknownPropertyName",
                                            unknownProperty);

                std::cerr << "Error: it should fail because of "
                             "missing property\n";
                ++fatalErrors_;
            }
            catch (const sdbusplus::exception::UnpackPropertyError& error)
            {
                logExpectedException(error);
            }

            try
            {
                uint32_t notMatchingType;
                sdbusplus::unpackProperties(properties, propertyGrettingName,
                                            notMatchingType);

                std::cerr << "Error: it should fail because of "
                             "not matched type\n";
                ++fatalErrors_;
            }
            catch (const sdbusplus::exception::UnpackPropertyError& error)
            {
                logExpectedException(error);
            }
        });
    }

  private:
    sdbusplus::asio::connection& bus_;
    sdbusplus::asio::object_server& objServer_;

    std::unique_ptr<sdbusplus::asio::dbus_interface> demo_;
    std::string greetings_ = "Hello";
    std::string goodbyes_ = "Bye";

    uint32_t fatalErrors_ = 0u;
};

int main(int, char**)
{
    boost::asio::io_context ioc;
    boost::asio::signal_set signals(ioc, SIGINT, SIGTERM);

    signals.async_wait(
        [&ioc](const boost::system::error_code&, const int&) { ioc.stop(); });

    auto bus = std::make_shared<sdbusplus::asio::connection>(ioc);
    auto objServer = std::make_unique<sdbusplus::asio::object_server>(bus);

    bus->request_name(demoServiceName.c_str());

    Application app(*bus, *objServer);

    boost::asio::post(ioc,
                      [&app] { app.asyncGetAllPropertiesStringTypeOnly(); });
    boost::asio::post(ioc, [&app] { app.asyncGetAllProperties(); });

    ioc.run();

    std::cout << "Fatal errors count: " << app.fatalErrors() << "\n";

    return app.fatalErrors();
}