/*
 * * Copyright (C) 2009-2011 Ali <aliov@xfce.org>
 *
 * 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
 */

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

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <gtk/gtk.h>
#include <gdk/gdk.h>
#include <gdk/gdkx.h>

#include <X11/Xatom.h>
#include <X11/Xlib.h>
#include <X11/extensions/Xrandr.h>

#include <libxfce4util/libxfce4util.h>

#include "xfpm-brightness.h"
#include "xfpm-debug.h"

static void xfpm_brightness_finalize   (GObject *object);

struct XfpmBrightnessPrivate
{
    XRRScreenResources *resource;
    Atom		backlight;
    gint 		output;
    gboolean		xrandr_has_hw;
    gboolean		helper_has_hw;

    gint32		max_level;
    gint32		current_level;
    gint32		min_level;
    gint32		step;
};

G_DEFINE_TYPE_WITH_PRIVATE (XfpmBrightness, xfpm_brightness, G_TYPE_OBJECT)

static gboolean
xfpm_brightness_xrand_get_limit (XfpmBrightness *brightness, RROutput output, gint *min, gint *max)
{
    XRRPropertyInfo *info;
    gboolean ret = TRUE;
    GdkDisplay *gdisplay;

    gdisplay = gdk_display_get_default ();

    gdk_x11_display_error_trap_push (gdisplay);
    info = XRRQueryOutputProperty (gdk_x11_get_default_xdisplay (), output, brightness->priv->backlight);

    if (gdk_x11_display_error_trap_pop (gdisplay) != 0
        || info == NULL)
    {
	g_warning ("could not get output property");
	return FALSE;
    }

    if (!info->range || info->num_values != 2)
    {
	g_warning ("no range found");
	ret = FALSE;
	goto out;
    }

    *min = info->values[0];
    *max = info->values[1];

out:
    XFree (info);
    return ret;
}

static gboolean
xfpm_brightness_xrandr_get_level (XfpmBrightness *brightness, RROutput output, gint32 *current)
{
    unsigned long nitems;
    unsigned long bytes_after;
    gint32 *prop;
    Atom actual_type;
    int actual_format;
    gboolean ret = FALSE;
    GdkDisplay *gdisplay;

    gdisplay = gdk_display_get_default ();

    gdk_x11_display_error_trap_push (gdisplay);
    if (XRRGetOutputProperty (gdk_x11_get_default_xdisplay (), output, brightness->priv->backlight,
			      0, 4, False, False, None,
			      &actual_type, &actual_format,
			      &nitems, &bytes_after, ((unsigned char **)&prop)) != Success
	|| gdk_x11_display_error_trap_pop (gdisplay) != 0)
    {
	g_warning ("failed to get property");
	return FALSE;
    }

    if (actual_type == XA_INTEGER && nitems == 1 && actual_format == 32)
    {
	memcpy (current, prop, sizeof (*current));
	ret = TRUE;
    }

    XFree (prop);

    return ret;
}

static gboolean
xfpm_brightness_xrandr_set_level (XfpmBrightness *brightness, RROutput output, gint32 level)
{
    gboolean ret = TRUE;
    Display *display;
    GdkDisplay *gdisplay;

    display = gdk_x11_get_default_xdisplay ();
    gdisplay = gdk_display_get_default ();

    gdk_x11_display_error_trap_push (gdisplay);
    XRRChangeOutputProperty (display, output, brightness->priv->backlight, XA_INTEGER, 32,
			     PropModeReplace, (unsigned char *) &level, 1);

    XFlush (display);
    gdk_display_flush (gdisplay);

    if ( gdk_x11_display_error_trap_pop (gdisplay) )
    {
	    g_warning ("failed to XRRChangeOutputProperty for brightness %d", level);
	    ret = FALSE;
    }

    return ret;
}

