/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
 * Copyright (C) 2011 Landry Breuil <landry@openbsd.org>
 * Copyright (C) 2011 Yaakov Selkowitz <yselkowitz@cygwin.com>
 *
 * Licensed under the GNU General Public License Version 2
 *
 * 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.
 */

#include "up-win-native.h"

#include "up-backend.h"
#include "up-daemon.h"
#include "up-marshal.h"
#include "up-device.h"
#include <unistd.h>	/* for sleep() */
#include <windows.h>

#define UP_BACKEND_SUSPEND_COMMAND	"/usr/bin/suspend"
#define UP_BACKEND_HIBERNATE_COMMAND	"/usr/bin/hibernate"

static void	up_backend_class_init	(UpBackendClass	*klass);
static void	up_backend_init		(UpBackend	*backend);
static void	up_backend_finalize	(GObject	*object);

static UpDeviceState up_backend_win_get_battery_state_value(u_char battery_state);

static gboolean	up_win_device_get_on_battery	(UpDevice *device, gboolean *on_battery);
static gboolean	up_win_device_get_low_battery	(UpDevice *device, gboolean *low_battery);
static gboolean	up_win_device_get_online	(UpDevice *device, gboolean *online);
static gboolean	up_win_device_refresh		(UpDevice *device);

#define UP_BACKEND_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), UP_TYPE_BACKEND, UpBackendPrivate))

struct UpBackendPrivate
{
	UpDaemon		*daemon;
	UpDevice		*ac;
	UpDevice		*battery;
	GThread			*win_thread;
	gboolean		is_laptop;
};

enum {
	SIGNAL_DEVICE_ADDED,
	SIGNAL_DEVICE_REMOVED,
	SIGNAL_LAST
};

static guint signals [SIGNAL_LAST] = { 0 };

G_DEFINE_TYPE (UpBackend, up_backend, G_TYPE_OBJECT)

/**
 * functions called by upower daemon
 **/


gboolean
up_win_device_get_on_battery (UpDevice *device, gboolean * on_battery)
{
	UpDeviceKind type;
	UpDeviceState state;
	gboolean is_present;

	g_return_val_if_fail (on_battery != NULL, FALSE);

	g_object_get (device,
		      "type", &type,
		      "state", &state,
		      "is-present", &is_present,
		      (void*) NULL);

	if (type != UP_DEVICE_KIND_BATTERY)
		return FALSE;
	if (state == UP_DEVICE_STATE_UNKNOWN)
		return FALSE;
	if (!is_present)
		return FALSE;

	*on_battery = (state == UP_DEVICE_STATE_DISCHARGING);
	return TRUE;
}
gboolean
up_win_device_get_low_battery (UpDevice *device, gboolean * low_battery)
{
	gboolean ret;
	gboolean on_battery;
	gdouble percentage;

	g_return_val_if_fail (low_battery != NULL, FALSE);

	ret = up_win_device_get_on_battery (device, &on_battery);
	if (!ret)
		return FALSE;

	if (!on_battery) {
		*low_battery = FALSE;
		return TRUE;
	}

	g_object_get (device, "percentage", &percentage, (void*) NULL);
	*low_battery = (percentage < 10.0f);
	return TRUE;
}

gboolean
up_win_device_get_online (UpDevice *device, gboolean * online)
{
	UpDeviceKind type;
	gboolean online_tmp;

	g_return_val_if_fail (online != NULL, FALSE);

	g_object_get (device,
		      "type", &type,
		      "online", &online_tmp,
		      (void*) NULL);

	if (type != UP_DEVICE_KIND_LINE_POWER)
		return FALSE;

	*online = online_tmp;

	return TRUE;
}
/**
 * up_backend_coldplug:
 * @backend: The %UpBackend class instance
 * @daemon: The %UpDaemon controlling instance
 *
 * Finds all the devices already plugged in, and emits device-add signals for
 * each of them.
 *
 * Return value: %TRUE for success
 **/
gboolean
up_backend_coldplug (UpBackend *backend, UpDaemon *daemon)
{
	UpWinNative *acnative = NULL;
	UpWinNative *battnative = NULL;
	backend->priv->daemon = g_object_ref (daemon);
	/* XXX no way to get lid status atm */
	up_daemon_set_lid_is_present (backend->priv->daemon, FALSE);

	acnative = up_win_native_new("/ac");
	if (!up_device_coldplug (backend->priv->ac, backend->priv->daemon, G_OBJECT(acnative)))
		g_warning ("failed to coldplug ac");
	else
		g_signal_emit (backend, signals[SIGNAL_DEVICE_ADDED], 0, acnative, backend->priv->ac);

	if (backend->priv->is_laptop)
	{
		battnative = up_win_native_new("/batt");
		if (!up_device_coldplug (backend->priv->battery, backend->priv->daemon, G_OBJECT(battnative)))
			g_warning ("failed to coldplug battery");
		else
			g_signal_emit (backend, signals[SIGNAL_DEVICE_ADDED], 0, battnative, backend->priv->battery);
	}

	return TRUE;
}


