/*
 * Copyright (C) 2015 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 "radio_hw_stub"
#define LOG_NDEBUG 0

#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <pthread.h>
#include <sys/prctl.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <cutils/log.h>
#include <cutils/list.h>
#include <system/radio.h>
#include <system/radio_metadata.h>
#include <hardware/hardware.h>
#include <hardware/radio.h>

static const radio_hal_properties_t hw_properties = {
    .class_id = RADIO_CLASS_AM_FM,
    .implementor = "The Android Open Source Project",
    .product = "Radio stub HAL",
    .version = "0.1",
    .serial = "0123456789",
    .num_tuners = 1,
    .num_audio_sources = 1,
    .supports_capture = false,
    .num_bands = 2,
    .bands = {
        {
            .type = RADIO_BAND_FM,
            .antenna_connected = false,
            .lower_limit = 87900,
            .upper_limit = 107900,
            .num_spacings = 1,
            .spacings = { 200 },
            .fm = {
                .deemphasis = RADIO_DEEMPHASIS_75,
                .stereo = true,
                .rds = RADIO_RDS_US,
                .ta = false,
                .af = false,
            }
        },
        {
            .type = RADIO_BAND_AM,
            .antenna_connected = true,
            .lower_limit = 540,
            .upper_limit = 1610,
            .num_spacings = 1,
            .spacings = { 10 },
            .am = {
                .stereo = true,
            }
        }
    }
};

struct stub_radio_tuner {
    struct radio_tuner interface;
    struct stub_radio_device *dev;
    radio_callback_t callback;
    void *cookie;
    radio_hal_band_config_t config;
    radio_program_info_t program;
    bool audio;
    pthread_t callback_thread;
    pthread_mutex_t lock;
    pthread_cond_t  cond;
    struct listnode command_list;
};

struct stub_radio_device {
    struct radio_hw_device device;
    struct stub_radio_tuner *tuner;
    pthread_mutex_t lock;
};


typedef enum {
    CMD_EXIT,
    CMD_CONFIG,
    CMD_STEP,
    CMD_SCAN,
    CMD_TUNE,
    CMD_CANCEL,
    CMD_METADATA,
} thread_cmd_type_t;

struct thread_command {
    struct listnode node;
    thread_cmd_type_t type;
    struct timespec ts;
    union {
        unsigned int param;
        radio_hal_band_config_t config;
    };
};

/* must be called with out->lock locked */
static int send_command_l(struct stub_radio_tuner *tuner,
                          thread_cmd_type_t type,
                          unsigned int delay_ms,
                          void *param)
{
    struct thread_command *cmd = (struct thread_command *)calloc(1, sizeof(struct thread_command));
    struct timespec ts;

    if (cmd == NULL)
        return -ENOMEM;

    ALOGV("%s %d delay_ms %d", __func__, type, delay_ms);

    cmd->type = type;
    if (param != NULL) {
        if (cmd->type == CMD_CONFIG) {
            cmd->config = *(radio_hal_band_config_t *)param;
            ALOGV("%s CMD_CONFIG type %d", __func__, cmd->config.type);
        } else
            cmd->param = *(unsigned int *)param;
    }

    clock_gettime(CLOCK_REALTIME, &ts);

    ts.tv_sec  += delay_ms/1000;
    ts.tv_nsec += (delay_ms%1000) * 1000000;
    if (ts.tv_nsec >= 1000000000) {
        ts.tv_nsec -= 1000000000;
        ts.tv_sec  += 1;
    }
    cmd->ts = ts;
    list_add_tail(&tuner->command_list, &cmd->node);
    pthread_cond_signal(&tuner->cond);
    return 0;
}

#define BITMAP_FILE_PATH "/data/misc/media/android.png"

static int add_bitmap_metadata(radio_metadata_t **metadata, radio_metadata_key_t key,
                               const char *source)
{
    int fd;
    ssize_t ret = 0;
    struct stat info;
    void *data = NULL;
    size_t size;

    fd = open(source, O_RDONLY);
    if (fd < 0)
        return -EPIPE;

    fstat(fd, &info);
    size = info.st_size;
    data = malloc(size);
    if (data == NULL) {
        ret = -ENOMEM;
        goto exit;
    }
    ret = read(fd, data, size);
    if (ret < 0)
        goto exit;
    ret = radio_metadata_add_raw(metadata, key, (const unsigned char *)data, size);

exit:
    close(fd);
    free(data);
    ALOGE_IF(ret != 0, "%s error %d", __func__, ret);
    return (int)ret;
}

