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

let
  inherit (lib)
    all filterAttrs hasAttr isStorePath literalExpression optionalAttrs types;
  inherit (lib.options) mkEnableOption mkOption;
  inherit (lib.modules) mkIf mkMerge;

  cfg = config.programs.waybar;

  jsonFormat = pkgs.formats.json { };

  mkMargin = name:
    mkOption {
      type = types.nullOr types.int;
      default = null;
      example = 10;
      description = "Margin value without unit.";
    };

  waybarBarConfig = with types;
    submodule {
      freeformType = jsonFormat.type;

      options = {
        layer = mkOption {
          type = nullOr (enum [ "top" "bottom" ]);
          default = null;
          description = ''
            Decide if the bar is displayed in front (<code>"top"</code>)
            of the windows or behind (<code>"bottom"</code>).
          '';
          example = "top";
        };

        output = mkOption {
          type = nullOr (either str (listOf str));
          default = null;
          example = literalExpression ''
            [ "DP-1" "!DP-2" "!DP-3" ]
          '';
          description = ''
            Specifies on which screen this bar will be displayed.
            Exclamation mark(!) can be used to exclude specific output.
          '';
        };

        position = mkOption {
          type = nullOr (enum [ "top" "bottom" "left" "right" ]);
          default = null;
          example = "right";
          description = "Bar position relative to the output.";
        };

        height = mkOption {
          type = nullOr ints.unsigned;
          default = null;
          example = 5;
          description =
            "Height to be used by the bar if possible. Leave blank for a dynamic value.";
        };

        width = mkOption {
          type = nullOr ints.unsigned;
          default = null;
          example = 5;
          description =
            "Width to be used by the bar if possible. Leave blank for a dynamic value.";
        };

        modules-left = mkOption {
          type = listOf str;
          default = [ ];
          description = "Modules that will be displayed on the left.";
          example = literalExpression ''
            [ "sway/workspaces" "sway/mode" "wlr/taskbar" ]
          '';
        };

        modules-center = mkOption {
          type = listOf str;
          default = [ ];
          description = "Modules that will be displayed in the center.";
          example = literalExpression ''
            [ "sway/window" ]
          '';
        };

        modules-right = mkOption {
          type = listOf str;
          default = [ ];
          description = "Modules that will be displayed on the right.";
          example = literalExpression ''
            [ "mpd" "custom/mymodule#with-css-id" "temperature" ]
          '';
        };

        modules = mkOption {
          type = jsonFormat.type;
          visible = false;
          default = null;
          description = "Modules configuration.";
          example = literalExpression ''
            {
              "sway/window" = {
                max-length = 50;
              };
              "clock" = {
                format-alt = "{:%a, %d. %b  %H:%M}";
              };
            }
          '';
        };

        margin = mkOption {
          type = nullOr str;
          default = null;
          description = "Margins value using the CSS format without units.";
          example = "20 5";
        };

        margin-left = mkMargin "left";
        margin-right = mkMargin "right";
        margin-bottom = mkMargin "bottom";
        margin-top = mkMargin "top";

        name = mkOption {
          type = nullOr str;
          default = null;
          description =
            "Optional name added as a CSS class, for styling multiple waybars.";
          example = "waybar-1";
        };

        gtk-layer-shell = mkOption {
          type = nullOr bool;
          default = null;
          example = false;
          description =
            "Option to disable the use of gtk-layer-shell for popups.";
        };
      };
    };
