#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <errno.h>

#include <poll.h>

#include <sys/stat.h>
#include <fcntl.h>

#include <libudev.h>

#define LOG_MODULE "battery"
#define LOG_ENABLE_DBG 0
#include "../log.h"
#include "../bar/bar.h"
#include "../config.h"
#include "../config-verify.h"
#include "../plugin.h"

enum state { STATE_FULL, STATE_NOTCHARGING, STATE_CHARGING, STATE_DISCHARGING, STATE_UNKNOWN };

struct private {
    struct particle *label;

    int poll_interval;
    char *battery;
    char *manufacturer;
    char *model;
    long energy_full_design;
    long energy_full;
    long charge_full_design;
    long charge_full;

    enum state state;
    long capacity;
    long energy;
    long power;
    long charge;
    long current;
    long time_to_empty;
};

static void
destroy(struct module *mod)
{
    struct private *m = mod->private;
    free(m->battery);
    free(m->manufacturer);
    free(m->model);

    m->label->destroy(m->label);

    free(m);
    module_default_destroy(mod);
}

static const char *
description(const struct module *mod)
{
    static char desc[32];
    const struct private *m = mod->private;
    snprintf(desc, sizeof(desc), "bat(%s)", m->battery);
    return desc;
}

static struct exposable *
content(struct module *mod)
{
    const struct private *m = mod->private;

    mtx_lock(&mod->lock);

    assert(m->state == STATE_FULL ||
           m->state == STATE_NOTCHARGING ||
           m->state == STATE_CHARGING ||
           m->state == STATE_DISCHARGING ||
           m->state == STATE_UNKNOWN);

    unsigned long hours;
    unsigned long minutes;

    if (m->time_to_empty >= 0) {
        hours = m->time_to_empty / 60;
        minutes = m->time_to_empty % 60;
    } else if  (m->energy_full >= 0 && m->charge && m->power >= 0) {
        unsigned long energy = m->state == STATE_CHARGING
            ? m->energy_full - m->energy : m->energy;

        double hours_as_float;
        if (m->state == STATE_FULL || m->state == STATE_NOTCHARGING)
            hours_as_float = 0.0;
        else if (m->power > 0)
            hours_as_float = (double)energy / m->power;
        else
            hours_as_float = 99.0;

        hours = hours_as_float;
        minutes = (hours_as_float - (double)hours) * 60;
    } else if (m->charge_full >= 0 && m->charge >= 0 && m->current >= 0) {
        unsigned long charge = m->state == STATE_CHARGING
            ? m->charge_full - m->charge : m->charge;

        double hours_as_float;
        if (m->state == STATE_FULL || m->state == STATE_NOTCHARGING)
            hours_as_float = 0.0;
        else if (m->current > 0)
            hours_as_float = (double)charge / m->current;
        else
            hours_as_float = 99.0;

        hours = hours_as_float;
        minutes = (hours_as_float - (double)hours) * 60;
    } else {
        hours = 99;
        minutes = 0;
    }

    char estimate[64];
    snprintf(estimate, sizeof(estimate), "%02lu:%02lu", hours, minutes);

    struct tag_set tags = {
        .tags = (struct tag *[]){
            tag_new_string(mod, "name", m->battery),
            tag_new_string(mod, "manufacturer", m->manufacturer),
            tag_new_string(mod, "model", m->model),
            tag_new_string(mod, "state",
                           m->state == STATE_FULL ? "full" :
                           m->state == STATE_NOTCHARGING ? "not charging" :
                           m->state == STATE_CHARGING ? "charging" :
                           m->state == STATE_DISCHARGING ? "discharging" :
                           "unknown"),
            tag_new_int_range(mod, "capacity", m->capacity, 0, 100),
            tag_new_string(mod, "estimate", estimate),
        },
        .count = 6,
    };

    mtx_unlock(&mod->lock);

    struct exposable *exposable = m->label->instantiate(m->label, &tags);

    tag_set_destroy(&tags);
    return exposable;
}

static const char *
readline_from_fd(int fd)
{
    static char buf[4096];

    ssize_t sz = read(fd, buf, sizeof(buf) - 1);
    lseek(fd, 0, SEEK_SET);

    if (sz < 0) {
        LOG_WARN("failed to read from FD=%d", fd);
        return NULL;
    }

    buf[sz] = '\0';
    for (ssize_t i = sz - 1; i >= 0 && buf[i] == '\n'; sz--)
        buf[i] = '\0';

    return buf;
}

static long
readint_from_fd(int fd)
{
    const char *s = readline_from_fd(fd);
    if (s == NULL)
        return 0;

    long ret;
    int r = sscanf(s, "%ld", &ret);
    if (r != 1) {
        LOG_WARN("failed to convert \"%s\" to an integer", s);
        return 0;
    }

    return ret;
}