static int prepare_metadata(struct stub_radio_tuner *tuner,
                            radio_metadata_t **metadata, bool program)
{
    int ret = 0;
    char text[RADIO_STRING_LEN_MAX];
    struct timespec ts;

    if (metadata == NULL)
        return -EINVAL;

    if (*metadata != NULL)
        radio_metadata_deallocate(*metadata);

    *metadata = NULL;

    ret = radio_metadata_allocate(metadata, tuner->program.channel, 0);
    if (ret != 0)
        return ret;

    if (program) {
        ret = radio_metadata_add_int(metadata, RADIO_METADATA_KEY_RBDS_PTY, 5);
        if (ret != 0)
            goto exit;
        ret = radio_metadata_add_text(metadata, RADIO_METADATA_KEY_RDS_PS, "RockBand");
        if (ret != 0)
            goto exit;
        ret = add_bitmap_metadata(metadata, RADIO_METADATA_KEY_ICON, BITMAP_FILE_PATH);
        if (ret != 0)
            goto exit;
    } else {
        ret = add_bitmap_metadata(metadata, RADIO_METADATA_KEY_ART, BITMAP_FILE_PATH);
        if (ret != 0)
            goto exit;
    }

    clock_gettime(CLOCK_REALTIME, &ts);
    snprintf(text, RADIO_STRING_LEN_MAX, "Artist %ld", ts.tv_sec % 10);
    ret = radio_metadata_add_text(metadata, RADIO_METADATA_KEY_ARTIST, text);
    if (ret != 0)
        goto exit;

    snprintf(text, RADIO_STRING_LEN_MAX, "Song %ld", ts.tv_nsec % 10);
    ret = radio_metadata_add_text(metadata, RADIO_METADATA_KEY_TITLE, text);
    if (ret != 0)
        goto exit;

    return 0;

exit:
    radio_metadata_deallocate(*metadata);
    *metadata = NULL;
    return ret;
}

