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

using GLib;

internal abstract class ToolInstance : Object
{
  public signal void done(bool success, bool cancelled);
  public signal void exited(int code);

  public bool verbose {get; private set; default = false;}
  public string forced_cache_dir {get; set; default = null;}

  public static int prefix_wrapper_args(ref List<string> args) throws ShellError
  {
    var settings = DejaDup.get_settings();
    var wrapper = settings.get_string(DejaDup.CUSTOM_TOOL_WRAPPER_KEY);
    if (wrapper == "")
      return 0;

    string[] parsed;
    Shell.parse_argv(wrapper, out parsed);

    for (var i = parsed.length - 1; i >= 0; i--)
      args.prepend(parsed[i]);

    return parsed.length;
  }

  public async void start(List<string> argv_in, List<string>? envp_in)
  {
    try {
      /* Make deep copies of the lists, so if our caller doesn't yield, the
         lists won't be invalidated. */
      var argv = argv_in.copy_deep(strdup);
      var envp = envp_in.copy_deep(strdup);
      yield start_internal(argv, envp);
    }
    catch (Error e) {
      // Fake a generic message
      _send_error(e);
      done(false, false);
    }
  }

  public bool is_started()
  {
    return (int)child_pid > 0;
  }

  public void cancel()
  {
    if (is_started())
      kill_child();
    else
      done(false, true);
  }

  public void pause()
  {
    if (is_started())
      stop_child();
  }

  public void resume()
  {
    if (is_started())
      cont_child();
  }

  protected abstract string _name();

  protected abstract void _send_error(Error e);

  // true if we finished the stanza, loggable indicates if the line should be saved in the debug logs
  protected abstract bool _process_line(string stanza, string line, out bool loggable) throws Error;

  // Called before we do standard stripping of env vars
  protected virtual void _edit_envp(List<string> envp) {}

  uint watch_id;
  Pid child_pid;
  bool reading_done;
  bool process_done;
  int status;
  int dd_stdout;
  int dd_stderr;
  MainLoop read_loop;

  ~ToolInstance()
  {
    if (watch_id != 0)
      Source.remove(watch_id);

    if (is_started()) {
      debug("tool (%i) process killed\n", (int)child_pid);
      kill_child();
    }
  }

  async void start_internal(List<string> argv_in, List<string>? envp_in) throws Error
  {
    var verbose_str = Environment.get_variable("DEJA_DUP_DEBUG");
    if (verbose_str != null && int.parse(verbose_str) > 0)
      verbose = true;

    _edit_envp(envp_in);
    var real_envp = DejaDup.copy_env(envp_in);

    List<string> argv = new List<string>();
    foreach (string arg in argv_in)
      argv.append(arg);

    prefix_wrapper_args(ref argv);

    // Grab version of command line to show user
    string user_cmd = null;
    foreach(string a in argv) {
      if (a == null)
        break;
      if (user_cmd == null)
        user_cmd = a;
      else
        user_cmd = "%s %s".printf(user_cmd, Shell.quote(a));
    }

    string[] real_argv = new string[argv.length()];
    int i = 0;
    foreach(string a in argv)
      real_argv[i++] = a;

    real_argv = DejaDup.nice_prefix(real_argv);

    Process.spawn_async_with_pipes(null, real_argv, real_envp,
                                   SpawnFlags.SEARCH_PATH |
                                   SpawnFlags.DO_NOT_REAP_CHILD,
                                   () => {
                                      // Drop support for /dev/tty inside the tool.
                                      // Helps duplicity with password handling,
                                      // and helps restic with rclone support.
                                      Posix.setsid();
                                   },
                                   out child_pid, null, out dd_stdout, out dd_stderr);

    var cmd_msg = "Running the following tool (%i) command: %s".printf(
      (int)child_pid, user_cmd
    );
    if (verbose)
      print("%s\n", cmd_msg);
    else
      debug("%s\n", cmd_msg);
    add_stanza_to_tail(cmd_msg);

    watch_id = ChildWatch.add(child_pid, spawn_finished);

    yield read_log();
  }

  void kill_child() {
    Posix.kill((Posix.pid_t)child_pid, Posix.Signal.KILL);
  }

  void stop_child() {
    Posix.kill((Posix.pid_t)child_pid, Posix.Signal.STOP);
  }

  void cont_child() {
    Posix.kill((Posix.pid_t)child_pid, Posix.Signal.CONT);
  }

  async void read_log_lines(DataInputStream reader)
  {
    string stanza = "";
    while (reader != null) {
      try {
        var line = yield reader.read_line_utf8_async();
        if (line == null) { // EOF
          reading_done = true;

          if (process_done)
              send_done_for_status();

          break;
        }

        if (verbose)
          print("TOOL: %s\n", line);

        stanza += line;

        try {
          bool loggable = true;
          if (_process_line(stanza, line, out loggable)) {
            if (loggable)
              add_stanza_to_tail(stanza);
            stanza = "";
          }
        }
        catch (Error err) {
          warning("%s\n", err.message);
          add_stanza_to_tail(stanza);
          stanza = "";
        }
      }
      catch (Error err) {
        warning("%s\n", err.message);
        break;
      }
    }
  }

  async void read_log()
  {
   /*
    * Asynchronous reading of restic's log via stream
    *
    * Stream initiated either from log file or pipe
    */
    var err_stream = new UnixInputStream(dd_stderr, true);
    var err_reader = new DataInputStream(err_stream);

    var out_stream = new UnixInputStream(dd_stdout, true);
    var out_reader = new DataInputStream(out_stream);

    // This loop goes on while rest of class is doing its work.  We ref
    // it to make sure that the rest of the class doesn't drop from under us.
    ref();
    read_loop = new MainLoop(null);
    read_log_lines.begin(err_reader);
    read_log_lines.begin(out_reader);
    read_loop.run();
    read_loop = null;
    unref();
  }

  void spawn_finished(Pid pid, int status)
  {
    this.status = status;

    if (Process.if_exited(status)) {
      var exitval = Process.exit_status(status);
      debug("tool (%i) exited with value %i\n", (int)pid, exitval);
    }
    else {
      debug("tool (%i) process killed\n", (int)pid);
    }

    watch_id = 0;
    Process.close_pid(pid);

    write_tail_to_cache();

    process_done = true;
    if (reading_done)
      send_done_for_status();
  }

  void send_done_for_status()
  {
    bool success = Process.if_exited(status) && Process.exit_status(status) == 0;
    bool cancelled = !Process.if_exited(status);

    if (Process.if_exited(status))
      exited(Process.exit_status(status));

    child_pid = (Pid)0;
    done(success, cancelled);
    read_loop.quit();
  }

  // Keeps track of recent processed stanzas
  Queue<string> tail;
  void add_stanza_to_tail(string stanza)
  {
    if (tail == null)
      tail = new Queue<string>();

    tail.push_tail(stanza);
    while (tail.get_length() > 50) {
      tail.pop_head();
    }
  }

  void write_tail_to_cache()
  {
    if (tail == null)
      return;

    var cachedir = Environment.get_user_cache_dir();
    if (cachedir == null)
      return;

    var cachefile = Path.build_filename(cachedir, Config.PACKAGE, _name() + ".log");

    var contents = "";
    foreach (var stanza in tail.head)
      contents += stanza + "\n";

    try {
      FileUtils.set_contents(cachefile, contents);
    }
    catch (Error e) {
      info("%s", e.message);
    }
  }
}
