/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 2 -*- */
/*
    This file is part of Déjà Dup.
    For copyright information, see AUTHORS.

    Déjà Dup 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.

    Déjà Dup 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 Déjà Dup.  If not, see <http://www.gnu.org/licenses/>.
*/

using GLib;

public abstract class AssistantOperation : Assistant
{
  /*
   * Abstract class for implementation of various common pages in assistant
   *
   * Abstract class that provides various methods that serve as pages in
   * assistant. Required methods that all classes that inherit from this
   * class must implement are create_op, make_confirm_page and
   * get_progress_file_prefix.
   *
   * Pages are shown in the following order:
   * 1. (Optional) Custom configuration pages
   * 2. Setup pages
   * 3. Confirmation page
   * 4. Password page
   * 5. Question page
   * 6. (Required) Progress page
   * 7. Summary
   */
  protected Gtk.Widget confirm_page {get; private set;}
  public signal void closing(bool success);
  
  public bool automatic {get; construct; default = false;}
  protected StatusIcon status_icon;
  protected bool succeeded = false;

  Gtk.Entry encrypt_entry;
  Gtk.Entry encrypt_confirm_entry;
  Gtk.RadioButton encrypt_enabled;
  Gtk.CheckButton encrypt_remember;
  protected Gtk.Widget password_page {get; private set;}
  List<Gtk.Widget> first_password_widgets;
  MainLoop password_ask_loop;
  MainLoop password_find_loop;
  DejaDup.ToggleGroup password_toggles;

  Gtk.Label question_label;
  protected Gtk.Widget question_page {get; private set;}

  Gtk.Label progress_label;
  Gtk.Label progress_file_label;
  Gtk.Label secondary_label;
  Gtk.ProgressBar progress_bar;
  Gtk.TextView progress_text;
  Gtk.ScrolledWindow progress_scroll;
  Gtk.Expander progress_expander;
  protected Gtk.Widget progress_page {get; private set;}
  
  protected Gtk.Label summary_label;
  Gtk.Widget error_widget;
  Gtk.TextView error_text_view;
  protected Gtk.Widget summary_page {get; private set;}
  
  protected Gdk.Pixbuf op_icon {get; private set;}
  protected DejaDup.Operation op;
  uint timeout_id;
  protected bool error_occurred {get; private set;}
  bool gives_progress;

  bool searched_for_passphrase = false;

  bool saved_pos;
  int saved_x;
  int saved_y;

  construct
  {
    icon_name = "deja-dup";
    op_icon = make_op_icon();
    header_icon.pixbuf = op_icon;

    add_custom_config_pages();
    add_setup_pages();
    add_confirm_page();
    add_password_page();
    add_question_page();
    add_progress_page();
    add_summary_page();
    
    canceled.connect(do_cancel);
    closed.connect(do_close);
    prepare.connect(do_prepare);
    delete_event.connect(do_minimize_to_tray);
  }

  /*
   * Creates confirmation page for particular assistant
   *
   * Creates confirmation page that should create confirm_page widget that
   * is presented for final confirmation.
   */
  protected virtual Gtk.Widget? make_confirm_page() {return null;}
  protected virtual void add_setup_pages() {}
  protected virtual void add_custom_config_pages(){}
  /*
   * Creates and calls appropriate operation
   *
   * Creates and calls appropriate operation (Backup, Restore, Status, Files)
   * that is then used to perform various defined tasks on backend. It is
   * also later connected to various signals.
   */
  protected abstract DejaDup.Operation? create_op();
  protected abstract string get_progress_file_prefix();

  protected Gdk.Pixbuf? make_op_icon()
  {
    if (this.icon_name == null)
      return null;
    try {
      var theme = Gtk.IconTheme.get_for_screen(get_screen());
      return theme.load_icon(this.icon_name, 48,
                             Gtk.IconLookupFlags.FORCE_SIZE);
    }
    catch (Error e) {
      warning("%s\n", e.message);
      return null;
    }
  }
  
  bool pulse()
  {
    if (!gives_progress)
      progress_bar.pulse();
    return true;
  }
  
  void show_progress(DejaDup.Operation op, double percent)
  {
    /*
     * Updates prograss bar
     *
     * Updates progress bar with percet provided.
     */
    progress_bar.fraction = percent;
    gives_progress = true;
  }
  
  void set_progress_label(DejaDup.Operation op, string label)
  {
    progress_label.label = label;
    progress_file_label.label = "";
  }
  