static void *callback_thread_loop(void *context)
{
    struct stub_radio_tuner *tuner = (struct stub_radio_tuner *)context;
    struct timespec ts = {0, 0};

    ALOGI("%s", __func__);

    prctl(PR_SET_NAME, (unsigned long)"sound trigger callback", 0, 0, 0);

    pthread_mutex_lock(&tuner->lock);

    while (true) {
        struct thread_command *cmd = NULL;
        struct listnode *item;
        struct listnode *tmp;
        struct timespec cur_ts;
        bool got_cancel = false;
        bool send_meta_data = false;

        if (list_empty(&tuner->command_list) || ts.tv_sec != 0) {
            ALOGV("%s SLEEPING", __func__);
            if (ts.tv_sec != 0) {
                ALOGV("%s SLEEPING with timeout", __func__);
                pthread_cond_timedwait(&tuner->cond, &tuner->lock, &ts);
            } else {
                ALOGV("%s SLEEPING forever", __func__);
                pthread_cond_wait(&tuner->cond, &tuner->lock);
            }
            ts.tv_sec = 0;
            ALOGV("%s RUNNING", __func__);
        }

        clock_gettime(CLOCK_REALTIME, &cur_ts);

        list_for_each_safe(item, tmp, &tuner->command_list) {
            cmd = node_to_item(item, struct thread_command, node);

            if (got_cancel && (cmd->type == CMD_STEP || cmd->type == CMD_SCAN ||
                    cmd->type == CMD_TUNE || cmd->type == CMD_METADATA)) {
                 list_remove(item);
                 free(cmd);
                 continue;
            }

            if ((cmd->ts.tv_sec < cur_ts.tv_sec) ||
                    ((cmd->ts.tv_sec == cur_ts.tv_sec) && (cmd->ts.tv_nsec < cur_ts.tv_nsec))) {
                radio_hal_event_t event;
                radio_metadata_t *metadata = NULL;

                event.type = RADIO_EVENT_HW_FAILURE;
                list_remove(item);

                ALOGV("%s processing command %d time %ld.%ld", __func__, cmd->type, cmd->ts.tv_sec,
                      cmd->ts.tv_nsec);

                switch (cmd->type) {
                default:
                case CMD_EXIT:
                    free(cmd);
                    goto exit;

                case CMD_CONFIG: {
                    tuner->config = cmd->config;
                    event.type = RADIO_EVENT_CONFIG;
                    event.config = tuner->config;
                    ALOGV("%s CMD_CONFIG type %d low %d up %d",
                          __func__, tuner->config.type,
                          tuner->config.lower_limit, tuner->config.upper_limit);
                    if (tuner->config.type == RADIO_BAND_FM) {
                        ALOGV("  - stereo %d\n  - rds %d\n  - ta %d\n  - af %d",
                              tuner->config.fm.stereo, tuner->config.fm.rds,
                              tuner->config.fm.ta, tuner->config.fm.af);
                    } else {
                        ALOGV("  - stereo %d", tuner->config.am.stereo);
                    }
                } break;

                case CMD_STEP: {
                    int frequency;
                    frequency = tuner->program.channel;
                    if (cmd->param == RADIO_DIRECTION_UP) {
                        frequency += tuner->config.spacings[0];
                    } else {
                        frequency -= tuner->config.spacings[0];
                    }
                    if (frequency > (int)tuner->config.upper_limit) {
                        frequency = tuner->config.lower_limit;
                    }
                    if (frequency < (int)tuner->config.lower_limit) {
                        frequency = tuner->config.upper_limit;
                    }
                    tuner->program.channel = frequency;
                    tuner->program.tuned  = (frequency / (tuner->config.spacings[0] * 5)) % 2;
                    tuner->program.signal_strength = 20;
                    if (tuner->config.type == RADIO_BAND_FM)
                        tuner->program.stereo = false;
                    else
                        tuner->program.stereo = false;

                    event.type = RADIO_EVENT_TUNED;
                    event.info = tuner->program;
                } break;

                case CMD_SCAN: {
                    int frequency;
                    frequency = tuner->program.channel;
                    if (cmd->param == RADIO_DIRECTION_UP) {
                        frequency += tuner->config.spacings[0] * 25;
                    } else {
                        frequency -= tuner->config.spacings[0] * 25;
                    }
                    if (frequency > (int)tuner->config.upper_limit) {
                        frequency = tuner->config.lower_limit;
                    }
                    if (frequency < (int)tuner->config.lower_limit) {
                        frequency = tuner->config.upper_limit;
                    }
                    tuner->program.channel = (unsigned int)frequency;
                    tuner->program.tuned  = true;
                    if (tuner->config.type == RADIO_BAND_FM)
                        tuner->program.stereo = tuner->config.fm.stereo;
                    else
                        tuner->program.stereo = tuner->config.am.stereo;
                    tuner->program.signal_strength = 50;

                    event.type = RADIO_EVENT_TUNED;
                    event.info = tuner->program;
                    send_meta_data = true;
                } break;

                case CMD_TUNE: {
                    tuner->program.channel = cmd->param;
                    tuner->program.tuned  = (tuner->program.channel /
                                                (tuner->config.spacings[0] * 5)) % 2;

                    if (tuner->program.tuned) {
                        prepare_metadata(tuner, &tuner->program.metadata, true);
                        send_command_l(tuner, CMD_METADATA, 5000, NULL);
                    } else {
                        if (tuner->program.metadata != NULL)
                            radio_metadata_deallocate(tuner->program.metadata);
                        tuner->program.metadata = NULL;
                    }
                    tuner->program.signal_strength = 100;
                    if (tuner->config.type == RADIO_BAND_FM)
                        tuner->program.stereo =
                                tuner->program.tuned ? tuner->config.fm.stereo : false;
                    else
                        tuner->program.stereo =
                            tuner->program.tuned ? tuner->config.am.stereo : false;
                    event.type = RADIO_EVENT_TUNED;
                    event.info = tuner->program;
                    send_meta_data = true;
                } break;

                case CMD_METADATA: {
                    int ret = prepare_metadata(tuner, &metadata, false);
                    if (ret == 0) {
                        event.type = RADIO_EVENT_METADATA;
                        event.metadata = metadata;
                    }
                    send_meta_data = true;
                } break;

                case CMD_CANCEL: {
                    got_cancel = true;
                } break;

                }
                if (event.type != RADIO_EVENT_HW_FAILURE && tuner->callback != NULL) {
                    pthread_mutex_unlock(&tuner->lock);
                    tuner->callback(&event, tuner->cookie);
                    pthread_mutex_lock(&tuner->lock);
                    if (event.type == RADIO_EVENT_METADATA && metadata != NULL) {
                        radio_metadata_deallocate(metadata);
                        metadata = NULL;
                    }
                }
                ALOGV("%s processed command %d", __func__, cmd->type);
                free(cmd);
            } else {
                if ((ts.tv_sec == 0) ||
                        (cmd->ts.tv_sec < ts.tv_sec) ||
                        ((cmd->ts.tv_sec == ts.tv_sec) && (cmd->ts.tv_nsec < ts.tv_nsec))) {
                    ts.tv_sec = cmd->ts.tv_sec;
                    ts.tv_nsec = cmd->ts.tv_nsec;
                }
            }
        }

        if (send_meta_data) {
            list_for_each_safe(item, tmp, &tuner->command_list) {
                cmd = node_to_item(item, struct thread_command, node);
                if (cmd->type == CMD_METADATA) {
                    list_remove(item);
                    free(cmd);
                }
            }
            send_command_l(tuner, CMD_METADATA, 100, NULL);
        }
    }

exit:
    pthread_mutex_unlock(&tuner->lock);

    ALOGV("%s Exiting", __func__);

    return NULL;
}