/**
 * up_backend_get_powersave_command:
 **/
const gchar *
up_backend_get_powersave_command (UpBackend *backend, gboolean powersave)
{
	return "/bin/true";
}

/**
 * up_backend_get_suspend_command:
 **/
const gchar *
up_backend_get_suspend_command (UpBackend *backend)
{
	return UP_BACKEND_SUSPEND_COMMAND;
}

/**
 * up_backend_get_hibernate_command:
 **/
const gchar *
up_backend_get_hibernate_command (UpBackend *backend)
{
	return UP_BACKEND_HIBERNATE_COMMAND;
}

gboolean
up_backend_emits_resuming (UpBackend *backend)
{
	return FALSE;
}

/**
 * up_backend_kernel_can_suspend:
 **/
gboolean
up_backend_kernel_can_suspend (UpBackend *backend)
{
	return TRUE;
}

/**
 * up_backend_kernel_can_hibernate:
 **/
gboolean
up_backend_kernel_can_hibernate (UpBackend *backend)
{
	return TRUE;
}

gboolean
up_backend_has_encrypted_swap (UpBackend *backend)
{
	return FALSE;
}

/* Return value: a percentage value */
gfloat
up_backend_get_used_swap (UpBackend *backend)
{
	return 0.0;
}

/**
 * Cygwin specific code
 **/

static UpDeviceState
up_backend_win_get_battery_flag_value(BYTE battery_flag) {
	if (battery_flag & 255)
		return UP_DEVICE_STATE_UNKNOWN;
	else if (battery_flag & 128)
		return UP_DEVICE_STATE_EMPTY;
	else if (battery_flag & 8)
		return UP_DEVICE_STATE_CHARGING;
	else if (battery_flag & 4)
		return UP_DEVICE_STATE_EMPTY;
	return UP_DEVICE_STATE_DISCHARGING;
}

static gboolean
up_backend_update_ac_state(UpDevice* device)
{
	gboolean ret, new_is_online, cur_is_online;
	SYSTEM_POWER_STATUS sps;

	ret = GetSystemPowerStatus(&sps);
	if (!ret)
		return FALSE;

	g_object_get (device, "online", &cur_is_online, (void*) NULL);
	new_is_online = (sps.ACLineStatus == 1 ? TRUE : FALSE);
	if (cur_is_online != new_is_online)
	{
		g_object_set (device,
			"online", new_is_online,
			(void*) NULL);
		return TRUE;
	}
	return FALSE;
}

static gboolean
up_backend_update_battery_state(UpDevice* device)
{
	gdouble cur_percentage, new_percentage;
	gboolean ret, is_present;
	UpDeviceState cur_state, new_state;
	gint64 cur_time_to_empty, new_time_to_empty;
	SYSTEM_POWER_STATUS sps;

	ret = GetSystemPowerStatus(&sps);
	if (!ret)
		return FALSE;

	g_object_get (device,
		"state", &cur_state,
		"percentage", &cur_percentage,
		"time-to-empty", &cur_time_to_empty,
		"is-present", &is_present,
		(void*) NULL);

	/*
	 * XXX: Stop having a split brain regarding
	 * up_backend_win_get_battery_flag_value(). Either move the state
	 * setting code below into that function, or inline that function here.
	 */
	new_state = up_backend_win_get_battery_flag_value(sps.BatteryFlag);
	new_percentage = (gdouble) sps.BatteryLifePercent;
	if (new_percentage > 254)
		new_percentage = 0.0f;
	// if percentage/minutes goes down or ac is off, we're likely discharging..
	if (new_percentage < cur_percentage || cur_time_to_empty < new_time_to_empty || sps.ACLineStatus == 0)
		new_state = UP_DEVICE_STATE_DISCHARGING;
	/*
	 * If we're on AC, we may either be charging, or the battery is already
	 * fully charged. Figure out which.
	 */
	if (sps.ACLineStatus == 1)
		if (new_percentage >= 99.0)
			new_state = UP_DEVICE_STATE_FULLY_CHARGED;
		else
			new_state = UP_DEVICE_STATE_CHARGING;

	if ((sps.BatteryFlag & 128) ||
	    (sps.BatteryFlag & 255)) {
		/* Reset some known fields which remain untouched below. */
		g_object_set(device,
			     "is-rechargeable", FALSE,
			     "energy", (gdouble) 0.0,
			     "energy-empty", (gdouble) 0.0,
			     "energy-full", (gdouble) 0.0,
			     "energy-full-design", (gdouble) 0.0,
			     "energy-rate", (gdouble) 0.0,
			     NULL);
		is_present = FALSE;
		if (sps.BatteryFlag & 128)
			new_state = UP_DEVICE_STATE_EMPTY;
		else
			new_state = UP_DEVICE_STATE_UNKNOWN;
	} else {
		is_present = TRUE;
	}

	// zero out new_time_to empty if we're not discharging or minutes_left is negative
	new_time_to_empty = (new_state == UP_DEVICE_STATE_DISCHARGING && sps.BatteryLifeTime > 0 ? sps.BatteryLifeTime : 0);

	if (cur_state != new_state ||
		cur_percentage != new_percentage ||
		cur_time_to_empty != new_time_to_empty)
	{
		g_object_set (device,
			"state", new_state,
			"percentage", new_percentage,
			"time-to-empty", new_time_to_empty,
			"is-present", is_present,
			(void*) NULL);
		return TRUE;
	}
	return FALSE;
}

