/* gcal-agenda-view.c
 *
 * Copyright (C) 2022 Purism SPC
 *
 * This program 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 Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
 */

#define G_LOG_DOMAIN "GcalAgendaView"

#include "gcal-agenda-view.h"
#include "gcal-debug.h"
#include "gcal-enums.h"
#include "gcal-event-widget.h"
#include "gcal-range-tree.h"
#include "gcal-timeline-subscriber.h"
#include "gcal-utils.h"
#include "gcal-view.h"

#include <adwaita.h>
#include <glib/gi18n.h>

#include <math.h>

typedef struct
{
  GtkWidget          *widget;
  GcalEvent          *event;
  GcalAgendaView     *self;
} ChildData;

struct _GcalAgendaView
{
  GtkBox              parent;

  GtkWidget          *list_box;
  GtkWidget          *no_events_row;
  GtkWidget          *scrolled_window;

  /* property */
  GDateTime          *date;
  GcalContext        *context;

  GcalRangeTree      *events;
  guint               scroll_grid_timeout_id;
  gulong              stack_page_changed_id;

  gint                events_on_date;
  gint                clicked_cell;
};

static void          schedule_position_scroll                    (GcalAgendaView       *self);

static void          gcal_view_interface_init                    (GcalViewInterface  *iface);

static void          gcal_timeline_subscriber_interface_init     (GcalTimelineSubscriberInterface *iface);

enum
{
  PROP_0,
  PROP_DATE,
  PROP_CONTEXT,
  N_PROPS,
};


G_DEFINE_TYPE_WITH_CODE (GcalAgendaView, gcal_agenda_view, GTK_TYPE_BOX,
                         G_IMPLEMENT_INTERFACE (GCAL_TYPE_VIEW, gcal_view_interface_init)
                         G_IMPLEMENT_INTERFACE (GCAL_TYPE_TIMELINE_SUBSCRIBER,
                                                gcal_timeline_subscriber_interface_init));

/*
 * Auxiliary methods
 */

static ChildData*
child_data_new (GtkWidget      *widget,
                GcalEvent      *event,
                GcalAgendaView *self)
{
  ChildData *data;

  data = g_new (ChildData, 1);
  data->widget = widget;
  data->event = g_object_ref (event);
  data->self = self;

  return data;
}

static void
child_data_free (gpointer data)
{
  ChildData *child_data = data;

  if (!child_data)
    return;

  if (child_data->widget && child_data->self)
    {
      gtk_list_box_remove (GTK_LIST_BOX (child_data->self->list_box), child_data->widget);
      child_data->widget = NULL;
      child_data->self = NULL;
    }
  g_clear_object (&child_data->event);
  g_free (child_data);
}

static GDateTime *
get_start_date_for_row (GcalAgendaView *self,
                        GtkListBoxRow  *row)
{
  GtkWidget *widget = gtk_list_box_row_get_child (row);

  if (GCAL_IS_EVENT_WIDGET (widget))
    {
      GDateTime *date = gcal_event_widget_get_date_start (GCAL_EVENT_WIDGET (widget));

      /* Pretend events start at least on the selected date */
      if (gcal_date_time_compare_date (date, self->date) > 0)
        return date;
      else
        return self->date;
    }


  return self->date;
}

static GDateTime *
get_end_date_for_row (GcalAgendaView *self,
                      GtkListBoxRow  *row)
{
  GtkWidget *widget = gtk_list_box_row_get_child (row);

  if (GCAL_IS_EVENT_WIDGET (widget))
    return gcal_event_widget_get_date_end (GCAL_EVENT_WIDGET (widget));

  return self->date;
}

static gboolean
get_row_is_multiday (GtkListBoxRow *row)
{
  GtkWidget *widget = gtk_list_box_row_get_child (row);

  if (GCAL_IS_EVENT_WIDGET (widget))
    {
      GcalEvent *event = gcal_event_widget_get_event (GCAL_EVENT_WIDGET (widget));

      return gcal_event_is_multiday (event);
    }

  return FALSE;
}

