#include "e.h"

/* TODO:
 *
 * Sanatize data received from acpi for message status into something
 * meaningful (ie: 00000002 == LID_CLOSED, etc, etc).
 *
 * Find someone with a WIFI that actually emits ACPI events and add/debug the
 * E_EVENT_ACPI for wifi.
 *
 */

/* local structures */
/* for simple acpi device mapping */
typedef struct _E_ACPI_Device_Simple       E_ACPI_Device_Simple;
typedef struct _E_ACPI_Device_Simple_State E_ACPI_Device_Simple_State;
typedef struct _E_ACPI_Device_Multiplexed  E_ACPI_Device_Multiplexed;

struct _E_ACPI_Device_Simple
{
   const char *name;
   // ->
   int         type;
};

struct _E_ACPI_Device_Simple_State
{
   const char *name;
   const char *bus;
   const char *state;
   // ->
   int         type;
};

struct _E_ACPI_Device_Multiplexed
{
   const char *name;
   const char *bus;
   int         status;
   // ->
   int         type;
};

/* local function prototypes */
static Eina_Bool _e_acpi_cb_server_del(void *data EINA_UNUSED, int type EINA_UNUSED, void *event);
static Eina_Bool _e_acpi_cb_server_data(void *data EINA_UNUSED, int type EINA_UNUSED, void *event);
static void      _e_acpi_cb_event_free(void *data EINA_UNUSED, void *event);
static int       _e_acpi_lid_status_get(const char *device, const char *bus);
static Eina_Bool _e_acpi_cb_event(void *data EINA_UNUSED, int type EINA_UNUSED, void *event);

/* local variables */
static int _e_acpi_events_frozen = 0;
static Ecore_Con_Server *_e_acpid = NULL;
static Eina_List *_e_acpid_hdls = NULL;
static Eina_Strbuf *acpibuf = NULL;
static int lid_is_closed = -1;

static E_ACPI_Device_Simple _devices_simple[] =
{
   /* NB: DO NOT TRANSLATE THESE. */
   {"ac_adapter", E_ACPI_TYPE_AC_ADAPTER},
   {"battery", E_ACPI_TYPE_BATTERY},
   {"button/lid", E_ACPI_TYPE_LID},
   {"button/power", E_ACPI_TYPE_POWER},
   {"button/sleep", E_ACPI_TYPE_SLEEP},
   {"button/volumedown", E_ACPI_TYPE_VOLUME_DOWN},
   {"button/volumeup", E_ACPI_TYPE_VOLUME_UP},
   {"button/mute", E_ACPI_TYPE_MUTE},
   {"button/wlan", E_ACPI_TYPE_WIFI},
   {"fan", E_ACPI_TYPE_FAN},
   {"processor", E_ACPI_TYPE_PROCESSOR},
   {"thermal_zone", E_ACPI_TYPE_THERMAL},
   {"video", E_ACPI_TYPE_VIDEO},
   {"video/brightnessdown", E_ACPI_TYPE_BRIGHTNESS_DOWN},
   {"video/brightnessup", E_ACPI_TYPE_BRIGHTNESS_UP},
   {"video/switchmode", E_ACPI_TYPE_VIDEO},
   {"button/zoom", E_ACPI_TYPE_ZOOM},
   {"button/screenlock", E_ACPI_TYPE_SCREENLOCK},
   {"button/battery", E_ACPI_TYPE_BATTERY_BUTTON},
   {"video/tabletmode", E_ACPI_TYPE_TABLET},

   {NULL, E_ACPI_TYPE_UNKNOWN}
};

static E_ACPI_Device_Simple_State _devices_simple_state[] =
{
   /* NB: DO NOT TRANSLATE THESE. */
   {"video/tabletmode", "TBLT", "on", E_ACPI_TYPE_TABLET_ON},
   {"video/tabletmode", "TBLT", "off", E_ACPI_TYPE_TABLET_OFF},

   {NULL, NULL, NULL, E_ACPI_TYPE_UNKNOWN}
};

