{
  config,
  lib,
  pkgs,
  ...
}:
let
  inherit (lib) mkIf mkOption types;

  cfg = config.xsession.windowManager.i3;

  commonOptions = import ./lib/options.nix {
    inherit
      config
      lib
      cfg
      pkgs
      ;
    moduleName = "i3";
  };

  configModule = types.submodule {
    options = {
      inherit (commonOptions)
        fonts
        window
        floating
        focus
        assigns
        modifier
        workspaceLayout
        workspaceAutoBackAndForth
        keycodebindings
        colors
        bars
        startup
        gaps
        menu
        terminal
        defaultWorkspace
        workspaceOutputAssign
        ;

      keybindings = mkOption {
        type = types.attrsOf (types.nullOr types.str);
        default = lib.mapAttrs (n: lib.mkOptionDefault) {
          "${cfg.config.modifier}+Return" = "exec ${cfg.config.terminal}";
          "${cfg.config.modifier}+Shift+q" = "kill";
          "${cfg.config.modifier}+d" = "exec ${cfg.config.menu}";

          "${cfg.config.modifier}+Left" = "focus left";
          "${cfg.config.modifier}+Down" = "focus down";
          "${cfg.config.modifier}+Up" = "focus up";
          "${cfg.config.modifier}+Right" = "focus right";

          "${cfg.config.modifier}+Shift+Left" = "move left";
          "${cfg.config.modifier}+Shift+Down" = "move down";
          "${cfg.config.modifier}+Shift+Up" = "move up";
          "${cfg.config.modifier}+Shift+Right" = "move right";

          "${cfg.config.modifier}+h" = "split h";
          "${cfg.config.modifier}+v" = "split v";
          "${cfg.config.modifier}+f" = "fullscreen toggle";

          "${cfg.config.modifier}+s" = "layout stacking";
          "${cfg.config.modifier}+w" = "layout tabbed";
          "${cfg.config.modifier}+e" = "layout toggle split";

          "${cfg.config.modifier}+Shift+space" = "floating toggle";
          "${cfg.config.modifier}+space" = "focus mode_toggle";

          "${cfg.config.modifier}+a" = "focus parent";

          "${cfg.config.modifier}+Shift+minus" = "move scratchpad";
          "${cfg.config.modifier}+minus" = "scratchpad show";

          "${cfg.config.modifier}+1" = "workspace number 1";
          "${cfg.config.modifier}+2" = "workspace number 2";
          "${cfg.config.modifier}+3" = "workspace number 3";
          "${cfg.config.modifier}+4" = "workspace number 4";
          "${cfg.config.modifier}+5" = "workspace number 5";
          "${cfg.config.modifier}+6" = "workspace number 6";
          "${cfg.config.modifier}+7" = "workspace number 7";
          "${cfg.config.modifier}+8" = "workspace number 8";
          "${cfg.config.modifier}+9" = "workspace number 9";
          "${cfg.config.modifier}+0" = "workspace number 10";

          "${cfg.config.modifier}+Shift+1" = "move container to workspace number 1";
          "${cfg.config.modifier}+Shift+2" = "move container to workspace number 2";
          "${cfg.config.modifier}+Shift+3" = "move container to workspace number 3";
          "${cfg.config.modifier}+Shift+4" = "move container to workspace number 4";
          "${cfg.config.modifier}+Shift+5" = "move container to workspace number 5";
          "${cfg.config.modifier}+Shift+6" = "move container to workspace number 6";
          "${cfg.config.modifier}+Shift+7" = "move container to workspace number 7";
          "${cfg.config.modifier}+Shift+8" = "move container to workspace number 8";
          "${cfg.config.modifier}+Shift+9" = "move container to workspace number 9";
          "${cfg.config.modifier}+Shift+0" = "move container to workspace number 10";

          "${cfg.config.modifier}+Shift+c" = "reload";
          "${cfg.config.modifier}+Shift+r" = "restart";
          "${cfg.config.modifier}+Shift+e" =
            "exec i3-nagbar -t warning -m 'Do you want to exit i3?' -b 'Yes' 'i3-msg exit'";

          "${cfg.config.modifier}+r" = "mode resize";
        };
        defaultText = "Default i3 keybindings.";
        description = ''
          An attribute set that assigns a key press to an action using a key symbol.
          See <https://i3wm.org/docs/userguide.html#keybindings>.

          Consider to use `lib.mkOptionDefault` function to extend or override
          default keybindings instead of specifying all of them from scratch.
        '';
        example = lib.literalExpression ''
          let
            modifier = config.xsession.windowManager.i3.config.modifier;
          in lib.mkOptionDefault {
            "''${modifier}+Return" = "exec i3-sensible-terminal";
            "''${modifier}+Shift+q" = "kill";
            "''${modifier}+d" = "exec ''${pkgs.dmenu}/bin/dmenu_run";
          }
        '';
      };

      modes = mkOption {
        type = types.attrsOf (types.attrsOf types.str);
        default = {
          resize = {
            "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";
            "Escape" = "mode default";
            "Return" = "mode default";
          };
        };
        description = ''
          An attribute set that defines binding modes and keybindings
          inside them

          Only basic keybinding is supported (bindsym keycomb action),
          for more advanced setup use 'i3.extraConfig'.
        '';
      };
    };
  };

  commonFunctions = import ./lib/functions.nix {
    inherit config cfg lib;
    moduleName = "i3";
  };

  inherit (commonFunctions)
    keybindingsStr
    keycodebindingsStr
    modeStr
    assignStr
    barStr
    gapsStr
    floatingCriteriaStr
    windowCommandsStr
    colorSetStr
    windowBorderString
    fontConfigStr
    keybindingDefaultWorkspace
    keybindingsRest
    workspaceOutputStr
    ;

  startupEntryStr =
    {
      command,
      always,
      notification,
      workspace,
      ...
    }:
    lib.concatStringsSep " " [
      (if always then "exec_always" else "exec")
      (if (notification && workspace == null) then "" else "--no-startup-id")
      (if (workspace == null) then command else "i3-msg 'workspace ${workspace}; exec ${command}'")
    ];

  configFile = pkgs.writeText "i3.conf" (
    lib.concatStringsSep "\n" (
      (
        if cfg.config != null then
          with cfg.config;
          (
            [
              (fontConfigStr fonts)
              "floating_modifier ${floating.modifier}"
              (windowBorderString window floating)
              "hide_edge_borders ${window.hideEdgeBorders}"
              "focus_wrapping ${focus.wrapping}"
              "focus_follows_mouse ${lib.hm.booleans.yesNo focus.followMouse}"
              "focus_on_window_activation ${focus.newWindow}"
              "mouse_warping ${if focus.mouseWarping then "output" else "none"}"
              "workspace_layout ${workspaceLayout}"
              "workspace_auto_back_and_forth ${lib.hm.booleans.yesNo workspaceAutoBackAndForth}"
              "client.focused ${colorSetStr colors.focused}"
              "client.focused_inactive ${colorSetStr colors.focusedInactive}"
              "client.unfocused ${colorSetStr colors.unfocused}"
              "client.urgent ${colorSetStr colors.urgent}"
              "client.placeholder ${colorSetStr colors.placeholder}"
              "client.background ${colors.background}"
              (keybindingsStr { keybindings = keybindingDefaultWorkspace; })
              (keybindingsStr { keybindings = keybindingsRest; })
              (keycodebindingsStr keycodebindings)
            ]
            ++ lib.mapAttrsToList (modeStr false) modes
            ++ lib.mapAttrsToList assignStr assigns
            ++ map barStr bars
            ++ lib.optional (gaps != null) gapsStr
            ++ map floatingCriteriaStr floating.criteria
            ++ map windowCommandsStr window.commands
            ++ map startupEntryStr startup
            ++ map workspaceOutputStr workspaceOutputAssign
          )
        else
          [ ]
      )
      ++ [ cfg.extraConfig ]
    )
  );

  # Validates the i3 configuration
  checkI3Config = pkgs.runCommandLocal "i3-config" { buildInputs = [ cfg.package ]; } ''
    # We have to make sure the wrapper does not start a dbus session
    export DBUS_SESSION_BUS_ADDRESS=1

    # A zero exit code means i3 successfully validated the configuration
    i3 -c ${configFile} -C -d all || {
      echo "i3 configuration validation failed"
      echo "For a verbose log of the failure, run 'i3 -c ${configFile} -C -d all'"
      exit 1
    };
    cp ${configFile} $out
  '';

