/*
 * AlsaLibMapping -- provide low level interface with ALSA lib (extracted from alsa-json-gateway code)
 * Copyright (C) 2015,2016,2017, 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-ApiHat.h"

// generic sndctrl event handle hook to event callback when pooling
typedef struct {
    struct pollfd pfds;
    sd_event_source *src;
    snd_ctl_t  *ctlDev;
    int mode;
    struct afb_event afbevt;
} evtHandleT;

typedef struct {
    int ucount;
    int cardId;
    evtHandleT *evtHandle;
} sndHandleT;

typedef struct {
    char *devid;
    char *apiprefix;
    char *shortname;
    char *longname;
}cardRegistryT;

cardRegistryT *cardRegistry[MAX_SND_CARD+1];

PUBLIC  json_object *alsaCheckQuery (afb_req request, queryValuesT *queryValues) {

    json_object *tmpJ;
    int done;

    // get query from request
    json_object *queryInJ = afb_req_json(request);
    
    done= json_object_object_get_ex (queryInJ, "devid" , &tmpJ);
    if (!done) {
        afb_req_fail_f(request, "devid-missing", "Invalid query='%s'", json_object_get_string(queryInJ));
        goto OnErrorExit;                                
    }
    queryValues->devid = json_object_get_string(tmpJ);

    done= json_object_object_get_ex (queryInJ, "mode" , &tmpJ);
    if (!done) queryValues->mode=QUERY_QUIET; // default quiet
    else queryValues->mode = json_object_get_int (tmpJ);
     
    return queryInJ;

OnErrorExit:
    return NULL;
}

// This routine is called when ALSA event are fired
STATIC  int sndCtlEventCB (sd_event_source* src, int fd, uint32_t revents, void* userData) {
    int err, ctlNumid;
    evtHandleT *evtHandle = (evtHandleT*)userData; 
    snd_ctl_event_t *eventId;
    json_object *ctlEventJ;
    unsigned int mask;
    int iface;
    int device;
    int subdev;
    const char*ctlName;
    ctlRequestT ctlRequest;
    snd_ctl_elem_id_t *elemId;
    
    if ((revents & EPOLLHUP) != 0) {
        AFB_NOTICE("SndCtl hanghup [car disconnected]");
        goto ExitOnSucess;
    }
    
    if ((revents & EPOLLIN) != 0)  {
        
        // initialise event structure on stack  
        snd_ctl_event_alloca(&eventId); 
        snd_ctl_elem_id_alloca(&elemId);
        
        err = snd_ctl_read(evtHandle->ctlDev, eventId);
        if (err < 0) goto OnErrorExit;
        
        // we only process sndctrl element
        if (snd_ctl_event_get_type(eventId) != SND_CTL_EVENT_ELEM) goto ExitOnSucess;
       
        // we only process value changed events
        mask = snd_ctl_event_elem_get_mask(eventId);
        if (!(mask & SND_CTL_EVENT_MASK_VALUE)) goto ExitOnSucess;
        
        snd_ctl_event_elem_get_id (eventId, elemId);
        
        err = alsaGetSingleCtl (evtHandle->ctlDev, elemId, &ctlRequest, evtHandle->mode);
        if (err) goto OnErrorExit;
               
        // If CTL as a value use it as container for response
        if (ctlRequest.valuesJ) ctlEventJ= ctlRequest.valuesJ;
        else {
            ctlEventJ = json_object_new_object();
            ctlNumid = snd_ctl_event_elem_get_numid(eventId);
            json_object_object_add(ctlEventJ, "id",json_object_new_int (ctlNumid));
        }

        if (evtHandle->mode >= QUERY_COMPACT) {
            ctlName = snd_ctl_event_elem_get_name(eventId);
            json_object_object_add(ctlEventJ, "name",json_object_new_string (ctlName));
        }

        if (evtHandle->mode >= QUERY_VERBOSE) {
            iface   = snd_ctl_event_elem_get_interface(eventId);
            device  = snd_ctl_event_elem_get_device(eventId);
            subdev  = snd_ctl_event_elem_get_subdevice(eventId);
            json_object_object_add(ctlEventJ, "ifc"  ,json_object_new_int (iface));
            json_object_object_add(ctlEventJ, "dev"  ,json_object_new_int (device));
            json_object_object_add(ctlEventJ, "sub"  ,json_object_new_int (subdev));
        }

    
        AFB_DEBUG( "sndCtlEventCB=%s", json_object_get_string(ctlEventJ));
        afb_event_push(evtHandle->afbevt, ctlEventJ);
    }

    ExitOnSucess:
        return 0;
    
    OnErrorExit:
        AFB_WARNING ("sndCtlEventCB: ignored unsupported event type");
	return (0);    
}

// Subscribe to every Alsa CtlEvent send by a given board
PUBLIC void alsaEvtSubcribe (afb_req request) {
    static sndHandleT sndHandles[MAX_SND_CARD];
    evtHandleT *evtHandle=NULL;
    snd_ctl_t  *ctlDev=NULL;
    int err, idx, cardId, idxFree=-1;
    snd_ctl_card_info_t *cardinfo;
    queryValuesT queryValues;

    json_object *queryJ = alsaCheckQuery (request, &queryValues);
    if (!queryJ) goto OnErrorExit;

    // open control interface for devid
    err = snd_ctl_open(&ctlDev, queryValues.devid, SND_CTL_READONLY);
    if (err < 0) {
        afb_req_fail_f (request, "devid-unknown", "SndCard devid=%s Not Found err=%s", queryValues.devid, snd_strerror(err));
        goto OnErrorExit;
    }
    
    snd_ctl_card_info_alloca(&cardinfo);    
    if ((err = snd_ctl_card_info(ctlDev, cardinfo)) < 0) {        
        afb_req_fail_f (request, "devid-invalid", "SndCard devid=%s Not Found err=%s", queryValues.devid, snd_strerror(err));
        goto OnErrorExit;
    }
    
    cardId=snd_ctl_card_info_get_card(cardinfo);
    
    // search for an existing subscription and mark 1st free slot
    for (idx= 0; idx < MAX_SND_CARD; idx ++) {
      if (sndHandles[idx].ucount > 0 && cardId == sndHandles[idx].cardId) {
         evtHandle= sndHandles[idx].evtHandle;
         break;
      } else if (idxFree == -1) idxFree= idx; 
    };
      
    // if not subscription exist for the event let's create one
    if (idx == MAX_SND_CARD) {
        
        // reach MAX_SND_CARD event registration
        if (idxFree == -1) {
            afb_req_fail_f (request, "register-toomany", "Cannot register new event Maxcard==%d", idx);
            goto OnErrorExit;            
        } 
        
        evtHandle = malloc (sizeof(evtHandleT));
        evtHandle->ctlDev = ctlDev;
        evtHandle->mode  = queryValues.mode;
        sndHandles[idxFree].ucount = 0;
        sndHandles[idxFree].cardId = cardId;
        sndHandles[idxFree].evtHandle = evtHandle;
        
        // subscribe for sndctl events attached to devid
        err = snd_ctl_subscribe_events(evtHandle->ctlDev, 1);
        if (err < 0) {
            afb_req_fail_f (request, "subscribe-fail", "Cannot subscribe events from devid=%s err=%d", queryValues.devid, err);
            goto OnErrorExit;
        }
            
        // get pollfd attach to this sound board
        snd_ctl_poll_descriptors(evtHandle->ctlDev, &evtHandle->pfds, 1);

        // register sound event to binder main loop
        err = sd_event_add_io(afb_daemon_get_event_loop(), &evtHandle->src, evtHandle->pfds.fd, EPOLLIN, sndCtlEventCB, evtHandle);
        if (err < 0) {
            afb_req_fail_f (request, "register-mainloop", "Cannot hook events to mainloop devid=%s err=%d", queryValues.devid, err);
            goto OnErrorExit;
        }

        // create binder event attached to devid name
        evtHandle->afbevt = afb_daemon_make_event (queryValues.devid);
        if (!afb_event_is_valid (evtHandle->afbevt)) {
            afb_req_fail_f (request, "register-event", "Cannot register new binder event name=%s", queryValues.devid);
            goto OnErrorExit; 
        }

        // everything looks OK let's move forward 
        idx=idxFree;
    }
    
    // subscribe to binder event    
    err = afb_req_subscribe(request, evtHandle->afbevt);
    if (err != 0) {
        afb_req_fail_f (request, "register-eventname", "Cannot subscribe binder event name=%s [invalid channel]", queryValues.devid);
        goto OnErrorExit;
    }

    // increase usage count and return success
    sndHandles[idx].ucount ++;
    afb_req_success(request, NULL, NULL);
    return;
    
  OnErrorExit:
        if (ctlDev) snd_ctl_close(ctlDev);            
        return;
}

// Subscribe to every Alsa CtlEvent send by a given board
STATIC json_object *alsaProbeCardId (afb_req request) {
    char devid [10];
    const char *ctlName, *shortname, *longname;
    int card, err, index, idx;
    json_object *responseJ;
    snd_ctl_t   *ctlDev;
    snd_ctl_card_info_t *cardinfo;
    
    const char *sndname = afb_req_value(request, "sndname");
    if (sndname == NULL) {
        afb_req_fail_f (request, "argument-missing", "sndname=SndCardName missing");
        goto OnErrorExit;
    }
    
    // loop on potential card number
    snd_ctl_card_info_alloca(&cardinfo);  
    for (card =0; card < MAX_SND_CARD; card++) {

        // build card devid and probe it
        snprintf (devid, sizeof(devid), "hw:%i", card);
        
        // open control interface for devid
        err = snd_ctl_open(&ctlDev, devid, SND_CTL_READONLY);
        if (err < 0) continue;   
        
        // extract sound card information
        snd_ctl_card_info(ctlDev, cardinfo);
        index    = snd_ctl_card_info_get_card(cardinfo);
        ctlName  = snd_ctl_card_info_get_id(cardinfo);
        shortname= snd_ctl_card_info_get_name(cardinfo);
        longname = snd_ctl_card_info_get_longname(cardinfo);
        
        // check if short|long name match        
        if (!strcmp (sndname, ctlName)) break;
        if (!strcmp (sndname, shortname)) break;
        if (!strcmp (sndname, longname)) break;
    }
    
    if (card == MAX_SND_CARD) {
        afb_req_fail_f (request, "ctlDev-notfound", "Fail to find card with name=%s", sndname);
        goto OnErrorExit;
    }
    
    // proxy ctlevent as a binder event        
    responseJ = json_object_new_object();
    json_object_object_add(responseJ, "index"     ,json_object_new_int (index));
    json_object_object_add(responseJ, "devid"     ,json_object_new_string (devid));
    json_object_object_add(responseJ, "shortname" ,json_object_new_string (shortname));
    json_object_object_add(responseJ, "longname"  ,json_object_new_string (longname));
        
    // search for a HAL binder card mapping name to api prefix
    for (idx=0; (idx < MAX_SND_CARD && cardRegistry[idx]); idx++) {
        if (!strcmp (cardRegistry[idx]->shortname, shortname)) {
            json_object_object_add(responseJ, "halapi",json_object_new_string (cardRegistry[idx]->apiprefix));
            break;
        }
    }
  
    return responseJ;
    
    OnErrorExit:
        return NULL;  
}

// Make alsaProbeCardId compatible with AFB request
PUBLIC void alsaGetCardId (afb_req request) {
    
    json_object *responseJ = alsaProbeCardId (request);
    if (responseJ) afb_req_success(request, responseJ, NULL);
}

// Return list of active resgistrated HAL with corresponding sndcard
PUBLIC void alsaActiveHal (afb_req request) {
    json_object *responseJ = json_object_new_array();
    
    for (int idx=0; idx < MAX_SND_CARD; idx++) {
        if (!cardRegistry[idx]) break;
        
        json_object *haldevJ = json_object_new_object();
        json_object_object_add(haldevJ, "api", json_object_new_string(cardRegistry[idx]->apiprefix));
        if (cardRegistry[idx]->devid)    json_object_object_add(haldevJ, "devid", json_object_new_string(cardRegistry[idx]->devid));
        if (cardRegistry[idx]->shortname)json_object_object_add(haldevJ, "shortname", json_object_new_string(cardRegistry[idx]->shortname));
        if (cardRegistry[idx]->longname) json_object_object_add(haldevJ, "longname", json_object_new_string(cardRegistry[idx]->longname));
        json_object_array_add (responseJ, haldevJ);
    }
    
    afb_req_success(request, responseJ, NULL);
}


// Register loaded HAL with board Name and API prefix
PUBLIC void alsaRegisterHal (afb_req request) {
    static int index=0;
    json_object *responseJ;
    const char *shortname, *apiPrefix;
    
    apiPrefix = afb_req_value(request, "prefix");
    if (apiPrefix == NULL) {
        afb_req_fail_f (request, "argument-missing", "prefix=BindingApiPrefix missing");
        goto OnErrorExit;
    }
    
    shortname = afb_req_value(request, "sndname");
    if (shortname == NULL) {
        afb_req_fail_f (request, "argument-missing", "sndname=SndCardName missing");
        goto OnErrorExit;
    }
    
    if (index == MAX_SND_CARD) {
        afb_req_fail_f (request, "alsahal-toomany", "Fail to register sndname=[%s]", shortname);
        goto OnErrorExit;        
    }
   
    // alsaGetCardId should be check to register only valid card    
    responseJ= alsaProbeCardId(request);
    if (responseJ) {
        json_object *tmpJ;
        int done;
        
        cardRegistry[index]= malloc (sizeof(cardRegistry));
        cardRegistry[index]->apiprefix=strdup(apiPrefix);
        cardRegistry[index]->shortname=strdup(shortname);
        
        done= json_object_object_get_ex (responseJ, "devid" , &tmpJ);
        if (done) cardRegistry[index]->devid = strdup (json_object_get_string(tmpJ));
        else cardRegistry[index]->devid=NULL;
        
        done = json_object_object_get_ex (responseJ, "longname" , &tmpJ);
        if (done) cardRegistry[index]->longname = strdup (json_object_get_string(tmpJ));
        else cardRegistry[index]->longname=NULL;
        
        // make sure register close with a null value
        index++;
        cardRegistry[index]=NULL;
        
        afb_req_success(request, responseJ, NULL); 
    }
    
    // If OK return sound card Alsa ID+Info
    return;
    
  OnErrorExit:
        return;  
}