/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * 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 LOG_TAG "audio_hw_sndmonitor"
/*#define LOG_NDEBUG 0*/
#define LOG_NDDEBUG 0

/* monitor sound card, cpe state

   audio_dev registers for a callback from this module in adev_open
   Each stream in audio_hal registers for a callback in
   adev_open_*_stream.

   A thread is spawned to poll() on sound card state files in /proc.
   On observing a sound card state change, this thread invokes the
   callbacks registered.

   Callbacks are deregistered in adev_close_*_stream and adev_close
*/
#include <stdlib.h>
#include <dirent.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/poll.h>
#include <pthread.h>
#include <cutils/list.h>
#include <cutils/hashmap.h>
#include <log/log.h>
#include <cutils/str_parms.h>
#include <ctype.h>

#include "audio_hw.h"
#include "audio_extn.h"

//#define MONITOR_DEVICE_EVENTS
#define CPE_MAGIC_NUM 0x2000
#define MAX_CPE_SLEEP_RETRY 2
#define CPE_SLEEP_WAIT 100

#define MAX_SLEEP_RETRY 100
#define AUDIO_INIT_SLEEP_WAIT 100 /* 100 ms */

#define AUDIO_PARAMETER_KEY_EXT_AUDIO_DEVICE "ext_audio_device"

typedef enum {
    audio_event_on,
    audio_event_off
} audio_event_status;

typedef struct {
    int card;
    int fd;
    struct listnode node; // membership in sndcards list
    card_status_t status;
} sndcard_t;

typedef struct {
    char * dev;
    int fd;
    int status;
    struct listnode node; // membership in deviceevents list;
} dev_event_t;

typedef void (* notifyfn)(const void * target, const char * msg);

typedef struct {
    const void * target;
    notifyfn notify;
    struct listnode cards;
    unsigned int num_cards;
    struct listnode dev_events;
    unsigned int num_dev_events;
    pthread_t monitor_thread;
    int intpipe[2];
    Hashmap * listeners; // from stream * -> callback func
    bool initcheck;
} sndmonitor_state_t;

static sndmonitor_state_t sndmonitor;

static char * read_state(int fd)
{
    struct stat buf;
    if (fstat(fd, &buf) < 0)
        return NULL;

    off_t pos = lseek(fd, 0, SEEK_CUR);
    off_t avail = buf.st_size - pos;
    if (avail <= 0) {
        ALOGD("avail %ld", avail);
        return NULL;
    }

    char * state = (char *)calloc(avail+1, sizeof(char));
    if (!state)
        return NULL;

    ssize_t bytes=read(fd, state, avail);
    if (bytes <= 0)
        return NULL;

    // trim trailing whitespace
    while (bytes && isspace(*(state+bytes-1))) {
        *(state + bytes - 1) = '\0';
        --bytes;
    }
    lseek(fd, 0, SEEK_SET);
    return state;
}

static int add_new_sndcard(int card, int fd)
{
    sndcard_t * s = (sndcard_t *)calloc(sizeof(sndcard_t), 1);

    if (!s)
        return -1;

    s->card = card;
    s->fd = fd; // dup?

    char * state = read_state(fd);
    bool online = state && !strcmp(state, "ONLINE");

    ALOGV("card %d initial state %s %d", card, state, online);

    if (state)
        free(state);

    s->status = online ? CARD_STATUS_ONLINE : CARD_STATUS_OFFLINE;
    list_add_tail(&sndmonitor.cards, &s->node);
    return 0;
}