in
{
  meta.maintainers = with lib.maintainers; [ sumnerevans ];

  options = {
    xsession.windowManager.i3 = {
      enable = lib.mkEnableOption "i3 window manager";

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

      config = mkOption {
        type = types.nullOr configModule;
        default = { };
        description = "i3 configuration options.";
      };

      extraConfig = mkOption {
        type = types.lines;
        default = "";
        description = "Extra configuration lines to add to ~/.config/i3/config.";
      };
    };
  };

  config = mkIf cfg.enable (
    lib.mkMerge [
      {
        assertions = [
          (lib.hm.assertions.assertPlatform "xsession.windowManager.i3" pkgs lib.platforms.linux)
        ];

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

        xsession.windowManager.command = "${cfg.package}/bin/i3";

        xdg.configFile."i3/config" = {
          source = checkI3Config;
          onChange = ''
            # There may be several sockets after log out/log in, but the old ones
            # will fail with "Connection refused".
            for i3Socket in ''${XDG_RUNTIME_DIR:-/run/user/$UID}/i3/ipc-socket.*; do
              if [[ -S $i3Socket ]]; then
                ${cfg.package}/bin/i3-msg -s $i3Socket reload >/dev/null |& grep -v "Connection refused" || true
              fi
            done
          '';
        };
      }

      (mkIf (cfg.config != null) {
        warnings =
          (lib.optional (lib.isList cfg.config.fonts) "Specifying i3.config.fonts as a list is deprecated. Use the attrset version instead.")
          ++ lib.flatten (
            map (
              b:
              lib.optional (lib.isList b.fonts) "Specifying i3.config.bars[].fonts as a list is deprecated. Use the attrset version instead."
            ) cfg.config.bars
          )
          ++ [
            (mkIf (lib.any (s: s.workspace != null) cfg.config.startup) (
              "'xsession.windowManager.i3.config.startup.*.workspace' is deprecated, "
              + "use 'xsession.windowManager.i3.config.assigns' instead."
              + "See https://github.com/nix-community/home-manager/issues/265."
            ))
            (mkIf cfg.config.focus.forceWrapping (
              "'xsession.windowManager.i3.config.focus.forceWrapping' is deprecated, "
              + "use 'xsession.windowManager.i3.config.focus.wrapping' instead."
            ))
          ];
      })
    ]
  );
}
