/* vi:set et ai sw=2 sts=2 ts=2: */
/*-
 * Copyright (c) 2006 Benedikt Meurer <benny@xfce.org>
 *
 * 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 2 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, write to the Free Software Foundation, Inc., 59 Temple
 * Place, Suite 330, Boston, MA  02111-1307  USA
 */

#ifdef HAVE_STDLIB_H
#include <stdlib.h>
#endif

#include "thunar/thunar-column-model.h"
#include "thunar/thunar-preferences.h"
#include "thunar/thunar-private.h"

#include <libxfce4util/libxfce4util.h>



/* Signal identifiers */
enum
{
  COLUMNS_CHANGED,
  LAST_SIGNAL,
};



static void
thunar_column_model_finalize (GObject *object);
static void
thunar_column_model_load_column_order (ThunarColumnModel *column_model);
static void
thunar_column_model_save_column_order (ThunarColumnModel *column_model);
static void
thunar_column_model_notify_column_order (ThunarPreferences *preferences,
                                         GParamSpec        *pspec,
                                         ThunarColumnModel *column_model);
static void
thunar_column_model_load_column_widths (ThunarColumnModel *column_model);
static void
thunar_column_model_save_column_widths (ThunarColumnModel *column_model);
static void
thunar_column_model_notify_column_widths (ThunarPreferences *preferences,
                                          GParamSpec        *pspec,
                                          ThunarColumnModel *column_model);
static void
thunar_column_model_load_visible_columns (ThunarColumnModel *column_model);
static void
thunar_column_model_save_visible_columns (ThunarColumnModel *column_model);
static void
thunar_column_model_notify_visible_columns (ThunarPreferences *preferences,
                                            GParamSpec        *pspec,
                                            ThunarColumnModel *column_model);
static gboolean
thunar_column_model_set_column_width_timer (gpointer user_data);



struct _ThunarColumnModelClass
{
  GObjectClass __parent__;

  /* signals */
  void (*columns_changed) (ThunarColumnModel *column_model);
};

struct _ThunarColumnModel
{
  GObject __parent__;

  /* the model stamp is only used when debugging is
   * enabled, to make sure we don't accept iterators
   * generated by another model.
   */
#ifndef NDEBUG
  gint stamp;
#endif

  ThunarPreferences *preferences;
  ThunarColumn       order[THUNAR_N_VISIBLE_COLUMNS];
  gboolean           visible[THUNAR_N_VISIBLE_COLUMNS];
  gint               width[THUNAR_N_VISIBLE_COLUMNS];
  guint              save_width_timer_id;
};



static guint column_model_signals[LAST_SIGNAL];



G_DEFINE_TYPE (ThunarColumnModel, thunar_column_model, G_TYPE_OBJECT)



static void
thunar_column_model_class_init (ThunarColumnModelClass *klass)
{
  GObjectClass *gobject_class;

  gobject_class = G_OBJECT_CLASS (klass);
  gobject_class->finalize = thunar_column_model_finalize;

  /**
   * ThunarColumnModel::columns-changed:
   * @column_model : a #ThunarColumnModel.
   *
   * Emitted by @column_model whenever the
   * order of the columns or the visibility
   * of a column in @column_model is changed.
   **/
  column_model_signals[COLUMNS_CHANGED] =
  g_signal_new (I_ ("columns-changed"),
                G_TYPE_FROM_CLASS (klass),
                G_SIGNAL_RUN_LAST,
                G_STRUCT_OFFSET (ThunarColumnModelClass, columns_changed),
                NULL, NULL,
                g_cclosure_marshal_VOID__VOID,
                G_TYPE_NONE, 0);
}



