/* gcal-month-cell.c
 *
 * Copyright (C) 2017 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 "GcalMonthCell"

#include "config.h"
#include "gcal-clock.h"
#include "gcal-debug.h"
#include "gcal-event-widget.h"
#include "gcal-month-cell.h"
#include "gcal-utils.h"
#include "gcal-weather-info.h"

struct _GcalMonthCell
{
  GtkWidget           parent;

  GDateTime          *date;
  guint               n_overflow;

  AdwBreakpoint      *breakpoint_narrow;

  GtkLabel           *day_label;
  GtkWidget          *header_box;
  GtkImage           *weather_icon;
  GtkLabel           *temp_label;
  GtkWidget          *breakpoint_bin;

  GtkWidget          *overflow_button;
  GtkInscription     *overflow_inscription;
  GtkWidget          *overlay;

  gboolean            different_month;
  gboolean            selected;

  GcalWeatherInfo    *weather_info;

  GBinding           *icon_tooltip_binding;
};

G_DEFINE_TYPE (GcalMonthCell, gcal_month_cell, GTK_TYPE_WIDGET)

enum
{
  SHOW_OVERFLOW,
  ACTIVATE,
  N_SIGNALS
};

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


/*
 * Auxiliary methods
 */

static inline gint
get_parent_bin_height (GtkWidget *widget)
{
  GtkWidget *parent;

  parent = gtk_widget_get_parent (widget);
  g_assert (ADW_IS_BIN (parent));

  return gtk_widget_get_height (parent);
}

static void
update_style_flags (GcalMonthCell *self)
{
  g_autoptr (GDateTime) today = NULL;
  gint weekday;

  /* Today */
  today = g_date_time_new_now_local ();

  if (gcal_date_time_compare_date (self->date, today) == 0)
    gtk_widget_add_css_class (GTK_WIDGET (self), "today");
  else
    gtk_widget_remove_css_class (GTK_WIDGET (self), "today");

  weekday = g_date_time_get_day_of_week (self->date);
  if (is_workday (weekday))
    gtk_widget_add_css_class (GTK_WIDGET (self), "workday");
  else
    gtk_widget_remove_css_class (GTK_WIDGET (self), "workday");
}

static void
add_month_separators (GcalMonthCell *self)
{
  gint day_of_month;

  gtk_widget_remove_css_class (GTK_WIDGET (self), "separator-top");
  gtk_widget_remove_css_class (GTK_WIDGET (self), "separator-side");

  day_of_month = g_date_time_get_day_of_month (self->date);
  if (day_of_month > 1 && day_of_month <= N_WEEKDAYS)
    {
      gtk_widget_add_css_class (GTK_WIDGET (self), "separator-top");
    }
  else if (day_of_month == 1)
    {
      gtk_widget_add_css_class (GTK_WIDGET (self), "separator-side");
      gtk_widget_add_css_class (GTK_WIDGET (self), "separator-top");
    }
}

static void
move_event (GcalMonthCell         *self,
            GcalEvent             *event,
            GcalRecurrenceModType  mod_type)
{

  g_autoptr (GcalEvent) changed_event = NULL;
  g_autoptr (GDateTime) start_dt = NULL;
  GTimeSpan timespan = 0;
  GcalContext *context;
  GDateTime *end_dt;
  gint diff;
  gint start_month, current_month;
  gint start_year, current_year;

  GCAL_ENTRY;

  changed_event = gcal_event_new_from_event (event);

  /* Move the event's date */
  start_dt = gcal_event_get_date_start (changed_event);
  end_dt = gcal_event_get_date_end (changed_event);

  start_month = g_date_time_get_month (start_dt);
  start_year = g_date_time_get_year (start_dt);

  current_month = g_date_time_get_month (self->date);
  current_year = g_date_time_get_year (self->date);

  timespan = g_date_time_difference (end_dt, start_dt);

  start_dt = g_date_time_add_full (start_dt,
                                   current_year - start_year,
                                   current_month - start_month,
                                   0, 0, 0, 0);

  diff = gcal_date_time_compare_date (self->date, start_dt);

  if (diff != 0)
    {
      g_autoptr (GDateTime) new_start = g_date_time_add_days (start_dt, diff);

      gcal_event_set_date_start (changed_event, new_start);

      /* The event may have a NULL end date, so we have to check it here */
      if (end_dt)
        {
          g_autoptr (GDateTime) new_end = g_date_time_add (new_start, timespan);

          gcal_event_set_date_end (changed_event, new_end);
        }

      context = gcal_application_get_context (GCAL_DEFAULT_APPLICATION);
      gcal_manager_update_event (gcal_context_get_manager (context), changed_event, mod_type);
    }
}

