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

with lib;

let
  desktopEntry = {
    options = {
      # Since this module uses the nixpkgs/pkgs/build-support/make-desktopitem function, 
      # our options and defaults follow its parameters, with the following exceptions:

      # `desktopName` on makeDesktopItem is controlled by `name`.
      # This is what we'd commonly consider the name of the application.
      # `name` on makeDesktopItem is controlled by this module's key in the attrset.
      # This is the file's filename excluding ".desktop".

      # `extraEntries` on makeDesktopItem is controlled by `extraConfig`,
      # and `extraDesktopEntries` by `settings`,
      # to match what's commonly used by other home manager modules.

      # `startupNotify` on makeDesktopItem asks for "true" or "false" strings,
      # for usability's sake we ask for a boolean.

      # `mimeType` and `categories` on makeDesktopItem ask for a string in the format "one;two;three;",
      # for the same reason we ask for a list of strings.

      # Descriptions are taken from the desktop entry spec: 
      # https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#recognized-keys

      type = mkOption {
        description = "The type of the desktop entry.";
        default = "Application";
        type = types.enum [ "Application" "Link" "Directory" ];
      };

      exec = mkOption {
        description = "Program to execute, possibly with arguments.";
        type = types.str;
      };

      icon = mkOption {
        description = "Icon to display in file manager, menus, etc.";
        type = types.nullOr types.str;
        default = null;
      };

      comment = mkOption {
        description = "Tooltip for the entry.";
        type = types.nullOr types.str;
        default = null;
      };

      terminal = mkOption {
        description = "Whether the program runs in a terminal window.";
        type = types.bool;
        default = false;
      };

      name = mkOption {
        description = "Specific name of the application.";
        type = types.str;
      };

      genericName = mkOption {
        description = "Generic name of the application.";
        type = types.nullOr types.str;
        default = null;
      };

      mimeType = mkOption {
        description = "The MIME type(s) supported by this application.";
        type = types.nullOr (types.listOf types.str);
        default = null;
      };

      categories = mkOption {
        description =
          "Categories in which the entry should be shown in a menu.";
        type = types.nullOr (types.listOf types.str);
        default = null;
      };

      startupNotify = mkOption {
        description = ''
          If true, it is KNOWN that the application will send a "remove" 
          message when started with the <literal>DESKTOP_STARTUP_ID</literal> 
          environment variable set. If false, it is KNOWN that the application 
          does not work with startup notification at all.'';
        type = types.nullOr types.bool;
        default = null;
      };

      extraConfig = mkOption {
        description = ''
          Extra configuration. Will be appended to the end of the file and 
          may thus contain extra sections.
        '';
        type = types.lines;
        default = "";
      };

      settings = mkOption {
        type = types.attrsOf types.string;
        description = ''
          Extra key-value pairs to add to the <literal>[Desktop Entry]</literal> section. 
          This may override other values.
        '';
        default = { };
        example = literalExpression ''
          {
            Keywords = "calc;math";
            DBusActivatable = "false";
          }
        '';
      };

      fileValidation = mkOption {
        type = types.bool;
        description = "Whether to validate the generated desktop file.";
        default = true;
      };
    };
  };

  #formatting helpers
  ifNotNull = a: a': if a == null then null else a';
  stringBool = bool: if bool then "true" else "false";
  semicolonList = list:
    (concatStringsSep ";" list) + ";"; # requires trailing semicolon

  #passes config options to makeDesktopItem in expected format
  makeFile = name: config:
    pkgs.makeDesktopItem {
      name = name;
      type = config.type;
      exec = config.exec;
      icon = config.icon;
      comment = config.comment;
      terminal = config.terminal;
      desktopName = config.name;
      genericName = config.genericName;
      mimeType = ifNotNull config.mimeType (semicolonList config.mimeType);
      categories =
        ifNotNull config.categories (semicolonList config.categories);
      startupNotify =
        ifNotNull config.startupNotify (stringBool config.startupNotify);
      extraEntries = config.extraConfig;
      extraDesktopEntries = config.settings;
    };
in {
  meta.maintainers = with maintainers; [ cwyc ];

  options.xdg.desktopEntries = mkOption {
    description = ''
      Desktop Entries allow applications to be shown in your desktop environment's app launcher. </para><para>
      You can define entries for programs without entries or override existing entries. </para><para>
      See <link xlink:href="https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#recognized-keys" /> for more information on options.
    '';
    default = { };
    type = types.attrsOf (types.submodule desktopEntry);
    example = literalExpression ''
      {
        firefox = {
          name = "Firefox";
          genericName = "Web Browser";
          exec = "firefox %U";
          terminal = false;
          categories = [ "Application" "Network" "WebBrowser" ];
          mimeType = [ "text/html" "text/xml" ];
        };
      }
    '';
  };

  config = mkIf (config.xdg.desktopEntries != { }) {
    assertions = [
      (hm.assertions.assertPlatform "xdg.desktopEntries" pkgs platforms.linux)
    ];

    home.packages = (map hiPrio # we need hiPrio to override existing entries
      (attrsets.mapAttrsToList makeFile config.xdg.desktopEntries));
  };

}