#include "efm_icon.h"
#include <Emotion.h>

// XXX: support animated files (anim gif etc.)
// XXX: support video files (mp4 etc.)
// XXX: support edj files ???
// XXX: url's ?

typedef struct _Smart_Data Smart_Data;

struct _Smart_Data
{
   Evas_Object_Smart_Clipped_Data __clipped_data;

   Eina_Rectangle        geom;

   Evas_Object          *o_smart; // the smart object container itself
   Evas_Object          *o_image; // the image to be shown right now
   Evas_Object          *o_image2; // the image being loaded still
   Evas_Object          *o_video; // the image used for video/audio playback

   Ecore_Timer          *settle_timer; // time to figure out when 
   Ecore_Job            *wakeup; // a job to wake up the loop
   Eina_Stringshare     *file; // file path or...
   Eina_Stringshare     *thumb; // thumb path
   Eina_Stringshare     *video; // video path
   Ecore_Timer          *anim_timer; // timer for animation frame flipping

   int                   load_size; // the sie we want to load now
   int                   orig_w, orig_h; // the sie of the img we loaded
   int                   frame; // current frame to display
   int                   frame_count; // number of frames in anim
   double                video_ratio; // aspect ratio for videos
   Evas_Image_Animated_Loop_Hint loop_type; // animated loop type
   Eina_Bool             alpha : 1; // does the img have alpha
   Eina_Bool             svg : 1; // is the img a svg
   Eina_Bool             newfile : 1; // did we just set a new file
   Eina_Bool             animated : 1; // is this animated?
   Eina_Bool             vid_stream : 1; // is this video (has video frames?)
   Eina_Bool             audio : 1; // is this audio (has audo)
   Eina_Bool             mono_thumb : 1; // is thumb of mono white alpha img
};

static Evas_Smart       *_smart = NULL;
static Evas_Smart_Class  _sc = EVAS_SMART_CLASS_INIT_NULL;
static Evas_Smart_Class  _sc_parent = EVAS_SMART_CLASS_INIT_NULL;

static void      _image_add(Smart_Data *sd);
static int       _size_choose(Smart_Data *sd, const int *sizes);
static void      _cb_wakeup(void *data);
static void      _wakeup(Smart_Data *sd);
static void      _image_file_set(Smart_Data *sd);
static void      _image_thumb_set(Smart_Data *sd);
static void      _image_resized(Smart_Data *sd);
static Eina_Bool _cb_settle_timer(void *data);
static void      _cb_image_preload(void *data, Evas *e EINA_UNUSED, Evas_Object *obj EINA_UNUSED, void *event_info EINA_UNUSED);
static void      _handle_frame(Smart_Data *sd);

// sizes stored in thumbnail files that we might want to look at
static const int _thumb_sizes[] = { 512, 256, 128, 64, 32, 16, 0 };
// sizes we might want to load/render svg's at
static const int _svg_sizes[] = { 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 0 };

#define ENTRY Smart_Data *sd = evas_object_smart_data_get(obj); if (!sd) return

static void
_image_add(Smart_Data *sd)
{ // add a new hidden image object (image2) that will be busy loading
   Evas_Object *o;

   if (sd->o_image2) evas_object_del(sd->o_image2);
   sd->o_image2 = o = evas_object_image_filled_add
     (evas_object_evas_get(sd->o_smart));
   evas_object_image_scale_hint_set(o, EVAS_IMAGE_SCALE_HINT_STATIC);
   evas_object_smart_member_add(o, sd->o_smart); // this is a member
   evas_object_image_load_head_skip_set(o, EINA_TRUE); // fileset no load head
   // when the image is fully loaded then call this callback so its all
   // ready and no more i/o is happening (this happens in a thread)
   evas_object_event_callback_add(o, EVAS_CALLBACK_IMAGE_PRELOADED,
                                  _cb_image_preload, sd);
}

