/* gcal-quick-add-popover.c
 *
 * Copyright (C) 2016-2020 Georges Basile Stavracas Neto <georges.stavracas@gmail.com>
 *
 * 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 "GcalQuickAddPopover"

#include <glib/gi18n.h>

#include "gcal-quick-add-popover.h"
#include "gcal-calendar-row.h"
#include "gcal-utils.h"

struct _GcalQuickAddPopover
{
  GtkPopover          parent;

  GtkWidget          *add_button;
  GtkWidget          *calendars_list_box;
  GtkWidget          *color_image;
  GtkWidget          *edit_button;
  GtkWidget          *summary_entry;
  GtkWidget          *title_label;

  /* Internal data */
  GcalRange          *range;

  GListModel         *read_write_calendars_model;
};

G_DEFINE_TYPE (GcalQuickAddPopover, gcal_quick_add_popover, GTK_TYPE_POPOVER)

enum {
  PROP_0,
  PROP_RANGE,
  N_PROPS
};

enum
{
  EDIT_EVENT,
  NUM_SIGNALS
};

static guint signals[NUM_SIGNALS] = { 0, };

/*
 * Utility functions
 */

static void
row_selected_cb (GtkListBox          *list_box,
                 GtkListBoxRow       *row,
                 GcalQuickAddPopover *self)
{
  GtkWidget *check_button;

  if (!row)
    return;

  check_button = gcal_calendar_row_get_first_suffix_child (GCAL_CALENDAR_ROW (row));

  gtk_check_button_set_active (GTK_CHECK_BUTTON (check_button), TRUE);
}

static void
emit_changed (GcalQuickAddPopover *self)
{
  GtkFilter *filter;

  filter = gtk_filter_list_model_get_filter (GTK_FILTER_LIST_MODEL (self->read_write_calendars_model));
  gtk_filter_changed (filter, GTK_FILTER_CHANGE_DIFFERENT);
}

static GtkWidget*
create_row_func (gpointer data,
                 gpointer user_data)
{
  GcalCalendar *default_calendar;
  GcalCalendar *calendar = data;
  GcalQuickAddPopover *self = user_data;
  GcalContext *context;
  GcalManager *manager;
  g_autoptr (GdkPaintable) paintable = NULL;
  g_autofree gchar *parent_name = NULL;
  g_autofree gchar *tooltip = NULL;
  GtkWidget *first_row, *row, *check_button;

  context = gcal_application_get_context (GCAL_DEFAULT_APPLICATION);
  manager = gcal_context_get_manager (context);

  default_calendar = gcal_manager_get_default_calendar (manager);

  /* The check button */
  check_button = gtk_check_button_new ();
  gtk_widget_set_valign (check_button, GTK_ALIGN_CENTER);
  gtk_widget_set_can_focus (check_button, FALSE);
  gtk_widget_set_can_target (check_button, FALSE);

  /* Turn these check buttons into radio buttons */
  first_row = gtk_widget_get_first_child (self->calendars_list_box);
  if (first_row)
    {
      GtkWidget *first_check_button;

      first_check_button = gcal_calendar_row_get_first_suffix_child (GCAL_CALENDAR_ROW (first_row));
      gtk_check_button_set_group (GTK_CHECK_BUTTON (check_button), GTK_CHECK_BUTTON (first_check_button));
    }

  row = gcal_calendar_row_new (calendar);
  gcal_calendar_row_add_header_class (GCAL_CALENDAR_ROW (row));
  gcal_calendar_row_add_suffix (GCAL_CALENDAR_ROW (row), check_button);

  if (calendar == default_calendar)
    gtk_list_box_select_row (GTK_LIST_BOX (self->calendars_list_box), GTK_LIST_BOX_ROW (row));

  g_signal_connect_object (calendar, "notify::visible", G_CALLBACK (emit_changed), self, G_CONNECT_SWAPPED);
  g_signal_connect_object (calendar, "notify::read-only", G_CALLBACK (emit_changed), self, G_CONNECT_SWAPPED);

  return row;
}

static void
bind_model (GcalQuickAddPopover *self)
{
  gtk_list_box_bind_model (GTK_LIST_BOX (self->calendars_list_box),
                           self->read_write_calendars_model,
                           create_row_func, self, NULL);
}