static void
thunar_column_model_init (ThunarColumnModel *column_model)
{
  /* grab a reference on the preferences */
  column_model->preferences = thunar_preferences_get ();
  g_signal_connect (G_OBJECT (column_model->preferences), "notify::last-details-view-column-order",
                    G_CALLBACK (thunar_column_model_notify_column_order), column_model);
  g_signal_connect (G_OBJECT (column_model->preferences), "notify::last-details-view-column-widths",
                    G_CALLBACK (thunar_column_model_notify_column_widths), column_model);
  g_signal_connect (G_OBJECT (column_model->preferences), "notify::last-details-view-visible-columns",
                    G_CALLBACK (thunar_column_model_notify_visible_columns), column_model);

  /* generate a random stamp if we're in debug mode */
#ifndef NDEBUG
  column_model->stamp = g_random_int ();
#endif

  /* load the column order */
  thunar_column_model_load_column_order (column_model);

  /* load the column widths */
  thunar_column_model_load_column_widths (column_model);

  /* load the list of visible columns */
  thunar_column_model_load_visible_columns (column_model);

  column_model->save_width_timer_id = 0;
}



static void
thunar_column_model_finalize (GObject *object)
{
  ThunarColumnModel *column_model = THUNAR_COLUMN_MODEL (object);

  /* disconnect from the global preferences */
  g_signal_handlers_disconnect_by_data (G_OBJECT (column_model->preferences), column_model);
  g_object_unref (G_OBJECT (column_model->preferences));

  /* drop any running "save width" timer and just save the most recent column widths */
  if (G_UNLIKELY (column_model->save_width_timer_id != 0))
    {
      g_source_remove (column_model->save_width_timer_id);
      thunar_column_model_save_column_widths (column_model);
    }

  (*G_OBJECT_CLASS (thunar_column_model_parent_class)->finalize) (object);
}



static void
thunar_column_model_load_column_order (ThunarColumnModel *column_model)
{
  ThunarColumn column;
  GEnumClass  *klass;
  GEnumValue  *value;
  gchar      **column_order;
  gchar       *tmp;
  gint         i, j;

  /* determine the column order from the preferences */
  g_object_get (G_OBJECT (column_model->preferences), "last-details-view-column-order", &tmp, NULL);
  column_order = g_strsplit (tmp, ",", -1);
  g_free (tmp);

  /* reset the column order to its default */
  for (j = 0; j < THUNAR_N_VISIBLE_COLUMNS; ++j)
    column_model->order[j] = j;

  /* now rearrange the columns according to the preferences */
  klass = g_type_class_ref (THUNAR_TYPE_COLUMN);
  for (i = 0; column_order[i] != NULL; ++i)
    {
      /* determine the enum value for the name */
      value = g_enum_get_value_by_name (klass, column_order[i]);
      if (G_UNLIKELY (value == NULL || value->value == i))
        continue;

      /* find the current position of the value */
      for (j = 0; j < THUNAR_N_VISIBLE_COLUMNS; ++j)
        if (column_model->order[j] == (guint) value->value)
          break;

      /* check if valid */
      if (G_LIKELY (j < THUNAR_N_VISIBLE_COLUMNS))
        {
          /* exchange the positions of i and j */
          column = column_model->order[i];
          column_model->order[i] = column_model->order[j];
          column_model->order[j] = column;
        }
    }
  g_type_class_unref (klass);

  /* release the column order */
  g_strfreev (column_order);
}



static void
thunar_column_model_save_column_order (ThunarColumnModel *column_model)
{
  GEnumClass *klass;
  GEnumValue *value;
  GString    *column_order;
  gint        n;

  /* allocate a string for the column order */
  column_order = g_string_sized_new (256);

  /* transform the internal visible column list */
  klass = g_type_class_ref (THUNAR_TYPE_COLUMN);
  for (n = 0; n < THUNAR_N_VISIBLE_COLUMNS; ++n)
    {
      /* append a comma if not empty */
      if (*column_order->str != '\0')
        g_string_append_c (column_order, ',');

      /* append the enum value name */
      value = g_enum_get_value (klass, column_model->order[n]);
      g_string_append (column_order, value->value_name);
    }
  g_type_class_unref (klass);

  /* save the list of visible columns */
  g_signal_handlers_block_by_func (G_OBJECT (column_model->preferences), thunar_column_model_notify_column_order, column_model);
  g_object_set (G_OBJECT (column_model->preferences), "last-details-view-column-order", column_order->str, NULL);
  g_signal_handlers_unblock_by_func (G_OBJECT (column_model->preferences), thunar_column_model_notify_column_order, column_model);

  /* release the string */
  g_string_free (column_order, TRUE);
}