static int
_size_choose(Smart_Data *sd, const int *sizes)
{ // find the next size up we want given the current object size
   int i, max_size = 0;

   // use the biggest axis as our desired size
   if (sd->geom.w > sd->geom.h) max_size = sd->geom.w;
   else max_size = sd->geom.h;
   for (i = 0; sizes[i] > 0; i++)
     {
        if (i > 0)
          { // if we're not the first item in the sizes
             // if size is bigger than this slot - return the size up
             if (max_size > sizes[i]) return sizes[i - 1];
          }
        else
          { // we're after the first item in our sizes so use biggest
             if (max_size > sizes[i]) return sizes[i];
          }
     }
   return sizes[i - 1]; // no size found - choose smallest
}

static void
_cb_wakeup(void *data)
{ // we woke up - mark obj as changed....
   Smart_Data *sd = data;
   sd->wakeup = NULL;
   evas_object_smart_changed(sd->o_smart);
}

static void
_wakeup(Smart_Data *sd)
{ // wake up the main loop so we spin around and do things
   if (sd->wakeup) ecore_job_del(sd->wakeup);
   sd->wakeup = ecore_job_add(_cb_wakeup, sd);
}

static void
_image_file_set_final(Smart_Data *sd, const char *file, const char *group)
{ // complete image file load/setup, gtrigger preload etc.
   const char *ext;

   // skip head will make animated not work - so skip it if format might
   // be animated so we can flip frames
   ext = strrchr(file, '.');
   if (ext && ((!strcasecmp(ext, ".gif")) ||
               (!strcasecmp(ext, ".webp")) ||
               (!strcasecmp(ext, ".avif")) ||
               (!strcasecmp(ext, ".avifs")) ||
               (!strcasecmp(ext, ".jxl"))))
     evas_object_image_load_head_skip_set(sd->o_image2, EINA_FALSE);
   evas_object_image_file_set(sd->o_image2, file, group);
   evas_object_image_preload(sd->o_image2, EINA_FALSE);
   evas_object_smart_changed(sd->o_smart);
   _wakeup(sd);
}

static void
_image_file_set(Smart_Data *sd)
{ // set a generic target image file
   int load_size = _size_choose(sd, _svg_sizes);

   if (sd->o_video)
     { // nuke previous video object if there
        evas_object_del(sd->o_video);
        sd->o_video = NULL;
     }
   if (sd->svg)
     { // if it's a svg file...
        if (!sd->settle_timer)
          { // if we dont have a pending settle timer anymore use an EXACT sz
             if (sd->geom.w > sd->geom.h) load_size = sd->geom.w;
             else load_size = sd->geom.h;
         }
     }
   // we already have this thumbnail size so no point loading it again
   if (load_size == sd->load_size) return;
   _image_add(sd);
   sd->load_size = load_size;
   if (sd->svg)
     { // it's a svg so tell the svg loader to render at this size
        if (sd->load_size == 0) return;
        evas_object_image_load_size_set(sd->o_image2,
                                        sd->load_size, sd->load_size);
     }
   _image_file_set_final(sd, sd->file, NULL);
}

static void
_image_thumb_set(Smart_Data *sd)
{ // set a thumb file from our stored thumbnails
   char buf[64];
   int load_size = _size_choose(sd, _thumb_sizes);

   // we already have this thumbnail size so no point loading it again
   if (load_size == sd->load_size) return;
   _image_add(sd);
   if (sd->newfile)
     {
        Eet_File *ef = eet_open(sd->thumb, EET_FILE_MODE_READ);
        if (ef)
          {
             int size;
             char *mono = eet_read(ef, "image/thumb/mono", &size);

             if (mono)
               {
                  sd->mono_thumb = *mono;
                  free(mono);
               }
             eet_close(ef);
          }
     }
   sd->load_size = load_size;
   snprintf(buf, sizeof(buf), "image/thumb/%i", sd->load_size);
   _image_file_set_final(sd, sd->thumb, buf);
}