static gint
get_number_of_days_from_today (GDateTime *day)
{
  g_autoptr (GDateTime) now = NULL;
  GDate today, event_day;
  gint n_days;

  now = g_date_time_new_now_local ();

  g_date_set_dmy (&today,
                  g_date_time_get_day_of_month (now),
                  g_date_time_get_month (now),
                  g_date_time_get_year (now));

  g_date_set_dmy (&event_day,
                  g_date_time_get_day_of_month (day),
                  g_date_time_get_month (day),
                  g_date_time_get_year (day));
  n_days = g_date_days_between (&event_day, &today);

  return n_days;
}

static gchar*
get_date_string_for_multiday (GDateTime *start,
                              GDateTime *end)
{
  g_autofree gchar *start_date_str = NULL;
  g_autofree gchar *end_date_str = NULL;
  gchar *date_string;
  gint n_days;

  const gchar *start_date_weekdays_strings[] = {
    N_("from next Monday"),
    N_("from next Tuesday"),
    N_("from next Wednesday"),
    N_("from next Thursday"),
    N_("from next Friday"),
    N_("from next Saturday"),
    N_("from next Sunday"),
    NULL
  };

  const gchar *end_date_weekdays_strings[] = {
    N_("to next Monday"),
    N_("to next Tuesday"),
    N_("to next Wednesday"),
    N_("to next Thursday"),
    N_("to next Friday"),
    N_("to next Saturday"),
    N_("to next Sunday"),
    NULL
  };

  const gchar *month_names[] = {
    N_("January"),
    N_("February"),
    N_("March"),
    N_("April"),
    N_("May"),
    N_("June"),
    N_("July"),
    N_("August"),
    N_("September"),
    N_("October"),
    N_("November"),
    N_("December"),
    NULL
  };

  n_days = get_number_of_days_from_today (start);

  if (n_days == 0)
    {
      start_date_str = g_strdup_printf (_("from Today"));
    }
  else if (n_days == -1)
    {
      start_date_str = g_strdup_printf (_("from Tomorrow"));
    }
  else if (n_days == 1)
    {
      start_date_str = g_strdup_printf (_("from Yesterday"));
    }
  else if (n_days < -1 && n_days > -8)
    {
      const gchar *start_weekday_str;

      start_weekday_str = gettext (start_date_weekdays_strings[g_date_time_get_day_of_week (start) - 1]);
      start_date_str = g_strdup_printf ("%s", start_weekday_str);
    }
  else
    {
      g_autofree gchar *day_number_str = NULL;

      day_number_str = g_strdup_printf ("%u", g_date_time_get_day_of_month (start));
      /* Translators:
       * this is the format string for representing a date consisting of a month
       * name and a date of month.
       */
      start_date_str = g_strdup_printf (_("from %1$s %2$s"),
                                        gettext (month_names[g_date_time_get_month (start) - 1]),
                                        day_number_str);
    }

  n_days = get_number_of_days_from_today (end);

  if (n_days == 0)
    {
      end_date_str = g_strdup_printf (_("to Today"));
    }
  else if (n_days == -1)
    {
      end_date_str = g_strdup_printf (_("to Tomorrow"));
    }
  else if (n_days == 1)
    {
      end_date_str = g_strdup_printf (_("to Yesterday"));
    }
  else if (n_days < -1 && n_days > -8)
    {
      const gchar *end_weekday_str;

      end_weekday_str = gettext (end_date_weekdays_strings[g_date_time_get_day_of_week (end) - 1]);
      end_date_str = g_strdup_printf ("%s", end_weekday_str);
    }
  else
    {
      g_autofree gchar *day_number_str = NULL;

      day_number_str = g_strdup_printf ("%u", g_date_time_get_day_of_month (end));
      /* Translators:
       * this is the format string for representing a date consisting of a month
       * name and a date of month.
       */
      end_date_str = g_strdup_printf (_("to %1$s %2$s"),
                                      gettext (month_names[g_date_time_get_month (end) - 1]),
                                      day_number_str);
    }

  /* Translators: %1$s is the start date (e.g. "from Today") and %2$s is the end date (e.g. "to Tomorrow") */
  date_string = g_strdup_printf (_("New Event %1$s %2$s"),
                                 start_date_str,
                                 end_date_str);
  return date_string;
}

