/*  RetroArch - A frontend for libretro.
 *  Copyright (C) 2014-2017 - Jean-André Santoni
 *  Copyright (C) 2015-2018 - Andre Leiradella
 *  Copyright (C) 2018-2020 - natinusala
 *
 *  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 "../gfx_display.h"
#include "../gfx_widgets.h"

#include "../cheevos/cheevos.h"

#define CHEEVO_NOTIFICATION_DURATION      4000

#define CHEEVO_QUEUE_SIZE 8

typedef struct cheevo_popup
{
   char* title;
   char* subtitle;
   uintptr_t badge;
} cheevo_popup;

enum
{
   ANCHOR_LEFT = 0,
   ANCHOR_CENTER,
   ANCHOR_RIGHT
};

enum
{
   ANCHOR_TOP = 0,
   ANCHOR_BOTTOM
};

struct gfx_widget_achievement_popup_state
{
#ifdef HAVE_THREADS
   slock_t* queue_lock;
#endif
   cheevo_popup queue[CHEEVO_QUEUE_SIZE]; /* ptr alignment */
   const dispgfx_widget_t* dispwidget_ptr;
   int queue_read_index;
   int queue_write_index;
   unsigned width;         /* Width of popup (in pixels), set in _start (length of strings) */
   unsigned height;        /* Height of popup (in pixels), set in _start (4 x line height) */
   float timer;            /* For delay of CHEEVO_NOTIFICATION_DURATION before starting _fold */
   float unfold;           /* Progress of unfolding animation, changes in _unfold and _fold */
   float slide_h;          /* Progress of horizontal sliding animation, changes in _unfold and _fold (stay centered) */
   float slide_v;          /* Progress of vertical sliding animation, changes in _start and _dismiss (slide on/off screen) */

   /* Values copied from user config in _start to prevent accessing every _frame */
   float target_h;         /* Horizontal sliding target, 0.0 to 0.5, convert to screen-space before use */
   float target_v;         /* Vertical sliding target, 0.0 to 0.5, convert to screen-space before use */
   uint8_t anchor_h;       /* Horizontal anchor */
   uint8_t anchor_v;       /* Vertical anchor */
   bool padding_auto;      /* Should we use target h/v or grab pixel values from p_dispwidget? */
};

typedef struct gfx_widget_achievement_popup_state gfx_widget_achievement_popup_state_t;

static gfx_widget_achievement_popup_state_t p_w_achievement_popup_st;

/* Forward declarations */
static void gfx_widget_achievement_popup_start(
   gfx_widget_achievement_popup_state_t* state);
static void gfx_widget_achievement_popup_free_current(
   gfx_widget_achievement_popup_state_t* state);

static bool gfx_widget_achievement_popup_init(gfx_display_t* p_disp,
   gfx_animation_t* p_anim, bool video_is_threaded, bool fullscreen)
{
   gfx_widget_achievement_popup_state_t* state = &p_w_achievement_popup_st;
   memset(state, 0, sizeof(*state));
   state->dispwidget_ptr = (const dispgfx_widget_t*)
      dispwidget_get_ptr();

   state->queue_read_index = -1;

   return true;
}

static void gfx_widget_achievement_popup_free_all(gfx_widget_achievement_popup_state_t* state)
{
   if (state->queue_read_index >= 0)
   {
#ifdef HAVE_THREADS
      slock_lock(state->queue_lock);
#endif
      while (state->queue[state->queue_read_index].title)
         gfx_widget_achievement_popup_free_current(state);
#ifdef HAVE_THREADS
      slock_unlock(state->queue_lock);
#endif
   }
}

static void gfx_widget_achievement_popup_free(void)
{
   gfx_widget_achievement_popup_state_t* state = &p_w_achievement_popup_st;

   gfx_widget_achievement_popup_free_all(state);

#ifdef HAVE_THREADS
   slock_free(state->queue_lock);
   state->queue_lock = NULL;
#endif
   state->dispwidget_ptr = NULL;
}

static void gfx_widget_achievement_popup_context_destroy(void)
{
   gfx_widget_achievement_popup_state_t* state = &p_w_achievement_popup_st;

   gfx_widget_achievement_popup_free_all(state);
}