static void
_cb_vid_frame(void *data, Evas_Object *obj EINA_UNUSED, void *event EINA_UNUSED)
{ // we decoded a frame - make sure vide3o is shown now we have a frame
   Smart_Data *sd = data;

   evas_object_show(sd->o_video);
}

static void
_cb_vid_resize(void *data, Evas_Object *obj EINA_UNUSED, void *event EINA_UNUSED)
{ // the video changed video size - tell owner we loaded or resized
   Smart_Data *sd = data;

   emotion_object_size_get(sd->o_video, &(sd->orig_w), &(sd->orig_h));
   sd->video_ratio = emotion_object_ratio_get(sd->o_video);
   if (sd->newfile)
     { // it's a new file, so say we loaded as its the first time
        evas_object_smart_callback_call(sd->o_smart, "loaded", NULL);
        sd->newfile = EINA_FALSE;
     }
   else // already playing the file - not new, si resized
     evas_object_smart_callback_call(sd->o_smart, "resized", NULL);
}

static void
_cb_vid_open_done(void *data, Evas_Object *obj EINA_UNUSED, void *event EINA_UNUSED)
{ // we finished opening - now go to 0 and play
   Smart_Data *sd = data;

   emotion_object_position_set(sd->o_video, 0.0);
   emotion_object_play_set(sd->o_video, EINA_TRUE);
}

static void
_cb_vid_play_finish(void *data, Evas_Object *obj EINA_UNUSED, void *event EINA_UNUSED)
{ // we finished playing go back to 0 and restart so we loop
   Smart_Data *sd = data;

   emotion_object_play_set(sd->o_video, EINA_FALSE);
   emotion_object_position_set(sd->o_video, 0.0);
   emotion_object_play_set(sd->o_video, EINA_TRUE);
}

static void
_image_video_set(Smart_Data *sd)
{ // set a video file
   static Eina_Bool emotion_initted = EINA_FALSE;
   Evas_Object *o;

   if (sd->o_image)
     { // nuke previous image object if there
        evas_object_del(sd->o_image);
        sd->o_image = NULL;
     }
   if (sd->o_image2)
     { // nuke previous image object if there
        evas_object_del(sd->o_image2);
        sd->o_image2 = NULL;
     }
   if (sd->o_video)
     { // nuke previous video object if there
        evas_object_del(sd->o_video);
        sd->o_video = NULL;
     }

   if (!emotion_initted)
     { // emotion needs an init - so init it first time only
        emotion_init();
        emotion_initted = EINA_TRUE;
     }
   // XXX: sometimes this stalls because gstreamer is updating registry data
   // in gstreamer_module_init -> gst_init_check -> g_option_context_parse
   //   -> gst_update_registry -> gst_poll_wait
   sd->o_video = o = emotion_object_add(evas_object_evas_get(sd->o_smart));
   emotion_object_keep_aspect_set(o, EMOTION_ASPECT_KEEP_NONE);
   evas_object_smart_member_add(o, sd->o_smart); // this is a member
   evas_object_smart_callback_add(o, "frame_decode", _cb_vid_frame, sd);
   evas_object_smart_callback_add(o, "frame_resize", _cb_vid_resize, sd);
   evas_object_smart_callback_add(o, "open_done", _cb_vid_open_done, sd);
   evas_object_smart_callback_add(o, "playback_finished", _cb_vid_play_finish, sd);
// other callbacks - we don't need these for now
//   evas_object_smart_callback_add(o, "decode_stop", _cb_vid_stop, obj);
//   evas_object_smart_callback_add(o, "progress_change", _cb_vid_progress, obj);
//   evas_object_smart_callback_add(o, "position_update", _cb_position_update, obj);
//   evas_object_smart_callback_add(o, "length_change", _cb_length_change, obj);
//   evas_object_smart_callback_add(o, "title_change", _cb_title_change, obj);
//   evas_object_smart_callback_add(o, "audio_level_change", _cb_audio_change, obj);
//   evas_object_smart_callback_add(o, "playback_started", _cb_play_start, obj);
   emotion_object_file_set(o, sd->video);
   emotion_object_audio_mute_set(o, EINA_TRUE);
   emotion_object_audio_volume_set(o, 0.0);
}

