/**
 * @file   video_decompress/cineform.cpp
 * @author Martin Piatka     <piatka@cesnet.cz>
 */
/*
 * Copyright (c) 2019-2023 CESNET, z. s. p. o.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, is permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of CESNET nor the names of its contributors may be
 *    used to endorse or promote products derived from this software without
 *    specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHORS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING,
 * BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
 * EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


#ifdef HAVE_CONFIG_H
#include "config.h"
#include "config_unix.h"
#include "config_win32.h"
#endif // HAVE_CONFIG_H

#include <memory>
#include <vector>

#include "debug.h"
#include "host.h"
#include "lib_common.h"
#include "tv.h"
#include "video.h"
#include "video_decompress.h"
#include "utils/macros.h" // to_fourcc

#include "CFHDTypes.h"
#include "CFHDDecoder.h"

#define MOD_NAME "[cineform] "

struct state_cineform_decompress {
        int pitch = 0;
        int rshift = 0;
        int gshift = 0;
        int bshift = 0;
        video_desc desc = {};
        codec_t in_codec = VIDEO_CODEC_NONE;
        codec_t out_codec = VIDEO_CODEC_NONE;
        CFHD_PixelFormat decode_codec = CFHD_PIXEL_FORMAT_UNKNOWN;
        int decode_linesize = 0;

        using convert_fun_t = void (*)(unsigned char *dst_buffer,
                        unsigned char *src_buffer,
                        int width, int height, int pitch);
        convert_fun_t convert = nullptr;

        std::vector<unsigned char> conv_buf;

        bool prepared_to_decode = false;

        CFHD_DecoderRef decoderRef = nullptr;
        CFHD_MetadataRef metadataRef = nullptr;
};

static void *cineform_decompress_init(){
        auto s = std::make_unique<state_cineform_decompress>();

        CFHD_Error status;
        status = CFHD_OpenDecoder(&s->decoderRef, nullptr);
        if(status != CFHD_ERROR_OKAY){
                log_msg(LOG_LEVEL_ERROR, MOD_NAME "Failed to open decoder\n");
                return nullptr;
        }
        status = CFHD_OpenMetadata(&s->metadataRef);
        if(status != CFHD_ERROR_OKAY){
                log_msg(LOG_LEVEL_ERROR, MOD_NAME "Failed to open metadata\n");
                CFHD_CloseDecoder(s->decoderRef);
                return nullptr;
        }

        return s.release();
}

static void cineform_decompress_done(void *state)
{
        auto s = static_cast<struct state_cineform_decompress *>(state);

        CFHD_CloseDecoder(s->decoderRef);
        CFHD_CloseMetadata(s->metadataRef);
        delete s;
}

static void rg48_to_r12l(unsigned char *dst_buffer,
                unsigned char *src_buffer,
                int width, int height, int pitch)
{
        int src_pitch = vc_get_linesize(width, RG48);
        int dst_len = vc_get_linesize(width, R12L);
        decoder_t vc_copylineRG48toR12L = get_decoder_from_to(RG48, R12L);

        for(int i = 0; i < height; i++){
                vc_copylineRG48toR12L(dst_buffer, src_buffer, dst_len, 0, 0, 0);
                src_buffer += src_pitch;
                dst_buffer += pitch;
        }
}

static void abgr_to_rgba(unsigned char *dst_buffer,
                unsigned char *src_buffer,
                int width, int height, int pitch)
{
        int linesize = vc_get_linesize(width, RGBA);

        for(int i = 0; i < height; i++){
                vc_copylineRGBA(dst_buffer, src_buffer, linesize, 16, 8, 0);
                src_buffer += linesize;
                dst_buffer += pitch;
        }
}

static void bgr_to_rgb_invert(unsigned char *dst_buffer,
                unsigned char *src_buffer,
                int width, int height, int pitch)
{
        int linesize = vc_get_linesize(width, RGB);
        src_buffer += linesize * (height - 1);
        decoder_t vc_copylineBGRtoRGB = get_decoder_from_to(BGR, RGB);

        for(int i = 0; i < height; i++){
                vc_copylineBGRtoRGB(dst_buffer, src_buffer, linesize, 0, 0, 0);
                src_buffer -= linesize;
                dst_buffer += pitch;
        }
}

static const struct {
        codec_t ug_codec;
        CFHD_PixelFormat cfhd_pixfmt;
        void (*convert)(unsigned char *dst_buffer,
                        unsigned char *src_buffer,
                        int width, int height, int pitch);
} decode_codecs[] = {
        {R12L, CFHD_PIXEL_FORMAT_RG48, rg48_to_r12l},
        {RG48, CFHD_PIXEL_FORMAT_RG48, nullptr},
        {UYVY, CFHD_PIXEL_FORMAT_2VUY, nullptr},
        {R10k, CFHD_PIXEL_FORMAT_DPX0, nullptr},
        {v210, CFHD_PIXEL_FORMAT_V210, nullptr},
        {RGB, CFHD_PIXEL_FORMAT_RG24, bgr_to_rgb_invert},
        {RGBA, CFHD_PIXEL_FORMAT_BGRa, abgr_to_rgba},
};

static int cineform_decompress_reconfigure(void *state, struct video_desc desc,
                int rshift, int gshift, int bshift, int pitch, codec_t out_codec)
{
        auto s = static_cast<struct state_cineform_decompress *>(state);

        s->pitch = pitch;
        s->rshift = rshift;
        s->gshift = gshift;
        s->bshift = bshift;
        s->in_codec = desc.color_spec;
        s->out_codec = out_codec;
        s->desc = desc;
        s->prepared_to_decode = false;

        if(s->out_codec == VIDEO_CODEC_NONE){
                log_msg(LOG_LEVEL_DEBUG, MOD_NAME "Will probe for internal format.\n");
                return true;
        }
        for(const auto& i : decode_codecs){
                if(i.ug_codec == s->out_codec){
                        s->decode_codec = i.cfhd_pixfmt;
                        s->convert = i.convert;
                        CFHD_GetImagePitch(desc.width, i.cfhd_pixfmt, &s->decode_linesize);
                        if(i.ug_codec == R12L){
                                log_msg(LOG_LEVEL_NOTICE, MOD_NAME "Decoding to 12-bit RGB.\n");
                        }
                        return true;
                }
        }
        return false;
}

static bool prepare(struct state_cineform_decompress *s,
                unsigned char *src,
                unsigned int src_len)
{
        if(s->prepared_to_decode){
                return true;
        }

        CFHD_Error status;

        int actualWidth;
        int actualHeight;
        CFHD_PixelFormat actualFormat;
        status = CFHD_PrepareToDecode(s->decoderRef,
                        s->desc.width,
                        s->desc.height,
                        s->decode_codec,
                        CFHD_DECODED_RESOLUTION_FULL,
                        CFHD_DECODING_FLAGS_NONE,
                        src,
                        src_len,
                        &actualWidth,
                        &actualHeight,
                        &actualFormat
                        );
        assert(actualWidth == (int) s->desc.width);
        assert(actualHeight == (int) s->desc.height);
        assert(actualFormat == s->decode_codec);
        if(s->convert){
                int actualPitch;
                CFHD_GetImagePitch(actualWidth, actualFormat, &actualPitch);
                assert(actualPitch == s->decode_linesize);
                s->conv_buf.resize(s->desc.height * s->decode_linesize);
        } else {
                s->conv_buf.clear();
        }
        if(status != CFHD_ERROR_OKAY){
                log_msg(LOG_LEVEL_ERROR, MOD_NAME "Failed to prepare for decoding\n");
                return false;
        }

        s->prepared_to_decode = true;
        return true;
}

static decompress_status probe_internal_cineform(struct state_cineform_decompress *s,
                                        unsigned char *src,
                                        unsigned src_len,
                                        struct pixfmt_desc *internal_prop)
{
        CFHD_Error status;
        CFHD_PixelFormat fmt_list[64];
        int count = 0;

        status = CFHD_GetOutputFormats(s->decoderRef,
                                       src,
                                       src_len,
                                       fmt_list,
                                       std::size(fmt_list),
                                       &count);
        log_msg(LOG_LEVEL_DEBUG, MOD_NAME "probing...\n");

        if(status != CFHD_ERROR_OKAY){
                log_msg(LOG_LEVEL_ERROR, MOD_NAME "probe failed\n");
                return DECODER_NO_FRAME;
        }

        for(int i = 0; i < count; i++){
                for(const auto &codec : decode_codecs){
                        if(codec.cfhd_pixfmt == fmt_list[i]){
                                *internal_prop = get_pixfmt_desc(codec.ug_codec);
                                return DECODER_GOT_CODEC;
                        }
                }
        }

        //Unknown internal format. This should never happen since cineform can
        //decode any internal codec to CFHD_PIXEL_FORMAT_YUY2.
        //Here we just select UYVY and hope for the best.
        *internal_prop = get_pixfmt_desc(UYVY);
        return DECODER_GOT_CODEC;
}

static void write_fcc(char *out, int pixelformat){
        out[4] = '\0';

        for(int i = 0; i < 4; i++){
                out[i] = pixelformat & 0xff;
                pixelformat >>= 8;
        }
}

static decompress_status probe_internal(struct state_cineform_decompress *s,
                                        unsigned char *src,
                                        unsigned src_len,
                                        struct pixfmt_desc *internal_prop)
{
        CFHD_Error status;

        status = CFHD_InitSampleMetadata(s->metadataRef,
                                         METADATATYPE_ORIGINAL,
                                         src,
                                         src_len);
        if(status != CFHD_ERROR_OKAY){
                log_msg(LOG_LEVEL_ERROR, MOD_NAME "InitSampleMetadata failed\n");
                return DECODER_NO_FRAME;
        }

        CFHD_MetadataTag tag;
        CFHD_MetadataType type;
        void *data;
        CFHD_MetadataSize size;
        char fcc[5];
        while((status = CFHD_ReadMetadata(s->metadataRef, &tag, &type, &data, &size)) == CFHD_ERROR_OKAY){
                write_fcc(fcc, tag);
                log_msg(LOG_LEVEL_DEBUG, MOD_NAME "Metadata found. tag = %s \n", fcc);
        }

        status = CFHD_FindMetadata(s->metadataRef,
                                   to_fourcc('U', 'G', 'P', 'F'),
                                   &type,
                                   &data,
                                   &size);
        if(status != CFHD_ERROR_OKAY || type != METADATATYPE_UINT32){
                log_msg(LOG_LEVEL_ERROR, MOD_NAME "UGPF metadata not found or wrong type, "
                                         "falling back to cineform internal format detection.\n");
        } else {
                codec_t pf = *static_cast<codec_t *>(data);
                *internal_prop = get_pixfmt_desc(pf);
                log_msg(LOG_LEVEL_NOTICE, MOD_NAME "Codec determined from metadata: %s \n", get_codec_name(pf));
                return DECODER_GOT_CODEC;
        }

        return probe_internal_cineform(s, src, src_len, internal_prop);
}

static decompress_status cineform_decompress(void *state, unsigned char *dst, unsigned char *src,
                unsigned int src_len, int frame_seq, struct video_frame_callbacks *callbacks,
                struct pixfmt_desc *internal_prop)
{
        UNUSED(frame_seq);
        UNUSED(callbacks);
        auto s = static_cast<struct state_cineform_decompress *>(state);
        decompress_status res = DECODER_NO_FRAME;

        CFHD_Error status;

        if(s->out_codec == VIDEO_CODEC_NONE){
                return probe_internal(s, src, src_len, internal_prop);
        }

        if(!prepare(s, src, src_len)){
                return res;
        }

        unsigned char *decode_dst = s->convert ? s->conv_buf.data() : dst;
        int pitch = s->convert ? s->decode_linesize : s->pitch;

        status = CFHD_DecodeSample(s->decoderRef,
                        src,
                        src_len,
                        decode_dst,
                        pitch);

        if(status == CFHD_ERROR_OKAY){
                if(s->convert){
                        s->convert(dst, decode_dst, s->desc.width, s->desc.height, s->pitch);
                }
                res = DECODER_GOT_FRAME;
        } else {
                log_msg(LOG_LEVEL_ERROR, MOD_NAME "Failed to decode %i\n", status);
        }

        return res;
}

static int cineform_decompress_get_property(void *state, int property, void *val, size_t *len)
{
        auto s = static_cast<struct state_cineform_decompress *>(state);
        UNUSED(s);
        int ret = false;

        switch(property) {
                case DECOMPRESS_PROPERTY_ACCEPTS_CORRUPTED_FRAME:
                        if(*len >= sizeof(int)) {
#ifdef CINEFORM_ACCEPT_CORRUPTED
                                *(int *) val = true;
#else
                                *(int *) val = false;
#endif
                                *len = sizeof(int);
                                ret = true;
                        }
                        break;
                default:
                        ret = false;
        }

        return ret;
}

static int cineform_decompress_get_priority(codec_t compression, struct pixfmt_desc internal, codec_t ugc) {
        UNUSED(internal);
        if (compression != CFHD) {
                return -1;
        }
        switch (ugc) {
                case VIDEO_CODEC_NONE: // probe
                        return 50;
                case UYVY:
                case RGBA:
                case R10k:
                case R12L:
                case RG48:
                case v210:
                        break;
                default:
                        return -1;
        }
        return VDEC_PRIO_PREFERRED;
}

static const struct video_decompress_info cineform_info = {
        cineform_decompress_init,
        cineform_decompress_reconfigure,
        cineform_decompress,
        cineform_decompress_get_property,
        cineform_decompress_done,
        cineform_decompress_get_priority,
};

REGISTER_MODULE(cineform, &cineform_info, LIBRARY_CLASS_VIDEO_DECOMPRESS, VIDEO_DECOMPRESS_ABI_VERSION);
