{
  config,
  lib,
  pkgs,
  ...
}:
let
  cfg = config.hardware.deviceTree;

  overlayType = lib.types.submodule {
    options = {
      name = lib.mkOption {
        type = lib.types.str;
        description = ''
          Name of this overlay
        '';
      };

      filter = lib.mkOption {
        type = lib.types.nullOr lib.types.str;
        default = null;
        example = "*rpi*.dtb";
        description = ''
          Only apply to .dtb files matching glob expression.
        '';
      };

      dtsFile = lib.mkOption {
        type = lib.types.nullOr lib.types.path;
        description = ''
          Path to .dts overlay file, overlay is applied to
          each .dtb file matching "compatible" of the overlay.
        '';
        default = null;
        example = lib.literalExpression "./dts/overlays.dts";
      };

      dtsText = lib.mkOption {
        type = lib.types.nullOr lib.types.str;
        default = null;
        description = ''
          Literal DTS contents, overlay is applied to
          each .dtb file matching "compatible" of the overlay.
        '';
        example = ''
          /dts-v1/;
          /plugin/;
          / {
                  compatible = "raspberrypi";
          };
          &{/soc} {
                  pps {
                          compatible = "pps-gpio";
                          status = "okay";
                  };
          };
        '';
      };

      dtboFile = lib.mkOption {
        type = lib.types.nullOr lib.types.path;
        default = null;
        description = ''
          Path to .dtbo compiled overlay file.
        '';
      };
    };
  };

  filterDTBs =
    src:
    if cfg.filter == null then
      src
    else
      pkgs.runCommand "dtbs-filtered" { } ''
        mkdir -p $out
        cd ${src}
        find . -type f -name '${cfg.filter}' -print0 \
          | xargs -0 cp -v --no-preserve=mode --target-directory $out --parents
      '';

  filteredDTBs = filterDTBs cfg.dtbSource;

  # Fill in `dtboFile` for each overlay if not set already.
  # Existence of one of these is guarded by assertion below
  withDTBOs =
    xs:
    lib.flip map xs (
      o:
      o
      // {
        dtboFile =
          let
            includePaths = [
              "${lib.getDev cfg.kernelPackage}/lib/modules/${cfg.kernelPackage.modDirVersion}/source/scripts/dtc/include-prefixes"
            ]
            ++ cfg.dtboBuildExtraIncludePaths;
            extraPreprocessorFlags = cfg.dtboBuildExtraPreprocessorFlags;
          in
          if o.dtboFile == null then
            let
              dtsFile = if o.dtsFile == null then (pkgs.writeText "dts" o.dtsText) else o.dtsFile;
            in
            pkgs.deviceTree.compileDTS {
              name = "${o.name}-dtbo";
              inherit includePaths extraPreprocessorFlags dtsFile;
            }
          else
            o.dtboFile;
      }
    );

in
{
  imports = [
    (lib.mkRemovedOptionModule [
      "hardware"
      "deviceTree"
      "base"
    ] "Use hardware.deviceTree.kernelPackage instead")
  ];

  options = {
    hardware.deviceTree = {
      enable = lib.mkOption {
        default = pkgs.stdenv.hostPlatform.linux-kernel.DTB or false;
        type = lib.types.bool;
        description = ''
          Build device tree files. These are used to describe the
          non-discoverable hardware of a system.
        '';
      };

      kernelPackage = lib.mkOption {
        default = config.boot.kernelPackages.kernel;
        defaultText = lib.literalExpression "config.boot.kernelPackages.kernel";
        example = lib.literalExpression "pkgs.linux_latest";
        type = lib.types.path;
        description = ''
          Kernel package where device tree include directory is from. Also used as default source of dtb package to apply overlays to
        '';
      };

      dtboBuildExtraPreprocessorFlags = lib.mkOption {
        default = [ ];
        example = lib.literalExpression "[ \"-DMY_DTB_DEFINE\" ]";
        type = lib.types.listOf lib.types.str;
        description = ''
          Additional flags to pass to the preprocessor during dtbo compilations
        '';
      };

      dtboBuildExtraIncludePaths = lib.mkOption {
        default = [ ];
        example = lib.literalExpression ''
          [
            ./my_custom_include_dir_1
            ./custom_include_dir_2
          ]
        '';
        type = lib.types.listOf lib.types.path;
        description = ''
          Additional include paths that will be passed to the preprocessor when creating the final .dts to compile into .dtbo
        '';
      };

      dtbSource = lib.mkOption {
        default = "${cfg.kernelPackage}/dtbs";
        defaultText = lib.literalExpression "\${cfg.kernelPackage}/dtbs";
        type = lib.types.path;
        description = ''
          Path to dtb directory that overlays and other processing will be applied to. Uses
          device trees bundled with the Linux kernel by default.
        '';
      };

      name = lib.mkOption {
        default = null;
        example = "some-dtb.dtb";
        type = lib.types.nullOr lib.types.str;
        description = ''
          The name of an explicit dtb to be loaded, relative to the dtb base.
          Useful in extlinux scenarios if the bootloader doesn't pick the
          right .dtb file from FDTDIR.
        '';
      };

      filter = lib.mkOption {
        type = lib.types.nullOr lib.types.str;
        default = null;
        example = "*rpi*.dtb";
        description = ''
          Only include .dtb files matching glob expression.
        '';
      };

      overlays = lib.mkOption {
        default = [ ];
        example = lib.literalExpression ''
          [
            { name = "pps"; dtsFile = ./dts/pps.dts; }
            { name = "spi";
              dtsText = "...";
            }
            { name = "precompiled"; dtboFile = ./dtbos/example.dtbo; }
          ]
        '';
        type = lib.types.listOf (
          lib.types.coercedTo lib.types.path (path: {
            name = baseNameOf path;
            filter = null;
            dtboFile = path;
          }) overlayType
        );
        description = ''
          List of overlays to apply to base device-tree (.dtb) files.
        '';
      };

      package = lib.mkOption {
        default = null;
        type = lib.types.nullOr lib.types.path;
        internal = true;
        description = ''
          A path containing the result of applying `overlays` to `kernelPackage`.
        '';
      };
    };
  };

  config = lib.mkIf (cfg.enable) {

    assertions =
      let
        invalidOverlay = o: (o.dtsFile == null) && (o.dtsText == null) && (o.dtboFile == null);
      in
      lib.singleton {
        assertion = lib.all (o: !invalidOverlay o) cfg.overlays;
        message = ''
          deviceTree overlay needs one of dtsFile, dtsText or dtboFile set.
          Offending overlay(s):
          ${toString (map (o: o.name) (builtins.filter invalidOverlay cfg.overlays))}
        '';
      };

    hardware.deviceTree.package =
      if (cfg.overlays != [ ]) then
        pkgs.deviceTree.applyOverlays filteredDTBs (withDTBOs cfg.overlays)
      else
        filteredDTBs;
  };
}