static E_ACPI_Device_Multiplexed _devices_multiplexed[] =
{
   /* NB: DO NOT TRANSLATE THESE. */
/* Sony VAIO - VPCF115FM / PCG-81114L - nvidia gfx */
   {"sony/hotkey", NULL, 0x10, E_ACPI_TYPE_BRIGHTNESS_DOWN},
   {"sony/hotkey", NULL, 0x11, E_ACPI_TYPE_BRIGHTNESS_UP},
   {"sony/hotkey", NULL, 0x12, E_ACPI_TYPE_VIDEO},
   {"sony/hotkey", NULL, 0x14, E_ACPI_TYPE_ZOOM_OUT},
   {"sony/hotkey", NULL, 0x15, E_ACPI_TYPE_ZOOM_IN},
   {"sony/hotkey", NULL, 0x17, E_ACPI_TYPE_HIBERNATE},
   {"sony/hotkey", NULL, 0xa6, E_ACPI_TYPE_ASSIST},
   {"sony/hotkey", NULL, 0x20, E_ACPI_TYPE_S1},
   {"sony/hotkey", NULL, 0xa5, E_ACPI_TYPE_VAIO},

/* Sony VAIO - X505 - intel gfx */
   {"sony/hotkey", NULL, 0x0e, E_ACPI_TYPE_MUTE},
   {"sony/hotkey", NULL, 0x0f, E_ACPI_TYPE_VOLUME},
   {"sony/hotkey", NULL, 0x10, E_ACPI_TYPE_BRIGHTNESS},
   {"sony/hotkey", NULL, 0x12, E_ACPI_TYPE_VIDEO},

/* HP Compaq Presario - CQ61 - intel gfx */
/** interesting these get auto-mapped to keys in x11. here for documentation
** but not enabled as we can use regular keybinds for these
   {"video",       "DD03", 0x87, E_ACPI_TYPE_BRIGHTNESS_DOWN},
   {"video",       "DD03", 0x86, E_ACPI_TYPE_BRIGHTNESS_UP},
   {"video",       "OVGA", 0x80, E_ACPI_TYPE_VIDEO},
*/
/* END */
   {NULL, NULL, 0x00, E_ACPI_TYPE_UNKNOWN}
};

/* public variables */
E_API int E_EVENT_ACPI = 0;

/* public functions */
EINTERN int
e_acpi_init(void)
{
   E_EVENT_ACPI = ecore_event_type_new();

   /* check for running acpid */
   if (!ecore_file_exists("/var/run/acpid.socket"))
     {
        if (ecore_file_exists("/proc/acpi"))
          e_util_dialog_show(_("Error"),
                             _("You seem to have an ACPI based system, but<br>"
                               "<hilight>acpid</hilight> does not seem to be running or<br>"
                               "contactable. Perhaps enable the <hilight>acpid</hilight><br>"
                               "service on your system?"));
        return 1;
     }

   /* try to connect to acpid socket */
   _e_acpid = ecore_con_server_connect(ECORE_CON_LOCAL_SYSTEM,
                                       "/var/run/acpid.socket", -1, NULL);
   if (!_e_acpid) return 1;

   /* setup handlers */
   _e_acpid_hdls =
     eina_list_append(_e_acpid_hdls,
                      ecore_event_handler_add(ECORE_CON_EVENT_SERVER_DEL,
                                              _e_acpi_cb_server_del, NULL));
   _e_acpid_hdls =
     eina_list_append(_e_acpid_hdls,
                      ecore_event_handler_add(ECORE_CON_EVENT_SERVER_DATA,
                                              _e_acpi_cb_server_data, NULL));

   /* Add handlers for standard acpi events */
   _e_acpid_hdls =
     eina_list_append(_e_acpid_hdls,
                      ecore_event_handler_add(E_EVENT_ACPI,
                                              _e_acpi_cb_event, NULL));
   return 1;
}

EINTERN int
e_acpi_shutdown(void)
{
   Ecore_Event_Handler *hdl;

   /* cleanup event handlers */
   EINA_LIST_FREE(_e_acpid_hdls, hdl)
     ecore_event_handler_del(hdl);

   /* kill the server if existing */
   if (_e_acpid)
     {
        ecore_con_server_del(_e_acpid);
        _e_acpid = NULL;
     }
   return 1;
}

EINTERN E_Acpi_Lid_Status
e_acpi_lid_status_get(void)
{
   int i;

   for (i = 0; _devices_simple[i].name; i++)
     {
        if (_devices_simple[i].type == E_ACPI_TYPE_LID)
          {
             /* TODO: Can bus be anything other than LID? */
             return _e_acpi_lid_status_get(_devices_simple[i].name, "LID");
          }
     }

   return E_ACPI_LID_UNKNOWN;
}

E_API Eina_Bool
e_acpi_lid_is_closed(void)
{
   if (lid_is_closed == -1)
     lid_is_closed = (e_acpi_lid_status_get() == E_ACPI_LID_CLOSED);
   return lid_is_closed;
}

