/*  RetroArch - A frontend for libretro.
 *  Copyright (C) 2010-2014 - Hans-Kristian Arntzen
 *  Copyright (C) 2011-2017 - Daniel De Matteis
 *  Copyright (C) 2011-2017 - Higor Euripedes
 *
 *  RetroArch 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 Found-
 *  ation, either version 3 of the License, or (at your option) any later version.
 *
 *  RetroArch 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 RetroArch.
 *  If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdlib.h>
#include <string.h>
#include <stdint.h>

#include <fcntl.h>
#include <rga/RgaApi.h>
#include <rga/RockchipRgaMacro.h>
#include <xf86drm.h>
#include <xf86drmMode.h>
#include <drm/drm_fourcc.h>

#include <libretro.h>

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

#ifdef HAVE_MENU
#include "../../menu/menu_driver.h"
#endif

#include "frontend/frontend_driver.h"

#include "../font_driver.h"

#include "../../configuration.h"
#include "../../retroarch.h"
#include "../../verbosity.h"

#define likely(x)   __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)

#define ALIGN(val, align) (((val) + (align) - 1) & ~((align) - 1))

#define NUM_PAGES 3

typedef struct oga_rect
{
   int x;
   int y;
   int w;
   int h;
} oga_rect_t;

typedef struct oga_surface
{
   uint8_t* map;
   int width;
   int height;
   int pitch;
   int prime_fd;
   int rk_format;
   int display_fd;
   uint32_t handle;
} oga_surface_t;

typedef struct oga_framebuf
{
   oga_surface_t* surface;
   uint32_t fb_id;
} oga_framebuf_t;

typedef struct oga_video
{
   drmModeModeInfo mode;
   oga_surface_t *frame_surface;
   oga_surface_t *menu_surface;
   oga_framebuf_t *pages[NUM_PAGES];
   oga_surface_t *msg_surface;
   const font_renderer_driver_t *font_driver;
   void *font;
   int msg_width;
   int msg_height;
   int fd;
   int drm_width;
   int drm_height;
   int cur_page;
   int scale_mode;
   int rotation;
   uint32_t crtc_id;
   uint32_t connector_id;
   float display_ar;
   bool threaded;
   char last_msg[128];
} oga_video_t;

static bool oga_create_display(oga_video_t* vid)
{
   int i, ret;
   drmModeConnector *connector;
   drmModeModeInfo *mode;
   drmModeEncoder *encoder;
   drmModeRes *resources;

   vid->fd = open("/dev/dri/card0", O_RDWR);
   if (vid->fd < 0)
   {
      RARCH_ERR("open /dev/dri/card0 failed.\n");
      return false;
   }

   resources = drmModeGetResources(vid->fd);
   if (!resources)
   {
      RARCH_ERR("drmModeGetResources failed: %s\n", strerror(errno));
      goto err_01;
   }

   for (i = 0; i < resources->count_connectors; i++)
   {
      connector = drmModeGetConnector(vid->fd, resources->connectors[i]);
      if (connector->connection == DRM_MODE_CONNECTED)
         break;

      drmModeFreeConnector(connector);
      connector = NULL;
   }

   if (!connector)
   {
      RARCH_ERR("DRM_MODE_CONNECTED not found.\n");
      goto err_02;
   }

   vid->connector_id = connector->connector_id;

   /* Find preferred mode */
   for (i = 0; i < connector->count_modes; i++)
   {
      drmModeModeInfo *current_mode = &connector->modes[i];
      if (current_mode->type & DRM_MODE_TYPE_PREFERRED)
      {
         mode = current_mode;
         break;
      }

      mode = NULL;
   }

   if (!mode)
   {
      RARCH_ERR("DRM_MODE_TYPE_PREFERRED not found.\n");
      goto err_03;
   }

   vid->mode   = *mode;

   /* Find encoder */
   for (i = 0; i < resources->count_encoders; i++)
   {
      encoder = drmModeGetEncoder(vid->fd, resources->encoders[i]);
      if (encoder->encoder_id == connector->encoder_id)
         break;

      drmModeFreeEncoder(encoder);
      encoder = NULL;
   }

   if (!encoder)
   {
      RARCH_ERR("could not find encoder!\n");
      goto err_03;
   }

   vid->crtc_id = encoder->crtc_id;

   drmModeFreeEncoder(encoder);
   drmModeFreeConnector(connector);
   drmModeFreeResources(resources);

   return true;