static int enum_sndcards()
{
    const char* cards = "/proc/asound/cards";
    int tries = 10;
    char *line = NULL;
    size_t len = 0;
    ssize_t bytes_read;
    char path[128] = {0};
    char *ptr, *saveptr, *card_id;
    int line_no=0;
    unsigned int num_cards=0, num_cpe=0;
    FILE *fp;
    int fd, ret;

    while (--tries) {
        if ((fp = fopen(cards, "r")) == NULL) {
            ALOGE("Cannot open %s file to get list of sound cards", cards);
            usleep(100000);
            continue;
        }
        break;
    }

    if (!tries)
        return -ENODEV;

    while ((bytes_read = getline(&line, &len, fp) != -1)) {
        // skip every other line to to match
        // the output format of /proc/asound/cards
        if (line_no++ % 2)
            continue;

        ptr = strtok_r(line, " [", &saveptr);
        if (!ptr)
            continue;

        card_id = strtok_r(saveptr+1, "]", &saveptr);
        if (!card_id)
            continue;

        // Limit to sound cards associated with ADSP
        if ((strncasecmp(card_id, "msm", 3) != 0) &&
            (strncasecmp(card_id, "sdm", 3) != 0) &&
            (strncasecmp(card_id, "sdc", 3) != 0) &&
            (strncasecmp(card_id, "apq", 3) != 0)) {
            ALOGW("Skip over non-ADSP snd card %s", card_id);
            continue;
        }

        snprintf(path, sizeof(path), "/proc/asound/card%s/state", ptr);
        ALOGV("Opening sound card state : %s", path);

        fd = open(path, O_RDONLY);
        if (fd == -1) {
            ALOGE("Open %s failed : %s", path, strerror(errno));
            continue;
        }

        ret = add_new_sndcard(atoi(ptr), fd);
        if (ret != 0) {
            close(fd); // card state fd ownership is taken by sndcard on success
            continue;
        }

        num_cards++;

        // query cpe state for this card as well
        tries=MAX_CPE_SLEEP_RETRY;
        snprintf(path, sizeof(path), "/proc/asound/card%s/cpe0_state", ptr);

        if (access(path, R_OK) < 0) {
            ALOGW("access %s failed w/ err %s", path, strerror(errno));
            continue;
        }

        ALOGV("Open cpe state card state %s", path);
        while (--tries) {
            if ((fd = open(path, O_RDONLY)) < 0) {
                ALOGW("Open cpe state card state failed, retry : %s", path);
                usleep(CPE_SLEEP_WAIT*1000);
                continue;
            }
            break;
        }

        if (!tries)
            continue;

        ret = add_new_sndcard(CPE_MAGIC_NUM+num_cpe, fd);
        if (ret != 0) {
            close(fd); // card state fd ownership is taken by sndcard on success
            continue;
        }

        num_cpe++;
        num_cards++;
    }
    if (line)
        free(line);
    fclose(fp);
    ALOGV("sndmonitor registerer num_cards %d", num_cards);
    sndmonitor.num_cards = num_cards;
    return num_cards ? 0 : -1;
}

static void free_sndcards()
{
    while (!list_empty(&sndmonitor.cards)) {
        struct listnode * n = list_head(&sndmonitor.cards);
        sndcard_t * s = node_to_item(n, sndcard_t, node);
        list_remove(n);
        close(s->fd);
        free(s);
    }
}

static int add_new_dev_event(char * d_name, int fd)
{
    dev_event_t * d = (dev_event_t *)calloc(sizeof(dev_event_t), 1);

    if (!d)
        return -1;

    d->dev = strdup(d_name);
    d->fd = fd;
    list_add_tail(&sndmonitor.dev_events, &d->node);
    return 0;
}

static int enum_dev_events()
{
    const char* events_dir = "/sys/class/switch/";
    DIR *dp;
    struct dirent* in_file;
    int fd;
    char path[128] = {0};
    unsigned int num_dev_events = 0;

    if ((dp = opendir(events_dir)) == NULL) {
        ALOGE("Cannot open switch directory %s err %s",
              events_dir, strerror(errno));
        return -1;
    }

    while ((in_file = readdir(dp)) != NULL) {
        if (!strstr(in_file->d_name, "qc_"))
            continue;

        snprintf(path, sizeof(path), "%s/%s/state",
                 events_dir, in_file->d_name);

        ALOGV("Opening audio dev event state : %s ", path);
        fd = open(path, O_RDONLY);
        if (fd == -1) {
            ALOGE("Open %s failed : %s", path, strerror(errno));
        } else {
            if (!add_new_dev_event(in_file->d_name, fd))
                num_dev_events++;
        }
    }
    closedir(dp);
    sndmonitor.num_dev_events = num_dev_events;
    return num_dev_events ? 0 : -1;
}

static void free_dev_events()
{
    while (!list_empty(&sndmonitor.dev_events)) {
        struct listnode * n = list_head(&sndmonitor.dev_events);
        dev_event_t * d = node_to_item(n, dev_event_t, node);
        list_remove(n);
        close(d->fd);
        free(d->dev);
        free(d);
    }
}

static int notify(const struct str_parms * params)
{
    if (!params)
        return -1;

    char * str = str_parms_to_str((struct str_parms *)params);

    if (!str)
        return -1;

    if (sndmonitor.notify)
        sndmonitor.notify(sndmonitor.target, str);

    ALOGV("%s", str);
    free(str);
    return 0;
}