static gboolean
xfpm_brightness_setup_xrandr (XfpmBrightness *brightness)
{
    GdkScreen *screen;
    GdkDisplay *gdisplay;
    XRROutputInfo *info;
    Window window;
    gint major, minor, screen_num;
    int event_base, error_base;
    gint32 min, max;
    gboolean ret = FALSE;
    gint i;

    gdisplay = gdk_display_get_default ();

    gdk_x11_display_error_trap_push (gdisplay);
    if (!XRRQueryExtension (gdk_x11_get_default_xdisplay (), &event_base, &error_base) ||
	!XRRQueryVersion (gdk_x11_get_default_xdisplay (), &major, &minor) )
    {
	gdk_x11_display_error_trap_pop_ignored (gdisplay);
	g_warning ("No XRANDR extension found");
	return FALSE;
    }
    gdk_x11_display_error_trap_pop_ignored (gdisplay);

    if (major == 1 && minor < 2)
    {
	g_warning ("XRANDR version < 1.2");
	return FALSE;
    }

#ifdef RR_PROPERTY_BACKLIGHT
    brightness->priv->backlight = XInternAtom (gdk_x11_get_default_xdisplay (), RR_PROPERTY_BACKLIGHT, True);
    if (brightness->priv->backlight == None) /* fall back to deprecated name */
#endif
    brightness->priv->backlight = XInternAtom (gdk_x11_get_default_xdisplay (), "BACKLIGHT", True);

    if (brightness->priv->backlight == None)
    {
	g_warning ("No outputs have backlight property");
	return FALSE;
    }

    screen = gdk_display_get_default_screen (gdisplay);

    screen_num = gdk_x11_screen_get_screen_number (screen);

    gdk_x11_display_error_trap_push (gdisplay);

    window = RootWindow (gdk_x11_get_default_xdisplay (), screen_num);

#if (RANDR_MAJOR == 1 && RANDR_MINOR >=3 )
    if (major > 1 || minor >= 3)
	brightness->priv->resource = XRRGetScreenResourcesCurrent (gdk_x11_get_default_xdisplay (), window);
    else
#endif
	brightness->priv->resource = XRRGetScreenResources (gdk_x11_get_default_xdisplay (), window);

    for ( i = 0; i < brightness->priv->resource->noutput; i++)
    {
	info = XRRGetOutputInfo (gdk_x11_get_default_xdisplay (), brightness->priv->resource, brightness->priv->resource->outputs[i]);

	if ( g_str_has_prefix (info->name, "LVDS") || g_str_has_prefix (info->name, "eDP") )
	{

	    if ( xfpm_brightness_xrand_get_limit (brightness, brightness->priv->resource->outputs[i], &min, &max) &&
		 min != max )
	    {
		ret = TRUE;
		brightness->priv->output = brightness->priv->resource->outputs[i];
		brightness->priv->step =  max <= 20 ? 1 : max / 10;
	    }

	}
	XRRFreeOutputInfo (info);
    }

    if (gdk_x11_display_error_trap_pop (gdisplay) != 0)
        g_critical ("Failed to get output/resource info");

    return ret;
}

static gboolean
xfpm_brightness_xrand_up (XfpmBrightness *brightness, gint32 *new_level)
{
    gint32 hw_level;
    gboolean ret = FALSE;
    gint32 set_level;

    ret = xfpm_brightness_xrandr_get_level (brightness, brightness->priv->output, &hw_level);

    if ( !ret )
	return FALSE;

    if ( hw_level == brightness->priv->max_level )
    {
	*new_level = brightness->priv->max_level;
	return TRUE;
    }

    set_level = MIN (hw_level + brightness->priv->step, brightness->priv->max_level );

    g_warn_if_fail (xfpm_brightness_xrandr_set_level (brightness, brightness->priv->output, set_level));

    ret = xfpm_brightness_xrandr_get_level (brightness, brightness->priv->output, new_level);

    if ( !ret )
    {
	g_warning ("xfpm_brightness_xrand_up failed for %d", set_level);
	return FALSE;
    }

    /* Nothing changed in the hardware*/
    if ( *new_level == hw_level )
    {
	g_warning ("xfpm_brightness_xrand_up did not change the hw level to %d", set_level);
	return FALSE;
    }

    return TRUE;
}

static gboolean
xfpm_brightness_xrand_down (XfpmBrightness *brightness, gint32 *new_level)
{
    gint32 hw_level;
    gboolean ret;
    gint32 set_level;

    ret = xfpm_brightness_xrandr_get_level (brightness, brightness->priv->output, &hw_level);

    if ( !ret )
	return FALSE;

    if ( hw_level == brightness->priv->min_level )
    {
	*new_level = brightness->priv->min_level;
	return TRUE;
    }

    set_level = MAX (hw_level - brightness->priv->step, brightness->priv->min_level);

    g_warn_if_fail (xfpm_brightness_xrandr_set_level (brightness, brightness->priv->output, set_level));

    ret = xfpm_brightness_xrandr_get_level (brightness, brightness->priv->output, new_level);

    if ( !ret )
    {
	g_warning ("xfpm_brightness_xrand_down failed for %d", set_level);
	return FALSE;
    }

    /* Nothing changed in the hardware*/
    if ( *new_level == hw_level )
    {
	g_warning ("xfpm_brightness_xrand_down did not change the hw level to %d", set_level);
	return FALSE;
    }

    return TRUE;
}