/* callback updating the device */
/* http://www.zachburlingame.com/2011/04/capturing-windows-power-events-in-a-console-application/ */
/* http://hg.zachburlingame.com/windowspowerevents */
LRESULT CALLBACK
up_backend_win_powerchange_event_cb(HWND hWnd, UINT message,
                                    WPARAM wParam, LPARAM lParam)
{
	LRESULT ret = 0;
	static UpBackend *backend = NULL;

	switch (message) {
	case WM_CREATE: {
		CREATESTRUCT cs = *(LPCREATESTRUCT)lParam;
		g_return_val_if_fail (UP_IS_BACKEND (cs.lpCreateParams), FALSE);
		backend = UP_BACKEND (cs.lpCreateParams);
		break;
	}
	case WM_DESTROY:
		PostQuitMessage (ERROR_SUCCESS);
		/* fall through */
	case WM_QUERYENDSESSION:
	case WM_ENDSESSION:
		break;
	case WM_POWERBROADCAST:
		if (wParam == PBT_POWERSETTINGCHANGE) {
			up_win_device_refresh(backend->priv->ac);
			up_win_device_refresh(backend->priv->battery);
			break;
		}
		/* else fall through */
	default:
		ret = DefWindowProc (hWnd, message, wParam, lParam);
	}
	return ret;
}

static gboolean
up_win_device_refresh(UpDevice* device)
{
	UpDeviceKind type;
	GTimeVal timeval;
	gboolean ret;
	g_object_get (device, "type", &type, NULL);

	switch (type) {
		case UP_DEVICE_KIND_LINE_POWER:
			ret = up_backend_update_ac_state(device);
			break;
		case UP_DEVICE_KIND_BATTERY:
			ret = up_backend_update_battery_state(device);
			break;
		default:
			g_assert_not_reached ();
			break;
	}

	if (ret) {
		g_get_current_time (&timeval);
		g_object_set (device, "update-time", (guint64) timeval.tv_sec, NULL);
	}

	return ret;
}

