# Copyright 2019 Google LLC.
# SPDX-License-Identifier: Apache-2.0
#
# buildGo provides Nix functions to build Go packages in the style of Bazel's
# rules_go.

{ pkgs ? import <nixpkgs> { }
, ...
}:

let
  inherit (pkgs) lib;

  go = pkgs.go;
  goStdlib = buildStdlib { inherit go; };

  splitSources = srcs: let
    # exts: { typeName = [ extension ] };
    exts = {
      go = [ ".go" ];
      asm = [ ".s" ".S" ];
      headers = [ ".h" ".hh" ".hpp" ".hxx" ".inc" ];
      c = [ ".c" ];
      cxx = [ ".cc" ".cxx" ".cpp" ];
      objc = [ ".m" ".mm" ];
    };
    # [ extension ] -> { extension = true; }
    extListToAttrs = exts: builtins.listToAttrs (builtins.map (v: lib.attrsets.nameValuePair v true) exts);
    # exts': { typeName = { extension = true; } };
    exts' = builtins.mapAttrs (_: extListToAttrs) exts;

    allExts = lib.lists.flatten (builtins.attrValues exts);
    allExts' = extListToAttrs allExts;

    matchesExts = exts: src: let
      srcExtList = builtins.match ".*([.][^.]+)$" (toString src);
      srcExt = builtins.elemAt srcExtList 0;
    in
      srcExtList != null && exts ? "${srcExt}";
    splitByExt = exts: builtins.filter (matchesExts exts) srcs;

    leftovers = builtins.filter (src: !(matchesExts allExts' src)) srcs;
  in assert (lib.assertMsg (builtins.length leftovers == 0) "uncategorisable files: ${toString leftovers}"); builtins.mapAttrs (name: value: splitByExt value) exts';

  importconfig = { name, deps }: let
    # Go through every dep and generate a packagefile importpath=${output}.
    depPackagefiles = map (dep: if dep ? importpath then "packagefile ${dep.importpath}=${dep}/pkg.a" else "") deps;
  in ''
    # nix buildGo ${name}
    ${builtins.concatStringsSep "\n" depPackagefiles}
  '';

  buildStdlib = { go }: pkgs.stdenv.mkDerivation {
    pname = "go-stdlib";
    inherit (go) version;

    nativeBuildInputs = [ go ];

    unpackPhase = ''
      HOME=$NIX_BUILD_TOP/home
      mkdir $HOME

      goroot="$(go env GOROOT)"
      cp -R "$goroot/src" "$goroot/pkg" .
    '';
    dontConfigure = true;
    buildPhase = ''
      chmod -R +w .
      GODEBUG=installgoroot=all GOROOT=$NIX_BUILD_TOP go install -v --trimpath std
    '';
    installPhase = ''
      mkdir $out
      cp -r pkg/*_*/* $out

      find $out -name '*.a' | while read -r ARCHIVE_FULL; do
        ARCHIVE="''${ARCHIVE_FULL#"$out/"}"
        PACKAGE="''${ARCHIVE%.a}"
        echo "packagefile $PACKAGE=$ARCHIVE_FULL"
      done > $out/importcfg
    '';
    dontFixup = true;

    passthru.go = go;
  };

  package = {
    name,
    path,
    srcs,
    deps ? [],
    cgo ? false,
    cgodeps ? [],
    go ? goStdlib.go,
    stdlib ? goStdlib,
  }@args: assert stdlib != null -> stdlib.go == go; let
    importcfg = importconfig { inherit name deps; stdlib = go; };

    srcs' = splitSources srcs;
    stdenv = if cgo then pkgs.stdenv else pkgs.stdenvNoCC;

    cgoSrcs = srcs'.c ++ srcs'.cxx ++ srcs'.objc;
    noCgoNoC = lib.assertMsg (builtins.length cgoSrcs == 0) "cgo source files present, but cgo not set to true: ${toString cgoSrcs}";

    baseNameOnly = map (f: baseNameOf (toString f));
  in assert !cgo -> noCgoNoC; stdenv.mkDerivation rec {
    inherit name;
    __structuredAttrs = true;

    buildInputs = cgodeps;
    nativeBuildInputs = [ go ] ++ (if cgo then [ pkgs.pkg-config ] else []);

    passthru = {
      importpath = path;
      inherit importcfg stdlib;
      cgo = cgo || lib.lists.any (x: x.cgo) deps;
    };

    importcfg = importconfig { inherit name deps; };
    allSrcs = srcs;
    goSrcs = baseNameOnly srcs'.go;
    asmSrcs = baseNameOnly srcs'.asm;
    cSrcs = baseNameOnly srcs'.c;
    cxxSrcs = baseNameOnly srcs'.cxx;
    objcSrcs = baseNameOnly srcs'.objc;

    unpackPhase = ''
      mkdir src
      ${builtins.concatStringsSep "\n" (map (f: ''
        cp "${f}" "src/${baseNameOf (toString f)}"
      '') srcs)}
      cd src
    '';
    configurePhase = ''
      echo "$importcfg" > $NIX_BUILD_TOP/importcfg
      ${if stdlib != null then ''
        cat ${stdlib}/importcfg >> $NIX_BUILD_TOP/importcfg
      '' else ""}
      for dep in deps; do
        if [[ ! -f "$dep/importcfg" ]]; then continue; fi
        cat $dep/importcfg >> $NIX_BUILD_TOP/importcfg
      done
    '';
    buildPhase = ''
      mkdir -p $out
      completeFlag="${if cgo then "" else "-complete"}"
      outputFlag="-o $out/pkg.a"
      if [ ''${#asmSrcs[@]} -gt 0 ]; then
        completeFlag=""
        mkdir $NIX_BUILD_TOP/pack $NIX_BUILD_TOP/include
        outputFlag="-symabis $NIX_BUILD_TOP/goasm.abi -o $out/pkg.a -asmhdr $NIX_BUILD_TOP/include/go_asm.h"

        touch $NIX_BUILD_TOP/include/go_asm.h

        ASMCMD="go tool asm -trimpath $NIX_BUILD_TOP/src -I $NIX_BUILD_TOP/include -I $(go env GOROOT)/pkg/include -D GOOS=$(go env GOOS) -D GOOS_$(go env GOOS) -D GOARCH=$(go env GOARCH) -D GOARCH_$(go env GOARCH) -p ${path}"
        $ASMCMD -gensymabis -o $NIX_BUILD_TOP/goasm.abi "''${asmSrcs[@]}"
      fi
      ${if cgo then ''
        mkdir $NIX_BUILD_TOP/cgo $NIX_BUILD_TOP/cgo_built
        go tool cgo -objdir $NIX_BUILD_TOP/cgo -trimpath $NIX_BUILD_TOP/src -importpath "${path}" -- "''${goSrcs[@]}"
        for f in "''${cSrcs[@]}"; do
          $CC -I$NIX_BUILD_TOP/cgo -I. -c "$f" -o "$NIX_BUILD_TOP/cgo_built/''${f}.o"
        done
        for f in "''${cxxSrcs[@]}"; do
          $CXX -I$NIX_BUILD_TOP/cgo -I. -c "$f" -o "$NIX_BUILD_TOP/cgo_built/''${f}.o"
        done
        for f in "''${objcSrcs[@]}"; do
          $OBJC -I$NIX_BUILD_TOP/cgo -I. -c "$f" -o "$NIX_BUILD_TOP/cgo_built/''${f}.o"
        done
        pushd $NIX_BUILD_TOP/cgo &>/dev/null
        for f in *.cgo2.c _cgo_export.c _cgo_main.c; do
          $CC -I$NIX_BUILD_TOP/src -c "$f" -o ''${f}.o
        done
        $CC -o _cgo_.o _cgo_main.c.o _cgo_export.c.o $NIX_BUILD_TOP/cgo_built/*.o *.cgo2.c.o
        popd &>/dev/null
        go tool cgo -dynpackage "${name}" -dynimport $NIX_BUILD_TOP/cgo/_cgo_.o -dynout $NIX_BUILD_TOP/cgo/_cgo_imports.go
        shopt -s nullglob
        goSrcs=($NIX_BUILD_TOP/cgo/*.go)
      '' else ""}
      go tool compile $completeFlag -importcfg $NIX_BUILD_TOP/importcfg -trimpath $NIX_BUILD_TOP/src -pack -p "${path}" $outputFlag "''${goSrcs[@]}"
      if [ ''${#asmSrcs[@]} -gt 0 ]; then
        $ASMCMD -o $NIX_BUILD_TOP/pack/goasm.o "''${asmSrcs[@]}"
        go tool pack r $out/pkg.a $NIX_BUILD_TOP/pack/*.o
      fi
      ${if cgo then ''
        go tool pack r $out/pkg.a $NIX_BUILD_TOP/cgo/*.cgo2.c.o $NIX_BUILD_TOP/cgo/_cgo_export.c.o $NIX_BUILD_TOP/cgo_built/*.o
      '' else ""}
    '';
  };

  program = {
    name,
    deps ? [],
    ...
  }@args: let
    pkg = package (args // {
      name = "${name}-lib";
      path = "main";
    });
    cgo = pkg.cgo;
    stdenv = if cgo then pkgs.stdenv else pkgs.stdenvNoCC;
  in stdenv.mkDerivation rec {
    inherit name;
    __structuredAttrs = true;

    nativeBuildInputs = [ go ];

    lib = pkg;
    inherit (pkg) importcfg stdlib;

    dontUnpack = true;
    configurePhase = ''
      echo "$importcfg" > importcfg
      ${if stdlib != null then ''
        cat ${stdlib}/importcfg >> importcfg
      '' else ""}
    '';
    buildPhase = ''
      mkdir out
      go tool link -importcfg importcfg -tmpdir "$TMPDIR" -o "out/bin" -s -w $lib/pkg.a
    '';
    installPhase = ''
      mkdir -p $out/bin
      cp out/bin "$out/bin/${name}"
    '';
  };

  external = {
    src,
    path,
    deps ? [ ],
    tags ? [ ],
    cgo ? false,
    cgodeps ? [ ],
    cgocflags ? [ ],
    cgoldflags ? [ ],
  }: {};
in
{
  #inherit program package proto external;
  inherit goStdlib package program external;
}