/* battstat        A MATE battery meter for laptops.
 * Copyright (C) 2000 by Jörgen Pehrson <jp@spektr.eu.org>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 $Id$
 */

/*
 * ACPI battery read-out functions for Linux >= 2.4.12
 * October 2001 by Lennart Poettering <lennart@poettering.de>
 */

#ifdef HAVE_CONFIG_H
    #include <config.h>
#endif

#ifdef __linux__

#include <stdio.h>
#include <apm.h>
#include <glib.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <dirent.h>
#include "acpi-linux.h"

static GHashTable*
read_file (const char* file, char* buf, size_t bufsize)
{
    GHashTable* hash = NULL;

    int fd, len, i;
    char* key;
    char* value;
    gboolean reading_key;

    fd = open (file, O_RDONLY);

    if (fd == -1)
    {
        return hash;
    }

    len = read (fd, buf, bufsize);

    close (fd);

    if (len < 0)
    {
        if (getenv ("BATTSTAT_DEBUG"))
        {
            g_message ("Error reading %s: %s", file, g_strerror (errno));
        }

        return hash;
    }

    hash = g_hash_table_new (g_str_hash, g_str_equal);

    for (i = 0, value = key = buf, reading_key = TRUE; i < len; i++)
    {
        if (buf[i] == ':' && reading_key)
        {
            reading_key = FALSE;
            buf[i] = '\0';
            value = buf + i + 1;
        }
        else if (buf[i] == '\n')
        {
            reading_key = TRUE;
            buf[i] = '\0';
            /* g_message ("Read: %s => %s\n", key, value); */
            g_hash_table_insert (hash, key, g_strstrip (value));
            key = buf + i + 1;
        }
        else if (reading_key)
        {
            /* in acpi 20020214 it switched to lower-case proc
             * entries.  fixing this up here simplifies the
             * code.
             */
            buf[i] = g_ascii_tolower (buf[i]);
        }
    }

    return hash;
}

#if 0
static gboolean
read_bool (GHashTable *hash, const char *key)
{
    char *s;

    g_return_val_if_fail (hash, FALSE);
    g_return_val_if_fail (key, FALSE);

    s = g_hash_table_lookup (hash, key);
    return s && (*s == 'y');
}
#endif

static long
read_long (GHashTable *hash, const char *key)
{
    char* s;

    g_return_val_if_fail (hash, 0);
    g_return_val_if_fail (key, 0);

    s = g_hash_table_lookup (hash, key);
    return s ? strtol (s, NULL, 10) : 0;
}

static gulong
read_ulong (GHashTable *hash, const char *key)
{
  char *s;

    g_return_val_if_fail (hash, 0);
    g_return_val_if_fail (key, 0);

    s = g_hash_table_lookup (hash, key);
    return s ? strtoul (s, NULL, 10) : 0;
}

static const char *
read_string (GHashTable *hash, const char *key)
{
    return g_hash_table_lookup (hash, key);
}

/* Reads the current status of the AC adapter and stores the
 * result in acpiinfo->ac_online. */
static gboolean
update_ac_info (struct acpi_info * acpiinfo)
{
    gchar *ac_state = NULL;
    DIR * procdir;
    struct dirent * procdirentry;
    char buf[BUFSIZ];
    GHashTable *hash;
    gboolean have_adaptor = FALSE;

    acpiinfo->ac_online = FALSE;

    procdir=opendir ("/proc/acpi/ac_adapter/");
    if (!procdir)
        return FALSE;

    while ((procdirentry=readdir (procdir)))
    {
        if (procdirentry->d_name[0]!='.')
        {
            have_adaptor = TRUE;
            ac_state = g_strconcat ("/proc/acpi/ac_adapter/",
                                    procdirentry->d_name,
                                    "/",
                                    acpiinfo->ac_state_state,
                                    NULL);
            hash = read_file (ac_state, buf, sizeof (buf));
            if (hash && !acpiinfo->ac_online)
            {
                const char *s;
                s = read_string (hash, acpiinfo->ac_state_state);
                acpiinfo->ac_online = s ? (strcmp (s, "on-line") == 0) : 0;
                g_hash_table_destroy (hash);
           }
           g_free (ac_state);
        }
    }

  /* If there are no AC adaptors registered in the system, then we're
     probably on a desktop (and therefore, on AC power).
   */
    if (have_adaptor == FALSE)
        acpiinfo->ac_online = 1;

    closedir (procdir);

    return TRUE;
}