static int tuner_set_configuration(const struct radio_tuner *tuner,
                         const radio_hal_band_config_t *config)
{
    struct stub_radio_tuner *stub_tuner = (struct stub_radio_tuner *)tuner;
    int status = 0;

    ALOGI("%s stub_tuner %p", __func__, stub_tuner);
    pthread_mutex_lock(&stub_tuner->lock);
    if (config == NULL) {
        status = -EINVAL;
        goto exit;
    }
    send_command_l(stub_tuner, CMD_CANCEL, 0, NULL);
    send_command_l(stub_tuner, CMD_CONFIG, 500, (void *)config);

exit:
    pthread_mutex_unlock(&stub_tuner->lock);
    return status;
}

static int tuner_get_configuration(const struct radio_tuner *tuner,
                         radio_hal_band_config_t *config)
{
    struct stub_radio_tuner *stub_tuner = (struct stub_radio_tuner *)tuner;
    int status = 0;
    struct listnode *item;
    radio_hal_band_config_t *src_config;

    ALOGI("%s stub_tuner %p", __func__, stub_tuner);
    pthread_mutex_lock(&stub_tuner->lock);
    src_config = &stub_tuner->config;

    if (config == NULL) {
        status = -EINVAL;
        goto exit;
    }
    list_for_each(item, &stub_tuner->command_list) {
        struct thread_command *cmd = node_to_item(item, struct thread_command, node);
        if (cmd->type == CMD_CONFIG) {
            src_config = &cmd->config;
        }
    }
    *config = *src_config;

exit:
    pthread_mutex_unlock(&stub_tuner->lock);
    return status;
}

static int tuner_step(const struct radio_tuner *tuner,
                     radio_direction_t direction, bool skip_sub_channel)
{
    struct stub_radio_tuner *stub_tuner = (struct stub_radio_tuner *)tuner;

    ALOGI("%s stub_tuner %p direction %d, skip_sub_channel %d",
          __func__, stub_tuner, direction, skip_sub_channel);

    pthread_mutex_lock(&stub_tuner->lock);
    send_command_l(stub_tuner, CMD_STEP, 20, &direction);
    pthread_mutex_unlock(&stub_tuner->lock);
    return 0;
}

