{ lib
, bison
, buildPackages
, directx-headers
, elfutils
, expat
, fetchCrate
, fetchurl
, fetchpatch
, file
, flex
, glslang
, intltool
, jdupes
, libdrm
, libglvnd
, libomxil-bellagio
, libunwind
, libva-minimal
, libvdpau
, llvmPackages
, lm_sensors
, meson
, ninja
, pkg-config
, python3Packages
, rust-bindgen
, rust-cbindgen
, rustPlatform
, rustc
, spirv-llvm-translator
, stdenv
, udev
, valgrind-light
, vulkan-loader
, wayland
, wayland-protocols
, wayland-scanner
, xcbutilkeysyms
, xorg
, zstd
, enablePatentEncumberedCodecs ? true

, galliumDrivers ? [
    "d3d12" # WSL emulated GPU (aka Dozen)
    "iris" # new Intel (Broadwell+)
    "kmsro" # special "render only" driver for GPUs without a display controller
    "nouveau" # Nvidia
    "radeonsi" # new AMD (GCN+)
    "r300" # very old AMD
    "r600" # less old AMD
    "swrast" # software renderer (aka LLVMPipe)
    "svga" # VMWare virtualized GPU
    "virgl" # QEMU virtualized GPU (aka VirGL)
    "zink" # generic OpenGL over Vulkan, experimental
  ] ++ lib.optionals (stdenv.isAarch64 || stdenv.isAarch32) [
    "etnaviv" # Vivante GPU designs (mostly NXP/Marvell SoCs)
    "freedreno" # Qualcomm Adreno (all Qualcomm SoCs)
    "lima" # ARM Mali 4xx
    "panfrost" # ARM Mali Midgard and up (T/G series)
    "vc4" # Broadcom VC4 (Raspberry Pi 0-3)
  ] ++ lib.optionals stdenv.isAarch64 [
    "tegra" # Nvidia Tegra SoCs
    "v3d" # Broadcom VC5 (Raspberry Pi 4)
  ] ++ lib.optionals stdenv.hostPlatform.isx86 [
    "crocus" # Intel legacy, x86 only
    "i915" # Intel extra legacy, x86 only
  ]
, vulkanDrivers ? [
    "amd" # AMD (aka RADV)
    "intel" # new Intel (aka ANV)
    "microsoft-experimental" # WSL virtualized GPU (aka DZN/Dozen)
    "nouveau" # Nouveau (aka NVK)
    "swrast" # software renderer (aka Lavapipe)
  ] ++ lib.optionals (stdenv.hostPlatform.isAarch -> lib.versionAtLeast stdenv.hostPlatform.parsed.cpu.version "6") [
    # QEMU virtualized GPU (aka VirGL)
    # Requires ATOMIC_INT_LOCK_FREE == 2.
    "virtio"
  ] ++ lib.optionals stdenv.isAarch64 [
    "broadcom" # Broadcom VC5 (Raspberry Pi 4, aka V3D)
    "freedreno" # Qualcomm Adreno (all Qualcomm SoCs)
    "imagination-experimental" # PowerVR Rogue (currently N/A)
    "panfrost" # ARM Mali Midgard and up (T/G series)
  ] ++ lib.optionals stdenv.hostPlatform.isx86 [
    "intel_hasvk" # Intel Haswell/Broadwell, "legacy" Vulkan driver (https://www.phoronix.com/news/Intel-HasVK-Drop-Dead-Code)
  ]
, eglPlatforms ? [ "x11" "wayland" ]
, vulkanLayers ? [
    "device-select"
    "overlay"
    "intel-nullhw"
  ]
, mesa
, makeSetupHook
}:

let
  rustDeps = [
    {
      pname = "paste";
      version = "1.0.14";
      hash = "sha256-+J1h7New5MEclUBvwDQtTYJCHKKqAEOeQkuKy+g0vEc=";
    }
    {
      pname = "proc-macro2";
      version = "1.0.70";
      hash = "sha256-e4ZgyZUTu5nAtaH5QVkLelqJQX/XPj/rWkzf/g2c+1g=";
    }
    {
      pname = "quote";
      version = "1.0.33";
      hash = "sha256-VWRCZJO0/DJbNu0/V9TLaqlwMot65YjInWT9VWg57DY=";
    }
    {
      pname = "syn";
      version = "2.0.39";
      hash = "sha256-Mjen2L/omhVbhU/+Ao65mogs3BP3fY+Bodab3uU63EI=";
    }
    {
      pname = "unicode-ident";
      version = "1.0.12";
      hash = "sha256-KX8NqYYw6+rGsoR9mdZx8eT1HIPEUUyxErdk2H/Rlj8=";
    }
  ];

  copyRustDep = dep: ''
    cp -R --no-preserve=mode,ownership ${fetchCrate dep} subprojects/${dep.pname}-${dep.version}
    cp -R subprojects/packagefiles/${dep.pname}/* subprojects/${dep.pname}-${dep.version}/
  '';

  copyRustDeps = lib.concatStringsSep "\n" (builtins.map copyRustDep rustDeps);

  needNativeCLC = !stdenv.buildPlatform.canExecute stdenv.hostPlatform;

  common = import ./common.nix { inherit lib fetchurl; };
in stdenv.mkDerivation {
  inherit (common) pname version src meta;

  patches = [
    ./opencl.patch

    # https://gitlab.freedesktop.org/mesa/mesa/-/issues/11533
    (fetchpatch {
      name = "ffmpeg.patch";
      url = "https://gitlab.freedesktop.org/mesa/mesa/-/commit/241f70e5a13bb9c13a168282446ad074e16c3d74.patch";
      hash = "sha256-Cx7OL8iXGAOuDbCQReCCxSrWYvfZVrGoP0txIKSLTvs=";
    })
  ];

  postPatch = ''
    patchShebangs .

    # The drirc.d directory cannot be installed to $drivers as that would cause a cyclic dependency:
    substituteInPlace src/util/xmlconfig.c --replace \
      'DATADIR "/drirc.d"' '"${placeholder "out"}/share/drirc.d"'
    substituteInPlace src/util/meson.build --replace \
      "get_option('datadir')" "'${placeholder "out"}/share'"
    substituteInPlace src/amd/vulkan/meson.build --replace \
      "get_option('datadir')" "'${placeholder "out"}/share'"

    ${copyRustDeps}
  '';

  outputs = [
    "out" "dev" "drivers" "driversdev" "opencl" "teflon" "osmesa"
    # the Dozen drivers depend on libspirv2dxil, but link it statically, and
    # libspirv2dxil itself is pretty chonky, so relocate it to its own output in
    # case anything wants to use it at some point
    "spirv2dxil"
  ];

  # Keep build-ids so drivers can use them for caching, etc.
  # Also some drivers segfault without this.
  separateDebugInfo = true;

  # Needed to discover llvm-config for cross
  preConfigure = ''
    PATH=${llvmPackages.libllvm.dev}/bin:$PATH
  '';

  mesonFlags = [
    "--sysconfdir=/etc"
    "--datadir=${placeholder "drivers"}/share"

    # What to build
    (lib.mesonOption "platforms" (lib.concatStringsSep "," eglPlatforms))
    (lib.mesonOption "gallium-drivers" (lib.concatStringsSep "," galliumDrivers))
    (lib.mesonOption "vulkan-drivers" (lib.concatStringsSep "," vulkanDrivers))
    (lib.mesonOption "vulkan-layers" (builtins.concatStringsSep "," vulkanLayers))

    # Make sure we know where to find all the drivers
    (lib.mesonOption "dri-drivers-path" "${placeholder "drivers"}/lib/dri")
    (lib.mesonOption "vdpau-libs-path" "${placeholder "drivers"}/lib/vdpau")
    (lib.mesonOption "omx-libs-path" "${placeholder "drivers"}/lib/bellagio")
    (lib.mesonOption "va-libs-path" "${placeholder "drivers"}/lib/dri")
    (lib.mesonOption "d3d-drivers-path" "${placeholder "drivers"}/lib/d3d")

    # Set search paths for non-Mesa drivers (e.g. Nvidia)
    (lib.mesonOption "dri-search-path" "${libglvnd.driverLink}/lib/dri")
    (lib.mesonOption "gbm-backends-path" "${libglvnd.driverLink}/lib/gbm:${placeholder "out"}/lib/gbm")

    # Enable glvnd for dynamic libGL dispatch
    (lib.mesonEnable "glvnd" true)

    (lib.mesonBool "gallium-nine" true) # Direct3D in Wine
    (lib.mesonBool "osmesa" true) # used by wine
    (lib.mesonBool "teflon" true) # TensorFlow frontend

    # Enable Intel RT stuff when available
    (lib.mesonBool "install-intel-clc" true)
    (lib.mesonEnable "intel-rt" stdenv.isx86_64)
    (lib.mesonOption "clang-libdir" "${llvmPackages.clang-unwrapped.lib}/lib")

    # Clover, old OpenCL frontend
    (lib.mesonOption "gallium-opencl" "icd")
    (lib.mesonBool "opencl-spirv" true)

    # Rusticl, new OpenCL frontend
    (lib.mesonBool "gallium-rusticl" true)

    # meson auto_features enables this, but we do not want it
    (lib.mesonEnable "android-libbacktrace" false)
    (lib.mesonEnable "microsoft-clc" false) # Only relevant on Windows (OpenCL 1.2 API on top of D3D12)
  ] ++ lib.optionals enablePatentEncumberedCodecs [
    (lib.mesonOption "video-codecs" "all")
  ] ++ lib.optionals needNativeCLC [
    (lib.mesonOption "intel-clc" "system")
  ];

  strictDeps = true;

  buildInputs = with xorg; [
    directx-headers
    elfutils
    expat
    glslang
    libglvnd
    libomxil-bellagio
    libunwind
    libva-minimal
    libvdpau
    libX11
    libxcb
    libXext
    libXfixes
    libXrandr
    libxshmfence
    libXxf86vm
    llvmPackages.clang
    llvmPackages.clang-unwrapped
    llvmPackages.libclc
    llvmPackages.libllvm
    lm_sensors
    python3Packages.python # for shebang
    spirv-llvm-translator
    udev
    valgrind-light
    vulkan-loader
    wayland
    wayland-protocols
    xcbutilkeysyms
    xorgproto
    zstd
  ];

  depsBuildBuild = [
    pkg-config
    buildPackages.stdenv.cc
  ];

  nativeBuildInputs = [
    meson
    pkg-config
    ninja
    intltool
    bison
    flex
    file
    python3Packages.python
    python3Packages.packaging
    python3Packages.pycparser
    python3Packages.mako
    python3Packages.ply
    jdupes
    glslang
    rustc
    rust-bindgen
    rust-cbindgen
    rustPlatform.bindgenHook
    wayland-scanner
  ] ++ lib.optionals needNativeCLC [
    buildPackages.mesa.driversdev
  ];

  disallowedRequisites = lib.optionals needNativeCLC [
    buildPackages.mesa.driversdev
  ];

  propagatedBuildInputs = [ libdrm ];

  doCheck = false;

  postInstall = ''
    # Move driver-related bits to $drivers
    moveToOutput "lib/lib*_mesa*" $drivers
    moveToOutput "lib/libpowervr_rogue*" $drivers
    moveToOutput "lib/libxatracker*" $drivers
    moveToOutput "lib/libvulkan_*" $drivers

    # Update search path used by glvnd (it's pointing to $out but drivers are in $drivers)
    for js in $drivers/share/glvnd/egl_vendor.d/*.json; do
      substituteInPlace "$js" --replace-fail '"libEGL_' '"'"$drivers/lib/libEGL_"
    done

    # And same for Vulkan
    for js in $drivers/share/vulkan/icd.d/*.json; do
      substituteInPlace "$js" --replace-fail "$out" "$drivers"
    done

    # Move Vulkan layers to $drivers and update manifests
    moveToOutput "lib/libVkLayer*" $drivers
    for js in $drivers/share/vulkan/{im,ex}plicit_layer.d/*.json; do
      substituteInPlace "$js" --replace '"libVkLayer_' '"'"$drivers/lib/libVkLayer_"
    done

    # Construct our own .icd files that contain absolute paths.
    mkdir -p $opencl/etc/OpenCL/vendors/
    echo $opencl/lib/libMesaOpenCL.so > $opencl/etc/OpenCL/vendors/mesa.icd
    echo $opencl/lib/libRusticlOpenCL.so > $opencl/etc/OpenCL/vendors/rusticl.icd

    moveToOutput bin/intel_clc $driversdev
    moveToOutput lib/gallium-pipe $opencl
    moveToOutput "lib/lib*OpenCL*" $opencl
    moveToOutput "lib/libOSMesa*" $osmesa
    moveToOutput bin/spirv2dxil $spirv2dxil
    moveToOutput "lib/libspirv_to_dxil*" $spirv2dxil
    moveToOutput lib/libteflon.so $teflon
  '';

  postFixup = ''
    # set the default search path for DRI drivers; used e.g. by X server
    for pc in lib/pkgconfig/{dri,d3d}.pc; do
      [ -f "$dev/$pc" ] && substituteInPlace "$dev/$pc" --replace "$drivers" "${libglvnd.driverLink}"
    done

    # remove pkgconfig files for GL/EGL; they are provided by libGL.
    rm -f $dev/lib/pkgconfig/{gl,egl}.pc

    # Move development files for libraries in $drivers to $driversdev
    mkdir -p $driversdev/include
    mv $dev/include/xa_* $dev/include/d3d* -t $driversdev/include || true
    mkdir -p $driversdev/lib/pkgconfig
    for pc in lib/pkgconfig/{xatracker,d3d}.pc; do
      if [ -f "$dev/$pc" ]; then
        substituteInPlace "$dev/$pc" --replace $out $drivers
        mv $dev/$pc $driversdev/$pc
      fi
    done

    # Don't depend on build python
    patchShebangs --host --update $out/bin/*

    # NAR doesn't support hard links, so convert them to symlinks to save space.
    jdupes --hard-links --link-soft --recurse "$drivers"

    # add RPATH so the drivers can find the moved libgallium and libdricore9
    # moved here to avoid problems with stripping patchelfed files
    for lib in $drivers/lib/*.so* $drivers/lib/*/*.so*; do
      if [[ ! -L "$lib" ]]; then
        patchelf --set-rpath "$(patchelf --print-rpath $lib):$drivers/lib" "$lib"
      fi
    done

    # add RPATH here so Zink can find libvulkan.so
    patchelf --add-rpath ${vulkan-loader}/lib $drivers/lib/dri/zink_dri.so
  '';

  env.NIX_CFLAGS_COMPILE = toString ([
    "-UPIPE_SEARCH_DIR"
    "-DPIPE_SEARCH_DIR=\"${placeholder "opencl"}/lib/gallium-pipe\""
  ]);

  passthru = {
    inherit (libglvnd) driverLink;
    inherit llvmPackages;

    tests.devDoesNotDependOnLLVM = stdenv.mkDerivation {
      name = "mesa-dev-does-not-depend-on-llvm";
      buildCommand = ''
        echo ${mesa.dev} >>$out
      '';
      disallowedRequisites = [ llvmPackages.llvm mesa.drivers ];
    };

    llvmpipeHook = makeSetupHook {
      name = "llvmpipe-hook";
      substitutions.drivers = mesa.drivers;
    } ./llvmpipe-hook.sh;
  };
}
