/* process.vala
 *
 * Copyright (C) 2017 Red Hat, Inc.
 *
 * This program 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.
 *
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Authors: Petr Štětka <pstetka@redhat.com>
 */

public class Usage.Process : Object {
    public Pid pid { get; private set; }
    public string cmdline { get; private set; }
    public uint uid { get; private set; }
    public uint64 start_time { get; set; default = 0; }

    public double cpu_load { get; set; default = 0; }
    public double x_cpu_load { get; set; default = 0; }
    public uint64 cpu_last_used { get; set; default = 0; }
    public uint64 x_cpu_last_used { get; set; default = 0; }
    public uint last_processor { get; set; default = 0; }

    public uint64 mem_usage { get; set; default = 0; }

    public bool gamemode { get; set; }

    public bool mark_as_updated { get; set; default = true; }
    public ProcessStatus status { get; private set; default = ProcessStatus.SLEEPING; }

    private string? _app_id = null;
    private bool _app_id_checked = false;

    public Process (Pid pid) {
        this.pid = pid;
        this.cmdline = get_full_process_cmd (pid);
        this.uid = _get_uid ();
    }

    public void update_status () {
        GTop.ProcState proc_state;
        GTop.get_proc_state (out proc_state, pid);

        switch (proc_state.state) {
            case GTop.PROCESS_RUNNING:
            case GTop.PROCESS_UNINTERRUPTIBLE:
                status = ProcessStatus.RUNNING;
                break;
            case GTop.PROCESS_SWAPPING:
            case GTop.PROCESS_INTERRUPTIBLE:
            case GTop.PROCESS_STOPPED:
                status = ProcessStatus.SLEEPING;
                break;
            case GTop.PROCESS_DEAD:
            case GTop.PROCESS_ZOMBIE:
            default:
                status = ProcessStatus.DEAD;
                break;
        }

        if (cpu_load > 0)
            status = ProcessStatus.RUNNING;

        mark_as_updated = true;
    }

    private uint _get_uid () {
        GTop.ProcUid procUid;
        GTop.get_proc_uid (out procUid, pid);
        return procUid.uid;
    }

    public string? app_id {
        get {
            if (!_app_id_checked)
                _app_id = read_app_id (pid);

            return _app_id;
        }
    }

    /* static pid related methods */
    public static string get_full_process_cmd (Pid pid) {
        GTop.ProcArgs proc_args;
        GTop.ProcState proc_state;
        string[] args = GTop.get_proc_argv (out proc_args, pid, 0);
        GTop.get_proc_state (out proc_state, pid);
        string cmd = (string) proc_state.cmd;

        /* cmd is most likely a truncated version, therefore
         * we check the first two arguments of the full argv
         * vector if they match cmd and if so, use that */
        for (int i = 0; i < uint.min (args.length, 2); i++) {
            if (args[i] == null)
                continue;

            /* TODO: this will fail if args[i] is a commandline,
             * i.e. composed of multiple segments and one of the
             * later ones is a unix path */
            var name = Path.get_basename (args[i]);
            if (!name.contains (cmd))
                continue;

            name = Process.first_component (name);
            return Process.sanitize_name (name);
        }

        return Process.sanitize_name (cmd);
    }

    public static string? read_cgroup (Pid pid) {
        string path = "/proc/%u/cgroup".printf ((uint) pid);
        int flags = Posix.O_RDONLY | StopGap.O_CLOEXEC | Posix.O_NOCTTY;

        int fd = StopGap.openat (StopGap.AT_FDCWD, path, flags);

        if (fd == -1)
            return null;

        try {
            string? data = null;
            string[] lines;
            string? cgroup = null;
            size_t len;

            // TODO use MappedFile.from_fd, requires vala 0.46
            IOChannel ch = new IOChannel.unix_new (fd);
            ch.set_close_on_unref (true);

            var status = ch.read_to_end (out data, out len);
            if (status != IOStatus.NORMAL)
                return null;

            lines = data.split ("\n");

            // Only do anything with cgroup v2
            if (!lines[0].has_prefix ("0::"))
                return null;

            cgroup = lines[0][3:lines[0].length];

            return cgroup;
        } catch (Error e) {
            return null;
        }
    }

    public static string? read_app_id (Pid pid) {
        KeyFile? kf = read_flatpak_info (pid);

        if (kf == null)
            return null;

        string? name = null;
        try {
            if (kf.has_key ("Application", "name"))
                name = kf.get_string ("Application", "name");
        } catch (Error e) {
            warning (@"Failed to parse faltpak info for: $pid");
        }

        return name;
    }

    public static KeyFile? read_flatpak_info (Pid pid) {
        string path = "/proc/%u/root".printf ((uint) pid);
        int flags = Posix.O_RDONLY | StopGap.O_CLOEXEC | Posix.O_NOCTTY;

        int root = StopGap.openat (StopGap.AT_FDCWD, path, flags | Posix.O_NONBLOCK | StopGap.O_DIRECTORY);

        if (root == -1)
            return null;

        int fd = StopGap.openat (root, ".flatpak-info", flags);

        Posix.close (root);

        if (fd == -1)
            return null;

        KeyFile kf = new KeyFile ();

        try {
            string? data = null;
            size_t len;

            // TODO use MappedFile.from_fd, requires vala 0.46
            IOChannel ch = new IOChannel.unix_new (fd);
            ch.set_close_on_unref (true);

            var status = ch.read_to_end (out data, out len);
            if (status != IOStatus.NORMAL)
                return null;

            kf.load_from_data (data, len, 0);
        } catch (Error e) {
            return null;
        }

        return kf;
    }

    /* static utility methods */
    public static string? sanitize_name (string name) {
        string? result = null;

        if (name == null)
            return null;

        try {
            var rgx = new Regex ("[^a-zA-Z0-9._-]");
            result = rgx.replace (name, name.length, 0, "");
        } catch (RegexError e) {
            warning ("Unable to sanitize name: %s", e.message);
        }

        return result;
    }

    public static string first_component (string str) {

        for (int i = 0; i < str.length; i++) {
            if (str[i] == ' ') {
                return str.substring (0, i);
            }
        }

        return str;
    }
}

public enum Usage.ProcessStatus {
    RUNNING,
    SLEEPING,
    DEAD;
}