  void set_progress_label_file(DejaDup.Operation op, File file, bool actual)
  {
    string prefix;
    if (actual) {
      prefix = get_progress_file_prefix();
      progress_label.label = prefix + " ";
      progress_file_label.label = DejaDup.get_display_name(file);
    }
    else {
      prefix = _("Scanning:");
      progress_label.label = _("Scanning…");
      progress_file_label.label = "";
    }

    string log_line = prefix + " " + file.get_parse_name();

    bool adjustment_at_end = false;
    Gtk.Adjustment adjust = progress_scroll.get_vadjustment();
    if (adjust.value >= adjust.upper - adjust.page_size ||
        adjust.page_size == 0 || // means never been set, means not realized
        !progress_expander.expanded)
      adjustment_at_end = true;

    var buffer = progress_text.buffer;
    if (buffer.get_char_count() > 0)
      log_line = "\n" + log_line;
    if (buffer.get_line_count() >= 100 && adjustment_at_end) {
      // If we're watching text scroll by, save memory by only keeping last 100 lines
      Gtk.TextIter start, line100;
      buffer.get_start_iter(out start);
      buffer.get_iter_at_line(out line100, buffer.get_line_count() - 100);
      buffer.delete(start, line100);
    }
    
    Gtk.TextIter iter;
    buffer.get_end_iter(out iter);
    buffer.insert_text(iter, log_line, (int)log_line.length);
    if (adjustment_at_end)
      adjust.value = adjust.upper;
  }
  
  protected void set_secondary_label(string text)
  {
    if (text != null && text != "") {
      secondary_label.label = "<i>" + text + "</i>";
      secondary_label.show();
    }
    else
      secondary_label.hide();
  }

  protected virtual Gtk.Widget make_progress_page()
  {
    var page = new Gtk.Grid();
    page.orientation = Gtk.Orientation.VERTICAL;
    page.row_spacing = 6;

    int row = 0;

    progress_label = new Gtk.Label("");
    progress_label.xalign = 0.0f;

    progress_file_label = new Gtk.Label("");
    progress_file_label.xalign = 0.0f;
    progress_file_label.ellipsize = Pango.EllipsizeMode.MIDDLE;
    progress_file_label.hexpand = true;

    page.attach(progress_label, 0, row, 1, 1);
    page.attach(progress_file_label, 1, row, 1, 1);
    ++row;

    secondary_label = new Gtk.Label("");
    secondary_label.xalign = 0.0f;
    secondary_label.wrap = true;
    secondary_label.max_width_chars = 30;
    secondary_label.no_show_all = true;
    secondary_label.use_markup = true;
    page.attach(secondary_label, 0, row, 2, 1);
    ++row;

    progress_bar = new Gtk.ProgressBar();
    page.attach(progress_bar, 0, row, 2, 1);
    ++row;

    progress_text = new Gtk.TextView();
    progress_text.editable = false;
    progress_scroll = new Gtk.ScrolledWindow(null, null);
    progress_scroll.set("child", progress_text,
                        "hscrollbar-policy", Gtk.PolicyType.AUTOMATIC,
                        "vscrollbar-policy", Gtk.PolicyType.AUTOMATIC,
                        "border-width", 0);
    progress_expander = new Gtk.Expander.with_mnemonic(_("_Details"));
    progress_expander.set("child", progress_scroll);
    progress_expander.expand = true;
    page.attach(progress_expander, 0, row, 2, 1);
    ++row;

    page.border_width = 12;

    // Reserve space for details + labels
    page.set_size_request(-1, 200);

    return page;
  }
  
  public virtual void show_error(string error, string? detail)
  {
    error_occurred = true;
    
    summary_label.label = error;
    summary_label.wrap = true;
    summary_label.selectable = true;
    summary_label.max_width_chars = 25;
    
    if (detail != null) {
      page_box.set_size_request(300, 200);
      error_widget.no_show_all = false;
      error_widget.show_all();
      error_text_view.buffer.set_text(detail, -1);
    }
    
    go_to_page(summary_page);
    set_header_icon(Gtk.Stock.DIALOG_ERROR);
    page_box.queue_resize();
  }