err_03:
   drmModeFreeConnector(connector);

err_02:
   drmModeFreeResources(resources);

err_01:
   close(vid->fd);

   return false;
}

static oga_surface_t* oga_create_surface(int display_fd,
      int width, int height, int rk_format)
{
   struct drm_mode_create_dumb args = {0};
   oga_surface_t* surface           = (oga_surface_t*)
      calloc(1, sizeof(oga_surface_t));
   if (!surface)
   {
      RARCH_ERR("Error allocating surface\n");
      return NULL;
   }

   args.width  = width;
   args.height = height;
   args.bpp    = rk_format == RK_FORMAT_BGRA_8888 ? 32 : 16;
   args.flags  = 0;

   if (drmIoctl(display_fd, DRM_IOCTL_MODE_CREATE_DUMB, &args) < 0)
   {
      RARCH_ERR("DRM_IOCTL_MODE_CREATE_DUMB failed.\n");
      goto out;
   }

   surface->display_fd = display_fd;
   surface->handle     = args.handle;
   surface->width      = width;
   surface->height     = height;
   surface->pitch      = width * args.bpp / 8;
   surface->rk_format  = rk_format;

   if (drmPrimeHandleToFD(display_fd, surface->handle, DRM_RDWR | DRM_CLOEXEC, &surface->prime_fd) < 0)
   {
      RARCH_ERR("drmPrimeHandleToFD failed.\n");
      goto out;
   }

   surface->map = mmap(NULL, args.size, PROT_READ | PROT_WRITE, MAP_SHARED, surface->prime_fd, 0);
   if (surface->map == MAP_FAILED)
   {
      RARCH_LOG("mmap failed.\n");
      return NULL;
   }

   return surface;

out:
   free(surface);
   return NULL;
}

static void oga_destroy_surface(oga_surface_t* surface)
{
   int io;
   struct drm_mode_destroy_dumb args = { 0 };

   args.handle = surface->handle;

   io          = drmIoctl(surface->display_fd,
         DRM_IOCTL_MODE_DESTROY_DUMB, &args);
   if (io < 0)
      RARCH_ERR("DRM_IOCTL_MODE_DESTROY_DUMB failed.\n");

   free(surface);
}

static oga_framebuf_t* oga_create_framebuf(oga_surface_t* surface)
{
   int ret;
   const uint32_t handles[4] = {surface->handle, 0, 0, 0};
   const uint32_t pitches[4] = {surface->pitch, 0, 0, 0};
   const uint32_t offsets[4] = {0, 0, 0, 0};
   oga_framebuf_t* framebuf  = calloc(1, sizeof(oga_framebuf_t));

   if (!framebuf)
   {
      RARCH_ERR("Error allocating framebuf\n");
      return NULL;
   }

   framebuf->surface = surface;
   ret               = drmModeAddFB2(surface->display_fd,
         surface->width,
         surface->height,
         surface->rk_format == RK_FORMAT_BGRA_8888
         ? DRM_FORMAT_ARGB8888
         : DRM_FORMAT_RGB565,
         handles,
         pitches,
         offsets,
         &framebuf->fb_id,
         0);

   if (ret)
   {
      RARCH_ERR("drmModeAddFB2 failed.\n");
      free(framebuf);
      return NULL;
   }

   return framebuf;
}

static void oga_destroy_framebuf(oga_framebuf_t* framebuf)
{
   if (drmModeRmFB(framebuf->surface->display_fd, framebuf->fb_id) != 0)
      RARCH_ERR("drmModeRmFB failed.\n");

   oga_destroy_surface(framebuf->surface);
   free(framebuf);
}

