{ config, lib, pkgs, ... }:
let
  isEnabled = config.my.home.wm.windowManager == "i3";

  terminal =
    if config.my.home.terminal.program != null
    then config.my.home.terminal.program
    else "i3-sensible-terminal";

  alt = "Mod1"; # `Alt` key
  modifier = "Mod4"; # `Super` key
  movementKeys = [ "Left" "Down" "Up" "Right" ];
  vimMovementKeys = [ "h" "j" "k" "l" ];
  shutdownMode =
    "(l)ock, (e)xit, switch_(u)ser, (h)ibernate, (r)eboot, (Shift+s)hutdown";

  # Takes an attrset of bindings for movement keys, transforms it to Vim keys
  toVimKeyBindings =
    let
      toVimKeys = builtins.replaceStrings movementKeys vimMovementKeys;
    in
    lib.my.renameAttrs toVimKeys;

  # Takes an attrset of bindings for movement keys, add equivalent Vim keys
  addVimKeyBindings = bindings: bindings // (toVimKeyBindings bindings);
  # Generate an attrset of movement bindings, using the mapper function
  genMovementBindings = f: addVimKeyBindings (lib.my.genAttrs' movementKeys f);

  # Used in multiple scripts to show messages through keybindings
  notify-send = lib.getExe pkgs.libnotify;

  # Screen backlight management
  changeBacklight = lib.getExe pkgs.ambroisie.change-backlight;

  # Audio and volume management
  changeAudio = lib.getExe pkgs.ambroisie.change-audio;

  # Lock management
  toggleXautolock =
    let
      systemctlUser = "${pkgs.systemd}/bin/systemctl --user";
      notify = "${notify-send} -u low"
        + " -h string:x-canonical-private-synchronous:xautolock-toggle";
    in
    pkgs.writeScript "toggle-xautolock" ''
      #!/bin/sh
      if ${systemctlUser} is-active xautolock-session.service; then
        ${systemctlUser} stop --user xautolock-session.service
        xset s off
        ${notify} "Disabled Xautolock"
      else
        ${systemctlUser} start xautolock-session.service
        xset s on
        ${notify} "Enabled Xautolock"
      fi
    '';
in
{
  config = lib.mkIf isEnabled {
    home.packages = with pkgs; [
      ambroisie.dragger # drag-and-drop from the CLI
      ambroisie.i3-get-window-criteria # little helper for i3 configuration
      arandr # Used by a mapping
      playerctl # Used by a mapping
      xdotool # Used by 'rofi-rbw', in a mapping
    ];

    xsession.windowManager.i3 = {
      enable = true;

      config = {
        inherit modifier;

        bars =
          let
            i3status-rs = lib.getExe config.programs.i3status-rust.package;
          in
          assert [ "top" ] == lib.attrNames config.programs.i3status-rust.bars;
          [
            {
              statusCommand = "${i3status-rs} config-top.toml";
              trayOutput = "primary";
              position = "top";

              colors = {
                background = "#021215";
                statusline = "#93a1a1";
                separator = "#2aa198";

                focusedWorkspace = {
                  border = "#2aa198";
                  background = "#073642";
                  text = "#eee895";
                };

                activeWorkspace = {
                  border = "#073642";
                  background = "#002b36";
                  text = "#839496";
                };

                inactiveWorkspace = {
                  border = "#002b36";
                  background = "#021215";
                  text = "#586e75";
                };

                urgentWorkspace = {
                  border = "#cb4b16";
                  background = "#dc322f";
                  text = "#fdf6e3";
                };
              };

              fonts = {
                names = [ "DejaVu Sans Mono" "FontAwesome5Free" ];
                size = 8.0;
              };
            }
          ];

        floating = {
          inherit modifier;

          criteria = [
            { class = "^tridactyl_editor$"; }
            { class = "^Blueman-.*$"; }
            { title = "^htop$"; }
            { class = "^Thunderbird$"; instance = "Mailnews"; window_role = "filterlist"; }
            { class = "^Pavucontrol.*$"; }
            { class = "^Arandr$"; }
            { class = ".?blueman-manager.*$"; }
          ];
        };

        focus = {
          followMouse = true; # It is annoying sometimes, but useful enough to use
          mouseWarping = true; # Let's moving around when switching screens
        };

        fonts = {
          names = [ "DejaVu Sans Mono" ];
          size = 8.0;
        };

        # I don't care for i3's default values, I specify them all explicitly
        keybindings = lib.my.recursiveMerge [
          {
            # The basics
            "${modifier}+Return" = "exec ${terminal} ${
              lib.optionalString config.my.home.tmux.enable "-e tmux new-session"
            }";
            "${modifier}+Shift+Return" = "exec env TMUX=nil ${terminal}";
            "${modifier}+Shift+q" = "kill";
            "${modifier}+f" = "fullscreen toggle";
            "${modifier}+Shift+c" = "reload";
            "${modifier}+Shift+r" = "restart";
            "${modifier}+Shift+e" =
              "exec i3-nagbar -t warning -m 'Do you want to exit i3?' -b 'Yes' 'i3-msg exit'";
          }
          {
            # Splits
            "${modifier}+g" = "split h"; # Horizontally
            "${modifier}+v" = "split v"; # Vertically
          }
          {
            # Layouts
            "${modifier}+s" = "layout stacking";
            "${modifier}+w" = "layout tabbed";
            "${modifier}+e" = "layout toggle split";
          }
          {
            # Toggle tiling/floating
            "${modifier}+Control+space" = "floating toggle";
            # Change focus between tiling/floating
            "${modifier}+space" = "focus mode_toggle";
            # Center floating window
            "${modifier}+c" = "move absolute position center";
            # Center floating window
            "${modifier}+Shift+s" = "sticky toggle";
          }
          {
            # Focus parent container
            "${modifier}+q" = "focus parent";
            # Focus child container
            "${modifier}+a" = "focus child";
          }
          (lib.optionalAttrs config.my.home.wm.rofi.enable {
            # Rofi tools
            "${modifier}+d" = "exec rofi -show drun -disable-history";
            "${modifier}+Shift+d" = "exec rofi -show run -disable-history";
            "${modifier}+p" = "exec --no-startup-id flameshot gui";
            "${modifier}+Ctrl+p" = "exec ${lib.getExe pkgs.rofi-rbw}";
            "${modifier}+Shift+p" = "exec rofi -show emoji";
            "${modifier}+b" =
              let
                inherit (config.my.home.bluetooth) enable;
                prog = lib.getExe pkgs.rofi-bluetooth;
              in
              lib.mkIf enable "exec ${prog} -i";
          })
          (
            # Changing container focus
            genMovementBindings (
              key: lib.nameValuePair
                "${modifier}+${key}"
                "focus ${lib.toLower key}"
            )
          )
          (
            # Changing screen focus
            genMovementBindings (
              key: lib.nameValuePair
                "${modifier}+${alt}+${key}"
                "focus output ${lib.toLower key}"
            )
          )
          (
            # Moving workspace to another screen
            genMovementBindings (
              key: lib.nameValuePair
                "${modifier}+${alt}+Control+${key}"
                "move workspace to output ${lib.toLower key}"
            )
          )
          (
            # Moving container to another screen
            genMovementBindings (
              key: lib.nameValuePair
                "${modifier}+${alt}+Shift+${key}"
                "move container to output ${lib.toLower key}"
            )
          )
          (addVimKeyBindings {
            # Scroll through workspaces on given screen
            "${modifier}+Control+Left" = "workspace prev_on_output";
            "${modifier}+Control+Right" = "workspace next_on_output";
            # Use scratchpad
            "${modifier}+Control+Up" = "move to scratchpad";
            "${modifier}+Control+Down" = "scratchpad show";
          })
          (
            # Moving floating window
            genMovementBindings (
              key: lib.nameValuePair
                "${modifier}+Shift+${key}"
                "move ${lib.toLower key} 10 px"
            )
          )
          {
            # Media keys
            "XF86AudioRaiseVolume" = "exec --no-startup-id ${changeAudio} up 5";
            "XF86AudioLowerVolume" = "exec --no-startup-id ${changeAudio} down 5";
            "Control+XF86AudioRaiseVolume" = "exec --no-startup-id ${changeAudio} up 1";
            "Control+XF86AudioLowerVolume" = "exec --no-startup-id ${changeAudio} down 1";

            "Shift+XF86AudioRaiseVolume" = "exec --no-startup-id ${changeAudio} up --force 5";
            "Shift+XF86AudioLowerVolume" = "exec --no-startup-id ${changeAudio} down --force 5";
            "Control+Shift+XF86AudioRaiseVolume" = "exec --no-startup-id ${changeAudio} up --force 1";
            "Control+Shift+XF86AudioLowerVolume" = "exec --no-startup-id ${changeAudio} down --force 1";

            "XF86AudioMute" = "exec --no-startup-id ${changeAudio} toggle";
            "XF86AudioMicMute" = "exec --no-startup-id ${changeAudio} toggle mic";

            "XF86AudioPlay" = "exec playerctl play-pause";
            "XF86AudioNext" = "exec playerctl next";
            "XF86AudioPrev" = "exec playerctl previous";
          }
          {
            # Screen management
            "XF86Display" = "exec arandr";
            "XF86MonBrightnessUp" = "exec --no-startup-id ${changeBacklight} up 10";
            "XF86MonBrightnessDown" = "exec --no-startup-id ${changeBacklight} down 10";
            "Control+XF86MonBrightnessUp" = "exec --no-startup-id ${changeBacklight} up 1";
            "Control+XF86MonBrightnessDown" = "exec --no-startup-id ${changeBacklight} down 1";
          }
          {
            # Sub-modes
            "${modifier}+r" = "mode resize";
            "${modifier}+Shift+space" = "mode floating";
          }
          (lib.optionalAttrs config.my.home.wm.screen-lock.enable {
            "${modifier}+x" = "exec ${toggleXautolock}";
          })
          (
            let
              execDunstctl = "exec ${pkgs.dunst}/bin/dunstctl";
            in
            lib.optionalAttrs config.my.home.wm.dunst.enable {
              "${modifier}+minus" = "${execDunstctl} close";
              "${modifier}+Shift+minus" = "${execDunstctl} close-all";
              "${modifier}+equal" = "${execDunstctl} history-pop";
            }
          )
        ];

        keycodebindings =
          let
            toKeycode = n: if n == 0 then 19 else n + 9;
            createWorkspaceBindings = mapping: command:
              let
                createWorkspaceBinding = num:
                  lib.nameValuePair
                    "${mapping}+${toString (toKeycode num)}"
                    "${command} ${toString num}";
                oneToNine = builtins.genList (x: x + 1) 9;
              in
              lib.my.genAttrs' oneToNine createWorkspaceBinding;
          in
          lib.my.recursiveMerge [
            (createWorkspaceBindings modifier "workspace number")
            (createWorkspaceBindings "${modifier}+Shift" "move container to workspace number")
            {
              "${modifier}+${toString (toKeycode 0)}" = ''mode "${shutdownMode}"'';
            }
          ];

        modes =
          let
            makeModeBindings = attrs: (addVimKeyBindings attrs) // {
              "Escape" = "mode default";
              "Return" = "mode default";
            };
          in
          {
            resize = makeModeBindings {
              # Normal movements
              "Left" = "resize shrink width 10 px or 10 ppt";
              "Down" = "resize grow height 10 px or 10 ppt";
              "Up" = "resize shrink height 10 px or 10 ppt";
              "Right" = "resize grow width 10 px or 10 ppt";
              # Small movements
              "Control+Left" = "resize shrink width 1 px or 1 ppt";
              "Control+Down" = "resize grow height 1 px or 1 ppt";
              "Control+Up" = "resize shrink height 1 px or 1 ppt";
              "Control+Right" = "resize grow width 1 px or 1 ppt";
              # Big movements
              "Shift+Left" = "resize shrink width 100 px or 100 ppt";
              "Shift+Down" = "resize grow height 100 px or 100 ppt";
              "Shift+Up" = "resize shrink height 100 px or 100 ppt";
              "Shift+Right" = "resize grow width 100 px or 100 ppt";
            };

            floating = makeModeBindings {
              # Normal movements
              "Left" = "move left 10 px";
              "Down" = "move down 10 px";
              "Up" = "move up 10 px";
              "Right" = "move right 10 px";
              # Small movements
              "Control+Left" = "move left 1 px";
              "Control+Down" = "move down 1 px";
              "Control+Up" = "move up 1 px";
              "Control+Right" = "move right 1 px";
              # Big movements
              "Shift+Left" = "move left 100 px";
              "Shift+Down" = "move down 100 px";
              "Shift+Up" = "move up 100 px";
              "Shift+Right" = "move right 100 px";
            };

            ${shutdownMode} = makeModeBindings {
              "l" = "exec --no-startup-id loginctl lock-session, mode default";
              "s" = "exec --no-startup-id systemctl suspend, mode default";
              "u" = "exec --no-startup-id dm-tool switch-to-greeter, mode default";
              "e" = "exec --no-startup-id i3-msg exit, mode default";
              "h" = "exec --no-startup-id systemctl hibernate, mode default";
              "r" = "exec --no-startup-id systemctl reboot, mode default";
              "Shift+s" = "exec --no-startup-id systemctl poweroff, mode default";
            };
          };

        startup = [
          # NOTE: rely on systemd user services instead...
        ];

        window = {
          commands = [
            # Make htop window bigger
            {
              criteria = { title = "^htop$"; };
              command = "resize set 80 ppt 80 ppt, move position center";
            }
          ];
        };
      };
    };
  };
}