static void
_image_resized(Smart_Data *sd)
{ // the image was just resized to handle that settling "idle" timer
   if (sd->svg)
     { // it's an svg - so set up a timer so if we dont resize again for 0.2s
        if (sd->settle_timer) ecore_timer_reset(sd->settle_timer);
        else sd->settle_timer = ecore_timer_add(0.2, _cb_settle_timer, sd);
     }
   else
     { // not a svg - nuke settle timer from space - the only way to be sure
        if (sd->settle_timer)
          {
             ecore_timer_del(sd->settle_timer);
             sd->settle_timer = NULL;
          }
     }
}

static void
_image_update(Smart_Data *sd)
{ // update the image - perhaps size changed...
   if      (sd->thumb) _image_thumb_set(sd);
   else if (sd->file)  _image_file_set(sd);
}

static Eina_Bool
_cb_settle_timer(void *data)
{ // when the image settles and stops being resized after a bit...
   Smart_Data *sd = data;

   sd->settle_timer = NULL;
   _image_update(sd);
   return EINA_FALSE;
}

static int
_frame_num_get(Smart_Data *sd)
{
   int fr, fr2;

   // ping pong - first and last frame only appear once at each end
   if (sd->loop_type == EVAS_IMAGE_ANIMATED_HINT_PINGPONG)
     {
        fr = sd->frame % ((sd->frame_count * 2) - 2);
        fr2 = sd->frame % sd->frame_count + 1;
        if (fr >= sd->frame_count) fr = sd->frame_count - 1 - fr2;
     }
   // loop 
   else fr = sd->frame % sd->frame_count;
   return fr;
}

static Eina_Bool
_cb_anim_timer(void *data)
{
   Smart_Data *sd = data;
   int fr;

   sd->anim_timer = NULL;
   sd->frame++;
   fr = _frame_num_get(sd);
   printf("T: %1.3f %i / %i\n", ecore_time_get(), fr, sd->frame_count);
   evas_object_image_animated_frame_set(sd->o_image, fr);
   _handle_frame(sd);
   return EINA_FALSE;
}

static void
_handle_frame(Smart_Data *sd)
{
   int fr;
   double t;

   fr = _frame_num_get(sd);
   t = evas_object_image_animated_frame_duration_get(sd->o_image, fr, 0);
   if (sd->anim_timer) ecore_timer_del(sd->anim_timer);
   sd->anim_timer = ecore_timer_add(t, _cb_anim_timer, sd);
}

static void
_cb_image_preload(void *data, Evas *e EINA_UNUSED, Evas_Object *obj EINA_UNUSED, void *event_info EINA_UNUSED)
{ // whenb an image that was being loaded in the background has now finished
   Smart_Data *sd = data;

   if (sd->o_image) evas_object_del(sd->o_image);
   sd->o_image = sd->o_image2;
   if (!sd->o_image2) return;
   sd->o_image2 = NULL;
   evas_object_image_size_get(sd->o_image, &(sd->orig_w), &(sd->orig_h));
   sd->alpha = evas_object_image_alpha_get(sd->o_image);
   evas_object_show(sd->o_image);
   if (sd->newfile)
     {
        sd->newfile = EINA_FALSE;
        evas_object_smart_callback_call(sd->o_smart, "loaded", NULL);
     }
   sd->animated = evas_object_image_animated_get(sd->o_image);
   if (sd->animated)
     {
        sd->loop_type = evas_object_image_animated_loop_type_get(sd->o_image);
        sd->frame_count = evas_object_image_animated_frame_count_get(sd->o_image);
        if (sd->frame_count < 3) sd->loop_type = EVAS_IMAGE_ANIMATED_HINT_LOOP;
        else if (sd->frame_count < 2) sd->animated = EINA_FALSE;
        if (sd->animated) _handle_frame(sd);
     }
}

