/*
 * Copyright (C) 2018 "IoT.bzh"
 * Author Loïc Collignon <loic.collignon@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.
 */

#include <algorithm>
#include "ahl-binding.hpp"

afb_dynapi* AFB_default; // BUG: Is it possible to get rid of this ?

/**
 * @brief Entry point for dynamic API.
 * @param[in] handle Handle to start with for API creation.
 * @return Status code, zero if success.
 */
int afbBindingVdyn(afb_dynapi* handle)
{
    using namespace std::placeholders;
    assert(handle != nullptr);

    AFB_default = handle;

    return afb_dynapi_new_api(
        handle,
        HL_API_NAME,
        HL_API_INFO,
        1,
        ahl_api_create,
        nullptr
    );
}

/**
 * @brief Callback to create the new api.
 * @param[in] handle Handle to the new api.
 * @return Status code, zero if success.
 */
int ahl_api_create(void*, struct afb_dynapi* handle)
{
    return ahl_binding_t::instance().preinit(handle);
}

ahl_binding_t::ahl_binding_t()
    : handle_{nullptr}
{
}

ahl_binding_t& ahl_binding_t::instance()
{
    static ahl_binding_t s;
    return s;
}

int ahl_binding_t::preinit(afb_dynapi* handle)
{
    handle_ = handle;

    try
    {
        load_static_verbs();
        load_controller_api();

        if (afb_dynapi_on_event(handle_,
            [](afb_dynapi*, const char* e, struct json_object* o)
                { ahl_binding_t::instance().event(e, o); }
            )
        ) throw std::runtime_error("Failed to register event handler callback.");

        if (afb_dynapi_on_init(handle_, 
            [](afb_dynapi*) { return ahl_binding_t::instance().init(); }
        )) throw std::runtime_error("Failed to register init handler callback.");
    }
    catch(std::exception& e)
    {
        AFB_DYNAPI_ERROR(handle, "%s", e.what());
        return -1;
    }

    return 0;
}

int ahl_binding_t::init()
{
    using namespace std::placeholders;
    
    if (afb_dynapi_require_api(handle_, HAL_MGR_API, 1))
    {
        AFB_DYNAPI_ERROR(handle_, "Failed to require '%s' API!", HAL_MGR_API);
        return -1;
    }
    AFB_DYNAPI_NOTICE(handle_, "Required '%s' API found!", HAL_MGR_API);
    
    if (afb_dynapi_require_api(handle_, "smixer", 1))
    {
        AFB_DYNAPI_ERROR(handle_, "Failed to require 'smixer' API!");
        return -1;
    }
    AFB_DYNAPI_NOTICE(handle_, "Required 'smixer' API found!");

    // Requires corresponding API
    for(const auto& h : config_.hals())
    {
        if (afb_dynapi_require_api(handle_, h.c_str(), 1))
        {
            AFB_DYNAPI_ERROR(handle_, "Failed to require '%s' API!", h.c_str());
            return -1;
        }
        AFB_DYNAPI_NOTICE(handle_, "Required '%s' API found!", h.c_str());
        json_object* result = nullptr;
        
        // if(afb_dynapi_call_sync(handle_, h.c_str(), "init-mixer", nullptr, &result))
        // {
        //     AFB_DYNAPI_ERROR(handle_, "Failed to call 'init-mixer' verb on '%s' API!", h.c_str());
        //     if (result) AFB_DYNAPI_NOTICE(handle_, "%s", json_object_to_json_string(result));
        //     return -1;
        // }
        // AFB_DYNAPI_NOTICE(handle_, "Mixer initialized using '%s' API.", h.c_str());
        
        // json_object* response = nullptr;
        // json_object* verbose = json_object_new_object();
        // json_object_object_add(verbose, "verbose", json_object_new_int(1));
        // if (afb_dynapi_call_sync(handle_, h.c_str(), "list", verbose, &response))
        // {
        //     AFB_DYNAPI_ERROR(handle_, "Failed to call 'list' verb on '%s' API!", h.c_str());
        //     if (result) AFB_DYNAPI_NOTICE(handle_, "%s", json_object_to_json_string(result));
        //     return -1;
        // }
        
        json_object* response = nullptr;
        json_object* arg = json_object_new_object();
        json_object_object_add(arg, "streams", json_object_new_boolean(TRUE));
        
        if (afb_dynapi_call_sync(handle_, "smixer", "info", arg, &response))
        {
            AFB_DYNAPI_ERROR(handle_, "Failed to call 'list' verb on '%s' API!", h.c_str());
            if (result) AFB_DYNAPI_NOTICE(handle_, "%s", json_object_to_json_string(result));
            return -1;
        } 


        json_object* streams = json_object_object_get(response, "response");
        AFB_DYNAPI_DEBUG(handle_, "Called 'list' verb on '%s' API: %s", h.c_str(), json_object_to_json_string(streams));
        json_object* array = json_object_object_get(streams, "streams");
        size_t streams_count = json_object_array_length(array);
        for(size_t i = 0; i < streams_count; ++i)
        {
            std::string stream_name;
            std::string device_uri;
            json_object* item = json_object_array_get_idx(array, i);
            
            jcast(stream_name, item, "uid");
            jcast(device_uri, item, "alsa");
            
            config_.set_device_uri(stream_name, device_uri);
        }
    }
    
    afb_dynapi_seal(handle_);
    AFB_DYNAPI_NOTICE(handle_, "API is now sealed!");
    
    actions_["volume"] = std::bind(&ahl_binding_t::volume, this, _1, _2, _3, _4);
    actions_["open"] = std::bind(&ahl_binding_t::open, this, _1, _2, _3, _4);
    actions_["close"] = std::bind(&ahl_binding_t::close, this, _1, _2, _3, _4);
    actions_["interrupt"] = std::bind(&ahl_binding_t::interrupt, this, _1, _2, _3, _4);
    
    return 0;
}

