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

using GLib;

public class DejaDupApp : Adw.Application
{
  WeakRef main_window;
  SimpleAction quit_action = null;

  const OptionEntry[] OPTIONS = {
    {"version", 0, 0, OptionArg.NONE, null, N_("Show version"), null},
    {"backup", 0, 0, OptionArg.NONE, null, N_("Immediately start a backup"), null},
    {"auto", 0, OptionFlags.HIDDEN, OptionArg.NONE, null, null, null},
    {"delay", 0, OptionFlags.HIDDEN, OptionArg.STRING, null, null, null},
    {"prompt", 0, OptionFlags.HIDDEN, OptionArg.NONE, null, null, null},
    {"", 0, 0, OptionArg.FILENAME_ARRAY, null, null, null}, // remaining
    {null}
  };

  const ActionEntry[] ACTIONS = {
    {"backup", backup},
    {"backup-auto", backup_auto},
    {"backup-auto-stop", backup_auto_stop},
    {"prompt-ok", prompt_ok},
    {"prompt-cancel", prompt_cancel},
    {"delay", delay, "s"},
    {"help", help},
    {"about", about},
    {"close", close_window},
    // redundant with default activation usually, but is used by notifications
    {"show", show},
  };

  static DejaDupApp instance;

  public static DejaDupApp get_instance() {
    if (instance == null)
      instance = new DejaDupApp();
    return instance;
  }

  private DejaDupApp()
  {
    Object(
      application_id: Config.APPLICATION_ID,
      flags: ApplicationFlags.HANDLES_COMMAND_LINE |
             // HANDLES_OPEN is required to support Open calls over dbus, which
             // we use for our registered custom schemes (which support our
             // oauth2 workflow).
             ApplicationFlags.HANDLES_OPEN
    );
    add_main_option_entries(OPTIONS);
  }

  public override int handle_local_options(VariantDict options)
  {
    if (options.contains("version")) {
      print("%s %s\n", "deja-dup", Config.VERSION);
      return 0;
    }
    return -1;
  }

  public override int command_line(ApplicationCommandLine command_line)
  {
    var options = command_line.get_options_dict();

    File[] files = {};
    if (options.contains("")) {
      var variant = options.lookup_value("", VariantType.BYTESTRING_ARRAY);
      foreach (var filename in variant.get_bytestring_array())
        files += command_line.create_file_for_arg(filename);
    }

    if (options.contains("backup")) {
      backup_full(options.contains("auto"));
    }
    else if (options.contains("delay")) {
      string reason = null;
      options.lookup("delay", "s", ref reason);
      Notifications.automatic_backup_delayed(reason);
    }
    else if (options.contains("prompt")) {
      Notifications.prompt();
    }
    else if (files.length > 0) {
      // If we were called without a mode (like --restore) but with file arguments,
      // let's do our "Open" action (which is mostly used for our oauth flow).
      // That oauth flow can happen via command line in some environments like
      // snaps, whereas the dbus Open call might happen for flatpaks. Regardless
      // of how they come in, treat them the same.
      open(files, "");
    }
    else {
      activate();
    }

    return 0;
  }

  void ensure_app_window()
  {
    if (get_app_window() == null) {
      main_window.set(new MainWindow());
      get_app_window().application = this;
    }
  }

  public override void activate()
  {
    base.activate();

    ensure_app_window();
    get_app_window().present();
  }

  public override void open(GLib.File[] files, string hint)
  {
    var operation = OperationLauncher.get_instance().operation;

    // We might be in middle of oauth flow, and are given an expected redirect
    // uri like 'com.googleusercontent.apps.123:/oauth2redirect?code=xxx'
    if (files.length == 1 && operation != null)
    {
      var provided_uri = files[0].get_uri();
      if (operation.set_oauth_token(provided_uri)) {
        activate();
        return;
      }
    }

    // Got passed files, but we don't know what to do with them.
    foreach (var file in files)
      warning("Ignoring unexpected file: %s", file.get_parse_name());
  }

  public MainWindow? get_app_window()
  {
    return main_window.get() as MainWindow;
  }

  public bool is_focused()
  {
    var window = get_app_window();
    return window != null && window.is_active && window.visible;
  }

  void show()
  {
    activate();
  }

  bool exit_cleanly() // used for signal handlers
  {
    shutdown();
    quit();
    return Source.REMOVE;
  }