/* setup windows notification */
/* http://www.zachburlingame.com/2011/04/capturing-windows-power-events-in-a-console-application/ */
/* http://hg.zachburlingame.com/windowspowerevents */
static gboolean
up_backend_win_event_thread(gpointer object)
{
	UpBackend *backend;
	HMODULE user32;
	HPOWERNOTIFY (WINAPI *pRPSN)(HANDLE, LPCGUID, DWORD);
	WNDCLASSEX wcx = { sizeof (WNDCLASSEX), 0, (WNDPROC) up_backend_win_powerchange_event_cb,
			   0, 0, NULL, NULL, NULL, NULL, NULL, "HiddenWndClass", NULL };
	HWND hwnd;
	HPOWERNOTIFY hpn[2];
	MSG msg;
	BOOL ret;

	g_return_val_if_fail (UP_IS_BACKEND (object), FALSE);
	backend = UP_BACKEND (object);

	g_debug("setting up win thread");

	/* only available since Vista/2008 */
	user32 = LoadLibrary ("user32.dll");
	pRPSN = (typeof (pRPSN)) GetProcAddress (user32, "RegisterPowerSettingNotification");
	if (!pRPSN) {
		FreeLibrary (user32);
		return FALSE;
	}

	RegisterClassEx (&wcx);
	hwnd = CreateWindow("HiddenWndClass", NULL, WS_OVERLAPPEDWINDOW, -1, -1,
			    0, 0, NULL, NULL, NULL, object);

	if (!hwnd)
		return FALSE;

	hpn[0] = pRPSN (hwnd, &GUID_ACDC_POWER_SOURCE, DEVICE_NOTIFY_WINDOW_HANDLE);
	hpn[1] = pRPSN (hwnd, &GUID_BATTERY_PERCENTAGE_REMAINING, DEVICE_NOTIFY_WINDOW_HANDLE);

	FreeLibrary (user32);

	while ((ret = GetMessage (&msg, hwnd, 0, 0)) != 0) {
		if (ret = -1)
			return FALSE;
		else {
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
	}

	return TRUE;
}

/**
 * GObject class functions
 **/

/**
 * up_backend_new:
 *
 * Return value: a new %UpBackend object.
 **/
UpBackend *
up_backend_new (void)
{
	return g_object_new (UP_TYPE_BACKEND, NULL);
}

/**
 * up_backend_class_init:
 * @klass: The UpBackendClass
 **/
static void
up_backend_class_init (UpBackendClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	object_class->finalize = up_backend_finalize;

	signals [SIGNAL_DEVICE_ADDED] =
		g_signal_new ("device-added",
			      G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
			      G_STRUCT_OFFSET (UpBackendClass, device_added),
			      NULL, NULL, up_marshal_VOID__POINTER_POINTER,
			      G_TYPE_NONE, 2, G_TYPE_POINTER, G_TYPE_POINTER);
	signals [SIGNAL_DEVICE_REMOVED] =
		g_signal_new ("device-removed",
			      G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
			      G_STRUCT_OFFSET (UpBackendClass, device_removed),
			      NULL, NULL, up_marshal_VOID__POINTER_POINTER,
			      G_TYPE_NONE, 2, G_TYPE_POINTER, G_TYPE_POINTER);

	g_type_class_add_private (klass, sizeof (UpBackendPrivate));
}

/**
 * up_backend_init:
 **/
static void
up_backend_init (UpBackend *backend)
{
	GError *err = NULL;
	GTimeVal timeval;
	UpDeviceClass *device_class;

	backend->priv = UP_BACKEND_GET_PRIVATE (backend);
	backend->priv->daemon = NULL;
	backend->priv->is_laptop = up_native_is_laptop();
	g_debug("is_laptop:%d",backend->priv->is_laptop);

	backend->priv->ac = UP_DEVICE(up_device_new());
	device_class = UP_DEVICE_GET_CLASS (backend->priv->ac);
	device_class->get_on_battery = up_win_device_get_on_battery;
	device_class->get_low_battery = up_win_device_get_low_battery;
	device_class->get_online = up_win_device_get_online;
	device_class->refresh = up_win_device_refresh;

	if (backend->priv->is_laptop)
	{
		backend->priv->battery = UP_DEVICE(up_device_new ());
		device_class = UP_DEVICE_GET_CLASS (backend->priv->battery);
		device_class->get_on_battery = up_win_device_get_on_battery;
		device_class->get_low_battery = up_win_device_get_low_battery;
		device_class->get_online = up_win_device_get_online;
		device_class->refresh = up_win_device_refresh;

		/* creates thread */
		if((backend->priv->win_thread = (GThread*) g_thread_try_new("win-poller",(GThreadFunc)up_backend_win_event_thread, (void*) backend, &err) == NULL))
		{
			g_warning("Thread create failed: %s", err->message);
			g_error_free (err);
		}

		/* setup dummy */
		g_get_current_time (&timeval);
		g_object_set (backend->priv->battery,
			      "type", UP_DEVICE_KIND_BATTERY,
			      "power-supply", TRUE,
			      "is-present", TRUE,
			      "is-rechargeable", TRUE,
			      "has-history", TRUE,
			      "state", UP_DEVICE_STATE_UNKNOWN,
			      "percentage", 0.0f,
			      "time-to-empty", (gint64) 0,
			      "update-time", (guint64) timeval.tv_sec,
			      (void*) NULL);

	}

	g_get_current_time (&timeval);
	g_object_set (backend->priv->ac,
		      "type", UP_DEVICE_KIND_LINE_POWER,
		      "online", TRUE,
		      "power-supply", TRUE,
		      "update-time", (guint64) timeval.tv_sec,
		      (void*) NULL);
}

/**
 * up_backend_finalize:
 **/
static void
up_backend_finalize (GObject *object)
{
	UpBackend *backend;

	g_return_if_fail (UP_IS_BACKEND (object));

	backend = UP_BACKEND (object);

	if (backend->priv->daemon != NULL)
		g_object_unref (backend->priv->daemon);
	if (backend->priv->battery != NULL)
		g_object_unref (backend->priv->battery);
	if (backend->priv->ac != NULL)
		g_object_unref (backend->priv->ac);
	/* XXX stop win_thread ? */

	G_OBJECT_CLASS (up_backend_parent_class)->finalize (object);
}