static void
thunar_column_model_notify_column_order (ThunarPreferences *preferences,
                                         GParamSpec        *pspec,
                                         ThunarColumnModel *column_model)
{
  _thunar_return_if_fail (THUNAR_IS_COLUMN_MODEL (column_model));
  _thunar_return_if_fail (THUNAR_IS_PREFERENCES (preferences));

  /* load the new column order */
  thunar_column_model_load_column_order (column_model);

  /* emit "columns-changed" */
  g_signal_emit (G_OBJECT (column_model), column_model_signals[COLUMNS_CHANGED], 0);
}



static void
thunar_column_model_load_column_widths (ThunarColumnModel *column_model)
{
  gchar **column_widths;
  gchar  *tmp;
  gint    width;
  gint    n;

  /* determine the column widths from the preferences */
  g_object_get (G_OBJECT (column_model->preferences), "last-details-view-column-widths", &tmp, NULL);
  column_widths = g_strsplit (tmp, ",", -1);
  g_free (tmp);

  /* reset the column widths for the model */
  for (n = 0; n < THUNAR_N_VISIBLE_COLUMNS; ++n)
    column_model->width[n] = 50;
  column_model->width[THUNAR_COLUMN_NAME] = 200;

  /* parse the widths from the preferences */
  for (n = 0; (n < THUNAR_N_VISIBLE_COLUMNS) && (column_widths[n] != NULL); ++n)
    {
      width = strtol (column_widths[n], NULL, 10);
      if (G_LIKELY (width >= 0))
        column_model->width[n] = width;
    }

  /* release the column width array */
  g_strfreev (column_widths);
}



static void
thunar_column_model_save_column_widths (ThunarColumnModel *column_model)
{
  GString *column_widths;
  gint     n;

  /* allocate a string for the column widths */
  column_widths = g_string_sized_new (96);

  /* transform the column widths to a string */
  for (n = 0; n < THUNAR_N_VISIBLE_COLUMNS; ++n)
    {
      /* append a comma if not empty */
      if (*column_widths->str != '\0')
        g_string_append_c (column_widths, ',');

      /* append the width */
      g_string_append_printf (column_widths, "%d", column_model->width[n]);
    }

  /* save the column widths */
  g_signal_handlers_block_by_func (G_OBJECT (column_model->preferences), thunar_column_model_notify_column_widths, column_model);
  g_object_set (G_OBJECT (column_model->preferences), "last-details-view-column-widths", column_widths->str, NULL);
  g_signal_handlers_unblock_by_func (G_OBJECT (column_model->preferences), thunar_column_model_notify_column_widths, column_model);

  /* release the string */
  g_string_free (column_widths, TRUE);
}



static void
thunar_column_model_notify_column_widths (ThunarPreferences *preferences,
                                          GParamSpec        *pspec,
                                          ThunarColumnModel *column_model)
{
  _thunar_return_if_fail (THUNAR_IS_COLUMN_MODEL (column_model));
  _thunar_return_if_fail (THUNAR_IS_PREFERENCES (preferences));

  /* load the new column widths */
  thunar_column_model_load_column_widths (column_model);
}