static void
update_weather (GcalMonthCell *self)
{
  GcalContext *context = gcal_application_get_context (GCAL_DEFAULT_APPLICATION);
  GcalWeatherService *weather_service;
  GcalWeatherInfo *weather_info;
  GDate date;
  gint day_of_month;

  day_of_month = g_date_time_get_day_of_month (self->date);

  g_date_set_dmy (&date,
                  day_of_month,
                  g_date_time_get_month (self->date),
                  g_date_time_get_year (self->date));

  weather_service = gcal_context_get_weather_service (context);
  weather_info = gcal_weather_service_get_weather_info_for_date (weather_service, &date);

  g_assert (!weather_info || GCAL_IS_WEATHER_INFO (weather_info));

  if (self->weather_info == weather_info)
    return;

  self->weather_info = weather_info;

  if (weather_info)
    {
      const gchar *icon_name; /* unowned */
      const gchar *temp_str;  /* unowned */

      icon_name = gcal_weather_info_get_icon_name (weather_info);
      temp_str = gcal_weather_info_get_temperature (weather_info);

      gtk_image_set_from_icon_name (self->weather_icon, icon_name);
      gtk_label_set_text (self->temp_label, temp_str);

      /* Use a short month name label to avoid conflicting with the weather forecast's labels */
      if (day_of_month == 1)
        {
          g_autofree gchar *month_name = g_date_time_format (self->date, "%b");
          gtk_label_set_text (self->day_label, month_name);
        }
    }
  else
    {
      gtk_image_clear (self->weather_icon);
      gtk_label_set_text (self->temp_label, "");

      /* No risk of conflicting with the weather forecast labels in their absence, use the full month name label */
      if (day_of_month == 1)
        {
          g_autofree gchar *month_name = g_date_time_format (self->date, "%OB");
          gtk_label_set_text (self->day_label, month_name);
        }
    }
}


/*
 * Callbacks
 */

static void
day_changed_cb (GcalClock     *clock,
                GcalMonthCell *self)
{
  update_style_flags (self);
}

static void
overflow_button_clicked_cb (GtkWidget     *button,
                            GcalMonthCell *self)
{
  g_signal_emit (self, signals[SHOW_OVERFLOW], 0, button);
}

static gboolean
on_drop_target_accept_cb (GtkDropTarget *drop_target,
                          GdkDrop       *drop,
                          GcalMonthCell *self)
{
  GCAL_ENTRY;

  if ((gdk_drop_get_actions (drop) & gtk_drop_target_get_actions (drop_target)) == 0)
    GCAL_RETURN (FALSE);

  if (!gdk_content_formats_contain_gtype (gdk_drop_get_formats (drop), GCAL_TYPE_EVENT_WIDGET))
    GCAL_RETURN (FALSE);

  GCAL_RETURN (TRUE);
}

static void
on_ask_recurrence_response_cb (GcalEvent             *event,
                               GcalRecurrenceModType  mod_type,
                               gpointer               user_data)
{
  GcalMonthCell *self = GCAL_MONTH_CELL (user_data);

  if (mod_type != GCAL_RECURRENCE_MOD_NONE)
    move_event (self, event, mod_type);
}

static gboolean
on_drop_target_drop_cb (GtkDropTarget *drop_target,
                        const GValue  *value,
                        gdouble        x,
                        gdouble        y,
                        GcalMonthCell *self)
{
  GcalEventWidget *event_widget;
  GcalEvent *event;

  GCAL_ENTRY;

  if (!G_VALUE_HOLDS (value, GCAL_TYPE_EVENT_WIDGET))
    GCAL_RETURN (FALSE);

  event_widget = g_value_get_object (value);

  event = gcal_event_widget_get_event (event_widget);

  if (gcal_event_has_recurrence (event))
    {
      gcal_utils_ask_recurrence_modification_type (GTK_WIDGET (self),
                                                   event,
                                                   FALSE,
                                                   on_ask_recurrence_response_cb,
                                                   self);
    }
  else
    {
      move_event (self, event, GCAL_RECURRENCE_MOD_THIS_ONLY);
    }

  GCAL_RETURN (TRUE);
}

static void
on_weather_service_weather_changed_cb (GcalWeatherService *weather_service,
                                       GcalMonthCell      *self)
{
  update_weather (self);
}

