/*
 * Copyright (c) 2022 Mohamed Khaled <Mohamed_Khaled_Kamal@outlook.com>
 *
 * This file is part of FFmpeg.
 *
 * FFmpeg 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.
 *
 * FFmpeg 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 FFmpeg; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include <float.h>
#include <stdio.h>

#include "libavutil/common.h"
#include "libavutil/hwcontext.h"
#include "libavutil/hwcontext_cuda_internal.h"
#include "libavutil/cuda_check.h"
#include "libavutil/internal.h"
#include "libavutil/opt.h"
#include "libavutil/pixdesc.h"

#include "avfilter.h"
#include "filters.h"
#include "cuda/load_helper.h"
#include "video.h"

static const enum AVPixelFormat supported_formats[] = {
    AV_PIX_FMT_YUV420P,
    AV_PIX_FMT_NV12,
    AV_PIX_FMT_YUVA420P
};

#define DIV_UP(a, b) (((a) + (b)-1) / (b))
#define BLOCKX 32
#define BLOCKY 16
#define CHECK_CU(x) FF_CUDA_CHECK_DL(ctx, s->hwctx->internal->cuda_dl, x)

typedef struct ChromakeyCUDAContext {
    const AVClass *class;

    AVCUDADeviceContext *hwctx;

    enum AVPixelFormat in_fmt, out_fmt;
    const AVPixFmtDescriptor *in_desc, *out_desc;
    int in_planes, out_planes;
    int in_plane_depths[4];
    int in_plane_channels[4];

    uint8_t chromakey_rgba[4];
    uint16_t chromakey_uv[2];
    int is_yuv;
    float similarity;
    float blend;

    CUcontext cu_ctx;
    CUmodule cu_module;
    CUfunction cu_func;
    CUfunction cu_func_uv;
    CUstream cu_stream;
} ChromakeyCUDAContext;

static av_cold void cudachromakey_uninit(AVFilterContext *ctx)
{
    ChromakeyCUDAContext *s = ctx->priv;

    if (s->hwctx && s->cu_module)
    {
        CudaFunctions *cu = s->hwctx->internal->cuda_dl;
        CUcontext context;

        CHECK_CU(cu->cuCtxPushCurrent(s->hwctx->cuda_ctx));
        CHECK_CU(cu->cuModuleUnload(s->cu_module));
        s->cu_module = NULL;
        CHECK_CU(cu->cuCtxPopCurrent(&context));
    }
}

static av_cold int init_hwframe_ctx(AVFilterContext *ctx, AVBufferRef *device_ctx, int width, int height)
{
    FilterLink *outl = ff_filter_link(ctx->outputs[0]);
    ChromakeyCUDAContext *s = ctx->priv;
    AVBufferRef *out_ref = NULL;
    AVHWFramesContext *out_ctx;
    int ret;

    out_ref = av_hwframe_ctx_alloc(device_ctx);
    if (!out_ref)
        return AVERROR(ENOMEM);
    out_ctx = (AVHWFramesContext *)out_ref->data;

    out_ctx->format     = AV_PIX_FMT_CUDA;
    out_ctx->sw_format  = s->out_fmt;
    out_ctx->width      = width;
    out_ctx->height     = height;

    ret = av_hwframe_ctx_init(out_ref);
    if (ret < 0)
        goto fail;

    outl->hw_frames_ctx = out_ref;

    return 0;
fail:
    av_buffer_unref(&out_ref);
    return ret;
}

static int format_is_supported(enum AVPixelFormat fmt)
{
    int i;

    for (i = 0; i < FF_ARRAY_ELEMS(supported_formats); i++)
        if (supported_formats[i] == fmt)
            return 1;
    return 0;
}

static av_cold void set_format_info(AVFilterContext *ctx, enum AVPixelFormat in_format, enum AVPixelFormat out_format)
{
    ChromakeyCUDAContext *s = ctx->priv;
    int i, p, d;

    s->in_fmt       = in_format;
    s->out_fmt      = out_format;

    s->in_desc      = av_pix_fmt_desc_get(s->in_fmt);
    s->out_desc     = av_pix_fmt_desc_get(s->out_fmt);
    s->in_planes    = av_pix_fmt_count_planes(s->in_fmt);
    s->out_planes   = av_pix_fmt_count_planes(s->out_fmt);

    // find maximum step of each component of each plane
    // For our subset of formats, this should accurately tell us how many channels CUDA needs
    // i.e. 1 for Y plane, 2 for UV plane of NV12, 4 for single plane of RGB0 formats

    for (i = 0; i < s->in_desc->nb_components; i++)
    {
        d = (s->in_desc->comp[i].depth + 7) / 8;
        p = s->in_desc->comp[i].plane;
        s->in_plane_channels[p] = FFMAX(s->in_plane_channels[p], s->in_desc->comp[i].step / d);

        s->in_plane_depths[p]   = s->in_desc->comp[i].depth;
    }
}

static av_cold int init_processing_chain(AVFilterContext *ctx, int width, int height)
{
    FilterLink         *inl = ff_filter_link(ctx->inputs[0]);
    ChromakeyCUDAContext *s = ctx->priv;
    AVHWFramesContext *in_frames_ctx;
    int ret;

    /* check that we have a hw context */
    if (!inl->hw_frames_ctx) {
        av_log(ctx, AV_LOG_ERROR, "No hw context provided on input\n");
        return AVERROR(EINVAL);
    }
    in_frames_ctx = (AVHWFramesContext *)inl->hw_frames_ctx->data;

    if (!format_is_supported(in_frames_ctx->sw_format))
    {
        av_log(ctx, AV_LOG_ERROR, "Unsupported format: %s\n", av_get_pix_fmt_name(in_frames_ctx->sw_format));
        return AVERROR(ENOSYS);
    }

    set_format_info(ctx, in_frames_ctx->sw_format, AV_PIX_FMT_YUVA420P);

    if (in_frames_ctx->sw_format == s->out_fmt) {
        ff_filter_link(ctx->outputs[0])->hw_frames_ctx = av_buffer_ref(inl->hw_frames_ctx);
        if (!ff_filter_link(ctx->outputs[0])->hw_frames_ctx)
            return AVERROR(ENOMEM);
    } else {
        ret = init_hwframe_ctx(ctx, in_frames_ctx->device_ref, width, height);
        if (ret < 0)
            return ret;
    }

    return 0;
}

