#pragma once
#include "evdev.hpp"

#include <systemd/sd-event.h>

#include <sdbusplus/bus.hpp>

#include <cstdlib>
#include <experimental/filesystem>
#include <string>

namespace phosphor
{
namespace gpio
{
namespace presence
{

static constexpr auto deviceField = 0;
static constexpr auto pathField = 1;
using Device = std::string;
using Path = std::experimental::filesystem::path;
using Driver = std::tuple<Device, Path>;
using Interface = std::string;

/** @class Presence
 *  @brief Responsible for determining and monitoring presence,
 *  by monitoring GPIO state changes, of inventory items and
 *  updating D-Bus accordingly.
 */
class Presence : public Evdev
{
    using Property = std::string;
    using Value = std::variant<bool, std::string>;
    // Association between property and its value
    using PropertyMap = std::map<Property, Value>;
    using Interface = std::string;
    // Association between interface and the D-Bus property
    using InterfaceMap = std::map<Interface, PropertyMap>;
    using Object = sdbusplus::message::object_path;
    // Association between object and the interface
    using ObjectMap = std::map<Object, InterfaceMap>;

  public:
    Presence() = delete;
    ~Presence() = default;
    Presence(const Presence&) = delete;
    Presence& operator=(const Presence&) = delete;
    Presence(Presence&&) = delete;
    Presence& operator=(Presence&&) = delete;

    /** @brief Constructs Presence object.
     *
     *  @param[in] bus       - D-Bus bus Object
     *  @param[in] inventory - Object path under inventory
                               to display this inventory item
     *  @param[in] path      - Device path to read for GPIO pin state
                               to determine presence of inventory item
     *  @param[in] key       - GPIO key to monitor
     *  @param[in] name      - Pretty name of the inventory item
     *  @param[in] event     - sd_event handler
     *  @param[in] drivers   - list of device drivers to bind and unbind
     *  @param[in] ifaces    - list of extra interfaces to associate with the
     *                         inventory item
     *  @param[in] handler   - IO callback handler. Defaults to one in this
     *                        class
     */
    Presence(sdbusplus::bus_t& bus, const std::string& inventory,
             const std::string& path, const unsigned int key,
             const std::string& name, EventPtr& event,
             const std::vector<Driver>& drivers,
             const std::vector<Interface>& ifaces,
             sd_event_io_handler_t handler = Presence::processEvents) :
        Evdev(path, key, event, handler, true),
        bus(bus), inventory(inventory), name(name), drivers(drivers),
        ifaces(ifaces)
    {
        // See if the environment (from configuration file?) has a
        // DRIVER_BIND_DELAY_MS set.
        if (char* envDelay = std::getenv("DRIVER_BIND_DELAY_MS"))
        {
            // DRIVER_BIND_DELAY_MS environment variable is set.
            // Update the bind delay (in milliseconds) to the value from the
            // environment.
            delay = std::strtoull(envDelay, NULL, 10);
        }
        determinePresence();
    }

    /** @brief Callback handler when the FD has some activity on it
     *
     *  @param[in] es       - Populated event source
     *  @param[in] fd       - Associated File descriptor
     *  @param[in] revents  - Type of event
     *  @param[in] userData - User data that was passed during registration
     *
     *  @return             - 0 or positive number on success and negative
     *                        errno otherwise
     */
    static int processEvents(sd_event_source* es, int fd, uint32_t revents,
                             void* userData);

  private:
    /**
     * @brief Update the present property for the inventory item.
     *
     * @param[in] present - What the present property should be set to.
     */
    void updateInventory(bool present);

    /**
     * @brief Construct the inventory object map for the inventory item.
     *
     * @param[in] present - What the present property should be set to.
     *
     * @return The inventory object map to update inventory
     */
    ObjectMap getObjectMap(bool present);

    /** @brief Connection for sdbusplus bus */
    sdbusplus::bus_t& bus;

    /**
     * @brief Read the GPIO device to determine initial presence and set
     *        present property at D-Bus path.
     */
    void determinePresence();

    /** @brief Object path under inventory to display this inventory item */
    const std::string inventory;

    /** @brief Delay in milliseconds from present to bind device driver */
    unsigned int delay = 0;

    /** @brief Pretty name of the inventory item*/
    const std::string name;

    /** @brief Analyzes the GPIO event and update present property*/
    void analyzeEvent();

    /** @brief  Vector of path and device tuples to bind/unbind*/
    const std::vector<Driver> drivers;

    /** @brief  Vector of extra inventory interfaces to associate with the
     *          inventory item
     */
    const std::vector<Interface> ifaces;

    /**
     * @brief Binds or unbinds drivers
     *
     * Called when a presence change is detected to either
     * bind the drivers for the new card or unbind them for
     * the just removed card.  Operates on the drivers vector.
     *
     * Writes <device> to <path>/bind (or unbind)
     *
     * @param present - when true, will bind the drivers
     *                  when false, will unbind them
     */
    void bindOrUnbindDrivers(bool present);
};

/**
 * @brief Get the service name from the mapper for the
 *        interface and path passed in.
 *
 * @param[in] path      - The D-Bus path name
 * @param[in] interface - The D-Bus interface name
 * @param[in] bus       - The D-Bus bus object
 *
 * @return The service name
 */
std::string getService(const std::string& path, const std::string& interface,
                       sdbusplus::bus_t& bus);

} // namespace presence
} // namespace gpio
} // namespace phosphor