static void
on_breakpoint_changed_cb (GcalMonthCell *self)
{
  AdwBreakpoint *current_breakpoint =
    adw_breakpoint_bin_get_current_breakpoint (ADW_BREAKPOINT_BIN (self->breakpoint_bin));

  if (current_breakpoint == self->breakpoint_narrow)
    {
      self->icon_tooltip_binding = g_object_bind_property (self->temp_label, "label",
                                                           self->weather_icon, "tooltip-text",
                                                           G_BINDING_SYNC_CREATE);
    }
  else
    {
      g_clear_pointer (&self->icon_tooltip_binding, g_binding_unbind);
      gtk_widget_set_tooltip_text (GTK_WIDGET (self->weather_icon), NULL);
    }
}


/*
 * GtkWidget overrides
 */

static gboolean
gcal_month_cell_focus (GtkWidget        *widget,
                       GtkDirectionType  direction)
{
  GtkRoot *root = gtk_widget_get_root (widget);

  if (gtk_root_get_focus (root) == NULL)
    return FALSE;

  return GTK_WIDGET_CLASS (gcal_month_cell_parent_class)->focus (widget, direction);
}


/*
 * GObject overrides
 */

static void
gcal_month_cell_dispose (GObject *object)
{
  GcalMonthCell *self = (GcalMonthCell *)object;
  GcalContext *context = gcal_application_get_context (GCAL_DEFAULT_APPLICATION);

  GcalWeatherService *weather_service = gcal_context_get_weather_service (context);
  gcal_weather_service_release (weather_service);

  gcal_clear_date_time (&self->date);
  g_clear_pointer (&self->breakpoint_bin, gtk_widget_unparent);

  G_OBJECT_CLASS (gcal_month_cell_parent_class)->dispose (object);
}

static void
gcal_month_cell_class_init (GcalMonthCellClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  object_class->dispose = gcal_month_cell_dispose;

  widget_class->focus = gcal_month_cell_focus;

  signals[SHOW_OVERFLOW] = g_signal_new ("show-overflow",
                                         GCAL_TYPE_MONTH_CELL,
                                         G_SIGNAL_RUN_LAST,
                                         0, NULL, NULL, NULL,
                                         G_TYPE_NONE,
                                         1,
                                         GTK_TYPE_WIDGET);

  signals[ACTIVATE] = gcal_create_activate_signal_and_shortcuts (widget_class, GCAL_TYPE_MONTH_CELL);

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

  gtk_widget_class_bind_template_child (widget_class, GcalMonthCell, breakpoint_bin);
  gtk_widget_class_bind_template_child (widget_class, GcalMonthCell, breakpoint_narrow);
  gtk_widget_class_bind_template_child (widget_class, GcalMonthCell, day_label);
  gtk_widget_class_bind_template_child (widget_class, GcalMonthCell, header_box);
  gtk_widget_class_bind_template_child (widget_class, GcalMonthCell, overflow_button);
  gtk_widget_class_bind_template_child (widget_class, GcalMonthCell, overflow_inscription);
  gtk_widget_class_bind_template_child (widget_class, GcalMonthCell, overlay);
  gtk_widget_class_bind_template_child (widget_class, GcalMonthCell, temp_label);
  gtk_widget_class_bind_template_child (widget_class, GcalMonthCell, weather_icon);

  gtk_widget_class_bind_template_callback (widget_class, overflow_button_clicked_cb);

  gtk_widget_class_set_css_name (widget_class, "monthcell");

  gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
}

static void
gcal_month_cell_init (GcalMonthCell *self)
{
  GcalContext *context = gcal_application_get_context (GCAL_DEFAULT_APPLICATION);
  GtkDropTarget *drop_target;

  gtk_widget_init_template (GTK_WIDGET (self));

  drop_target = gtk_drop_target_new (GCAL_TYPE_EVENT_WIDGET, GDK_ACTION_COPY);
  g_signal_connect (drop_target, "accept", G_CALLBACK (on_drop_target_accept_cb), self);
  g_signal_connect (drop_target, "drop", G_CALLBACK (on_drop_target_drop_cb), self);
  gtk_widget_add_controller (GTK_WIDGET (self), GTK_EVENT_CONTROLLER (drop_target));

  g_signal_connect_swapped (self->breakpoint_bin,
                            "notify::current-breakpoint", G_CALLBACK (on_breakpoint_changed_cb), self);

  g_signal_connect_object (gcal_context_get_clock (context),
                           "day-changed",
                           G_CALLBACK (day_changed_cb),
                           self,
                           0);

  g_signal_connect_object (gcal_context_get_weather_service (context),
                           "weather-changed",
                           G_CALLBACK (on_weather_service_weather_changed_cb),
                           self,
                           0);

  GcalWeatherService *weather_service = gcal_context_get_weather_service (context);
  gcal_weather_service_hold (weather_service);
}

