{
  config,
  lib,
  pkgs,
  namespace,
  ...
}:
let
  cfg = config.${namespace}.hardware.raspberry-pi;
  dt_ao_overlay = _final: prev: {
    deviceTree = prev.deviceTree // {
      applyOverlays = _final.callPackage ./apply-overlays-dtmerge.nix { };
    };
  };

  # installs raspberry's firmware independent of the nixos generations
  # sometimes referred to as "boot code"
  raspberryPiFirmware = (
    {
      pkgs,
      firmware,
      configTxt,
    }:
    pkgs.replaceVarsWith {
      src = ./generational/install-firmware.sh;
      isExecutable = true;

      replacements = {
        inherit (pkgs) bash;
        path = pkgs.lib.makeBinPath [
          pkgs.coreutils
        ];

        inherit firmware configTxt;
      };
    }
  );

  kernelbootGenBuilder = (
    {
      pkgs,
      deviceTreeInstaller,
    }:
    pkgs.replaceVarsWith {
      src = ./generational/kernelboot-gen-builder.sh;
      isExecutable = true;

      replacements = {
        inherit (pkgs) bash;
        path = pkgs.lib.makeBinPath [
          pkgs.coreutils
        ];

        installDeviceTree = deviceTreeInstaller;
      };
    }
  );

  deviceTree = (
    {
      pkgs,
      firmware,
    }:
    pkgs.replaceVarsWith {
      src = ./generational/install-device-tree.sh;
      isExecutable = true;

      replacements = {
        inherit (pkgs) bash;
        path = pkgs.lib.makeBinPath [
          pkgs.coreutils
        ];

        inherit firmware;
      };
    }
  );

  mkBootloader =
    pkgs:
    bootloader {
      inherit pkgs;
      inherit (cfg) nixosGenerationsDir;

      firmwareInstaller = "${raspberryPiFirmware {
        inherit pkgs;
        firmware = pkgs.${namespace}.raspberrypifw;
        configTxt = pkgs.writeTextFile {
          name = "config.txt";
          text = ''
            # Do not edit!
            # This configuration file is generated from NixOS configuration
            # options `hardware.raspberry-pi.config`.
            # Any manual changes will be overwritten on the next configuration
            # switch.
            ${config.${namespace}.hardware.raspberry-pi.config-generated}
          '';
        };
      }}";

      nixosGenBuilder = "${kernelbootGenBuilder {
        inherit pkgs;
        deviceTreeInstaller =
          let
            cmd = deviceTree {
              inherit pkgs;
              firmware = cfg.firmwarePackage;
            };
            args = lib.optionalString (!cfg.useGenerationDeviceTree) " -r";
          in
          "${cmd} ${args}";
      }}";

    };

  bootloader = (
    {
      pkgs,
      nixosGenerationsDir,
      firmwareInstaller,
      nixosGenBuilder,
    }:
    pkgs.replaceVarsWith {
      src = ./generational/nixos-generations-builder.sh;
      isExecutable = true;

      replacements = {
        inherit (pkgs) bash;
        path = pkgs.lib.makeBinPath [
          pkgs.coreutils
          pkgs.gnused
        ];

        # NixOS-generations -independent
        installFirmwareBuilder = firmwareInstaller;
        # NixOS-generations -dependent
        inherit nixosGenerationsDir nixosGenBuilder;
      };
    }
  );

  # Builders used to write during system activation

  ubootBuilder = import ./uboot-builder.nix {
    inherit pkgs;
    ubootPackage = (
      if (cfg.variant == "5") then pkgs.${namespace}.uboot-pi5 else pkgs.${namespace}.uboot-pi4
    );
    firmwareBuilder = firmwarePopulateCmd;
    extlinuxConfBuilder = config.boot.loader.generic-extlinux-compatible.populateCmd;
  };

  uefiBuilder = import ./uefi-builder.nix {
    inherit pkgs;
    uefiPackage = (
      if (cfg.variant == "5") then
        pkgs.${namespace}.uefi-rpi5
      else
        pkgs.${namespace}.edk2.override { MODEL = "4"; }
    );
    firmwareBuilder = firmwarePopulateCmd;
  };

  # Builders exposed via populateCmd, which run on the build architecture
  populateFirmwareBuilder = import ./firmware-builder.nix {
    pkgs = pkgs.buildPackages;
    configTxt = pkgs.writeTextFile {
      name = "config.txt";
      text = ''
        # Do not edit!
        # This configuration file is generated from NixOS configuration
        # options `hardware.raspberry-pi.config`.
        # Any manual changes will be overwritten on the next configuration
        # switch.
        ${config.${namespace}.hardware.raspberry-pi.config-generated}
      '';
    };
    firmware = pkgs.${namespace}.raspberrypifw;
  };

  firmwarePopulateCmd = "${populateFirmwareBuilder} ${firmwareBuilderArgs}";
  firmwareBuilderArgs = lib.optionalString (!true) " -r";

  # these will receive the top-level path as an argument when invoked as
  # system.build.installBootloader
  builder = {
    # system.build.installBootLoader
    uboot = "${ubootBuilder} -f /boot/firmware -b /boot -c";
    uefi = "${uefiBuilder} -f /boot/firmware -b /boot -c";
    kernel = builtins.concatStringsSep " " [
      "${mkBootloader pkgs}"
      "-g ${toString 10}"
      "-f /boot/firmware"
      "-c"
    ];
  };

  # firmware: caller must provide `-c <nixos configuration>` and  `-f <firmware target path>`
  # boot: caller must provide `-c <nixos configuration>` and `-b <boot-dir>`
