/*
 * This file is part of mpv.
 *
 * Parts of video mixer creation code:
 * Copyright (C) 2008 NVIDIA (Rajib Mahapatra <rmahapatra@nvidia.com>)
 * Copyright (C) 2009 Uoti Urpala
 *
 * mpv is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * mpv 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with mpv.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <assert.h>

#include "vdpau_mixer.h"

static void free_mixed_frame(void *arg)
{
    struct mp_vdpau_mixer_frame *frame = arg;
    talloc_free(frame);
}

// This creates an image of format IMGFMT_VDPAU with a mp_vdpau_mixer_frame
// struct. Use mp_vdpau_mixed_frame_get() to retrieve the struct and to
// initialize it.
// "base" is used only to set parameters, no image data is referenced.
struct mp_image *mp_vdpau_mixed_frame_create(struct mp_image *base)
{
    mp_assert(base->imgfmt == IMGFMT_VDPAU);

    struct mp_vdpau_mixer_frame *frame =
        talloc_zero(NULL, struct mp_vdpau_mixer_frame);
    for (int n = 0; n < MP_VDP_HISTORY_FRAMES; n++)
        frame->past[n] = frame->future[n] = VDP_INVALID_HANDLE;
    frame->current = VDP_INVALID_HANDLE;
    frame->field = VDP_VIDEO_MIXER_PICTURE_STRUCTURE_FRAME;

    struct mp_image *mpi = mp_image_new_custom_ref(base, frame, free_mixed_frame);
    if (mpi) {
        mpi->planes[2] = (void *)frame;
        mpi->planes[3] = (void *)(uintptr_t)VDP_INVALID_HANDLE;
    }
    return mpi;
}

struct mp_vdpau_mixer_frame *mp_vdpau_mixed_frame_get(struct mp_image *mpi)
{
    if (mpi->imgfmt != IMGFMT_VDPAU)
        return NULL;
    return (void *)mpi->planes[2];
}

struct mp_vdpau_mixer *mp_vdpau_mixer_create(struct mp_vdpau_ctx *vdp_ctx,
                                             struct mp_log *log)
{
    struct mp_vdpau_mixer *mixer = talloc_ptrtype(NULL, mixer);
    *mixer = (struct mp_vdpau_mixer){
        .ctx = vdp_ctx,
        .log = log,
        .video_mixer = VDP_INVALID_HANDLE,
    };
    mp_vdpau_handle_preemption(mixer->ctx, &mixer->preemption_counter);
    return mixer;
}

void mp_vdpau_mixer_destroy(struct mp_vdpau_mixer *mixer)
{
    struct vdp_functions *vdp = &mixer->ctx->vdp;
    VdpStatus vdp_st;
    if (mixer->video_mixer != VDP_INVALID_HANDLE) {
        vdp_st = vdp->video_mixer_destroy(mixer->video_mixer);
        CHECK_VDP_WARNING(mixer, "Error when calling vdp_video_mixer_destroy");
    }
    talloc_free(mixer);
}

static bool opts_equal(const struct mp_vdpau_mixer_opts *a,
                       const struct mp_vdpau_mixer_opts *b)
{
    return a->deint == b->deint && a->chroma_deint == b->chroma_deint &&
           a->pullup == b->pullup && a->hqscaling == b->hqscaling &&
           a->sharpen == b->sharpen && a->denoise == b->denoise;
}

static int set_video_attribute(struct mp_vdpau_mixer *mixer,
                               VdpVideoMixerAttribute attr,
                               const void *value, char *attr_name)
{
    struct vdp_functions *vdp = &mixer->ctx->vdp;
    VdpStatus vdp_st;

    vdp_st = vdp->video_mixer_set_attribute_values(mixer->video_mixer, 1,
                                                   &attr, &value);
    if (vdp_st != VDP_STATUS_OK) {
        MP_ERR(mixer, "Error setting video mixer attribute %s: %s\n", attr_name,
               vdp->get_error_string(vdp_st));
        return -1;
    }
    return 0;
}

#define SET_VIDEO_ATTR(attr_name, attr_type, value) set_video_attribute(mixer, \
                 VDP_VIDEO_MIXER_ATTRIBUTE_ ## attr_name, &(attr_type){value},\
                 # attr_name)
static int create_vdp_mixer(struct mp_vdpau_mixer *mixer,
                            VdpChromaType chroma_type, uint32_t w, uint32_t h)
{
    struct vdp_functions *vdp = &mixer->ctx->vdp;
    VdpDevice vdp_device = mixer->ctx->vdp_device;
    struct mp_vdpau_mixer_opts *opts = &mixer->opts;
#define VDP_NUM_MIXER_PARAMETER 3
#define MAX_NUM_FEATURES 6
    int i;
    VdpStatus vdp_st;

    MP_VERBOSE(mixer, "Recreating vdpau video mixer.\n");

    int feature_count = 0;
    VdpVideoMixerFeature features[MAX_NUM_FEATURES];
    VdpBool feature_enables[MAX_NUM_FEATURES];
    static const VdpVideoMixerParameter parameters[VDP_NUM_MIXER_PARAMETER] = {
        VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_WIDTH,
        VDP_VIDEO_MIXER_PARAMETER_VIDEO_SURFACE_HEIGHT,
        VDP_VIDEO_MIXER_PARAMETER_CHROMA_TYPE,
    };
    const void *const parameter_values[VDP_NUM_MIXER_PARAMETER] = {
        &(uint32_t){w},
        &(uint32_t){h},
        &(VdpChromaType){chroma_type},
    };
    if (opts->deint >= 3)
        features[feature_count++] = VDP_VIDEO_MIXER_FEATURE_DEINTERLACE_TEMPORAL;
    if (opts->deint == 4)
        features[feature_count++] =
            VDP_VIDEO_MIXER_FEATURE_DEINTERLACE_TEMPORAL_SPATIAL;
    if (opts->pullup)
        features[feature_count++] = VDP_VIDEO_MIXER_FEATURE_INVERSE_TELECINE;
    if (opts->denoise)
        features[feature_count++] = VDP_VIDEO_MIXER_FEATURE_NOISE_REDUCTION;
    if (opts->sharpen)
        features[feature_count++] = VDP_VIDEO_MIXER_FEATURE_SHARPNESS;
    if (opts->hqscaling) {
        VdpVideoMixerFeature hqscaling_feature =
            VDP_VIDEO_MIXER_FEATURE_HIGH_QUALITY_SCALING_L1 + opts->hqscaling - 1;
        VdpBool hqscaling_available;
        vdp_st = vdp->video_mixer_query_feature_support(vdp_device,
                                                        hqscaling_feature,
                                                        &hqscaling_available);
        CHECK_VDP_ERROR(mixer, "Error when calling video_mixer_query_feature_support");
        if (hqscaling_available) {
            features[feature_count++] = hqscaling_feature;
        } else {
            MP_ERR(mixer, "Your hardware or VDPAU library does not support "
                   "requested hqscaling.\n");
        }
    }

    vdp_st = vdp->video_mixer_create(vdp_device, feature_count, features,
                                     VDP_NUM_MIXER_PARAMETER,
                                     parameters, parameter_values,
                                     &mixer->video_mixer);
    if (vdp_st != VDP_STATUS_OK)
        mixer->video_mixer = VDP_INVALID_HANDLE;

    CHECK_VDP_ERROR(mixer, "Error when calling vdp_video_mixer_create");

    mixer->initialized = true;
    mixer->current_chroma_type = chroma_type;
    mixer->current_w = w;
    mixer->current_h = h;

    for (i = 0; i < feature_count; i++)
        feature_enables[i] = VDP_TRUE;
    if (feature_count) {
        vdp_st = vdp->video_mixer_set_feature_enables(mixer->video_mixer,
                                                      feature_count, features,
                                                      feature_enables);
        CHECK_VDP_WARNING(mixer, "Error calling vdp_video_mixer_set_feature_enables");
    }
    if (opts->denoise)
        SET_VIDEO_ATTR(NOISE_REDUCTION_LEVEL, float, opts->denoise);
    if (opts->sharpen)
        SET_VIDEO_ATTR(SHARPNESS_LEVEL, float, opts->sharpen);
    if (!opts->chroma_deint)
        SET_VIDEO_ATTR(SKIP_CHROMA_DEINTERLACE, uint8_t, 1);

    struct pl_transform3x3 yuv2rgb;
    VdpCSCMatrix matrix;

    struct mp_csp_params cparams = MP_CSP_PARAMS_DEFAULTS;
    mp_csp_set_image_params(&cparams, &mixer->image_params);
    if (mixer->video_eq)
        mp_csp_equalizer_state_get(mixer->video_eq, &cparams);
    mp_get_csp_matrix(&cparams, &yuv2rgb);

    for (int r = 0; r < 3; r++) {
        for (int c = 0; c < 3; c++)
            matrix[r][c] = yuv2rgb.mat.m[r][c];
        matrix[r][3] = yuv2rgb.c[r];
    }

    set_video_attribute(mixer, VDP_VIDEO_MIXER_ATTRIBUTE_CSC_MATRIX,
                        &matrix, "CSC matrix");

    return 0;
}

// If opts is NULL, use the opts as implied by the video image.
int mp_vdpau_mixer_render(struct mp_vdpau_mixer *mixer,
                          struct mp_vdpau_mixer_opts *opts,
                          VdpOutputSurface output, VdpRect *output_rect,
                          struct mp_image *video, VdpRect *video_rect)
{
    struct vdp_functions *vdp = &mixer->ctx->vdp;
    VdpStatus vdp_st;
    VdpRect fallback_rect = {0, 0, video->w, video->h};

    if (!video_rect)
        video_rect = &fallback_rect;

    int pe = mp_vdpau_handle_preemption(mixer->ctx, &mixer->preemption_counter);
    if (pe < 1) {
        mixer->video_mixer = VDP_INVALID_HANDLE;
        if (pe < 0)
            return -1;
    }

    if (video->imgfmt == IMGFMT_VDPAU_OUTPUT) {
        VdpOutputSurface surface = (uintptr_t)video->planes[3];
        int flags = VDP_OUTPUT_SURFACE_RENDER_ROTATE_0;
        vdp_st = vdp->output_surface_render_output_surface(output,
                                                           output_rect,
                                                           surface,
                                                           video_rect,
                                                           NULL, NULL, flags);
        CHECK_VDP_WARNING(mixer, "Error when calling "
                          "vdp_output_surface_render_output_surface");
        return 0;
    }

    if (video->imgfmt != IMGFMT_VDPAU)
        return -1;

    struct mp_vdpau_mixer_frame *frame = mp_vdpau_mixed_frame_get(video);
    struct mp_vdpau_mixer_frame fallback = {{0}};
    if (!frame) {
        frame = &fallback;
        frame->current = (uintptr_t)video->planes[3];
        for (int n = 0; n < MP_VDP_HISTORY_FRAMES; n++)
            frame->past[n] = frame->future[n] = VDP_INVALID_HANDLE;
        frame->field = VDP_VIDEO_MIXER_PICTURE_STRUCTURE_FRAME;
    }

    if (!opts)
        opts = &frame->opts;

    if (mixer->video_mixer == VDP_INVALID_HANDLE)
        mixer->initialized = false;

    if (mixer->video_eq && mp_csp_equalizer_state_changed(mixer->video_eq))
        mixer->initialized = false;

    VdpChromaType s_chroma_type;
    uint32_t s_w, s_h;

    vdp_st = vdp->video_surface_get_parameters(frame->current, &s_chroma_type,
                                               &s_w, &s_h);
    CHECK_VDP_ERROR(mixer, "Error when calling vdp_video_surface_get_parameters");

    if (!mixer->initialized || !opts_equal(opts, &mixer->opts) ||
        !mp_image_params_static_equal(&video->params, &mixer->image_params) ||
        mixer->current_w != s_w || mixer->current_h != s_h ||
        mixer->current_chroma_type != s_chroma_type)
    {
        mixer->opts = *opts;
        mixer->image_params = video->params;
        if (mixer->video_mixer != VDP_INVALID_HANDLE) {
            vdp_st = vdp->video_mixer_destroy(mixer->video_mixer);
            CHECK_VDP_WARNING(mixer, "Error when calling vdp_video_mixer_destroy");
        }
        mixer->video_mixer = VDP_INVALID_HANDLE;
        mixer->initialized = false;
        if (create_vdp_mixer(mixer, s_chroma_type, s_w, s_h) < 0)
            return -1;
    }

    vdp_st = vdp->video_mixer_render(mixer->video_mixer, VDP_INVALID_HANDLE,
                                     0, frame->field,
                                     MP_VDP_HISTORY_FRAMES, frame->past,
                                     frame->current,
                                     MP_VDP_HISTORY_FRAMES, frame->future,
                                     video_rect,
                                     output, NULL, output_rect,
                                     0, NULL);
    CHECK_VDP_WARNING(mixer, "Error when calling vdp_video_mixer_render");
    return 0;
}