/*
 * Non-XRandR fallback using xfpm-backlight-helper
 */

#ifdef ENABLE_POLKIT

static gint
xfpm_brightness_helper_get_value (const gchar *argument)
{
    gboolean ret;
    GError *error = NULL;
    gchar *stdout_data = NULL;
    gint exit_status = 0;
    gint value = -1;
    gchar *command = NULL;

    command = g_strdup_printf (SBINDIR "/xfpm-power-backlight-helper --%s", argument);
    ret = g_spawn_command_line_sync (command,
	    &stdout_data, NULL, &exit_status, &error);
    if ( !ret )
    {
	g_warning ("failed to get value: %s", error->message);
	g_error_free (error);
	goto out;
    }
    g_debug ("executed %s; retval: %i", command, exit_status);
    if ( exit_status != 0 )
	goto out;

#if !defined(BACKEND_TYPE_FREEBSD)
    if ( stdout_data[0] == 'N' )
        value = 0;
    else if ( stdout_data[0] == 'Y' )
        value = 1;
    else
        value = atoi (stdout_data);
#else
    value = atoi (stdout_data);
#endif

out:
    g_free (command);
    g_free (stdout_data);
    return value;
}

static gboolean
xfpm_brightness_setup_helper (XfpmBrightness *brightness)
{
    gint32 ret;

    ret = (gint32) xfpm_brightness_helper_get_value ("get-max-brightness");
    g_debug ("xfpm_brightness_setup_helper: get-max-brightness returned %i", ret);
    if ( ret < 0 ) {
	brightness->priv->helper_has_hw = FALSE;
    } else {
	brightness->priv->helper_has_hw = TRUE;
	brightness->priv->min_level = 0;
	brightness->priv->max_level = ret;
	brightness->priv->step =  ret <= 20 ? 1 : ret / 10;
    }

    return brightness->priv->helper_has_hw;
}

static gboolean
xfpm_brightness_helper_get_level (XfpmBrightness *brg, gint32 *level)
{
    gint32 ret;

    if ( ! brg->priv->helper_has_hw )
	return FALSE;

    ret = (gint32) xfpm_brightness_helper_get_value ("get-brightness");

    g_debug ("xfpm_brightness_helper_get_level: get-brightness returned %i", ret);

    if ( ret >= 0 )
    {
	*level = ret;
	return TRUE;
    }

    return FALSE;
}

static gboolean
xfpm_brightness_helper_set_level (XfpmBrightness *brg, gint32 level)
{
    gboolean ret;
    GError *error = NULL;
    gint exit_status = 0;
    gchar *command = NULL;

    command = g_strdup_printf ("pkexec " SBINDIR "/xfpm-power-backlight-helper --set-brightness %i", level);
    ret = g_spawn_command_line_sync (command, NULL, NULL, &exit_status, &error);
    if ( !ret )
    {
	g_warning ("xfpm_brightness_helper_set_level: failed to set value: %s", error->message);
	g_error_free (error);
	goto out;
    }
    g_debug ("executed %s; retval: %i", command, exit_status);
    ret = (exit_status == 0);

out:
    g_free (command);
    return ret;
}

static gboolean
xfpm_brightness_helper_get_switch (XfpmBrightness *brg, gint *brightness_switch)
{
    gint ret;

    ret = xfpm_brightness_helper_get_value ("get-brightness-switch");

    if ( ret >= 0 )
    {
	*brightness_switch = ret;
	return TRUE;
    }

    return FALSE;
}

