/*
* Copyright (c) 2014-2017, The Linux Foundation. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*     * Redistributions of source code must retain the above copyright
*       notice, this list of conditions and the following disclaimer.
*     * Redistributions in binary form must reproduce the above
*       copyright notice, this list of conditions and the following
*       disclaimer in the documentation and/or other materials provided
*       with the distribution.
*     * Neither the name of The Linux Foundation nor the names of its
*       contributors may be used to endorse or promote products derived
*       from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
* ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
* BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
* IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

#define LOG_TAG "keep_alive"
/*#define LOG_NDEBUG 0*/
#include <stdlib.h>
#include <cutils/log.h>
#include "audio_hw.h"
#include "audio_extn.h"
#include "platform_api.h"
#include <platform.h>

#define SILENCE_INTERVAL 2 /*In secs*/

typedef enum {
    STATE_DEINIT = -1,
    STATE_IDLE,
    STATE_ACTIVE,
    STATE_DISABLED,
} state_t;

typedef enum {
    REQUEST_WRITE,
} request_t;

typedef struct {
    pthread_mutex_t lock;
    pthread_mutex_t sleep_lock;
    pthread_cond_t  cond;
    pthread_cond_t  wake_up_cond;
    pthread_t thread;
    state_t state;
    struct listnode cmd_list;
    struct pcm *pcm;
    bool done;
    void * userdata;
} keep_alive_t;

struct keep_alive_cmd {
    struct listnode node;
    request_t req;
};

static keep_alive_t ka;

static struct pcm_config silence_config = {
    .channels = 2,
    .rate = DEFAULT_OUTPUT_SAMPLING_RATE,
    .period_size = DEEP_BUFFER_OUTPUT_PERIOD_SIZE,
    .period_count = DEEP_BUFFER_OUTPUT_PERIOD_COUNT,
    .format = PCM_FORMAT_S16_LE,
    .start_threshold = DEEP_BUFFER_OUTPUT_PERIOD_SIZE / 4,
    .stop_threshold = INT_MAX,
    .avail_min = DEEP_BUFFER_OUTPUT_PERIOD_SIZE / 4,
};

static void * keep_alive_loop(void * context);

void audio_extn_keep_alive_init(struct audio_device *adev)
{
    ka.userdata = adev;
    ka.state = STATE_IDLE;
    ka.pcm = NULL;

    if (property_get_bool("audio.keep_alive.disabled", false)) {
        ALOGE("keep alive disabled");
        ka.state = STATE_DISABLED;
        return;
    }

    pthread_mutex_init(&ka.lock, (const pthread_mutexattr_t *) NULL);
    pthread_cond_init(&ka.cond, (const pthread_condattr_t *) NULL);
    pthread_cond_init(&ka.wake_up_cond, (const pthread_condattr_t *) NULL);
    pthread_mutex_init(&ka.sleep_lock, (const pthread_mutexattr_t *) NULL);
    list_init(&ka.cmd_list);
    if (pthread_create(&ka.thread,  (const pthread_attr_t *) NULL,
                       keep_alive_loop, NULL) < 0) {
        ALOGW("Failed to create keep_alive_thread");
        /* can continue without keep alive */
        ka.state = STATE_DEINIT;
    }
}

static void send_cmd_l(request_t r)
{
    if (ka.state == STATE_DEINIT || ka.state == STATE_DISABLED)
        return;

    struct keep_alive_cmd *cmd =
        (struct keep_alive_cmd *)calloc(1, sizeof(struct keep_alive_cmd));

    if (cmd == NULL) {
        ALOGE("%s: cmd is NULL", __func__);
        return -ENOMEM;
    }

    cmd->req = r;
    list_add_tail(&ka.cmd_list, &cmd->node);
    pthread_cond_signal(&ka.cond);
}

static int close_silence_stream()
{
    if (!ka.pcm)
        return -ENODEV;

    pcm_close(ka.pcm);
    ka.pcm = NULL;
    return 0;
}

