{ config, lib, pkgs, ... }:

let
  cfg = config.services.system76-scheduler;

  inherit (builtins) concatStringsSep map toString attrNames;
  inherit (lib) boolToString types mkOption literalExpression optional mkIf mkMerge;
  inherit (types) nullOr listOf bool int ints float str enum;

  withDefaults = optionSpecs: defaults:
    lib.genAttrs (attrNames optionSpecs) (name:
      mkOption (optionSpecs.${name} // {
        default = optionSpecs.${name}.default or defaults.${name} or null;
      }));

  latencyProfile = withDefaults {
    latency = {
      type = int;
      description = "`sched_latency_ns`.";
    };
    nr-latency = {
      type = int;
      description = "`sched_nr_latency`.";
    };
    wakeup-granularity = {
      type = float;
      description = "`sched_wakeup_granularity_ns`.";
    };
    bandwidth-size = {
      type = int;
      description = "`sched_cfs_bandwidth_slice_us`.";
    };
    preempt = {
      type = enum [ "none" "voluntary" "full" ];
      description = "Preemption mode.";
    };
  };
  schedulerProfile = withDefaults {
    nice = {
      type = nullOr (ints.between (-20) 19);
      description = "Niceness.";
    };
    class = {
      type = nullOr (enum [ "idle" "batch" "other" "rr" "fifo" ]);
      example = literalExpression "\"batch\"";
      description = "CPU scheduler class.";
    };
    prio = {
      type = nullOr (ints.between 1 99);
      example = literalExpression "49";
      description = "CPU scheduler priority.";
    };
    ioClass = {
      type = nullOr (enum [ "idle" "best-effort" "realtime" ]);
      example = literalExpression "\"best-effort\"";
      description = "IO scheduler class.";
    };
    ioPrio = {
      type = nullOr (ints.between 0 7);
      example = literalExpression "4";
      description = "IO scheduler priority.";
    };
    matchers = {
      type = nullOr (listOf str);
      default = [];
      example = literalExpression ''
        [
          "include cgroup=\"/user.slice/*.service\" parent=\"systemd\""
          "emacs"
        ]
      '';
      description = "Process matchers.";
    };
  };

  cfsProfileToString = name: let
    p = cfg.settings.cfsProfiles.${name};
  in
    "${name} latency=${toString p.latency} nr-latency=${toString p.nr-latency} wakeup-granularity=${toString p.wakeup-granularity} bandwidth-size=${toString p.bandwidth-size} preempt=\"${p.preempt}\"";

  prioToString = class: prio: if prio == null then "\"${class}\"" else "(${class})${toString prio}";

  schedulerProfileToString = name: a: indent:
    concatStringsSep " "
      (["${indent}${name}"]
       ++ (optional (a.nice != null) "nice=${toString a.nice}")
       ++ (optional (a.class != null) "sched=${prioToString a.class a.prio}")
       ++ (optional (a.ioClass != null) "io=${prioToString a.ioClass a.ioPrio}")
       ++ (optional ((builtins.length a.matchers) != 0) ("{\n${concatStringsSep "\n" (map (m: "  ${indent}${m}") a.matchers)}\n${indent}}")));

