/* -*- c++ -*- */
/*
 * Copyright 2006,2010,2012 Free Software Foundation, Inc.
 *
 * This file is part of GNU Radio
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 *
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "sink_s_impl.h"
#include <gnuradio/io_signature.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <stdexcept>

namespace gr {
namespace video_sdl {


sink_s::sptr
sink_s::make(double framerate, int width, int height, int dst_width, int dst_height)
{
    return gnuradio::make_block_sptr<sink_s_impl>(
        framerate, width, height, dst_width, dst_height);
}

sink_s_impl::sink_s_impl(
    double framerate, int width, int height, int dst_width, int dst_height)
    : sync_block("video_sdl_sink_s",
                 io_signature::make(1, 3, sizeof(short)),
                 io_signature::make(0, 0, 0)),
      d_chunk_size(width * height),
      d_framerate(framerate),
      d_wanted_frametime_ms(0),
      d_width(width),
      d_height(height),
      d_dst_width(dst_width),
      d_dst_height(dst_height),
      d_current_line(0),
      d_screen(NULL),
      d_image(NULL),
      d_avg_delay(0.0),
      d_wanted_ticks(0)
{
    if (framerate <= 0.0)
        d_wanted_frametime_ms = 0; // Go as fast as possible
    else
        d_wanted_frametime_ms = (int)(1000.0 / framerate);

    if (dst_width < 0)
        d_dst_width = d_width;
    if (dst_height < 0)
        d_dst_height = d_height;

    atexit(SDL_Quit); // check if this is the way to do this
    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        d_logger->error("Couldn't initialize SDL: {:s}; SDL_Init(SDL_INIT_VIDEO) failed",
                        SDL_GetError());
        throw std::runtime_error("video_sdl::sink_s");
    };

    /* accept any depth */
    d_screen = SDL_SetVideoMode(
        dst_width,
        dst_height,
        0,
        SDL_SWSURFACE | SDL_RESIZABLE |
            SDL_ANYFORMAT); // SDL_DOUBLEBUF |SDL_SWSURFACE| SDL_HWSURFACE||SDL_FULLSCREEN

    if (d_screen == NULL) {
        d_logger->error("Unable to set SDL video mode: {:s}; SDL_SetVideoMode() Failed",
                        SDL_GetError());
        throw std::runtime_error("video_sdl::sink_s");
    }
    if (d_image) {
        SDL_FreeYUVOverlay(d_image);
    }

    /* Initialize and create the YUV Overlay used for video out */
    if (!(d_image =
              SDL_CreateYUVOverlay(d_width, d_height, SDL_IYUV_OVERLAY, d_screen))) {
        d_logger->error("Couldn't create a YUV overlay: {:s}", SDL_GetError());
        throw std::runtime_error("video_sdl::sink_s");
    }

    d_debug_logger->info("SDL screen_mode {:d} bits-per-pixel",
                         d_screen->format->BitsPerPixel);
    d_debug_logger->info("SDL overlay_mode {:d}", d_image->format);

    d_chunk_size = std::min(1, 16384 / width); // width*16;
    d_chunk_size = d_chunk_size * width;
    // d_chunk_size = (int) (width);
    set_output_multiple(d_chunk_size);

    /* Set the default playback area */
    d_dst_rect.x = 0;
    d_dst_rect.y = 0;
    d_dst_rect.w = d_dst_width;
    d_dst_rect.h = d_dst_height;
    // clear the surface to grey

    if (SDL_LockYUVOverlay(d_image)) {
        d_logger->error("Couldn't lock a YUV overlay: {:s}", SDL_GetError());
        throw std::runtime_error("video_sdl::sink_s");
    }

    memset(d_image->pixels[0], 128, d_image->pitches[0] * d_height);
    memset(d_image->pixels[1], 128, d_image->pitches[1] * d_height / 2);
    memset(d_image->pixels[2], 128, d_image->pitches[2] * d_height / 2);
    SDL_UnlockYUVOverlay(d_image);
}

sink_s_impl::~sink_s_impl() { SDL_Quit(); }

void sink_s_impl::copy_line_pixel_interleaved(unsigned char* dst_pixels_u,
                                              unsigned char* dst_pixels_v,
                                              const short* src_pixels,
                                              int src_width)
{
    for (int i = 0; i < src_width; i++) {
        dst_pixels_u[i] = (unsigned char)src_pixels[i * 2];
        dst_pixels_v[i] = (unsigned char)src_pixels[i * 2 + 1];
    }
}

void sink_s_impl::copy_line_line_interleaved(unsigned char* dst_pixels_u,
                                             unsigned char* dst_pixels_v,
                                             const short* src_pixels,
                                             int src_width)
{
    for (int i = 0; i < src_width; i++) {
        dst_pixels_u[i] = (unsigned char)src_pixels[i];
        dst_pixels_v[i] = (unsigned char)src_pixels[i + src_width];
    }

    for (int i = src_width; i < src_width * 2; i++) {
        dst_pixels_v[i] = (unsigned char)src_pixels[i];
    }
}

void sink_s_impl::copy_line_single_plane(unsigned char* dst_pixels,
                                         const short* src_pixels,
                                         int src_width)
{
    for (int i = 0; i < src_width; i++) {
        dst_pixels[i] = (unsigned char)src_pixels[i];
    }
}

void sink_s_impl::copy_line_single_plane_dec2(unsigned char* dst_pixels,
                                              const short* src_pixels,
                                              int src_width)
{
    for (int i = 0, j = 0; i < src_width; i += 2, j++) {
        dst_pixels[j] = (unsigned char)src_pixels[i];
    }
}