void ahl_binding_t::event(std::string name, json_object* arg)
{
    AFB_DYNAPI_DEBUG(handle_, "Event '%s' received with the following arg: %s", name.c_str(), json_object_to_json_string(arg));
}

void ahl_binding_t::load_static_verbs()
{
    if (afb_dynapi_add_verb(
            handle_,
            "get_roles",
            "Retrieve array of available audio roles",
            [](afb_request* r) { ahl_binding_t::instance().get_roles(r); },
            nullptr,
            nullptr,
            AFB_SESSION_NONE_V2))
    {
        throw std::runtime_error("Failed to add 'get_role' verb to the API.");
    }
}

void ahl_binding_t::load_controller_api()
{
    char* dir_list = getenv("CONTROL_CONFIG_PATH");
    if (!dir_list) dir_list = strdup(CONTROL_CONFIG_PATH);
    struct json_object* config_files = CtlConfigScan(dir_list, "policy");
    if (!config_files) throw std::runtime_error("No config files found!");

    // Only one file should be found this way, but read all just in case
    size_t config_files_count = json_object_array_length(config_files);
    for(size_t i = 0; i < config_files_count; ++i)
    {
        config_entry_t file {json_object_array_get_idx(config_files, i)};

        if(load_controller_config(file.filepath()) < 0)
        {
            std::stringstream ss;
            ss  << "Failed to load config file '"
                << file.filename()
                << "' from '"
                << file.fullpath()
                << "'!";
            throw std::runtime_error(ss.str());
        }
    }
}

int ahl_binding_t::load_controller_config(const std::string& path)
{
    CtlConfigT* controller_config;

    controller_config = CtlLoadMetaData(handle_, path.c_str());
    if (!controller_config)
    {
        AFB_DYNAPI_ERROR(handle_, "Failed to load controller from config file!");
        return -1;
    }
    
    static CtlSectionT controller_sections[] =
    {
        {.key = "plugins",  .uid = nullptr, .info = nullptr, .loadCB = PluginConfig,  .handle = nullptr, .actions = nullptr},
        {.key = "onload",   .uid = nullptr, .info = nullptr, .loadCB = OnloadConfig,  .handle = nullptr, .actions = nullptr},
        {.key = "controls", .uid = nullptr, .info = nullptr, .loadCB = ControlConfig, .handle = nullptr, .actions = nullptr},
        {.key = "events",   .uid = nullptr, .info = nullptr, .loadCB = EventConfig,   .handle = nullptr, .actions = nullptr},
        {
            .key = "ahl-4a",
            .uid = nullptr,
            .info = nullptr,
            .loadCB = [](afb_dynapi*, CtlSectionT* s, json_object* o){
                return ahl_binding_t::instance().load_config(s, o);
            },
            .handle = nullptr,
            .actions = nullptr
        },
        {.key = nullptr, .uid = nullptr, .info = nullptr, .loadCB = nullptr, .handle = nullptr, .actions = nullptr}
    };

    CtlLoadSections(handle_, controller_config, controller_sections);

    return 0;
}

int ahl_binding_t::load_config(CtlSectionT* section, json_object* o)
{
    config_ << o;
    
    // Add corresponding verbs
    for(const auto& r : config_.roles())
    {
        AFB_DYNAPI_NOTICE(handle_, "New audio role: %s", r.name().c_str());

        if (afb_dynapi_add_verb(
            handle_,
            r.name().c_str(),
            r.description().c_str(),
            [](afb_request* r) { ahl_binding_t::instance().audiorole(r); },
            nullptr,
            nullptr,
            AFB_SESSION_NONE_V2))
        {
            std::stringstream ss;
            ss << "Failed to add '" << r.name() << "' verb to the API.";
            throw std::runtime_error(ss.str());
        }
    }
    
    return 0;
}