static av_cold int cudachromakey_load_functions(AVFilterContext *ctx)
{
    ChromakeyCUDAContext *s   = ctx->priv;
    CUcontext context, cuda_ctx = s->hwctx->cuda_ctx;
    CudaFunctions *cu = s->hwctx->internal->cuda_dl;
    int ret;

    extern const unsigned char ff_vf_chromakey_cuda_ptx_data[];
    extern const unsigned int ff_vf_chromakey_cuda_ptx_len;

    ret = CHECK_CU(cu->cuCtxPushCurrent(cuda_ctx));
    if (ret < 0)
        return ret;

    ret = ff_cuda_load_module(ctx, s->hwctx, &s->cu_module,
                              ff_vf_chromakey_cuda_ptx_data, ff_vf_chromakey_cuda_ptx_len);
    if (ret < 0)
        goto fail;

    ret = CHECK_CU(cu->cuModuleGetFunction(&s->cu_func, s->cu_module, "Process_uchar"));
    if (ret < 0)
    {
        av_log(ctx, AV_LOG_FATAL, "Failed loading Process_uchar\n");
        goto fail;
    }

    ret = CHECK_CU(cu->cuModuleGetFunction(&s->cu_func_uv, s->cu_module, "Process_uchar2"));
    if (ret < 0)
    {
        av_log(ctx, AV_LOG_FATAL, "Failed loading Process_uchar2\n");
        goto fail;
    }

fail:
    CHECK_CU(cu->cuCtxPopCurrent(&context));

    return ret;
}

#define FIXNUM(x) lrint((x) * (1 << 10))
#define RGB_TO_U(rgb) (((-FIXNUM(0.16874) * rgb[0] - FIXNUM(0.33126) * rgb[1] + FIXNUM(0.50000) * rgb[2] + (1 << 9) - 1) >> 10) + 128)
#define RGB_TO_V(rgb) (((FIXNUM(0.50000) * rgb[0] - FIXNUM(0.41869) * rgb[1] - FIXNUM(0.08131) * rgb[2] + (1 << 9) - 1) >> 10) + 128)

