{
  config,
  lib,
  pkgs,
  ...
}:
with lib;
let
  cfg = config.mjallen.programs.hyprland;
  drawer = "nwg-drawer -fm nautilus -term kitty -mb 10 -mt 10 -ml 10 -mr 10 -pbuseicontheme -i ${config.gtk.iconTheme.name}";
in
{
  imports = [
    ./options.nix
    ./theme.nix
  ];

  config = mkIf cfg.enable {
    # Home packages
    home.packages =
      with pkgs;
      (
        [
          box64
          brightnessctl
          ddcutil
          egl-wayland
          file-roller
          glib
          gnome-calculator
          gnome-calendar
          gnome-disk-utility
          gnome-firmware
          gnome-font-viewer
          gnome-logs
          gnome-photos
          gnome-tweaks
          gnome-weather
          gsettings-desktop-schemas
          hyprcursor
          hyprland
          hyprpaper
          hyprshot
          hyprsysteminfo
          kdePackages.qtmultimedia
          libnotify
          libz
          meson
          nautilus
          nomacs
          nwg-look
          overskride
          pamixer
          pavucontrol
          playerctl
          qt5.qtwayland
          qt6.qtwayland
          waybar
          wayland-protocols
          wayland-utils
          waypaper
          wev
          wl-clipboard
          wlogout
          wlroots
          xorg.xhost
          xsettingsd
          xwayland

          pkgs.mjallen.pipewire-python
        ]
        ++ (if cfg.notificationDaemon == "mako" then [ mako ] else [ dunst ])
        ++ (if cfg.launcher == "wofi" then [ wofi ] else [ rofi ])
      );

    # Session variables
    home.sessionVariables = {
      BROWSER = "${cfg.defaultApps.browser.pname}";
      CLUTTER_BACKEND = "wayland";
      EDITOR = "${cfg.defaultApps.editor.pname}";
      VISUAL = "${cfg.defaultApps.visual.pname}";
      ICON_THEME = config.gtk.iconTheme.name;
      GTK_CSD = "0";
      GTK_THEME = config.gtk.theme.name;
      GTK_USE_PORTAL = "1";
      HYPRCURSOR_THEME = config.home.pointerCursor.name;
      HYPRCURSOR_SIZE = config.home.pointerCursor.size;
      MOZ_ENABLE_WAYLAND = "1";
      NIXOS_OZONE_WL = "1";
      NIXOS_XDG_OPEN_USE_PORTAL = "1";
      QT_AUTO_SCREEN_SCALE_FACTOR = "1";
      QT_QPA_PLATFORM = "wayland-egl";
      QT_QPA_PLATFORMTHEME = "gtk3";
      QT_SCALE_FACTOR = "1";
      QT_WAYLAND_DISABLE_WINDOWDECORATION = "1";
      SDL_VIDEODRIVER = "wayland";
      TERMINAL = "${cfg.defaultApps.terminal.pname}";
      XCURSOR_THEME = config.home.pointerCursor.name;
      XCURSOR_SIZE = config.home.pointerCursor.size;
      XDG_CACHE_HOME = "\${HOME}/.cache";
      XDG_CONFIG_HOME = "\${HOME}/.config";
      XDG_CURRENT_DESKTOP = "Hyprland";
      XDG_DATA_HOME = "\${HOME}/.local/share";
      XDG_SESSION_DESKTOP = "Hyprland";
      XDG_SESSION_TYPE = "wayland";
    };

    # Services
    services = {
      hyprpolkitagent.enable = true;
      hyprpaper = {
        enable = true;
        settings = {
          preload = [ cfg.hyprpaper.wallpaperPath ];
          wallpaper =
            let
              useMonitorV2 = (lib.versionAtLeast pkgs.hyprland.version "0.40.0") && (cfg.monitorv2 != [ ]);
              names =
                if useMonitorV2 then
                  map (m: m.name) cfg.monitorv2
                else
                  [
                    cfg.display1.input
                    cfg.display2.input
                  ];
            in
            if cfg.hyprpaper.usePerMonitor then
              map (n: "${n}, ${cfg.hyprpaper.wallpaperPath}") names
            else
              [ ", ${cfg.hyprpaper.wallpaperPath}" ];
          splash = false;
        };
      };

      hypridle = {
        enable = true;
        settings = {
          general = {
            before_sleep_cmd = "loginctl lock-session"; # lock before suspend.
            after_sleep_cmd = "hyprctl dispatch dpms on"; # to avoid having to press a key twice to turn on the display.
            ignore_dbus_inhibit = false;
            lock_cmd = "pidof hyprlock || hyprlock"; # avoid starting multiple hyprlock instances.
          };
          listener = [
            # {
            #   timeout = 300; # 5min
            #   on-timeout = "brightnessctl -s set 10"; # set monitor backlight to minimum, avoid 0 on OLED monitor.
            #   on-resume = "brightnessctl -r"; # monitor backlight restore.
            # }
            {
              timeout = cfg.hyprIdle.lockScreenTimer;
              on-timeout = "loginctl lock-session"; # lock screen when timeout has passed
            }
            {
              timeout = cfg.hyprIdle.screenOffTimer;
              on-timeout = "hyprctl dispatch dpms off"; # screen off when timeout has passed
              on-resume = "hyprctl dispatch dpms on"; # screen on when activity is detected after timeout has fired.
            }
            {
              timeout = cfg.hyprIdle.suspendTimer;
              on-timeout = "systemctl suspend"; # suspend pc
            }
          ];
        };
      };
    };

    # Programs
    programs = {
      vscode.profiles.default.userSettings."window"."titleBarStyle" = "custom";

      hyprlock = {
        enable = true;
        settings = {
          background = [
            {
              monitor = "";
              path = "/run/wallpaper.jpg"; # supports png, jpg, webp (no animations, though)
              color = "rgba(25, 20, 20, 1.0)";

              # all these options are taken from hyprland, see https://wiki.hyprland.org/Configuring/Variables/#blur for explanations
              blur_passes = "3"; # 0 disables blurring
              blur_size = "7";
              noise = "0.0117";
              contrast = "0.8916";
              brightness = "0.8172";
              vibrancy = "0.1696";
              vibrancy_darkness = "0.0";
            }
          ];
          input-field = [
            {
              size = "200, 50";
              position = "0, -80";
              monitor = cfg.primaryDisplay;
              dots_center = true;
              fade_on_empty = true;
              font_color = "rgb(202, 211, 245)";
              inner_color = "rgb(91, 96, 120)";
              outer_color = "rgb(24, 25, 38)";
              bothlock_color = -1;
              outline_thickness = 5;
              placeholder_text = ''<span foreground="##cad3f5">Password...</span>'';
              shadow_passes = 2;
            }
          ];
        };
      };
    };

    # Wayland configuration
    wayland.windowManager.hyprland = {
      enable = true;
      xwayland.enable = true;
      systemd = {
        enable = true;
        enableXdgAutostart = true;
      };

      plugins = with pkgs.hyprlandPlugins; [
        # hyprgrass
      ];

      settings =
        let
          useMonitorV2 = (lib.versionAtLeast pkgs.hyprland.version "0.40.0") && (cfg.monitorv2 != [ ]);
          names =
            if useMonitorV2 then
              map (m: m.name) cfg.monitorv2
            else
              [
                cfg.display1.input
                cfg.display2.input
              ];
          firstMonitor = builtins.elemAt names 0;
          secondMonitor = if builtins.length names > 1 then builtins.elemAt names 1 else firstMonitor;
        in
        {
          "$mod" = cfg.modKey;

          # Mouse
          # mouse_[up|down] - scroll wheel
          # middle_mouse - 274
          # thumb_up - 276
          # thumb_down - 275

          # l -> locked, will also work when an input inhibitor (e.g. a lockscreen) is active.
          # r -> release, will trigger on release of a key.
          # e -> repeat, will repeat when held.
          # n -> non-consuming, key/mouse events will be passed to the active window in addition to triggering the dispatcher.
          # m -> mouse, see below.
          # t -> transparent, cannot be shadowed by other binds.
          # i -> ignore mods, will ignore modifiers.
          # s -> separate, will arbitrarily combine keys between each mod/key, see [Keysym combos](#keysym-combos) above.
          # d -> has description, will allow you to write a description for your bind.
          # p -> bypasses the app's requests to inhibit keybinds.

          # https://wiki.hyprland.org/Configuring/Binds/
          # https://wiki.hyprland.org/Configuring/Binds/#mouse-buttons

          bind = [
            "$mod, Return, exec, ${cfg.defaultApps.terminal.pname}"
            "$mod, SPACE, exec, ${if cfg.launcher == "wofi" then "wofi --show drun" else "rofi -show drun"}"
            ", xf86Search, exec, ${if cfg.launcher == "wofi" then "wofi --show drun" else "rofi -show drun"}"
            "$mod, Q, killactive, "
            "$mod, M, exec, wlogout --protocol layer-shell"
            "$mod, E, exec, ${cfg.defaultApps.fileExplorer.pname}"
            "$mod, V, togglefloating, "
            "$mod, D, exec, ${drawer}"
            "$mod, P, pseudo, " # dwindle
            "$mod, S, togglesplit, " # dwindle
            "$mod SHIFT, Q, exec, hyprlock"
            "$mod SHIFT, 4, exec, hyprshot -m region --clipboard-only"
            "$mod, F, fullscreen, 1"
            "$mod SHIFT, F, fullscreen, 0"
            "$mod SHIFT, E, exec, smile"

            "$mod, mouse:276, movecurrentworkspacetomonitor, ${firstMonitor}"
            "$mod, mouse:275, movecurrentworkspacetomonitor, ${secondMonitor}"

            # alt-tab between workspaces on active monitor
            "$mod, Tab, workspace, m+1"
            "$mod SHIFT, Tab, workspace, m-1"

            "$mod, h, movefocus, l"
            "$mod, l, movefocus, r"
            "$mod, k, movefocus, u"
            "$mod, j, movefocus, d"

            "$mod, 1, workspace, 1"
            "$mod, 2, workspace, 2"
            "$mod, 3, workspace, 3"
            "$mod, 4, workspace, 4"
            "$mod, 5, workspace, 5"
            "$mod, 6, workspace, 6"
            "$mod, 7, workspace, 7"
            "$mod, 8, workspace, 8"
            "$mod, 9, workspace, 9"
            "$mod, 0, workspace, 10"

            "$mod ALT, 1, movetoworkspace, 1"
            "$mod ALT, 2, movetoworkspace, 2"
            "$mod ALT, 3, movetoworkspace, 3"
            "$mod ALT, 4, movetoworkspace, 4"
            "$mod ALT, 5, movetoworkspace, 5"
            "$mod ALT, 6, movetoworkspace, 6"
            "$mod ALT, 7, movetoworkspace, 7"
            "$mod ALT, 8, movetoworkspace, 8"
            "$mod ALT, 9, movetoworkspace, 9"
            "$mod ALT, 0, movetoworkspace, discord"

            "$mod CTRL, l, resizeactive, 10 0"
            "$mod CTRL, h, resizeactive, -10 0"
            "$mod CTRL, k, resizeactive, 0 -10"
            "$mod CTRL, j, resizeactive, 0 10"

            "$mod SHIFT, l, movewindow, r"
            "$mod SHIFT, h, movewindow, l"
            "$mod SHIFT, k, movewindow, u"
            "$mod SHIFT, j, movewindow, d"

            "$mod, b, exec, ${cfg.defaultApps.browser.pname}"
          ]
          ++ cfg.keybinds.bind;

          bindm = [
            # Move/resize windows with mod + LMB/RMB and dragging
            "$mod, mouse:272, movewindow"
            "$mod, mouse:273, resizewindow"
            # middle mouse will grab a window, mod + middle mouse will close it
            "$mod SHIFT, mouse:274, movewindow"
          ]
          ++ cfg.keybinds.bindm;

          bindel = [
            ", XF86AudioRaiseVolume, exec, wpctl set-volume -l 1.5 @DEFAULT_AUDIO_SINK@ 5%+"
            ", XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"
          ]
          ++ cfg.keybinds.bindel;

          bindl = [
            ", XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"
            ", XF86AudioPlay, exec, playerctl play-pause"
            ", XF86AudioPrev, exec, playerctl previous"
            ", XF86AudioNext, exec, playerctl next"
            ", XF86AudioMicMute, exec, wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"

            ", XF86MonBrightnessUp, exec, brightnessctl set +5%"
            ", XF86MonBrightnessDown, exec, brightnessctl set 5%-"

            "$mod, XF86MonBrightnessUp, exec, brightnessctl -d kbd_backlight set +10%"
            "$mod, XF86MonBrightnessDown, exec, brightnessctl -d kbd_backlight set 10%-"
          ]
          ++ cfg.keybinds.bindl;

          monitor =
            cfg.monitor
            ++ map (
              m:
              if m.disabled then
                "${m.name}, disable"
              else if m.mirrorOf != null then
                "${m.name}, mirror, ${m.mirrorOf}"
              else
                let
                  mode = if m.mode == null then "preferred" else m.mode;
                  position = if m.position == null then "0x0" else m.position;
                  scale = if m.scale == null then "1" else (toString m.scale);
                  parts = [
                    m.name
                    mode
                    position
                    scale
                  ];
                  # Append transform only when set, as "transform, <value>"
                  transformTokens =
                    if m.transform == null then
                      [ ]
                    else
                      [
                        "transform"
                        m.transform
                      ];
                  tokens = parts ++ transformTokens ++ m.extra;
                in
                builtins.concatStringsSep ", " tokens
            ) cfg.monitorv2;

          render = {
            cm_fs_passthrough = 1;
          };

          misc = {
            vrr = if cfg.enableVRR then 1 else 0;
            force_default_wallpaper = 0;
          };

          general = {
            gaps_in = 5;
            gaps_out = 10;
            border_size = 1;
            "col.active_border" = "rgb(8aadf4) rgb(24273A) rgb(24273A) rgb(8aadf4) 45deg";
            "col.inactive_border" = "rgb(24273A) rgb(24273A) rgb(24273A) rgb(24273A) 45deg";
            layout = "dwindle";
            allow_tearing = cfg.allowTearing;
          };

          decoration = {
            rounding = 10;
            blur = {
              enabled = true;
              size = 2;
              passes = 2;
              new_optimizations = true;
              xray = false;
            };
          };

          animations = {
            enabled = "yes";
            bezier = [
              "overshot, 0.05, 0.9, 0.1, 1.05"
              "smoothOut, 0.36, 0, 0.66, -0.56"
              "smoothIn, 0.25, 1, 0.5, 1"
            ];
            animation = [
              "windows, 1, 5, overshot, slide"
              "windowsOut, 1, 4, smoothOut, slide"
              "windowsMove, 1, 4, default"
              "border, 1, 10, default"
              "fade, 1, 10, smoothIn"
              "fadeDim, 1, 10, smoothIn"
              "workspaces, 1, 6, default"
            ];
          };

          dwindle = {
            pseudotile = "yes";
            preserve_split = "yes";
          };

          workspace = cfg.workspace;

          windowrule = [
            "float, title:(file_progress)"
            "float, title:(.*[Cc]onfirm.*)"
            "float, title:(.*[Dd]ialog.*)"
            "float, title:(.*[Dd]ownload.*)"
            "float, title:(.*[Nn]otification.*)"
            "float, title:(.*[Ee]rror.*)"
            "float, title:(.*[Ss]plash.*)"
            "float, title:(.*[Cc]onfirmreset.*)"
            "float, title:(.*[Ss]ign [Ii]n - .*)"
            "float, title:(.*[Oo]pen [Ff]ile.*)"
            "float, title:(.*branchdialog.*)"
            "float, class:(.*pavucontrol.*)"
            "move onscreen cursor 0% 0%, class:(.*pavucontrol.*)"
            "float, class:(.*[Oo]verskride.*)"
            "float, class:(.*FileRoller.*)"
            "float, class:(.*wlogout.*)"
            "idleinhibit stayfocused, title:(.*mpv.*)"

            "float, class:(.*nm-connection-editor.*)"
            "move onscreen cursor 0% 0%, class:(.*nm-connection-editor.*)"

            "float, title:(Media viewer)"
            "float, class:(it.mijorus.smile),title:(Smile)"
            "float, class:(.blueman-manager-wrapped)$,title:(Bluetooth Devices)"
            # Picture in picture windows
            "float, title:(.*Picture-in-Picture.*)"
            "pin, title::(.*Picture-in-Picture.*)"

            # discord/vesktop
            # "workspace: name:discord, class:(.*vesktop)"
            # "float, class:(.*vesktop),title:(.*Discord Popout.*)"
            # "pin, class:(.*vesktop),title:(.*Discord Popout.*)"

            # Music
            # "workspace: name:discord, class:(Apple Music.*)"

            # Steam
            "float, class:(.*[Ss]team), title:(.*[Ss]team.*)$"
            "workspace name:steam silent, class:(.*[Ss]team), title:(.*[Ss]team.*)$"
            "tile, class:(.*[Ss]team), title:(.*[Ss]team.*)$"
            "float, class:(.*steam),title:(.*Friends List.*)"

            # Code
            "pin, class:(.*codium.*),title:(Save As)"
            "float, class:(.*codium.*),title:(Save As)"
            "float, class:(xdg-desktop-portal-gtk),title:(Open Workspace from File)"

            # Game Tearing??? https://wiki.hypr.land/Configuring/Tearing/
            "immediate, class:(.*gamescope)"

            # vmware
            # this tag will set the below options to the vdi window
            # this will have it auto open as a 2160x7680 window
            # and makes multi-monitor work
            "tag +horizonrdp, class:(.*[Hh]orizon-client),title:(USPS Next VDI)"

            "noanim, tag:horizonrdp"
            "noblur, tag:horizonrdp"
            "norounding, tag:horizonrdp"
            "noshadow, tag:horizonrdp"
            "immediate, tag:horizonrdp"
            "allowsinput, tag:horizonrdp"
            "noborder, tag:horizonrdp"
            "nodim, tag:horizonrdp"
            "nomaxsize, tag:horizonrdp"
            "renderunfocused, tag:horizonrdp"
            "idleinhibit, tag:horizonrdp"
            "float, tag:horizonrdp"
            # float the vmware window cause its annoying to use in fullscreen
            "float, class:(.*[Hh]orizon-client),title:([Oo]mnissa [Hh]orizon [Cc]lient)"

            "tag +waydroid, class:([Ww]aydroid.*)"
            "float, tag:waydroid"
            "pin, tag:waydroid"
          ]
          ++ cfg.windowRule;

          plugin = {
            # touch_gestures = {
            #   # The default sensitivity is probably too low on tablet screens,
            #   # I recommend turning it up to 4.0
            #   sensitivity = "4.0";

            #   # must be >= 3
            #   workspace_swipe_fingers = "3";

            #   # switching workspaces by swiping from an edge, this is separate from workspace_swipe_fingers
            #   # and can be used at the same time
            #   # possible values: l, r, u, or d
            #   # to disable it set it to anything else
            #   workspace_swipe_edge = "d";

            #   # in milliseconds
            #   long_press_delay = "400";

            #   # resize windows by long-pressing on window borders and gaps.
            #   # If general:resize_on_border is enabled, general:extend_border_grab_area is used for floating
            #   # windows
            #   resize_on_border_long_press = true;

            #   # in pixels, the distance from the edge that is considered an edge
            #   edge_margin = "10";

            #   # emulates touchpad swipes when swiping in a direction that does not trigger workspace swipe.
            #   # ONLY triggers when finger count is equal to workspace_swipe_fingers
            #   #
            #   # might be removed in the future in favor of event hooks
            #   emulate_touchpad_swipe = false;

            #   experimental = {
            #     # send proper cancel events to windows instead of hacky touch_up events,
            #     # NOT recommended as it crashed a few times, once it's stabilized I'll make it the default
            #     send_cancel = "0";
            #   };

            # hyprgrass-bind = [
            #   # swipe left from right edge
            #   ", edge:r:l, workspace, +1"

            #   # swipe up from bottom edge
            #   ", edge:d:u, exec, ${cfg.defaultApps.browser.pname}"

            #   # swipe down from left edge
            #   ", edge:l:d, exec, pactl set-sink-volume @DEFAULT_SINK@ -4%"

            #   # swipe down with 4 fingers
            #   ", swipe:4:d, killactive"

            #   # swipe diagonally left and down with 3 fingers
            #   # l (or r) must come before d and u
            #   ", swipe:3:ld, exec, foot"

            #   # tap with 3 fingers
            #   ", tap:3, exec, foot"

            #   # longpress can trigger mouse binds:
            #   ", longpress:2, movewindow"
            #   ", longpress:3, resizewindow"
            # ];
            # };
          };

          gesture = [
            "3, horizontal, scale: 0.75, workspace" # swipe 3 fingers to change workspace
            "3, pinch, mod: SUPER, resize"
            "4, pinch, fullscreen"
          ];

          input = {
            kb_layout = "us";

            kb_variant = "";
            kb_model = "";
            kb_options = "";
            kb_rules = "";

            numlock_by_default = true;

            follow_mouse = 1;

            touchpad = {
              clickfinger_behavior = 1;
              natural_scroll = "yes";
            };

            sensitivity = 0; # -1.0 - 1.0, 0 means no modification.
          };

          experimental = {
            xx_color_management_v4 = true;
          };

          debug = {
            full_cm_proto = cfg.debug.fullCmProto;
            disable_logs = cfg.debug.disableLogs;
            disable_scale_checks = cfg.debug.disableScaleChecks;
          };
        };

      extraConfig =
        let
          autostarts = builtins.concatStringsSep "\n" (map (cmd: "exec-once = ${cmd}") cfg.autostartCommands);
        in
        ''
          exec-once = dbus-update-activation-environment --systemd --all
          exec-once = systemctl --user import-environment WAYLAND_DISPLAY XDG_CURRENT_DESKTOP
          exec-once = xhost +SI:localuser:root
        ''
        + autostarts
        + "\n"
        + (cfg.extraConfig or "");
    };
  };
}
