/*
 * Copyright (C) 2018 "IoT.bzh"
 * Author : Thierry Bultel <thierry.bultel@iot.bzh>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>

#include <wrap-json.h>

#include <errno.h>
#include <ctl-config.h>
#include <systemd/sd-event.h>
#include <bluealsa/bluealsa.h>

#include "hal-bluealsa.h"

#define HAL_BLUEALSA_PLUGIN_NAME "hal-bluealsa"

#define SCO_TALK_RATE 44100
#define SCO_TALK_FORMAT "S16_LE"

#define SCO_TALK_ZONE "sco_talk_zone"
#define SCO_CHANNEL_MONO "sco-mono"

/* forward static declarations */
static int halBlueAlsaTransportEventCB(sd_event_source* src, int fd, uint32_t revents, void* userData);
static int halBlueAlsaFetchTransports(bluealsa_watch * plugin);
static int halBlueAlsaRegisterAll(CtlPluginT* plugin);
static int halBlueAlsaRegister(CtlPluginT* plugin, const char * interface);

CTLP_CAPI_REGISTER(HAL_BLUEALSA_PLUGIN_NAME)


// Call at initialization time ('requires' are forbidden at this stage)
CTLP_ONLOAD(plugin, callbacks)
{
	AFB_ApiNotice(plugin->api, "%s Plugin Registered correctly: uid='%s' 'info='%s'", HAL_BLUEALSA_PLUGIN_NAME, plugin->uid, plugin->info);
	return 0;
}


CTLP_INIT(plugin, callbacks)
{
	json_object *actionsToAdd = NULL;
	CtlConfigT *ctrlConfig;

	wrap_json_pack(&actionsToAdd, "{s:s s:s s:s}",
				      "uid", "init-bluealsa-plugin",
				      "info", "Init Bluez-Alsa hal plugin",
				      "action", "plugin://hal-bluealsa#init");

	if (!(ctrlConfig = (CtlConfigT *) AFB_ApiGetUserData(plugin->api))) {
		AFB_ApiError(plugin->api, "Can't get current hal controller config");
		goto fail;
	}

	int idx = 0;
	while (ctrlConfig->sections[idx].key && strcasecmp(ctrlConfig->sections[idx].key, "onload"))
		idx++;

	if (!ctrlConfig->sections[idx].key) {
		AFB_ApiError(plugin->api, "Wasn't able to add '%s' as a new onload, 'onload' section not found", json_object_get_string(actionsToAdd));
		goto fail;
	}

	if(AddActionsToSection(plugin->api, &ctrlConfig->sections[idx], actionsToAdd, 0)) {
		AFB_ApiError(plugin->api, "Wasn't able to add '%s' as a new onload to %s", json_object_get_string(actionsToAdd), ctrlConfig->sections[idx].uid);
		goto fail;
	}

	AFB_ApiNotice(plugin->api, "Plugin initialization of %s plugin correctly done", HAL_BLUEALSA_PLUGIN_NAME);

	return 0;
fail:
	json_object_put(actionsToAdd);
	return -1;
}