E_API void
e_acpi_events_freeze(void)
{
   _e_acpi_events_frozen++;
}

E_API void
e_acpi_events_thaw(void)
{
   _e_acpi_events_frozen--;
   if (_e_acpi_events_frozen < 0) _e_acpi_events_frozen = 0;
}

/* local functions */
static Eina_Bool
_e_acpi_cb_server_del(void *data EINA_UNUSED, int type EINA_UNUSED, void *event)
{
   Ecore_Con_Event_Server_Del *ev;
   Ecore_Event_Handler *hdl;

   ev = event;
   if (ev->server != _e_acpid) return ECORE_CALLBACK_PASS_ON;

   /* cleanup event handlers */
   EINA_LIST_FREE(_e_acpid_hdls, hdl)
     ecore_event_handler_del(hdl);

   /* kill the server if existing */
   if (_e_acpid)
     {
        ecore_con_server_del(_e_acpid);
        _e_acpid = NULL;
     }
   return ECORE_CALLBACK_PASS_ON;
}

static Eina_Bool
_e_acpi_cb_server_data(void *data EINA_UNUSED, int type EINA_UNUSED, void *event)
{
   Ecore_Con_Event_Server_Data *ev;
   E_Event_Acpi *acpi_event;
   int sig, status, i, done = 0;
   char device[1024], bus[1024], state[1024], *sdata;
   const char *str, *p;

   ev = event;

   if ((!ev->data) || (ev->size < 1)) return ECORE_CALLBACK_PASS_ON;

   /* write out actual acpi received data to stdout for debugging
      res = fwrite(ev->data, ev->size, 1, stdout);
    */
   /* data from a server isn't a string - its not 0 byte terminated. it's just
    * a blob of data. copy to string and 0 byte terminate it so it can be
    * string-swizzled/parsed etc. */
   if (!acpibuf) acpibuf = eina_strbuf_new();
   eina_strbuf_append_length(acpibuf, ev->data, ev->size);
   str = eina_strbuf_string_get(acpibuf);
   p = strchr(str, '\n');
   if (!p) return ECORE_CALLBACK_PASS_ON;
   while (p)
     {
        device[0] = bus[0] = state[0] = 0;
        sdata = alloca(p - str + 1);
        strncpy(sdata, str, (int)(p - str));
        sdata[p - str] = 0;
        /* parse out this acpi string into separate pieces */
        if (sscanf(sdata, "%1023s %1023s %x %x",
                   device, bus, &sig, &status) != 4)
          {
             sig = -1;
             status = -1;
             if (sscanf(sdata, "%1023s %1023s", device, bus) != 2)
               {
                  if (sscanf(sdata, "%1023s %1023s %1023s", device, bus, state) != 3)
                    goto done_event;
               }
          }

        /* create new event structure to raise */
        acpi_event = E_NEW(E_Event_Acpi, 1);
        acpi_event->bus_id = eina_stringshare_add(bus);
        acpi_event->signal = sig;
        acpi_event->status = status;

        /* FIXME: add in a key faking layer */
        if ((!done) && (sig >= 0) && (status >= 0))
          {
             for (i = 0; _devices_multiplexed[i].name; i++)
               {
                  // if device name matches
                  if ((!strcmp(device, _devices_multiplexed[i].name)) &&
                      // AND busname not set OR device name matches
                      (!_devices_multiplexed[i].bus ||
                       (_devices_multiplexed[i].bus &&
                        (!strcmp(bus, _devices_multiplexed[i].bus)))) &&
                      // AND status matches
                      (_devices_multiplexed[i].status == status))
                    {
                       acpi_event->type = _devices_multiplexed[i].type;
                       done = 1;
                       break;
                    }
               }
          }
        if ((!done) && (state[0]))
          {
             for (i = 0; _devices_simple_state[i].name; i++)
               {
                  if ((!strcmp(device, _devices_simple_state[i].name)) &&
                      ((!_devices_simple_state[i].bus) || (!strcmp(bus, _devices_simple_state[i].bus))) &&
                      (!strcmp(state, _devices_simple_state[i].state)))
                    {
                       acpi_event->type =  _devices_simple_state[i].type;
                       done = 1;
                       break;
                    }
               }
          }
        if (!done)
          {
             // if device name matches
             for (i = 0; _devices_simple[i].name; i++)
               {
                  if (!strcmp(device, _devices_simple[i].name))
                    {
                       acpi_event->type = _devices_simple[i].type;
                       done = 1;
                       break;
                    }
               }
          }
        if (!done)
          {
             free(acpi_event);
             acpi_event = NULL;
          }
        else
          {
             switch (acpi_event->type)
               {
                case E_ACPI_TYPE_LID:
                  acpi_event->status =
                    _e_acpi_lid_status_get(device, bus);
                  printf("RRR: acpi event @%1.8f\n", ecore_time_get());
                  /* no change in lid state */
                  if (lid_is_closed == (acpi_event->status == E_ACPI_LID_CLOSED)) break;
                  lid_is_closed = (acpi_event->status == E_ACPI_LID_CLOSED);
                  printf("RRR: lid event for lid %i\n", lid_is_closed);
                  if (!e_randr2_cfg->ignore_acpi_events)
                    e_randr2_screen_refresh_queue(EINA_TRUE);
                  if (!lid_is_closed) e_powersave_defer_cancel();
                  break;

                default:
                  break;
               }
             /* actually raise the event */
             ecore_event_add(E_EVENT_ACPI, acpi_event,
                             _e_acpi_cb_event_free, NULL);
          }
done_event:
        str = p + 1;
        p = strchr(str, '\n');
     }
   if (str[0] == 0)
     {
        eina_strbuf_free(acpibuf);
        acpibuf = NULL;
     }
   else
     {
        Eina_Strbuf *newbuf;

        newbuf = eina_strbuf_new();
        eina_strbuf_append(newbuf, str);
        eina_strbuf_free(acpibuf);
        acpibuf = newbuf;
     }
   return ECORE_CALLBACK_PASS_ON;
}