static int open_silence_stream()
{
    unsigned int flags = PCM_OUT|PCM_MONOTONIC;

    if (ka.pcm)
        return -EEXIST;

    int silence_pcm_dev_id = platform_get_pcm_device_id(USECASE_AUDIO_PLAYBACK_EXT_DISP_SILENCE,
                                                        PCM_PLAYBACK);

    ALOGD("opening silence device %d", silence_pcm_dev_id);
    struct audio_device * adev = (struct audio_device *)ka.userdata;
    ka.pcm = pcm_open(adev->snd_card, silence_pcm_dev_id,
                      flags, &silence_config);
    ALOGD("opened silence device %d", silence_pcm_dev_id);
    if (ka.pcm == NULL || !pcm_is_ready(ka.pcm)) {
        ALOGE("%s: %s", __func__, pcm_get_error(ka.pcm));
        if (ka.pcm != NULL) {
            pcm_close(ka.pcm);
            ka.pcm = NULL;
        }
        return -1;
    }
    return 0;
}


static int set_mixer_control(struct mixer *mixer,
                             const char * mixer_ctl_name,
                             const char *mixer_val)
{
    struct mixer_ctl *ctl;
    if ((mixer == NULL) || (mixer_ctl_name == NULL) || (mixer_val == NULL)) {
       ALOGE("%s: Invalid input", __func__);
       return -EINVAL;
    }
    ALOGD("setting mixer ctl %s with value %s", mixer_ctl_name, mixer_val);
    ctl = mixer_get_ctl_by_name(mixer, mixer_ctl_name);
    if (!ctl) {
        ALOGE("%s: could not get ctl for mixer cmd - %s",
              __func__, mixer_ctl_name);
        return -EINVAL;
    }

    return mixer_ctl_set_enum_by_string(ctl, mixer_val);
}