static gchar *
new_date_header_string (GDateTime *date)
{
  g_autoptr (GDateTime) today = NULL;
  g_autoptr (GDateTime) tomorrow = NULL;
  g_autoptr (GDateTime) yesterday = NULL;

  if (date == NULL)
    return NULL;

  today = g_date_time_new_now_local ();
  tomorrow = g_date_time_add_days (today, 1);
  yesterday = g_date_time_add_days (today, -1);

  if (gcal_date_time_compare_date (date, today) == 0)
    return g_strdup ("Today");
  else if (gcal_date_time_compare_date (date, tomorrow) == 0)
    return g_strdup ("Tomorrow");
  else if (gcal_date_time_compare_date (date, yesterday) == 0)
    return g_strdup ("Yesterday");
  else
    return g_date_time_format (date, "%A %B %e");
}


/*
 * Callbacks
 */

static void
on_event_activated (GcalAgendaView *self,
                    GtkWidget      *widget)
{
  g_signal_emit_by_name (self, "event-activated", widget);
}

static void
stack_visible_child_changed_cb (AdwViewStack   *stack,
                                GParamSpec     *pspec,
                                GcalAgendaView *self)
{
  if (adw_view_stack_get_visible_child (stack) != (GtkWidget*) self)
    return;

  schedule_position_scroll (self);

  g_clear_signal_handler (&self->stack_page_changed_id, stack);
}

/* Auxiliary methods */
static void
update_no_events_row (GcalAgendaView *self)
{
  gboolean visible = !self->events_on_date;
  gboolean was_visible = gtk_widget_get_visible (self->no_events_row);

  if (visible != was_visible)
    {
      gtk_widget_set_visible (self->no_events_row, visible);
      gtk_list_box_invalidate_headers (GTK_LIST_BOX (self->list_box));
    }
}

static gboolean
update_grid_scroll_position (GcalAgendaView *self)
{
  g_autoptr(GDateTime) week_start = NULL;
  g_autoptr(GDateTime) week_end = NULL;
  g_autoptr(GDateTime) now = NULL;
  GtkAdjustment *vadjustment;
  gdouble minutes, real_value;
  gdouble max, page, page_increment, value;
  gboolean dummy;

  /* While the scrolled window is not mapped, we keep waiting */
  if (!gtk_widget_get_realized (self->scrolled_window) ||
      !gtk_widget_get_mapped (self->scrolled_window))
    {
      if (self->stack_page_changed_id == 0)
        {
          GtkWidget *stack = gtk_widget_get_ancestor (GTK_WIDGET (self), ADW_TYPE_VIEW_STACK);

          self->stack_page_changed_id = g_signal_connect_object (stack,
                                                                 "notify::visible-child",
                                                                 G_CALLBACK (stack_visible_child_changed_cb),
                                                                 self,
                                                                 0);
        }

      self->scroll_grid_timeout_id = 0;

      GCAL_RETURN (G_SOURCE_REMOVE);
    }

  now = g_date_time_new_now_local ();
  week_start = gcal_date_time_get_start_of_week (self->date);
  week_end = gcal_date_time_get_end_of_week (self->date);

  /* Don't animate when not today */
  if (gcal_date_time_compare_date (now, week_start) < 0 || gcal_date_time_compare_date (now, week_end) >= 0)
    GCAL_GOTO (out);

  vadjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolled_window));
  minutes = g_date_time_get_hour (now) * 60 + g_date_time_get_minute (now);
  page = gtk_adjustment_get_page_size (vadjustment);
  max = gtk_adjustment_get_upper (vadjustment);

  real_value = max / MINUTES_PER_DAY * minutes - (page / 2.0);
  page_increment = gtk_adjustment_get_page_increment (vadjustment);
  value = gtk_adjustment_get_value (vadjustment);

  gtk_adjustment_set_page_increment (vadjustment, real_value - value);

  g_signal_emit_by_name (self->scrolled_window,
                         "scroll-child",
                         GTK_SCROLL_PAGE_FORWARD,
                         FALSE,
                         &dummy);

  gtk_adjustment_set_page_increment (vadjustment, page_increment);

out:
  self->scroll_grid_timeout_id = 0;
  GCAL_RETURN (G_SOURCE_REMOVE);
}

static void
schedule_position_scroll (GcalAgendaView *self)
{
  /* Nothing is scheduled, we should do it now */
  if (self->scroll_grid_timeout_id > 0)
    g_source_remove (self->scroll_grid_timeout_id);

  self->scroll_grid_timeout_id = g_timeout_add (200,
                                                (GSourceFunc) update_grid_scroll_position,
                                                self);
}

