{ config
, defaultVersion
, forceUpdate
, gnused
, graalVersion
, javaVersion
, jq
, lib
, name
, sourcesPath
, writeShellScript
}:

/*
  How to  use:
  run `nix-shell maintainers/scripts/update.nix --argstr package graalvmXX-ce`
  to update the graalvmXX-ce-sources.json file.
  E.g: nix-shell maintainers/scripts/update.nix --argstr package graalvm17-ce

  Basic idea:
  If we know the platform, product, javaVersion and graalVersion
  we can create the url. This leads to the following json structure:
  {
  "platform/arch1": {
  "product1|javaVersion|graalVersion": { "sha256": "...", "url": "..."},
  "product2|javaVersion|graalVersion": { "sha256": "...", "url": "..."},
  ...
  },
  "platform/arch2": {
  ...
  }
  }
*/

let
  separator = "|";

  # isDev :: String -> Boolean
  isDev = version:
    lib.hasInfix "dev" version;

  # getLatestVersion :: String -> String
  getLatestVersion = currentVersion:
    let
      dev = if isDev currentVersion then "dev-" else "";
      url = "https://api.github.com/repos/graalvm/graalvm-ce-${dev}builds/releases/latest";
      file = builtins.fetchurl url;
      json = builtins.fromJSON (builtins.readFile file);
    in
    lib.removePrefix "vm-" json.tag_name;

  # getArchString :: String -> String
  getArchString = nixArchString:
    {
      "aarch64-linux" = "linux-aarch64";
      "aarch64-darwin" = "darwin-aarch64";
      "x86_64-linux" = "linux-amd64";
      "x86_64-darwin" = "darwin-amd64";
    }.${nixArchString};


  # getProductSuffix :: String -> String
  getProductSuffix = productName:
    {
      "graalvm-ce" = ".tar.gz";
      "native-image-installable-svm" = ".jar";
      "ruby-installable-svm" = ".jar";
      "wasm-installable-svm" = ".jar";
      "python-installable-svm" = ".jar";
    }.${productName};

  # getProductSuffix :: String -> String
  getProductBaseUrl = productName:
    {
      "graalvm-ce" = "https://github.com/graalvm/graalvm-ce-builds/releases/download";
      "native-image-installable-svm" = "https://github.com/graalvm/graalvm-ce-builds/releases/download";
      "ruby-installable-svm" = "https://github.com/oracle/truffleruby/releases/download";
      "wasm-installable-svm" = "https://github.com/graalvm/graalvm-ce-builds/releases/download";
      "python-installable-svm" = "https://github.com/graalvm/graalpython/releases/download";
    }.${productName};

  # getDevUrl :: String
  getDevUrl = { arch, graalVersion, product, javaVersion }:
    let
      baseUrl = https://github.com/graalvm/graalvm-ce-dev-builds/releases/download;
    in
    "${baseUrl}/${graalVersion}/${product}-${javaVersion}-${arch}-dev${getProductSuffix product}";

  # getReleaseUrl :: AttrSet -> String
  getReleaseUrl = { arch, graalVersion, product, javaVersion }:
    let baseUrl = getProductBaseUrl product;
    in
    "${baseUrl}/vm-${graalVersion}/${product}-${javaVersion}-${arch}-${graalVersion}${getProductSuffix product}";

  # getUrl :: AttrSet -> String
  getUrl = args@{ arch, graalVersion, product, javaVersion }:
    if isDev graalVersion
    then getDevUrl args
    else getReleaseUrl args;

  # computeSha256 :: String -> String
  computeSha256 = url:
    builtins.hashFile "sha256" (builtins.fetchurl url);

  # downloadSha256 :: String -> String
  downloadSha256 = url:
    let sha256Url = url + ".sha256";
    in
    builtins.readFile (builtins.fetchurl sha256Url);

  # getSha256 :: String -> String -> String
  getSha256 = graalVersion: url:
    if isDev graalVersion
    then computeSha256 url
    else downloadSha256 url;

  # cartesianZipListsWith :: (a -> b -> c) -> [a] -> [b] -> [c]
  cartesianZipListsWith = f: fst: snd:
    let
      cartesianProduct = lib.cartesianProductOfSets { a = fst; b = snd; };
      fst' = builtins.catAttrs "a" cartesianProduct;
      snd' = builtins.catAttrs "b" cartesianProduct;
    in
    lib.zipListsWith f fst' snd';

  # zipListsToAttrs :: [a] -> [b] -> AttrSet
  zipListsToAttrs = names: values:
    lib.listToAttrs (
      lib.zipListsWith (name: value: { inherit name value; }) names values
    );

  # genProductJavaVersionGraalVersionAttrSet :: String -> AttrSet
  genProductJavaVersionGraalVersionAttrSet = product_javaVersion_graalVersion:
    let
      attrNames = [ "product" "javaVersion" "graalVersion" ];
      attrValues = lib.splitString separator product_javaVersion_graalVersion;
    in
    zipListsToAttrs attrNames attrValues;

  # genUrlAndSha256 :: String -> String -> AttrSet
  genUrlAndSha256 = arch: product_javaVersion_graalVersion:
    let
      productJavaVersionGraalVersion =
        (genProductJavaVersionGraalVersionAttrSet product_javaVersion_graalVersion)
        // { inherit arch; };
      url = getUrl productJavaVersionGraalVersion;
      sha256 = getSha256 productJavaVersionGraalVersion.graalVersion url;
    in
    {
      ${arch} = {
        ${product_javaVersion_graalVersion} = {
          inherit sha256 url;
        };
      };
    };

  # genArchProductVersionPairs :: String -> -> String -> AttrSet -> [AttrSet]
  genArchProductVersionList = javaVersion: graalVersion: archProducts:
    let
      arch = archProducts.arch;
      products = archProducts.products;
      javaGraalVersion = javaVersion + separator + (getLatestVersion (archProducts.version or graalVersion));
      productJavaGraalVersionList =
        cartesianZipListsWith (a: b: a + separator + b)
          products [ javaGraalVersion ];
    in
    cartesianZipListsWith (genUrlAndSha256) [ arch ] productJavaGraalVersionList;


  # genSources :: String -> String -> AttrSet -> Path String
  genSources = javaVersion: defaultVersion: config:
    let
      archProducts = builtins.attrValues config;
      sourcesList = builtins.concatMap (genArchProductVersionList javaVersion defaultVersion) archProducts;
      sourcesAttr = builtins.foldl' (lib.recursiveUpdate) { } sourcesList;
    in
    builtins.toFile "sources.json" (builtins.toJSON sourcesAttr);

  # isNew :: String -> String -> Boolean
  isNew = newVersion: currentVersion:
    {
      "-1" = false;
      "0" = false;
      "1" = true;
    }.${builtins.toString (builtins.compareVersions newVersion currentVersion)};

  newVersion = getLatestVersion graalVersion;
  sourcesJson = genSources javaVersion defaultVersion config;
  sourcesJsonPath = lib.strings.escapeShellArg sourcesPath;

  # versionKeyInDefaultNix String -> String
  versionKeyInDefaultNix = graalVersion:
    if isDev graalVersion
    then "${name}-dev-version"
    else "${name}-release-version";

  /*
    updateScriptText :: String -> String -> String
    Writes the json file and updates the version in default.nix using sed
    because update-source-version does not work srcs.
  */
  updateScriptText = newVersion: currentVersion:

    if (forceUpdate || (isNew newVersion currentVersion))
    then
      let
        versionKey = versionKeyInDefaultNix currentVersion;
      in
      ''
        echo "New version found. Updating ${currentVersion} -> ${newVersion}".
        export PATH="${lib.makeBinPath [ jq gnused ]}:$PATH"
        jq . ${sourcesJson} > ${sourcesJsonPath}
        sed -i 's|${versionKey} = "${currentVersion}";|${versionKey} = "${newVersion}";|' \
          ${lib.strings.escapeShellArg ./default.nix}
      ''
    else ''echo "No new version found. Skip updating."'';

in
writeShellScript "update-graal.sh" ''
  set -o errexit
  set -o nounset
  set -o pipefail

  ${updateScriptText newVersion graalVersion}
''