/* must be called with adev lock held */
void audio_extn_keep_alive_start()
{
    struct audio_device * adev = (struct audio_device *)ka.userdata;
    char mixer_ctl_name[MAX_LENGTH_MIXER_CONTROL_IN_INT];
    int app_type_cfg[MAX_LENGTH_MIXER_CONTROL_IN_INT], len = 0, rc;
    struct mixer_ctl *ctl;
    int acdb_dev_id, snd_device;
    struct listnode *node;
    struct audio_usecase *usecase;
    int32_t sample_rate = DEFAULT_OUTPUT_SAMPLING_RATE;

    if (ka.state == STATE_DISABLED)
        return;

    pthread_mutex_lock(&ka.lock);

    if (ka.state == STATE_DEINIT) {
        ALOGE(" %s : Invalid state ",__func__);
        goto exit;
    }

    if (audio_extn_passthru_is_active()) {
        ALOGE(" %s : Pass through is already active", __func__);
        goto exit;
    }

    if (ka.state == STATE_ACTIVE) {
        ALOGV(" %s : Keep alive state is already Active",__func__ );
        goto exit;
    }

    /* Dont start keep_alive if any other PCM session is routed to HDMI*/
    list_for_each(node, &adev->usecase_list) {
         usecase = node_to_item(node, struct audio_usecase, list);
         if (usecase->type == PCM_PLAYBACK &&
                 usecase->devices & AUDIO_DEVICE_OUT_AUX_DIGITAL)
             goto exit;
    }

    ka.done = false;

    /*configure app type */
    int silence_pcm_dev_id = platform_get_pcm_device_id(USECASE_AUDIO_PLAYBACK_EXT_DISP_SILENCE,
                                                        PCM_PLAYBACK);
    snprintf(mixer_ctl_name, sizeof(mixer_ctl_name),
             "Audio Stream %d App Type Cfg", silence_pcm_dev_id);

    ctl = mixer_get_ctl_by_name(adev->mixer, mixer_ctl_name);
    if (!ctl) {
        ALOGE("%s: Could not get ctl for mixer cmd - %s", __func__,
              mixer_ctl_name);
        rc = -EINVAL;
        goto exit;
    }

    /* Configure HDMI/DP Backend with default values, this as well
     * helps reconfigure HDMI/DP backend after passthrough.
     */
    int ext_disp_type = platform_get_ext_disp_type(adev->platform);
    switch(ext_disp_type) {
        case EXT_DISPLAY_TYPE_HDMI:
            snd_device = SND_DEVICE_OUT_HDMI;
            set_mixer_control(adev->mixer, "HDMI RX Format", "LPCM");
            set_mixer_control(adev->mixer, "HDMI_RX SampleRate", "KHZ_48");
            set_mixer_control(adev->mixer, "HDMI_RX Channels", "Two");
            break;
        case EXT_DISPLAY_TYPE_DP:
            snd_device = SND_DEVICE_OUT_DISPLAY_PORT;
            set_mixer_control(adev->mixer, "Display Port RX Format", "LPCM");
            set_mixer_control(adev->mixer, "Display Port RX SampleRate", "KHZ_48");
            set_mixer_control(adev->mixer, "Display Port RX Channels", "Two");
            break;
        default:
            ALOGE("%s: Invalid external display type:%d", __func__, ext_disp_type);
            goto exit;
    }

    acdb_dev_id = platform_get_snd_device_acdb_id(snd_device);
    if (acdb_dev_id < 0) {
        ALOGE("%s: Couldn't get the acdb dev id", __func__);
        rc = -EINVAL;
        goto exit;
    }

    sample_rate = DEFAULT_OUTPUT_SAMPLING_RATE;
    app_type_cfg[len++] = platform_get_default_app_type(adev->platform);
    app_type_cfg[len++] = acdb_dev_id;
    app_type_cfg[len++] = sample_rate;

    ALOGI("%s:%d PLAYBACK app_type %d, acdb_dev_id %d, sample_rate %d",
          __func__, __LINE__,
          platform_get_default_app_type(adev->platform),
          acdb_dev_id, sample_rate);
    mixer_ctl_set_array(ctl, app_type_cfg, len);

    /*send calibration*/
    usecase = calloc(1, sizeof(struct audio_usecase));

    if (usecase == NULL) {
        ALOGE("%s: usecase is NULL", __func__);
        rc = -ENOMEM;
        goto exit;
    }
    usecase->type = PCM_PLAYBACK;
    usecase->out_snd_device = snd_device;

    platform_send_audio_calibration(adev->platform, usecase,
                platform_get_default_app_type(adev->platform), sample_rate);

    /*apply audio route */
    switch(ext_disp_type) {
        case EXT_DISPLAY_TYPE_HDMI:
            audio_route_apply_and_update_path(adev->audio_route, "silence-playback hdmi");
            break;
        case EXT_DISPLAY_TYPE_DP:
            audio_route_apply_and_update_path(adev->audio_route, "silence-playback display-port");
            break;
        default:
            ALOGE("%s: Invalid external display type:%d", __func__, ext_disp_type);
            goto exit;
    }

    if (open_silence_stream() == 0) {
        send_cmd_l(REQUEST_WRITE);
        while (ka.state != STATE_ACTIVE) {
            pthread_cond_wait(&ka.cond, &ka.lock);
        }
    }

exit:
    pthread_mutex_unlock(&ka.lock);
}

