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

// All the similar Operation* classes can be a little confusing.
// Here's a breakdown.
//
// OperationLauncher:
// - singleton, GUI side
// - How new operations get started, makes sure there's only one active
// - Handles some global GUI business like opening a URL, inhibiting, and
//   many desktop notifications (others are handled by DejaDupApp)
//
// OperationWrapper:
// - multi-instance, GUI side
// - Consolidates all of an operation's interactions into a few properties
//   and signals that are easier for GUIs to bind to (i.e. a label and button)
// - Used by things like OperationBanner
//
// OperationBanner:
// - multi-instance, GUI side
// - Connects to the global OperationWatcher
//
// OperationStatusPage:
// - multi-instance, GUI side
// - Connects to a single operation
//
// OperationWatcher:
// - singleton, library side
// - Is an abstraction for "the currently running operation" without having
//   to disconnect and reconnect to an operation
// - Only watches "important" operations like backup/restore, not list
//   operations
// - Used by OperationBanner
//
// Operation:
// - multi-instance, library side
// - Parent class of the various OperationBackup, OperationRestore, etc

using GLib;

public class OperationLauncher : Object
{
  static OperationLauncher instance;
  public static OperationLauncher get_instance()
  {
    if (instance == null)
      instance = new OperationLauncher();
    return instance;
  }

  public DejaDup.Operation operation;
  public OperationWrapper wrapper;

  public void start_backup(bool automatic)
  {
    if (!stop_op_if_dormant()) {
      var msg = _("Failed to start backup; there's already an ongoing operation");
      DejaDupApp.get_instance().get_app_window().add_toast(msg);
      return;
    }

    operation = new DejaDup.OperationBackup(DejaDup.Backend.get_default(), automatic);
    connect_op(true);
    inhibit(automatic);
    operation.start.begin();

    if (automatic)
      Notifications.automatic_backup_started();
  }

  public void start_restore(
    DejaDup.Operation.State state,
    string restore_location,
    DejaDup.FileTree tree,
    string tag,
    List<File> files
  ) {
    if (!stop_op_if_dormant()) {
      var msg = _("Failed to restore; there's already an ongoing operation");
      DejaDupApp.get_instance().get_app_window().add_toast(msg);
      return;
    }

    operation = new DejaDup.OperationRestore(
      state.backend, restore_location, tree, tag, files
    );
    operation.set_state(state);
    connect_op(true);
    inhibit();
    operation.start.begin();
  }

  public DejaDup.OperationStatus create_list_snapshots(
    DejaDup.Backend? backend = null
  ) throws Error
  {
    if (!stop_op_if_dormant())
      throw new SpawnError.FAILED(_("There's already an ongoing operation"));

    var op_backend = backend;
    if (op_backend == null)
      op_backend = DejaDup.Backend.get_default();

    operation = new DejaDup.OperationStatus(op_backend);
    connect_op(false);
    return (DejaDup.OperationStatus)operation;
  }

  public DejaDup.OperationFiles create_list_files(
    string tag,
    DejaDup.Operation.State? state = null
  ) throws Error
  {
    if (!stop_op_if_dormant())
      throw new SpawnError.FAILED(_("There's already an ongoing operation"));

    DejaDup.Backend backend;
    if (state != null)
      backend = state.backend;
    else
      backend = DejaDup.Backend.get_default();

    operation = new DejaDup.OperationFiles(backend, tag);
    if (state != null)
      operation.set_state(state);
    connect_op(false);
    return (DejaDup.OperationFiles)operation;
  }

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

  uint inhibit_id;

  // Returns true if we can continue with a new operation.
  // False means that another operation is running.
  // This closes any open alert dialogs.
  bool stop_op_if_dormant()
  {
    if (operation == null)
      return true;

    if (!operation.stop_if_dormant())
      return false; // operation is still going

    // OK, we just stopped a dormant operation. Let's do some cleanup.

    // Disconnect now, while job is stopped async in background
    handle_done(wrapper, true);

    // Close open alert dialogs from previous operation.
    var app = DejaDupApp.get_instance();
    var window = app.get_app_window();
    while ((window.visible_dialog as Adw.AlertDialog) != null)
      window.visible_dialog.close();

    return true;
  }

  // "Primary" here means a major operation that should be in banners (versus
  // status page operations which are more ephemeral)
  void connect_op(bool primary_operation)
  {
    if (primary_operation)
      DejaDup.OperationWatcher.get_instance().set_current_op(operation);

    operation.open_url.connect(handle_open_url);

    wrapper = new OperationWrapper(operation, DejaDupApp.get_instance().get_app_window());
    wrapper.cancel_stops = primary_operation;
    wrapper.done.connect(handle_done);
    wrapper.attention_needed.connect(handle_attention_needed);
    wrapper.pause_changed.connect(handle_pause_changed);
    wrapper.succeeded.connect(handle_succeeded);
  }