// Call at controller onload time
CTLP_CAPI(init, source, argsJ, queryJ)
{
	AFB_ApiNotice(source->api, "Controller onload event");

	CtlPluginT * plugin = source->plugin;

	hal_bluealsa_plugin_data_t * pluginData  = (hal_bluealsa_plugin_data_t*) calloc(1, sizeof(hal_bluealsa_plugin_data_t));
	int error;

	/*
	 * "params": {
	      "sco": {
	          "mic": "2CH-GENERIC-USB",
	          "zone": "full-stereo",
	          "delayms": 300
	      },
	      "a2dp": {
	          "zone": "full-stereo",
	          "delayms": 1000
	      }
	  }
	 */

	json_object * scoParamsJ = NULL;
	json_object * a2dpParamsJ = NULL;

	error = wrap_json_unpack(plugin->paramsJ, "{s?o,s?o !}",
				"sco", &scoParamsJ,
				"a2dp", &a2dpParamsJ);

	if (error) {
		AFB_ApiError(plugin->api, "%s: wrong parameters", __func__);
		goto fail;
	}

	if (scoParamsJ) {
		AFB_ApiInfo(plugin->api, "%s: sco parameters: %s", __func__, json_object_get_string(scoParamsJ));
		error = wrap_json_unpack(scoParamsJ, "{s:s,s:s,s?i !}",
				"mic", &pluginData->sco.mic,
				"zone", &pluginData->sco.speaker,
				"delayms", &pluginData->sco.delayms);
		if (error) {
			AFB_ApiError(plugin->api, "%s: wrong sco parameters: err %s", __func__, wrap_json_get_error_string(error));
			goto fail;
		}
	}

	if (a2dpParamsJ) {
		AFB_ApiInfo(plugin->api, "%s: a2dp parameters: %s", __func__, json_object_get_string(a2dpParamsJ));
		error = wrap_json_unpack(a2dpParamsJ, "{s:s,s?i !}",
				"zone", &pluginData->a2dp.zone,
				"delayms", &pluginData->a2dp.delayms);
		if (error) {
			AFB_ApiError(plugin->api, "%s: wrong a2dp parameters: err=%s", __func__, wrap_json_get_error_string(error));
			goto fail;
		}
	}

	halBlueAlsaTransportsInit(&pluginData->transport_list);

	setPluginContext(plugin, pluginData);

	if (halBlueAlsaRegisterAll(plugin) != 0)
		goto fail;

	return 0;
fail:
    return -1;
}


static int halBlueAlsaTransportEventCB(sd_event_source* src, int fd, uint32_t revents, void* userData) {

	bluealsa_watch * watch = (bluealsa_watch *)userData;
	CtlPluginT * plugin = watch->plugin;

	struct ba_msg_event event;
	ssize_t ret;

	AFB_ApiDebug(plugin->api, "--- %s ----!", __func__);

	if ((revents & EPOLLIN) == 0)
		goto done;

	if (revents & EPOLLHUP) {
		AFB_ApiInfo(plugin->api, "Lost connection with bluealsa on interface %s", watch->interface);
		sd_event_source_unref(src);
		close(fd);
		halBlueAlsaRegister(plugin, watch->interface);
		goto done;
	}

	while ((ret = recv(watch->fd, &event, sizeof(event), MSG_DONTWAIT)) == -1 && errno == EINTR)
		continue;

	if (ret != sizeof(event)) {
		AFB_ApiError(plugin->api, "Couldn't read event: %s", strerror(ret == -1 ? errno : EBADMSG));
		goto done;
	}

	halBlueAlsaFetchTransports(watch);

done:
	return 0;
}


/*
 *
 * The following function builds that kind of json data and send it to smixer
 *
 * A2DP Case:
 *
 *  {
    "uid":"a2dp_F6:32:15:2A:80:70",
    "captures":
     {
       "uid":"a2dp_listen_capture",
       "pcmplug_params" : "bluealsa:HCI=hci0,DEV=F6:32:15:2A:80:70,PROFILE=a2dp",
       "source" : {
       	   "channels": [
       	       {
                   "uid": "a2dp-right",
                   "port": 0
               },
               {
                   "uid": "a2dp-left",
                   "port": 1
               }
            ]
        }
     },
    "streams":
    {
    	"uid" : "a2dp_listen_stream",
    	"source":"a2dp_listen_capture",
     	"zone": "full-stereo"
    }
   }
 *
 *-----------------------------------------------------------------------------------
 *
 * SCO Case:
 *
 *  {
    "uid":"sco_F6:32:15:2A:80:70",",
    "captures":
     {
       "uid":"sco_listen_capture",
       "pcmplug_params" : "bluealsa:HCI=hci0,DEV=F6:32:15:2A:80:70,PROFILE=sco",
       "source" : {
       	   "channels": [
       	       {
                   "uid": "sco-right",
                   "port": 0
               },
               {
                   "uid": "sco-left",
                   "port": 1
               }
            ]
        }
     },
	"playbacks" :
    {
    	"uid"="sco_talk_playback",
    	"pcmplug_params" : "bluealsa:HCI=hci0,DEV=F6:32:15:2A:80:70,PROFILE=sco",
    	"params": {
            "rate": 44100,
            "format": "S16_LE",
        },
    	"sink": {
    		"channels": [
            	{
            	"uid": "sco-mono"
            	}
            ]
    	}
    },
    "zones" : [
       {
           "uid": "sco_talk_zone",
           "sink": [
                {
                    "target": "sco-mono",
                    "channel": 0,
                    "volume": 0.5
                },
                {
                    "target": "sco-mono",
                    "channel": 1,
                    "volume": 0.5
                }
            ]
        }
    ] ,
    "streams": [
    	{
    		"uid" : "sco_listen_stream",
    		"source":"sco_listen_capture",
     		"zone": "full-stereo",
     		"delayms": 300
    	},
    	{
    		"uid" : "sco_talk_stream",
    		"source": "2CH-GENERIC-USB",
     		"zone": "sco_talk_zone",
     		"delayms": 300
    	}
    ]
   }
 *
 * */