/* Reads the ACPI info for the system batteries, and finds
 * the total capacity, which is stored in acpiinfo. */
static gboolean
update_battery_info (struct acpi_info* acpiinfo)
{
    gchar* batt_info = NULL;
    GHashTable* hash;
    DIR* procdir;
    struct dirent* procdirentry;
    char buf[BUFSIZ];

    acpiinfo->max_capacity = 0;
    acpiinfo->low_capacity = 0;
    acpiinfo->critical_capacity = 0;

    procdir = opendir ("/proc/acpi/battery/");

    if (!procdir)
    {
        return FALSE;
    }

    while ((procdirentry = readdir (procdir)))
    {
        if (procdirentry->d_name[0] != '.')
        {
            batt_info = g_strconcat ("/proc/acpi/battery/", procdirentry->d_name, "/info", NULL);
            hash = read_file (batt_info, buf, sizeof (buf));

            if (hash)
            {
                acpiinfo->max_capacity += read_long (hash, "last full capacity");
                acpiinfo->low_capacity += read_long (hash, "design capacity warning");
                acpiinfo->critical_capacity += read_long (hash, "design capacity low");

                g_hash_table_destroy (hash);
            }
            g_free (batt_info);
        }
    }

    closedir (procdir);

    return TRUE;
}

/* Initializes the ACPI-reading subsystem by opening a file
 * descriptor to the ACPI event file.  This can either be the
 * /proc/acpi/event exported by the kernel, or if it's already
 * in use, the /var/run/acpid.socket maintained by acpid.  Also
 * initializes the stored battery and AC adapter information. */
gboolean
acpi_linux_init (struct acpi_info * acpiinfo)
{
    GHashTable *hash;
    char buf[BUFSIZ];
    gchar *pbuf;
    gulong acpi_ver;
    int fd;

    g_assert (acpiinfo);

    if (g_file_get_contents ("/sys/module/acpi/parameters/acpica_version", &pbuf, NULL, NULL)) {
        acpi_ver = strtoul (pbuf, NULL, 10);
        g_free (pbuf);
    } else if ((hash = read_file ("/proc/acpi/info", buf, sizeof (buf)))) {
        acpi_ver = read_ulong (hash, "version");
        g_hash_table_destroy (hash);
    } else
        return FALSE;

    if (acpi_ver < (gulong)20020208) {
        acpiinfo->ac_state_state = "status";
        acpiinfo->batt_state_state = "status";
        acpiinfo->charging_state = "state";
    } else {
        acpiinfo->ac_state_state = "state";
        acpiinfo->batt_state_state = "state";
        acpiinfo->charging_state = "charging state";
    }

    if (!update_battery_info (acpiinfo) || !update_ac_info (acpiinfo))
        return FALSE;

    fd = open ("/proc/acpi/event", 0);
    if (fd >= 0) {
        acpiinfo->event_fd = fd;
        acpiinfo->channel = g_io_channel_unix_new (fd);
        return TRUE;
    }

    fd = socket (PF_UNIX, SOCK_STREAM, 0);
    if (fd >= 0) {
        struct sockaddr_un addr;
        addr.sun_family = AF_UNIX;
        strcpy (addr.sun_path, "/var/run/acpid.socket");
        if (connect (fd, (struct sockaddr *) &addr, sizeof (addr)) == 0) {
            acpiinfo->event_fd = fd;
            acpiinfo->channel = g_io_channel_unix_new (fd);
            return TRUE;
        }
    }

    close (fd);
    acpiinfo->event_fd = -1;
    return FALSE;
}

/* Cleans up ACPI */
void
acpi_linux_cleanup (struct acpi_info * acpiinfo)
{
    g_assert (acpiinfo);

    if (acpiinfo->event_fd >= 0) {
        g_io_channel_unref (acpiinfo->channel);
        close (acpiinfo->event_fd);
        acpiinfo->event_fd = -1;
    }
}

#define ACPI_EVENT_IGNORE       0
#define ACPI_EVENT_DONE         1
#define ACPI_EVENT_AC           2
#define ACPI_EVENT_BATTERY_INFO 3

/* Given a string event from the ACPI system, returns the type
 * of event if we're interested in it.  str is updated to point
 * to the next event. */
