/*
 * Copyright (C) 2018 "IoT.bzh"
 * Author Fulup Ar Foll <fulup@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  // needed for vasprintf

#include "alsa-softmixer.h"
#include <stdbool.h>

ALSA_PLUG_PROTO(route);

static void routeConfigClean(SoftMixerT *mixer, void * arg) {
	snd_config_t * routeConfig = arg;
	AFB_API_DEBUG(mixer->api, "%s... route config", __func__);
	snd_config_delete(routeConfig);
	snd_config_update();
}


typedef struct {
    const char *uid;
    const char *cardid;
	const char * pcmplug_params;
    int cardidx;
    int ccount;
    int port;
	bool isPcmPlug;
	unsigned int quirks;
} ChannelCardPortT;

STATIC int CardChannelByUid(SoftMixerT *mixer, const char *uid, ChannelCardPortT *response) {

	AlsaSndPcmT * pcm = NULL;
	bool found = false;

    // search for channel within all sound card sink (channel port target is computed by order)
	cds_list_for_each_entry(pcm, &mixer->sinks.list, list) {

	   if (pcm->nbChannels == 0) {
            AFB_ApiError(mixer->api,
                         "%s: No Sink card=%s [should declare channels]",
                         __func__, pcm->uid);
            goto OnErrorExit;
        }

	   AlsaPcmChannelT * channel = NULL;
	   cds_list_for_each_entry(channel, &pcm->channels.list, list) {

		   if (channel->uid && !strcasecmp(channel->uid, uid)) {
			   response->port 	  = channel->port;
			   response->uid	 = pcm->uid;
			   response->ccount  = pcm->nbChannels;
			   response->cardid  = pcm->sndcard->cid.cardid;
			   response->cardidx = pcm->sndcard->cid.cardidx;
			   response->pcmplug_params = pcm->sndcard->cid.pcmplug_params;
			   response->isPcmPlug= pcm->isPcmPlug;
			   response->quirks   = pcm->quirks;
			   found = true;
			   break;
		   }
        }
    }

    if (!found) {
        AFB_ApiError(mixer->api,
                     "%s: No Channel with uid=%s [should declare channels]",
                     __func__, uid);
        goto OnErrorExit;
    }

    return 0;

OnErrorExit:
    return -1;
}

PUBLIC AlsaPcmCtlT* AlsaCreateRoute(SoftMixerT *mixer, AlsaSndZoneT *zone, int open) {
    snd_config_t *routeConfig = NULL, *elemConfig, *slaveConfig, *tableConfig, *pcmConfig;
	int error = 0;
	ChannelCardPortT slave, channelCardPort;
	AlsaPcmCtlT *pcmRoute = NULL;

    char *cardid = NULL;
	char *slaveUid = NULL;

	AFB_ApiDebug(mixer->api, "%s (zone %s)", __func__, zone->uid);

	if (mixer->nbSinks == 0) {
		AFB_ApiError(mixer->api, "%s: mixer=%s zone(%s)(zone) No sink found [should Registry sound card first]", __func__, mixer->uid, zone->uid);
		goto fail;
    }

	if (asprintf(&cardid, "route-%s", zone->uid) == -1) {
		SOFTMIXER_NOMEM(mixer->api);
		goto fail;
	}

	pcmRoute = AlsaPcmCtlNew(mixer, cardid);
	if (!pcmRoute) {
		SOFTMIXER_NOMEM(mixer->api);
		goto fail;
	}

    pcmRoute->cid.cardid = (const char *) cardid;
    
    pcmRoute->params = ApiSinkGetParamsByZone(mixer, zone->uid);
	if (pcmRoute->params == NULL) {
		AFB_ApiError(mixer->api, "%s: Failed to retrieve zone parameters", __func__);
		goto fail_nodump;
	}

	pcmRoute->params = ApiPcmParamsDup(mixer, pcmRoute->params);
	if (!pcmRoute->params) {
		SOFTMIXER_NOMEM(mixer->api);
		goto fail_nodump;
	}

	zone->params = ApiPcmParamsDup(mixer, pcmRoute->params);
	if (!zone->params) {
		SOFTMIXER_NOMEM(mixer->api);
		goto fail_nodump;
	}
    
    // use 1st zone channel to retrieve sound card name + channel count. 

	AlsaPcmChannelT * channel = cds_list_first_entry(&zone->sinks.list, AlsaPcmChannelT, list);
	error = CardChannelByUid(mixer, channel->uid, &slave);
    if (error) {
		AFB_ApiError(mixer->api, "%s:zone(%s) fail to find channel=%s", __func__, zone->uid, channel->uid);
		goto fail;
    }
    
	// DMIX only supports real hardware as a slave

	if (!slave.isPcmPlug)  {
		// move from hardware to DMIX attach to sndcard
		if (asprintf(&slaveUid, "dmix-%s", slave.uid) == -1) {
			SOFTMIXER_NOMEM(mixer->api);
			goto fail;
		}
	} else {
		slaveUid = strdup(slave.pcmplug_params);
		if (!slaveUid) {
			SOFTMIXER_NOMEM(mixer->api);
			goto fail;
		}
		pcmRoute->isPcmPlug = true;
	}

	pcmRoute->quirks = slave.quirks;

    // temporary store to enable multiple channel to route to the same port

	snd_config_t **cports = alloca(zone->nbSinks * sizeof (void*));
	memset(cports, 0, zone->nbSinks * sizeof (void*));
    int zcount = 0;

    // We create 1st ttable to retrieve sndcard slave and channel count
    (void)snd_config_make_compound(&tableConfig, "ttable", 0);

	cds_list_for_each_entry(channel, &zone->sinks.list, list) {

		AFB_ApiDebug(mixer->api, "%s: zone->sink channel %s ", __func__, channel->uid);

		error = CardChannelByUid(mixer, channel->uid, &channelCardPort);
        if (error) {
			AFB_ApiError(mixer->api, "%s: zone(%s) fail to find channel=%s", __func__, zone->uid, channel->uid);
			goto fail;
        }

		if (slave.cardidx != channelCardPort.cardidx) {
			AFB_ApiError(mixer->api,
						 "%s: zone(%s) cannot span over multiple sound card %s != %s ",
						 __func__, zone->uid, slave.cardid, channelCardPort.cardid);
			goto fail;
        }

		int target = channelCardPort.port;
		int port = channel->port;

        double volume = 1.0; // currently only support 100%

		// if channel entry does not exist into ttable create it now
		if (!cports[port]) {

            char channelS[4]; // 999 channel should be more than enough
            snprintf(channelS, sizeof (channelS), "%d", port);

			error = snd_config_make_compound(&cports[port], channelS, 0);
			if (error) {
				AFB_ApiError(mixer->api, "%s: make_compound failed , err %s", __func__, snd_strerror(error));
				goto fail;
			}

			error = snd_config_add(tableConfig, cports[port]);
			if (error) {
				AFB_ApiError(mixer->api, "%s: add compound to table failed, err %s", __func__, snd_strerror(error));
				goto fail;
			}
        }

		zcount++;

        // ttable require target port as a table and volume as a value
        char targetS[4];
        snprintf(targetS, sizeof (targetS), "%d", target);

		if (channel->volume != -1)
			volume = channel->volume;
		else
			volume = 1.0;

		error = snd_config_imake_real(&elemConfig, targetS, volume);
		if (error) {
			AFB_ApiError(mixer->api, "%s: Failed to interpret volume real value, err %s", __func__, snd_strerror(error));
			goto fail;
		}

		error = snd_config_add(cports[port], elemConfig);
		if (error) {
			AFB_ApiError(mixer->api, "%s Failed to add ttable entry:err=%s", __func__, snd_strerror(error));
			goto fail;
		}

    }

    // update zone with route channel count and sndcard params
    pcmRoute->ccount = zcount;
    zone->ccount=zcount;

    // refresh global alsalib config and create PCM top config
    snd_config_update();
    error += snd_config_top(&routeConfig);
	if (error) goto fail;
    error += snd_config_set_id(routeConfig, cardid);
	if (error) goto fail;
    error += snd_config_imake_string(&elemConfig, "type", "route");
	if (error) goto fail;
    error += snd_config_add(routeConfig, elemConfig);
	if (error) goto fail;
    error += snd_config_make_compound(&slaveConfig, "slave", 0);
	if (error) goto fail;
	error += snd_config_imake_string(&elemConfig, "pcm", slaveUid);
	if (error) goto fail;
    error += snd_config_add(slaveConfig, elemConfig);
	if (error) goto fail;

    error += snd_config_imake_integer(&elemConfig, "channels", slave.ccount);
	if (error) goto fail;
    error += snd_config_add(slaveConfig, elemConfig);

	if (error) goto fail;
    error += snd_config_add(routeConfig, slaveConfig);
	if (error) goto fail;
    error += snd_config_add(routeConfig, tableConfig);
	if (error) goto fail;

    if (open) error = _snd_pcm_route_open(&pcmRoute->handle, pcmRoute->cid.cardid, snd_config, routeConfig, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
    if (error) {
        AFB_ApiError(mixer->api,
                     "%s: zone(%s) fail to create Plug=%s error=%s",
                     __func__, zone->uid, pcmRoute->cid.cardid, snd_strerror(error));
		goto fail;
    }

    snd_config_update();
    error += snd_config_search(snd_config, "pcm", &pcmConfig);
    error += snd_config_add(pcmConfig, routeConfig);
    if (error) {
        AFB_ApiError(mixer->api,
                     "%s: %s fail to add config route=%s error=%s",
                     __func__, zone->uid, pcmRoute->cid.cardid, snd_strerror(error));
		goto fail;
    }

	pcmRoute->private_data = routeConfig;
	pcmRoute->private_data_clean = routeConfigClean;

    // Debug config & pcm
    AFB_ApiNotice(mixer->api, "%s: zone(%s) DONE", __func__, zone->uid);
    AlsaDumpCtlConfig(mixer, "plug-route", routeConfig, 1);

    return pcmRoute;

fail:
//	AFB_ApiError(mixer->api, "%s ERROR, DUMPING PCM...", __func__);
//	AlsaDumpCtlConfig(mixer, "plug-pcm", snd_config, 1);
	AFB_ApiError(mixer->api, "%s ERROR, DUMPING ROUTE...", __func__);
	AlsaDumpCtlConfig(mixer, "plug-route", routeConfig, 1);

fail_nodump:
    free(pcmRoute);
    free(cardid);
	free(slaveUid);

    AFB_ApiNotice(mixer->api, "%s: zone(%s) FAIL", __func__, zone->uid);
    return NULL;
}