static void oga_free(void *data)
{
   unsigned i;
   oga_video_t *vid = (oga_video_t*)data;

   if (!vid)
      return;

   if (vid->font)
   {
      vid->font_driver->free(vid->font);
      vid->font_driver = NULL;
   }

   for (i = 0; i < NUM_PAGES; ++i)
      oga_destroy_framebuf(vid->pages[i]);

   oga_destroy_surface(vid->frame_surface);
   oga_destroy_surface(vid->msg_surface);
   oga_destroy_surface(vid->menu_surface);

   close(vid->fd);

   free(vid);
   vid = NULL;
}

static void *oga_init(const video_info_t *video,
      input_driver_t **input, void **input_data)
{
   int i;
   oga_video_t *vid                     = NULL;
   settings_t *settings                 = config_get_ptr();
   video_driver_state_t *video_st       = video_state_get_ptr();
   struct retro_system_av_info *av_info = &video_st->av_info;
   struct retro_game_geometry  *geom    = &av_info->geometry;
   int aw                               = ALIGN(geom->base_width, 32);
   int ah                               = ALIGN(geom->base_height, 32);

   frontend_driver_install_signal_handler();

   if (input && input_data)
   {
      void* udev = input_driver_init_wrap(
            &input_udev, settings->arrays.input_joypad_driver);
      if (udev)
      {
         *input       = &input_udev;
         *input_data  = udev;
      }
      else
         *input = NULL;
   }

   vid = (oga_video_t*)calloc(1, sizeof(*vid));
   if (!vid)
   {
      RARCH_ERR("Error allocating vid\n");
      return NULL;
   }

   if (!oga_create_display(vid))
   {
      RARCH_ERR("Error initializing drm\n");
      return NULL;
   }

   vid->display_ar = (float)vid->mode.vdisplay / vid->mode.hdisplay;
   vid->drm_width = vid->mode.vdisplay;
   vid->drm_height = vid->mode.hdisplay;

   vid->menu_surface = oga_create_surface(vid->fd, vid->drm_width, vid->drm_height, RK_FORMAT_BGRA_8888);
   vid->threaded = video->is_threaded;

   /*
    * From RGA2 documentation:
    *
    *  0  CATROM
    *  1  MITCHELL
    *  2  HERMITE
    *  3  B-SPLINE
    */
   vid->scale_mode = video->ctx_scaling << 1 | video->smooth;
   vid->rotation = 0;

   vid->frame_surface = oga_create_surface(vid->fd, geom->max_width, geom->max_height, video->rgb32 ? RK_FORMAT_BGRA_8888 : RK_FORMAT_RGB_565);
   vid->msg_surface   = oga_create_surface(vid->fd, vid->drm_width, vid->drm_height, RK_FORMAT_BGRA_8888);
   vid->last_msg[0]   = 0;

   /* bitmap only for now */
   if (settings->bools.video_font_enable)
   {
      vid->font_driver = &bitmap_font_renderer;
      vid->font        = vid->font_driver->init("", settings->floats.video_font_size);
   }

   for (i = 0; i < NUM_PAGES; ++i)
   {
      oga_surface_t* surface = oga_create_surface(vid->fd, vid->drm_height, vid->drm_width, RK_FORMAT_BGRA_8888);
      vid->pages[i] = oga_create_framebuf(surface);
      if (!vid->pages[i])
         return NULL;
   }

   return vid;
}

static void rga_clear_surface(oga_surface_t* surface, int color)
{
   rga_info_t dst   = { 0 };
   dst.fd           = surface->prime_fd;
   dst.mmuFlag      = 1;
   dst.rect.xoffset = 0;
   dst.rect.yoffset = 0;
   dst.rect.width   = surface->width;
   dst.rect.height  = surface->height;
   dst.rect.wstride = dst.rect.width;
   dst.rect.hstride = dst.rect.height;
   dst.rect.format  = surface->rk_format;
   dst.color        = color;

   c_RkRgaColorFill(&dst);
}

