/* apmlib.c -- Sample APM interface routines
 * Created: Mon Jan  8 10:28:16 1996 by faith@acm.org
 * Revised: Fri Dec 26 21:38:29 1997 by faith@acm.org
 * Copyright 1996, 1997 Rickard E. Faith (faith@acm.org)
 * 
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Library General Public License as published
 * by the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 * 
 * This library 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
 * Library General Public License for more details.
 * 
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <ctype.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/sysmacros.h>
#include "apm.h"

#define BACKWARD_COMPAT 1

/* If APM support of the right version exists in kernel, return zero.
 * Otherwise, return 1 if no support exists, or 2 if it is the wrong
 * version.  *NOTE* The sense of the return value is not intuitive.
 */
int
apm_exists (void)
{
    apm_info i;

    if (access (APM_PROC, R_OK))
        return 1;
    return apm_read (&i);
}


/* Read information from /proc/apm.  Return 0 on success, 1 if APM not
 * installed, 2 if APM installed, but old version. 
 */
int
apm_read (apm_info * i)
{
    FILE *str;
    char units[10];
    char buffer[100];
    int retcode = 0;

    if (!(str = fopen (APM_PROC, "r")))
        return 1;

    if (fgets (buffer, sizeof (buffer) - 1, str) == NULL)
        printf ("fgets error\n");

    buffer[sizeof (buffer) - 1] = '\0';

    /* Should check for other driver versions; driver 1.9 (and some
     * others) uses this format, which doesn't expose # batteries.
     */
    sscanf (buffer, "%s %d.%d %hx %hx %hx %hx %d%% %d %s\n",
            (char *) i->driver_version,
            &i->apm_version_major,
            &i->apm_version_minor,
            &i->apm_flags,
            &i->ac_line_status,
            &i->battery_status,
            &i->battery_flags,
            &i->battery_percentage,
            &i->battery_time,
            units);
    i->using_minutes = !strncmp (units, "min", 3) ? 1 : 0;
    if (i->driver_version[0] == 'B')
    {                /* old style.  argh. */
#if !BACKWARD_COMPAT
    retcode = 2;
#else
    strcpy ((char *) i->driver_version, "pre-0.7");
    i->apm_version_major = 0;
    i->apm_version_minor = 0;
    i->apm_flags = 0;
    i->ac_line_status = 0xff;
    i->battery_status = 0xff;
    i->battery_flags = 0xff;
    i->battery_percentage = -1;
    i->battery_time = -1;
    i->using_minutes = 1;

    sscanf (buffer, "BIOS version: %d.%d",
            &i->apm_version_major, &i->apm_version_minor);

    if (fgets (buffer, sizeof (buffer) - 1, str) == NULL)
        printf ("fgets error\n");

    sscanf (buffer, "Flags: 0x%02hx", &i->apm_flags);
    if (i->apm_flags & APM_32_BIT_SUPPORT)
    {
        if (fgets (buffer, sizeof (buffer) - 1, str) == NULL)
            printf ("fgets error\n");

        if (fgets (buffer, sizeof (buffer) - 1, str) == NULL)
            printf ("fgets error\n");

        if (buffer[0] != 'P')
        {
            if (!strncmp (buffer + 4, "off line", 8))
                i->ac_line_status = 0;
            else if (!strncmp (buffer + 4, "on line", 7))
                i->ac_line_status = 1;
            else if (!strncmp (buffer + 4, "on back", 7))
                i->ac_line_status = 2;

            if (fgets (buffer, sizeof (buffer) - 1, str) == NULL)
                printf ("fgets error\n");

            if (!strncmp (buffer + 16, "high", 4))
                i->battery_status = 0;
            else if (!strncmp (buffer + 16, "low", 3))
                i->battery_status = 1;
            else if (!strncmp (buffer + 16, "crit", 4))
                i->battery_status = 2;
            else if (!strncmp (buffer + 16, "charg", 5))
                i->battery_status = 3;

            if (fgets (buffer, sizeof (buffer) - 1, str) == NULL)
                printf ("fgets error\n");

            if (strncmp (buffer + 14, "unknown", 7))
                i->battery_percentage = atoi (buffer + 14);
            if (i->apm_version_major >= 1 && i->apm_version_minor >= 1)
            {
                if (fgets (buffer, sizeof (buffer) - 1, str) == NULL)
                    printf ("fgets error\n");

                sscanf (buffer, "Battery flag: 0x%02hx", &i->battery_flags);

                if (fgets (buffer, sizeof (buffer) - 1, str) == NULL)
                    printf ("fgets error\n");

                if (strncmp (buffer + 14, "unknown", 7))
                    i->battery_time = atoi (buffer + 14);
            }
        }
    }
#endif
    }

    /* Fix possible kernel bug -- percentage
     * set to 0xff (==255) instead of -1. 
     */
    if (i->battery_percentage > 100)
        i->battery_percentage = -1;

    fclose (str);
    return retcode;
}