static void
thunar_column_model_load_visible_columns (ThunarColumnModel *column_model)
{
  GEnumClass *klass;
  GEnumValue *value;
  gchar     **visible_columns;
  gchar      *tmp;
  gint        n;

  /* determine the list of visible columns from the preferences */
  g_object_get (G_OBJECT (column_model->preferences), "last-details-view-visible-columns", &tmp, NULL);
  visible_columns = g_strsplit (tmp, ",", -1);
  g_free (tmp);

  /* reset the visible columns for the model */
  for (n = 0; n < THUNAR_N_VISIBLE_COLUMNS; ++n)
    column_model->visible[n] = FALSE;

  /* mark all columns included in the list as visible */
  klass = g_type_class_ref (THUNAR_TYPE_COLUMN);
  for (n = 0; visible_columns[n] != NULL; ++n)
    {
      /* determine the enum value from the string */
      value = g_enum_get_value_by_name (klass, visible_columns[n]);
      if (G_LIKELY (value != NULL && value->value < THUNAR_N_VISIBLE_COLUMNS))
        column_model->visible[value->value] = TRUE;
    }
  g_type_class_unref (klass);

  /* the name column is always visible */
  column_model->visible[THUNAR_COLUMN_NAME] = TRUE;

  /* release the list of visible columns */
  g_strfreev (visible_columns);
}



static void
thunar_column_model_save_visible_columns (ThunarColumnModel *column_model)
{
  GEnumClass *klass;
  GString    *visible_columns;
  gint        n;

  /* allocate a string for the visible columns list */
  visible_columns = g_string_sized_new (128);

  /* transform the internal visible column list */
  klass = g_type_class_ref (THUNAR_TYPE_COLUMN);
  for (n = 0; n < THUNAR_N_VISIBLE_COLUMNS; ++n)
    if (column_model->visible[klass->values[n].value])
      {
        /* skip special columns */
        if (thunar_column_is_special (klass->values[n].value))
          continue;

        /* append a comma if not empty */
        if (*visible_columns->str != '\0')
          g_string_append_c (visible_columns, ',');

        /* append the enum value name */
        g_string_append (visible_columns, klass->values[n].value_name);
      }
  g_type_class_unref (klass);

  /* save the list of visible columns */
  g_signal_handlers_block_by_func (G_OBJECT (column_model->preferences), thunar_column_model_notify_visible_columns, column_model);
  g_object_set (G_OBJECT (column_model->preferences), "last-details-view-visible-columns", visible_columns->str, NULL);
  g_signal_handlers_unblock_by_func (G_OBJECT (column_model->preferences), thunar_column_model_notify_visible_columns, column_model);

  /* release the string */
  g_string_free (visible_columns, TRUE);
}



static void
thunar_column_model_notify_visible_columns (ThunarPreferences *preferences,
                                            GParamSpec        *pspec,
                                            ThunarColumnModel *column_model)
{
  _thunar_return_if_fail (THUNAR_IS_COLUMN_MODEL (column_model));
  _thunar_return_if_fail (THUNAR_IS_PREFERENCES (preferences));

  /* load the new list of visible columns */
  thunar_column_model_load_visible_columns (column_model);

  /* emit "columns-changed" */
  g_signal_emit (G_OBJECT (column_model), column_model_signals[COLUMNS_CHANGED], 0);
}



static gboolean
thunar_column_model_set_column_width_timer (gpointer user_data)
{
  ThunarColumnModel *column_model = THUNAR_COLUMN_MODEL (user_data);

  thunar_column_model_save_column_widths (column_model);
  column_model->save_width_timer_id = 0;

  return FALSE;
}



/**
 * thunar_column_model_get_default:
 *
 * Returns the default, shared #ThunarColumnModel
 * instance.
 *
 * The caller is responsible to free the returned
 * object using g_object_unref() when no longer
 * needed.
 *
 * Return value: the default #ThunarColumnModel.
 **/
ThunarColumnModel *
thunar_column_model_get_default (void)
{
  static ThunarColumnModel *column_model = NULL;

  if (G_UNLIKELY (column_model == NULL))
    {
      column_model = g_object_new (THUNAR_TYPE_COLUMN_MODEL, NULL);
      g_object_add_weak_pointer (G_OBJECT (column_model), (gpointer) &column_model);
    }
  else
    {
      g_object_ref (G_OBJECT (column_model));
    }

  return column_model;
}