in {
  meta.maintainers = with lib.maintainers; [ berbiche ];

  options.programs.waybar = with lib.types; {
    enable = mkEnableOption "Waybar";

    package = mkOption {
      type = package;
      default = pkgs.waybar;
      defaultText = literalExpression "pkgs.waybar";
      description = ''
        Waybar package to use. Set to <code>null</code> to use the default package.
      '';
    };

    settings = mkOption {
      type = either (listOf waybarBarConfig) (attrsOf waybarBarConfig);
      default = [ ];
      description = ''
        Configuration for Waybar, see <link
          xlink:href="https://github.com/Alexays/Waybar/wiki/Configuration"/>
        for supported values.
      '';
      example = literalExpression ''
        {
          mainBar = {
            layer = "top";
            position = "top";
            height = 30;
            output = [
              "eDP-1"
              "HDMI-A-1"
            ];
            modules-left = [ "sway/workspaces" "sway/mode" "wlr/taskbar" ];
            modules-center = [ "sway/window" "custom/hello-from-waybar" ];
            modules-right = [ "mpd" "custom/mymodule#with-css-id" "temperature" ];

            "sway/workspaces" = {
              disable-scroll = true;
              all-outputs = true;
            };
            "custom/hello-from-waybar" = {
              format = "hello {}";
              max-length = 40;
              interval = "once";
              exec = pkgs.writeShellScript "hello-from-waybar" '''
                echo "from within waybar"
              ''';
            };
          };
        }
      '';
    };

    systemd.enable = mkEnableOption "Waybar systemd integration";

    systemd.target = mkOption {
      type = str;
      default = "graphical-session.target";
      example = "sway-session.target";
      description = ''
        The systemd target that will automatically start the Waybar service.
        </para>
        <para>
        When setting this value to <literal>"sway-session.target"</literal>,
        make sure to also enable <option>wayland.windowManager.sway.systemdIntegration</option>,
        otherwise the service may never be started.
      '';
    };

    style = mkOption {
      type = nullOr (either path str);
      default = null;
      description = ''
        CSS style of the bar.
        </para>
        <para>
        See <link xlink:href="https://github.com/Alexays/Waybar/wiki/Configuration"/>
        for the documentation.
        </para>
        <para>
        If the value is set to a path literal, then the path will be used as the css file.
      '';
      example = ''
        * {
          border: none;
          border-radius: 0;
          font-family: Source Code Pro;
        }
        window#waybar {
          background: #16191C;
          color: #AAB2BF;
        }
        #workspaces button {
          padding: 0 5px;
        }
      '';
    };
  };

  config = let
    # Removes nulls because Waybar ignores them.
    # This is not recursive.
    removeTopLevelNulls = filterAttrs (_: v: v != null);

    # Makes the actual valid configuration Waybar accepts
    # (strips our custom settings before converting to JSON)
    makeConfiguration = configuration:
      let
        # The "modules" option is not valid in the JSON
        # as its descendants have to live at the top-level
        settingsWithoutModules = removeAttrs configuration [ "modules" ];
        settingsModules =
          optionalAttrs (configuration.modules != null) configuration.modules;
      in removeTopLevelNulls (settingsWithoutModules // settingsModules);

    # Allow using attrs for settings instead of a list in order to more easily override
    settings = if builtins.isAttrs cfg.settings then
      lib.attrValues cfg.settings
    else
      cfg.settings;

    # The clean list of configurations
    finalConfiguration = map makeConfiguration settings;

    configSource = jsonFormat.generate "waybar-config.json" finalConfiguration;

  in mkIf cfg.enable (mkMerge [
    {
      assertions = [
        (lib.hm.assertions.assertPlatform "programs.waybar" pkgs
          lib.platforms.linux)
        ({
          assertion =
            if lib.versionAtLeast config.home.stateVersion "22.05" then
              all (x: !hasAttr "modules" x || x.modules == null) settings
            else
              true;
          message = ''
            The `programs.waybar.settings.[].modules` option has been removed.
            It is now possible to declare modules in the configuration without nesting them under the `modules` option.
          '';
        })
      ];

      home.packages = [ cfg.package ];

      xdg.configFile."waybar/config" = mkIf (settings != [ ]) {
        source = configSource;
        onChange = ''
          ${pkgs.procps}/bin/pkill -u $USER -USR2 waybar || true
        '';
      };

      xdg.configFile."waybar/style.css" = mkIf (cfg.style != null) {
        source = if builtins.isPath cfg.style || isStorePath cfg.style then
          cfg.style
        else
          pkgs.writeText "waybar/style.css" cfg.style;
        onChange = ''
          ${pkgs.procps}/bin/pkill -u $USER -USR2 waybar || true
        '';
      };
    }

    (mkIf cfg.systemd.enable {
      systemd.user.services.waybar = {
        Unit = {
          Description =
            "Highly customizable Wayland bar for Sway and Wlroots based compositors.";
          Documentation = "https://github.com/Alexays/Waybar/wiki";
          PartOf = [ "graphical-session.target" ];
          After = [ "graphical-session.target" ];
        };

        Service = {
          ExecStart = "${cfg.package}/bin/waybar";
          ExecReload = "${pkgs.coreutils}/bin/kill -SIGUSR2 $MAINPID";
          Restart = "on-failure";
          KillMode = "mixed";
        };

        Install = { WantedBy = [ cfg.systemd.target ]; };
      };
    })
  ]);
}
