/*
     This file is part of GNUnet.
     Copyright (C) 2010, 2011, 2012, 2013, 2017 GNUnet e.V.

     GNUnet 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, or (at your
     option) any later version.

     GNUnet 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 GNUnet; see the file COPYING.  If not, write to the
     Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
     Boston, MA 02110-1301, USA.
*/

/**
 * @file src/setup/gnunet-setup.c
 * @brief Main function of gnunet-setup
 * @author Christian Grothoff
 */
#include "gnunet-setup.h"
#include "gnunet-setup-options.h"
#include <gnunet/gnunet_constants.h>
#include <regex.h>
#if ENABLE_NLS
#include <locale.h>
#endif


/**
 * Main loop handle.
 */
static struct GNUNET_GTK_MainLoop *ml;

/**
 * Name of the configuration file.
 */
const char *option_cfg_name;

/**
 * Our configuration.
 */
struct GNUNET_CONFIGURATION_Handle *cfg;

/**
 * Global return value (for success/failure of gnunet-setup).
 */
static int gret;

#ifndef MINGW
/**
 * Flag to enable privilege escalation.
 */
static int do_gksu;
#endif


/**
 * Get an object from the main window.
 *
 * @param name name of the object
 * @return NULL on error, otherwise the object
 */
GObject *
GNUNET_SETUP_get_object (const char *name)
{
  if (NULL == ml)
    return NULL;
  return GNUNET_GTK_main_loop_get_object (ml, name);
}


/**
 * User clicked on some widget, update the help button label and link.
 *
 * @param widget widget that was clicked on (unused)
 * @param event the current event
 * @param user_data context with the option specification to evaluate
 * @return FALSE (continue event handling)
 */
static gboolean
help_click_callback (GtkWidget *widget,
                     GdkEventButton *event,
                     gpointer user_data)
{
  const struct GNUNET_SETUP_OptionSpecification *os = user_data;
  GtkLinkButton *help;

  if (GDK_BUTTON_PRESS != event->type)
    return FALSE;
  help = GTK_LINK_BUTTON (GNUNET_SETUP_get_object ("GNUNET_setup_help_text"));
  gtk_link_button_set_uri (help, os->help_url);
  gtk_button_set_label (GTK_BUTTON (help), os->help_text);
  return FALSE;
}


/**
 * The main visible page in our main notebook changed.  If the
 * GNS page is visible, hide the help text, otherwise show it.
 */
void
GNUNET_setup_notebook_switch_page_cb (GtkNotebook *notebook,
                                      GtkWidget *page,
                                      guint page_num,
                                      gpointer user_data)
{
  GtkWidget *help;
  GtkWidget *gnu;

  help = GTK_WIDGET (GNUNET_SETUP_get_object ("GNUNET_setup_help_text"));
  gnu = GTK_WIDGET (GNUNET_SETUP_get_object ("GNUNET_setup_gns_vbox"));
  if (gnu == page)
    gtk_widget_hide (help);
  else
    gtk_widget_show (help);
}


/**
 * Change the visibility of widgets according to the
 * value and visibility specification given.
 *
 * @param os option specification
 * @param value current value for the given option
 */
static void
update_visibility (const struct GNUNET_SETUP_OptionSpecification *os,
                   const char *value)
{
  unsigned int i;
  const struct GNUNET_SETUP_VisibilitySpecification *vs;
  GtkWidget *widget;
  regex_t r;

  if (NULL == os->visibility)
    return;
  i = 0;
  while (os->visibility[i].widget_name != NULL)
  {
    vs = &os->visibility[i];
    widget = GTK_WIDGET (GNUNET_SETUP_get_object (vs->widget_name));
    if (NULL == widget)
    {
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  _ ("Widget `%s' not found\n"),
                  vs->widget_name);
    }
    if (NULL != vs->show_value)
    {
      if (0 !=
          regcomp (&r, vs->show_value, REG_EXTENDED | REG_ICASE | REG_NOSUB))
      {
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    _ ("Invalid regular expression `%s'\n"),
                    vs->show_value);
        i++;
        continue;
      }
      if (0 == regexec (&r, value, 0, NULL, 0))
        gtk_widget_show (widget);
      else
        gtk_widget_hide (widget);
      regfree (&r);
    }
    if (NULL != vs->hide_value)
    {
      if (0 != regcomp (&r, vs->hide_value, REG_ICASE | REG_NOSUB))
      {
        GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                    _ ("Invalid regular expression `%s'\n"),
                    vs->show_value);
        i++;
        continue;
      }
      if (0 == regexec (&r, value, 0, NULL, 0))
        gtk_widget_hide (widget);
      else
        gtk_widget_show (widget);
      regfree (&r);
    }
    i++;
  }
}


/**
 * Function called whenever a widget changes its state.
 *
 * @param os details about the option
 */