void
thunar_column_model_move (ThunarColumnModel *column_model,
                          gint               source_index,
                          gint               dest_index)
{
  ThunarColumn saved_order[THUNAR_N_VISIBLE_COLUMNS];
  gint         i, j;

  _thunar_return_if_fail (THUNAR_IS_COLUMN_MODEL (column_model));
  _thunar_return_if_fail (source_index >= 0 && source_index < THUNAR_N_VISIBLE_COLUMNS);
  _thunar_return_if_fail (dest_index >= 0 && dest_index < THUNAR_N_VISIBLE_COLUMNS);

  /* the code below does the same thing as:
   * tmp = order.remove(source_index)
   * order.insert(dest_index, tmp) */
  memcpy (saved_order, column_model->order, sizeof (saved_order));
  /* i - destination index (column_model->order), j - source index (saved_order) */
  for (i = 0, j = 0; i < THUNAR_N_VISIBLE_COLUMNS; ++i)
    {
      /* skip "removed" element */
      if (j == source_index)
        ++j;

      /* put the next element from the previous order into the destination, or "insert" the element from source_index */
      column_model->order[i] = i != dest_index ? saved_order[j++] : saved_order[source_index];
    }

  thunar_column_model_save_column_order (column_model);
  g_signal_emit (G_OBJECT (column_model), column_model_signals[COLUMNS_CHANGED], 0);
}



/**
 * thunar_column_model_get_column_order:
 * @column_model : a #ThunarColumnModel.
 *
 * Returns the current #ThunarColumn order for @column_model.
 *
 * Return value: the current #ThunarColumn order for the given
 *               @column_model instance.
 **/
const ThunarColumn *
thunar_column_model_get_column_order (ThunarColumnModel *column_model)
{
  _thunar_return_val_if_fail (THUNAR_IS_COLUMN_MODEL (column_model), NULL);
  return column_model->order;
}



/**
 * thunar_column_model_get_column_name:
 * @column_model : a #ThunarColumnModel.
 * @column       : a #ThunarColumn.
 *
 * Returns the user visible name for the @column in
 * the @column_model.
 *
 * Return value: the user visible name for @column.
 **/
const gchar *
thunar_column_model_get_column_name (ThunarColumnModel *column_model,
                                     ThunarColumn       column)
{
  const gchar *column_name = _("Unknown");
  GEnumClass  *klass;
  guint        n;

  _thunar_return_val_if_fail (THUNAR_IS_COLUMN_MODEL (column_model), NULL);
  _thunar_return_val_if_fail (column < THUNAR_N_VISIBLE_COLUMNS, NULL);

  /* determine the column name from the ThunarColumn enum type */
  klass = g_type_class_ref (THUNAR_TYPE_COLUMN);
  for (n = 0; n < klass->n_values; ++n)
    if (klass->values[n].value == (gint) column)
      column_name = _(klass->values[n].value_nick);
  g_type_class_unref (klass);

  return column_name;
}



/**
 * thunar_column_model_get_column_visible:
 * @column_model : a #ThunarColumnModel.
 * @column       : a #ThunarColumn.
 *
 * Returns %TRUE if the @column should be displayed
 * in the details view.
 *
 * Return value: %TRUE if @column is visible.
 **/
gboolean
thunar_column_model_get_column_visible (ThunarColumnModel *column_model,
                                        ThunarColumn       column)
{
  _thunar_return_val_if_fail (THUNAR_IS_COLUMN_MODEL (column_model), FALSE);
  _thunar_return_val_if_fail (column < THUNAR_N_VISIBLE_COLUMNS, FALSE);
  return column_model->visible[column];
}



