/*
 * Copyright © 2024 Red Hat, Inc.
 *
 * Permission to use, copy, modify, distribute, and sell this software
 * and its documentation for any purpose is hereby granted without
 * fee, provided that the above copyright notice appear in all copies
 * and that both that copyright notice and this permission notice
 * appear in supporting documentation, and that the name of Red Hat
 * not be used in advertising or publicity pertaining to distribution
 * of the software without specific, written prior permission.  Red
 * Hat makes no representations about the suitability of this software
 * for any purpose.  It is provided "as is" without express or implied
 * warranty.
 *
 * THE AUTHORS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
 * INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
 * NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY SPECIAL, INDIRECT OR
 * CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
 * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
 * NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * Authors:
 *        Peter Hutterer <peter.hutterer@redhat.com>
 */

#include "config.h"

#include <glib.h>
#include <glib/gi18n.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include "libwacom.h"

static char *database_path;
static gboolean with_styli;

/* clang-format off */
static GOptionEntry opts[] = {
	{ "database", 0, 0, G_OPTION_ARG_FILENAME, &database_path, N_("Path to device database"), NULL },
	{ "with-styli", 0, 0, G_OPTION_ARG_NONE, &with_styli, N_("Select to also list styli for this device"), NULL },
	{ .long_name = NULL }
};
/* clang-format on */

static int indent = 0;

#define push() indent += 2
#define pop() indent -= 2

#define ip(fmt_, ...) \
	printf("%-*s" fmt_, indent, "", __VA_ARGS__)
/* Usage: p("function_name", "return value is %d", a) */
#define p(f_, fmt_, ...) \
	printf("%-*s%-*s -> " fmt_ "\n", indent, "", (46 - indent), f_, __VA_ARGS__)