static int
sort_func (GtkListBoxRow *row1,
           GtkListBoxRow *row2,
           gpointer       user_data)
{
  GcalAgendaView *self = GCAL_AGENDA_VIEW (user_data);
  gboolean multiday1, multiday2;
  int result;

  result = gcal_date_time_compare_date (get_start_date_for_row (self, row1),
                                        get_start_date_for_row (self, row2));

  if (result != 0)
    return result;

  multiday1 = get_row_is_multiday (row1);
  multiday2 = get_row_is_multiday (row2);

  /* Sort multi day events before multi single day events of the same day. */
  if (multiday1 != multiday2)
    return multiday1 ? -1 : 1;

  return gcal_date_time_compare_date (get_end_date_for_row (self, row1),
                                      get_end_date_for_row (self, row2));
}

static void
update_header (GtkListBoxRow *row,
               GtkListBoxRow *before,
               gpointer       user_data)
{
  GcalAgendaView *self = GCAL_AGENDA_VIEW (user_data);
  GDateTime *start = get_start_date_for_row (self, row);
  GDateTime *end = get_end_date_for_row (self, row);
  g_autofree gchar *label = NULL;
  GtkWidget *header;
  g_autoptr (GDateTime) today = NULL;

  today = g_date_time_new_now_local ();

  if (before)
    {
      GDateTime *before_start = get_start_date_for_row (self, before);
      GDateTime *before_end = get_end_date_for_row (self, before);

      if (get_row_is_multiday (row) &&
          get_row_is_multiday (before) &&
          gcal_date_time_compare_date (start, before_start) == 0 &&
          gcal_date_time_compare_date (start, today) == 0)
        {
          gtk_list_box_row_set_header (row, NULL);
          return;
        }

      if (gcal_date_time_compare_date (start, before_start) == 0 &&
          gcal_date_time_compare_date (end, before_end) == 0)
        {
          gtk_list_box_row_set_header (row, NULL);
          return;
        }
    }

  if (get_row_is_multiday (row))
    {
      g_autofree gchar *start_label = NULL;
      g_autofree gchar *end_label = NULL;
      if (gcal_date_time_compare_date (start, today) == 0)
        {
          label = g_strdup ("On-going");
        }
      else
        {
          start_label = new_date_header_string (start);
          end_label = new_date_header_string (end);

          label = g_strdup_printf ("%s – %s", start_label, end_label);
        }
    }
  else
    {
      label = new_date_header_string (start);
    }

  header = g_object_new (GTK_TYPE_LABEL,
                         "label", label,
                         "xalign", 0,
                         NULL);
  gtk_widget_add_css_class (header, "caption-heading");

  gtk_list_box_row_set_header (row, header);
}


/*
 * GcalView interface
 */

static GDateTime*
gcal_agenda_view_get_date (GcalView *view)
{
  GcalAgendaView *self = GCAL_AGENDA_VIEW (view);

  return self->date;
}

static inline gboolean
count_events_on_data (GcalRange *range,
                      gpointer   data,
                      gpointer   user_data)
{
  ChildData *child_data = data;
  GcalAgendaView *self = user_data;

  if (gcal_date_time_compare_date (self->date, gcal_event_get_date_start (child_data->event)) == 0)
    self->events_on_date++;

  return FALSE;
}

static void
gcal_agenda_view_set_date (GcalView  *view,
                           GDateTime *date)
{
  GcalAgendaView *self = GCAL_AGENDA_VIEW (view);

  GCAL_ENTRY;

  gcal_set_date_time (&self->date, date);

  schedule_position_scroll (self);

  self->events_on_date = 0;
  gcal_range_tree_traverse (self->events, G_IN_ORDER, count_events_on_data, self);
  update_no_events_row (self);

  gcal_timeline_subscriber_range_changed (GCAL_TIMELINE_SUBSCRIBER (view));

  GCAL_EXIT;
}

static GList*
gcal_agenda_view_get_children_by_uuid (GcalView              *view,
                                       GcalRecurrenceModType  mod,
                                       const gchar           *uuid)
{
  GCAL_ENTRY;

  /* FIXME Not sure what to do here. */

  GCAL_RETURN (NULL);
}

static void
gcal_agenda_view_clear_marks (GcalView *view)
{
  /* FIXME Not sure what to do here. */
}