static bool
initialize(struct private *m)
{
    int pw_fd = open("/sys/class/power_supply", O_RDONLY);
    if (pw_fd < 0) {
        LOG_ERRNO("/sys/class/power_supply");
        return false;
    }

    int base_dir_fd = openat(pw_fd, m->battery, O_RDONLY);
    close(pw_fd);

    if (base_dir_fd < 0) {
        LOG_ERRNO("/sys/class/power_supply/%s", m->battery);
        return false;
    }

    {
        int fd = openat(base_dir_fd, "manufacturer", O_RDONLY);
        if (fd == -1) {
            LOG_WARN("/sys/class/power_supply/%s/manufacturer: %s",
                     m->battery, strerror(errno));
            m->manufacturer = NULL;
        } else {
            m->manufacturer = strdup(readline_from_fd(fd));
            close(fd);
        }
    }

    {
        int fd = openat(base_dir_fd, "model_name", O_RDONLY);
        if (fd == -1) {
            LOG_WARN("/sys/class/power_supply/%s/model_name: %s",
                     m->battery, strerror(errno));
            m->model = NULL;
        } else {
            m->model = strdup(readline_from_fd(fd));
            close(fd);
        }
    }

    if (faccessat(base_dir_fd, "energy_full_design", O_RDONLY, 0) == 0 &&
        faccessat(base_dir_fd, "energy_full", O_RDONLY, 0) == 0)
    {
        {
            int fd = openat(base_dir_fd, "energy_full_design", O_RDONLY);
            if (fd == -1) {
                LOG_ERRNO("/sys/class/power_supply/%s/energy_full_design", m->battery);
                goto err;
            }

            m->energy_full_design = readint_from_fd(fd);
            close(fd);
        }

        {
            int fd = openat(base_dir_fd, "energy_full", O_RDONLY);
            if (fd == -1) {
                LOG_ERRNO("/sys/class/power_supply/%s/energy_full", m->battery);
                goto err;
            }

            m->energy_full = readint_from_fd(fd);
            close(fd);
        }
    } else {
        m->energy_full = m->energy_full_design = -1;
    }

    if (faccessat(base_dir_fd, "charge_full_design", O_RDONLY, 0) == 0 &&
        faccessat(base_dir_fd, "charge_full", O_RDONLY, 0) == 0)
    {
        {
            int fd = openat(base_dir_fd, "charge_full_design", O_RDONLY);
            if (fd == -1) {
                LOG_ERRNO("/sys/class/power_supply/%s/charge_full_design", m->battery);
                goto err;
            }

            m->charge_full_design = readint_from_fd(fd);
            close(fd);
        }

        {
            int fd = openat(base_dir_fd, "charge_full", O_RDONLY);
            if (fd == -1) {
                LOG_ERRNO("/sys/class/power_supply/%s/charge_full", m->battery);
                goto err;
            }

            m->charge_full = readint_from_fd(fd);
            close(fd);
        }
    } else {
        m->charge_full = m->charge_full_design = -1;
    }

    close(base_dir_fd);
    return true;

err:
    close(base_dir_fd);
    return false;
}

static bool
update_status(struct module *mod)
{
    struct private *m = mod->private;

    int pw_fd = open("/sys/class/power_supply", O_RDONLY);
    if (pw_fd < 0) {
        LOG_ERRNO("/sys/class/power_supply");
        return false;
    }

    int base_dir_fd = openat(pw_fd, m->battery, O_RDONLY);
    close(pw_fd);

    if (base_dir_fd < 0) {
        LOG_ERRNO("/sys/class/power_supply/%s", m->battery);
        return false;
    }

    int status_fd = openat(base_dir_fd, "status", O_RDONLY);
    if (status_fd < 0) {
        LOG_ERRNO("/sys/class/power_supply/%s/status", m->battery);
        close(base_dir_fd);
        return false;
    }

    int capacity_fd = openat(base_dir_fd, "capacity", O_RDONLY);
    if (capacity_fd < 0) {
        LOG_ERRNO("/sys/class/power_supply/%s/capacity", m->battery);
        close(status_fd);
        close(base_dir_fd);
        return false;
    }

    int energy_fd = openat(base_dir_fd, "energy_now", O_RDONLY);
    int power_fd = openat(base_dir_fd, "power_now", O_RDONLY);
    int charge_fd = openat(base_dir_fd, "charge_now", O_RDONLY);
    int current_fd = openat(base_dir_fd, "current_now", O_RDONLY);
    int time_to_empty_fd = openat(base_dir_fd, "time_to_empty_now", O_RDONLY);

    long capacity = readint_from_fd(capacity_fd);
    long energy = energy_fd >= 0 ? readint_from_fd(energy_fd) : -1;
    long power = power_fd >= 0 ? readint_from_fd(power_fd) : -1;
    long charge = charge_fd >= 0 ? readint_from_fd(charge_fd) : -1;
    long current = current_fd >= 0 ? readint_from_fd(current_fd) : -1;
    long time_to_empty = time_to_empty_fd >= 0 ? readint_from_fd(time_to_empty_fd) : -1;

    const char *status = readline_from_fd(status_fd);

    if (status_fd >= 0)
        close(status_fd);
    if (capacity_fd >= 0)
        close(capacity_fd);
    if (energy_fd >= 0)
        close(energy_fd);
    if (power_fd >= 0)
        close(power_fd);
    if (charge_fd >= 0)
        close(charge_fd);
    if (current_fd >= 0)
        close(current_fd);
    if (time_to_empty_fd >= 0)
        close(time_to_empty_fd);
    if (base_dir_fd >= 0)
        close(base_dir_fd);

    enum state state;

    if (status == NULL) {
        LOG_WARN("failed to read battery state");
        state = STATE_UNKNOWN;
    } else if (strcmp(status, "Full") == 0)
        state = STATE_FULL;
    else if (strcmp(status, "Not charging") == 0)
        state = STATE_NOTCHARGING;
    else if (strcmp(status, "Charging") == 0)
        state = STATE_CHARGING;
    else if (strcmp(status, "Discharging") == 0)
        state = STATE_DISCHARGING;
    else if (strcmp(status, "Unknown") == 0)
        state = STATE_UNKNOWN;
    else {
        LOG_ERR("unrecognized battery state: %s", status);
        state = STATE_UNKNOWN;
    }

    LOG_DBG("capacity: %ld, energy: %ld, power: %ld, charge=%ld, current=%ld, "
            "time-to-empty: %ld", capacity, energy, power, charge, current,
            time_to_empty);

    mtx_lock(&mod->lock);
    m->state = state;
    m->capacity = capacity;
    m->energy = energy;
    m->power = power;
    m->charge = charge;
    m->current = current;
    m->time_to_empty = time_to_empty;
    mtx_unlock(&mod->lock);
    return true;
}

