/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
 *
 * Copyright 2009-2010  Red Hat, Inc,
 *
 * 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, see <http://www.gnu.org/licenses/>.
 *
 * Written by: Matthias Clasen <mclasen@redhat.com>
 */

#include "config.h"

#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

#include <glib.h>
#include <glib/gi18n.h>
#include <gtk/gtk.h>
#include <act/act.h>

#include "um-password-dialog.h"
#include "um-utils.h"
#include "run-passwd.h"
#include "pw-utils.h"

#define PASSWORD_CHECK_TIMEOUT 600

struct _UmPasswordDialog {
        GtkWidget *dialog;
        GtkWidget *action_radio_box;
        GtkWidget *action_now_radio;
        GtkWidget *action_login_radio;
        GtkWidget *password_entry;
        GtkWidget *verify_entry;
        gint       password_entry_timeout_id;
        GtkWidget *strength_indicator;
        GtkWidget *ok_button;
        GtkWidget *password_hint;
        GtkWidget *verify_hint;

        ActUser *user;
        ActUserPasswordMode password_mode;

        GtkWidget *old_password_label;
        GtkWidget *old_password_entry;
        gboolean   old_password_ok;
        gint       old_password_entry_timeout_id;

        PasswdHandler *passwd_handler;
};

static gint
update_password_strength (UmPasswordDialog *um)
{
        const gchar *password;
        const gchar *old_password;
        const gchar *username;
        gint strength_level;
        const gchar *hint;
        const gchar *long_hint;

        if (um->user == NULL) {
                return 0;
        }

        password = gtk_entry_get_text (GTK_ENTRY (um->password_entry));
        old_password = gtk_entry_get_text (GTK_ENTRY (um->old_password_entry));
        username = act_user_get_user_name (um->user);

        pw_strength (password, old_password, username,
                     &hint, &long_hint, &strength_level);

        gtk_level_bar_set_value (GTK_LEVEL_BAR (um->strength_indicator), strength_level);
        gtk_label_set_label (GTK_LABEL (um->password_hint), long_hint);

        if (strength_level > 0) {
                set_entry_validation_checkmark (GTK_ENTRY (um->password_entry));
        } else if (strlen (password) == 0) {
                set_entry_generation_icon (GTK_ENTRY (um->password_entry));
        } else {
                clear_entry_validation_error (GTK_ENTRY (um->password_entry));
        }

        return strength_level;
}

static void
finish_password_change (UmPasswordDialog *um)
{
        gtk_widget_hide (um->dialog);

        gtk_entry_set_text (GTK_ENTRY (um->password_entry), " ");
        gtk_entry_set_text (GTK_ENTRY (um->verify_entry), "");
        gtk_entry_set_text (GTK_ENTRY (um->old_password_entry), "");

        um_password_dialog_set_user (um, NULL);
}

static void
cancel_password_dialog (GtkButton        *button,
                        UmPasswordDialog *um)
{
        finish_password_change (um);
}

static void
dialog_closed (GtkWidget        *dialog,
               gint              response_id,
               UmPasswordDialog *um)
{
        gtk_widget_destroy (dialog);
}

static void
password_changed_cb (PasswdHandler    *handler,
                     GError           *error,
                     UmPasswordDialog *um)
{
        GtkWidget *dialog;
        const gchar *primary_text;
        const gchar *secondary_text;

        gtk_widget_set_sensitive (um->dialog, TRUE);
        gdk_window_set_cursor (gtk_widget_get_window (um->dialog), NULL);

        if (!error) {
                finish_password_change (um);
                return;
        }

        if (error->code == PASSWD_ERROR_REJECTED) {
                primary_text = error->message;
                secondary_text = _("Please choose another password.");

                gtk_entry_set_text (GTK_ENTRY (um->password_entry), "");
                gtk_widget_grab_focus (um->password_entry);

                gtk_entry_set_text (GTK_ENTRY (um->verify_entry), "");
        }
        else if (error->code == PASSWD_ERROR_AUTH_FAILED) {
                primary_text = error->message;
                secondary_text = _("Please type your current password again.");

                gtk_entry_set_text (GTK_ENTRY (um->old_password_entry), "");
                gtk_widget_grab_focus (um->old_password_entry);
        }
        else {
                primary_text = _("Password could not be changed");
                secondary_text = error->message;
        }

        dialog = gtk_message_dialog_new (GTK_WINDOW (um->dialog),
                                         GTK_DIALOG_MODAL,
                                         GTK_MESSAGE_ERROR,
                                         GTK_BUTTONS_CLOSE,
                                         "%s", primary_text);
        gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
                                                  "%s", secondary_text);
        g_signal_connect (dialog, "response",
                          G_CALLBACK (dialog_closed), um);
        gtk_window_present (GTK_WINDOW (dialog));

}