static gchar*
get_date_string_for_day (GDateTime *day)
{
  gchar *string_for_date;
  gint n_days;

  string_for_date = NULL;
  n_days = get_number_of_days_from_today (day);

  if (n_days == 0)
    {
      string_for_date = g_strdup_printf (_("New Event Today"));
    }
  else if (n_days == -1)
    {
      string_for_date = g_strdup_printf (_("New Event Tomorrow"));
    }
  else if (n_days == 1)
    {
      string_for_date = g_strdup_printf (_("New Event Yesterday"));
    }
  else if (n_days < -1 && n_days > -8)
    {
       const gchar *event_weekday;
       const gchar *event_weekday_names[] = {
         N_("New Event next Monday"),
         N_("New Event next Tuesday"),
         N_("New Event next Wednesday"),
         N_("New Event next Thursday"),
         N_("New Event next Friday"),
         N_("New Event next Saturday"),
         N_("New Event next Sunday"),
         NULL
       };

       event_weekday = gettext (event_weekday_names [g_date_time_get_day_of_week (day) - 1]);
       string_for_date = g_strdup_printf ("%s", event_weekday);
    }
  else
    {
      const gchar *event_month;
      const gchar *event_month_names[] = {
        /* Translators: %d is the numeric day of month */
        N_("New Event on January %d"),
        N_("New Event on February %d"),
        N_("New Event on March %d"),
        N_("New Event on April %d"),
        N_("New Event on May %d"),
        N_("New Event on June %d"),
        N_("New Event on July %d"),
        N_("New Event on August %d"),
        N_("New Event on September %d"),
        N_("New Event on October %d"),
        N_("New Event on November %d"),
        N_("New Event on December %d"),
        NULL
      };

      event_month = gettext (event_month_names [g_date_time_get_month (day) - 1]);
      string_for_date = g_strdup_printf (event_month, g_date_time_get_day_of_month (day));
    }

  return string_for_date;
}

static void
update_header (GcalQuickAddPopover *self)
{
  g_autoptr (GDateTime) range_start = NULL;
  g_autoptr (GDateTime) range_end = NULL;
  g_autofree gchar *title_date = NULL;
  gboolean multiday_or_timed;
  GcalContext *context;

  if (!self->range)
    return;

  range_start = gcal_range_get_start (self->range);
  range_end = gcal_range_get_end (self->range);

  multiday_or_timed = gcal_range_get_range_type (self->range) == GCAL_RANGE_DEFAULT ||
                      gcal_date_time_compare_date (range_start, range_end) != 0;

  if (multiday_or_timed)
    {
      gboolean all_day;

      all_day = gcal_range_get_range_type (self->range) == GCAL_RANGE_DATE_ONLY;

      if (all_day &&
          (g_date_time_difference (range_end, range_start) / G_TIME_SPAN_DAY > 1 &&
           g_date_time_difference (range_end, range_start) / G_TIME_SPAN_MINUTE > 1))
        {
          title_date = get_date_string_for_multiday (range_start, g_date_time_add_days (range_end, -1));
        }
      else
        {
          g_autofree gchar *event_date_name = NULL;
          g_autofree gchar *start_hour = NULL;
          g_autofree gchar *end_hour = NULL;
          GcalTimeFormat time_format;
          const gchar *hour_format;

          context = gcal_application_get_context (GCAL_DEFAULT_APPLICATION);
          time_format = gcal_context_get_time_format (context);

          if (time_format == GCAL_TIME_FORMAT_24H)
              hour_format = "%R";
          else
              hour_format = "%I:%M %P";

          start_hour = g_date_time_format (range_start, hour_format);
          end_hour = g_date_time_format (range_end, hour_format);

          event_date_name = get_date_string_for_day (range_start);

          if (all_day)
            {
              title_date = g_strdup (event_date_name);
            }
          else
            {
              /* Translators: %1$s is the event name, %2$s is the start hour, and %3$s is the end hour.
               * To avoid spurious line wrapping, use non-breaking space characters around the en dash. */
              title_date = g_strdup_printf (_("%1$s, %2$s – %3$s"),
                                            event_date_name,
                                            start_hour,
                                            end_hour);
            }

        }
    }
  else
    {
      g_autofree gchar *event_date_name = NULL;
      event_date_name = get_date_string_for_day (range_start);
      title_date = g_strdup_printf ("%s", event_date_name);
    }

  gtk_label_set_label (GTK_LABEL (self->title_label), title_date);
}


/*
 * Callbacks
 */