static void
widget_state_change_callback (const struct GNUNET_SETUP_OptionSpecification *os)
{
  GObject *widget;
  char *value;

  widget = GNUNET_SETUP_get_object (os->widget_name);
  GNUNET_assert (NULL != os->save_function);
  if (GNUNET_OK != os->save_function (os->load_save_cls,
                                      os->section,
                                      os->option,
                                      widget,
                                      cfg))
  {
    GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                _ ("Failed to obtain option value from widget `%s'\n"),
                os->widget_name);
    return;
  }
  if (NULL != os->input_validation_function)
    os->input_validation_function (os->input_validation_function_cls, widget);
  if ((NULL != os->section) && (NULL != os->option))
  {
    if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cfg,
                                                            os->section,
                                                            os->option,
                                                            &value))
    {
      GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
                                 os->section,
                                 os->option);
      value = GNUNET_strdup ("");
    }
  }
  else
    return;
  update_visibility (os, value);
  GNUNET_free (value);
}


/**
 * Load options into the main dialog.
 */
static void
load_options ()
{
  const struct GNUNET_SETUP_OptionSpecification *os;
  GObject *widget;
  char *value;

#ifndef LINUX
  gtk_widget_hide (GTK_WIDGET (
                     GNUNET_SETUP_get_object (
                       "GNUNET_setup_gns_hijack_checkbutton")));
#endif
  for (unsigned int i = 0; NULL != option_specifications[i].widget_name; i++)
  {
    os = &option_specifications[i];
    widget = GNUNET_SETUP_get_object (os->widget_name);
    if (NULL == widget)
    {
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  _ ("Widget `%s' not found\n"),
                  os->widget_name);
      continue;
    }
    if (NULL != os->load_function)
    {
      if ((NULL == os->section) || (NULL == os->option))
      {
        if (GNUNET_OK != os->load_function (os->load_save_cls,
                                            NULL,
                                            NULL,
                                            NULL,
                                            widget,
                                            cfg))
        {
          GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                      _ ("Failed to initialize widget `%s'\n"),
                      os->widget_name);
        }
      }
      else
      {
        if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cfg,
                                                                os->section,
                                                                os->option,
                                                                &value))
        {
          GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                      _ (
                        "No default value known for option `%s' in section `%s'\n"),
                      os->option,
                      os->section);
        }
        else
        {
          if (GNUNET_OK != os->load_function (os->load_save_cls,
                                              os->section,
                                              os->option,
                                              value,
                                              widget,
                                              cfg))
          {
            GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                        _ (
                          "Failed to initialize widget `%s' with value `%s'\n"),
                        os->widget_name,
                        value);
          }
          else
          {
            update_visibility (os, value);
          }
          GNUNET_free (value);
        }
      }
    }
    if (NULL != os->input_validation_function)
      os->input_validation_function (os->input_validation_function_cls, widget);
    if (os->help_text != NULL)
    {
      g_signal_connect (widget,
                        "button-press-event",
                        G_CALLBACK (&help_click_callback),
                        (void *) os);
    }
    if (NULL != os->change_signal)
    {
      GNUNET_assert (NULL != os->save_function);
      g_signal_connect_swapped (widget,
                                os->change_signal,
                                G_CALLBACK (&widget_state_change_callback),
                                (void *) os);
    }
  }
}


/**
 * Write final configuration to disk.
 *
 * @return #GNUNET_OK on success
 */
static int
write_configuration ()
{
  struct GNUNET_CONFIGURATION_Handle *cfgDefault;
  int ret;

  cfgDefault = GNUNET_CONFIGURATION_create ();
  (void) GNUNET_CONFIGURATION_load (cfgDefault, NULL); /* load defaults only */
  ret = GNUNET_CONFIGURATION_write_diffs (cfgDefault, cfg, option_cfg_name);
  GNUNET_CONFIGURATION_destroy (cfgDefault);
  return ret;
}


/**
 * Method run on shutdown.
 *
 * @param cls the main loop handle
 */
static void
cleanup_task (void *cls)
{
  if (NULL == ml)
  {
    GNUNET_break (0);
    return;
  }
  GNUNET_GTK_main_loop_quit (ml);
  ml = NULL;
  if (GNUNET_OK != write_configuration ())
    gret = 1;
  cfg = NULL;
}


/**
 * Callback invoked if the application is supposed to exit.
 */
void
GNUNET_SETUP_quit_cb (GObject *object, gpointer user_data)
{
  GNUNET_SCHEDULER_shutdown ();
}


#ifndef MINGW
/**
 * Try elevating user privileges to run as user 'gnunet' or 'root'.
 *
 * @param username user gnunet-setup should be run as
 * @return #GNUNET_OK on success
 */