static void
accept_password_dialog (GtkButton        *button,
                        UmPasswordDialog *um)
{
        const gchar *password;

        password = gtk_entry_get_text (GTK_ENTRY (um->password_entry));

        switch (um->password_mode) {
                case ACT_USER_PASSWORD_MODE_REGULAR:
                        if (act_user_get_uid (um->user) == getuid ()) {
                                GdkDisplay *display;
                                GdkCursor *cursor;

                                /* When setting a password for the current user,
                                 * use passwd directly, to preserve the audit trail
                                 * and to e.g. update the keyring password.
                                 */
                                passwd_change_password (um->passwd_handler, password,
                                                        (PasswdCallback) password_changed_cb, um);
                                gtk_widget_set_sensitive (um->dialog, FALSE);
                                display = gtk_widget_get_display (um->dialog);
                                cursor = gdk_cursor_new_for_display (display, GDK_WATCH);
                                gdk_window_set_cursor (gtk_widget_get_window (um->dialog), cursor);
                                gdk_display_flush (display);
                                g_object_unref (cursor);
                                return;
                        }

                        act_user_set_password_mode (um->user, ACT_USER_PASSWORD_MODE_REGULAR);
                        act_user_set_password (um->user, password, "");
                        break;

                case ACT_USER_PASSWORD_MODE_SET_AT_LOGIN:
                        act_user_set_password_mode (um->user,  um->password_mode);
                        act_user_set_automatic_login (um->user, FALSE);
                        break;

                default:
                        g_assert_not_reached ();
        }

        finish_password_change (um);
}

static void
update_sensitivity (UmPasswordDialog *um)
{
        const gchar *password, *verify;
        gboolean can_change;
        int strength;

        password = gtk_entry_get_text (GTK_ENTRY (um->password_entry));
        verify = gtk_entry_get_text (GTK_ENTRY (um->verify_entry));

        if (um->password_mode == ACT_USER_PASSWORD_MODE_REGULAR) {
                strength = update_password_strength (um);
                can_change = strength > 0 && strcmp (password, verify) == 0 &&
                             (um->old_password_ok || !gtk_widget_get_visible (um->old_password_entry));
        }
        else {
                can_change = TRUE;
        }

        gtk_widget_set_sensitive (um->ok_button, can_change);
}

static void
mode_change (UmPasswordDialog *um,
             ActUserPasswordMode mode)
{
        gboolean active;

        active = (mode == ACT_USER_PASSWORD_MODE_REGULAR);
        gtk_widget_set_sensitive (um->password_entry, active);
        gtk_widget_set_sensitive (um->verify_entry, active);
        gtk_widget_set_sensitive (um->old_password_entry, active);
        gtk_widget_set_sensitive (um->password_hint, active);
        gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (um->action_now_radio), active);
        gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (um->action_login_radio), !active);

        um->password_mode = mode;
        update_sensitivity (um);
}

static void
action_changed (GtkRadioButton   *radio,
                UmPasswordDialog *um)
{
        gint active;
        ActUserPasswordMode mode;

        active = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (radio));
        mode = active ? ACT_USER_PASSWORD_MODE_REGULAR : ACT_USER_PASSWORD_MODE_SET_AT_LOGIN;
        mode_change (um, mode);
}

static void
update_password_match (UmPasswordDialog *um)
{
        const char *password;
        const char *verify;

        password = gtk_entry_get_text (GTK_ENTRY (um->password_entry));
        verify = gtk_entry_get_text (GTK_ENTRY (um->verify_entry));

        if (strlen (password) > 0 && strlen (verify) > 0) {
                if (strcmp (password, verify) != 0) {
                        gtk_label_set_label (GTK_LABEL (um->verify_hint),
                                             _("The passwords do not match."));
                }
                else {
                        gtk_label_set_label (GTK_LABEL (um->verify_hint), "");
                        set_entry_validation_checkmark (GTK_ENTRY (um->verify_entry));
                }
        }
}

static gboolean
password_entry_timeout (UmPasswordDialog *um)
{
        update_password_strength (um);
        update_sensitivity (um);
        update_password_match (um);

        um->password_entry_timeout_id = 0;

        return FALSE;
}

static void
password_entry_changed (GtkEntry         *entry,
                      GParamSpec       *pspec,
                      UmPasswordDialog *um)
{
        const char *password;

        if (um->password_entry_timeout_id != 0) {
                g_source_remove (um->password_entry_timeout_id);
                um->password_entry_timeout_id = 0;
        }

        clear_entry_validation_error (GTK_ENTRY (entry));
        clear_entry_validation_error (GTK_ENTRY (um->verify_entry));
        gtk_widget_set_sensitive (um->ok_button, FALSE);

        password = gtk_entry_get_text (GTK_ENTRY (um->password_entry));
        if (strlen (password) == 0) {
                gtk_entry_set_visibility (GTK_ENTRY (um->password_entry), FALSE);
        }

        um->password_entry_timeout_id = g_timeout_add (PASSWORD_CHECK_TIMEOUT,
                                                     (GSourceFunc) password_entry_timeout,
                                                     um);
}