static gboolean
xfpm_brightness_helper_set_switch (XfpmBrightness *brg, gint brightness_switch)
{
    gboolean ret;
    GError *error = NULL;
    gint exit_status = 0;
    gchar *command = NULL;

    command = g_strdup_printf ("pkexec " SBINDIR "/xfpm-power-backlight-helper --set-brightness-switch %i", brightness_switch);
    ret = g_spawn_command_line_sync (command, NULL, NULL, &exit_status, &error);
    if ( !ret )
    {
	g_warning ("xfpm_brightness_helper_set_switch: failed to set value: %s", error->message);
	g_error_free (error);
	goto out;
    }
    g_debug ("executed %s; retval: %i", command, exit_status);
    ret = (exit_status == 0);

out:
    g_free (command);
    return ret;
}

static gboolean
xfpm_brightness_helper_up (XfpmBrightness *brightness, gint32 *new_level)
{
    gint32 hw_level;
    gboolean ret = FALSE;
    gint32 set_level;

    ret = xfpm_brightness_helper_get_level (brightness, &hw_level);

    if ( !ret )
	return FALSE;

    if ( hw_level >= brightness->priv->max_level )
    {
	*new_level = brightness->priv->max_level;
	return TRUE;
    }

    set_level = MIN (hw_level + brightness->priv->step, brightness->priv->max_level );

    g_warn_if_fail (xfpm_brightness_helper_set_level (brightness, set_level));

    ret = xfpm_brightness_helper_get_level (brightness, new_level);

    if ( !ret )
    {
	g_warning ("xfpm_brightness_helper_up failed for %d", set_level);
	return FALSE;
    }

    /* Nothing changed in the hardware*/
    if ( *new_level == hw_level )
    {
	g_warning ("xfpm_brightness_helper_up did not change the hw level to %d", set_level);
	return FALSE;
    }

    return TRUE;
}

static gboolean
xfpm_brightness_helper_down (XfpmBrightness *brightness, gint32 *new_level)
{
    gint32 hw_level;
    gboolean ret;
    gint32 set_level;

    ret = xfpm_brightness_helper_get_level (brightness, &hw_level);

    if ( !ret )
	return FALSE;

    if ( hw_level <= brightness->priv->min_level )
    {
	*new_level = brightness->priv->min_level;
	return TRUE;
    }

    set_level = MAX (hw_level - brightness->priv->step, brightness->priv->min_level);

    g_warn_if_fail (xfpm_brightness_helper_set_level (brightness, set_level));

    ret = xfpm_brightness_helper_get_level (brightness, new_level);

    if ( !ret )
    {
	g_warning ("xfpm_brightness_helper_down failed for %d", set_level);
	return FALSE;
    }

    /* Nothing changed in the hardware*/
    if ( *new_level == hw_level )
    {
	g_warning ("xfpm_brightness_helper_down did not change the hw level to %d", set_level);
	return FALSE;
    }

    return TRUE;
}

#endif

static void
xfpm_brightness_class_init (XfpmBrightnessClass *klass)
{
    GObjectClass *object_class = G_OBJECT_CLASS (klass);

    object_class->finalize = xfpm_brightness_finalize;
}

static void
xfpm_brightness_init (XfpmBrightness *brightness)
{
    brightness->priv = xfpm_brightness_get_instance_private (brightness);

    brightness->priv->resource = NULL;
    brightness->priv->xrandr_has_hw = FALSE;
    brightness->priv->helper_has_hw = FALSE;
    brightness->priv->max_level = 0;
    brightness->priv->min_level = 0;
    brightness->priv->current_level = 0;
    brightness->priv->output = 0;
    brightness->priv->step = 0;
}

static void
xfpm_brightness_free_data (XfpmBrightness *brightness)
{
    if ( brightness->priv->resource )
	XRRFreeScreenResources (brightness->priv->resource);
}

