/*
 * This file is part of libplacebo.
 *
 * libplacebo is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * libplacebo is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with libplacebo. If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdlib.h>
#include <shaderc/shaderc.h>

#include "hash.h"
#include "spirv.h"
#include "utils.h"

#define VK_API_VERSION_MAJOR(version) (((uint32_t)(version) >> 22U) & 0x7FU)
#define VK_API_VERSION_MINOR(version) (((uint32_t)(version) >> 12U) & 0x3FFU)

#if defined(PL_HAVE_SHADERC_VK_1_4)
# define SHADERC_VULKAN_MAX shaderc_env_version_vulkan_1_4
#elif defined(PL_HAVE_SHADERC_VK_1_3)
# define SHADERC_VULKAN_MAX shaderc_env_version_vulkan_1_3
#else
# define SHADERC_VULKAN_MAX shaderc_env_version_vulkan_1_2
#endif

const struct spirv_compiler pl_spirv_shaderc;

struct priv {
    shaderc_compiler_t compiler;
};

static void shaderc_destroy(pl_spirv spirv)
{
    struct priv *p = PL_PRIV(spirv);
    shaderc_compiler_release(p->compiler);
    pl_free((void *) spirv);
}

static pl_spirv shaderc_create(pl_log log, struct pl_spirv_version spirv_ver)
{
    struct pl_spirv_t *spirv = pl_alloc_obj(NULL, spirv, struct priv);
    *spirv = (struct pl_spirv_t) {
        .signature = pl_str0_hash(pl_spirv_shaderc.name),
        .impl      = &pl_spirv_shaderc,
        .version   = spirv_ver,
        .log       = log,
    };

    struct priv *p = PL_PRIV(spirv);
    p->compiler = shaderc_compiler_initialize();
    if (!p->compiler)
        goto error;

    unsigned int ver = 0, rev = 0;
    shaderc_get_spv_version(&ver, &rev);
    PL_INFO(spirv, "shaderc SPIR-V version %u.%u rev %u",
            ver >> 16, (ver >> 8) & 0xff, rev);

    // Clamp to supported version by shaderc
    if (ver < spirv->version.spv_version) {
        PL_WARN(spirv, "SPIR-V %u.%u is not supported by the current"
                       " version of shaderc. Falling back to %u.%u!",
                spirv->version.spv_version >> 16, (spirv->version.spv_version >> 8) & 0xff,
                ver >> 16, (ver >> 8) & 0xff);
        spirv->version.spv_version = ver;
        spirv->version.env_version = pl_spirv_version_to_vulkan(ver);
    }

    if (SHADERC_VULKAN_MAX < spirv->version.env_version) {
        PL_WARN(spirv, "Vulkan %u.%u is not supported by the current"
                       " version of shaderc. Falling back to %u.%u!",
                       VK_API_VERSION_MAJOR(spirv->version.env_version),
                       VK_API_VERSION_MINOR(spirv->version.env_version),
                       VK_API_VERSION_MAJOR(SHADERC_VULKAN_MAX),
                       VK_API_VERSION_MINOR(SHADERC_VULKAN_MAX));
        // The SPIR-V version has already been clamped above.
        // In practice, this only occurs for Vulkan 1.3 and 1.4,
        // where the SPIR-V version is the same (1.6).
        spirv->version.env_version = SHADERC_VULKAN_MAX;
    }

    pl_hash_merge(&spirv->signature, (uint64_t) spirv->version.spv_version << 32 |
                                                spirv->version.env_version);
    pl_hash_merge(&spirv->signature, (uint64_t) ver << 32 | rev);
    return spirv;

error:
    shaderc_destroy(spirv);
    return NULL;
}

static pl_str shaderc_compile(pl_spirv spirv, void *alloc,
                              struct pl_glsl_version glsl_ver,
                              enum glsl_shader_stage stage,
                              const char *shader)
{
    struct priv *p = PL_PRIV(spirv);
    const size_t len = strlen(shader);

    shaderc_compile_options_t opts = shaderc_compile_options_initialize();
    if (!opts)
        return (pl_str) {0};

    shaderc_compile_options_set_optimization_level(opts,
            shaderc_optimization_level_performance);
    shaderc_compile_options_set_target_spirv(opts, spirv->version.spv_version);
    shaderc_compile_options_set_target_env(opts, shaderc_target_env_vulkan,
                                                 spirv->version.env_version);

    for (int i = 0; i < 3; i++) {
        shaderc_compile_options_set_limit(opts,
                shaderc_limit_max_compute_work_group_size_x + i,
                glsl_ver.max_group_size[i]);
    }

    shaderc_compile_options_set_limit(opts,
            shaderc_limit_min_program_texel_offset,
            glsl_ver.min_gather_offset);
    shaderc_compile_options_set_limit(opts,
            shaderc_limit_max_program_texel_offset,
            glsl_ver.max_gather_offset);

    static const shaderc_shader_kind kinds[] = {
        [GLSL_SHADER_VERTEX]   = shaderc_glsl_vertex_shader,
        [GLSL_SHADER_FRAGMENT] = shaderc_glsl_fragment_shader,
        [GLSL_SHADER_COMPUTE]  = shaderc_glsl_compute_shader,
    };

    static const char * const file_name = "input";
    static const char * const entry_point = "main";

    shaderc_compilation_result_t res;
    res = shaderc_compile_into_spv(p->compiler, shader, len, kinds[stage],
                                   file_name, entry_point, opts);

    int errs = shaderc_result_get_num_errors(res),
        warn = shaderc_result_get_num_warnings(res);

    enum pl_log_level lev = errs ? PL_LOG_ERR : warn ? PL_LOG_INFO : PL_LOG_DEBUG;

    int s = shaderc_result_get_compilation_status(res);
    bool success = s == shaderc_compilation_status_success;
    if (!success)
        lev = PL_LOG_ERR;

    const char *msg = shaderc_result_get_error_message(res);
    if (msg[0])
        PL_MSG(spirv, lev, "shaderc output:\n%s", msg);

    static const char *results[] = {
        [shaderc_compilation_status_success]            = "success",
        [shaderc_compilation_status_invalid_stage]      = "invalid stage",
        [shaderc_compilation_status_compilation_error]  = "error",
        [shaderc_compilation_status_internal_error]     = "internal error",
        [shaderc_compilation_status_null_result_object] = "no result",
        [shaderc_compilation_status_invalid_assembly]   = "invalid assembly",
    };

    const char *status = s < PL_ARRAY_SIZE(results) ? results[s] : "unknown";
    PL_MSG(spirv, lev, "shaderc compile status '%s' (%d errors, %d warnings)",
           status, errs, warn);

    pl_str ret = {0};
    if (success) {
        void *bytes = (void *) shaderc_result_get_bytes(res);
        pl_assert(bytes);
        ret.len = shaderc_result_get_length(res);
        ret.buf = pl_memdup(alloc, bytes, ret.len);

        if (pl_msg_test(spirv->log, PL_LOG_TRACE)) {
            shaderc_compilation_result_t dis;
            dis = shaderc_compile_into_spv_assembly(p->compiler, shader, len,
                                                    kinds[stage], file_name,
                                                    entry_point, opts);
            PL_TRACE(spirv, "Generated SPIR-V:\n%.*s",
                     (int) shaderc_result_get_length(dis),
                     shaderc_result_get_bytes(dis));
            shaderc_result_release(dis);
        }
    }

    shaderc_result_release(res);
    shaderc_compile_options_release(opts);
    return ret;
}

const struct spirv_compiler pl_spirv_shaderc = {
    .name       = "shaderc",
    .destroy    = shaderc_destroy,
    .create     = shaderc_create,
    .compile    = shaderc_compile,
};