static json_object* halBlueAlsaA2DPTransportChannels(const char * transportTypeS) {
	json_object* channelRightJ;
	json_object* channelLeftJ;
	json_object* channelsJ = json_object_new_array();
	char * channelRightS;
	char * channelLeftS;

	channelRightJ = json_object_new_object();
	if (asprintf(&channelRightS, "%s-right", transportTypeS) == -1)
		goto fail;

	json_object_object_add(channelRightJ, "uid", json_object_new_string(channelRightS));
	json_object_object_add(channelRightJ, "port", json_object_new_int(0));

	channelLeftJ = json_object_new_object();
	if (asprintf(&channelLeftS, "%s-left", transportTypeS) == -1)
		goto fail;

	json_object_object_add(channelLeftJ, "uid", json_object_new_string(channelLeftS));
	json_object_object_add(channelLeftJ, "port", json_object_new_int(1));

	json_object_array_add(channelsJ, channelRightJ);
	json_object_array_add(channelsJ, channelLeftJ);
fail:
	return channelsJ;
}


static json_object* halBlueAlsaSCOTransportChannels(const char * transportTypeS) {
	json_object* channelMonoJ;

	json_object* channelsJ = json_object_new_array();
	char * channelMonoS;

	channelMonoJ = json_object_new_object();
	if (asprintf(&channelMonoS, "%s-mono", transportTypeS) == -1)
		goto fail;

	json_object_object_add(channelMonoJ, "uid", json_object_new_string(channelMonoS));
	json_object_object_add(channelMonoJ, "port", json_object_new_int(0));

	json_object_array_add(channelsJ, channelMonoJ);

fail:
	return channelsJ;
}


static json_object* halBlueAlsaPcmPlugParams(const char * interface, const char * addr, const char * transportTypeS) {
	char * pcmplug_paramsS = NULL;
	if (asprintf(&pcmplug_paramsS, "bluealsa:HCI=%s,DEV=%s,PROFILE=%s", interface, addr, transportTypeS) == -1)
		goto fail;
	return json_object_new_string(pcmplug_paramsS);
fail:
	return NULL;
}


static json_object* halBlueAlsaListenCapture(
	const char * listenCaptureS,
	json_object * pcmplugParamsJ,
	json_object * sourceJ) {

	json_object * captureJ = json_object_new_object();

	json_object_object_add(captureJ, "uid", json_object_new_string(listenCaptureS));
	json_object_object_add(captureJ, "pcmplug_params", pcmplugParamsJ);
	json_object_object_add(captureJ, "source", sourceJ);

	return captureJ;
}

static json_object * halBlueAlsaTalkPlayback(
	const char * talkPlaybackS,
	json_object* pcmplugParamsJ,
	json_object* paramsJ,
	json_object* sinkJ ) {

	json_object * playbackJ = json_object_new_object();

	json_object_object_add(playbackJ, "uid", json_object_new_string(talkPlaybackS));
	json_object_object_add(playbackJ, "pcmplug_params", pcmplugParamsJ);
	json_object_object_add(playbackJ, "params", paramsJ);
	json_object_object_add(playbackJ, "sink", sinkJ);

	return playbackJ;
}