/* must be called with adev lock held */
void audio_extn_keep_alive_stop()
{
    struct audio_device * adev = (struct audio_device *)ka.userdata;

    if (ka.state == STATE_DISABLED)
        return;

    pthread_mutex_lock(&ka.lock);

    if ((ka.state == STATE_DEINIT) || (ka.state == STATE_IDLE))
        goto exit;

    pthread_mutex_lock(&ka.sleep_lock);
    ka.done = true;
    pthread_cond_signal(&ka.wake_up_cond);
    pthread_mutex_unlock(&ka.sleep_lock);
    while (ka.state != STATE_IDLE) {
        pthread_cond_wait(&ka.cond, &ka.lock);
    }
    close_silence_stream();

    /*apply audio route */
    int ext_disp_type = platform_get_ext_disp_type(adev->platform);
    switch(ext_disp_type) {
        case EXT_DISPLAY_TYPE_HDMI:
            audio_route_reset_and_update_path(adev->audio_route, "silence-playback hdmi");
            break;
        case EXT_DISPLAY_TYPE_DP:
            audio_route_reset_and_update_path(adev->audio_route, "silence-playback display-port");
            break;
        default:
            ALOGE("%s: Invalid external display type:%d", __func__, ext_disp_type);
    }

exit:
    pthread_mutex_unlock(&ka.lock);
}

bool audio_extn_keep_alive_is_active()
{
    return ka.state == STATE_ACTIVE;
}

int audio_extn_keep_alive_set_parameters(struct audio_device *adev __unused,
                                         struct str_parms *parms)
{
    char value[32];
    int ret;

    if (ka.state == STATE_DISABLED)
        return;

    ret = str_parms_get_str(parms, AUDIO_PARAMETER_DEVICE_CONNECT, value, sizeof(value));
    if (ret >= 0) {
        int val = atoi(value);
        if (val & AUDIO_DEVICE_OUT_AUX_DIGITAL) {
            if (!audio_extn_passthru_is_active()) {
                ALOGV("start keep alive");
                audio_extn_keep_alive_start();
            }
        }
    }

    ret = str_parms_get_str(parms, AUDIO_PARAMETER_DEVICE_DISCONNECT, value,
                            sizeof(value));
    if (ret >= 0) {
        int val = atoi(value);
        if (val & AUDIO_DEVICE_OUT_AUX_DIGITAL) {
            ALOGV("stop keep_alive");
            audio_extn_keep_alive_stop();
        }
    }
    return 0;
}


static void * keep_alive_loop(void * context __unused)
{
    struct keep_alive_cmd *cmd = NULL;
    struct listnode *item;
    uint8_t * silence = NULL;
    int32_t bytes = 0;
    struct timespec ts;

    while (true) {
        pthread_mutex_lock(&ka.lock);
        if (list_empty(&ka.cmd_list)) {
            pthread_cond_wait(&ka.cond, &ka.lock);
            pthread_mutex_unlock(&ka.lock);
            continue;
        }

        item = list_head(&ka.cmd_list);
        cmd = node_to_item(item, struct keep_alive_cmd, node);
        list_remove(item);

        if (cmd->req != REQUEST_WRITE) {
            free(cmd);
            pthread_mutex_unlock(&ka.lock);
            continue;
        }

        free(cmd);
        ka.state = STATE_ACTIVE;
        pthread_cond_signal(&ka.cond);
        pthread_mutex_unlock(&ka.lock);

        if (!silence) {
            /* 50 ms */
            bytes =
                (silence_config.rate * silence_config.channels * sizeof(int16_t)) / 20;
            silence = (uint8_t *)calloc(1, bytes);
        }

        while (!ka.done) {
            ALOGV("write %d bytes of silence", bytes);
            pcm_write(ka.pcm, (void *)silence, bytes);
            /* This thread does not have to write silence continuously.
             * Just something to keep the connection alive is sufficient.
             * Hence a short burst of silence periodically.
             */
            pthread_mutex_lock(&ka.sleep_lock);
            clock_gettime(CLOCK_REALTIME, &ts);
            ts.tv_sec += SILENCE_INTERVAL;
            ts.tv_nsec = 0;

            if (!ka.done)
              pthread_cond_timedwait(&ka.wake_up_cond,
                            &ka.sleep_lock, &ts);

            pthread_mutex_unlock(&ka.sleep_lock);
        }
        pthread_mutex_lock(&ka.lock);
        ka.state = STATE_IDLE;
        pthread_cond_signal(&ka.cond);
        pthread_mutex_unlock(&ka.lock);
    }
    return 0;
}