static int tuner_scan(const struct radio_tuner *tuner,
                     radio_direction_t direction, bool skip_sub_channel)
{
    struct stub_radio_tuner *stub_tuner = (struct stub_radio_tuner *)tuner;

    ALOGI("%s stub_tuner %p direction %d, skip_sub_channel %d",
          __func__, stub_tuner, direction, skip_sub_channel);

    pthread_mutex_lock(&stub_tuner->lock);
    send_command_l(stub_tuner, CMD_SCAN, 200, &direction);
    pthread_mutex_unlock(&stub_tuner->lock);
    return 0;
}

static int tuner_tune(const struct radio_tuner *tuner,
                     unsigned int channel, unsigned int sub_channel)
{
    struct stub_radio_tuner *stub_tuner = (struct stub_radio_tuner *)tuner;

    ALOGI("%s stub_tuner %p channel %d, sub_channel %d",
          __func__, stub_tuner, channel, sub_channel);

    pthread_mutex_lock(&stub_tuner->lock);
    if (channel < stub_tuner->config.lower_limit || channel > stub_tuner->config.upper_limit) {
        pthread_mutex_unlock(&stub_tuner->lock);
        ALOGI("%s channel out of range", __func__);
        return -EINVAL;
    }
    send_command_l(stub_tuner, CMD_TUNE, 100, &channel);
    pthread_mutex_unlock(&stub_tuner->lock);
    return 0;
}

static int tuner_cancel(const struct radio_tuner *tuner)
{
    struct stub_radio_tuner *stub_tuner = (struct stub_radio_tuner *)tuner;

    ALOGI("%s stub_tuner %p", __func__, stub_tuner);

    pthread_mutex_lock(&stub_tuner->lock);
    send_command_l(stub_tuner, CMD_CANCEL, 0, NULL);
    pthread_mutex_unlock(&stub_tuner->lock);
    return 0;
}

static int tuner_get_program_information(const struct radio_tuner *tuner,
                                        radio_program_info_t *info)
{
    struct stub_radio_tuner *stub_tuner = (struct stub_radio_tuner *)tuner;
    int status = 0;
    radio_metadata_t *metadata;

    ALOGI("%s stub_tuner %p", __func__, stub_tuner);
    pthread_mutex_lock(&stub_tuner->lock);
    if (info == NULL) {
        status = -EINVAL;
        goto exit;
    }
    metadata = info->metadata;
    *info = stub_tuner->program;
    info->metadata = metadata;
    if (metadata != NULL && stub_tuner->program.metadata != NULL)
        radio_metadata_add_metadata(&info->metadata, stub_tuner->program.metadata);

exit:
    pthread_mutex_unlock(&stub_tuner->lock);
    return status;
}

static int rdev_get_properties(const struct radio_hw_device *dev,
                                radio_hal_properties_t *properties)
{
    struct stub_radio_device *rdev = (struct stub_radio_device *)dev;

    ALOGI("%s", __func__);
    if (properties == NULL)
        return -EINVAL;
    memcpy(properties, &hw_properties, sizeof(radio_hal_properties_t));
    return 0;
}