int sink_s_impl::copy_plane_to_surface(int plane,
                                       int noutput_items,
                                       const short* src_pixels)
{
    const int first_dst_plane = (12 == plane || 1122 == plane) ? 1 : plane;
    const int second_dst_plane = (12 == plane || 1122 == plane) ? 2 : plane;
    int current_line = (0 == plane) ? d_current_line : d_current_line / 2;

    unsigned char* dst_pixels = (unsigned char*)d_image->pixels[first_dst_plane];
    dst_pixels = &dst_pixels[current_line * d_image->pitches[first_dst_plane]];

    unsigned char* dst_pixels_2 = (unsigned char*)d_image->pixels[second_dst_plane];
    dst_pixels_2 = &dst_pixels_2[current_line * d_image->pitches[second_dst_plane]];

    int src_width = d_width;
    int noutput_items_produced = 0;
    int max_height = (0 == plane) ? d_height - 1 : d_height / 2 - 1;

    for (int i = 0; i < noutput_items; i += src_width) {
        // output one line at a time
        if (12 == plane) {
            copy_line_pixel_interleaved(dst_pixels, dst_pixels_2, src_pixels, src_width);
            dst_pixels_2 += d_image->pitches[second_dst_plane];
        } else if (1122 == plane) {
            copy_line_line_interleaved(dst_pixels, dst_pixels_2, src_pixels, src_width);
            dst_pixels_2 += d_image->pitches[second_dst_plane];
            src_pixels += src_width;
        } else if (0 == plane)
            copy_line_single_plane(dst_pixels, src_pixels, src_width);
        else /* 1==plane || 2==plane*/
            copy_line_single_plane_dec2(
                dst_pixels, src_pixels, src_width); // decimate by two horizontally

        src_pixels += src_width;
        dst_pixels += d_image->pitches[first_dst_plane];
        noutput_items_produced += src_width;
        current_line++;
        if (current_line > max_height) {
            // Start new frame
            // TODO, do this all in a separate thread
            current_line = 0;
            dst_pixels = d_image->pixels[first_dst_plane];
            dst_pixels_2 = d_image->pixels[second_dst_plane];
            if (0 == plane) {
                SDL_DisplayYUVOverlay(d_image, &d_dst_rect);
                // SDL_Flip(d_screen);
                unsigned int ticks = SDL_GetTicks(); // milliseconds
                d_wanted_ticks += d_wanted_frametime_ms;
                float avg_alpha = 0.1;
                int time_diff = d_wanted_ticks - ticks;
                d_avg_delay = time_diff * avg_alpha + d_avg_delay * (1.0 - avg_alpha);
            }
        }
    }

    if (0 == plane)
        d_current_line = current_line;

    return noutput_items_produced;
}

int sink_s_impl::work(int noutput_items,
                      gr_vector_const_void_star& input_items,
                      gr_vector_void_star& output_items)
{
    const short *src_pixels_0, *src_pixels_1, *src_pixels_2;
    int noutput_items_produced = 0;
    int plane;
    int delay = (int)d_avg_delay;

    if (0 == d_wanted_ticks)
        d_wanted_ticks = SDL_GetTicks();
    if (delay > 0)
        SDL_Delay((unsigned int)delay); // compensate if running too fast

    if (SDL_LockYUVOverlay(d_image)) {
        return 0;
    }

    switch (input_items.size()) {
    case 3: // first channel=Y, second channel is  U , third channel is V
        src_pixels_0 = (const short*)input_items[0];
        src_pixels_1 = (const short*)input_items[1];
        src_pixels_2 = (const short*)input_items[2];
        for (int i = 0; i < noutput_items; i += d_chunk_size) {
            copy_plane_to_surface(1, d_chunk_size, src_pixels_1);
            copy_plane_to_surface(2, d_chunk_size, src_pixels_2);
            noutput_items_produced +=
                copy_plane_to_surface(0, d_chunk_size, src_pixels_0);
            src_pixels_0 += d_chunk_size;
            src_pixels_1 += d_chunk_size;
            src_pixels_2 += d_chunk_size;
        }
        break;
    case 2:
        // first channel=Y, second channel is alternating pixels U and V
        src_pixels_0 = (const short*)input_items[0];
        src_pixels_1 = (const short*)input_items[1];
        for (int i = 0; i < noutput_items; i += d_chunk_size) {
            copy_plane_to_surface(12, d_chunk_size / 2, src_pixels_1);
            noutput_items_produced +=
                copy_plane_to_surface(0, d_chunk_size, src_pixels_0);
            src_pixels_0 += d_chunk_size;
            src_pixels_1 += d_chunk_size;
        }
        break;
    case 1: // grey (Y) input
        /* Y component */
        plane = 0;
        src_pixels_0 = (const short*)input_items[plane];
        for (int i = 0; i < noutput_items; i += d_chunk_size) {
            noutput_items_produced +=
                copy_plane_to_surface(plane, d_chunk_size, src_pixels_0);
            src_pixels_0 += d_chunk_size;
        }
        break;
    default: // 0 or more then 3 channels
        d_logger->error(
            "Wrong number of channels: 1, 2 or 3 channels are supported. Requested "
            "number of channels is {:d}",
            input_items.size());
        throw std::runtime_error("video_sdl::sink_s");
    }

    SDL_UnlockYUVOverlay(d_image);
    return noutput_items_produced;
}

} /* namespace video_sdl */
} /* namespace gr */