static GDateTime*
gcal_agenda_view_get_next_date (GcalView *view)
{
  GcalAgendaView *self = GCAL_AGENDA_VIEW (view);

  g_assert (self->date != NULL);
  return g_date_time_add_weeks (self->date, 1);
}


static GDateTime*
gcal_agenda_view_get_previous_date (GcalView *view)
{
  GcalAgendaView *self = GCAL_AGENDA_VIEW (view);

  g_assert (self->date != NULL);
  return g_date_time_add_weeks (self->date, -1);
}

static void
gcal_view_interface_init (GcalViewInterface *iface)
{
  iface->get_date = gcal_agenda_view_get_date;
  iface->set_date = gcal_agenda_view_set_date;
  iface->get_children_by_uuid = gcal_agenda_view_get_children_by_uuid;
  iface->clear_marks = gcal_agenda_view_clear_marks;
  iface->get_next_date = gcal_agenda_view_get_next_date;
  iface->get_previous_date = gcal_agenda_view_get_previous_date;
}


/*
 * GcalTimelineSubscriber iface
 */

static GcalRange*
gcal_agenda_view_get_range (GcalTimelineSubscriber *subscriber)
{
  GcalAgendaView *self = GCAL_AGENDA_VIEW (subscriber);

  return gcal_range_new_take (g_date_time_ref (self->date),
                              gcal_date_time_get_end_of_week (self->date),
                              GCAL_RANGE_DEFAULT);
}

static void
gcal_agenda_view_add_event (GcalTimelineSubscriber *subscriber,
                            GcalEvent              *event)
{
  GcalAgendaView *self = GCAL_AGENDA_VIEW (subscriber);
  GtkWidget *widget, *row;
  GcalTimestampPolicy timestamp_policy;

  GCAL_ENTRY;

  /* FIXME Check gcal_event_is_multiday (event) and gcal_event_get_all_day (event) */

  g_object_ref (event);

  if (gcal_date_time_compare_date (self->date, gcal_event_get_date_start (event)) == 0)
    {
      self->events_on_date++;
      update_no_events_row (self);
    }

  /* Create and add the new event widget */
  if (gcal_event_get_all_day (event) || gcal_event_is_multiday (event))
    timestamp_policy = GCAL_TIMESTAMP_POLICY_END;
  else
    timestamp_policy = GCAL_TIMESTAMP_POLICY_START;

  widget = g_object_new (GCAL_TYPE_EVENT_WIDGET,
                         "context", self->context,
                         "event", event,
                         "orientation", GTK_ORIENTATION_VERTICAL,
                         "timestamp-policy", timestamp_policy,
                         NULL);

  row = g_object_new (GTK_TYPE_LIST_BOX_ROW,
                      "activatable", FALSE,
                      "child", widget,
                      NULL);

  gcal_range_tree_add_range (self->events,
                             gcal_event_get_range (event),
                             child_data_new (row, event, self));

  /* FIXME */
  /* g_signal_connect (widget, "activate", G_CALLBACK (on_event_widget_activated_cb), self); */

  gtk_list_box_append (GTK_LIST_BOX (self->list_box), row);

  gtk_list_box_invalidate_headers (GTK_LIST_BOX (self->list_box));

  GCAL_EXIT;
}

static void
gcal_agenda_view_remove_event (GcalTimelineSubscriber *subscriber,
                               GcalEvent              *event)
{
  GcalAgendaView *self = GCAL_AGENDA_VIEW (subscriber);
  const gchar *uid = gcal_event_get_uid (event);
  GPtrArray *widgets;
  guint i;

  GCAL_ENTRY;

  widgets = gcal_range_tree_get_all_data (self->events);

  if (gcal_date_time_compare_date (self->date, gcal_event_get_date_start (event)) == 0)
    {
      self->events_on_date--;
      update_no_events_row (self);
    }

  for (i = 0; widgets && i < widgets->len; i++)
    {
      ChildData *data;
      GtkWidget *widget;
      GcalEvent *event;

      data = g_ptr_array_index (widgets, i);
      widget = gtk_list_box_row_get_child (GTK_LIST_BOX_ROW (data->widget));
      event = gcal_event_widget_get_event (GCAL_EVENT_WIDGET (widget));

      if (g_strcmp0 (gcal_event_get_uid (event), uid) != 0)
        continue;

      gcal_range_tree_remove_range (self->events, gcal_event_get_range (event), data);
      gtk_widget_queue_allocate (GTK_WIDGET (self));
    }

  g_clear_pointer (&widgets, g_ptr_array_unref);

  gtk_list_box_invalidate_headers (GTK_LIST_BOX (self->list_box));

  GCAL_EXIT;
}