/**
 * thunar_column_model_set_column_visible:
 * @column_model : a #ThunarColumnModel.
 * @column       : a #ThunarColumn.
 * @visible      : %TRUE to make @column visible.
 *
 * Changes the visibility of the @column to @visible in
 * @column_model.
 **/
void
thunar_column_model_set_column_visible (ThunarColumnModel *column_model,
                                        ThunarColumn       column,
                                        gboolean           visible)
{
  _thunar_return_if_fail (THUNAR_IS_COLUMN_MODEL (column_model));
  _thunar_return_if_fail (column < THUNAR_N_VISIBLE_COLUMNS);

  /* cannot change the visibility of the name column */
  if (G_UNLIKELY (column == THUNAR_COLUMN_NAME))
    return;

  /* normalize the value */
  visible = !!visible;

  /* check if we have a new value */
  if (G_LIKELY (column_model->visible[column] != visible) || thunar_column_is_special (column))
    {
      /* apply the new value */
      column_model->visible[column] = visible;

      /* emit "columns-changed" */
      g_signal_emit (G_OBJECT (column_model), column_model_signals[COLUMNS_CHANGED], 0);

      /* save the list of visible columns (excluding special columns) */
      if (!thunar_column_is_special (column))
        thunar_column_model_save_visible_columns (column_model);
    }
}



/**
 * thunar_column_model_get_column_width:
 * @column_model : a #ThunarColumnModel.
 * @column       : a #ThunarColumn.
 *
 * Returns the fixed column width for @column in
 * @column_model.
 *
 * Return value: the fixed width for @column.
 **/
gint
thunar_column_model_get_column_width (ThunarColumnModel *column_model,
                                      ThunarColumn       column)
{
  _thunar_return_val_if_fail (THUNAR_IS_COLUMN_MODEL (column_model), -1);
  _thunar_return_val_if_fail (column < THUNAR_N_VISIBLE_COLUMNS, -1);
  return column_model->width[column];
}



/**
 * thunar_column_model_set_column_width:
 * @column_model : a #ThunarColumnModel.
 * @column       : a #ThunarColumn.
 * @width        : the new fixed width for @column.
 *
 * Sets the fixed width for @column in @column_model
 * to @width.
 **/
void
thunar_column_model_set_column_width (ThunarColumnModel *column_model,
                                      ThunarColumn       column,
                                      gint               width)
{
  _thunar_return_if_fail (THUNAR_IS_COLUMN_MODEL (column_model));
  _thunar_return_if_fail (column < THUNAR_N_VISIBLE_COLUMNS);
  _thunar_return_if_fail (width >= 0);

  /* check if we have a new width */
  if (G_LIKELY (column_model->width[column] != width))
    {
      /* apply the new value */
      column_model->width[column] = width;

      /* store the settings... */
      if (column_model->save_width_timer_id != 0)
        {
          g_source_remove (column_model->save_width_timer_id);
          column_model->save_width_timer_id = 0;
        }

      /* ... asynchronously and only once to not overload xfconf */
      column_model->save_width_timer_id = g_timeout_add (1000,
                                                         thunar_column_model_set_column_width_timer,
                                                         column_model);
    }
}



void
thunar_column_model_reset (ThunarColumnModel *column_model)
{
  static const gchar *property_names[] = {
    "last-details-view-column-order",
    "last-details-view-visible-columns",
  };
  guint       n;
  GParamSpec *pspec;
  GValue      value = G_VALUE_INIT;

  /* reset the given properties to its default values */
  for (n = 0; n < G_N_ELEMENTS (property_names); ++n)
    {
      pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (column_model->preferences), property_names[n]);
      g_value_init (&value, pspec->value_type);
      g_param_value_set_default (pspec, &value);
      g_object_set_property (G_OBJECT (column_model->preferences), property_names[n], &value);
      g_value_unset (&value);
    }

  g_signal_emit (G_OBJECT (column_model), column_model_signals[COLUMNS_CHANGED], 0);
}