GtkWidget*
gcal_month_cell_new (void)
{
  return g_object_new (GCAL_TYPE_MONTH_CELL, NULL);
}


GDateTime*
gcal_month_cell_get_date (GcalMonthCell *self)
{
  g_return_val_if_fail (GCAL_IS_MONTH_CELL (self), NULL);

  return self->date;
}

void
gcal_month_cell_set_date (GcalMonthCell *self,
                          GDateTime     *date)
{
  gint day_of_month;

  g_return_if_fail (GCAL_IS_MONTH_CELL (self));

  if (self->date && date && gcal_date_time_compare_date (self->date, date) == 0)
    return;

  gcal_clear_date_time (&self->date);
  self->date = g_date_time_ref (date);
  day_of_month = g_date_time_get_day_of_month (date);

  update_style_flags (self);
  add_month_separators (self);

  if (day_of_month == 1)
    {
      g_autofree gchar *month_name = g_date_time_format (date, "%OB");
      gtk_label_set_text (self->day_label, month_name);
      gtk_widget_add_css_class (GTK_WIDGET (self->day_label), "first-day");
    }
  else
    {
      g_autofree gchar *date_number = g_strdup_printf ("%d", day_of_month);
      gtk_label_set_text (self->day_label, date_number);
      gtk_widget_remove_css_class (GTK_WIDGET (self->day_label), "first-day");
    }

  update_weather (self);
}

guint
gcal_month_cell_get_overflow (GcalMonthCell *self)
{
  g_return_val_if_fail (GCAL_IS_MONTH_CELL (self), 0);

  return self->n_overflow;
}

void
gcal_month_cell_set_overflow (GcalMonthCell *self,
                              guint          n_overflow)
{
  g_return_if_fail (GCAL_IS_MONTH_CELL (self));

  if (self->n_overflow == n_overflow)
    return;

  self->n_overflow = n_overflow;

  gtk_widget_set_sensitive (self->overflow_button, n_overflow > 0);

  if (n_overflow > 0)
    {
      g_autofree gchar *text = g_strdup_printf ("+%d", n_overflow);
      gtk_inscription_set_text (self->overflow_inscription, text);
    }
  else
    {
      gtk_inscription_set_text (self->overflow_inscription, "");
    }
}

gint
gcal_month_cell_get_content_space (GcalMonthCell *self)
{
  g_return_val_if_fail (GCAL_IS_MONTH_CELL (self), -1);

  return gtk_widget_get_height (GTK_WIDGET (self)) -
         gcal_month_cell_get_header_height (self);
}

gint
gcal_month_cell_get_header_height (GcalMonthCell *self)
{
  g_return_val_if_fail (GCAL_IS_MONTH_CELL (self), -1);

  return get_parent_bin_height (self->header_box);
}

gint
gcal_month_cell_get_overflow_height (GcalMonthCell *self)
{
  g_return_val_if_fail (GCAL_IS_MONTH_CELL (self), -1);

  return get_parent_bin_height (self->overflow_button);
}

gboolean
gcal_month_cell_get_selected (GcalMonthCell *self)
{
  g_return_val_if_fail (GCAL_IS_MONTH_CELL (self), FALSE);

  return self->selected;
}

void
gcal_month_cell_set_selected (GcalMonthCell *self,
                              gboolean       selected)
{
  GtkStateFlags flags;

  g_return_if_fail (GCAL_IS_MONTH_CELL (self));

  if (self->selected == selected)
    return;

  self->selected = selected;
  gtk_accessible_update_state (GTK_ACCESSIBLE (self),
                               GTK_ACCESSIBLE_STATE_SELECTED, selected,
                               -1);

  /* Update style context state */
  flags = gtk_widget_get_state_flags (GTK_WIDGET (self));

  if (selected)
    flags |= GTK_STATE_FLAG_SELECTED;
  else
    flags &= ~GTK_STATE_FLAG_SELECTED;

  gtk_widget_set_state_flags (GTK_WIDGET (self), flags, TRUE);

  gtk_widget_queue_resize (GTK_WIDGET (self));
}

GtkWidget *
gcal_month_cell_get_overflow_button (GcalMonthCell *self)
{
  g_assert (GCAL_IS_MONTH_CELL (self));

  return self->overflow_button;
}