static int
parse_acpi_event (GString *buffer)
{

    if (strstr (buffer->str, "ac_adapter"))
        return ACPI_EVENT_AC;
    if (strstr (buffer->str, "battery"))
        return ACPI_EVENT_BATTERY_INFO;

    return ACPI_EVENT_IGNORE;
}

/* Handles a new ACPI event by reading it from the event file
 * and calling any handlers.  Since this does a blocking read,
 * it should only be called when there is a new event as indicated
 * by select (). */
gboolean acpi_process_event (struct acpi_info * acpiinfo)
{
    gsize i;
    int evt;
    GString *buffer;
    GError *gerror=NULL;
    buffer=g_string_new (NULL);
    g_io_channel_read_line_string (acpiinfo->channel,
                                   buffer,
                                   &i,
                                   &gerror);

    if (gerror != NULL) {
        g_warning ("%s", gerror->message);
        g_error_free (gerror);
    }

    gboolean result;

    evt = parse_acpi_event (buffer);
    switch (evt) {
        case ACPI_EVENT_AC:
            result = update_ac_info (acpiinfo);
            break;
        case ACPI_EVENT_BATTERY_INFO:
            if (update_battery_info (acpiinfo)) {
                /* Update AC info on battery info updates.  This works around
                 * a bug in ACPI (as per bug #163013).
                 */
                result = update_ac_info (acpiinfo);
                break;
            }
        /* fall-through */
        default:
            result = FALSE;
    }

    g_string_free (buffer, FALSE);
    return result;
}

/*
 * Fills out a classic apm_info structure with the data gathered from
 * the ACPI kernel interface in /proc
 */
gboolean
acpi_linux_read (struct apm_info *apminfo, struct acpi_info * acpiinfo)
{
    guint32 remain;
    guint32 rate;
    gboolean charging;
    GHashTable *hash;
    gchar* batt_state = NULL;
    DIR * procdir;
    struct dirent * procdirentry;
    char buf[BUFSIZ];

    g_assert (acpiinfo);

    /*
     * apminfo.ac_line_status must be one when on ac power
     * apminfo.battery_status must be 0 for high, 1 for low, 2 for critical, 3 for charging
     * apminfo.battery_percentage must contain batter charge percentage
     * apminfo.battery_flags & 0x8 must be nonzero when charging
     */

    g_assert (apminfo);

    charging = FALSE;
    remain = 0;
    rate = 0;

    procdir=opendir ("/proc/acpi/battery/");
    if (!procdir)
        return FALSE;

    /* Get the remaining capacity for the batteries.  Other information
     * such as AC state and battery max capacity are read only when they
     * change using acpi_process_event (). */
    while ((procdirentry=readdir (procdir)))
    {
        if (procdirentry->d_name[0]!='.')
        {
            batt_state = g_strconcat ("/proc/acpi/battery/",
                                      procdirentry->d_name,
                                      "/",
                                      acpiinfo->batt_state_state,
                                      NULL);
            hash = read_file (batt_state, buf, sizeof (buf));
            if (hash)
            {
                const char *s;
                if (!charging)
                {
                    s = read_string (hash, acpiinfo->charging_state);
                    charging = s ? (strcmp (s, "charging") == 0) : 0;
                }
                remain += read_long (hash, "remaining capacity");
                rate += read_long (hash, "present rate");
                g_hash_table_destroy (hash);
            }
            g_free (batt_state);
        }
    }
    closedir (procdir);

    apminfo->ac_line_status = acpiinfo->ac_online ? 1 : 0;
    apminfo->battery_status =
        remain < acpiinfo->low_capacity ? 1 : remain < acpiinfo->critical_capacity ? 2 : 0;
    if (!acpiinfo->max_capacity)
        apminfo->battery_percentage = -1;
    else
        apminfo->battery_percentage = (int) (remain/(float)acpiinfo->max_capacity*100);
    apminfo->battery_flags = charging ? 0x8 : 0;
    if (rate && !charging)
        apminfo->battery_time = (int) (remain/(float)rate * 60);
    else if (rate && charging)
        apminfo->battery_time = (int) ((acpiinfo->max_capacity-remain)/(float)rate * 60);
    else
        apminfo->battery_time = -1;

    return TRUE;
}

#endif /* __linux__ */