static bool render_msg(oga_video_t* vid, const char* msg)
{
   const struct font_atlas* atlas;
   uint32_t* fb;
   const char *c    = msg;
   int dest_x       = 0;
   int dest_y       = 0;
   int dest_stride;

   if (msg[0] == '\0')
      return false;

   if (strcmp(msg, vid->last_msg) == 0)
      return true;

   strlcpy(vid->last_msg, c, sizeof(vid->last_msg));
   rga_clear_surface(vid->msg_surface, 0);

   atlas          = vid->font_driver->get_atlas(vid->font);
   fb             = (uint32_t*)vid->msg_surface->map;
   dest_stride    = vid->msg_surface->pitch / 4;
   vid->msg_width = vid->msg_height = 0;

   while (*c)
   {
      int x, y;
      uint32_t* dest             = NULL;
      const uint8_t *source      = NULL;
      const struct font_glyph* g = vid->font_driver->get_glyph(vid->font, *c);

      if (!g)
         continue;

      if (vid->msg_height == 0)
         vid->msg_height = g->height;

      if (dest_x >= vid->drm_width)
      {
         dest_x = 0;
         dest_y += g->height;
         vid->msg_height += g->height;
      }

      source = atlas->buffer + g->atlas_offset_y *
         atlas->width  + g->atlas_offset_x;
      dest   = fb + dest_y * dest_stride + dest_x;

      for (y = 0; y < g->height; y++)
      {
         for (x = 0; x < g->advance_x; x++)
         {
            uint32_t px = (x < g->width) ? *(source++) : 0x00;
            *(dest++)   = (0xCD << 24) | (px << 16) | (px << 8) | px;
         }
         dest   += dest_stride - g->advance_x;
         source += atlas->width - g->width;
      }

      c++;
      dest_x += g->advance_x;

      if (vid->msg_width < dest_x)
         vid->msg_width = MIN(dest_x, vid->msg_surface->width);
   }


   return true;
}

static void oga_blit(oga_surface_t* src, int sx, int sy, int sw, int sh,
      oga_surface_t* dst, int dx, int dy, int dw, int dh,
      int rotation, int scale_mode, unsigned int blend)
{
   rga_info_t s = {
      .fd = src->prime_fd,
      .rect = { sx, sy, sw, sh, src->width, src->height, src->rk_format },
      .rotation = rotation,
      .mmuFlag = 1,
      .scale_mode = scale_mode,
      .blend = blend,
   };

   rga_info_t d = {
      .fd = dst->prime_fd,
      .rect = { dx, dy, dw, dh, dst->width, dst->height, dst->rk_format },
      .mmuFlag = 1,
   };

   c_RkRgaBlit(&s, &d, NULL);
}

static void oga_calc_bounds(oga_rect_t* r, int dw, int dh, int sw, int sh, float aspect, float dar)
{
   if (dar >= aspect)
   {
      r->h = dh;
      r->w = MIN(dw, (dh * aspect + 0.5));
      r->x = (int)((dw - r->w) / 2) + 0.5;
      r->y = 0;
   }
   else
   {
      r->w = dw;
      r->h = MIN(dh, (dw / aspect + 0.5));
      r->x = 0;
      r->y = (int)((dh - r->h) / 2) + 0.5;
   }
}