static void
xfpm_brightness_finalize (GObject *object)
{
    XfpmBrightness *brightness;

    brightness = XFPM_BRIGHTNESS (object);

    xfpm_brightness_free_data (brightness);

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

XfpmBrightness *
xfpm_brightness_new (void)
{
    XfpmBrightness *brightness = NULL;
    brightness = g_object_new (XFPM_TYPE_BRIGHTNESS, NULL);
    return brightness;
}

gboolean
xfpm_brightness_setup (XfpmBrightness *brightness)
{
    xfpm_brightness_free_data (brightness);
    brightness->priv->xrandr_has_hw = xfpm_brightness_setup_xrandr (brightness);

    if ( brightness->priv->xrandr_has_hw )
    {
	xfpm_brightness_xrand_get_limit (brightness,
					 brightness->priv->output,
					 &brightness->priv->min_level,
					 &brightness->priv->max_level);
	g_debug ("Brightness controlled by xrandr, min_level=%d max_level=%d",
		 brightness->priv->min_level,
		 brightness->priv->max_level);

	return TRUE;
    }
#ifdef ENABLE_POLKIT
    else
    {
	if ( xfpm_brightness_setup_helper (brightness) ) {
#if defined(BACKEND_TYPE_FREEBSD)
	    g_debug ("xrandr not available, brightness controlled by sysctl helper; min_level=%d max_level=%d",
#else
	    g_debug ("xrandr not available, brightness controlled by sysfs helper; min_level=%d max_level=%d",
#endif
		     brightness->priv->min_level,
		     brightness->priv->max_level);
	    return TRUE;
	}
    }
#endif
    g_debug ("no brightness controls available");
    return FALSE;
}

gboolean xfpm_brightness_up (XfpmBrightness *brightness, gint32 *new_level)
{
    gboolean ret = FALSE;

    if ( brightness->priv->xrandr_has_hw )
    {
	ret = xfpm_brightness_xrand_up (brightness, new_level);
    }
#ifdef ENABLE_POLKIT
    else if ( brightness->priv->helper_has_hw )
    {
	ret = xfpm_brightness_helper_up (brightness, new_level);
    }
#endif
    return ret;
}

gboolean xfpm_brightness_down (XfpmBrightness *brightness, gint32 *new_level)
{
    gboolean ret = FALSE;

    if ( brightness->priv->xrandr_has_hw )
    {
	ret = xfpm_brightness_xrand_down (brightness, new_level);
	if ( ret )
	    ret = xfpm_brightness_xrandr_get_level (brightness, brightness->priv->output, new_level);
    }
#ifdef ENABLE_POLKIT
    else if ( brightness->priv->helper_has_hw )
    {
	ret = xfpm_brightness_helper_down (brightness, new_level);
    }
#endif
    return ret;
}

gboolean xfpm_brightness_has_hw (XfpmBrightness *brightness)
{
    return brightness->priv->xrandr_has_hw || brightness->priv->helper_has_hw;
}

gint32 xfpm_brightness_get_max_level (XfpmBrightness *brightness)
{
    return brightness->priv->max_level;
}

gboolean xfpm_brightness_get_level	(XfpmBrightness *brightness, gint32 *level)
{
    gboolean ret = FALSE;

    if ( brightness->priv->xrandr_has_hw )
	ret = xfpm_brightness_xrandr_get_level (brightness, brightness->priv->output, level);
#ifdef ENABLE_POLKIT
    else if ( brightness->priv->helper_has_hw )
	ret = xfpm_brightness_helper_get_level (brightness, level);
#endif

    return ret;
}

gboolean xfpm_brightness_set_level (XfpmBrightness *brightness, gint32 level)
{
    gboolean ret = FALSE;

    if (brightness->priv->xrandr_has_hw )
	ret = xfpm_brightness_xrandr_set_level (brightness, brightness->priv->output, level);
#ifdef ENABLE_POLKIT
    else if ( brightness->priv->helper_has_hw )
	ret = xfpm_brightness_helper_set_level (brightness, level);
#endif

    return ret;
}

gboolean xfpm_brightness_dim_down (XfpmBrightness *brightness)
{
    gboolean ret = FALSE;

    if (brightness->priv->xrandr_has_hw )
	ret = xfpm_brightness_xrandr_set_level (brightness, brightness->priv->output, brightness->priv->min_level);
#ifdef ENABLE_POLKIT
    else if ( brightness->priv->helper_has_hw )
	ret = xfpm_brightness_helper_set_level (brightness, brightness->priv->min_level);
#endif

    return ret;
}

gboolean xfpm_brightness_get_switch (XfpmBrightness *brightness, gint *brightness_switch)
{
    gboolean ret = FALSE;

#ifdef ENABLE_POLKIT
    if ( brightness->priv->helper_has_hw )
    ret = xfpm_brightness_helper_get_switch (brightness, brightness_switch);
#endif

    return ret;
}

gboolean xfpm_brightness_set_switch (XfpmBrightness *brightness, gint brightness_switch)
{
    gboolean ret = FALSE;

#ifdef ENABLE_POLKIT
    if ( brightness->priv->helper_has_hw )
	ret = xfpm_brightness_helper_set_switch (brightness, brightness_switch);
#endif

    return ret;
}