  protected Gtk.Widget make_password_page()
  {
    int rows = 0;
    Gtk.Widget w, label;

    var page = new Gtk.Grid();
    page.set("row-spacing", 6,
             "column-spacing", 6,
             "border-width", 12);

    w = new Gtk.RadioButton.with_mnemonic(null,
                                          _("_Allow restoring without a password"));
    page.attach(w, 0, rows, 3, 1);
    first_password_widgets.append(w);
    ++rows;

    encrypt_enabled = new Gtk.RadioButton.with_mnemonic_from_widget(w as Gtk.RadioButton,
                                                                    _("_Password-protect your backup"));
    encrypt_enabled.active = true; // always default to encrypted
    page.attach(encrypt_enabled, 0, rows, 3, 1);
    first_password_widgets.append(encrypt_enabled);
    encrypt_enabled.toggled.connect(() => {check_password_validity();});
    ++rows;

    password_toggles = new DejaDup.ToggleGroup.with_button(encrypt_enabled);

    w = new Gtk.Label("    "); // indent
    page.attach(w, 0, rows, 1, 1);
    first_password_widgets.append(w);

    w = new Gtk.Label("<i>%s</i>".printf(
      _("You will need your password to restore your files. You might want to write it down.")));
    w.set("xalign", 0.0f,
          "use-markup", true,
          "max-width-chars", 25,
          "wrap", true);
    page.attach(w, 1, rows, 2, 1);
    password_toggles.add_dependent(w);
    first_password_widgets.append(w);
    ++rows;

    w = new Gtk.Entry();
    w.set("visibility", false,
          "hexpand", true,
          "activates-default", true);
    ((Gtk.Entry)w).changed.connect(() => {check_password_validity();});
    label = new Gtk.Label(_("E_ncryption password"));
    label.set("mnemonic-widget", w,
              "use-underline", true,
              "xalign", 1.0f);
    page.attach(label, 1, rows, 1, 1);
    page.attach(w, 2, rows, 1, 1);
    password_toggles.add_dependent(label);
    password_toggles.add_dependent(w);
    ++rows;
    encrypt_entry = (Gtk.Entry)w;

    // Add a confirmation entry if this is user's first time
    w = new Gtk.Entry();
    w.set("visibility", false,
          "hexpand", true,
          "activates-default", true);
    ((Gtk.Entry)w).changed.connect(() => {check_password_validity();});
    label = new Gtk.Label(_("Confir_m password"));
    label.set("mnemonic-widget", w,
              "use-underline", true,
              "xalign", 1.0f);
    page.attach(label, 1, rows, 1, 1);
    page.attach(w, 2, rows, 1, 1);
    password_toggles.add_dependent(w);
    password_toggles.add_dependent(label);
    ++rows;
    encrypt_confirm_entry = (Gtk.Entry)w;
    first_password_widgets.append(w);
    first_password_widgets.append(label);

    w = new Gtk.CheckButton.with_mnemonic(_("_Show password"));
    ((Gtk.CheckButton)w).toggled.connect((button) => {
      encrypt_entry.visibility = button.get_active();
      encrypt_confirm_entry.visibility = button.get_active();
    });
    page.attach(w, 2, rows, 1, 1);
    password_toggles.add_dependent(w);
    ++rows;

    w = new Gtk.CheckButton.with_mnemonic(_("_Remember password"));
    page.attach(w, 2, rows, 1, 1);
    password_toggles.add_dependent(w);
    ++rows;
    encrypt_remember = (Gtk.CheckButton)w;

    return page;
  }

  protected Gtk.Widget make_question_page()
  {
    int rows = 0;

    var page = new Gtk.Grid();
    page.set("row-spacing", 6,
             "column-spacing", 6,
             "border-width", 12);

    var label = new Gtk.Label("");
    label.set("use-underline", true,
              "wrap", true,
              "max-width-chars", 25,
              "hexpand", true,
              "xalign", 0.0f);
    page.attach(label, 0, rows, 1, 1);
    ++rows;
    question_label = label;

    return page;
  }

  protected virtual Gtk.Widget make_summary_page()
  {
    summary_label = new Gtk.Label("");
    summary_label.set("xalign", 0.0f);
    
    error_text_view = new Gtk.TextView();
    error_text_view.editable = false;
    error_text_view.wrap_mode = Gtk.WrapMode.WORD;
    error_text_view.height_request = 150;

    var scroll = new Gtk.ScrolledWindow(null, null);
    scroll.add(error_text_view);
    scroll.no_show_all = true; // only will be shown if an error occurs
    error_widget = scroll;
    
    var page = new Gtk.Box(Gtk.Orientation.VERTICAL, 6);
    page.set("child", summary_label,
             "child", error_widget,
             "border-width", 12);
    page.child_set(summary_label, "expand", false);
    page.child_set(error_widget, "expand", true);
    
    return page;
  }

