#define pr_fmt(fmt) "[hui][led] " fmt

#include "events.h"

#include <avm/hui/led.h>
#include "hui_internal.h"

#include <linux/list.h>
#include <linux/slab.h>

#include <avm/sammel/simple_proc.h>

#include "leds.h"
#include "hui_internal.h"
#include "log.h"

#define BLINK_CODE(...)                                                    \
	({                                                                 \
		struct blink_code ___code = { .params = { 0, 0 },          \
					      .color = led->current_color, \
					      __VA_ARGS__ };               \
		___code;                                                   \
	})
#define BLINK_CODE_SKIP BLINK_CODE(.type = blink_invalid)

#define COLOR(name)                                                         \
	static inline struct blink_code set_current_color_##name(           \
		struct led *led)                                            \
	{                                                                   \
		led->current_color =                                        \
			led_lookup_color(led, led_color_name_##name, true); \
		return BLINK_CODE_SKIP;                                     \
	}
#include "../def/colors.h"

static u8 __leds_max_location;
static inline u8 leds_max_location(void)
{
	return __leds_max_location;
}

static bool __led_all_dimmable;
static inline bool leds_all_dimmable(void)
{
	return __led_all_dimmable;
}

#define OVERLAYS(...) (led_overlay_t[]){ __VA_ARGS__ NULL }
#define OVERLAY(_name_) _name_

#include "../def/leds.c"

static LIST_HEAD(leds);
static struct kset *led_kset;

enum ambient_light_dim_modes { AMBIENT_DIMMING_OFF, AMBIENT_DIMMING_ON = 1 };

const struct led_type *lookup_led_type(const char *name)
{
	int i;

	for (i = 0; i < ARRAY_SIZE(led_types); i++) {
		if (sysfs_streq(led_types[i].name, name)) {
			return led_types + i;
		}
	}

	return NULL;
}

struct led_color led_lookup_color(const struct led *led,
				  enum led_color_name name, bool allow_fallback)
{
	if (!allow_fallback || led_color_valid(led->obj.colors[name]))
		return led->obj.colors[name];

	return hui_kobj->obj.colors[name];
}

struct led_color led_color_premultiply(struct led_color c)
{
	u32 i;
	struct led_color res;

	for (i = 0; i < 3; i++) {
		u32 t = c.c[i];

		t *= c.brightness;
		t /= 0xFF;

		res.c[i] = t;
	}

	res.brightness = 0xFF;

	return res;
}

/*
 * This function scales the brightness 0-100 into the brightness min/max range (0-255).
 *
 * Other color components will be left unchanged.
 */
static struct led_color
led_dim_brightness(const struct led *led, struct led_color color, u8 brightness)
{
	u32 t, t2;
	struct led_color res = color;

	// First calculate our effective min brightness
	t = color.brightness;
	t *= led->brightness_min;
	t /= 255;

	// Now calculate the variable part
	t2 = color.brightness;
	t2 *= led->brightness_max - led->brightness_min;
	t2 /= 255;

	// Now Change it according to the specified brightness
	// This is from 0-100
	t2 *= brightness;
	t2 /= 100;

	t += t2;

	res.brightness = t;

	return res;
}

static void update_leds_all_dimmable(void) {
	struct led *led;
	bool all_dimmable = true;

	list_for_each_entry(led, &leds, list) {
		if (led->options.location == 0)
			continue;

		if (!led->dimmable)
			all_dimmable = false;
	}

	__led_all_dimmable = all_dimmable;
}

/*
 * This function will iterate over all overlays in a led and call each in turn.
 * As soon as one returns an valid blink_code, the evaluation is stopped and
 * this new code is stored.
 */
static void led_calculate(struct led *led)
{
	struct blink_code code = BLINK_CODE_SKIP;
	led_overlay_t overlay;
	int j;

	pr_debug("Update led <%s> with type <%s>\n", led->name, led->type->name);

	// Set default color to white
	led->current_color.brightness = 0xFF;
	led->current_color.c[0] = 0xFF;
	led->current_color.c[1] = 0xFF;
	led->current_color.c[2] = 0xFF;

	for (j = 0, overlay = led->type->overlays[j];
	     overlay != NULL;
	     j++, overlay = led->type->overlays[j]) {
		pr_debug("  -> consult %ps\n", overlay);
		code = overlay(led);

		if (code.type != blink_invalid) {
			led->current_code = code;
			led->current_code_source = overlay;
			break;
		}
	}
}

void leds_update(void)
{
	struct led *led;

	lockdep_assert_held(&hui_update_mutex);

	list_for_each_entry(led, &leds, list) {
		struct blink_code old_code = led->current_code;

		if (led->forced_code.type != blink_invalid) {
			pr_debug("Force led <%s>\n", led->name);

			led->current_code = led->forced_code;
			led->current_code_source = NULL;
		} else
			led_calculate(led);

		if (!blink_code_equals(old_code, led->current_code))
			hui_log_led(led);
	}
}

/*
 * The brightness calculation is done as extra step, as this may change quite
 * often, when using auto dimming.
 *
 * This way we can recalculate the brightness without consulting all the
 * overlays again. Those would just calculate the same result.
 *
 * This function also calls the output driver. If the driver needs
 * pre-multiplied colors, it can use #led_color_premultiply.
 */
#define DIM_INTERVALL_SIZE 50
#define DIM_MAX_VALUE 100
#define DIM_MIN_VALUE 3
void leds_update_output(void)
{
	struct led *led;

	lockdep_assert_held(&hui_update_mutex);

	list_for_each_entry(led, &leds, list) {
		struct blink_code code = led->current_code;
		u8 brightness = 100;

		if (led->dimmable) {
			bool is_ambient_dimming = event_get_value(dim_modus) == AMBIENT_DIMMING_ON;

			if (is_ambient_dimming) {
				u8 interval_min;

				if (hui_kobj->brightness + DIM_INTERVALL_SIZE/2 > 100) {
					interval_min = 100-DIM_INTERVALL_SIZE;
				} else if (hui_kobj->brightness - DIM_INTERVALL_SIZE/2 < 0) {
					interval_min = DIM_MIN_VALUE;
				} else {
					interval_min = hui_kobj->brightness - DIM_INTERVALL_SIZE/2;
				}
				brightness = interval_min + DIM_INTERVALL_SIZE*hui_kobj->ambient_light/100;
			} else {
				brightness = hui_kobj->brightness;
			}
		}

		code.color = led_dim_brightness(
				led, code.color, brightness);

		led->ops->out(code, led->ops_ctx);
	}
}

void *avm_hui_add_led(const char *name, const char *type_name,
		      struct device_node *node,
		      const struct avm_hui_led_ops *ops, void *ctx)
{
	int err;
	const struct led_type *type;
	struct led *led;

	type = lookup_led_type(type_name);

	if (!type) {
		type = lookup_led_type("unknown");
		pr_err("Unknown led type %s for led %s\n", type_name, name);
	}

	led = kzalloc(sizeof(struct led), GFP_KERNEL);
	if (!led)
		return ERR_PTR(-ENOMEM);

	led->obj.kobj.kset = led_kset;

	led->name = name;
	led->type = type;
	led->ops = ops;
	led->ops_ctx = ctx;
	led->forced_code.type = blink_invalid;
	led->brightness_min = 0;
	led->brightness_max = 255;

	// First copy the original options and then use the dt node to update
	// them
	led->options = type->options;

	if (node) {
		u32 temp;

		if (!of_property_read_u32(node, "location", &temp))
			led->options.location = temp;

		led->dimmable = of_property_read_bool(node, "dimmable");
		if (!of_property_read_u32_index(node, "brightness-range", 0,
						&temp))
			led->brightness_min = temp;
		if (!of_property_read_u32_index(node, "brightness-range", 1,
						&temp))
			led->brightness_max = temp;

		colored_kobject_init_from_dt(&led->obj, node);
	}

	err = kobject_init_and_add(&led->obj.kobj, &led_ktype, NULL, "%s",
				   name);
	if (err) {
		return ERR_PTR(err);
	}

	err = sysfs_create_group(&led->obj.kobj, &colored_attribute_group);
	if (err) {
		kobject_put(&led->obj.kobj);
		return ERR_PTR(err);
	}

	mutex_lock(&hui_update_mutex);
	list_add_tail(&led->list, &leds);

	if (led->options.location > __leds_max_location)
		__leds_max_location = led->options.location;
	update_leds_all_dimmable();

	mutex_unlock(&hui_update_mutex);

	hui_schedule_update();
	return led;
}
EXPORT_SYMBOL(avm_hui_add_led);

void avm_hui_remove_led(void *handle)
{
	struct led *led = handle;

	if (!led || IS_ERR(led))
		return;

	mutex_lock(&hui_update_mutex);
	list_del(&led->list);
	mutex_unlock(&hui_update_mutex);

	kfree(led);
}
EXPORT_SYMBOL(avm_hui_remove_led);

#if IS_ENABLED(CONFIG_AVM_FLASH_UPDATE)
static bool hui_is_bare = false;

/*
 * This is called, when the linux kernel is stopped and the flash_update
 * module assumed total control.
 *
 * Therefore we do not actually need to disable the timer. We just turn off all
 * possible leds.
 *
 * This is called from an atomic context.
 */
void led_event_disable_timer(void)
{
	struct led *led;

	hui_is_bare = true;

	list_for_each_entry (led, &leds, list) {
		struct led_color color = { 0 };

		if (!led->ops->raw_out)
			continue;

		led->ops->raw_out(color, led->ops_ctx);
	}
}
EXPORT_SYMBOL(led_event_disable_timer);

int hui_bare_handle_event(enum _led_event event, int val)
{
	struct led *led;

	if (!hui_is_bare)
		return 0;

	list_for_each_entry (led, &leds, list) {
		struct led_color color = { 0 };

		if (!led->ops->raw_out)
			continue;

		if (!(led->options.flags & LED_FLAG_BARE_UPDATE))
			continue;

		if (val)
			color = led_lookup_color(led, led_color_name_normal,
						 true);

		led->ops->raw_out(color, led->ops_ctx);
	}

	return 1;
}
#endif

static void proc_led_state(struct seq_file *seq, void *context)
{
	struct led *led;

	mutex_lock(&hui_update_mutex);
	list_for_each_entry (led, &leds, list) {
		seq_printf(seq, "%s (%s): %d (%d, %d, #%02X%02X%02X) by %ps\n",
			   led->name, led->type->name, led->current_code.type,
			   led->current_code.params[0],
			   led->current_code.params[1],
			   led->current_code.color.c[0],
			   led->current_code.color.c[1],
			   led->current_code.color.c[2],
			   led->current_code_source);
	}
	mutex_unlock(&hui_update_mutex);
}

int leds_init(void)
{
	led_kset = kset_create_and_add("led", NULL, &hui_kobj->obj.kobj);
	if (!led_kset)
		return -ENOMEM;

	add_simple_proc_file("avm/hui/led_state", NULL, proc_led_state, NULL);

	return 0;
}

void leds_exit(void)
{
	remove_simple_proc_file("avm/hui/led_state");
}