static void
gcal_agenda_view_update_event (GcalTimelineSubscriber *subscriber,
                               GcalEvent              *event)
{
  GCAL_ENTRY;

  gcal_agenda_view_remove_event (subscriber, event);
  gcal_agenda_view_add_event (subscriber, event);

  GCAL_EXIT;
}

static void
gcal_timeline_subscriber_interface_init (GcalTimelineSubscriberInterface *iface)
{
  iface->get_range = gcal_agenda_view_get_range;
  iface->add_event = gcal_agenda_view_add_event;
  iface->update_event = gcal_agenda_view_update_event;
  iface->remove_event = gcal_agenda_view_remove_event;
}


/*
 * GObject overrides
 */

static void
gcal_agenda_view_dispose (GObject *object)
{
  GcalAgendaView *self;

  self = GCAL_AGENDA_VIEW (object);

  g_clear_pointer (&self->events, gcal_range_tree_unref);

  /* Chain up to parent's dispose() method. */
  G_OBJECT_CLASS (gcal_agenda_view_parent_class)->dispose (object);
}

static void
gcal_agenda_view_finalize (GObject       *object)
{
  GcalAgendaView *self;

  self = GCAL_AGENDA_VIEW (object);

  g_clear_pointer (&self->date, g_date_time_unref);

  g_clear_object (&self->context);

  /* Chain up to parent's finalize() method. */
  G_OBJECT_CLASS (gcal_agenda_view_parent_class)->finalize (object);
}

static void
gcal_agenda_view_set_property (GObject      *object,
                               guint         property_id,
                               const GValue *value,
                               GParamSpec   *pspec)
{
  GcalAgendaView *self = (GcalAgendaView *) object;

  switch (property_id)
    {
    case PROP_DATE:
      gcal_view_set_date (GCAL_VIEW (object), g_value_get_boxed (value));
      break;

    case PROP_CONTEXT:
      g_assert (self->context == NULL);
      self->context = g_value_dup_object (value);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
gcal_agenda_view_get_property (GObject    *object,
                               guint       property_id,
                               GValue     *value,
                               GParamSpec *pspec)
{
  GcalAgendaView *self;

  g_return_if_fail (GCAL_IS_AGENDA_VIEW (object));
  self = GCAL_AGENDA_VIEW (object);

  switch (property_id)
    {
    case PROP_DATE:
      g_value_set_boxed (value, self->date);
      break;

    case PROP_CONTEXT:
      g_value_set_object (value, self->context);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
      break;
    }
}

static void
gcal_agenda_view_class_init (GcalAgendaViewClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  object_class->dispose = gcal_agenda_view_dispose;
  object_class->finalize = gcal_agenda_view_finalize;
  object_class->set_property = gcal_agenda_view_set_property;
  object_class->get_property = gcal_agenda_view_get_property;

  g_object_class_override_property (object_class, PROP_DATE, "active-date");
  g_object_class_override_property (object_class, PROP_CONTEXT, "context");

  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/views/gcal-agenda-view.ui");

  gtk_widget_class_bind_template_child (widget_class, GcalAgendaView, list_box);
  gtk_widget_class_bind_template_child (widget_class, GcalAgendaView, no_events_row);
  gtk_widget_class_bind_template_child (widget_class, GcalAgendaView, scrolled_window);

  gtk_widget_class_bind_template_callback (widget_class, on_event_activated);

  gtk_widget_class_set_css_name (widget_class, "agenda-view");
}

static void
gcal_agenda_view_init (GcalAgendaView *self)
{
  gtk_widget_init_template (GTK_WIDGET (self));

  self->events = gcal_range_tree_new_with_free_func (child_data_free);

  gtk_list_box_set_sort_func (GTK_LIST_BOX (self->list_box), sort_func, self, NULL);
  gtk_list_box_set_header_func (GTK_LIST_BOX (self->list_box), update_header, self, NULL);
}