int on_dev_event(dev_event_t * dev_event)
{
    char state_buf[2];
    if (read(dev_event->fd, state_buf, 1) <= 0)
        return -1;

    lseek(dev_event->fd, 0, SEEK_SET);
    state_buf[1]='\0';
    if (atoi(state_buf) == dev_event->status)
        return 0;

    dev_event->status = atoi(state_buf);

    struct str_parms * params = str_parms_create();

    if (!params)
        return -1;

    char val[32] = {0};
    snprintf(val, sizeof(val), "%s,%s", dev_event->dev,
             dev_event->status ? "ON" : "OFF");

    if (str_parms_add_str(params, AUDIO_PARAMETER_KEY_EXT_AUDIO_DEVICE, val) < 0)
        return -1;

    int ret = notify(params);
    str_parms_destroy(params);
    return ret;
}

bool on_sndcard_state_update(sndcard_t * s)
{
    char rd_buf[9]={0};
    card_status_t status;

    if (read(s->fd, rd_buf, 8) <= 0)
        return -1;

    rd_buf[8] = '\0';
    lseek(s->fd, 0, SEEK_SET);

    ALOGV("card num %d, new state %s", s->card, rd_buf);

    bool is_cpe = (s->card >= CPE_MAGIC_NUM);
    if (strstr(rd_buf, "OFFLINE"))
        status = CARD_STATUS_OFFLINE;
    else if (strstr(rd_buf, "ONLINE"))
        status = CARD_STATUS_ONLINE;
    else {
        ALOGE("unknown state");
        return 0;
    }

    if (status == s->status) // no change
        return 0;

    s->status = status;

    struct str_parms * params = str_parms_create();

    if (!params)
        return -1;

    char val[32] = {0};
    // cpe actual card num is (card - MAGIC_NUM). so subtract accordingly
    snprintf(val, sizeof(val), "%d,%s", s->card - (is_cpe ? CPE_MAGIC_NUM : 0),
                 status == CARD_STATUS_ONLINE ? "ONLINE" : "OFFLINE");

    if (str_parms_add_str(params, is_cpe ? "CPE_STATUS" : "SND_CARD_STATUS",
                          val) < 0)
        return -1;

    int ret = notify(params);
    str_parms_destroy(params);
    return ret;
}

void * monitor_thread_loop(void * args __unused)
{
    ALOGV("Start threadLoop()");
    unsigned int num_poll_fds = sndmonitor.num_cards +
                                sndmonitor.num_dev_events + 1/*pipe*/;
    struct pollfd * pfd = (struct pollfd *)calloc(sizeof(struct pollfd),
                                                  num_poll_fds);
    if (!pfd)
        return NULL;

    pfd[0].fd = sndmonitor.intpipe[0];
    pfd[0].events = POLLPRI|POLLIN;

    int i=1;
    struct listnode *node;
    list_for_each(node, &sndmonitor.cards) {
        sndcard_t * s = node_to_item(node, sndcard_t, node);
        pfd[i].fd = s->fd;
        pfd[i].events = POLLPRI;
        ++i;
    }

    list_for_each(node, &sndmonitor.dev_events) {
        dev_event_t * d = node_to_item(node, dev_event_t, node);
        pfd[i].fd = d->fd;
        pfd[i].events = POLLPRI;
        ++i;
    }

    while (1) {
        if (poll(pfd, num_poll_fds, -1) < 0) {
            int errno_ = errno;
            ALOGE("poll() failed w/ err %s", strerror(errno));
            switch (errno_) {
            case EINTR:
            case ENOMEM:
                sleep(2);
                continue;
            default:
                /* above errors can be caused due to current system
                   state .. any other error is not expected */
                LOG_ALWAYS_FATAL("unxpected poll() system call failure");
                break;
            }
        }
        ALOGV("out of poll()");

#define READY_TO_READ(p) ((p)->revents & (POLLIN|POLLPRI))
#define ERROR_IN_FD(p) ((p)->revents & (POLLERR|POLLHUP|POLLNVAL))

        // check if requested to exit
        if (READY_TO_READ(&pfd[0])) {
            char buf[2]={0};
            read(pfd[0].fd, buf, 1);
            if (!strcmp(buf, "Q"))
                break;
        } else if (ERROR_IN_FD(&pfd[0])) {
            // do not consider for poll again
            // POLLERR - can this happen?
            // POLLHUP - adev must not close pipe
            // POLLNVAL - fd is valid
            LOG_ALWAYS_FATAL("unxpected error in pipe poll fd 0x%x",
                             pfd[0].revents);
            pfd[0].fd *= -1;
        }

        i=1;
        list_for_each(node, &sndmonitor.cards) {
            sndcard_t * s = node_to_item(node, sndcard_t, node);
            if (READY_TO_READ(&pfd[i]))
                on_sndcard_state_update(s);
            else if (ERROR_IN_FD(&pfd[i])) {
                // do not consider for poll again
                // POLLERR - can this happen as we are reading from a fs?
                // POLLHUP - not valid for cardN/state
                // POLLNVAL - fd is valid
                LOG_ALWAYS_FATAL("unxpected error in card poll fd 0x%x",
                                 pfd[i].revents);
                pfd[i].fd *= -1;
            }
            ++i;
        }

        list_for_each(node, &sndmonitor.dev_events) {
            dev_event_t * d = node_to_item(node, dev_event_t, node);
            if (READY_TO_READ(&pfd[i]))
                on_dev_event(d);
            else if (ERROR_IN_FD(&pfd[i])) {
                // do not consider for poll again
                // POLLERR - can this happen as we are reading from a fs?
                // POLLHUP - not valid for switch/state
                // POLLNVAL - fd is valid
                LOG_ALWAYS_FATAL("unxpected error in dev poll fd 0x%x",
                                 pfd[i].revents);
                pfd[i].fd *= -1;
            }
            ++i;
        }
    }

    return NULL;
}