static json_object * halBlueAlsaScoTalkParamsJ() {
	json_object * paramsJ = json_object_new_object();

	json_object_object_add(paramsJ, "rate",   json_object_new_int(SCO_TALK_RATE));
	json_object_object_add(paramsJ, "format", json_object_new_string(SCO_TALK_FORMAT));
	return paramsJ;
}

static json_object * halBlueAlsaScoZone() {
	json_object * zoneJ = json_object_new_object();
	json_object * sinkJ = json_object_new_array();
	json_object_object_add(zoneJ, "uid", json_object_new_string(SCO_TALK_ZONE));

	json_object * channel1J = json_object_new_object();
	json_object_object_add(channel1J, "target" , json_object_new_string(SCO_CHANNEL_MONO));
	json_object_object_add(channel1J, "channel", json_object_new_int(0));
	json_object_object_add(channel1J, "volume", json_object_new_double(0.5));

	json_object * channel2J = json_object_new_object();
	json_object_object_add(channel2J, "target" , json_object_new_string(SCO_CHANNEL_MONO));
	json_object_object_add(channel2J, "channel", json_object_new_int(1));
	json_object_object_add(channel2J, "volume", json_object_new_double(0.5));

	json_object_array_add(sinkJ, channel1J);
	json_object_array_add(sinkJ, channel2J);

	json_object_object_add(zoneJ, "sink", sinkJ);

	return zoneJ;
}