static av_cold int cudachromakey_config_props(AVFilterLink *outlink)
{
    AVFilterContext *ctx = outlink->src;
    AVFilterLink *inlink = outlink->src->inputs[0];
    FilterLink      *inl = ff_filter_link(inlink);
    ChromakeyCUDAContext *s = ctx->priv;
    AVHWFramesContext *frames_ctx;
    AVCUDADeviceContext *device_hwctx;
    int ret;

    if (s->is_yuv) {
        s->chromakey_uv[0] = s->chromakey_rgba[1];
        s->chromakey_uv[1] = s->chromakey_rgba[2];
    } else {
        s->chromakey_uv[0] = RGB_TO_U(s->chromakey_rgba);
        s->chromakey_uv[1] = RGB_TO_V(s->chromakey_rgba);
    }

    ret = init_processing_chain(ctx, inlink->w, inlink->h);
    if (ret < 0)
        return ret;

    frames_ctx = (AVHWFramesContext *)inl->hw_frames_ctx->data;
    device_hwctx = frames_ctx->device_ctx->hwctx;

    s->hwctx = device_hwctx;
    s->cu_stream = s->hwctx->stream;

    outlink->sample_aspect_ratio = inlink->sample_aspect_ratio;
    outlink->alpha_mode = AVALPHA_MODE_STRAIGHT;

    ret = cudachromakey_load_functions(ctx);
    if (ret < 0)
        return ret;

    return 0;
}

static int call_cuda_kernel(AVFilterContext *ctx, CUfunction func,
                            CUtexObject src_tex[3], AVFrame *out_frame,
                            int width, int height, int pitch,
                            int width_uv, int height_uv, int pitch_uv,
                            float u_key, float v_key, float similarity,
                            float blend)
{
    ChromakeyCUDAContext *s = ctx->priv;
    CudaFunctions *cu = s->hwctx->internal->cuda_dl;

    CUdeviceptr dst_devptr[4] = {
        (CUdeviceptr)out_frame->data[0], (CUdeviceptr)out_frame->data[1],
        (CUdeviceptr)out_frame->data[2], (CUdeviceptr)out_frame->data[3]
    };

    void *args_uchar[] = {
        &src_tex[0], &src_tex[1], &src_tex[2],
        &dst_devptr[0], &dst_devptr[1], &dst_devptr[2], &dst_devptr[3],
        &width, &height, &pitch,
        &width_uv, &height_uv, &pitch_uv,
        &u_key, &v_key, &similarity, &blend
    };

    return CHECK_CU(cu->cuLaunchKernel(func,
                                       DIV_UP(width, BLOCKX), DIV_UP(height, BLOCKY), 1,
                                       BLOCKX, BLOCKY, 1, 0, s->cu_stream, args_uchar, NULL));
}

static int cudachromakey_process_internal(AVFilterContext *ctx,
                                          AVFrame *out, AVFrame *in)
{
    ChromakeyCUDAContext *s = ctx->priv;
    CudaFunctions *cu       = s->hwctx->internal->cuda_dl;
    CUcontext context, cuda_ctx = s->hwctx->cuda_ctx;
    int i, ret;

    CUtexObject tex[3] = {0, 0, 0};

    ret = CHECK_CU(cu->cuCtxPushCurrent(cuda_ctx));
    if (ret < 0)
        return ret;

    for (i = 0; i < s->in_planes; i++)
    {
        CUDA_TEXTURE_DESC tex_desc = {
            .filterMode = CU_TR_FILTER_MODE_LINEAR,
            .flags = 0, // CU_TRSF_READ_AS_INTEGER to get raw ints instead of normalized floats from tex2D
        };

        CUDA_RESOURCE_DESC res_desc = {
            .resType = CU_RESOURCE_TYPE_PITCH2D,
            .res.pitch2D.format   = CU_AD_FORMAT_UNSIGNED_INT8,
            .res.pitch2D.numChannels = s->in_plane_channels[i],
            .res.pitch2D.pitchInBytes = in->linesize[i],
            .res.pitch2D.devPtr = (CUdeviceptr)in->data[i],
        };

        if (i == 1 || i == 2)
        {
            res_desc.res.pitch2D.width  = AV_CEIL_RSHIFT(in->width, s->in_desc->log2_chroma_w);
            res_desc.res.pitch2D.height = AV_CEIL_RSHIFT(in->height, s->in_desc->log2_chroma_h);
        }
        else
        {
            res_desc.res.pitch2D.width  = in->width;
            res_desc.res.pitch2D.height = in->height;
        }

        ret = CHECK_CU(cu->cuTexObjectCreate(&tex[i], &res_desc, &tex_desc, NULL));
        if (ret < 0)
            goto exit;
    }

    ret = call_cuda_kernel(ctx, (s->in_plane_channels[1] > 1) ? s->cu_func_uv : s->cu_func,
                           tex, out,
                           out->width, out->height, out->linesize[0],
                           AV_CEIL_RSHIFT(out->width, s->out_desc->log2_chroma_w),
                           AV_CEIL_RSHIFT(out->height, s->out_desc->log2_chroma_h),
                           out->linesize[1],
                           s->chromakey_uv[0], s->chromakey_uv[1],
                           s->similarity, s->blend);
    if (ret < 0)
        goto exit;

exit:
    for (i = 0; i < s->in_planes; i++)
        if (tex[i])
            CHECK_CU(cu->cuTexObjectDestroy(tex[i]));

    CHECK_CU(cu->cuCtxPopCurrent(&context));

    return ret;
}