// gui code
static void
_smart_add(Evas_Object *obj)
{ // create a new efm icon
   Smart_Data *sd;

   sd = calloc(1, sizeof(Smart_Data));
   if (!sd) return;
   evas_object_smart_data_set(obj, sd);

   _sc_parent.add(obj);

   sd->o_smart = obj;
}

static void
_smart_del(Evas_Object *obj)
{ // delete/free efm view
   ENTRY;

   if (sd->o_image)
     {
        evas_object_del(sd->o_image);
        sd->o_image = NULL;
     }
   if (sd->o_image2)
     {
        evas_object_del(sd->o_image2);
        sd->o_image2 = NULL;
     }
   if (sd->o_video)
     {
        evas_object_del(sd->o_video);
        sd->o_video = NULL;
     }
   if (sd->settle_timer)
     {
        ecore_timer_del(sd->settle_timer);
        sd->settle_timer = NULL;
     }
   if (sd->wakeup)
     {
        ecore_job_del(sd->wakeup);
        sd->wakeup = NULL;
     }
   if (sd->anim_timer)
     {
        ecore_timer_del(sd->anim_timer);
        sd->anim_timer = NULL;
     }
   eina_stringshare_replace(&(sd->thumb), NULL);
   eina_stringshare_replace(&(sd->file), NULL);
   eina_stringshare_replace(&(sd->video), NULL);

   _sc_parent.del(obj);
   evas_object_smart_data_set(obj, NULL);
}

static void
_smart_move(Evas_Object *obj, Evas_Coord x, Evas_Coord y)
{ // efm icon object moved
   ENTRY;

   if ((sd->geom.x == x) && (sd->geom.y == y)) return;
   sd->geom.x = x;
   sd->geom.y = y;
   evas_object_smart_changed(obj);
}

static void
_smart_resize(Evas_Object *obj, Evas_Coord w, Evas_Coord h)
{ // efm icon object resized
   ENTRY;

   if ((sd->geom.w == w) && (sd->geom.h == h)) return;
   sd->geom.w = w;
   sd->geom.h = h;
   _image_resized(sd);
   _image_update(sd);
   evas_object_smart_changed(obj);
}

static void
_smart_calculate(Evas_Object *obj)
{ // recalce position/size
   ENTRY;

   if (sd->o_image)
     evas_object_geometry_set(sd->o_image,
                              sd->geom.x, sd->geom.y,
                              sd->geom.w, sd->geom.h);
   if (sd->o_image2)
     evas_object_geometry_set(sd->o_image2,
                              sd->geom.x, sd->geom.y,
                              sd->geom.w, sd->geom.h);
   if (sd->o_video)
     evas_object_geometry_set(sd->o_video,
                              sd->geom.x, sd->geom.y,
                              sd->geom.w, sd->geom.h);
   _image_update(sd);
}

Evas_Object *
efm_icon_add(Evas_Object *parent)
{ // add new icon object
   if (!_smart)
     {
        evas_object_smart_clipped_smart_set(&_sc_parent);
        _sc           = _sc_parent;
        _sc.name      = "efm_icon";
        _sc.version   = EVAS_SMART_CLASS_VERSION;
        _sc.add       = _smart_add;
        _sc.del       = _smart_del;
        _sc.resize    = _smart_resize;
        _sc.move      = _smart_move;
        _sc.calculate = _smart_calculate;
     };
   if (!_smart) _smart = evas_smart_class_new(&_sc);
   return evas_object_smart_add(evas_object_evas_get(parent), _smart);
}