in
{
  options.${namespace}.hardware.raspberry-pi = {
    enable = lib.mkEnableOption "Raspberry Pi common configuration";

    variant = lib.mkOption {
      type = lib.types.enum [
        "4"
        "5"
      ];
      description = "Raspberry Pi variant (4 or 5)";
    };

    bootType = lib.mkOption {
      type = lib.types.enum [
        "uefi"
        "uboot"
        "kernel"
      ];
      default = "uefi";
    };

    apply-overlays-dtmerge = {
      enable = lib.mkEnableOption "" // {
        description = ''
          Whether replace deviceTree.applyOverlays implementation to use dtmerge from libraspberrypi.
          This can resolve issues with applying dtbs for the pi.
        '';
      };
    };
  };

  imports = [
    ./audio.nix
    ./bluetooth.nix
    ./config.nix
    ./i2c.nix
    ./leds.nix
    ./modesetting.nix
    ./pwm.nix
    ./wifi.nix
  ];

  config = lib.mkIf cfg.enable {

    boot = {
      initrd.availableKernelModules = [
        "usbhid"
        "usb-storage"
      ]
      ++ (
        if (cfg.variant == "5") then
          [
            "nvme"
          ]
        else
          [
            "vc4"
            "pcie-brcmstb" # required for the pcie bus to work
            "reset-raspberrypi" # required for vl805 firmware to load
          ]
      );
      loader = {
        # kernelFile = pkgs.stdenv.hostPlatform.linux-kernel.target;
        generic-extlinux-compatible = {
          enable = lib.mkDefault (if cfg.bootType == "uefi" then false else true);
          useGenerationDeviceTree = lib.mkOverride 60 (if cfg.bootType == "uefi" then false else true);
        };
        systemd-boot.enable = (if cfg.bootType == "uefi" then true else false);
        systemd-boot.extraInstallCommands =
          let
            bootloaderInstaller = (builder."${cfg.bootType}");
          in
          ''
            ${bootloaderInstaller} -f /boot/firmware -b /boot -c
          '';
        grub.enable = lib.mkForce false;
      };
    };

    # Common Raspberry Pi packages
    environment.systemPackages = with pkgs; [
      dconf
      i2c-tools
      raspberrypi-eeprom
      pkgs.${namespace}.raspberrypi-utils
      pkgs.${namespace}.raspberrypifw
      pkgs.${namespace}.raspberryPiWirelessFirmware
      raspberrypi-armstubs
      erofs-utils
      fex
      squashfuse
      squashfsTools
    ];

    # Common Bluetooth configuration
    systemd = {
      services.btattach = {
        before = [ "bluetooth.service" ];
        after = [ "dev-ttyAMA0.device" ];
        wantedBy = [ "multi-user.target" ];
        serviceConfig = {
          ExecStart = "${lib.getExe' pkgs.bluez "btattach"} -B /dev/ttyAMA0 -P bcm -S 3000000";
        };
      };

      tmpfiles.packages = [
        pkgs.${namespace}.udev-rules
      ];
    };

    ${namespace}.hardware.raspberry-pi = {
      config = {
        all = {
          options = {
            os_prefix = lib.mkIf (cfg.bootType == "kernel") {
              enable = true;
              value = "${cfg.nixosGenerationsDir}/default/"; # "nixos/<generation-name>/"
            };
            kernel = lib.mkIf (cfg.bootType == "kernel" || cfg.bootType == "uboot") {
              enable = true;
              value = (
                if cfg.bootType == "uboot" then
                  "u-boot.bin"
                else if cfg.bootType == "kernel" then
                  "kernel.img"
                else
                  ""
              );
            };
          };
        };
      };
      extra-config =
        let
          # https://www.raspberrypi.com/documentation/computers/config_txt.html#initramfs
          ramfsfile = "initrd";
          ramfsaddr = "followkernel"; # same as 0 = "after the kernel image"
        in
        ''
          [all]
          initramfs ${ramfsfile} ${ramfsaddr}
        '';
    };

    # Common hardware settings
    hardware = {
      deviceTree = {
        enable = true;
        package = config.boot.kernelPackages.kernel;
        name = "bcm2712-rpi5-b.dtb";
        filter = lib.mkDefault (if (cfg.variant == "5") then "bcm2712-rpi5-*.dtb" else "bcm2711-rpi4-*.dtb");
        overlays = (
          if (cfg.variant == "4") then
            [
              {
                name = "rpi4-cpu-revision";
                dtsText = ''
                  /dts-v1/;
                  /plugin/;

                  / {
                    compatible = "brcm,bcm2711";

                    fragment@0 {
                      target-path = "/";
                      __overlay__ {
                        system {
                          linux,revision = <0x00d03114>;
                        };
                      };
                    };
                  };
                '';
              }
            ]
          else
            [
              # {
              #   name = "bcm2712d0-overlay";
              #   dtsFile = "${pkgs.${namespace}.raspberrypi-overlays}/dtbs/raspberrypi-overlays/bcm2712d0-overlay.dts";
              # }
            ]
        );
      };
      firmware = [ pkgs.${namespace}.raspberryPiWirelessFirmware ];
      graphics.enable32Bit = lib.mkForce false;
      i2c.enable = lib.mkDefault true;
    };

    system = {
      #build.installBootLoader = lib.mkOverride 60 (if cfg.bootType == "uefi" then (builder."uefi") else (builder."uboot")); # todo
      #boot = {
      #  loader = {
      #    id = lib.mkOverride 60 (if cfg.bootType == "uefi" then "raspberrypi-uefi" else "raspberrypi-uboot"); # todo
      #    kernelFile = pkgs.stdenv.hostPlatform.linux-kernel.target;
      #  };
      #};
      # Pi specific system tags
      nixos.tags = [
        "raspberry-pi-${cfg.variant}"
        # config.boot.loader.raspberry-pi.bootloader
        config.boot.kernelPackages.kernel.version
      ];
    };

    # Common programs
    programs.kdeconnect.enable = lib.mkDefault false;

    # Root user shell configuration
    users = {
      users.root.shell = pkgs.zsh;
      extraGroups = {
        gpio = { };
        i2c = { };
        input = { };
        plugdev = { };
        spi = { };
        video = { };
      };
    };

    services = {
      udev.packages = [
        pkgs.${namespace}.udev-rules
      ];
      xserver.extraConfig =
        let
          identifier = "rp1";
          driver = "rp1-vec|rp1-dsi|rp1-dpi";
        in
        ''
          Section "OutputClass"
            Identifier "${identifier}"
            MatchDriver "${driver}"
            Driver "modesetting"
            Option "PrimaryGPU" "true"
          EndSection
        '';
    };

    nixpkgs.overlays =
      [ ]
      ++ (
        if cfg.variant == "5" then
          [
            (_final: prev: {
              # https://github.com/nvmd/nixos-raspberrypi/issues/64
              # credit for the initial version of this snippet goes to @micahcc
              jemalloc = prev.jemalloc.overrideAttrs (old: {
                # --with-lg-page=(log2 page_size)
                # RPi5 (bcm2712): since our page size is 16384 (2**14), we need 14
                configureFlags =
                  let
                    pageSizeFlag = "--with-lg-page";
                  in
                  (prev.lib.filter (flag: prev.lib.hasPrefix pageSizeFlag flag == false) old.configureFlags)
                  ++ [ "${pageSizeFlag}=14" ];
              });
            })
          ]
        else
          [ ]
      )
      ++ (if cfg.apply-overlays-dtmerge.enable then [ dt_ao_overlay ] else [ ]);
  };
}