  void close_window() // used for ctrl+w/ctrl+q
  {
    var window = get_app_window();
    if (window != null)
      window.close();
  }

  // Eventually, when we can assume that the system supports color schemes,
  // we can drop this legacy check.
  bool has_dark_gtk_theme()
  {
    // libadwaita will call this for us, but we need it now to check the
    // settings - it's safe to call this multiple times.
    Gtk.init();

    var theme_name = Gtk.Settings.get_default().gtk_theme_name.casefold();
    var dark_suffix = "-dark".casefold();
    return theme_name.has_suffix(dark_suffix); // very rough heuristic
  }

  public override void startup()
  {
    // grab this before libadwaita overrides it
    var dark_gtk_theme = has_dark_gtk_theme();

    base.startup();
    DejaDup.gui_initialize();

    set_accels_for_action("app.help", {"F1"});
    set_accels_for_action("app.close", {"<Control>w", "<Control>q"});
    set_accels_for_action("restore.select-all", {"<Control>a"});
    set_accels_for_action("restore.go-up", {"<Alt>Up"});
    set_accels_for_action("restore.search", {"<Control>f"});

    add_action_entries(ACTIONS, this);
    quit_action = lookup_action("close") as SimpleAction;

    // Cleanly exit (shutting down duplicity as we go)
    Unix.signal_add(ProcessSignal.HUP, exit_cleanly);
    Unix.signal_add(ProcessSignal.INT, exit_cleanly);
    Unix.signal_add(ProcessSignal.TERM, exit_cleanly);

    var display = Gdk.Display.get_default();
    var style_manager = Adw.StyleManager.get_for_display(display);

    if (!style_manager.system_supports_color_schemes && dark_gtk_theme) {
      // We can't follow the gtk theme as it changes, but this is good
      // enough for now - start up with the right dark/light preference.
      style_manager.color_scheme = Adw.ColorScheme.PREFER_DARK;
    }

    if (DejaDup.in_demo_mode())
    {
      // Use default GNOME settings as much as possible.
      // The goal here is that we are suitable for screenshots.
      // https://gitlab.gnome.org/GNOME/gsettings-desktop-schemas
      var gtksettings = Gtk.Settings.get_for_display(display);

      gtksettings.gtk_decoration_layout = ":close";
      gtksettings.gtk_font_name = "Adwaita Sans 11";
      gtksettings.gtk_icon_theme_name = "Adwaita";
    }
  }

  public override void shutdown()
  {
    var current_op = OperationLauncher.get_instance().operation;
    if (current_op != null)
      current_op.stop();
    DejaDup.MountManager.get_instance().unmount_all();
    Notifications.withdraw();
    base.shutdown();
  }

  public void delay(GLib.SimpleAction action, GLib.Variant? parameter)
  {
    string reason = null;
    parameter.get("s", ref reason);
    Notifications.automatic_backup_delayed(reason);
  }

  void help()
  {
    // This does not use Gtk.UriLauncher because we don't want to escape any
    // sandboxes, if they exist. If we did that, the host's yelp would likely
    // not be able to find our help files inside our flatpak.
    // (That problem is https://gitlab.gnome.org/GNOME/yelp/-/issues/192)
    //
    // You think we'd just use Gtk.show_uri, but that is deprecated and throws
    // annoying compile-time warnings. So instead, just manually do what it
    // does under the covers, which will launch a local copy of yelp from
    // org.gnome.Platform or direct from the host if we aren't sandboxed.
    var context = Gdk.Display.get_default().get_app_launch_context();
    var uri = "help:" + Config.PACKAGE;
    AppInfo.launch_default_for_uri_async.begin(uri, context, null);
  }

