/* -*- Mode: Vala; indent-tabs-mode: nil; tab-width: 2 -*-
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 * SPDX-FileCopyrightText: Michael Terry
 */

// This holds an abstraction of an Operation, exposing just a few dynamic
// properties that UIs can bind to (summary & button).
// It also handles some of the GTK-side business logic (like dialogs).
// This is meant to be cheap and created per-widget.
// All global GTK-business logic is in OperationLauncher.

using GLib;

public class OperationWrapper : Object
{
  public DejaDup.Operation operation {get; protected set;}
  public Gtk.Widget parent {get; construct;}

  // If this is true, cancel buttons in dialogs will stop() the operation.
  public bool cancel_stops {get; set;}

  public bool active {get; private set;}
  public string summary {get; private set; default = "";}
  public string button_label {get; private set; default = "";}
  public bool button_suggested {get; private set;}
  public bool error_mode {get; private set;}
  public double fraction {get; private set;}
  public bool pulsing { get {return pulse_id != 0;} }

  public signal void pulse();

  // Event based signals, for when that's helpful (like notifications)
  public signal void attention_needed(bool needs_input);
  public signal void pause_changed(bool paused);
  public signal void succeeded(bool incomplete);
  public signal void done(bool cancelled);

  public OperationWrapper(DejaDup.Operation operation, Gtk.Widget parent)
  {
    Object(operation: operation, parent: parent);
    connect_operation();
  }

  public void click()
  {
    if (button_action != null)
      button_action();
  }

  // used when the user is in-app and we want to show a dialog to save them
  // a click (but we don't want to say, open an oauth tab in their browser
  // automatically)
  public void auto_click()
  {
    if (button_is_dialog)
      click();
  }

  public void clear()
  {
    operation = null;
  }

  // #############
  // Private
  // #############

  enum Mode {
    PROGRESS,
    ERROR_PERSISTENT, // stays after operation
    ERROR_TEMPORARY, // dies with operation
  }

  delegate void ButtonAction();

  Mode mode = Mode.PROGRESS;
  bool progress_enabled = false;
  uint pulse_id;
  ButtonAction button_action;
  bool button_is_dialog;

  void connect_operation() {
    operation.started.connect(handle_started);
    operation.done.connect(handle_done);
    operation.raise_error.connect(handle_error);
    operation.action_desc_changed.connect(handle_action_desc);
    operation.progress.connect(handle_progress);
    operation.passphrase_required.connect(handle_passphrase_required);
    operation.backend_password_required.connect(handle_backend_password_required);
    operation.question.connect(handle_question);
    operation.mount_op_required.connect(handle_mount_op_required);
    operation.pause_changed.connect(handle_pause_changed);
  }

  // Note: does not change progress bar, call set_progress_enabled too
  void reset()
  {
    summary = "";
    button_label = "";
    button_suggested = false;
    error_mode = false;

    mode = Mode.PROGRESS;
    button_action = null;
    button_is_dialog = false;
  }

  void set_progress_enabled(bool enabled)
  {
    progress_enabled = enabled;
    if (enabled && fraction == 0 && pulse_id == 0)
      pulse_id = Timeout.add(250, send_pulse);
    else if (!enabled) {
      if (pulse_id > 0)
        Source.remove(pulse_id);
      pulse_id = 0;
      // reset to clean look (not pulsing) by changing off zero and back
      fraction = 1;
      fraction = 0;
    }
    notify_property("pulsing");
  }

  bool send_pulse()
  {
    if (fraction == 0 && progress_enabled) {
      pulse();
      return Source.CONTINUE;
    } else {
      pulse_id = 0;
      notify_property("pulsing");
      return Source.REMOVE;
    }
  }

  void activate_error_mode(Mode m)
  {
    mode = m;
    error_mode = m == ERROR_PERSISTENT;
  }

  // #############
  // Actions
  // #############

  void dialog_closed()
  {
    if (cancel_stops)
      operation.stop();
  }

  void handle_started()
  {
    fraction = 0;
    set_progress_enabled(true);
    reset();
    active = true;
  }

  void handle_done(bool success, bool cancelled, string? detail)
  {
    if (cancelled) {
      reset();
      active = false;
    }
    else if (success && detail != null) {
      reset();

      // Note: keep this language in sync with notification sent from OperationLauncher
      if (operation.mode == DejaDup.ToolJob.Mode.BACKUP)
        summary = _("Backup completed, but not all files were successfully backed up");
      else if (operation.mode == DejaDup.ToolJob.Mode.RESTORE)
        summary = _("Restore completed, but not all files were successfully restored");

      button_label = _("_Details");
      button_action = () => {
        ErrorDialog.create(parent, detail);
      };
      button_is_dialog = true;

      succeeded(true);
    } else if (success) {
      reset();

      if (operation.mode == DejaDup.ToolJob.Mode.BACKUP)
        summary = _("Backup completed");
      else if (operation.mode == DejaDup.ToolJob.Mode.RESTORE)
        summary = _("Restore completed");

      active = false;
      succeeded(false);
    }
    // else in error case, we are already showing the error

    set_progress_enabled(false);
    done(cancelled);
  }

  void handle_pause_changed(bool paused)
  {
    pause_changed(paused);
  }