/* Lookup the device number for the apm_bios device. */
dev_t apm_dev (void)
{
    FILE *str;
    static int cached = -1;
    char buf[80];
    char *pt;
    apm_info i;

    if (cached >= 0)
        return cached;

    if (access (APM_PROC, R_OK) || apm_read (&i) == 1)
        return cached = -1;
    if (i.driver_version[0] == '1')
        return cached = makedev (10, 134);

    if (!(str = fopen (APM_DEV, "r")))
        return -1;
    while (fgets (buf, sizeof (buf) - 1, str))
    {
        buf[sizeof (buf) - 1] = '\0';
        for (pt = buf; *pt && isspace (*pt); ++pt);    /* skip leading spaces */
        for (; *pt && !isspace (*pt); ++pt);    /* find next space */
        if (isspace (*pt))
        {
            *pt++ = '\0';
            pt[strlen (pt) - 1] = '\0';    /* get rid of newline */
            if (!strcmp (pt, APM_NAME))
            {
                fclose (str);
                return cached = makedev (atoi (buf), 0);
            }
        }
    }
    fclose (str);
    return cached = -1;
}


/* Return a file descriptor for the apm_bios device, or -1 if there is an
 * error.  Is this method secure?  Should we make the device in /dev
 * instead of /tmp? 
 *
 * apenwarr 2001/05/11: just throw out the weird temporary device file stuff.
 * It was only for ancient kernel versions anyway.
 */
int
apm_open (void)
{
    int fd;
    apm_info i;

    if (access (APM_PROC, R_OK) || apm_read (&i) == 1)
        return -1;
    if (i.driver_version[0] >= '1')
    {
        if ((fd = open (APM_DEVICE, O_RDWR)) < 0)
        {
            /* Try to create it.  This is reasonable
             * for backward compatibility.
             */
            if (mknod (APM_DEVICE, S_IFCHR | S_IRUSR | S_IWUSR, apm_dev ()))
            {
                unlink (APM_DEVICE);
                return -1;
            }
            fd = open (APM_DEVICE, O_RDWR);
        }
        return fd;
    }
    return -1;
}


/* Given a file descriptor for the apm_bios device, close it. */
int
apm_close (int fd)
{
    return close (fd);
}

/* Given a file descriptor for the apm_bios device, this routine will wait
 * timeout seconds for APM events.  Up to n events will be placed in the
 * events queue.  The return code will indicate the number of events
 * stored.  Since this routine uses select (2), it will return if an
 * unblocked signal is caught.  A timeout < 0 means to block indefinately.
 *
 * Note that if you read a request to standby or to suspend, the kernel
 * will be waiting for you to respond to it with a call to apm_suspend ()
 * or to apm_standby () !
 */
int
apm_get_events (int fd, int timeout, apm_event_t * events, int n)
{
    int retcode;
    fd_set fds;
    struct timeval t;

    t.tv_sec = timeout;
    t.tv_usec = 0;

    FD_ZERO (&fds);
    FD_SET (fd, &fds);
    retcode = select (fd + 1, &fds, NULL, NULL, timeout < 0 ? NULL : &t);
    if (retcode <= 0)
        return 0;
    return read (fd, events, n * sizeof (apm_event_t)) / sizeof (apm_event_t);
}


/* Try to set the Power State to Suspend. */
int
apm_suspend (int fd)
{
    sync ();
    return ioctl (fd, APM_IOC_SUSPEND, NULL);
}


/* Try to set the Power State to Standby. */
int
apm_standby (int fd)
{
    sync ();
    return ioctl (fd, APM_IOC_STANDBY, NULL);
}

/* Return the last error code generated by the kernel APM driver */
unsigned int
apm_last_error ( int fd )
{
    int err = 0;

#ifdef APM_IOC_LAST_ERROR
    int ierr = 0;
    if ((ierr = ioctl (fd, APM_IOC_LAST_ERROR, &err)))
        return ierr;
#endif
    return err;
}

/* Define lookup table for error messages */
typedef struct lookup_t {
    int    key;
    char*  msg;
} lookup_t;

/* APM error messages, arranged by error code */
static const lookup_t error_table[] = {
/* N/A { APM_SUCCESS, "Operation succeeded" }, */
    { APM_DISABLED,        "Power management disabled" },
    { APM_CONNECTED,       "Real mode interface already connected" },
    { APM_NOT_CONNECTED,   "Interface not connected" },
    { APM_16_CONNECTED,    "16 bit interface already connected" },
/* N/A { APM_16_UNSUPPORTED, "16 bit interface not supported" }, */
    { APM_32_CONNECTED,    "32 bit interface already connected" },
    { APM_32_UNSUPPORTED,  "32 bit interface not supported" },
    { APM_BAD_DEVICE,      "Unrecognized device ID" },
    { APM_BAD_PARAM,       "Parameter out of range" },
    { APM_NOT_ENGAGED,     "Interface not engaged" },
#ifdef APM_BAD_FUNCTION
    { APM_BAD_FUNCTION,    "Function not supported" },
#endif
#ifdef APM_RESUME_DISABLED
    { APM_RESUME_DISABLED, "Resume timer disabled" },
#endif
    { APM_BAD_STATE,       "Unable to enter requested state" },
/* N/A { APM_NO_EVENTS, "No events pending" }, */
    { APM_NOT_PRESENT,     "No APM present" }
};
#define ERROR_COUNT (sizeof(error_table)/sizeof(lookup_t))