static int
run(struct module *mod)
{
    const struct bar *bar = mod->bar;
    struct private *m = mod->private;

    if (!initialize(m))
        return -1;

    LOG_INFO("%s: %s %s (at %.1f%% of original capacity)",
             m->battery, m->manufacturer, m->model,
             (m->energy_full > 0
              ? 100.0 * m->energy_full / m->energy_full_design
              : m->charge_full > 0
              ? 100.0 * m->charge_full / m->charge_full_design
              : 0.0));

    int ret = 1;

    struct udev *udev = udev_new();
    struct udev_monitor *mon = udev_monitor_new_from_netlink(udev, "udev");

    if (udev == NULL || mon == NULL)
        goto out;

    udev_monitor_filter_add_match_subsystem_devtype(mon, "power_supply", NULL);
    udev_monitor_enable_receiving(mon);

    if (!update_status(mod))
        goto out;

    bar->refresh(bar);

    while (true) {
        struct pollfd fds[] = {
            {.fd = mod->abort_fd, .events = POLLIN},
            {.fd = udev_monitor_get_fd(mon), .events = POLLIN},
        };
        if (poll(fds, sizeof(fds) / sizeof(fds[0]),
                 m->poll_interval > 0 ? m->poll_interval * 1000 : -1) < 0)
        {
            if (errno == EINTR)
                continue;

            LOG_ERRNO("failed to poll");
            break;
        }

        if (fds[0].revents & POLLIN) {
            ret = 0;
            break;
        }

        if (fds[1].revents & POLLIN) {
            struct udev_device *dev = udev_monitor_receive_device(mon);
            if (dev == NULL)
                continue;

            const char *sysname = udev_device_get_sysname(dev);
            bool is_us = sysname != NULL && strcmp(sysname, m->battery) == 0;
            udev_device_unref(dev);

            if (!is_us)
                continue;
        }

        if (update_status(mod))
            bar->refresh(bar);
    }

out:
    if (mon != NULL)
        udev_monitor_unref(mon);
    if (udev != NULL)
        udev_unref(udev);
    return ret;
}

static struct module *
battery_new(const char *battery, struct particle *label, int poll_interval_secs)
{
    struct private *m = calloc(1, sizeof(*m));
    m->label = label;
    m->poll_interval = poll_interval_secs;
    m->battery = strdup(battery);
    m->state = STATE_UNKNOWN;

    struct module *mod = module_common_new();
    mod->private = m;
    mod->run = &run;
    mod->destroy = &destroy;
    mod->content = &content;
    mod->description = &description;
    return mod;
}

static struct module *
from_conf(const struct yml_node *node, struct conf_inherit inherited)
{
    const struct yml_node *c = yml_get_value(node, "content");
    const struct yml_node *name = yml_get_value(node, "name");
    const struct yml_node *poll_interval = yml_get_value(node, "poll-interval");

    return battery_new(
        yml_value_as_string(name),
        conf_to_particle(c, inherited),
        poll_interval != NULL ? yml_value_as_int(poll_interval) : 60);
}

static bool
verify_conf(keychain_t *chain, const struct yml_node *node)
{
    static const struct attr_info attrs[] = {
        {"name", true, &conf_verify_string},
        {"poll-interval", false, &conf_verify_unsigned},
        MODULE_COMMON_ATTRS,
    };

    return conf_verify_dict(chain, node, attrs);
}

const struct module_iface module_battery_iface = {
    .verify_conf = &verify_conf,
    .from_conf = &from_conf,
};

#if defined(CORE_PLUGINS_AS_SHARED_LIBRARIES)
extern const struct module_iface iface __attribute__((weak, alias("module_battery_iface")));
#endif