static void
_e_acpi_cb_event_free(void *data EINA_UNUSED, void *event)
{
   E_Event_Acpi *ev;

   if (!(ev = event)) return;
   if (ev->device) eina_stringshare_del(ev->device);
   if (ev->bus_id) eina_stringshare_del(ev->bus_id);
   E_FREE(ev);
}

static int
_e_acpi_lid_status_get(const char *device, const char *bus)
{
   FILE *f;
   int i = 0;
   char buff[PATH_MAX], *ret;

   /* the acpi driver code in the kernel has a nice acpi function to return
    * the lid status easily, but that function is not exposed for user_space
    * so we need to check the proc fs to get the actual status */

   /* make sure we have a device and bus */
   if ((!device) || (!bus)) return E_ACPI_LID_UNKNOWN;

   /* open the state file from /proc */
   snprintf(buff, sizeof(buff), "/proc/acpi/%s/%s/state", device, bus);
   if (!(f = fopen(buff, "r")))
     {
        /* hack around ppurka's Thinkpad (G460 + Linux) that reports lid
         * state as "/proc/acpi/button/lid/LID0/state" but where the lid
         * event says "button/lid LID close".
         *
         * so let's take the base device name "LID" and add a numeric like
         * 0, 1, 2, 3 so we have LID0, LID1, LID2 etc. - try up to LID9
         * and then give up.
         */
        for (i = 0; i < 10; i++)
          {
             snprintf(buff, sizeof(buff), "/proc/acpi/%s/%s%i/state",
                      device, bus, i);
             if ((f = fopen(buff, "r"))) break;
             f = NULL;
          }
        if (!f) return E_ACPI_LID_UNKNOWN;
     }

   /* read the line from state file */
   ret = fgets(buff, sizeof(buff), f);
   fclose(f);
   if (!ret)
     return E_ACPI_LID_UNKNOWN;

   /* parse out state file */
   i = 0;
   while (buff[i] != ':')
     i++;
   while (!isalnum(buff[i]))
     i++;
   ret = &(buff[i]);
   while (isalnum(buff[i]))
     i++;
   buff[i] = 0;

   /* compare value from state file and return something sane */
   if (!strcmp(ret, "open")) return E_ACPI_LID_OPEN;
   else if (!strcmp(ret, "closed"))
     return E_ACPI_LID_CLOSED;
   else return E_ACPI_LID_UNKNOWN;
}

static Eina_Bool
_e_acpi_cb_event(void *data EINA_UNUSED, int type EINA_UNUSED, void *event)
{
   E_Event_Acpi *ev;

   ev = event;
   if (_e_acpi_events_frozen > 0) return ECORE_CALLBACK_PASS_ON;
   e_bindings_acpi_event_handle(E_BINDING_CONTEXT_NONE, NULL, ev);
   return ECORE_CALLBACK_PASS_ON;
}

