{
  config,
  lib,
  pkgs,
  ...
}:
let
  inherit (lib)
    literalExpression
    mkOption
    optional
    types
    ;

  cfg = config.services.dunst;

  eitherStrBoolIntList = with types; either str (either bool (either int (listOf str)));

  toDunstIni = lib.generators.toINI {
    mkKeyValue =
      key: value:
      let
        value' =
          if lib.isBool value then
            (lib.hm.booleans.yesNo value)
          else if lib.isString value then
            ''"${value}"''
          else
            toString value;
      in
      "${key}=${value'}";
  };

  themeType = types.submodule {
    options = {
      package = mkOption {
        type = types.package;
        example = literalExpression "pkgs.adwaita-icon-theme";
        description = "Package providing the theme.";
      };

      name = mkOption {
        type = types.str;
        example = "Adwaita";
        description = "The name of the theme within the package.";
      };

      size = mkOption {
        type = types.str;
        default = "32x32";
        example = "16x16";
        description = "The desired icon size.";
      };
    };
  };

  hicolorTheme = {
    package = pkgs.hicolor-icon-theme;
    name = "hicolor";
    size = "32x32";
  };

in
{
  meta.maintainers = [ lib.maintainers.rycee ];

  options = {
    services.dunst = {
      enable = lib.mkEnableOption "the dunst notification daemon";

      package = lib.mkPackageOption pkgs "dunst" { };

      configFile = mkOption {
        type = with types; nullOr (either str path);
        default = null;
        description = ''
          Path to the configuration file read by dunst.

          Note that the configuration generated by Home Manager will be
          written to {file}`$XDG_CONFIG_HOME/dunst/dunstrc`
          regardless. This allows using a mutable configuration file generated
          from the immutable one, useful in scenarios where live reloading is
          desired.
        '';
      };

      iconTheme = mkOption {
        type = themeType;
        default = hicolorTheme;
        description = "Set the icon theme.";
      };

      waylandDisplay = mkOption {
        type = types.str;
        default = "";
        description = "Set the service's {env}`WAYLAND_DISPLAY` environment variable.";
      };

      settings = mkOption {
        type = types.submodule {
          freeformType = with types; attrsOf (attrsOf eitherStrBoolIntList);
          options = {
            global.icon_path = mkOption {
              type = types.separatedString ":";
              description = "Paths where dunst will look for icons.";
            };
          };
        };
        default = { };
        description = "Configuration written to {file}`$XDG_CONFIG_HOME/dunst/dunstrc`.";
        example = literalExpression ''
          {
            global = {
              width = "(200,300)";
              height = "(0,150)";
              offset = "(30,50)";
              origin = "top-right";
              transparency = 10;
              frame_color = "#eceff1";
              font = "Droid Sans 9";
            };

            urgency_normal = {
              background = "#37474f";
              foreground = "#eceff1";
              timeout = 10;
            };
          };
        '';
      };
    };
  };

  config = lib.mkIf cfg.enable {
    assertions = [
      (lib.hm.assertions.assertPlatform "services.dunst" pkgs lib.platforms.linux)
    ];

    home.packages = [ cfg.package ];

    xdg.dataFile."dbus-1/services/org.knopwob.dunst.service".source =
      "${cfg.package}/share/dbus-1/services/org.knopwob.dunst.service";

    xdg.configFile."dunst/dunstrc" = lib.mkIf (cfg.settings != { }) {
      text = toDunstIni cfg.settings;
    };

    services.dunst.settings.global.icon_path =
      let
        useCustomTheme =
          cfg.iconTheme.package != hicolorTheme.package
          || cfg.iconTheme.name != hicolorTheme.name
          || cfg.iconTheme.size != hicolorTheme.size;

        basePaths = [
          "/run/current-system/sw"
          config.home.profileDirectory
          cfg.iconTheme.package
        ]
        ++ optional useCustomTheme hicolorTheme.package;

        themes = [
          cfg.iconTheme
        ]
        ++ optional useCustomTheme (hicolorTheme // { size = cfg.iconTheme.size; });

        categories = [
          "actions"
          "animations"
          "apps"
          "categories"
          "devices"
          "emblems"
          "emotes"
          "filesystem"
          "intl"
          "legacy"
          "mimetypes"
          "places"
          "status"
          "stock"
        ];

        mkPath =
          {
            basePath,
            theme,
            category,
          }:
          "${basePath}/share/icons/${theme.name}/${theme.size}/${category}";
      in
      lib.concatMapStringsSep ":" mkPath (
        lib.cartesianProduct {
          basePath = basePaths;
          theme = themes;
          category = categories;
        }
      );

    systemd.user.services.dunst = {
      Unit = {
        Description = "Dunst notification daemon";
        After = [ config.wayland.systemd.target ];
        PartOf = [ config.wayland.systemd.target ];
        X-Reload-Triggers = lib.mkIf (cfg.settings != { }) [
          "${config.xdg.configFile."dunst/dunstrc".source}"
        ];
      };

      Service = {
        Type = "dbus";
        BusName = "org.freedesktop.Notifications";
        ExecStart = lib.escapeShellArgs (
          [ "${cfg.package}/bin/dunst" ]
          ++
            # Using `-config` breaks dunst's drop-ins, so only use it when an alternative path is set
            lib.optionals (cfg.configFile != null) [
              "-config"
              cfg.configFile
            ]
        );
        ExecReload = "${cfg.package}/bin/dunstctl reload";
        Environment = lib.optionalString (cfg.waylandDisplay != "") "WAYLAND_DISPLAY=${cfg.waylandDisplay}";
      };
    };
  };
}