static void gfx_widget_achievement_popup_frame(void* data, void* userdata)
{
   gfx_widget_achievement_popup_state_t* state = &p_w_achievement_popup_st;

   /* if there's nothing in the queue, just bail */
   if (state->queue_read_index < 0
      || !state->queue[state->queue_read_index].title)
      return;

#ifdef HAVE_THREADS
   slock_lock(state->queue_lock);
#endif

   {
      static float pure_white[16] = {
         1.00, 1.00, 1.00, 1.00,
         1.00, 1.00, 1.00, 1.00,
         1.00, 1.00, 1.00, 1.00,
         1.00, 1.00, 1.00, 1.00,
      };
      const video_frame_info_t* video_info = (const video_frame_info_t*)data;
      const unsigned video_width           = video_info->width;
      const unsigned video_height          = video_info->height;
      gfx_display_t* p_disp                = (gfx_display_t*)video_info->disp_userdata;
      gfx_display_ctx_driver_t* dispctx    = p_disp->dispctx;
      dispgfx_widget_t* p_dispwidget       = (dispgfx_widget_t*)userdata;

      unsigned text_unfold_offset = 0;
      bool is_folding = false;
      unsigned screen_padding_x = 0;
      unsigned screen_padding_y = 0;
      int screen_pos_x = 0;
      int screen_pos_y = 0;

      /* Slight additional offset for title/subtitle while unfolding */
      text_unfold_offset = ((1.0f - state->unfold) * state->width) * 0.5;

      /* Whether gfx scissoring should occur, partially hiding popup */
      is_folding = fabs(state->unfold - 1.0f) > 0.01;

      /* Calculate padding in screen space */
      if (state->padding_auto)
      {
         screen_padding_x = p_dispwidget->msg_queue_rect_start_x -
            p_dispwidget->msg_queue_icon_size_x;
         screen_padding_y = screen_padding_x;
      }
      else
      {
         screen_padding_x = state->target_h * video_width;
         screen_padding_y = state->target_v * video_height;
      }

      /* Initial horizontal position, then apply animated offset */
      switch (state->anchor_h)
      {
         case ANCHOR_LEFT:
            screen_pos_x = screen_padding_x;
            break;
         case ANCHOR_CENTER:
            screen_pos_x = (video_width - state->height) * 0.5;
            screen_pos_x -= (state->width / 2.0f) * state->slide_h;
            break;
         case ANCHOR_RIGHT:
            screen_pos_x = video_width - state->height - screen_padding_x;
            screen_pos_x -= state->width * state->slide_h;
            break;
      }

      /* Initial vertical position (off-screen), then apply animated offset */
      switch (state->anchor_v)
      {
         case ANCHOR_TOP:
            screen_pos_y = -(state->height);
            screen_pos_y += (screen_padding_y + state->height) * state->slide_v;
            break;
         case ANCHOR_BOTTOM:
            screen_pos_y = video_height;
            screen_pos_y -= (screen_padding_y + state->height) * state->slide_v;
            break;
      }

      gfx_display_set_alpha(p_dispwidget->backdrop_orig, DEFAULT_BACKDROP);
      gfx_display_set_alpha(pure_white, 1.0f);

      /* Default Badge */
      if (!state->queue[state->queue_read_index].badge)
      {
         /* Backdrop */
         gfx_display_draw_quad(
            p_disp,
            video_info->userdata,
            video_width,
            video_height,
            screen_pos_x,
            screen_pos_y,
            state->height,
            state->height,
            video_width,
            video_height,
            p_dispwidget->backdrop_orig,
            NULL);

         /* Icon */
         if (p_dispwidget->gfx_widgets_icons_textures[MENU_WIDGETS_ICON_ACHIEVEMENT])
         {
            if (dispctx && dispctx->blend_begin)
               dispctx->blend_begin(video_info->userdata);

            gfx_widgets_draw_icon(
               video_info->userdata,
               p_disp,
               video_width,
               video_height,
               state->height,
               state->height,
               p_dispwidget->gfx_widgets_icons_textures[
                  MENU_WIDGETS_ICON_ACHIEVEMENT],
               screen_pos_x,
               screen_pos_y,
               0.0f, /* rad */
               1.0f, /* cos(rad)   = cos(0)  = 1.0f */
               0.0f, /* sine(rad)  = sine(0) = 0.0f */
               pure_white);

            if (dispctx && dispctx->blend_end)
               dispctx->blend_end(video_info->userdata);
         }
      }
      /* Badge */
      else
      {
         gfx_widgets_draw_icon(
            video_info->userdata,
            p_disp,
            video_width,
            video_height,
            state->height,
            state->height,
            state->queue[state->queue_read_index].badge,
            screen_pos_x,
            screen_pos_y,
            0.0f, /* rad */
            1.0f, /* cos(rad)   = cos(0)  = 1.0f */
            0.0f, /* sine(rad)  = sine(0) = 0.0f */
            pure_white);
      }

      if (is_folding)
      {
         gfx_display_scissor_begin(
            p_disp,
            video_info->userdata,
            video_width,
            video_height,
            screen_pos_x + state->height,
            screen_pos_y,
            (unsigned)((float)(state->width) * state->unfold),
            state->height);
      }

      /* Backdrop */
      gfx_display_draw_quad(
         p_disp,
         video_info->userdata,
         video_width,
         video_height,
         screen_pos_x + state->height,
         screen_pos_y,
         state->width,
         state->height,
         video_width,
         video_height,
         p_dispwidget->backdrop_orig,
         NULL);

      /* Title */
      gfx_widgets_draw_text(
         &p_dispwidget->gfx_widget_fonts.regular,
         state->queue[state->queue_read_index].title,
         screen_pos_x + state->height - text_unfold_offset
         + p_dispwidget->simple_widget_padding,
         screen_pos_y
         + p_dispwidget->gfx_widget_fonts.regular.line_height
         + p_dispwidget->gfx_widget_fonts.regular.line_ascender,
         video_width,
         video_height,
         TEXT_COLOR_FAINT,
         TEXT_ALIGN_LEFT,
         true);

      /* Subtitle */

      /* TODO: is a ticker necessary ? */
      gfx_widgets_draw_text(
         &p_dispwidget->gfx_widget_fonts.regular,
         state->queue[state->queue_read_index].subtitle,
         screen_pos_x + state->height - text_unfold_offset
         + p_dispwidget->simple_widget_padding,
         screen_pos_y + state->height
         - p_dispwidget->gfx_widget_fonts.regular.line_height
         - p_dispwidget->gfx_widget_fonts.regular.line_descender,
         video_width,
         video_height,
         TEXT_COLOR_INFO,
         TEXT_ALIGN_LEFT,
         true);

      if (is_folding)
      {
         gfx_widgets_flush_text(video_width, video_height,
            &p_dispwidget->gfx_widget_fonts.regular);

         if (dispctx && dispctx->scissor_end)
            dispctx->scissor_end(video_info->userdata,
               video_width, video_height);
      }
   }

#ifdef HAVE_THREADS
   slock_unlock(state->queue_lock);
#endif
}