in {
  options = {
    services.system76-scheduler = {
      enable = lib.mkEnableOption "system76-scheduler";

      package = mkOption {
        type = types.package;
        default = config.boot.kernelPackages.system76-scheduler;
        defaultText = literalExpression "config.boot.kernelPackages.system76-scheduler";
        description = "Which System76-Scheduler package to use.";
      };

      useStockConfig = mkOption {
        type = bool;
        default = true;
        description = ''
          Use the (reasonable and featureful) stock configuration.

          When this option is `true`, `services.system76-scheduler.settings`
          are ignored.
        '';
      };

      settings = {
        cfsProfiles = {
          enable = mkOption {
            type = bool;
            default = true;
            description = "Tweak CFS latency parameters when going on/off battery";
          };

          default = latencyProfile {
            latency = 6;
            nr-latency = 8;
            wakeup-granularity = 1.0;
            bandwidth-size = 5;
            preempt = "voluntary";
          };
          responsive = latencyProfile {
            latency = 4;
            nr-latency = 10;
            wakeup-granularity = 0.5;
            bandwidth-size = 3;
            preempt = "full";
          };
        };

        processScheduler = {
          enable = mkOption {
            type = bool;
            default = true;
            description = "Tweak scheduling of individual processes in real time.";
          };

          useExecsnoop = mkOption {
            type = bool;
            default = true;
            description = "Use execsnoop (otherwise poll the precess list periodically).";
          };

          refreshInterval = mkOption {
            type = int;
            default = 60;
            description = "Process list poll interval, in seconds";
          };

          foregroundBoost = {
            enable = mkOption {
              type = bool;
              default = true;
              description = ''
                Boost foreground process priorities.

                (And de-boost background ones).  Note that this option needs cooperation
                from the desktop environment to work.  On Gnome the client side is
                implemented by the "System76 Scheduler" shell extension.
              '';
            };
            foreground = schedulerProfile {
              nice = 0;
              ioClass = "best-effort";
              ioPrio = 0;
            };
            background = schedulerProfile {
              nice = 6;
              ioClass = "idle";
            };
          };

          pipewireBoost = {
            enable = mkOption {
              type = bool;
              default = true;
              description = "Boost Pipewire client priorities.";
            };
            profile = schedulerProfile {
              nice = -6;
              ioClass = "best-effort";
              ioPrio = 0;
            };
          };
        };
      };

      assignments = mkOption {
        type = types.attrsOf (types.submodule {
          options = schedulerProfile { };
        });
        default = {};
        example = literalExpression ''
          {
            nix-builds = {
              nice = 15;
              class = "batch";
              ioClass = "idle";
              matchers = [
                "nix-daemon"
              ];
            };
          }
        '';
        description = "Process profile assignments.";
      };

      exceptions = mkOption {
        type = types.listOf str;
        default = [];
        example = literalExpression ''
          [
            "include descends=\"schedtool\""
            "schedtool"
          ]
        '';
        description = "Processes that are left alone.";
      };
    };
  };

  config = mkIf cfg.enable {
    environment.systemPackages = [ cfg.package ];
    services.dbus.packages = [ cfg.package ];

    systemd.services.system76-scheduler = {
      description = "Manage process priorities and CFS scheduler latencies for improved responsiveness on the desktop";
      wantedBy = [ "multi-user.target" ];
      path = [
        # execsnoop needs those to extract kernel headers:
        pkgs.kmod
        pkgs.gnutar
        pkgs.xz
      ];
      serviceConfig = {
        Type = "dbus";
        BusName= "com.system76.Scheduler";
        ExecStart = "${cfg.package}/bin/system76-scheduler daemon";
        ExecReload = "${cfg.package}/bin/system76-scheduler daemon reload";
      };
    };

    environment.etc = mkMerge [
      (mkIf cfg.useStockConfig {
        # No custom settings: just use stock configuration with a fix for Pipewire
        "system76-scheduler/config.kdl".source = "${cfg.package}/data/config.kdl";
        "system76-scheduler/process-scheduler/00-dist.kdl".source = "${cfg.package}/data/pop_os.kdl";
        "system76-scheduler/process-scheduler/01-fix-pipewire-paths.kdl".source = ../../../../pkgs/os-specific/linux/system76-scheduler/01-fix-pipewire-paths.kdl;
      })

      (let
        settings = cfg.settings;
        cfsp = settings.cfsProfiles;
        ps = settings.processScheduler;
      in mkIf (!cfg.useStockConfig) {
        "system76-scheduler/config.kdl".text = ''
          version "2.0"
          autogroup-enabled false
          cfs-profiles enable=${boolToString cfsp.enable} {
            ${cfsProfileToString "default"}
            ${cfsProfileToString "responsive"}
          }
          process-scheduler enable=${boolToString ps.enable} {
            execsnoop ${boolToString ps.useExecsnoop}
            refresh-rate ${toString ps.refreshInterval}
            assignments {
              ${if ps.foregroundBoost.enable then (schedulerProfileToString "foreground" ps.foregroundBoost.foreground "    ") else ""}
              ${if ps.foregroundBoost.enable then (schedulerProfileToString "background" ps.foregroundBoost.background "    ") else ""}
              ${if ps.pipewireBoost.enable then (schedulerProfileToString "pipewire" ps.pipewireBoost.profile "    ") else ""}
            }
          }
        '';
      })

      {
        "system76-scheduler/process-scheduler/02-config.kdl".text =
          "exceptions {\n${concatStringsSep "\n" (map (e: "  ${e}") cfg.exceptions)}\n}\n"
          + "assignments {\n"
          + (concatStringsSep "\n" (map (name: schedulerProfileToString name cfg.assignments.${name} "  ")
            (attrNames cfg.assignments)))
          + "\n}\n";
      }
    ];
  };

  meta = {
    maintainers = [ lib.maintainers.cmm ];
  };
}
