# launchd option type from nix-darwin
#
# Original code from https://github.com/nix-darwin/nix-darwin/commit/861af0fc94df9454f4e92d6892f75588763164bb

{ lib, ... }:

let
  inherit (lib)
    imap1
    types
    mkOption
    showOption
    mergeDefinitions
    ;
  inherit (builtins)
    filter
    length
    deepSeq
    concatLists
    ;
  inherit (lib.options) showDefs;
  wildcardText = lib.literalMD "`*`";

  /*
    *
    A type of list which does not allow duplicate elements. The base/inner
    list type to use (e.g. `types.listOf` or `types.nonEmptyListOf`) is passed
    via argument `listType`, which must be the final type and not a function.

    NOTE: The extra check for duplicates is quadratic and strict, so use this
    type sparingly and only:

      * when needed, and
      * when the list is expected to be recursively short (e.g. < 10 elements)
        and shallow (i.e. strict evaluation of the list won't take too long)

    The implementation of this function is similar to that of
    `types.nonEmptyListOf`.
  */
  types'.uniqueList =
    listType:
    listType
    // {
      description = "unique ${types.optionDescriptionPhrase (class: class == "noun") listType}";
      substSubModules = m: types'.uniqueList (listType.substSubModules m);
      # This has been taken from the implementation of `types.listOf`, but has
      # been modified to throw on duplicates. This check cannot be done in the
      # `check` fn as this check is deep/strict, and because `check` runs
      # prior to merging.
      merge =
        loc: defs:
        let
          # Each element of `dupes` is a list. When there are duplicates,
          # later lists will be duplicates of earlier lists, so just throw on
          # the first set of duplicates found so that we don't have duplicate
          # error msgs.
          checked = filter (
            li:
            if length li > 1 then
              throw ''
                The option `${showOption loc}' contains duplicate entries after merging:
                ${showDefs li}''
            else
              false
          ) dupes;
          dupes = map (def: filter (def': def'.value == def.value) merged) merged;
          merged = filter (x: x ? value) (
            concatLists (
              imap1 (
                n: def:
                imap1 (
                  m: el:
                  let
                    inherit (def) file;
                    loc' = loc ++ [ "[definition ${toString n}-entry ${toString m}]" ];
                  in
                  (mergeDefinitions loc' listType.nestedTypes.elemType [
                    {
                      inherit file;
                      value = el;
                    }
                  ]).optionalValue
                  // {
                    inherit loc' file;
                  }
                ) def.value
              ) defs
            )
          );
        in
        deepSeq checked (map (x: x.value) merged);
    };
in
{
  StartCalendarInterval =
    let
      CalendarIntervalEntry = types.submodule {
        options = {
          Minute = mkOption {
            type = types.nullOr (types.ints.between 0 59);
            default = null;
            defaultText = wildcardText;
            description = ''
              The minute on which this job will be run.
            '';
          };

          Hour = mkOption {
            type = types.nullOr (types.ints.between 0 23);
            default = null;
            defaultText = wildcardText;
            description = ''
              The hour on which this job will be run.
            '';
          };

          Day = mkOption {
            type = types.nullOr (types.ints.between 1 31);
            default = null;
            defaultText = wildcardText;
            description = ''
              The day on which this job will be run.
            '';
          };

          Weekday = mkOption {
            type = types.nullOr (types.ints.between 0 7);
            default = null;
            defaultText = wildcardText;
            description = ''
              The weekday on which this job will be run (0 and 7 are Sunday).
            '';
          };

          Month = mkOption {
            type = types.nullOr (types.ints.between 1 12);
            default = null;
            defaultText = wildcardText;
            description = ''
              The month on which this job will be run.
            '';
          };
        };
      };
    in
    types.either CalendarIntervalEntry (types'.uniqueList (types.nonEmptyListOf CalendarIntervalEntry));
}
