# khal config loader is sensitive to leading space !
{
  config,
  lib,
  pkgs,
  ...
}:
let
  inherit (lib)
    mkOption
    optional
    optionalString
    filterAttrs
    recursiveUpdate
    ;

  cfg = config.programs.khal;

  iniFormat = pkgs.formats.ini { };

  khalCalendarAccounts = filterAttrs (_: a: a.khal.enable) config.accounts.calendar.accounts;

  # a contact account may have multiple collections, each a separate calendar
  expandContactAccount =
    name: acct:
    if acct.khal.collections != null then
      lib.listToAttrs (
        map (c: {
          name = "${name}-${c}";
          value = recursiveUpdate acct { khal.thisCollection = c; };
        }) acct.khal.collections
      )
    else
      {
        ${name} = acct;
      };

  khalContactAccounts = lib.concatMapAttrs expandContactAccount (
    lib.mapAttrs (_: v: recursiveUpdate v { khal.type = "birthdays"; }) (
      filterAttrs (_: a: a.khal.enable) config.accounts.contact.accounts
    )
  );

  khalAccounts = khalCalendarAccounts // khalContactAccounts;

  primaryAccount = lib.findSingle (a: a.primary) null null (
    lib.mapAttrsToList (n: v: v // { name = n; }) khalCalendarAccounts
  );

  definedAttrs = filterAttrs (_: v: !isNull v);

  toKeyValueIfDefined = attrs: lib.generators.toKeyValue { } (definedAttrs attrs);

  genCalendarStr =
    name: value:
    lib.concatStringsSep "\n" (
      [
        "[[${name}]]"
        "path = ${
          value.local.path
          + (if value.local.type == "singlefile" then "" else "/")
          + (optionalString (value.khal.type == "discover") value.khal.glob)
          + (optionalString (
            value.khal.type == "birthdays" && value.khal ? thisCollection
          ) value.khal.thisCollection)
        }"
      ]
      ++ optional (value.khal.readOnly) "readonly = True"
      ++ optional (
        value.khal.addresses != [ ]
      ) "addresses= ${lib.concatStringsSep ", " value.khal.addresses}"
      ++ optional (value.khal.color != null) "color = '${value.khal.color}'"
      ++ [
        (toKeyValueIfDefined (lib.getAttrs [ "type" "priority" ] value.khal))
      ]
      ++ [ "\n" ]
    );

  localeFormatOptions =
    let
      T = lib.types;
      suffix = ''
        Format strings are for Python `strftime`, similarly to
        {manpage}`strftime(3)`.
      '';
    in
    {
      dateformat = mkOption {
        type = T.str;
        default = "%x";
        description = ''
          khal will display and understand all dates in this format.

          ${suffix}
        '';
      };

      timeformat = mkOption {
        type = T.str;
        default = "%X";
        description = ''
          khal will display and understand all times in this format.

          ${suffix}
        '';
      };

      datetimeformat = mkOption {
        type = T.str;
        default = "%c";
        description = ''
          khal will display and understand all datetimes in this format.

          ${suffix}
        '';
      };

      longdateformat = mkOption {
        type = T.str;
        default = "%x";
        description = ''
          khal will display and understand all dates in this format.
          It should contain a year (e.g. `%Y`).

          ${suffix}
        '';
      };

      longdatetimeformat = mkOption {
        type = T.str;
        default = "%c";
        description = ''
          khal will display and understand all datetimes in this format.
          It should contain a year (e.g. `%Y`).

          ${suffix}
        '';
      };
    };

  localeOptions =
    let
      T = lib.types;
    in
    localeFormatOptions
    // {
      unicode_symbols = mkOption {
        type = T.bool;
        default = true;
        description = ''
          By default khal uses some Unicode symbols (as in "non-ASCII") as
          indicators for things like repeating events.
          If your font, encoding etc. does not support those symbols, set this
          to false (this will enable ASCII-based replacements).
        '';
      };

      default_timezone = mkOption {
        type = T.nullOr T.str;
        default = null;
        description = ''
          Default for new events or if khal does not understand the timezone
          in an ical file.
          If `null`, the timezone of your computer will be used.
        '';
      };

      local_timezone = mkOption {
        type = T.nullOr T.str;
        default = null;
        description = ''
          khal will show all times in this timezone.
          If `null`, the timezone of your computer will be used.
        '';
      };

      firstweekday = mkOption {
        type = T.ints.between 0 6;
        default = 0;
        description = ''
          The first day of the week, where Monday is 0 and Sunday is 6.
        '';
      };

      weeknumbers = mkOption {
        type = T.enum [
          "off"
          "left"
          "right"
        ];
        default = "off";
        description = ''
          Enable week numbers in calendar and interactive (ikhal) mode.
          As those are ISO week numbers, they only work properly if
          {option}`firstweekday` is set to 0.
        '';
      };
    };

in
{
  options.programs.khal = {
    enable = lib.mkEnableOption "khal, a CLI calendar application";

    package = lib.mkPackageOption pkgs "khal" { nullable = true; };

    locale = mkOption {
      type = lib.types.submodule { options = localeOptions; };
      description = ''
        khal locale settings.
      '';
      default = { };
    };

    settings = mkOption {
      inherit (iniFormat) type;
      default = { };
      example = {
        default = {
          default_calendar = "Calendar";
          timedelta = "5d";
        };
        view = {
          agenda_event_format = "{calendar-color}{cancelled}{start-end-time-style} {title}{repeat-symbol}{reset}";
        };
      };
      description = ''
        Configuration options to add to the various sections in the configuration file.
      '';
    };
  };

  config = lib.mkIf cfg.enable {
    home.packages = lib.mkIf (cfg.package != null) [ cfg.package ];

    xdg.configFile."khal/config".text = lib.concatStringsSep "\n" (
      let
        settingsWithPrimaryDefaults =
          cfg.settings
          // lib.optionalAttrs (!isNull primaryAccount) {
            default = recursiveUpdate {
              highlight_event_days = true;
              default_calendar =
                if isNull primaryAccount.primaryCollection then
                  primaryAccount.name
                else
                  primaryAccount.primaryCollection;
            } (cfg.settings.default or { });
          };
      in
      [ "[calendars]" ]
      ++ lib.mapAttrsToList genCalendarStr khalAccounts
      ++ [
        (lib.generators.toINI { } (
          recursiveUpdate settingsWithPrimaryDefaults {
            locale = definedAttrs (cfg.locale // { _module = null; });
          }
        ))
      ]
    );
  };
}