static void
edit_or_create_event (GcalQuickAddPopover *self,
                      GtkWidget           *button)
{
  g_autoptr (GDateTime) range_start = NULL;
  g_autoptr (GDateTime) range_end = NULL;
  ECalComponent *component;
  GcalCalendar *calendar;
  GtkListBoxRow *selected_row;
  GcalContext *context;
  GcalManager *manager;
  GDateTime *date_start, *date_end;
  GTimeZone *tz;
  GcalEvent *event;
  const gchar *summary;
  gboolean all_day, single_day;

  range_start = gcal_range_get_start (self->range);
  range_end = gcal_range_get_end (self->range);

  context = gcal_application_get_context (GCAL_DEFAULT_APPLICATION);
  manager = gcal_context_get_manager (context);
  selected_row = gtk_list_box_get_selected_row (GTK_LIST_BOX (self->calendars_list_box));
  calendar = gcal_calendar_row_get_calendar (GCAL_CALENDAR_ROW (selected_row));

  single_day = gcal_date_time_compare_date (range_end, range_start) == 0;
  all_day = gcal_date_time_compare_date (range_end, range_start) > 1 ||
            g_date_time_compare (range_end, range_start) == 0;
  tz = all_day ? g_time_zone_new_utc () : g_time_zone_new_local ();

  /* Gather start date */
  date_start = g_date_time_new (tz,
                                g_date_time_get_year (range_start),
                                g_date_time_get_month (range_start),
                                g_date_time_get_day_of_month (range_start),
                                g_date_time_get_hour (range_start),
                                g_date_time_get_minute (range_start),
                                0);

  /* Gather date end */
  if (range_end)
    {
      date_end = g_date_time_new (tz,
                                  g_date_time_get_year (range_end),
                                  g_date_time_get_month (range_end),
                                  g_date_time_get_day_of_month (range_end) + (single_day && all_day ? 1 : 0),
                                  g_date_time_get_hour (range_end),
                                  g_date_time_get_minute (range_end),
                                  0);
    }
  else
    {
      date_end = all_day ? g_date_time_add_days (date_start, 1) : g_date_time_add_hours (date_start, 1);
    }

  /* Gather the summary */
  summary = gtk_editable_get_text (GTK_EDITABLE (self->summary_entry));

  /* Create an ECalComponent from the data above */
  component = build_component_from_details (summary, date_start, date_end);

  event = gcal_event_new (calendar, component, NULL);
  gcal_event_set_all_day (event, all_day);

  /* If we clicked edit button, send a signal; otherwise, create the event */
  if (button == self->add_button)
    gcal_manager_create_event (manager, event);
  else
    g_signal_emit (self, signals[EDIT_EVENT], 0, event);

  gtk_widget_set_visible (GTK_WIDGET (self), FALSE);

  g_clear_pointer (&date_start, g_date_time_unref);
  g_clear_pointer (&date_end, g_date_time_unref);
  g_clear_pointer (&tz, g_time_zone_unref);
  g_clear_object (&event);
}

static void
summary_entry_text_changed (AdwEntryRow         *entry,
                            GParamSpec          *pspec,
                            GcalQuickAddPopover *self)
{
  gboolean is_valid_event_name;

  is_valid_event_name = gcal_is_valid_event_name (gtk_editable_get_text (GTK_EDITABLE (entry)));

  if (is_valid_event_name)
    gtk_widget_remove_css_class (GTK_WIDGET (entry), "error");
  else
    gtk_widget_add_css_class (GTK_WIDGET (entry), "error");

  gtk_widget_set_sensitive (self->add_button, is_valid_event_name);
}

static void
summary_entry_activated (AdwEntryRow         *entry,
                         GcalQuickAddPopover *self)
{
  if (gcal_is_valid_event_name (gtk_editable_get_text (GTK_EDITABLE (entry))))
    edit_or_create_event (self, self->add_button);
  else
    edit_or_create_event (self, self->edit_button);
}


/*
 * Overrides
 */