static gboolean
password_entry_focus_out (GtkWidget        *entry,
                        GdkEventFocus    *event,
                        UmPasswordDialog *um)
{
        if (um->password_entry_timeout_id != 0) {
                g_source_remove (um->password_entry_timeout_id);
                um->password_entry_timeout_id = 0;
        }

        password_entry_timeout (um);

        return FALSE;
}

static void
auth_cb (PasswdHandler    *handler,
         GError           *error,
         UmPasswordDialog *um)
{
        if (error) {
                um->old_password_ok = FALSE;
        }
        else {
                um->old_password_ok = TRUE;
                set_entry_validation_checkmark (GTK_ENTRY (um->old_password_entry));
        }

        update_sensitivity (um);
}

static gboolean
old_password_entry_timeout (UmPasswordDialog *um)
{
        const char *text;

        update_sensitivity (um);

        text = gtk_entry_get_text (GTK_ENTRY (um->old_password_entry));
        if (!um->old_password_ok) {
                passwd_authenticate (um->passwd_handler, text, (PasswdCallback)auth_cb, um);
        }

        um->old_password_entry_timeout_id = 0;

        return FALSE;
}

static gboolean
old_password_entry_focus_out (GtkWidget        *entry,
                              GdkEventFocus    *event,
                              UmPasswordDialog *um)
{
        if (um->old_password_entry_timeout_id != 0) {
                g_source_remove (um->old_password_entry_timeout_id);
                um->old_password_entry_timeout_id = 0;
        }

        old_password_entry_timeout (um);

        return FALSE;
}

static void
old_password_entry_changed (GtkEntry         *entry,
                            GParamSpec       *pspec,
                            UmPasswordDialog *um)
{
        if (um->old_password_entry_timeout_id != 0) {
                g_source_remove (um->old_password_entry_timeout_id);
                um->old_password_entry_timeout_id = 0;
        }

        clear_entry_validation_error (GTK_ENTRY (entry));
        gtk_widget_set_sensitive (um->ok_button, FALSE);

        um->old_password_ok = FALSE;
        um->old_password_entry_timeout_id = g_timeout_add (PASSWORD_CHECK_TIMEOUT,
                                                           (GSourceFunc) old_password_entry_timeout,
                                                           um);
}

static void
on_generate (GtkEntry             *entry,
             GtkEntryIconPosition  pos,
             GdkEventButton       *event,
             UmPasswordDialog     *um)
{
        gchar *pwd;

        pwd = pw_generate ();

        gtk_entry_set_text (GTK_ENTRY (um->password_entry), pwd);
        gtk_entry_set_text (GTK_ENTRY (um->verify_entry), pwd);
        gtk_entry_set_visibility (GTK_ENTRY (um->password_entry), TRUE);

        g_free (pwd);
}