static void gfx_widget_achievement_popup_free_current(
   gfx_widget_achievement_popup_state_t* state)
{
   if (state->queue[state->queue_read_index].title)
   {
      free(state->queue[state->queue_read_index].title);
      state->queue[state->queue_read_index].title = NULL;
   }

   if (state->queue[state->queue_read_index].subtitle)
   {
      free(state->queue[state->queue_read_index].subtitle);
      state->queue[state->queue_read_index].subtitle = NULL;
   }

   if (state->queue[state->queue_read_index].badge)
   {
      video_driver_texture_unload(&state->queue[state->queue_read_index].badge);
      state->queue[state->queue_read_index].badge = 0;
   }

   state->queue_read_index = (state->queue_read_index + 1) % ARRAY_SIZE(state->queue);
}

static void gfx_widget_achievement_popup_next(void* userdata)
{
   gfx_widget_achievement_popup_state_t* state = &p_w_achievement_popup_st;

#ifdef HAVE_THREADS
   slock_lock(state->queue_lock);
#endif

   if (state->queue_read_index >= 0)
   {
      if (state->queue[state->queue_read_index].title)
         gfx_widget_achievement_popup_free_current(state);

      /* start the next popup (if present) */
      if (state->queue[state->queue_read_index].title)
         gfx_widget_achievement_popup_start(state);
   }

#ifdef HAVE_THREADS
   slock_unlock(state->queue_lock);
#endif
}

static void gfx_widget_achievement_popup_dismiss(void* userdata)
{
   gfx_animation_ctx_entry_t entry;
   gfx_widget_achievement_popup_state_t* state = &p_w_achievement_popup_st;
   const dispgfx_widget_t* p_dispwidget = (const dispgfx_widget_t*)
      state->dispwidget_ptr;

   /* Slide off-screen */
   entry.cb             = gfx_widget_achievement_popup_next;
   entry.duration       = MSG_QUEUE_ANIMATION_DURATION;
   entry.easing_enum    = EASING_OUT_QUAD;
   entry.subject        = &state->slide_v;
   entry.tag            = p_dispwidget->gfx_widgets_generic_tag;
   entry.target_value   = 0.0f;
   entry.userdata       = NULL;

   gfx_animation_push(&entry);
}