/* Usage: func(myfunc, "return value is %d", a) */
#define func(f_, fmt_, ...) \
	p(#f_"()", fmt_, __VA_ARGS__)

/* Usage: func(myfunc, "%d", argval, "return value is %d", a) */
#define func_arg(f_, arg_fmt_, arg_val_, fmt_, ...) \
	do { \
		char buf_[256]; \
		snprintf(buf_, sizeof(buf_), #f_"(" arg_fmt_ ")", arg_val_); \
		p(buf_, fmt_, __VA_ARGS__); \
	} while(0)

#define strfunc(f_, dev_) \
	func(f_, "\"%s\"", f_(dev_))

#define hexfunc(f_, dev_) \
	func(f_, "0x%04x", f_(dev_))

#define intfunc(f_, dev_) \
	func(f_, "%d", f_(dev_))

static void
handle_match(const WacomMatch *m)
{
	if (m == NULL) {
		printf(" <none>\n");
		return;
	}
	push();
	ip("%s\n", "{");
	push();
	strfunc(libwacom_match_get_match_string, m);
	strfunc(libwacom_match_get_name, m);
	strfunc(libwacom_match_get_uniq, m);
	hexfunc(libwacom_match_get_bustype, m);
	hexfunc(libwacom_match_get_vendor_id, m);
	hexfunc(libwacom_match_get_product_id, m);
	pop();
	ip("%s\n", "}");
	pop();
}

static WacomDevice *
device_from_device_match(WacomDeviceDatabase *db,
			 char **parts)
{
	WacomBusType bustype;
	guint64 vid, pid;
	const char *name = NULL;
	const char *uniq = NULL;
	WacomBuilder *builder;
	WacomDevice *device;

	if (g_str_equal(parts[0], "usb"))
		bustype = WBUSTYPE_USB;
	else if (g_str_equal(parts[0], "serial"))
		bustype = WBUSTYPE_SERIAL;
	else if (g_str_equal(parts[0], "bluetooth"))
		bustype = WBUSTYPE_BLUETOOTH;
	else if (g_str_equal(parts[0], "i2c"))
		bustype = WBUSTYPE_I2C;
	else {
		fprintf(stderr, "Unknown bus type %s\n", parts[0]);
		return NULL;
	}

	if (!g_ascii_string_to_unsigned(parts[1], 16, 0, 0xffff, &vid, NULL) ||
	    !g_ascii_string_to_unsigned(parts[2], 16, 0, 0xffff, &pid, NULL)) {
		fprintf(stderr, "Failed to parse vid/pid\n");
		return NULL;
	}

	if (parts[3]) {
		name = parts[3];
		if (parts[4])
			uniq = parts[4];
	}

	builder = libwacom_builder_new();
	libwacom_builder_set_bustype(builder, bustype);
	libwacom_builder_set_usbid(builder, vid, pid);
	if (name)
		libwacom_builder_set_match_name(builder, name);
	if (name)
		libwacom_builder_set_uniq(builder, uniq);

	device = libwacom_new_from_builder(db, builder, WFALLBACK_NONE, NULL);
	libwacom_builder_destroy(builder);
	return device;
}

static int
handle_device(WacomDeviceDatabase *db,
	      const char *path)
{
	WacomDevice *device;
	g_auto(GStrv) parts = g_strsplit(path, "|", 5);

	if (parts && parts[0] && parts[1]) {
		device = device_from_device_match(db, parts);
	} else {
		device = libwacom_new_from_path(db, path, WFALLBACK_NONE, NULL);
	}

	if (!device) {
		fprintf(stderr, "Device not known to libwacom\n");
		return EXIT_FAILURE;
	}

	strfunc(libwacom_get_name, device);
	strfunc(libwacom_get_model_name, device);
	strfunc(libwacom_get_layout_filename, device);

	hexfunc(libwacom_get_vendor_id, device);
	hexfunc(libwacom_get_product_id, device);
	{
		char *busstr = NULL;
		switch (libwacom_get_bustype(device)) {
		case WBUSTYPE_UNKNOWN:
			busstr = "UNKNOWN";
			break;
		case WBUSTYPE_USB:
			busstr = "USB";
			break;
		case WBUSTYPE_SERIAL:
			busstr = "SERIAL";
			break;
		case WBUSTYPE_BLUETOOTH:
			busstr = "BLUETOOTH";
			break;
		case WBUSTYPE_I2C:
			busstr = "I2C";
			break;
		}
		func(libwacom_get_bustype, "%s", busstr);
	}

	{
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
		WacomClass cls = libwacom_get_class(device);
#pragma GCC diagnostic pop
		const char *str = NULL;

		switch (cls) {
		case WCLASS_UNKNOWN:
			str = "UNKNOWN";
			break;
		case WCLASS_INTUOS3:
			str = "INTUOS3";
			break;
		case WCLASS_INTUOS4:
			str = "INTUOS4";
			break;
		case WCLASS_INTUOS5:
			str = "INTUOS5";
			break;
		case WCLASS_CINTIQ:
			str = "CINTIQ";
			break;
		case WCLASS_BAMBOO:
			str = "BAMBOO";
			break;
		case WCLASS_GRAPHIRE:
			str = "GRAPHIRE";
			break;
		case WCLASS_ISDV4:
			str = "ISDV4";
			break;
		case WCLASS_INTUOS:
			str = "INTUOS";
			break;
		case WCLASS_INTUOS2:
			str = "INTUOS2";
			break;
		case WCLASS_PEN_DISPLAYS:
			str = "PEN_DISPLAYS";
			break;
		case WCLASS_REMOTE:
			str = "REMOTE";
			break;
			break;
		}
		func(libwacom_get_class, "%s", str);
	}

	intfunc(libwacom_get_width, device);
	intfunc(libwacom_get_height, device);
	intfunc(libwacom_get_width_mm, device);
	intfunc(libwacom_get_height_mm, device);

	intfunc(libwacom_is_reversible, device);

	{
		const WacomMatch **matches = libwacom_get_matches(device);
		const WacomMatch **m;

		printf("libwacom_get_matches() -> {\n");
		for (m = matches; *m; m++) {
			handle_match(*m);
		}
		printf("}\n");
	}

	strfunc(libwacom_get_match, device);

	printf("libwacom_get_paired_device() -> {");
	handle_match(libwacom_get_paired_device(device));
	printf("}\n");

	intfunc(libwacom_has_stylus, device);
	intfunc(libwacom_has_touch, device);
	intfunc(libwacom_get_num_buttons, device);
	intfunc(libwacom_get_num_keys, device);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
	intfunc(libwacom_has_ring, device);
	intfunc(libwacom_has_ring2, device);
#pragma GCC diagnostic pop
	intfunc(libwacom_has_touchswitch, device);
	intfunc(libwacom_get_ring_num_modes, device);
	intfunc(libwacom_get_ring2_num_modes, device);
	intfunc(libwacom_get_num_strips, device);
	intfunc(libwacom_get_strips_num_modes, device);
	intfunc(libwacom_get_num_dials, device);
	intfunc(libwacom_get_dial_num_modes, device);
	intfunc(libwacom_get_dial2_num_modes, device);

	{
		WacomIntegrationFlags flags = libwacom_get_integration_flags(device);
		func(libwacom_get_integration_flags,
		     "%s%s %s %s",
		     flags == WACOM_DEVICE_INTEGRATED_NONE ? "NONE" : "",
		     flags == WACOM_DEVICE_INTEGRATED_DISPLAY ? "DISPLAY" : "",
		     flags == WACOM_DEVICE_INTEGRATED_SYSTEM ? "SYSTEM" : "",
		     flags == WACOM_DEVICE_INTEGRATED_REMOTE ? "REMOTE" : "");
	}

	{
		for (int i = 0; i < libwacom_get_num_buttons(device); i++) {
			char b = 'A' + i;
			func_arg(libwacom_get_button_led_group,
				 "%c",
				 b,
				 "%d",
				 libwacom_get_button_led_group(device, b));
		}

		for (int i = 0; i < libwacom_get_num_buttons(device); i++) {
			char b = 'A' + i;
			func_arg(libwacom_get_button_evdev_code,
				 "%c",
				 b,
				 "0x%x",
				 libwacom_get_button_evdev_code(device, b));
		}

		for (int i = 0; i < libwacom_get_num_buttons(device); i++) {
			char b = 'A' + i;
			WacomButtonFlags flags;

			flags = libwacom_get_button_flag(device, b);
			func_arg(libwacom_get_button_flag,
				 "%c",
				 b,
				 "%s%s%s%s%s%s%s%s%s%s%s%s",
				 flags == WACOM_BUTTON_NONE ? "NONE" : "",
				 flags & WACOM_BUTTON_POSITION_LEFT ? "POSITION_LEFT|"
								    : "",
				 flags & WACOM_BUTTON_POSITION_RIGHT ? "POSITION_RIGHT|"
								     : "",
				 flags & WACOM_BUTTON_POSITION_TOP ? "POSITION_TOP|"
								   : "",
				 flags & WACOM_BUTTON_POSITION_BOTTOM
					 ? "POSITION_BOTTOM|"
					 : "",
				 flags & WACOM_BUTTON_RING_MODESWITCH
					 ? "RING_MODESWITCH|"
					 : "",
				 flags & WACOM_BUTTON_RING2_MODESWITCH
					 ? "RING2_MODESWITCH|"
					 : "",
				 flags & WACOM_BUTTON_TOUCHSTRIP_MODESWITCH
					 ? "TOUCHSTRIP_MODESWITCH|"
					 : "",
				 flags & WACOM_BUTTON_TOUCHSTRIP2_MODESWITCH
					 ? "TOUCHSTRIP2_MODESWITCH|"
					 : "",
				 flags & WACOM_BUTTON_DIAL_MODESWITCH
					 ? "DIAL_MODESWITCH|"
					 : "",
				 flags & WACOM_BUTTON_DIAL2_MODESWITCH
					 ? "DIAL2_MODESWITCH|"
					 : "",
				 flags & WACOM_BUTTON_OLED ? "OLED " : "");
		}

		for (int i = 0; i < libwacom_get_num_buttons(device); i++) {
			char b = 'A' + i;
			WacomModeSwitch mode;

			if ((libwacom_get_button_flag(device, b) &
			     WACOM_BUTTON_MODESWITCH) == 0)
				continue;

			mode = libwacom_get_button_modeswitch_mode(device, b);
			if (mode == WACOM_MODE_SWITCH_NEXT)
				func_arg(libwacom_get_button_modeswitch_mode,
					 "%c",
					 b,
					 "%s",
					 "MODE_SWITCH_NEXT");
			else
				func_arg(libwacom_get_button_modeswitch_mode,
					 "%c",
					 b,
					 "%d",
					 (int)mode);
		}
	}

	{
		g_autoptr(GStrvBuilder) builder = g_strv_builder_new();
		g_auto(GStrv) strv = NULL;
		g_autofree char *str = NULL;
		int nleds;
		const WacomStatusLEDs *leds = libwacom_get_status_leds(device, &nleds);

		for (int i = 0; i < nleds; i++) {
			const char *ledstr = "UNKNOWN";
			switch (leds[i]) {
			case WACOM_STATUS_LED_UNAVAILABLE:
				ledstr = "UNAVAILABLE";
				break;
			case WACOM_STATUS_LED_RING:
				ledstr = "RING";
				break;
			case WACOM_STATUS_LED_RING2:
				ledstr = "RING2";
				break;
			case WACOM_STATUS_LED_TOUCHSTRIP:
				ledstr = "TOUCHSTRIP";
				break;
			case WACOM_STATUS_LED_TOUCHSTRIP2:
				ledstr = "TOUCHSTRIP2";
				break;
			case WACOM_STATUS_LED_DIAL:
				ledstr = "DIAL";
				break;
			case WACOM_STATUS_LED_DIAL2:
				ledstr = "DIAL2";
				break;
			}
			g_strv_builder_add(builder, ledstr);
		}
		strv = g_strv_builder_end(builder);
		str = g_strjoinv(", ", strv);
		func(libwacom_get_status_leds, "[%s]", str);
	}

	{
		int nstyli;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
		const int *styli = libwacom_get_supported_styli(device, &nstyli);
#pragma GCC diagnostic pop

		{
			g_autoptr(GStrvBuilder) builder = g_strv_builder_new();
			g_auto(GStrv) strv = NULL;
			g_autofree char *str = NULL;
			for (int i = 0; i < nstyli; i++) {
				g_autofree char *s =
					g_strdup_printf("0x%06x", styli[i]);
				g_strv_builder_add(builder, s);
			}
			strv = g_strv_builder_end(builder);
			str = g_strjoinv(", ", strv);
			func(libwacom_get_supported_styli, "[%s]", str);
		}
	}

	{
		int nstyli;
		g_autofree const WacomStylus **styli =
			libwacom_get_styli(device, &nstyli);
		{
			g_autoptr(GStrvBuilder) builder = g_strv_builder_new();
			g_auto(GStrv) strv = NULL;
			g_autofree char *str = NULL;
			for (int i = 0; i < nstyli; i++) {
				const WacomStylus *s = styli[i];
				g_autofree char *entry = g_strdup_printf(
					"[0x%04x, 0x%06x]",
					libwacom_stylus_get_vendor_id(s),
					libwacom_stylus_get_id(s));
				g_strv_builder_add(builder, entry);
			}
			strv = g_strv_builder_end(builder);
			str = g_strjoinv(", ", strv);
			func(libwacom_get_styli, "[%s]", str);
		}

		if (with_styli) {
			printf("\n---------- Listing styli for this device ----------\n");

			for (int i = 0; i < nstyli; i++) {
				const WacomStylus *stylus = styli[i];
				int id = libwacom_stylus_get_id(stylus);

				ip("%s\n", "{");
				push();
				func_arg(libwacom_stylus_get_id,
					 "0x%04x",
					 id,
					 "0x%04x",
					 libwacom_stylus_get_id(stylus));
				func_arg(libwacom_stylus_get_name,
					 "0x%04x",
					 id,
					 "%s",
					 libwacom_stylus_get_name(stylus));
				func_arg(libwacom_stylus_get_num_buttons,
					 "0x%04x",
					 id,
					 "%d",
					 libwacom_stylus_get_num_buttons(stylus));
				func_arg(libwacom_stylus_has_eraser,
					 "0x%04x",
					 id,
					 "%d",
					 libwacom_stylus_has_eraser(stylus));
				func_arg(libwacom_stylus_is_eraser,
					 "0x%04x",
					 id,
					 "%d",
					 libwacom_stylus_is_eraser(stylus));
				func_arg(libwacom_stylus_has_lens,
					 "0x%04x",
					 id,
					 "%d",
					 libwacom_stylus_has_lens(stylus));
				func_arg(libwacom_stylus_has_wheel,
					 "0x%04x",
					 id,
					 "%d",
					 libwacom_stylus_has_wheel(stylus));

				{
					g_autoptr(GStrvBuilder) builder =
						g_strv_builder_new();
					g_auto(GStrv) strv = NULL;
					g_autofree char *str = NULL;
					int npaired;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
					const int *paired =
						libwacom_stylus_get_paired_ids(
							stylus,
							&npaired);
#pragma GCC diagnostic pop

					for (int i = 0; i < npaired; i++) {
						g_autofree char *s =
							g_strdup_printf("0x%06x",
									paired[i]);
						g_strv_builder_add(builder, s);
					}
					strv = g_strv_builder_end(builder);
					str = g_strjoinv(", ", strv);
					func_arg(libwacom_stylus_get_paired_ids,
						 "0x%04x",
						 id,
						 "[%s]",
						 str);
				}

				{
					g_autoptr(GStrvBuilder) builder =
						g_strv_builder_new();
					g_auto(GStrv) strv = NULL;
					g_autofree char *str = NULL;
					int npaired;
					g_autofree const WacomStylus **paired =
						libwacom_stylus_get_paired_styli(
							stylus,
							&npaired);

					for (int i = 0; i < npaired; i++) {
						const WacomStylus *p = paired[i];
						g_autofree char *entry = g_strdup_printf(
							"[0x%04x, 0x%06x]",
							libwacom_stylus_get_vendor_id(
								p),
							libwacom_stylus_get_id(p));
						g_strv_builder_add(builder, entry);
					}
					strv = g_strv_builder_end(builder);
					str = g_strjoinv(", ", strv);
					func_arg(libwacom_stylus_get_paired_ids,
						 "0x%04x",
						 id,
						 "[%s]",
						 str);
				}

				{
					WacomAxisTypeFlags flags =
						libwacom_stylus_get_axes(stylus);
					func_arg(libwacom_stylus_has_wheel,
						 "0x%04x",
						 id,
						 "%s%s%s%s%s%s",
						 flags == WACOM_AXIS_TYPE_NONE ? "NONE"
									       : "",
						 flags & WACOM_AXIS_TYPE_TILT ? "TILT|"
									      : "",
						 flags & WACOM_AXIS_TYPE_ROTATION_Z
							 ? "ROTATION_Z|"
							 : "",
						 flags & WACOM_AXIS_TYPE_DISTANCE
							 ? "DISTANCE|"
							 : "",
						 flags & WACOM_AXIS_TYPE_PRESSURE
							 ? "PRESSURE|"
							 : "",
						 flags & WACOM_AXIS_TYPE_SLIDER
							 ? "SLIDER"
							 : "");
				}

				{
					const char *typestr = NULL;
					switch (libwacom_stylus_get_type(stylus)) {
					case WSTYLUS_UNKNOWN:
						typestr = "UNKNOWN";
						break;
					case WSTYLUS_GENERAL:
						typestr = "GENERAL";
						break;
					case WSTYLUS_INKING:
						typestr = "INKING";
						break;
					case WSTYLUS_AIRBRUSH:
						typestr = "AIRBRUSH";
						break;
					case WSTYLUS_CLASSIC:
						typestr = "CLASSIC";
						break;
					case WSTYLUS_MARKER:
						typestr = "MARKER";
						break;
					case WSTYLUS_STROKE:
						typestr = "STROKE";
						break;
					case WSTYLUS_PUCK:
						typestr = "PUCK";
						break;
					case WSTYLUS_3D:
						typestr = "3D";
						break;
					case WSTYLUS_MOBILE:
						typestr = "MOBILE";
						break;
					}

					func_arg(libwacom_stylus_get_type,
						 "0x%04x",
						 id,
						 "%s",
						 typestr);
				}

				{
					const char *eraserstr = NULL;
					switch (libwacom_stylus_get_eraser_type(
						stylus)) {
					case WACOM_ERASER_UNKNOWN:
						eraserstr = "UNKNOWN";
						break;
					case WACOM_ERASER_NONE:
						eraserstr = "NONE";
						break;
					case WACOM_ERASER_INVERT:
						eraserstr = "INVERT";
						break;
					case WACOM_ERASER_BUTTON:
						eraserstr = "BUTTON";
						break;
					}

					func_arg(libwacom_stylus_get_type,
						 "0x%04x",
						 id,
						 "%s",
						 eraserstr);
				}
				pop();
				ip("%s\n", "}");
			}
		}
	}

	libwacom_destroy(device);

	return EXIT_SUCCESS;
}

int
main(int argc,
     char **argv)
{
	WacomDeviceDatabase *db;
	GOptionContext *context;
	g_autoptr(GError) error = NULL;
	int rc;

	context = g_option_context_new(
		"[/dev/input/event0 | \"usb|0123|abcd|some tablet\"]");
	g_option_context_set_description(
		context,
		"The argument may be a device node or a single "
		"DeviceMatch string as listed in .tablet files.");

	g_option_context_add_main_entries(context, opts, NULL);

	if (!g_option_context_parse(context, &argc, &argv, &error)) {
		if (error != NULL)
			fprintf(stderr, "%s\n", error->message);
		return EXIT_FAILURE;
	}

	g_option_context_free(context);

	if (database_path) {
		db = libwacom_database_new_for_path(database_path);
		g_free(database_path);
	} else {
#ifdef DATABASEPATH
		db = libwacom_database_new_for_path(DATABASEPATH);
#else
		db = libwacom_database_new();
#endif
	}

	if (!db) {
		fprintf(stderr, "Failed to initialize device database\n");
		return EXIT_FAILURE;
	}

	if (argc <= 1) {
		fprintf(stderr, "Missing device node or match string\n");
		libwacom_database_destroy(db);
		return EXIT_FAILURE;
	}

	rc = handle_device(db, argv[1]);

	libwacom_database_destroy(db);
	return rc;
}

/* vim: set noexpandtab tabstop=8 shiftwidth=8: */
