/*
 * cl_kernel.cpp - CL kernel
 *
 *  Copyright (c) 2015 Intel Corporation
 *
 * 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.
 *
 * Author: Wind Yuan <feng.yuan@intel.com>
 */

#include "cl_kernel.h"
#include "cl_context.h"
#include "cl_device.h"
#include "file_handle.h"

#include <sys/stat.h>

#define ENABLE_DEBUG_KERNEL 0

#define XCAM_CL_KERNEL_DEFAULT_LOCAL_WORK_SIZE 0

namespace XCam {

CLKernel::KernelMap CLKernel::_kernel_map;
Mutex CLKernel::_kernel_map_mutex;

static const char*
default_cache_path () {
    static std::string path = "/tmp";
    const char *home_dir = std::getenv ("HOME");
    if (home_dir)
        path.assign (home_dir, strlen (home_dir));

    path += "/.xcam";

    return path.c_str ();
}

const char* CLKernel::_kernel_cache_path = default_cache_path ();

CLKernel::CLKernel (const SmartPtr<CLContext> &context, const char *name)
    : _name (NULL)
    , _kernel_id (NULL)
    , _context (context)
{
    XCAM_ASSERT (context.ptr ());
    //XCAM_ASSERT (name);

    if (name)
        _name = strndup (name, XCAM_MAX_STR_SIZE);

    set_default_work_size ();

    XCAM_OBJ_PROFILING_INIT;
}

CLKernel::~CLKernel ()
{
    destroy ();
    if (_name)
        xcam_free (_name);
}

void
CLKernel::destroy ()
{
    if (!_parent_kernel.ptr ())
        _context->destroy_kernel_id (_kernel_id);
}

static void
get_string_key_id (const char *str, uint32_t len, uint8_t key_id[8])
{
    uint32_t key[2];
    uint32_t *ptr = (uint32_t*)(str);
    uint32_t aligned_len = 0;
    uint32_t i = 0;

    xcam_mem_clear (key);
    if (!len)
        len = strlen (str);
    aligned_len = XCAM_ALIGN_DOWN (len, 8);

    for (i = 0; i < aligned_len / 8; ++i) {
        key[0] ^= ptr[0];
        key[1] ^= ptr[1];
        ptr += 2;
    }
    memcpy (key_id, key, 8);
    len -= aligned_len;
    str += aligned_len;
    for (i = 0; i < len; ++i) {
        key_id[i] ^= (uint8_t)str[i];
    }
}

XCamReturn
CLKernel::build_kernel (const XCamKernelInfo& info, const char* options)
{
    KernelMap::iterator i_kernel;
    SmartPtr<CLKernel> single_kernel;
    char key_str[1024];
    uint8_t body_key[8];
    std::string key;
    XCamReturn ret = XCAM_RETURN_NO_ERROR;

    XCAM_FAIL_RETURN (ERROR, info.kernel_name, XCAM_RETURN_ERROR_PARAM, "build kernel failed since kernel name null");

    xcam_mem_clear (body_key);
    get_string_key_id (info.kernel_body, info.kernel_body_len, body_key);
    snprintf (
        key_str, sizeof(key_str),
        "%s#%02x%02x%02x%02x%02x%02x%02x%02x#%s",
        info.kernel_name,
        body_key[0], body_key[1], body_key[2], body_key[3], body_key[4], body_key[5], body_key[6], body_key[7],
        XCAM_STR(options));
    key = key_str;

    char temp_filename[XCAM_MAX_STR_SIZE] = {0};
    char cache_filename[XCAM_MAX_STR_SIZE] = {0};
    FileHandle temp_file;
    FileHandle cache_file;
    size_t read_cache_size = 0;
    size_t write_cache_size = 0;
    uint8_t *kernel_cache = NULL;
    bool load_cache = false;
    struct timeval ts;

    std::string cache_path = _kernel_cache_path;
    const char *env = std::getenv ("XCAM_CL_KERNEL_CACHE_PATH");
    if (env)
        cache_path.assign (env, strlen (env));

    snprintf (
        cache_filename, XCAM_MAX_STR_SIZE - 1,
        "%s/%s",
        cache_path.c_str (), key_str);

    {
        SmartLock locker (_kernel_map_mutex);

        i_kernel = _kernel_map.find (key);
        if (i_kernel == _kernel_map.end ()) {
            SmartPtr<CLContext>  context = get_context ();
            single_kernel = new CLKernel (context, info.kernel_name);
            XCAM_ASSERT (single_kernel.ptr ());

            if (access (cache_path.c_str (), F_OK) == -1) {
                mkdir (cache_path.c_str (), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
            }

            ret = cache_file.open (cache_filename, "r");
            if (ret == XCAM_RETURN_NO_ERROR) {
                cache_file.get_file_size (read_cache_size);
                if (read_cache_size > 0) {
                    kernel_cache = (uint8_t*) xcam_malloc0 (sizeof (uint8_t) * (read_cache_size + 1));
                    if (NULL != kernel_cache) {
                        cache_file.read_file (kernel_cache, read_cache_size);
                        cache_file.close ();

                        ret = single_kernel->load_from_binary (kernel_cache, read_cache_size);
                        xcam_free (kernel_cache);
                        kernel_cache = NULL;

                        XCAM_FAIL_RETURN (
                            ERROR, ret == XCAM_RETURN_NO_ERROR, ret,
                            "build kernel(%s) from binary failed", key_str);

                        load_cache = true;
                    }
                }
            } else {
                XCAM_LOG_DEBUG ("open kernel cache file to read failed ret(%d)", ret);
            }

            if (load_cache == false) {
                ret = single_kernel->load_from_source (info.kernel_body, strlen (info.kernel_body), &kernel_cache, &write_cache_size, options);
                XCAM_FAIL_RETURN (
                    ERROR, ret == XCAM_RETURN_NO_ERROR, ret,
                    "build kernel(%s) from source failed", key_str);
            }

            _kernel_map.insert (std::make_pair (key, single_kernel));
            //_kernel_map[key] = single_kernel;
        } else {
            single_kernel = i_kernel->second;
        }
    }

    if (load_cache == false && NULL != kernel_cache) {
        gettimeofday (&ts, NULL);
        snprintf (
            temp_filename, XCAM_MAX_STR_SIZE - 1,
            "%s." XCAM_TIMESTAMP_FORMAT,
            cache_filename, XCAM_TIMESTAMP_ARGS (XCAM_TIMEVAL_2_USEC (ts)));

        ret = temp_file.open (temp_filename, "wb");
        if (ret == XCAM_RETURN_NO_ERROR) {
            ret = temp_file.write_file (kernel_cache, write_cache_size);
            temp_file.close ();
            if (ret == XCAM_RETURN_NO_ERROR && write_cache_size > 0) {
                rename (temp_filename, cache_filename);
            } else {
                remove (temp_filename);
            }
        } else {
            XCAM_LOG_ERROR ("open kernel cache file to write failed ret(%d)", ret);
        }
        xcam_free (kernel_cache);
        kernel_cache = NULL;
    }

    XCAM_FAIL_RETURN (
        ERROR, (single_kernel.ptr () && single_kernel->is_valid ()), XCAM_RETURN_ERROR_UNKNOWN,
        "build kernel(%s) failed, unknown error", key_str);

    ret = this->clone (single_kernel);
    XCAM_FAIL_RETURN (
        ERROR, ret == XCAM_RETURN_NO_ERROR, ret,
        "load kernel(%s) from kernel failed", key_str);
    return ret;
}

XCamReturn
CLKernel::load_from_source (
    const char *source, size_t length,
    uint8_t **gen_binary, size_t *binary_size,
    const char *build_option)
{
    cl_kernel new_kernel_id = NULL;

    XCAM_ASSERT (source);
    if (!source) {
        XCAM_LOG_WARNING ("kernel:%s source empty", XCAM_STR (_name));
        return XCAM_RETURN_ERROR_PARAM;
    }

    if (_kernel_id) {
        XCAM_LOG_WARNING ("kernel:%s already build yet", XCAM_STR (_name));
        return XCAM_RETURN_ERROR_PARAM;
    }

    XCAM_ASSERT (_context.ptr ());

    if (length == 0)
        length = strlen (source);

    new_kernel_id =
        _context->generate_kernel_id (
            this,
            (const uint8_t *)source, length,
            CLContext::KERNEL_BUILD_SOURCE,
            gen_binary, binary_size,
            build_option);
    XCAM_FAIL_RETURN(
        WARNING,
        new_kernel_id != NULL,
        XCAM_RETURN_ERROR_CL,
        "cl kernel(%s) load from source failed", XCAM_STR (_name));

    _kernel_id = new_kernel_id;
    return XCAM_RETURN_NO_ERROR;
}

XCamReturn
CLKernel::load_from_binary (const uint8_t *binary, size_t length)
{
    cl_kernel new_kernel_id = NULL;

    XCAM_ASSERT (binary);
    if (!binary || !length) {
        XCAM_LOG_WARNING ("kernel:%s binary empty", XCAM_STR (_name));
        return XCAM_RETURN_ERROR_PARAM;
    }

    if (_kernel_id) {
        XCAM_LOG_WARNING ("kernel:%s already build yet", XCAM_STR (_name));
        return XCAM_RETURN_ERROR_PARAM;
    }

    XCAM_ASSERT (_context.ptr ());

    new_kernel_id =
        _context->generate_kernel_id (
            this,
            binary, length,
            CLContext::KERNEL_BUILD_BINARY,
            NULL, NULL,
            NULL);
    XCAM_FAIL_RETURN(
        WARNING,
        new_kernel_id != NULL,
        XCAM_RETURN_ERROR_CL,
        "cl kernel(%s) load from binary failed", XCAM_STR (_name));

    _kernel_id = new_kernel_id;
    return XCAM_RETURN_NO_ERROR;
}

XCamReturn
CLKernel::clone (SmartPtr<CLKernel> kernel)
{
    XCAM_FAIL_RETURN (
        WARNING,
        kernel.ptr () && kernel->is_valid (),
        XCAM_RETURN_ERROR_CL,
        "cl kernel(%s) load from kernel failed", XCAM_STR (_name));
    _kernel_id = kernel->get_kernel_id ();
    _parent_kernel = kernel;
    if (!_name && kernel->get_kernel_name ()) {
        _name = strndup (kernel->get_kernel_name (), XCAM_MAX_STR_SIZE);
    }
    return XCAM_RETURN_NO_ERROR;
}

XCamReturn
CLKernel::set_arguments (const CLArgList &args, const CLWorkSize &work_size)
{
    XCamReturn ret = XCAM_RETURN_NO_ERROR;
    uint32_t i_count = 0;

    XCAM_FAIL_RETURN (
        ERROR, _arg_list.empty (), XCAM_RETURN_ERROR_PARAM,
        "cl image kernel(%s) arguments was already set, can NOT be set twice", get_kernel_name ());

    for (CLArgList::const_iterator iter = args.begin (); iter != args.end (); ++iter, ++i_count) {
        const SmartPtr<CLArgument> &arg = *iter;
        XCAM_FAIL_RETURN (
            WARNING, arg.ptr (),
            XCAM_RETURN_ERROR_PARAM, "cl image kernel(%s) argc(%d) is NULL", get_kernel_name (), i_count);

        void *adress = NULL;
        uint32_t size = 0;
        arg->get_value (adress, size);
        ret = set_argument (i_count, adress, size);
        XCAM_FAIL_RETURN (
            WARNING, ret == XCAM_RETURN_NO_ERROR,
            ret, "cl image kernel(%s) set argc(%d) failed", get_kernel_name (), i_count);
    }

    ret = set_work_size (work_size);
    XCAM_FAIL_RETURN (
        WARNING, ret == XCAM_RETURN_NO_ERROR, ret,
        "cl image kernel(%s) set worksize(global:%dx%dx%d, local:%dx%dx%d) failed",
        XCAM_STR(get_kernel_name ()),
        (int)work_size.global[0], (int)work_size.global[1], (int)work_size.global[2],
        (int)work_size.local[0], (int)work_size.local[1], (int)work_size.local[2]);

    _arg_list = args;
    return ret;
}

XCamReturn
CLKernel::set_argument (uint32_t arg_i, void *arg_addr, uint32_t arg_size)
{
    cl_int error_code = clSetKernelArg (_kernel_id, arg_i, arg_size, arg_addr);
    if (error_code != CL_SUCCESS) {
        XCAM_LOG_DEBUG ("kernel(%s) set arg_i(%d) failed", _name, arg_i);
        return XCAM_RETURN_ERROR_CL;
    }
    return XCAM_RETURN_NO_ERROR;
}

XCamReturn
CLKernel::set_work_size (const CLWorkSize &work_size)
{
    uint32_t i = 0;
    uint32_t work_group_size = 1;
    const CLDevieInfo &dev_info = CLDevice::instance ()->get_device_info ();

    XCAM_FAIL_RETURN (
        WARNING,
        work_size.dim <= dev_info.max_work_item_dims,
        XCAM_RETURN_ERROR_PARAM,
        "kernel(%s) work dims(%d) greater than device max dims(%d)",
        _name, work_size.dim, dev_info.max_work_item_dims);

    for (i = 0; i < work_size.dim; ++i) {
        work_group_size *= work_size.local [i];

        XCAM_FAIL_RETURN (
            WARNING,
            work_size.local [i] <= dev_info.max_work_item_sizes [i],
            XCAM_RETURN_ERROR_PARAM,
            "kernel(%s) work item(%d) size:%d is greater than device max work item size(%d)",
            _name, i, (uint32_t)work_size.local [i], (uint32_t)dev_info.max_work_item_sizes [i]);
    }

    XCAM_FAIL_RETURN (
        WARNING,
        work_group_size == 0 || work_group_size <= dev_info.max_work_group_size,
        XCAM_RETURN_ERROR_PARAM,
        "kernel(%s) work-group-size:%d is greater than device max work-group-size(%d)",
        _name, work_group_size, (uint32_t)dev_info.max_work_group_size);

    _work_size = work_size;

    return XCAM_RETURN_NO_ERROR;
}

void
CLKernel::set_default_work_size ()
{
    _work_size.dim = XCAM_DEFAULT_IMAGE_DIM;
    for (uint32_t i = 0; i < _work_size.dim; ++i) {
        //_global_work_size [i] = XCAM_CL_KERNEL_DEFAULT_GLOBAL_WORK_SIZE;
        _work_size.local [i] = XCAM_CL_KERNEL_DEFAULT_LOCAL_WORK_SIZE;
    }
}

struct KernelUserData {
    SmartPtr<CLKernel>  kernel;
    SmartPtr<CLEvent>   event;
    CLArgList           arg_list;

    KernelUserData (const SmartPtr<CLKernel> &k, SmartPtr<CLEvent> &e)
        : kernel (k)
        , event (e)
    {}
};

void
CLKernel::event_notify (cl_event event, cl_int status, void* data)
{
    KernelUserData *kernel_data = (KernelUserData *)data;
    XCAM_ASSERT (event == kernel_data->event->get_event_id ());
    XCAM_UNUSED (status);
    XCAM_UNUSED (event);

    delete kernel_data;
}

XCamReturn
CLKernel::execute (
    const SmartPtr<CLKernel> self,
    bool block,
    CLEventList &events,
    SmartPtr<CLEvent> &event_out)
{
    XCAM_ASSERT (self.ptr () == this);
    XCAM_ASSERT (_context.ptr ());
    SmartPtr<CLEvent> kernel_event = event_out;

    if (!block && !kernel_event.ptr ()) {
        kernel_event = new CLEvent;
    }

#if ENABLE_DEBUG_KERNEL
    XCAM_OBJ_PROFILING_START;
#endif

    XCamReturn ret = _context->execute_kernel (self, NULL, events, kernel_event);

    XCAM_FAIL_RETURN (
        ERROR,
        ret == XCAM_RETURN_NO_ERROR,
        ret,
        "kernel(%s) execute failed", XCAM_STR(_name));


    if (block) {
        _context->finish ();
    } else {
        XCAM_ASSERT (kernel_event.ptr () && kernel_event->get_event_id ());
        KernelUserData *user_data = new KernelUserData (self, kernel_event);
        user_data->arg_list.swap (_arg_list);
        ret = _context->set_event_callback (kernel_event, CL_COMPLETE, event_notify, user_data);
        if (ret != XCAM_RETURN_NO_ERROR) {
            XCAM_LOG_WARNING ("kernel(%s) set event callback failed", XCAM_STR (_name));
            _context->finish ();
            delete user_data;
        }
    }
    _arg_list.clear ();

#if ENABLE_DEBUG_KERNEL
    _context->finish ();
    char name[1024];
    snprintf (name, 1024, "%s-%p", XCAM_STR (_name), this);
    XCAM_OBJ_PROFILING_END (name, XCAM_OBJ_DUR_FRAME_NUM);
#endif
    return ret;
}

};