  void about()
  {
    var dialog = new Adw.AboutDialog();
    dialog.application_icon = Config.ICON_NAME;
    dialog.application_name = Environment.get_application_name();
    dialog.artists = {
      "Barbara Muraus",
      "Jakub Steiner",
    };
    dialog.debug_info = DebugInfo.get_debug_info();
    dialog.debug_info_filename = "deja-dup-debug.txt";
    dialog.developers = {
      "Michael Terry",
    };
    dialog.issue_url = "https://gitlab.gnome.org/World/deja-dup/-/issues/new";
    dialog.license_type = Gtk.License.GPL_3_0;
    dialog.release_notes = """
      <p>50.1</p>
      <ul>
        <li>Show disk usage of include/exclude folders in the preferences screen</li>
        <li>Be less aggressive when cleaning up /tmp files, to avoid hitting other Duplicity or
            Restic processes on the system</li>
        <li>Duplicity: avoid a possible error at the end of a backup warning about not overwriting
            a "metadata" file (an internal canary file we insert into Duplicity backups)</li>
      </ul>
      <p>50.0</p>
      <ul>
        <li>If a network URL has an unrecognized scheme like `nope://`, give a nicer error message</li>
        <li>Explain that rest:// URLs are not supported, if you try to use one</li>
        <li>Duplicity: fix a race condition between reading messages and it exiting</li>
        <li>Restic: avoid backing up the restic repository, if local</li>
        <li>Restic: when restoring using the flatpak release on Ubuntu, fall back to the old in-app browser
            if apparmor rejects our FUSE mount attempt</li>
      </ul>
    """;
    dialog.support_url = "https://discourse.gnome.org/tags/c/applications/7/deja-dup";
    dialog.translator_credits = _("translator-credits");
    dialog.version = Config.VERSION;
    dialog.website = "https://dejadup.org/";
    dialog.present(get_app_window());
  }

  public void backup()
  {
    if (OperationLauncher.get_instance().operation != null) {
      activate();
    } else {
      backup_full(false);
    }
  }

  public void backup_auto()
  {
    if (OperationLauncher.get_instance().operation == null) {
      backup_full(true);
    }
  }

  public void backup_auto_stop()
  {
    var op = OperationLauncher.get_instance().operation;
    var backup_op = op as DejaDup.OperationBackup;
    if (backup_op != null && backup_op.automatic) {
      backup_op.stop();
    }
  }

  void backup_full(bool automatic)
  {
    ensure_app_window();
    if (!automatic)
      activate();
    OperationLauncher.get_instance().start_backup(automatic);
  }

  void prompt_ok()
  {
    prompt_cancel();
    activate();
  }

  void prompt_cancel()
  {
    DejaDup.update_prompt_time(true);
  }
}

int main(string[] args)
{
  DejaDup.i18n_setup();

  // Translators: The name is a play on the French phrase "déjà vu" meaning
  // "already seen", but with the "vu" replaced with "dup".  "Dup" in this
  // context is itself a reference to both the underlying command line tool
  // "duplicity" and the act of duplicating data for backup.  As a whole, the
  // phrase "Déjà Dup" may not be very translatable.
  var appname = _("Déjà Dup Backups");

  if (Config.PROFILE == "Devel") {
    Environment.set_variable("DEJA_DUP_DEBUG", "1", false);
    Environment.set_variable("G_MESSAGES_DEBUG", "deja-dup", false);
  }

  Environment.set_application_name(appname);
  Environment.set_prgname(Config.APPLICATION_ID);
  Gtk.Window.set_default_icon_name(Config.ICON_NAME);

  // FIXME: there must be a better way than this?
  typeof(ActionDialog).ensure();
  typeof(BasicNavigationPage).ensure();
  typeof(Browser).ensure();
  typeof(BrowserPage).ensure();
  typeof(ConfigAutoBackupRow).ensure();
  typeof(ConfigDelete).ensure();
  typeof(ConfigExcludeFolderGroup).ensure();
  typeof(ConfigFolderGroup).ensure();
  typeof(ConfigFolderPage).ensure();
  typeof(ConfigIncludeFolderGroup).ensure();
  typeof(ConfigLocationGroup).ensure();
  typeof(ConfigLocationRow).ensure();
  typeof(ConfigLocationRow.Item).ensure();
  typeof(ConfigPeriodRow).ensure();
  typeof(ConfigResticRow).ensure();
  typeof(FolderChooserButton).ensure();
  typeof(HelpButton).ensure();
  typeof(OperationBanner).ensure();
  typeof(OperationStatusPage).ensure();
  typeof(OverviewPage).ensure();
  typeof(PrimaryMenuButton).ensure();
  typeof(RecentBackupRow).ensure();
  typeof(RestoreToolbarView).ensure();
  typeof(RestoreTargetWarningButton).ensure();
  typeof(SavePasswordRow).ensure();
  typeof(ServerAddressHelp).ensure();
  typeof(SnapshotsPage).ensure();
  typeof(TooltipBox).ensure();
  typeof(WelcomePage).ensure();

  return DejaDupApp.get_instance().run(args);
}