/* Return character string describing error messages from APM kernel */
const char *
apm_error_name (unsigned int err)
{
  int i;
  
  for (i=0; i<ERROR_COUNT; i++)
    if (err == error_table[i].key) return (error_table[i].msg);

  return "Unknown error";
}

int
apm_reject (int fd)
{
#ifdef APM_IOC_REJECT
    if (ioctl fd, APM_IOC_REJECT, NULL )) 
        return apm_last_error (fd);
    else
#endif
        return 0;
}

#ifdef APM_IOC_IGNORE   /* detect kernel support of IGNORE/NOIGNORE functions */
int
apm_set_ignore (int fd, int mode)
/* Ignore Standby. */
{
    if (mode == IGNORE)
    {
        printf ("Telling kernel to ignore system standby/suspend mode\n");
        return ioctl (fd, APM_IOC_IGNORE, NULL);
    }
    else
    {
        printf ("Telling kernel not to ignore system standby/suspend mode\n");
        return ioctl (fd, APM_IOC_NOIGNORE, NULL);
    }
    printf ("NOTE: User-generated suspend/standby requests are not ignored\n");
}
#endif

/* Return a string describing the event. From p. 16 of the Intel/Microsoft
 * Advanded Power Management (APM) BIOS Interface Specification, Revision
 * 1.1 (September 1993). Intel Order Number: 241704-001.  Microsoft Part
 * Number: 781-110-X01.
 *
 * Updated to APM BIOS 1.2 spec (February 1996).  Available on-line.
 */
const char *
apm_event_name (apm_event_t event)
{
    switch (event)
    {
    case APM_SYS_STANDBY:
        return "System Standby Request";
    case APM_SYS_SUSPEND:
        return "System Suspend Request";
    case APM_NORMAL_RESUME:
        return "Normal Resume System";
    case APM_CRITICAL_RESUME:
        return "Critical Resume System";
    case APM_LOW_BATTERY:
        return "Battery Low";
    case APM_POWER_STATUS_CHANGE:
        return "Power Status Change";
    case APM_UPDATE_TIME:
        return "Update Time";
    case APM_CRITICAL_SUSPEND:
        return "Critical Suspend";
    case APM_USER_STANDBY:
        return "User System Standby Request";
    case APM_USER_SUSPEND:
        return "User System Suspend Request";
    case APM_STANDBY_RESUME:
        return "System Standby Resume";
#ifdef APM_CAPABILITY_CHANGE
    case APM_CAPABILITY_CHANGE:
        return "Capability Change";
#endif
    }
    return "Unknown";
}


/* This is a convenience function that has nothing to do with APM.  It just
 * formats a time nicely.  If you don't like this format, then write your
 * own.
 */
#define SEC_PER_DAY  (60*60*24)
#define SEC_PER_HOUR (60*60)
#define SEC_PER_MIN  (60)

const char *
apm_delta_time (time_t then, time_t now)
{
    return apm_time (now - then);
}

const char *
apm_time (time_t t)
{
    static char buffer[128];
    unsigned long s, m, h, d;

    d = t / SEC_PER_DAY;
    t -= d * SEC_PER_DAY;
    h = t / SEC_PER_HOUR;
    t -= h * SEC_PER_HOUR;
    m = t / SEC_PER_MIN;
    t -= m * SEC_PER_MIN;
    s = t;

    if (d)
        sprintf (buffer, "%lu day%s, %02lu:%02lu:%02lu",
                 d, d > 1 ? "s" : "", h, m, s);
    else
        sprintf (buffer, "%02lu:%02lu:%02lu", h, m, s);

    if (t == -1)
        sprintf (buffer, "unknown");

    return buffer;
}

const char *
apm_time_nosec (time_t t)
{
    static char buffer[128];
    unsigned long s, m, h, d;

    d = t / SEC_PER_DAY;
    t -= d * SEC_PER_DAY;
    h = t / SEC_PER_HOUR;
    t -= h * SEC_PER_HOUR;
    m = t / SEC_PER_MIN;
    t -= m * SEC_PER_MIN;
    s = t;

    if (s > 30)
        ++m;

    if (d)
        sprintf (buffer, "%lu day%s, %lu:%02lu",
                 d, d > 1 ? "s" : "", h, m);
    else
        sprintf (buffer, "%lu:%02lu", h, m);

    if (t == -1)
        sprintf (buffer, "unknown");

    return buffer;
}