  void add_confirm_page()
  {
    /*
     * Adds confirm page to the sequence of pages
     *
     * Adds confirm_page widget to the sequence of pages in assistant.
     */
    var page = make_confirm_page();
    if (page == null)
      return;
    append_page(page, Type.SUMMARY);
    set_page_title(page, _("Summary"));
    confirm_page = page;
  }

  void add_progress_page()
  {
    var page = make_progress_page();
    append_page(page, Type.PROGRESS);
    progress_page = page;
  }

  void add_password_page()
  {
    var page = make_password_page();
    append_page(page, Type.INTERRUPT);
    password_page = page;
  }

  void add_question_page()
  {
    var page = make_question_page();
    append_page(page, Type.INTERRUPT);
    question_page = page;
  }

  void add_summary_page()
  {
    var page = make_summary_page();
    append_page(page, Type.FINISH);
    summary_page = page;
  }
  
  protected virtual void apply_finished(DejaDup.Operation op, bool success, bool cancelled)
  {
    if (status_icon != null) {
      status_icon.done(success, cancelled);
      status_icon = null;
    }
    this.op = null;

    if (cancelled) {
      if (success) // stop (resume later) vs cancel
        Gtk.main_quit();
      else
        do_close();
    }
    else {
      if (success) {
        succeeded = true;
        go_to_page(summary_page);
      }
      else // show error
        force_visible(false);
    }
  }

  protected void ensure_status_icon(DejaDup.Operation o)
  {
    if (status_icon == null) {
      status_icon = StatusIcon.create(this, o, automatic);
      status_icon.show_window.connect((s) => {force_visible(true);});
      status_icon.hide_all.connect((s) => {hide_everything();});
    }
  }

  protected async void do_apply()
  {
    /*
     * Applies/starts operation that was configured during assistant process and
     * connect appropriate signals
     *
     * Mounts appropriate backend, creates child operation, connects signals to
     * handler functions and starts operation.
     */
    op = create_op();
    if (op == null) {
      show_error(_("Failed with an unknown error."), null);
      return;
    }

    op.done.connect(apply_finished);
    op.raise_error.connect((o, e, d) => {show_error(e, d);});
    op.passphrase_required.connect(get_passphrase);
    op.action_desc_changed.connect(set_progress_label);
    op.action_file_changed.connect(set_progress_label_file);
    op.progress.connect(show_progress);
    op.question.connect(show_question);
    op.backend.mount_op = new MountOperationAssistant(this);
    op.backend.pause_op.connect(pause_op);

    ensure_status_icon(op);

    op.start();
  }

  protected virtual void do_prepare(Assistant assist, Gtk.Widget page)
  {
    /*
     * Prepare page in assistant
     *
     * Prepares every page in assistant for various operations. For example, if 
     * user returns to confirmation page from progress page, it is necessary
     * to kill running operation. If user advances to progress page, it runs
     * do_apply and runs the needed operation.
     *
     * do_prepare is run when user switches pages and not when pages are built.
     */

    if (timeout_id > 0) {
      Source.remove(timeout_id);
      timeout_id = 0;
    }
    
    if (page == confirm_page) {
      if (op != null) {
        op.done.disconnect(apply_finished);
        op.cancel(); // in case we just went back from progress page
        op = null;
      }
    }
    else if (page == progress_page) {
      progress_bar.fraction = 0;
      timeout_id = Timeout.add(250, pulse);
      if (op != null && op.needs_password) {
        // Operation is waiting for password
        provide_password();
      }
      else if (op == null)
        do_apply();
    }
    else if (page == password_page)
      set_header_icon(Gtk.Stock.DIALOG_AUTHENTICATION);
  }

  // Make Deja Dup invisible, used when we are shutting down or some such.
  public void hide_everything()
  {
    hide();
    if (status_icon != null) {
      status_icon.done(false, true);
      status_icon = null; // hide immediately to seem responsive
    }
  }

  protected virtual void do_cancel()
  {
    hide_everything();
    if (op != null) {
      op.cancel(); // do_close will happen in done() callback
    }
    else
      do_close();
  }

  bool do_minimize_to_tray(Gdk.Event event)
  {
    if (is_interrupted() || op == null)
      do_cancel(); // instead, do the normal cancel operation
    else
      hide_for_now ();

    return true;
  }
  
  protected virtual void do_close()
  {
    if (timeout_id > 0) {
      Source.remove(timeout_id);
      timeout_id = 0;
    }

    closing(succeeded);

    DejaDup.destroy_widget(this);
  }