UmPasswordDialog *
um_password_dialog_new (void)
{
        GtkBuilder *builder;
        GError *error;
        UmPasswordDialog *um;
        GtkWidget *widget;

        builder = gtk_builder_new ();

        error = NULL;
        if (!gtk_builder_add_from_resource (builder,
                                            "/org/gnome/control-center/user-accounts/password-dialog.ui",
                                            &error)) {
                g_error ("%s", error->message);
                g_error_free (error);
                return NULL;
        }

        um = g_new0 (UmPasswordDialog, 1);

        um->action_radio_box = (GtkWidget *) gtk_builder_get_object (builder, "action-radio-box");
        widget = (GtkWidget *) gtk_builder_get_object (builder, "action-now-radio");
        g_signal_connect (widget, "toggled", G_CALLBACK (action_changed), um);
        um->action_now_radio = widget;
        um->action_login_radio = (GtkWidget *) gtk_builder_get_object (builder, "action-login-radio");

        widget = (GtkWidget *) gtk_builder_get_object (builder, "dialog");
        g_signal_connect (widget, "delete-event",
                          G_CALLBACK (gtk_widget_hide_on_delete), NULL);
        um->dialog = widget;

        widget = (GtkWidget *) gtk_builder_get_object (builder, "cancel-button");
        g_signal_connect (widget, "clicked",
                          G_CALLBACK (cancel_password_dialog), um);

        widget = (GtkWidget *) gtk_builder_get_object (builder, "ok-button");
        g_signal_connect (widget, "clicked",
                          G_CALLBACK (accept_password_dialog), um);
        gtk_widget_grab_default (widget);
        um->ok_button = widget;

        widget = (GtkWidget *) gtk_builder_get_object (builder, "password-entry");
        g_signal_connect (widget, "notify::text",
                          G_CALLBACK (password_entry_changed), um);
        g_signal_connect_after (widget, "focus-out-event",
                                G_CALLBACK (password_entry_focus_out), um);
        g_signal_connect_swapped (widget, "activate", G_CALLBACK (password_entry_timeout), um);
        gtk_entry_set_visibility (GTK_ENTRY (widget), FALSE);
        um->password_entry = widget;
        g_signal_connect (widget, "icon-press", G_CALLBACK (on_generate), um);

        widget = (GtkWidget *) gtk_builder_get_object (builder, "old-password-entry");
        g_signal_connect_after (widget, "focus-out-event",
                                G_CALLBACK (old_password_entry_focus_out), um);
        g_signal_connect (widget, "notify::text",
                          G_CALLBACK (old_password_entry_changed), um);
        g_signal_connect_swapped (widget, "activate", G_CALLBACK (password_entry_timeout), um);
        um->old_password_entry = widget;
        um->old_password_label = (GtkWidget *) gtk_builder_get_object (builder, "old-password-label");

        widget = (GtkWidget *) gtk_builder_get_object (builder, "verify-entry");
        g_signal_connect (widget, "notify::text",
                          G_CALLBACK (password_entry_changed), um);
        g_signal_connect_after (widget, "focus-out-event",
                                G_CALLBACK (password_entry_focus_out), um);
        g_signal_connect_swapped (widget, "activate", G_CALLBACK (password_entry_timeout), um);
        um->verify_entry = widget;

        um->strength_indicator = (GtkWidget *) gtk_builder_get_object (builder, "strength-indicator");

        widget = (GtkWidget *)gtk_builder_get_object (builder, "password-hint");
        um->password_hint = widget;

        widget = (GtkWidget *)gtk_builder_get_object (builder, "verify-hint");
        um->verify_hint = widget;

        g_object_unref (builder);

        return um;
}

void
um_password_dialog_free (UmPasswordDialog *um)
{
        gtk_widget_destroy (um->dialog);

        g_clear_object (&um->user);

        if (um->passwd_handler)
                passwd_destroy (um->passwd_handler);

        if (um->old_password_entry_timeout_id != 0) {
                g_source_remove (um->old_password_entry_timeout_id);
                um->old_password_entry_timeout_id = 0;
        }
 
        if (um->password_entry_timeout_id != 0) {
                g_source_remove (um->password_entry_timeout_id);
                um->password_entry_timeout_id = 0;
        }

        g_free (um);
}

void
um_password_dialog_set_user (UmPasswordDialog *um,
                             ActUser          *user)
{
        gboolean visible;

        if (um->user) {
                g_object_unref (um->user);
                um->user = NULL;
        }
        if (user) {
                um->user = g_object_ref (user);

                gtk_entry_set_text (GTK_ENTRY (um->password_entry), "");
                gtk_entry_set_text (GTK_ENTRY (um->verify_entry), "");
                gtk_entry_set_text (GTK_ENTRY (um->old_password_entry), "");

                gtk_entry_set_visibility (GTK_ENTRY (um->password_entry), FALSE);
                gtk_entry_set_visibility (GTK_ENTRY (um->verify_entry), FALSE);

                if (act_user_get_uid (um->user) == getuid ()) {
                        mode_change (um, ACT_USER_PASSWORD_MODE_REGULAR);
                        gtk_widget_hide (um->action_radio_box);

                        visible = (act_user_get_password_mode (user) != ACT_USER_PASSWORD_MODE_NONE);
                        gtk_widget_set_visible (um->old_password_label, visible);
                        gtk_widget_set_visible (um->old_password_entry, visible);
                        um->old_password_ok = !visible;
                }
                else {
                        mode_change (um, ACT_USER_PASSWORD_MODE_SET_AT_LOGIN);
                        gtk_widget_show (um->action_radio_box);

                        gtk_widget_hide (um->old_password_label);
                        gtk_widget_hide (um->old_password_entry);
                        um->old_password_ok = TRUE;
                }
                if (act_user_get_uid (um->user) == getuid()) {
                        if (um->passwd_handler != NULL)
                                passwd_destroy (um->passwd_handler);
                        um->passwd_handler = passwd_init ();
                }
        }
}

void
um_password_dialog_show (UmPasswordDialog *um,
                         GtkWindow        *parent)
{
        gtk_window_set_transient_for (GTK_WINDOW (um->dialog), parent);
        gtk_window_present (GTK_WINDOW (um->dialog));
        if (um->old_password_ok == FALSE)
                gtk_widget_grab_focus (um->old_password_entry);
        else
                gtk_widget_grab_focus (um->password_entry);
}

