/*
 * BlueALSA - main.c
 * Copyright (c) 2016-2018 Arkadiusz Bokowy
 *
 * This file is a part of bluez-alsa.
 *
 * This project is licensed under the terms of the MIT license.
 *
 */

#if HAVE_CONFIG_H
# include "config.h"
#endif

#include <errno.h>
#include <getopt.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>

#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>

#include <glib.h>
#include <gio/gio.h>

#if ENABLE_LDAC
# include <ldacBT.h>
#endif

#include "bluealsa.h"
#include "bluez.h"
#include "ctl.h"
#include "transport.h"
#include "utils.h"
#include "shared/defs.h"
#include "shared/log.h"


static char *get_a2dp_codecs(
		const struct bluez_a2dp_codec **codecs,
		enum bluez_a2dp_dir dir) {

	const char *tmp[16] = { NULL };
	int i = 0;

	while (*codecs != NULL) {
		const struct bluez_a2dp_codec *c = *codecs++;
		if (c->dir != dir)
			continue;
		tmp[i++] = bluetooth_a2dp_codec_to_string(c->id);
	}

	return g_strjoinv(", ", (char **)tmp);
}

static GMainLoop *loop = NULL;
static void main_loop_stop(int sig) {
	/* Call to this handler restores the default action, so on the
	 * second call the program will be forcefully terminated. */

	struct sigaction sigact = { .sa_handler = SIG_DFL };
	sigaction(sig, &sigact, NULL);

	g_main_loop_quit(loop);
}