  public void force_visible(bool user_click)
  {
    show_to_user(this, Gtk.get_current_event_time(), user_click);
  }

  bool user_focused(Gtk.Widget win, Gdk.EventFocus e)
  {
    ((Gtk.Window)win).urgency_hint = false;
    win.focus_in_event.disconnect(user_focused);
    return false;
  }

  void show_to_user(Gtk.Window win, uint time, bool user_click)
  {
    win.focus_on_map = user_click;
    if (saved_pos)
      win.move(saved_x, saved_y);
    if (user_click)
      win.present_with_time(time);
    else if (!win.is_active || !win.visible) {
      win.urgency_hint = true;
      win.show();
      Idle.add(() => {
        win.focus_in_event.connect(user_focused);
        return false;
      });
    }
  }

  void found_passphrase(GnomeKeyring.Result result, string? str)
  {
    if (str != null) {
      op.set_passphrase(str);
    }
    else {
      ask_passphrase();
    }
    password_find_loop.quit();
    password_find_loop = null;
  }

  protected void get_passphrase()
  {
    // DEJA_DUP_TESTING only set when we are in test suite
    if (!searched_for_passphrase && !DejaDup.in_testing_mode()) {
      // First, try user's keyring
      GnomeKeyring.find_password(PASSPHRASE_SCHEMA,
                                 found_passphrase,
                                 "owner", Config.PACKAGE,
                                 "type", "passphrase");
      // If we get asked for passphrase again, it is because a
      // saved or entered passphrase didn't work.  So don't bother
      // searching a second time.
      searched_for_passphrase = true;
      // block until found
      password_find_loop = new MainLoop(null);
      password_find_loop.run();
    }
    else {
      // just jump straight to asking user
      ask_passphrase();
    }
  }

  void save_password_callback(GnomeKeyring.Result result)
  {
  }

  void check_password_validity()
  {
    if (!encrypt_enabled.active) {
      allow_forward(true);
      return;
    }

    var passphrase = encrypt_entry.get_text();
    if (passphrase == "") {
      allow_forward(false);
      return;
    }

    if (encrypt_confirm_entry.visible) {
      var passphrase2 = encrypt_confirm_entry.get_text();
      var valid = (passphrase == passphrase2);
      allow_forward(valid);
    }
    else
      allow_forward(true);
  }

  void configure_password_page(bool first)
  {
    if (first)
      set_page_title(password_page, _("Require Password?"));
    else
      set_page_title(password_page, _("Encryption Password Needed"));
    foreach (Gtk.Widget w in first_password_widgets) {
      w.visible = first;
    }
    check_password_validity();
    encrypt_entry.select_region(0, -1);
    encrypt_entry.grab_focus();
  }

  void stop_password_loop(Assistant dlg, int resp)
  {
    Idle.add(() => {
      password_ask_loop.quit();
      password_ask_loop = null;
      return false;
    });
    response.disconnect(stop_password_loop);
  }

  protected void ask_passphrase(bool first = false)
  {
    op.needs_password = true;
    interrupt(password_page);
    configure_password_page(first);
    force_visible(false);
    // pause until we can provide password by entering new main loop
    password_ask_loop = new MainLoop(null);
    response.connect(stop_password_loop);
    password_ask_loop.run();
  }

  protected void provide_password()
  {
    var passphrase = "";

    if (encrypt_enabled.active)
      passphrase = encrypt_entry.get_text().strip();

    if (passphrase != "") {
      // Save it
      if (encrypt_remember.active) {
        GnomeKeyring.store_password(PASSPHRASE_SCHEMA,
                                    GnomeKeyring.DEFAULT,
                                    _("Backup encryption password"),
                                    passphrase, save_password_callback,
                                    "owner", Config.PACKAGE,
                                    "type", "passphrase");
      }
    }

    op.set_passphrase(passphrase);
  }

  void stop_question(Assistant dlg, int resp)
  {
    Gtk.main_quit();
    response.disconnect(stop_question);
  }

  void show_question(DejaDup.Operation op, string title, string message)
  {
    set_page_title(question_page, title);
    question_label.label = message;
    interrupt(question_page);
    force_visible(false);
    response.connect(stop_question);
    Gtk.main();
  }

  protected void pause_op(DejaDup.Backend back, string? header, string? msg)
  {
    // Basically a question without a response expected
    if (header == null) // unpause
      go_forward();
    else {
      set_page_title(question_page, header);
      question_label.label = msg;
      interrupt(question_page, false);
      force_visible(false);
    }
  }
}