static int
try_gksu (const char *username)
{
  struct GNUNET_OS_Process *proc;

  proc = GNUNET_OS_start_process (
    GNUNET_OS_INHERIT_STD_ALL,
    NULL,
    NULL,
    NULL,
    "gksu",
    "-u",
    username,
    "-m",
    _ (
      "Enter YOUR password to run gnunet-setup as user 'gnunet' (assuming 'sudo' allows it)"),
    "-D",
    _ (
      "Enter YOUR password to run gnunet-setup as user 'gnunet' (assuming 'sudo' allows it)"),
    NULL);
  if (NULL == proc)
    return GNUNET_SYSERR;
  GNUNET_OS_process_wait (proc);
  GNUNET_OS_process_destroy (proc);
  return GNUNET_OK;
}


#endif


/**
 * User clicked on the button to edit the list of friends.
 * Launch gnunet-peerinfo-gtk.
 */
void
GNUNET_setup_launch_edit_friends_button_clicked_cb (GtkButton *button,
                                                    gpointer *user_data)
{
  struct GNUNET_OS_Process *proc;

  proc = GNUNET_OS_start_process (GNUNET_OS_INHERIT_STD_ALL,
                                  NULL,
                                  NULL,
                                  NULL,
                                  "gnunet-peerinfo-gtk",
                                  "-c",
                                  option_cfg_name,
                                  NULL);
  if (NULL == proc)
  {
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                _ ("Failed to launch gnunet-peerinfo-gtk\n"));
    return;
  }
  /* simply yield control, let it run */
  GNUNET_free (proc);
}


/**
 * Actual main method that sets up the configuration window.
 *
 * @param cls the main loop handle
 */
static void
run (void *cls)
{
  GtkWidget *main_window;
#ifndef MINGW
  uid_t my_uid;
  struct passwd *gnunet_pw;

  my_uid = getuid ();
  gnunet_pw = getpwnam ("gnunet");
  if ((0 != do_gksu) && (0 != my_uid) && (NULL != gnunet_pw) &&
      (my_uid != gnunet_pw->pw_uid) && (GNUNET_OK == try_gksu ("gnunet")))
  {
    GNUNET_GTK_main_loop_quit (cls);
    return;
  }
#endif

  ml = cls;
  if (GNUNET_OK != GNUNET_GTK_main_loop_build_window (ml, NULL))
    return;

  option_cfg_name = GNUNET_GTK_main_loop_get_configuration_file (ml);
  cfg = GNUNET_CONFIGURATION_create ();
  if (GNUNET_YES == GNUNET_DISK_file_test (option_cfg_name))
    (void) GNUNET_CONFIGURATION_load (cfg, option_cfg_name);
  else
    (void) GNUNET_CONFIGURATION_load (cfg, NULL);
  if (0 != access (option_cfg_name, W_OK))
  {
    if (ENOENT == errno)
    {
      char *dirname;
      size_t len;

      (void) GNUNET_DISK_directory_create_for_file (option_cfg_name);
      dirname = GNUNET_STRINGS_filename_expand (option_cfg_name);
      len = strlen (dirname) - 1;
      while ((len > 0) && (dirname[len] != DIR_SEPARATOR))
        len--;
      dirname[len] = '\0';
      if (0 != access (dirname, X_OK | W_OK | R_OK))
      {
        if (ENOENT == errno)
          errno = EACCES; /* really means somewhere access was denied */
      }
      else
        errno = ENOENT; /* all good now */
    }
    if (ENOENT != errno)
    {
      fprintf (
        stderr,
        "Refusing to start as I will be unable to write configuration file `%s': %s\n",
        option_cfg_name,
        strerror (errno));
      GNUNET_GTK_main_loop_quit (ml);
      return;
    }
  }
  main_window = GTK_WIDGET (GNUNET_SETUP_get_object ("GNUNET_setup_dialog"));
  load_options ();
  GNUNET_SCHEDULER_add_shutdown (&cleanup_task, NULL);
  gtk_widget_show (main_window);
  gtk_window_present (GTK_WINDOW (main_window));
}


/**
 * Main function for gnunet-setup.
 *
 * @param argc number of arguments
 * @param argv arguments
 * @return 0 on success
 */
int
main (int argc, char *const *argv)
{
  struct GNUNET_GETOPT_CommandLineOption options[] = {
#ifndef MINGW
    GNUNET_GETOPT_option_flag (
      'e',
      "elevate-privileges",
      gettext_noop (
        "run as user 'gnunet', if necessary by executing gksu to elevate rights"),
      &do_gksu),
#endif
    GNUNET_GETOPT_OPTION_END
  };
  int ret;

  if (GNUNET_OK == GNUNET_GTK_main_loop_start ("gnunet-setup",
                                               "gnunet-setup",
                                               argc,
                                               argv,
                                               options,
                                               "gnunet_setup_main_window.glade",
                                               &run))
    ret = gret;
  else
    ret = 1;
  return ret;
}


/* end of gnunet-setup.c */