void
efm_icon_file_set(Evas_Object *obj, const char *file)
{ // set a regular file as the icon
   ENTRY;

   if ((sd->file) && (file) && (!strcmp(sd->file, file))) return;
   if ((!sd->file) && (!file)) return;
   eina_stringshare_replace(&(sd->thumb), NULL);
   eina_stringshare_replace(&(sd->file), file);
   eina_stringshare_replace(&(sd->video), NULL);
   sd->svg = EINA_FALSE;
   if ((sd->file) &&
       ((eina_fnmatch("*.svg", sd->file, EINA_FNMATCH_CASEFOLD) ||
         eina_fnmatch("*.svgz", sd->file, EINA_FNMATCH_CASEFOLD) ||
         eina_fnmatch("*.svg.gz", sd->file, EINA_FNMATCH_CASEFOLD))))
     sd->svg = EINA_TRUE;
   sd->load_size = -1;
   sd->newfile = EINA_TRUE;
   sd->mono_thumb = EINA_FALSE;
   sd->frame = 0;
   if (sd->anim_timer)
     {
        ecore_timer_del(sd->anim_timer);
        sd->anim_timer = NULL;
     }
   if (sd->settle_timer)
     { // thumbnails dont need settle timers like svg's might
        ecore_timer_del(sd->settle_timer);
        sd->settle_timer = NULL;
     }
   _image_resized(sd);
   _image_file_set(sd);
}

void
efm_icon_thumb_set(Evas_Object *obj, const char *thumb)
{ // specifically add generated thumb file
   ENTRY;

   if ((sd->thumb) && (thumb) && (!strcmp(sd->thumb, thumb))) return;
   if ((!sd->thumb) && (!thumb)) return;
   eina_stringshare_replace(&(sd->file), NULL);
   eina_stringshare_replace(&(sd->thumb), thumb);
   eina_stringshare_replace(&(sd->video), NULL);
   sd->svg = EINA_FALSE;
   sd->load_size = -1;
   sd->newfile = EINA_TRUE;
   sd->mono_thumb = EINA_FALSE;
   sd->frame = 0;
   if (sd->anim_timer)
     {
        ecore_timer_del(sd->anim_timer);
        sd->anim_timer = NULL;
     }
   if (sd->settle_timer)
     { // thumbnails dont need settle timers like svg's might
        ecore_timer_del(sd->settle_timer);
        sd->settle_timer = NULL;
     }
   _image_thumb_set(sd);
}

void
efm_icon_video_set(Evas_Object *obj, const char *video)
{ // specifically load file as a video (or audio) and not as a still image
   ENTRY;

   if ((sd->video) && (video) && (!strcmp(sd->video, video))) return;
   if ((!sd->video) && (!video)) return;
   eina_stringshare_replace(&(sd->file), NULL);
   eina_stringshare_replace(&(sd->thumb), NULL);
   eina_stringshare_replace(&(sd->video), video);
   sd->svg = EINA_FALSE;
   sd->load_size = -1;
   sd->newfile = EINA_TRUE;
   sd->mono_thumb = EINA_FALSE;
   sd->frame = 0;
   sd->video_ratio = 0.0;
   if (sd->anim_timer)
     {
        ecore_timer_del(sd->anim_timer);
        sd->anim_timer = NULL;
     }
   if (sd->settle_timer)
     { // thumbnails dont need settle timers like svg's might
        ecore_timer_del(sd->settle_timer);
        sd->settle_timer = NULL;
     }
   _image_resized(sd);
   _image_video_set(sd);
}

void
efm_icon_size_get(Evas_Object *obj, int *w, int *h)
{ // get image pixel size
   ENTRY;

   if ((sd->o_video) && (sd->video_ratio > 0.0))
     {
        int vid_w, vid_h;

        vid_w = ((double)sd->orig_h + 0.5) * sd->video_ratio;
        vid_h = sd->orig_h;
        if (w) *w = vid_w;
        if (h) *h = vid_h;
     }
   else
     {
        if (w) *w = sd->orig_w;
        if (h) *h = sd->orig_h;
     }
}

Eina_Bool
efm_icon_alpha_get(Evas_Object *obj)
{ // get image alpha flag
   ENTRY EINA_FALSE;

   return sd->alpha;
}

Eina_Bool
efm_icon_mono_get(Evas_Object *obj)
{ // get thumb mono flag
   ENTRY EINA_FALSE;

   return sd->mono_thumb;
}