// ---- listener static APIs ---- //
static int hashfn(void * key)
{
    return (int)key;
}

static bool hasheq(void * key1, void *key2)
{
    return key1 == key2;
}

static bool snd_cb(void* key, void* value, void* context)
{
    snd_mon_cb cb = (snd_mon_cb)value;
    cb(key, context);
    return true;
}

static void snd_mon_update(const void * target __unused, const char * msg)
{
    // target can be used to check if this message is intended for the
    // recipient or not. (using some statically saved state)

    struct str_parms *parms = str_parms_create_str(msg);

    if (!parms)
        return;

    hashmapLock(sndmonitor.listeners);
    hashmapForEach(sndmonitor.listeners, snd_cb, parms);
    hashmapUnlock(sndmonitor.listeners);

    str_parms_destroy(parms);
}

static int listeners_init()
{
    sndmonitor.listeners = hashmapCreate(5, hashfn, hasheq);
    if (!sndmonitor.listeners)
        return -1;
    return 0;
}

static int listeners_deinit()
{
    // XXX TBD
    return -1;
}

static int add_listener(void *stream, snd_mon_cb cb)
{
    Hashmap * map = sndmonitor.listeners;
    hashmapLock(map);
    hashmapPut(map, stream, cb);
    hashmapUnlock(map);
    return 0;
}

static int del_listener(void * stream)
{
    Hashmap * map = sndmonitor.listeners;
    hashmapLock(map);
    hashmapRemove(map, stream);
    hashmapUnlock(map);
    return 0;
}

// --- public APIs --- //

int audio_extn_snd_mon_deinit()
{
    if (!sndmonitor.initcheck)
        return -1;

    write(sndmonitor.intpipe[1], "Q", 1);
    pthread_join(sndmonitor.monitor_thread, (void **) NULL);
    free_dev_events();
    listeners_deinit();
    free_sndcards();
    close(sndmonitor.intpipe[0]);
    close(sndmonitor.intpipe[1]);

    sndmonitor.initcheck = 0;
    return 0;
}

int audio_extn_snd_mon_init()
{
    sndmonitor.notify = snd_mon_update;
    sndmonitor.target = NULL; // unused for now
    list_init(&sndmonitor.cards);
    list_init(&sndmonitor.dev_events);
    sndmonitor.initcheck = false;

    if (pipe(sndmonitor.intpipe) < 0)
        goto pipe_error;

    if (enum_sndcards() < 0)
        goto enum_sncards_error;

    if (listeners_init() < 0)
        goto listeners_error;

#ifdef MONITOR_DEVICE_EVENTS
    enum_dev_events(); // failure here isn't fatal
#endif

    int ret = pthread_create(&sndmonitor.monitor_thread,
                             (const pthread_attr_t *) NULL,
                             monitor_thread_loop, NULL);

    if (ret) {
        goto monitor_thread_create_error;
    }
    sndmonitor.initcheck = true;
    return 0;

monitor_thread_create_error:
    listeners_deinit();
listeners_error:
    free_sndcards();
enum_sncards_error:
    close(sndmonitor.intpipe[0]);
    close(sndmonitor.intpipe[1]);
pipe_error:
    return -ENODEV;
}

int audio_extn_snd_mon_register_listener(void *stream, snd_mon_cb cb)
{
    if (!sndmonitor.initcheck) {
        ALOGW("sndmonitor initcheck failed, cannot register");
        return -1;
    }

    return add_listener(stream, cb);
}

int audio_extn_snd_mon_unregister_listener(void * stream)
{
    if (!sndmonitor.initcheck) {
        ALOGW("sndmonitor initcheck failed, cannot deregister");
        return -1;
    }

    ALOGV("deregister listener for stream %p ", stream);
    return del_listener(stream);
}