/*
 * Copyright (c) 2014 Samsung Electronics Co., Ltd All Rights Reserved
 *
 * 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.
 */

#include "audio-inout.h"
#include "../../common/inc/logger.h"

#include <audio_io.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
#include <Ecore.h>

#define AUDIO_BUFFER_SIZE 512
#define PROGRESS_UPD_PERIOD 0.2

typedef void (*audio_function)(audio_inout *obj);

struct _audio_inout
{
    void *handle; // tizen audio-io handle
    audio_buffer *buffer;

    audio_function start_fn;
    audio_function destroy_fn;

    audio_state_cb start_cb;
    audio_state_cb stop_cb;
    audio_progress_cb prog_cb;
    void *cb_user_data;

    Ecore_Thread *thread;
    bool stop;
    bool delete_me;
    int byte_per_sec;
};

typedef struct
{
    size_t size;
    double elapsed_time;
    audio_inout *inout;
} audio_progress_cb_wrapper;

static int _get_sample_type_size(audio_sample_type_e type);
static void _audio_in_destroy(audio_inout *obj);
static void _audio_out_destroy(audio_inout *obj);
static void _routine(void *data, Ecore_Thread *thread);
static void _audio_in_start(audio_inout *obj);
static void _audio_out_start(audio_inout *obj);
static void _report_progress(audio_inout *obj, size_t processed_bytes);
static int _get_channel_count(audio_channel_e type);
static size_t _get_size(audio_param *param);
void _destroy(audio_inout *obj);
void _deferred_destroy(audio_inout *obj);

static void _on_audio_progress_mainloop_cb(void *data, Ecore_Thread *thread, void *msg_data);
static void _on_end_mainloop_cb(void *data, Ecore_Thread *thread);

size_t audio_get_buffer_len(audio_param *param, unsigned time)
{
    RETVM_IF(!param, 0, "audio_param is null");
    return _get_size(param) * time;
}

audio_buffer *audio_buffer_new(audio_param *param, unsigned time)
{
    RETVM_IF(!param, 0, "audio_param is null");
    audio_buffer *buf = calloc(1, sizeof(audio_buffer));

    if(buf)
    {
        size_t size = audio_get_buffer_len(param, time);
        buf->samples = calloc(size, 1);
        buf->counter = 1;
        buf->len = 0;
        buf->capacity = size;
    }

    return buf;
}

void audio_buffer_link(audio_buffer *buffer)
{
    if(buffer)
    {
        ++(buffer->counter);
    }
}

void audio_buffer_unlink(audio_buffer *buffer)
{
    if(buffer)
    {
        --(buffer->counter);
        if(buffer->counter == 0)
        {
            free(buffer->samples);
            free(buffer);
        }
    }
}

int audio_get_duration(audio_param *param, size_t buffer_len)
{
    size_t size = _get_size(param);
    assert(size);
    return size ? ((double)buffer_len / size + 0.5) : 0;
}

audio_inout *audio_input_create(audio_param *param)
{
    RETVM_IF(!param, NULL, "param is null");
    audio_inout *obj = calloc(1, sizeof(audio_inout));

    if(obj)
    {
        obj->start_fn = _audio_in_start;
        obj->destroy_fn = _audio_in_destroy;
        obj->byte_per_sec = audio_get_buffer_len(param, 1);

        if(audio_in_create(param->sample_rate, param->channel, param->sample_type, (audio_in_h*)&obj->handle) == 0)
        {
            audio_in_prepare(obj->handle);
        }
    }

    return obj;
}

audio_inout *audio_output_create(audio_param *param)
{
    RETVM_IF(!param, NULL, "param is null");
    audio_inout *obj = calloc(1, sizeof(audio_inout));

    if(obj)
    {
        obj->start_fn = _audio_out_start;
        obj->destroy_fn = _audio_out_destroy;
        obj->byte_per_sec = audio_get_buffer_len(param, 1);

        if(audio_out_create(param->sample_rate, param->channel, param->sample_type, SOUND_TYPE_MEDIA, (audio_out_h*)&obj->handle) == 0)
        {
            audio_out_prepare(obj->handle);
        }
    }

    return obj;
}

void audio_destroy(audio_inout *obj)
{
    if(obj)
    {
        if(obj->thread)
        {
            _deferred_destroy(obj);
            audio_stop(obj);
        }
        else
        {
            _destroy(obj);
        }
    }
}

void audio_set_cbs(audio_inout *obj, audio_state_cb start_cb, audio_state_cb stop_cb, audio_progress_cb prog_cb, void *user_data)
{
    if(obj)
    {
        obj->start_cb = start_cb;
        obj->stop_cb = stop_cb;
        obj->prog_cb = prog_cb;
        obj->cb_user_data = user_data;
    }
}

void audio_start(audio_inout *obj)
{
    if(obj && obj->buffer && obj->buffer->samples)
    {
        audio_stop(obj);

        obj->thread = ecore_thread_feedback_run(_routine, _on_audio_progress_mainloop_cb,
                                                _on_end_mainloop_cb, NULL, obj, EINA_TRUE);

        if(obj->thread)
        {
            obj->stop = false;
            if(obj->start_cb)
            {
                obj->start_cb(obj, obj->cb_user_data);
            }
        }
    }
}

bool audio_is_started(const audio_inout *obj)
{
    return obj ? !obj->stop : false;
}