static bool oga_frame(void *data, const void *frame, unsigned width,
      unsigned height, uint64_t frame_count,
      unsigned pitch, const char *msg, video_frame_info_t *video_info)
{
   oga_video_t *vid            = (oga_video_t*)data;
   oga_framebuf_t* page        = vid->pages[vid->cur_page];
   oga_surface_t *page_surface = page->surface;
   float aspect_ratio          = video_driver_get_aspect_ratio();
#ifdef HAVE_MENU
   bool menu_is_alive = (video_info->menu_st_flags & MENU_ST_FLAG_ALIVE) ? true : false;
#endif

   if (unlikely(!frame || width == 0 || height == 0))
      return true;

   if (unlikely(video_info->input_driver_nonblock_state) && !vid->threaded)
   {
      if (frame_count % 4 != 0)
         return true;
   }

   if (msg && vid->font)
   {
        if (!render_msg(vid, msg))
            msg = NULL;
   }

   rga_clear_surface(page_surface, 0);

#ifdef HAVE_MENU
   if (menu_is_alive)
   {
      oga_rect_t r;
      menu_driver_frame(true, video_info);

      width        = vid->menu_surface->width;
      height       = vid->menu_surface->height;
      aspect_ratio = (float)width / height;

      oga_calc_bounds(&r, vid->drm_width, vid->drm_height, width, height, aspect_ratio, vid->display_ar);
      oga_blit(vid->menu_surface, 0, 0, width, height,
            page_surface, r.y, r.x, r.h, r.w, HAL_TRANSFORM_ROT_270, vid->scale_mode, 0);
   }
   else
#endif
   {
      uint8_t* src = (uint8_t*)frame;
      uint8_t* dst = (uint8_t*)vid->frame_surface->map;
      unsigned int blend = video_info->runloop_is_paused ? 0x800105 : 0;
      oga_rect_t r;

      if (src != dst)
      {
         int dst_pitch = vid->frame_surface->pitch;
         int yy = height;

         while (yy > 0)
         {
             memcpy(dst, src, pitch);
             src += pitch;
             dst += dst_pitch;
             --yy;
         }
      }

      oga_calc_bounds(&r, vid->drm_width, vid->drm_height, width, height, aspect_ratio, vid->display_ar);
      oga_blit(vid->frame_surface, 0, 0, width, height,
            page_surface, r.y, r.x, r.h, r.w, vid->rotation, vid->scale_mode, blend);
   }

   if (msg)
   {
      oga_blit(vid->msg_surface, 0, 0, vid->msg_width, vid->msg_height,
            page_surface, 0, 0, vid->msg_height, vid->msg_width,
            HAL_TRANSFORM_ROT_270, vid->scale_mode, 0xff0105);
   }

   if (unlikely(drmModeSetCrtc(vid->fd, vid->crtc_id, page->fb_id, 0, 0, &vid->connector_id, 1, &vid->mode) != 0))
      RARCH_ERR("drmModeSetCrtc failed.\n");

   vid->cur_page = (vid->cur_page + 1) % NUM_PAGES;

   return true;
}

static void oga_set_texture_frame(void *data, const void *frame, bool rgb32,
      unsigned width, unsigned height, float alpha)
{
   oga_video_t *vid             = (oga_video_t*)data;
   unsigned i, j;
   /* Borrowed from drm_gfx
    *
    * We have to go on a pixel format conversion adventure
    * for now, until we can convince RGUI to output
    * in an 8888 format. */
   unsigned int src_pitch        = width * 2;
   unsigned int dst_pitch        = width * 4;
   unsigned int dst_width        = width;
   uint32_t line[dst_width];
   char *frame_output;

   if (vid->menu_surface->width != width || vid->menu_surface->height != height)
   {
      oga_destroy_surface(vid->menu_surface);
      vid->menu_surface = oga_create_surface(vid->fd, width, height,
            RK_FORMAT_BGRA_8888);
   }

   /* The output pixel array with the converted pixels. */
   frame_output = (char*)vid->menu_surface->map;

   for (i = 0; i < height; i++)
   {
      for (j = 0; j < src_pitch / 2; j++)
      {
         uint16_t src_pix = *((uint16_t*)frame + (src_pitch / 2 * i) + j);
         /* The hex AND is for keeping only the part
          * we need for each component. */
         uint32_t R       = (src_pix << 8) & 0x00FF0000;
         uint32_t G       = (src_pix << 4) & 0x0000FF00;
         uint32_t B       = (src_pix << 0) & 0x000000FF;
         line[j]          = (0x00 | R | G | B);
      }
      memcpy(frame_output + (dst_pitch * i), (char*)line, dst_pitch);
   }
}

