{
  config,
  lib,
  pkgs,
  pkgsHyprland,
  ...
}: {
  imports = [
    ../../../../../internal/wayland
    ../../../home/sessionVariables
    ../../../programs/rofi
    ../../../programs/wlogout
    ../../../services/dunst
    ../../../services/swayidle
  ];

  options.dotfiles.inputs.home-manager.wayland.windowManager.hyprland.enable =
    lib.dotfiles.mkEnableOption;

  config =
    lib.mkIf
    config.dotfiles.inputs.home-manager.wayland.windowManager.hyprland.enable
    {
      dotfiles = {
        inputs = {
          home-manager = {
            home.sessionVariables = {
              BROWSER.enable = true;
              TERMINAL.enable = true;
              TMPDIR.enable = true;
              enable = true;
            };

            programs = {
              rofi = {
                enable = true;
                pass.enable = true;
              };

              wlogout.enable = true;
            };

            services = {
              dunst.enable = true;

              swayidle.displayTimeout = {
                enable = true;

                timeouts = let
                  hyprctl = lib.getExe' pkgs.hyprland "hyprctl";
                in {
                  command = "${hyprctl} dispatch dpms off";
                  resumeCommand = "${hyprctl} dispatch dpms on";
                };
              };
            };
          };
        };

        internal.wayland.enable = true;
      };

      home.activation =
        lib.dotfiles.home.activation.systemRequirement
        "dotfiles.inputs.home-manager.wayland.windowManager.hyprland"
        "programs.hyprland.enable = true;"
        "https://wiki.nixos.org/wiki/Hyprland";

      wayland.windowManager.hyprland = {
        enable = true;
        package = pkgsHyprland.hyprland;

        settings = let
          helpers.bind = mods: list:
            lib.concatStringsSep "," (
              [(lib.concatStringsSep " " (["SUPER"] ++ mods))] ++ list
            );

          windowGap = 5;
        in {
          animations = let
            animationName = "windowsBezier";
          in {
            animation = [
              (
                builtins.concatStringsSep
                ","
                ["windows" (toString 1) (toString 7) animationName]
              )
            ];

            bezier =
              builtins.concatStringsSep
              ","
              ([animationName] ++ (map toString [0.05 0.9 0.1 1.05]));

            # Despite the animations being disabled, the animation settings are
            # written to the configuration file. Enabling the animations at run
            # time ensures the correct settings are used.
            enabled = false;
          };

          bind = let
            applications = let
              getBrightnessPercentage = pkgs.writeShellApplication {
                name = "get-brightness-percentage";
                runtimeInputs = with pkgs; [brightnessctl gawk];

                text = ''
                  brightnessctl --machine-readable |
                    awk -F ',' -v FPAT='[[:digit:]]+' '{print $3}'
                '';
              };

              getVolumePercentage = pkgs.writeShellApplication {
                name = "get-volume-percentage";
                runtimeInputs = with pkgs; [gawk wireplumber];

                text = ''
                  wpctl get-volume @DEFAULT_AUDIO_SINK@ |
                    awk '{ printf "%d\n", $NF * 100 }'
                '';
              };

              helpers = {
                increaseBrightness = let
                  stepPercentage = toString 10;
                in
                  increase: let
                    maximumBrightnessPercentageString =
                      toString
                      maximumBrightnessPercentage;

                    minimumBrightnessPercentageString =
                      toString
                      minimumBrightnessPercentage;

                    type = helpers.increaseToString increase;
                    value = let
                      percentage = "${stepPercentage}%";
                    in
                      if increase
                      then "+${percentage}"
                      else "${percentage}-";
                  in
                    lib.getExe (
                      pkgs.writeShellApplication {
                        name = "set-brightness-${type}";

                        runtimeInputs = with pkgs;
                          [brightnessctl libnotify]
                          ++ [getBrightnessPercentage];

                        text = ''
                          # The --min-value key and its value must be separated
                          # by an equal sign. Using a space results in undefined
                          # behaviour. [1]
                          #
                          # [1]: https://github.com/Hummer12007/brightnessctl/issues/82
                          brightnessctl \
                            --min-value="${minimumBrightnessPercentageString}" \
                            set "${value}"

                          brightness="$(
                            ${getBrightnessPercentage.meta.mainProgram}
                          )"

                          if ((
                            brightness <= ${minimumBrightnessPercentageString} ||
                            brightness >= ${maximumBrightnessPercentageString}
                          )); then
                            notify-send \
                              "Brightness" \
                              "<u>Value:</u> $brightness%"
                          fi
                        '';
                      }
                    );

                increaseToString = increase:
                  if increase
                  then "increase"
                  else "decrease";

                increaseVolume = let
                  stepPercentage = toString 5;
                in
                  increase: let
                    value = "${stepPercentage}%${
                      if increase
                      then "+"
                      else "-"
                    }";
                  in
                    lib.getExe (
                      pkgs.writeShellApplication {
                        name = "set-volume-${
                          helpers.increaseToString increase
                        }";

                        runtimeInputs = with pkgs;
                          [libnotify wireplumber]
                          ++ [getVolumePercentage];

                        text = ''
                          wpctl set-volume @DEFAULT_AUDIO_SINK@ "${value}"

                          volume="$(${getVolumePercentage.meta.mainProgram})"

                          if ((
                            volume <= ${toString minimumVolumePercentage} ||
                            volume >= ${toString maximumVolumePercentage}
                          )); then
                            notify-send "Volume" "<u>Value:</u> $volume%"
                          fi
                        '';
                      }
                    );

                maximiseBrightness = maximise: let
                  type = helpers.maximiseToString maximise;

                  value = "${
                    toString (
                      if maximise
                      then maximumBrightnessPercentage
                      else minimumBrightnessPercentage
                    )
                  }%";
                in
                  lib.getExe (
                    pkgs.writeShellApplication {
                      name = "set-brightness-${type}";
                      runtimeInputs = [pkgs.brightnessctl];
                      text = ''brightnessctl set "${value}"'';
                    }
                  );

                maximiseToString = maximise:
                  if maximise
                  then "maximise"
                  else "minimise";

                maximiseVolume = maximise:
                  lib.getExe (
                    pkgs.writeShellApplication {
                      name = "set-volume-${helpers.maximiseToString maximise}";
                      runtimeInputs = [pkgs.wireplumber];

                      text = ''wpctl set-volume @DEFAULT_AUDIO_SINK@ "${
                          toString (
                            if maximise
                            then maximumVolumePercentage
                            else minimumVolumePercentage
                          )
                        }%"'';
                    }
                  );
              };

              maximumBrightnessPercentage = 100;
              maximumVolumePercentage = 100;
              minimumBrightnessPercentage = 1;
              minimumVolumePercentage = 0;

              recordOutputFile = ''${
                  config.home.sessionVariables.TMPDIR
                }/$(date "+%Y_%m_%d_%H_%M_%S").mp4'';
            in {
              cycleLayout = lib.getExe (
                pkgs.writeShellApplication {
                  name = "${pkgs.hyprland.meta.mainProgram}-cycle-layout";
                  runtimeInputs = with pkgs; [hyprland jq];

                  text = let
                    general.layout = {
                      dwindle = "dwindle";
                      master = "master";
                    };
                  in ''
                    old_layout="$(
                      hyprctl getoption -j general:layout | jq -r '.str'
                    )"

                    if [[ "$old_layout" == "${general.layout.dwindle}" ]]; then
                      new_layout="${general.layout.master}"
                    else
                      new_layout="${general.layout.dwindle}"
                    fi

                    hyprctl keyword general:layout "$new_layout"
                  '';
                }
              );

              decreaseBrightness = helpers.increaseBrightness false;
              decreaseVolume = helpers.increaseVolume false;

              hyprlandToggleMode = lib.getExe (
                pkgs.writeShellApplication {
                  name = "${pkgs.hyprland.meta.mainProgram}-toggle-mode";
                  runtimeInputs = with pkgs; [hyprland jq];

                  text = let
                    batch = let
                      fancy_gap = 20;
                    in
                      lib.concatMapStringsSep
                      ";"
                      (
                        {
                          name,
                          value,
                        }: "keyword ${name} ${toString value}"
                      )
                      [
                        (lib.nameValuePair "animations:enabled" 1)
                        (lib.nameValuePair "decoration:blur:enabled" 1)
                        (lib.nameValuePair "decoration:drop_shadow" 1)
                        (lib.nameValuePair "decoration:inactive_opacity" 0.85)
                        (lib.nameValuePair "decoration:rounding" fancy_gap)
                        (lib.nameValuePair "general:border_size" 1)
                        (lib.nameValuePair "general:gaps_in" fancy_gap)
                        (lib.nameValuePair "general:gaps_out" (fancy_gap * 2))
                        (lib.nameValuePair "misc:animate_manual_resizes" 1)

                        (
                          lib.nameValuePair
                          "misc:animate_mouse_windowdragging_resizes"
                          1
                        )
                      ];
                  in ''
                    animations_enabled="$(
                      hyprctl getoption -j animations:enabled | jq -r '.int'
                    )"

                    if (( animations_enabled == 0 )); then
                      hyprctl --batch '${batch}'
                    else
                      hyprctl reload
                    fi
                  '';
                }
              );

              increaseBrightness = helpers.increaseBrightness true;
              increaseVolume = helpers.increaseVolume true;
              maximiseBrightness = helpers.maximiseBrightness true;
              maximiseVolume = helpers.maximiseVolume true;
              minimiseBrightness = helpers.maximiseBrightness false;
              minimiseVolume = helpers.maximiseVolume false;

              recordEntireScreen = lib.getExe (
                pkgs.writeShellApplication {
                  name = "record-entire-screen";
                  runtimeInputs = [pkgs.wf-recorder];

                  text = ''
                    ${config.home.sessionVariables.TERMINAL} \
                      -e \
                      wf-recorder \
                      --audio \
                      -f "${recordOutputFile}"
                  '';
                }
              );

              recordSelection = lib.getExe (
                pkgs.writeShellApplication {
                  name = "record-selection";
                  runtimeInputs = with pkgs; [wf-recorder slurp];

                  text = ''
                    ${config.home.sessionVariables.TERMINAL} \
                      -e \
                      wf-recorder \
                      --audio \
                      --geometry "$(slurp)" \
                      -f "${recordOutputFile}"
                  '';
                }
              );

              screenshotActiveWindow = lib.getExe (
                pkgs.writeShellApplication {
                  name = "screenshot-active-window";
                  runtimeInputs = with pkgs; [grim hyprland jq wl-clipboard];

                  text = ''
                    hyprctl -j activewindow |
                      jq -r '"\(.at[0]),\(.at[1]) \(.size[0])x\(.size[1])"' |
                      grim -g - - |
                      wl-copy
                  '';
                }
              );

              screenshotSelection = lib.getExe (
                pkgs.writeShellApplication {
                  name = "screenshot-selection";
                  runtimeInputs = with pkgs; [grim slurp wl-clipboard];
                  text = "slurp | grim -g - - | wl-copy";
                }
              );

              systemStatus = lib.getExe (
                pkgs.writeShellApplication {
                  name = "system-status";

                  runtimeInputs = with pkgs;
                    [acpi gawk libnotify]
                    ++ [getBrightnessPercentage getVolumePercentage];

                  text = let
                    body = lib.dotfiles.notifySend.body [
                      (
                        lib.nameValuePair
                        "Battery"
                        "$(acpi | awk '{print substr($0, index($0, $3))}')"
                      )

                      (
                        lib.nameValuePair
                        "Brightness"
                        "$(${getBrightnessPercentage.meta.mainProgram})%"
                      )

                      (
                        lib.nameValuePair
                        "Date"
                        ''$(date "+%Y-%m-%d %H:%M:%S")''
                      )

                      (
                        lib.nameValuePair
                        "Volume"
                        "$(${getVolumePercentage.meta.mainProgram})%"
                      )
                    ];
                  in ''
                    notify-send "System Status" "${body}"
                  '';
                }
              );
            };

            bind = mods: key: dispatcher: parameters:
              helpers.bind mods [key dispatcher parameters];

            windowResize = "10%";
          in
            builtins.concatMap
            (
              index: let
                key = toString index;
                workspace = toString (index + 1);
              in [
                (bind ["SHIFT"] key "movetoworkspacesilent" workspace)
                (bind [] key "workspace" workspace)
              ]
            )
            (lib.range 0 9)
            ++ [
              (bind ["ALT" "SHIFT"] "H" "layoutmsg" "orientationleft")
              (bind ["ALT" "SHIFT"] "J" "layoutmsg" "orientationbottom")
              (bind ["ALT" "SHIFT"] "K" "layoutmsg" "orientationtop")
              (bind ["ALT" "SHIFT"] "L" "layoutmsg" "orientationright")
              (bind ["ALT"] "C" "centerwindow" "")
              (bind ["ALT"] "D" "exec" "dunstctl close-all")
              (bind ["ALT"] "H" "resizeactive" "-${windowResize} 0")
              (bind ["ALT"] "J" "resizeactive" "0 -${windowResize}")
              (bind ["ALT"] "K" "resizeactive" "0 ${windowResize}")
              (bind ["ALT"] "L" "resizeactive" "${windowResize} 0")
              (bind ["ALT"] "M" "exec" applications.hyprlandToggleMode)
              (bind ["ALT"] "N" "exec" applications.cycleLayout)
              (bind ["ALT"] "P" "exec" applications.cycleLayout)
              (bind ["CTRL" "SHIFT"] "H" "exec" applications.minimiseVolume)
              (bind ["CTRL" "SHIFT"] "J" "exec" applications.minimiseBrightness)
              (bind ["CTRL" "SHIFT"] "K" "exec" applications.maximiseBrightness)
              (bind ["CTRL" "SHIFT"] "L" "exec" applications.maximiseVolume)
              (bind ["CTRL"] "C" "exec" "loginctl lock-session")
              (bind ["CTRL"] "H" "exec" applications.decreaseVolume)
              (bind ["CTRL"] "J" "exec" applications.decreaseBrightness)
              (bind ["CTRL"] "K" "exec" applications.increaseBrightness)
              (bind ["CTRL"] "L" "exec" applications.increaseVolume)

              (
                bind
                ["CTRL"]
                "Q"
                "exec"
                (lib.getExe config.programs.wlogout.package)
              )

              (bind ["CTRL"] "S" "exec" "systemctl suspend")
              (bind ["SHIFT"] "C" "exec" applications.screenshotActiveWindow)
              (bind ["SHIFT"] "F" "fakefullscreen" "")
              (bind ["SHIFT"] "J" "swapnext" "next")
              (bind ["SHIFT"] "K" "swapnext" "prev")
              (bind ["SHIFT"] "P" "pin" "")
              (bind ["SHIFT"] "Q" "killactive" "")
              (bind ["SHIFT"] "V" "exec" applications.recordSelection)
              (bind [] "B" "exec" config.home.sessionVariables.BROWSER)
              (bind [] "C" "exec" applications.screenshotSelection)
              (bind [] "F" "fullscreen" "")
              (bind [] "H" "focusmonitor" "-1")
              (bind [] "J" "cyclenext" "next")
              (bind [] "K" "cyclenext" "prev")
              (bind [] "L" "focusmonitor" "+1")

              (
                bind
                []
                "P"
                "exec"
                (lib.getExe config.programs.rofi.pass.package)
              )

              (
                bind
                []
                "R"
                "exec"
                "${lib.getExe config.programs.rofi.finalPackage} -show run"
              )

              (bind [] "S" "exec" applications.systemStatus)
              (bind [] "T" "exec" config.home.sessionVariables.TERMINAL)
              (bind [] "V" "exec" applications.recordEntireScreen)
              (bind [] "W" "togglefloating" "")
              (bind [] "mouse_down" "workspace" "e+1")
              (bind [] "mouse_up" "workspace" "e-1")
            ];

          bindm = let
            bindm = mods: key: dispatcher: helpers.bind mods [key dispatcher];
          in [
            (bindm [] "mouse:272" "movewindow")
            (bindm [] "mouse:273" "resizewindow")
          ];

          general = {
            gaps_in = windowGap;
            gaps_out = windowGap;
          };

          gestures.workspace_swipe = true;

          decoration = {
            blur.enabled = false;
            dim_inactive = true;

            # The toString conversion prevents the following harmless warning:
            #
            #     trace: warning: Imprecise conversion from float to string
            #     0.166667
            dim_strength = toString (1.0 / 6);

            drop_shadow = false;
            rounding = windowGap;
          };
        };
      };
    };
}