static void _routine(void *data, Ecore_Thread *thread)
{
    audio_inout *obj = data;
    if(obj)
    {
        obj->stop = false;
        obj->start_fn(obj);
        obj->stop = true;
        obj->thread = NULL;
    }
}

void audio_stop(audio_inout *obj)
{
    if(obj)
    {
        obj->stop = true;
    }
}

void audio_set_buffer(audio_inout *obj, audio_buffer *buffer)
{
    if(obj)
    {
        audio_buffer_unlink(obj->buffer);
        obj->buffer = buffer;
        audio_buffer_link(obj->buffer);
    }
}

void _destroy(audio_inout *obj)
{
    audio_stop(obj);
    if(obj->destroy_fn)
    {
        obj->destroy_fn(obj);
    }
    audio_buffer_unlink(obj->buffer);
    free(obj);
}

void _deferred_destroy(audio_inout *obj)
{
    obj->delete_me = true;
    obj->prog_cb = NULL;
    obj->start_cb = NULL;
    obj->stop_cb = NULL;
}

static int _get_sample_type_size(audio_sample_type_e type)
{
    switch(type)
    {
        case AUDIO_SAMPLE_TYPE_U8:
            return 1;

        case AUDIO_SAMPLE_TYPE_S16_LE:
            return 2;

        default:
            assert(false);
            break;
    }

    return 0;
}

static int _get_channel_count(audio_channel_e type)
{
    switch(type)
    {
        case AUDIO_CHANNEL_MONO:
            return 1;

        case AUDIO_CHANNEL_STEREO:
            return 2;

        default:
            assert(false);
            break;
    }

    return 0;
}

static size_t _get_size(audio_param *param)
{
    return _get_channel_count(param->channel) * _get_sample_type_size(param->sample_type) * param->sample_rate;
}

static void _audio_in_start(audio_inout *obj)
{
    audio_buffer *buf = obj->buffer;
    char *smaples = buf->samples;
    int len = 0;
    size_t read_bytes = 0;
    size_t last_reported_bytes = 0;

    do
    {
        len = AUDIO_BUFFER_SIZE;
        if(buf->len + len > buf->capacity)
        {
            len = buf->capacity - buf->len;
        }

        if(len > 0)
        {
            int bytes = audio_in_read((audio_in_h)obj->handle, smaples, len);
            assert(bytes >= 0);

            if(bytes)
            {
                smaples += bytes;
                buf->len += bytes;
                read_bytes += bytes;

                if(read_bytes - last_reported_bytes >= obj->byte_per_sec * PROGRESS_UPD_PERIOD)
                {
                    _report_progress(obj, read_bytes);
                    last_reported_bytes = read_bytes;
                }
            }
        }
    } while(!obj->stop && len > 0);

    _report_progress(obj, read_bytes);
}

static void _audio_out_start(audio_inout *obj)
{
    audio_buffer *buf = obj->buffer;
    char *smaples = buf->samples;
    int len = 0;
    size_t written_bytes = 0;
    size_t last_reported_bytes = 0;

    do
    {
        len = AUDIO_BUFFER_SIZE;
        if(written_bytes + len > buf->len)
        {
            len = buf->len - written_bytes;
        }

        if(len > 0)
        {
            int bytes = audio_out_write((audio_out_h)obj->handle, smaples, len);
            assert(bytes >= 0);

            if(bytes)
            {
                smaples += bytes;
                written_bytes += bytes;

                if(written_bytes - last_reported_bytes >= obj->byte_per_sec * PROGRESS_UPD_PERIOD)
                {
                    _report_progress(obj, written_bytes);
                    last_reported_bytes = written_bytes;
                }
            }
        }
    } while(!obj->stop && len > 0);

    _report_progress(obj, written_bytes);
}

static void _report_progress(audio_inout *obj, size_t processed_bytes)
{
    if(obj->prog_cb)
    {
        audio_progress_cb_wrapper *wrapper = calloc(1, sizeof(audio_progress_cb_wrapper));

        if(wrapper)
        {
            wrapper->inout = obj;
            wrapper->size = processed_bytes;
            wrapper->elapsed_time = processed_bytes / (double)obj->byte_per_sec;
            ecore_thread_feedback(obj->thread, wrapper);
        }
    }
}

static void _audio_in_destroy(audio_inout *obj)
{
    if(obj->handle)
    {
        audio_in_unprepare((audio_in_h)obj->handle);
        audio_in_destroy((audio_in_h)obj->handle);
    }
}

static void _audio_out_destroy(audio_inout *obj)
{
    if(obj->handle)
    {
        audio_out_unprepare((audio_out_h)obj->handle);
        audio_out_destroy((audio_out_h)obj->handle);
    }
}

static void _on_end_mainloop_cb(void *data, Ecore_Thread *thread)
{
    audio_inout *obj = data;
    if(obj)
    {
        if(obj->delete_me)
        {
            _destroy(obj);
        }
        else
        {
            if(obj->stop_cb)
            {
                obj->stop_cb(obj, obj->cb_user_data);
            }
            obj->thread = NULL;
        }
    }
}

static void _on_audio_progress_mainloop_cb(void *data, Ecore_Thread *thread, void *msg_data)
{
    audio_progress_cb_wrapper *wrapper = msg_data;
    if(wrapper && wrapper->inout->prog_cb)
    {
        wrapper->inout->prog_cb(wrapper->inout, wrapper->size, wrapper->elapsed_time, wrapper->inout->cb_user_data);
        free(wrapper);
    }
}