static int halBlueAlsaAttachTransportStreams(bluealsa_transport_t * transport) {
	int ret = -1;
	const char* transportTypeS;

	struct ba_msg_transport * ba_transport = &transport->transport;
	const bluealsa_watch * watch = transport->watch;

	CtlPluginT* plugin = watch->plugin;
	hal_bluealsa_plugin_data_t * pluginData=getPluginContext(plugin);

	json_object* requestJ = NULL;
	json_object* returnJ = NULL;

	json_object* sourceJ = NULL;
	json_object* playbackJ = NULL;

	json_object* pcmplugParamsJ, *pcmplugParamsJ2 = NULL;

	json_object* streamsJ = NULL;
	json_object* streamJ = NULL;

	json_object* zonesJ = NULL;

	char * playbackZoneS;
	uint32_t delayms;

	if (ba_transport->type == BA_PCM_TYPE_SCO) {
		transportTypeS = "sco";
		delayms = pluginData->sco.delayms;
		playbackZoneS = pluginData->sco.speaker;
	}
	else if (ba_transport->type == BA_PCM_TYPE_A2DP) {
		transportTypeS = "a2dp";
		delayms = pluginData->a2dp.delayms;
		playbackZoneS = pluginData->a2dp.zone;
	} else {
		AFB_ApiError(plugin->api, "%s: unsupported transport type", __func__ );
		goto fail;
	}

	char * captureS = NULL;
	if (asprintf(&captureS, "%s_listen_capture", transportTypeS) == -1)
		goto fail;

	// for SCO only
	char * playbackS = NULL;
	if (asprintf(&playbackS, "%s_talk_playback", transportTypeS) == -1)
		goto fail;

	char * transactionUidS = NULL;
	char * streamS = NULL;

	char addr[18];
	ba2str(&ba_transport->addr, addr);

	requestJ = json_object_new_object();
	if (!requestJ)
		goto fail;

	if (asprintf(&transactionUidS, "%s_%s", transportTypeS, addr) == -1)
		goto fail;

	json_object_object_add(requestJ, "uid", json_object_new_string(transactionUidS));

	pcmplugParamsJ = halBlueAlsaPcmPlugParams(watch->interface, addr, transportTypeS);

	sourceJ = json_object_new_object();
	if (ba_transport->type == BA_PCM_TYPE_A2DP)
		json_object_object_add(sourceJ, "channels", halBlueAlsaA2DPTransportChannels(transportTypeS));
	else
		json_object_object_add(sourceJ, "channels", halBlueAlsaSCOTransportChannels(transportTypeS));

	json_object_object_add(requestJ, "captures", halBlueAlsaListenCapture(captureS, pcmplugParamsJ, sourceJ));

	if (ba_transport->type == BA_PCM_TYPE_SCO) {
		playbackJ = json_object_new_object();
		// that is a shame that deep copy in not available in the current json-c version
		pcmplugParamsJ2 = halBlueAlsaPcmPlugParams(watch->interface, addr, transportTypeS);
		json_object * paramsJ = halBlueAlsaScoTalkParamsJ();

		json_object_object_add(playbackJ, "channels", halBlueAlsaSCOTransportChannels(transportTypeS));
		json_object_object_add(requestJ, "playbacks", halBlueAlsaTalkPlayback(playbackS, pcmplugParamsJ2, paramsJ, playbackJ));

	}

	/* ZONES */

	if (ba_transport->type == BA_PCM_TYPE_SCO) {
		zonesJ = json_object_new_array();
		json_object_array_add(zonesJ, halBlueAlsaScoZone());
		json_object_object_add(requestJ, "zones", zonesJ);
	}

	/* STREAMS */

	/* Build the array of streams */
	streamsJ = json_object_new_array();

	streamJ = json_object_new_object();
	if (asprintf(&streamS, "%s_listen_stream", transportTypeS) == -1)
		goto fail;

	json_object_object_add(streamJ, "uid", json_object_new_string(streamS));
	json_object_object_add(streamJ, "source", json_object_new_string(captureS));
	json_object_object_add(streamJ, "zone", json_object_new_string(playbackZoneS));
	if (delayms != 0) {
		json_object_object_add(streamJ, "delayms", json_object_new_int(delayms));
	}

	json_object_array_add(streamsJ, streamJ);

	/* In case of SCO, to have full-duplex, we instantiate a stream for talk */
	if (ba_transport->type == BA_PCM_TYPE_SCO ) {

		streamJ = json_object_new_object();
		if (asprintf(&streamS, "%s_talk_stream", transportTypeS) == -1)
			goto fail;

		json_object_object_add(streamJ, "uid", json_object_new_string(streamS));
		json_object_object_add(streamJ, "source", json_object_new_string(pluginData->sco.mic));
		json_object_object_add(streamJ, "zone", json_object_new_string(SCO_TALK_ZONE));
		if (delayms != 0)
			json_object_object_add(streamJ, "delayms", json_object_new_int(delayms));
		json_object_array_add(streamsJ, streamJ);
	}

	json_object_object_add(requestJ, "streams", streamsJ);

	/* In softmixer, this will create a transaction verb (whose name is transactionUidS),
	 * will be used later to destroy all the created objects upon transport removal */

	if (AFB_ServiceSync(plugin->api, SMIXER_API_NAME, "attach", requestJ, &returnJ)) {
		AFB_ApiError(plugin->api, "Error calling attach verb of mixer" );
		goto done;
	}

	transport->transactionUidS = transactionUidS;

	if (returnJ)
		json_object_put(returnJ);

	ret = 0;
	goto done;

fail:
	return -1;
done:
	AFB_ApiDebug(plugin->api, "DONE.");
	return ret;
}


static int halBluezAlsaRemoveTransportStream(bluealsa_transport_t * transport) {

	CtlPluginT * plugin = transport->watch->plugin;
	json_object* requestJ = NULL;
	json_object* returnJ = NULL;

	AFB_ApiInfo(plugin->api, "Call transaction detach verb %s", transport->transactionUidS);
	if (transport->transactionUidS == NULL)
		goto fail;

	requestJ = json_object_new_object();
	if (!requestJ)
		goto fail;

	json_object_object_add(requestJ, "action", json_object_new_string("remove"));

	if (AFB_ServiceSync(plugin->api, SMIXER_API_NAME, transport->transactionUidS, requestJ, &returnJ)) {
		AFB_ApiError(plugin->api, "Error calling attach verb of mixer" );
		goto fail;
	}

	if (returnJ)
		json_object_put(returnJ);

	return 0;

fail:
	return -1;
}