void ahl_binding_t::audiorole(afb_request* req)
{
    std::string verb = afb_request_get_verb(req);
    const auto& roles = config_.roles();
    auto r = std::find_if(roles.cbegin(), roles.cend(), [&verb](const role_t& r) { return r.name() == verb; });
    
    if (r == roles.cend())
    {
        afb_request_fail(req, "The requested role doesn't exist!", nullptr);
        return;
    }
    
    json_object* query = json_object_get(afb_request_json(req));
    
    std::string action;
    jcast(action, query, "action");
    std::transform(action.begin(), action.end(), action.begin(), ::tolower);
    
    auto a = actions_.find(action);
    if (a == actions_.end())
    {
        afb_request_fail(req, "The requested action doesn't exist!", nullptr);
        return;
    }
    a->second(req, verb, r->stream(), query);
}

void ahl_binding_t::get_roles(afb_request* req)
{
    json_object* result = json_object_new_array();
    for(const auto& r : config_.roles())
        json_object_array_add(result, json_object_new_string(r.name().c_str()));
    afb_request_success(req, result, nullptr);
}

void ahl_binding_t::volume(afb_request* req, std::string role, std::string stream, json_object* arg)
{
    json_object* value = json_object_object_get(arg, "value");
    json_object_get(value);
    
    if (!value)
    {
        afb_request_fail(req, "No value given!", nullptr);
        return;
    }
    
    json_object* a = json_object_new_object();
    json_object_object_add(a, "volume", value);
    AFB_DYNAPI_DEBUG(handle_, "Call the HAL with the following argument: %s", json_object_to_json_string(a));
    
    afb_dynapi_call(
        handle_,
        config_.hals()[0].c_str(), // BUG: What to do if multiple hals ?
        stream.c_str(),
        a,
        [](void* closure, int status, json_object* result, afb_dynapi* handle)
        {
            AFB_DYNAPI_DEBUG(handle, "Got the following answer: %s", json_object_to_json_string(result));
            afb_request* r = (afb_request*)closure;
            
            json_object_get(result);
            if (status) afb_request_fail(r, json_object_to_json_string(result), nullptr);
            else afb_request_success(r, result, nullptr);
            afb_request_unref(r);
        },
        afb_request_addref(req));
}

void ahl_binding_t::open(afb_request* req, std::string role, std::string stream, json_object* arg)
{
    for(const auto& r : config_.roles())
    {
        if (r.name() == role)
        {
            if (
                ext::cfind_if(opened_roles_,
                    [&role](const role_t& r){ return r.name() == role;}) != opened_roles_.end()
            )
            {
                afb_request_fail(req, "This role is already opened!", nullptr);
                return;
            }            
            // Execute policy for current asked role
            policy_open(req, r);
            return;
        }
    }
    afb_request_fail(req, "Can't open the specified role!", nullptr);
}

void ahl_binding_t::policy_open(afb_request* req, const role_t& role)
{
    if(role.interrupts().size())
    {
        const interrupt_t& i = role.interrupts()[0];
        /*if (i.type() == "mute")
        {
        }
        else if (i.type() == "continue")
        {
        }
        else if (i.type() == "cancel")
        {
        }
        else */if (i.type() == "ramp")
        {
            for(const auto& r: opened_roles_)
            {
                if (role.priority() > r.priority())
                {
                    // { "ramp" : { "uid" : "ramp-slow", "volume" : 30 } }
                    json_object* arg = json_object_new_object();
                    json_object_object_add(arg, "ramp", i.args());
                    json_object_get(i.args());
                    json_object* result = nullptr;

                    AFB_DYNAPI_NOTICE(handle_, "Call 'smixer'/'%s' '%s", r.stream().c_str(), json_object_to_json_string(arg));

                    if(afb_dynapi_call_sync(handle_, "smixer", r.stream().c_str(), arg, &result))
                    {
                        afb_request_fail(req, "Failed to call 'ramp' action on stream", nullptr);
                        return;
                    }
                    AFB_DYNAPI_NOTICE(handle_, "POLICY: Applying a ramp to '%s' stream because '%s' is opened and have higher priority!", r.stream().c_str(), role.stream().c_str());
                }
            }
        }
        else
        {
            afb_request_fail(req, "Unkown interrupt uid!", nullptr);
            return;
        }
    }

    json_object* result = json_object_new_object();
    json_object_object_add(result, "device_uri", json_object_new_string(role.device_uri().c_str()));
    afb_request_success(req, result, nullptr);
    opened_roles_.push_back(role);
}

void ahl_binding_t::close(afb_request* req, std::string role, std::string stream, json_object* arg)
{
    AFB_DYNAPI_DEBUG(handle_, "Got this arg: %s", json_object_to_json_string(arg));
    auto it = ext::cfind_if(opened_roles_, [&role](const role_t& r) { return r.name() == role; });
    if (it == opened_roles_.cend())
    {
        afb_request_fail(req, "This role is already closed!", nullptr);
        return;
    }
    opened_roles_.erase(it);
    afb_request_success(req, nullptr, "Role closed!");
}

void ahl_binding_t::interrupt(afb_request* req, std::string role, std::string stream, json_object* arg)
{
    afb_request_fail(req, "Not implemented yet!", nullptr);
}