static void gfx_widget_achievement_popup_fold(void* userdata)
{
   gfx_animation_ctx_entry_t anim_fold;
   gfx_animation_ctx_entry_t anim_slide;
   gfx_widget_achievement_popup_state_t* state = &p_w_achievement_popup_st;
   const dispgfx_widget_t* p_dispwidget = (const dispgfx_widget_t*)
      state->dispwidget_ptr;

   /* Fold */
   anim_fold.cb           = gfx_widget_achievement_popup_dismiss;
   anim_fold.duration     = MSG_QUEUE_ANIMATION_DURATION;
   anim_fold.easing_enum  = EASING_OUT_QUAD;
   anim_fold.subject      = &state->unfold;
   anim_fold.tag          = p_dispwidget->gfx_widgets_generic_tag;
   anim_fold.target_value = 0.0f;
   anim_fold.userdata     = NULL;

   gfx_animation_push(&anim_fold);

   /* Slide horizontal (if required) */
   if (state->anchor_h != ANCHOR_LEFT)
   {
      anim_slide.cb           = NULL;
      anim_slide.duration     = MSG_QUEUE_ANIMATION_DURATION;
      anim_slide.easing_enum  = EASING_OUT_QUAD;
      anim_slide.subject      = &state->slide_h;
      anim_slide.tag          = p_dispwidget->gfx_widgets_generic_tag;
      anim_slide.target_value = 0.0f;
      anim_slide.userdata     = NULL;

      gfx_animation_push(&anim_slide);
   }
}

static void gfx_widget_achievement_popup_unfold(void* userdata)
{
   gfx_timer_ctx_entry_t timer;
   gfx_animation_ctx_entry_t anim_unfold;
   gfx_animation_ctx_entry_t anim_slide;
   gfx_widget_achievement_popup_state_t* state = &p_w_achievement_popup_st;
   const dispgfx_widget_t* p_dispwidget = (const dispgfx_widget_t*)
      state->dispwidget_ptr;

   /* Unfold */
   anim_unfold.cb           = NULL;
   anim_unfold.duration     = MSG_QUEUE_ANIMATION_DURATION;
   anim_unfold.easing_enum  = EASING_OUT_QUAD;
   anim_unfold.subject      = &state->unfold;
   anim_unfold.tag          = p_dispwidget->gfx_widgets_generic_tag;
   anim_unfold.target_value = 1.0f;
   anim_unfold.userdata     = NULL;

   gfx_animation_push(&anim_unfold);

   /* Slide horizontal (if required) */
   if (state->anchor_h != ANCHOR_LEFT)
   {
      anim_slide.cb           = NULL;
      anim_slide.duration     = MSG_QUEUE_ANIMATION_DURATION;
      anim_slide.easing_enum  = EASING_OUT_QUAD;
      anim_slide.subject      = &state->slide_h;
      anim_slide.tag          = p_dispwidget->gfx_widgets_generic_tag;
      anim_slide.target_value = 1.0f;
      anim_slide.userdata     = NULL;

      gfx_animation_push(&anim_slide);
   }

   /* Wait before folding */
   timer.cb       = gfx_widget_achievement_popup_fold;
   timer.duration = MSG_QUEUE_ANIMATION_DURATION + CHEEVO_NOTIFICATION_DURATION;
   timer.userdata = NULL;

   gfx_animation_timer_start(&state->timer, &timer);
}