static int cudachromakey_filter_frame(AVFilterLink *link, AVFrame *in)
{
    AVFilterContext *ctx    = link->dst;
    ChromakeyCUDAContext *s = ctx->priv;
    AVFilterLink *outlink   = ctx->outputs[0];
    CudaFunctions *cu = s->hwctx->internal->cuda_dl;

    AVFrame *out;
    CUcontext context;
    int ret = 0;

    out = ff_get_video_buffer(outlink, outlink->w, outlink->h);
    if (!out) {
        ret = AVERROR(ENOMEM);
        goto fail;
    }

    ret = CHECK_CU(cu->cuCtxPushCurrent(s->hwctx->cuda_ctx));
    if (ret < 0)
        goto fail;

    ret = cudachromakey_process_internal(ctx, out, in);
    if (ret < 0)
        goto fail;

    ret = av_frame_copy_props(out, in);
    if (ret < 0)
        goto fail;
    out->alpha_mode = outlink->alpha_mode;

    ret = CHECK_CU(cu->cuCtxPopCurrent(&context));
    if (ret < 0)
        goto fail;

    av_frame_free(&in);
    return ff_filter_frame(outlink, out);
fail:
    av_frame_free(&in);
    av_frame_free(&out);
    return ret;
}

#define OFFSET(x) offsetof(ChromakeyCUDAContext, x)
#define FLAGS (AV_OPT_FLAG_FILTERING_PARAM | AV_OPT_FLAG_VIDEO_PARAM)
static const AVOption options[] = {
    {"color", "set the chromakey key color", OFFSET(chromakey_rgba), AV_OPT_TYPE_COLOR, {.str = "black"}, 0, 0, FLAGS},
    {"similarity", "set the chromakey similarity value", OFFSET(similarity), AV_OPT_TYPE_FLOAT, {.dbl = 0.01}, 0.01, 1.0, FLAGS},
    {"blend", "set the chromakey key blend value", OFFSET(blend), AV_OPT_TYPE_FLOAT, {.dbl = 0.0}, 0.0, 1.0, FLAGS},
    {"yuv", "color parameter is in yuv instead of rgb", OFFSET(is_yuv), AV_OPT_TYPE_BOOL, {.i64 = 0}, 0, 1, FLAGS},
    {NULL},
};

static const AVClass cudachromakey_class = {
    .class_name = "cudachromakey",
    .item_name = av_default_item_name,
    .option = options,
    .version = LIBAVUTIL_VERSION_INT,
};

static const AVFilterPad cudachromakey_inputs[] = {
    {
        .name = "default",
        .type = AVMEDIA_TYPE_VIDEO,
        .filter_frame = cudachromakey_filter_frame,
    },
};

static const AVFilterPad cudachromakey_outputs[] = {
    {
        .name = "default",
        .type = AVMEDIA_TYPE_VIDEO,
        .config_props = cudachromakey_config_props,
    },
};

const FFFilter ff_vf_chromakey_cuda = {
    .p.name        = "chromakey_cuda",
    .p.description = NULL_IF_CONFIG_SMALL("GPU accelerated chromakey filter"),

    .p.priv_class  = &cudachromakey_class,
    .uninit = cudachromakey_uninit,
    .priv_size = sizeof(ChromakeyCUDAContext),

    FILTER_INPUTS(cudachromakey_inputs),
    FILTER_OUTPUTS(cudachromakey_outputs),

    FILTER_SINGLE_PIXFMT(AV_PIX_FMT_CUDA),

    .flags_internal = FF_FILTER_FLAG_HWFRAME_AWARE,
};