/* TODO/FIXME - implement */
static void oga_texture_enable(void *data, bool state, bool full_screen) { }
static void oga_set_nonblock_state(void *a, bool b, bool c, unsigned d) { }

static bool oga_alive(void *data)
{
   return !frontend_driver_get_signal_handler_state();
}

static bool oga_focus(void *data) { return true; }
static bool oga_suppress_screensaver(void *a, bool b) { return false; }
static bool oga_has_windowed(void *data) { return false; }

static void oga_viewport_info(void *data, struct video_viewport *vp)
{
   oga_video_t *vid = (oga_video_t*)data;
   if (unlikely(!vid))
      return;

   vp->x = vp->y = 0;
   vp->width = vp->full_width = vid->mode.vdisplay;
   vp->height = vp->full_height = vid->mode.hdisplay;
}

static bool oga_set_shader(void *data, enum rarch_shader_type type, const char *path)
{
   return false;
}

static void oga_set_rotation(void *data, unsigned rotation)
{
   oga_video_t *vid = (oga_video_t*)data;
   if (!vid)
      return;

   switch (rotation)
   {
   case 0:
      vid->rotation = HAL_TRANSFORM_ROT_270;
      break;
   case 1:
      vid->rotation = HAL_TRANSFORM_ROT_180;
      break;
   case 2:
      vid->rotation = HAL_TRANSFORM_ROT_90;
      break;
   case 3:
      vid->rotation = 0;
      break;
   default:
      RARCH_ERR("Unhandled rotation %hu\n", rotation);
      break;
   }
}

static bool oga_get_current_software_framebuffer(void *data, struct retro_framebuffer *framebuffer)
{
   oga_video_t *vid = (oga_video_t*)data;
   if (!vid)
      return false;

   framebuffer->format = vid->frame_surface->rk_format == RK_FORMAT_BGRA_8888 ?
      RETRO_PIXEL_FORMAT_XRGB8888 : RETRO_PIXEL_FORMAT_RGB565;
   framebuffer->data = (uint8_t*)vid->frame_surface->map;
   framebuffer->pitch = vid->frame_surface->pitch;

   return true;
}

video_poke_interface_t oga_poke_interface = {
   NULL, /* get_flags */
   NULL, /* load_texture */
   NULL, /* unload_texture */
   NULL, /* set_video_mode */
   NULL, /* get_refresh_rate */
   NULL, /* set_filtering */
   NULL, /* get_video_output_size */
   NULL, /* get_video_output_prev */
   NULL, /* get_video_output_next */
   NULL, /* get_current_framebuffer */
   NULL, /* get_proc_address */
   NULL, /* set_aspect_ratio */
   NULL, /* apply_state_changes */
   oga_set_texture_frame,
   oga_texture_enable,
   NULL, /* set_osd_msg */
   NULL, /* show_mouse */
   NULL, /* grab_mouse_toggle */
   NULL, /* get_current_shader */
   oga_get_current_software_framebuffer,
   NULL, /* get_hw_render_interface */
   NULL, /* set_hdr_max_nits */
   NULL, /* set_hdr_paper_white_nits */
   NULL, /* set_hdr_contrast */
   NULL  /* set_hdr_expand_gamut */
};

static void oga_get_poke_interface(void *data, const video_poke_interface_t **iface)
{
   *iface = &oga_poke_interface;
}

video_driver_t video_oga = {
   oga_init,
   oga_frame,
   oga_set_nonblock_state,
   oga_alive,
   oga_focus,
   oga_suppress_screensaver,
   oga_has_windowed,
   oga_set_shader,
   oga_free,
   "oga",
   NULL, /* set_viewport */
   oga_set_rotation,
   oga_viewport_info,
   NULL, /* read_viewport */
   NULL, /* read_frame_raw */
#ifdef HAVE_OVERLAY
   NULL, /* get_overlay_interface */
#endif
   oga_get_poke_interface,
   NULL, /* wrap_type_to_enum */
#ifdef HAVE_GFX_WIDGETS
   NULL  /* gfx_widgets_enabled */
#endif
};