static void
gcal_quick_add_popover_get_property (GObject    *object,
                                     guint       prop_id,
                                     GValue     *value,
                                     GParamSpec *pspec)
{
  GcalQuickAddPopover *self = GCAL_QUICK_ADD_POPOVER (object);

  switch (prop_id)
    {
    case PROP_RANGE:
      g_value_set_object (value, self->range);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
gcal_quick_add_popover_set_property (GObject      *object,
                                     guint         prop_id,
                                     const GValue *value,
                                     GParamSpec   *pspec)
{
  GcalQuickAddPopover *self = GCAL_QUICK_ADD_POPOVER (object);

  switch (prop_id)
    {
    case PROP_RANGE:
      gcal_quick_add_popover_set_range (self, g_value_get_boxed (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
gcal_quick_add_popover_closed (GtkPopover *popover)
{
  GcalQuickAddPopover *self;

  self = GCAL_QUICK_ADD_POPOVER (popover);

  /* Clear text */
  gtk_editable_set_text (GTK_EDITABLE (self->summary_entry), "");

  /* Remove "error" class */
  gtk_widget_remove_css_class (self->summary_entry, "error");

  g_clear_pointer (&self->range, gcal_range_unref);

  bind_model (self);
}

static void
gcal_quick_add_popover_class_init (GcalQuickAddPopoverClass *klass)
{
  GtkPopoverClass *popover_class = GTK_POPOVER_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  popover_class->closed = gcal_quick_add_popover_closed;

  object_class->get_property = gcal_quick_add_popover_get_property;
  object_class->set_property = gcal_quick_add_popover_set_property;

  /**
   * GcalQuickAddPopover::edit-event:
   * @self: a #GcalQuickAddPopover
   * @event: the new #GcalEvent
   *
   * Emitted when the user clicks 'Edit event' button.
   */
  signals[EDIT_EVENT] = g_signal_new ("edit-event",
                                      GCAL_TYPE_QUICK_ADD_POPOVER,
                                      G_SIGNAL_RUN_LAST,
                                      0, NULL, NULL, NULL,
                                      G_TYPE_NONE,
                                      1,
                                      GCAL_TYPE_EVENT);

  /**
   * GcalQuickAddPopover::range:
   *
   * The range of the new event.
   */
  g_object_class_install_property (object_class,
                                   PROP_RANGE,
                                   g_param_spec_boxed ("range",
                                                       "Range of the new event",
                                                       "The range of the new event",
                                                       G_TYPE_DATE_TIME,
                                                       G_PARAM_READWRITE));

  gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/calendar/ui/gui/gcal-quick-add-popover.ui");

  gtk_widget_class_bind_template_child (widget_class, GcalQuickAddPopover, add_button);
  gtk_widget_class_bind_template_child (widget_class, GcalQuickAddPopover, calendars_list_box);
  gtk_widget_class_bind_template_child (widget_class, GcalQuickAddPopover, edit_button);
  gtk_widget_class_bind_template_child (widget_class, GcalQuickAddPopover, summary_entry);
  gtk_widget_class_bind_template_child (widget_class, GcalQuickAddPopover, title_label);

  gtk_widget_class_bind_template_callback (widget_class, edit_or_create_event);
  gtk_widget_class_bind_template_callback (widget_class, row_selected_cb);
  gtk_widget_class_bind_template_callback (widget_class, summary_entry_activated);
  gtk_widget_class_bind_template_callback (widget_class, summary_entry_text_changed);
}

static void
gcal_quick_add_popover_init (GcalQuickAddPopover *self)
{
  GcalContext *context = gcal_application_get_context (GCAL_DEFAULT_APPLICATION);
  GcalManager *manager = gcal_context_get_manager (context);

  gtk_widget_init_template (GTK_WIDGET (self));

  self->read_write_calendars_model = gcal_create_writable_calendars_model (manager);

  bind_model (self);

  /* Connect to the manager signals and keep the list updates */
  g_signal_connect_object (manager,
                           "notify::default-calendar",
                           G_CALLBACK (bind_model),
                           self,
                           G_CONNECT_SWAPPED);

  g_signal_connect_object (context,
                           "notify::time-format",
                           G_CALLBACK (update_header),
                           self,
                           G_CONNECT_SWAPPED);
}

GtkWidget*
gcal_quick_add_popover_new (void)
{
  return g_object_new (GCAL_TYPE_QUICK_ADD_POPOVER, NULL);
}

/**
 * gcal_quick_add_popover_get_range:
 * @self: a #GcalQuickAddPopover
 *
 * Retrieves the range of @self.
 *
 * Returns: (transfer none): a #GcalRange
 */
GcalRange*
gcal_quick_add_popover_get_range (GcalQuickAddPopover *self)
{
  g_return_val_if_fail (GCAL_IS_QUICK_ADD_POPOVER (self), NULL);

  return self->range;
}

/**
 * gcal_quick_add_popover_set_range:
 * @self: a #GcalQuickAddPopover
 * @range: a #GcalRange
 *
 * Sets the range of the to-be-created event.
 */
void
gcal_quick_add_popover_set_range (GcalQuickAddPopover *self,
                                  GcalRange           *range)
{
  g_return_if_fail (GCAL_IS_QUICK_ADD_POPOVER (self));

  if (self->range != range &&
      (!self->range || !range ||
       (self->range && range && gcal_range_compare (self->range, range) != 0)))
    {
      g_clear_pointer (&self->range, gcal_range_unref);
      self->range = gcal_range_ref (range);

      update_header (self);

      g_object_notify (G_OBJECT (self), "range");
    }
}