int main(int argc, char **argv) {

	int opt;
	const char *opts = "hVSi:p:";
	const struct option longopts[] = {
		{ "help", no_argument, NULL, 'h' },
		{ "version", no_argument, NULL, 'V' },
		{ "syslog", no_argument, NULL, 'S' },
		{ "device", required_argument, NULL, 'i' },
		{ "profile", required_argument, NULL, 'p' },
		{ "a2dp-force-mono", no_argument, NULL, 6 },
		{ "a2dp-force-audio-cd", no_argument, NULL, 7 },
		{ "a2dp-keep-alive", required_argument, NULL, 8 },
		{ "a2dp-volume", no_argument, NULL, 9 },
#if ENABLE_AAC
		{ "aac-afterburner", no_argument, NULL, 4 },
		{ "aac-vbr-mode", required_argument, NULL, 5 },
#endif
#if ENABLE_LDAC
		{ "ldac-abr", no_argument, NULL, 10 },
		{ "ldac-eqmid", required_argument, NULL, 11 },
#endif
		{ 0, 0, 0, 0 },
	};

	bool syslog = false;
	struct hci_dev_info *hci_devs;
	int hci_devs_num;

	/* Check if syslog forwarding has been enabled. This check has to be
	 * done before anything else, so we can log early stage warnings and
	 * errors. */
	opterr = 0;
	while ((opt = getopt_long(argc, argv, opts, longopts, NULL)) != -1)
		switch (opt) {
		case 'S':
			syslog = true;
			break;
		case 'p':
			/* reset defaults if user has specified profile option */
			memset(&config.enable, 0, sizeof(config.enable));
			break;
		}

	log_open(argv[0], syslog, BLUEALSA_LOGTIME);

	if (bluealsa_config_init() != 0) {
		error("Couldn't initialize bluealsa config");
		return EXIT_FAILURE;
	}

	if (hci_devlist(&hci_devs, &hci_devs_num)) {
		error("Couldn't enumerate HCI devices: %s", strerror(errno));
		return EXIT_FAILURE;
	}

	if (!hci_devs_num) {
		error("No HCI device available");
		return EXIT_FAILURE;
	}

	{ /* try to get default device (if possible get active one) */
		int i;
		for (i = 0; i < hci_devs_num; i++)
			if (i == 0 || hci_test_bit(HCI_UP, &hci_devs[i].flags))
				memcpy(&config.hci_dev, &hci_devs[i], sizeof(config.hci_dev));
	}

	/* parse options */
	optind = 0; opterr = 1;
	while ((opt = getopt_long(argc, argv, opts, longopts, NULL)) != -1)
		switch (opt) {

		case 'h' /* --help */ :
			printf("Usage:\n"
					"  %s [OPTION]...\n"
					"\nOptions:\n"
					"  -h, --help\t\tprint this help and exit\n"
					"  -V, --version\t\tprint version and exit\n"
					"  -S, --syslog\t\tsend output to syslog\n"
					"  -i, --device=hciX\tHCI device to use\n"
					"  -p, --profile=NAME\tenable BT profile\n"
					"  --a2dp-force-mono\tforce monophonic sound\n"
					"  --a2dp-force-audio-cd\tforce 44.1 kHz sampling\n"
					"  --a2dp-keep-alive=SEC\tkeep A2DP transport alive\n"
					"  --a2dp-volume\t\tcontrol volume natively\n"
#if ENABLE_AAC
					"  --aac-afterburner\tenable afterburner\n"
					"  --aac-vbr-mode=NB\tset VBR mode to NB\n"
#endif
#if ENABLE_LDAC
					"  --ldac-abr\t\tenable adaptive bit rate\n"
					"  --ldac-eqmid=NB\tset encoder quality to NB\n"
#endif
					"\nAvailable BT profiles:\n"
					"  - a2dp-source\tAdvanced Audio Source (%s)\n"
					"  - a2dp-sink\tAdvanced Audio Sink (%s)\n"
					"  - hfp-hf\tHands-Free (%s)\n"
					"  - hfp-ag\tHands-Free Audio Gateway (%s)\n"
					"  - hsp-hs\tHeadset (%s)\n"
					"  - hsp-ag\tHeadset Audio Gateway (%s)\n"
					"\n"
					"By default only output profiles are enabled, which includes A2DP Source and\n"
					"HSP/HFP Audio Gateways. If one wants to enable other set of profiles, it is\n"
					"required to explicitly specify all of them using `-p NAME` options.\n",
					argv[0],
					get_a2dp_codecs(config.a2dp.codecs, BLUEZ_A2DP_SOURCE),
					get_a2dp_codecs(config.a2dp.codecs, BLUEZ_A2DP_SINK),
					"v1.7", "v1.7", "v1.2", "v1.2");
			return EXIT_SUCCESS;

		case 'V' /* --version */ :
			printf("%s\n", PACKAGE_VERSION);
			return EXIT_SUCCESS;

		case 'S' /* --syslog */ :
			break;

		case 'i' /* --device=HCI */ : {

			bdaddr_t addr;
			int i = hci_devs_num;
			int found = 0;

			if (str2ba(optarg, &addr) == 0) {
				while (i--)
					if (bacmp(&addr, &hci_devs[i].bdaddr) == 0) {
						memcpy(&config.hci_dev, &hci_devs[i], sizeof(config.hci_dev));
						found = 1;
						break;
					}
			}
			else {
				while (i--)
					if (strcmp(optarg, hci_devs[i].name) == 0) {
						memcpy(&config.hci_dev, &hci_devs[i], sizeof(config.hci_dev));
						found = 1;
						break;
				}
			}

			if (found == 0) {
				error("HCI device not found: %s", optarg);
				return EXIT_FAILURE;
			}

			break;
		}

		case 'p' /* --profile=NAME */ : {

			size_t i;
			const struct {
				char *name;
				bool *ptr;
			} map[] = {
				{ "a2dp-source", &config.enable.a2dp_source },
				{ "a2dp-sink", &config.enable.a2dp_sink },
				{ "hfp-hf", &config.enable.hfp_hf },
				{ "hfp-ag", &config.enable.hfp_ag },
				{ "hsp-hs", &config.enable.hsp_hs },
				{ "hsp-ag", &config.enable.hsp_ag },
			};

			for (i = 0; i < ARRAYSIZE(map); i++)
				if (strcasecmp(optarg, map[i].name) == 0) {
					*map[i].ptr = true;
					break;
				}

			if (i == ARRAYSIZE(map)) {
				error("Invalid BT profile name: %s", optarg);
				return EXIT_FAILURE;
			}

			break;
		}

		case 6 /* --a2dp-force-mono */ :
			config.a2dp.force_mono = true;
			break;
		case 7 /* --a2dp-force-audio-cd */ :
			config.a2dp.force_44100 = true;
			break;
		case 8 /* --a2dp-keep-alive=SEC */ :
			config.a2dp.keep_alive = atoi(optarg);
			break;
		case 9 /* --a2dp-volume */ :
			config.a2dp.volume = true;
			break;

#if ENABLE_AAC
		case 4 /* --aac-afterburner */ :
			config.aac_afterburner = true;
			break;
		case 5 /* --aac-vbr-mode=NB */ :
			config.aac_vbr_mode = atoi(optarg);
			if (config.aac_vbr_mode > 5) {
				error("Invalid bitrate mode [0, 5]: %s", optarg);
				return EXIT_FAILURE;
			}
			break;
#endif

#if ENABLE_LDAC
		case 10 /* --ldac-abr */ :
			config.ldac_abr = true;
			break;
		case 11 /* --ldac-eqmid=NB */ :
			config.ldac_eqmid = atoi(optarg);
			if (config.ldac_eqmid >= LDACBT_EQMID_NUM) {
				error("Invalid encoder quality index [0, %d]: %s", LDACBT_EQMID_NUM - 1, optarg);
				return EXIT_FAILURE;
			}
			break;
#endif

		default:
			fprintf(stderr, "Try '%s --help' for more information.\n", argv[0]);
			return EXIT_FAILURE;
		}

	/* device list is no longer required */
	free(hci_devs);

	/* initialize random number generator */
	srandom(time(NULL));

	if ((bluealsa_ctl_thread_init()) == -1)
		return EXIT_FAILURE;

	gchar *address;
	GError *err;

	err = NULL;
	address = g_dbus_address_get_for_bus_sync(G_BUS_TYPE_SYSTEM, NULL, NULL);
	if ((config.dbus = g_dbus_connection_new_for_address_sync(address,
					G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |
					G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION,
					NULL, NULL, &err)) == NULL) {
		error("Couldn't obtain D-Bus connection: %s", err->message);
		return EXIT_FAILURE;
	}

	bluez_subscribe_signals();

	bluez_register_a2dp();
	bluez_register_hfp();

	/* In order to receive EPIPE while writing to the pipe whose reading end
	 * is closed, the SIGPIPE signal has to be handled. For more information
	 * see the io_thread_write_pcm() function. */
	struct sigaction sigact = { .sa_handler = SIG_IGN };
	sigaction(SIGPIPE, &sigact, NULL);

	/* register main loop exit handler */
	sigact.sa_handler = main_loop_stop;
	sigaction(SIGTERM, &sigact, NULL);
	sigaction(SIGINT, &sigact, NULL);

	/* main dispatching loop */
	debug("Starting main dispatching loop");
	loop = g_main_loop_new(NULL, FALSE);
	g_main_loop_run(loop);

	debug("Exiting main loop");

	/* From all of the cleanup routines, these ones cannot be omitted. We have
	 * to unlink named sockets, otherwise service will not start any more. */
	bluealsa_ctl_free();
	bluealsa_config_free();

	return EXIT_SUCCESS;
}