  void handle_action_desc(string action)
  {
    set_progress_enabled(true);
    reset();
    summary = action;
    button_action = operation.stop;
    if (operation.mode == DejaDup.ToolJob.Mode.BACKUP)
      button_label = _("_Pause");
    else
      button_label = _("_Stop");
  }

  void handle_progress(double percent)
  {
    fraction = percent;
    set_progress_enabled(true);
  }

  void handle_error(string errstr, string? detail)
  {
    reset();
    set_progress_enabled(false);
    activate_error_mode(Mode.ERROR_PERSISTENT);

    if (
      operation.mode == DejaDup.ToolJob.Mode.BACKUP ||
      operation.mode == DejaDup.ToolJob.Mode.VERIFY_BASIC ||
      operation.mode == DejaDup.ToolJob.Mode.VERIFY_CLEAN
    )
      summary = _("Failed to back up");
    else if (
      operation.mode == DejaDup.ToolJob.Mode.LIST_SNAPSHOTS ||
      operation.mode == DejaDup.ToolJob.Mode.LIST_FILES
    )
      summary = _("Failed to list files");
    else
      summary = _("Failed to restore");

    button_label = _("_Details");

    button_action = () => {
      ErrorDialog.create(parent, errstr, detail);
    };
    button_is_dialog = true;

    attention_needed(false);
  }

  async void _ask_passphrase(string heading, string body, bool first, bool nag)
  {
    var dialog = new PassphraseDialog();
    dialog.heading = heading;
    dialog.body = body;
    dialog.label = _("E_ncryption password");
    dialog.first_time = first;
    dialog.nag_mode = nag;

    var response = yield dialog.choose(parent, null);
    if (response != "continue") {
      dialog_closed();
      return;
    }

    yield operation.set_passphrase(dialog.passphrase, dialog.remember);
  }

  void handle_passphrase_required(bool repeat, bool first)
  {
    reset();
    set_progress_enabled(false);
    activate_error_mode(Mode.ERROR_TEMPORARY);

    button_label = _("_Enter Password…");

    string header = "", body = "";
    if (repeat) {
      summary = _("Wrong encryption password, try again");
      header = _("Wrong Encryption Password");
      body = _("Try again.");
    }
    else if (first) {
      summary = _("Encryption setup needed");
      button_label = _("_Details");
      header = _("Require Password?");
      body = _("You will need your password to restore your files. You might want to write it down.");
    }
    else {
      summary = _("Encryption password needed");
      header = _("Encryption Password Needed");
    }

    var nag = !operation.use_cached_password;
    if (nag) {
      body = _("In order to check that you will be able to retrieve your files in the case " +
               "of an emergency, please enter your encryption password again to perform a " +
               "brief restore test.");
    }

    button_action = () => { _ask_passphrase.begin(header, body, first, nag); };
    button_is_dialog = true;

    attention_needed(true);
  }

  async void _ask_backend_password(string body, string label)
  {
    var dialog = new PassphraseDialog();
    dialog.heading = _("Enter Password");
    dialog.body = body;
    dialog.label = label;

    var response = yield dialog.choose(parent, null);
    if (response != "continue" || dialog.passphrase == "") {
      dialog_closed();
      return;
    }

    yield operation.set_backend_password(dialog.passphrase, dialog.remember);
  }

  void handle_backend_password_required(string desc, string label, string? error)
  {
    reset();
    set_progress_enabled(false);
    activate_error_mode(Mode.ERROR_TEMPORARY);

    summary = error == null ? desc : error;
    button_label = _("_Enter Password…");
    button_suggested = true;
    button_action = () => {
      _ask_backend_password.begin(summary, label);
    };
    button_is_dialog = true;

    attention_needed(true);
  }

  void handle_mount_op_required()
  {
    reset();
    set_progress_enabled(false);
    activate_error_mode(Mode.ERROR_TEMPORARY);

    summary = _("Authentication needed");
    button_label = _("_Connect…");
    button_suggested = true;
    button_action = () => {
      var mount_op = new Gtk.MountOperation(parent.root as Gtk.Window);
      operation.set_mount_op(mount_op);
    };

    attention_needed(false);
  }

  // Kinds of questions: oauth connect, packagekit install, hostname changed.
  // They all allow throwing up a dialog with a cancel button. That way you
  // can either bail on the operation or continue.
  void handle_question(string summary, string? header, string? description, string? action, bool safe)
  {
    reset();
    set_progress_enabled(false);
    activate_error_mode(Mode.ERROR_TEMPORARY);

    this.summary = summary;

    button_label = _("_Details");
    button_action = () => {
      var dlg = new Adw.AlertDialog(header, description);
      dlg.body_use_markup = true;
      dlg.add_response("close", _("_Cancel"));
      dlg.add_response("continue", action == null ? _("Co_ntinue") : action);
      if (safe) {
        dlg.default_response = "continue";
        dlg.set_response_appearance("continue", Adw.ResponseAppearance.SUGGESTED);
      }
      dlg.response["close"].connect(() => { dialog_closed(); });
      dlg.response["continue"].connect(() => { operation.resume(); });
      dlg.choose.begin(parent, null);
    };
    button_is_dialog = true;

    attention_needed(true);
  }
}