static int halBlueAlsaFetchTransports(bluealsa_watch * watch) {
	ssize_t nbTransports;
	struct ba_msg_transport *transports;
	CtlPluginT * plugin = watch->plugin;

	hal_bluealsa_plugin_data_t * pluginData = (hal_bluealsa_plugin_data_t*)getPluginContext(plugin);
	bluealsa_transport_t * transport_list = &pluginData->transport_list;
	bluealsa_transport_t * transport = NULL;

	AFB_ApiDebug(plugin->api, "Fetching available transports of interface %s", watch->interface);

	if ((nbTransports = bluealsa_get_transports(watch->fd, &transports)) == -1) {
		AFB_ApiError(plugin->api, "Couldn't get transports: %s", strerror(errno));
		goto done;
	}

	AFB_ApiDebug(plugin->api, "Got %zu transport(s)", nbTransports);

	for (int ix=0; ix<nbTransports; ix++) {
		char addr[18];
		struct ba_msg_transport * ba_transport = &transports[ix];
		ba2str(&ba_transport->addr, addr);
		const char * typeS;
		if (ba_transport->type == BA_PCM_TYPE_SCO)
			typeS = "sco";
		else if (ba_transport->type == BA_PCM_TYPE_A2DP)
			typeS = "a2dp";
		else
			typeS = "unknown";

		AFB_ApiDebug(plugin->api, "Transport %d: type %s, dev %s", ix, typeS, addr);

		if (halBlueAlsaTransportFind(watch, transport_list, ba_transport)) {
			AFB_ApiDebug(plugin->api, "This transport is already streamed");
			continue;
		}

		AFB_ApiInfo(plugin->api, "Registering transport type %s, dev %s", typeS, addr);
		transport = halBlueAlsaTransportsAdd(watch, transport_list, ba_transport);
		if (transport == NULL) {
			AFB_ApiError(plugin->api, "Failed to register this transport");
			goto done;
		}

		// Do the softmixer stuff
		if (halBlueAlsaAttachTransportStreams(transport) != 0) {
			AFB_ApiError(plugin->api, "Failed create transport streams");
			goto done;
		}

		AFB_ApiDebug(plugin->api, "%s: transaction id %s", __func__, transport->transactionUidS);

	}

	halBlueAlsaTransportUpdate(watch, transport_list, transports, nbTransports , halBluezAlsaRemoveTransportStream);

done:
	free(transports);
	return 0;
}

static int halBlueAlsaRegister(CtlPluginT* plugin, const char * interface) {
	int ret;
    sd_event *sdLoop;
    sd_event_source* evtsrc;

    sdLoop = AFB_GetEventLoop(plugin->api);

    enum ba_event transport_mask = BA_EVENT_TRANSPORT_ADDED |
//    							   BA_EVENT_TRANSPORT_CHANGED |
								   BA_EVENT_TRANSPORT_REMOVED;

    bluealsa_watch * watch = (bluealsa_watch *)malloc(sizeof(bluealsa_watch));
    if (watch == NULL)
    	goto fail;

    watch->interface = interface;
    watch->plugin    = plugin;

    if ((watch->fd = bluealsa_open(interface)) == -1) {
    	AFB_ApiError(plugin->api, "BlueALSA connection failed: %s", strerror(errno));
    	goto fail;
    }

	halBlueAlsaFetchTransports(watch);

    if (bluealsa_subscribe(watch->fd, transport_mask) == -1) {
    	AFB_ApiError(plugin->api, "BlueALSA subscription failed: %s", strerror(errno));
    	goto fail;
    }

    // Register sound event to the main loop of the binder
    if ((ret = sd_event_add_io(sdLoop, &evtsrc, watch->fd, EPOLLIN, halBlueAlsaTransportEventCB, watch)) < 0) {
        AFB_ApiError(plugin->api,
        		     "%s: Failed to register event fd to io loop",
					 __func__);
        goto fail;
    }
	return 0;

fail:
	if (watch->fd)
		close(watch->fd);
	if (watch)
		free(watch);
	return -1;
}


static int halBlueAlsaRegisterAll(CtlPluginT* plugin) {
    /* TODO add a watch to all the available interfaces */
	halBlueAlsaRegister(plugin, "hci0");
	return 0;
}