static int rdev_open_tuner(const struct radio_hw_device *dev,
                          const radio_hal_band_config_t *config,
                          bool audio,
                          radio_callback_t callback,
                          void *cookie,
                          const struct radio_tuner **tuner)
{
    struct stub_radio_device *rdev = (struct stub_radio_device *)dev;
    int status = 0;

    ALOGI("%s rdev %p", __func__, rdev);
    pthread_mutex_lock(&rdev->lock);

    if (rdev->tuner != NULL) {
        status = -ENOSYS;
        goto exit;
    }

    if (config == NULL || callback == NULL || tuner == NULL) {
        status = -EINVAL;
        goto exit;
    }

    rdev->tuner = (struct stub_radio_tuner *)calloc(1, sizeof(struct stub_radio_tuner));
    if (rdev->tuner == NULL) {
        status = -ENOMEM;
        goto exit;
    }

    rdev->tuner->interface.set_configuration = tuner_set_configuration;
    rdev->tuner->interface.get_configuration = tuner_get_configuration;
    rdev->tuner->interface.scan = tuner_scan;
    rdev->tuner->interface.step = tuner_step;
    rdev->tuner->interface.tune = tuner_tune;
    rdev->tuner->interface.cancel = tuner_cancel;
    rdev->tuner->interface.get_program_information = tuner_get_program_information;

    rdev->tuner->audio = audio;
    rdev->tuner->callback = callback;
    rdev->tuner->cookie = cookie;

    rdev->tuner->dev = rdev;

    pthread_mutex_init(&rdev->tuner->lock, (const pthread_mutexattr_t *) NULL);
    pthread_cond_init(&rdev->tuner->cond, (const pthread_condattr_t *) NULL);
    pthread_create(&rdev->tuner->callback_thread, (const pthread_attr_t *) NULL,
                        callback_thread_loop, rdev->tuner);
    list_init(&rdev->tuner->command_list);

    pthread_mutex_lock(&rdev->tuner->lock);
    send_command_l(rdev->tuner, CMD_CONFIG, 500, (void *)config);
    pthread_mutex_unlock(&rdev->tuner->lock);

    *tuner = &rdev->tuner->interface;

exit:
    pthread_mutex_unlock(&rdev->lock);
    ALOGI("%s DONE", __func__);
    return status;
}

static int rdev_close_tuner(const struct radio_hw_device *dev,
                            const struct radio_tuner *tuner)
{
    struct stub_radio_device *rdev = (struct stub_radio_device *)dev;
    struct stub_radio_tuner *stub_tuner = (struct stub_radio_tuner *)tuner;
    int status = 0;

    ALOGI("%s tuner %p", __func__, tuner);
    pthread_mutex_lock(&rdev->lock);

    if (tuner == NULL) {
        status = -EINVAL;
        goto exit;
    }

    pthread_mutex_lock(&stub_tuner->lock);
    stub_tuner->callback = NULL;
    send_command_l(stub_tuner, CMD_EXIT, 0, NULL);
    pthread_mutex_unlock(&stub_tuner->lock);
    pthread_join(stub_tuner->callback_thread, (void **) NULL);

    if (stub_tuner->program.metadata != NULL)
        radio_metadata_deallocate(stub_tuner->program.metadata);

    free(stub_tuner);
    rdev->tuner = NULL;

exit:
    pthread_mutex_unlock(&rdev->lock);
    return status;
}

static int rdev_close(hw_device_t *device)
{
    struct stub_radio_device *rdev = (struct stub_radio_device *)device;
    if (rdev != NULL) {
        free(rdev->tuner);
    }
    free(rdev);
    return 0;
}

static int rdev_open(const hw_module_t* module, const char* name,
                     hw_device_t** device)
{
    struct stub_radio_device *rdev;
    int ret;

    if (strcmp(name, RADIO_HARDWARE_DEVICE) != 0)
        return -EINVAL;

    rdev = calloc(1, sizeof(struct stub_radio_device));
    if (!rdev)
        return -ENOMEM;

    rdev->device.common.tag = HARDWARE_DEVICE_TAG;
    rdev->device.common.version = RADIO_DEVICE_API_VERSION_1_0;
    rdev->device.common.module = (struct hw_module_t *) module;
    rdev->device.common.close = rdev_close;
    rdev->device.get_properties = rdev_get_properties;
    rdev->device.open_tuner = rdev_open_tuner;
    rdev->device.close_tuner = rdev_close_tuner;

    pthread_mutex_init(&rdev->lock, (const pthread_mutexattr_t *) NULL);

    *device = &rdev->device.common;

    return 0;
}


static struct hw_module_methods_t hal_module_methods = {
    .open = rdev_open,
};

struct radio_module HAL_MODULE_INFO_SYM = {
    .common = {
        .tag = HARDWARE_MODULE_TAG,
        .module_api_version = RADIO_MODULE_API_VERSION_1_0,
        .hal_api_version = HARDWARE_HAL_API_VERSION,
        .id = RADIO_HARDWARE_MODULE_ID,
        .name = "Stub radio HAL",
        .author = "The Android Open Source Project",
        .methods = &hal_module_methods,
    },
};