  void handle_done(OperationWrapper signal_wrapper, bool cancelled)
  {
    SignalHandler.disconnect_matched(signal_wrapper.operation, SignalMatchType.DATA,
                                     0, 0, null, null, this);
    SignalHandler.disconnect_matched(signal_wrapper.operation, SignalMatchType.DATA,
                                     0, 0, null, null, this);

    if (signal_wrapper != wrapper)
      return;

    operation = null;
    wrapper = null;

    if (cancelled)
      Notifications.withdraw();

    uninhibit();
  }

  void handle_open_url(string url)
  {
    handle_open_url_async.begin(url);
  }

  async void handle_open_url_async(string url)
  {
    var launcher = new Gtk.UriLauncher(url);
    var app = DejaDupApp.get_instance();
    try {
      yield launcher.launch(app.get_app_window(), null);
    } catch (Error e) {
      yield DejaDup.run_error_dialog(app.get_app_window(), _("Failed to Open URL"), e.message);
    }
  }

  void handle_attention_needed(bool needs_input)
  {
    // There are some general possibilities for the user's attention:
    // - Sitting in Deja Dup, waiting.
    //   -> In this case, we just open the interruption dialog directly.
    // - Sitting in Deja Dup, but busy looking at an existing dialog like the
    //   About dialog or keyboard shortcuts.
    //   -> In this case, we just send a desktop-wide notification, to avoid
    //      interrupting them.
    // - Off in some other app.
    //   -> In this case, we open the interruption dialog and also send a
    //      desktop-wide notification to bring the user here.
    var app = DejaDupApp.get_instance();
    var window = app.get_app_window();
    if (app.is_focused()) {
      if (window.get_visible_dialog() == null) {
        // The user is just sitting in Deja Dup - activate the button now
        if (needs_input)
          wrapper.auto_click();
      } else {
        // The user is interacting with Deja Dup, but is busy
        Notifications.attention_needed(window, wrapper.summary);
      }
    } else {
      // The user is in some other window
      wrapper.auto_click();
      Notifications.attention_needed(window, wrapper.summary);
    }
  }

  void handle_pause_changed(bool paused)
  {
    var app = DejaDupApp.get_instance();
    if (!paused)
      Notifications.withdraw();
    else if (!app.is_focused())
      Notifications.operation_blocked(wrapper.summary);
  }

  void handle_succeeded(bool incomplete)
  {
    var important = operation.mode == DejaDup.ToolJob.Mode.RESTORE || incomplete;

    string header = null, body = null;
    if (operation.mode == DejaDup.ToolJob.Mode.BACKUP) {
      header = _("Backup completed");
      if (incomplete)
        body = _("But not all files were successfully backed up");
    }
    else if (operation.mode == DejaDup.ToolJob.Mode.RESTORE) {
      header = _("Restore completed");
      if (incomplete)
        body = _("But not all files were successfully restored");
    }
    else
      return;

    Notifications.operation_succeeded(header, body, important);
  }

  void inhibit(bool automatic = false)
  {
    uninhibit();

    string message;
    Gtk.ApplicationInhibitFlags flags;

    if (operation.mode == DejaDup.ToolJob.Mode.BACKUP) {
      message = _("Backup in progress");
      flags = Gtk.ApplicationInhibitFlags.SUSPEND;

      // We don't prevent logging out for automatic backups, because they can
      // just be resumed later and weren't triggered by user actions.
      // So they aren't worth preventing the user from doing something and might
      // be a surprising addition to the logout dialog.
      if (!automatic)
        flags |= Gtk.ApplicationInhibitFlags.LOGOUT;

    } else if (operation.mode == DejaDup.ToolJob.Mode.RESTORE) {
      message = _("Restore in progress");
      flags = Gtk.ApplicationInhibitFlags.LOGOUT |
              Gtk.ApplicationInhibitFlags.SUSPEND;
    } else {
      return;
    }

    var app = DejaDupApp.get_instance();
    app.hold();

    inhibit_id = app.inhibit(app.get_app_window(), flags, message);
  }

  void uninhibit()
  {
    if (inhibit_id == 0)
      return;

    var app = DejaDupApp.get_instance();

    app.uninhibit(inhibit_id);
    inhibit_id = 0;

    app.release();
  }
}