void gfx_widgets_update_cheevos_appearance(void)
{
   gfx_widget_achievement_popup_state_t* state = &p_w_achievement_popup_st;
   const settings_t* settings = config_get_ptr();
   const float target_h = settings->floats.cheevos_appearance_padding_h;
   const float target_v = settings->floats.cheevos_appearance_padding_v;
   const bool autopadding = settings->bools.cheevos_appearance_padding_auto;
   const unsigned anchor = settings->uints.cheevos_appearance_anchor;

   state->target_h = target_h;
   state->target_v = target_v;
   state->padding_auto = autopadding;

   if (anchor == CHEEVOS_APPEARANCE_ANCHOR_TOPCENTER ||
      anchor == CHEEVOS_APPEARANCE_ANCHOR_BOTTOMCENTER)
      state->anchor_h = ANCHOR_CENTER;
   else if (anchor == CHEEVOS_APPEARANCE_ANCHOR_TOPRIGHT ||
      anchor == CHEEVOS_APPEARANCE_ANCHOR_BOTTOMRIGHT)
      state->anchor_h = ANCHOR_RIGHT;
   else
      state->anchor_h = ANCHOR_LEFT;

   if (anchor == CHEEVOS_APPEARANCE_ANCHOR_BOTTOMLEFT ||
      anchor == CHEEVOS_APPEARANCE_ANCHOR_BOTTOMCENTER ||
      anchor == CHEEVOS_APPEARANCE_ANCHOR_BOTTOMRIGHT)
      state->anchor_v = ANCHOR_BOTTOM;
   else
      state->anchor_v = ANCHOR_TOP;
}

static void gfx_widget_achievement_popup_start(
   gfx_widget_achievement_popup_state_t* state)
{
   gfx_animation_ctx_entry_t anim_slide;
   const dispgfx_widget_t* p_dispwidget = (const dispgfx_widget_t*)
      state->dispwidget_ptr;

   state->height = p_dispwidget->gfx_widget_fonts.regular.line_height * 4.0f;
   state->width = MAX(
      font_driver_get_message_width(
         p_dispwidget->gfx_widget_fonts.regular.font,
         state->queue[state->queue_read_index].title, 0, 1.0f),
      font_driver_get_message_width(
         p_dispwidget->gfx_widget_fonts.regular.font,
         state->queue[state->queue_read_index].subtitle, 0, 1.0f)
   );
   state->width += p_dispwidget->simple_widget_padding * 2;
   state->unfold = 0.0f;
   state->slide_h = 0.0f;
   state->slide_v = 0.0f;

   /* Store settings to prevent lookup every _frame */
   gfx_widgets_update_cheevos_appearance();

   /* Slide vertical onto screen */
   anim_slide.cb = gfx_widget_achievement_popup_unfold;
   anim_slide.duration = MSG_QUEUE_ANIMATION_DURATION;
   anim_slide.easing_enum = EASING_OUT_QUAD;
   anim_slide.subject = &state->slide_v;
   anim_slide.tag = p_dispwidget->gfx_widgets_generic_tag;
   anim_slide.target_value = 1.0f;
   anim_slide.userdata = NULL;

   gfx_animation_push(&anim_slide);
}

void gfx_widgets_push_achievement(const char* title, const char* subtitle, const char* badge)
{
   gfx_widget_achievement_popup_state_t* state = &p_w_achievement_popup_st;
   int start_notification = 1;

   /* important - this must be done outside the lock because it has the potential to need to
    * lock the video thread, which may be waiting for the popup queue lock to render popups */
   uintptr_t badge_id = rcheevos_get_badge_texture(badge, 0);

   if (state->queue_read_index < 0)
   {
      /* queue uninitialized */
      memset(&state->queue, 0, sizeof(state->queue));
      state->queue_read_index = 0;

#ifdef HAVE_THREADS
      state->queue_lock = slock_new();
#endif
   }

#ifdef HAVE_THREADS
   slock_lock(state->queue_lock);
#endif

   if (state->queue_write_index == state->queue_read_index)
   {
      if (state->queue[state->queue_write_index].title)
      {
         /* queue full */
#ifdef HAVE_THREADS
         slock_unlock(state->queue_lock);
#endif
         return;
      }

      /* queue empty */
   }
   else
      start_notification = 0; /* notification already being displayed */

   state->queue[state->queue_write_index].badge = badge_id;
   state->queue[state->queue_write_index].title = strdup(title);
   state->queue[state->queue_write_index].subtitle = strdup(subtitle);

   state->queue_write_index = (state->queue_write_index + 1) % ARRAY_SIZE(state->queue);

   if (start_notification)
      gfx_widget_achievement_popup_start(state);

#ifdef HAVE_THREADS
   slock_unlock(state->queue_lock);
#endif
}

const gfx_widget_t gfx_widget_achievement_popup = {
   &gfx_widget_achievement_popup_init,
   &gfx_widget_achievement_popup_free,
   NULL, /* context_reset*/
   &gfx_widget_achievement_popup_context_destroy,
   NULL, /* layout */
   NULL, /* iterate */
   &gfx_widget_achievement_popup_frame
};