//file:noinspection GroovyAssignabilityCheck

buildscript() {
  dependencies() {
    classpath("commons-io:commons-io:2.6")
    classpath("com.google.guava:guava:28.0-jre")
  }
}

plugins() {
  id("java")
  id("com.gradleup.shadow").version("8.3.6")
}

compileJava() {
  getOptions().getRelease().set(17)
  getOptions().setEncoding("UTF-8")
}

dependencies() {
  implementation(project(":api"))
  implementation("net.elytrium.commons:config:$elytriumCommonsVersion")
  implementation("net.elytrium.commons:utils:$elytriumCommonsVersion")
  implementation("net.elytrium.commons:velocity:$elytriumCommonsVersion")
  implementation("net.elytrium.commons:kyori:$elytriumCommonsVersion")

  implementation("net.elytrium:fastprepare:$fastPrepareVersion")
  compileOnly("com.velocitypowered:velocity-api:$velocityVersion")
  annotationProcessor("com.velocitypowered:velocity-api:$velocityVersion")
  compileOnly("com.velocitypowered:velocity-proxy:$velocityVersion") // From Elytrium Repo.
  compileOnly("com.velocitypowered:velocity-native:$velocityVersion")

  // Needs for some velocity methods.
  compileOnly("io.netty:netty-codec:$nettyVersion")
  compileOnly("io.netty:netty-handler:$nettyVersion")
  compileOnly("it.unimi.dsi:fastutil-core:$fastutilVersion")

  implementation("org.bstats:bstats-velocity:$bstatsVersion")

  compileOnly("com.github.spotbugs:spotbugs-annotations:$spotbugsVersion")
}

shadowJar() {
  getArchiveClassifier().set("")
  setArchiveFileName("limboapi-${project.version}.jar")

  exclude("META-INF/versions/**")
  exclude("net/kyori/**")

  relocate("org.bstats", "net.elytrium.limboapi.thirdparty.org.bstats")
  relocate("net.elytrium.fastprepare", "net.elytrium.limboapi.thirdparty.fastprepare")
  relocate("net.elytrium.commons.velocity", "net.elytrium.limboapi.thirdparty.commons.velocity")
  relocate("net.elytrium.commons.kyori", "net.elytrium.limboapi.thirdparty.commons.kyori")
  relocate("net.elytrium.commons.config", "net.elytrium.limboapi.thirdparty.commons.config")
}

license() {
  matching(includes: ["**/mcprotocollib/**"]) {
    setHeader(getRootProject().file("HEADER_MCPROTOCOLLIB.txt"))
  }
  matching(includes: ["**/LoginListener.java", "**/KickListener.java", "**/LoginTasksQueue.java", "**/MinecraftLimitedCompressDecoder.java"]) {
    setHeader(getRootProject().file("HEADER_MIXED.txt"))
  }

  setHeader(getRootProject().file("HEADER.txt"))
}

tasks.register("finalize") {
  doLast {
    file("build/libs/${project.name}-${project.version}.jar").delete()
  }
}

assemble.dependsOn(shadowJar)
build.finalizedBy(finalize)

import groovy.io.FileType
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
import org.apache.commons.io.FilenameUtils
import org.apache.commons.io.FileUtils
import com.google.common.hash.Hashing
import com.google.common.io.Files

import java.nio.file.Path
import java.util.function.Function
import java.util.stream.Collectors

enum MinecraftVersion {
  MINECRAFT_1_7_2(4),
  MINECRAFT_1_7_6(5),
  MINECRAFT_1_8(47),
  MINECRAFT_1_9(107),
  MINECRAFT_1_9_1(108),
  MINECRAFT_1_9_2(109),
  MINECRAFT_1_9_4(110),
  MINECRAFT_1_10(210),
  MINECRAFT_1_11(315),
  MINECRAFT_1_11_1(316),
  MINECRAFT_1_12(335),
  MINECRAFT_1_12_1(338),
  MINECRAFT_1_12_2(340),
  MINECRAFT_1_13(393),
  MINECRAFT_1_13_1(401),
  MINECRAFT_1_13_2(404),
  MINECRAFT_1_14(477),
  MINECRAFT_1_14_1(480),
  MINECRAFT_1_14_2(485),
  MINECRAFT_1_14_3(490),
  MINECRAFT_1_14_4(498),
  MINECRAFT_1_15(573),
  MINECRAFT_1_15_1(575),
  MINECRAFT_1_15_2(578),
  MINECRAFT_1_16(735),
  MINECRAFT_1_16_1(736),
  MINECRAFT_1_16_2(751),
  MINECRAFT_1_16_3(753),
  MINECRAFT_1_16_4(754),
  MINECRAFT_1_17(755),
  MINECRAFT_1_17_1(756),
  MINECRAFT_1_18(757),
  MINECRAFT_1_18_2(758),
  MINECRAFT_1_19(759),
  MINECRAFT_1_19_1(760),
  MINECRAFT_1_19_3(761),
  MINECRAFT_1_19_4(762),
  MINECRAFT_1_20(763),
  MINECRAFT_1_20_2(764),
  MINECRAFT_1_20_3(765),
  MINECRAFT_1_20_5(766),
  MINECRAFT_1_21(767),
  MINECRAFT_1_21_2(768),
  MINECRAFT_1_21_4(769),
  MINECRAFT_1_21_5(770),
  MINECRAFT_1_21_6(771),
  MINECRAFT_1_21_7(772)

  public static final List<MinecraftVersion> WORLD_VERSIONS = List.of(
    MINECRAFT_1_13,
    MINECRAFT_1_13_2,
    MINECRAFT_1_14,
    MINECRAFT_1_15,
    MINECRAFT_1_16,
    MINECRAFT_1_16_2,
    MINECRAFT_1_17,
    MINECRAFT_1_19,
    MINECRAFT_1_19_3,
    MINECRAFT_1_19_4,
    MINECRAFT_1_20,
    MINECRAFT_1_20_3,
    MINECRAFT_1_20_5,
    MINECRAFT_1_21_2,
    MINECRAFT_1_21_4,
    MINECRAFT_1_21_5,
    MINECRAFT_1_21_6,
    MINECRAFT_1_21_7
  )

  public static final MinecraftVersion MINIMUM_VERSION = MINECRAFT_1_7_2
  public static final MinecraftVersion MAXIMUM_VERSION = values()[values().length - 1]

  static MinecraftVersion fromVersionName(String name) {
    return valueOf("MINECRAFT_" + name.replace('.', '_'))
  }

  // Cache version name to reduce memory usage in general
  final String versionName = this.toString().substring(10).replace('_', '.')
  final int protocolVersion

  MinecraftVersion(int protocolVersion) {
    this.protocolVersion = protocolVersion
  }

  int getProtocolVersion() {
    return this.protocolVersion
  }

  String getVersionName() {
    return this.versionName
  }
}

project.ext.dataDirectory = new File(this.getLayout().getBuildDirectory().get().getAsFile(), "minecraft")
project.ext.generatedDir = new File(this.getLayout().getBuildDirectory().get().getAsFile(), "generated/minecraft")
project.ext.versionManifestFile = new File(dataDirectory, "manifest.json")

sourceSets {
  main {
    resources {
      srcDirs += generatedDir
    }
  }
}

tasks.register("downloadManifest") {
  this.println("> Downloading version manifest...")
  versionManifestFile.getParentFile().mkdirs()
  if (checkIsCacheValid(versionManifestFile)) {
    FileUtils.copyURLToFile(new URL(manifestUrl), versionManifestFile)
  }
}

boolean checkIsCacheValid(File file) {
  if (file.exists() && System.currentTimeMillis() - file.lastModified() < Long.parseLong(cacheValidMillis)) {
    println("> Found cached " + file.getName())
    return false
  }

  return true
}
File downloadVersionManifest(String version) {
  this.println("> Downloading ${version} manifest...")

  Object manifest = new JsonSlurper().parse(versionManifestFile)
  def optional = manifest.versions.stream().filter({ it.id == version }).findFirst()
  if (optional.empty()) {
    throw new RuntimeException("Couldn't find version: ${version}")
  }

  File output = new File(dataDirectory, "${version}/manifest.json")
  output.getParentFile().mkdirs()
  FileUtils.copyURLToFile(new URL(optional.get().url), output)
  return output
}

@SuppressWarnings('GrMethodMayBeStatic')
File getGeneratedCache(MinecraftVersion version) {
  File generated = new File(dataDirectory, "${version.getVersionName()}/generated")
  return new File(generated, "reports/blocks.json").exists()
          && new File(generated, "reports/${version >= MinecraftVersion.MINECRAFT_1_14 ? "registries" : "items"}.json").exists()
          && new File(generated, "data/minecraft/tags").exists()
          ? generated : null
}

static boolean validateServer(File file, String expected) {
  if (file == null || !file.exists()) {
    return false
  }

  def hash = Files.asByteSource(file).hash(Hashing.sha1())
  StringBuilder hashBuilder = new StringBuilder()
  hash.asBytes().each({hashBuilder.append(Integer.toString((it & 0xFF) + 0x100, 16).substring(1))})
  return hashBuilder.toString() == expected
}

File getServerJar(String version) {
  File manifestFile = this.downloadVersionManifest(version)
  Object manifest = new JsonSlurper().parse(manifestFile)

  File jarFile = new File(dataDirectory, "${version}/server.jar")
  if (!validateServer(jarFile, manifest.downloads.server.sha1)) {
    this.println("> Downloading ${version} server...")
    jarFile.getParentFile().mkdirs()
    FileUtils.copyURLToFile(new URL(manifest.downloads.server.url), jarFile)
  }

  return jarFile
}

File generateData(MinecraftVersion version) {
  File cache = getGeneratedCache(version)
  if (cache != null) {
    return cache
  }

  File jarFile = this.getServerJar(version.getVersionName())
  File targetDir = new File(jarFile.getParentFile(), "generated")

  try {
    FileUtils.deleteDirectory(targetDir)
  } catch (IOException ignored) {
    // Ignored.
  }

  String command
  if (version >= MinecraftVersion.MINECRAFT_1_18) {
    command = "\"%s\" -DbundlerMainClass=net.minecraft.data.Main -jar \"${jarFile.getAbsolutePath()}\" --reports --server"
  } else {
    command = "\"%s\" -cp \"${jarFile.getAbsolutePath()}\" net.minecraft.data.Main --reports --server"
  }

  exec {
    if (System.getProperty("os.name").toLowerCase().contains("win")) {
      File java = new File(System.getProperty("java.home"), "bin/java.exe")
      commandLine("cmd", "/c", String.format(command, java))
    } else {
      File java = new File(System.getProperty("java.home"), "bin/java")
      commandLine("bash", "-c", String.format(command, java))
    }

    workingDir(jarFile.getParentFile())
  }

  return targetDir
}

static Map<String, Map<String, String>> getDefaultProperties(Object data) {
  Map<String, Map<String, String>> defaultProperties = new HashMap<>()

  data.forEach({ key, block ->
    if (!block.containsKey("properties")) {
      return
    }

    for (Object blockState : block.states) {
      if (!blockState.containsKey("default") || !blockState.default) {
        continue
      }

      Map<String, String> properties = blockState["properties"]
      defaultProperties.put(key, properties)
      break
    }
  })

  return defaultProperties
}

static Map<MinecraftVersion, Map<String, String>> loadFallbackMapping(File file) {
  Object map = new JsonSlurper().parse(file)
  return MinecraftVersion.values().collectEntries({ version ->
    [version, map.getOrDefault(version.toString(), Collections.emptyMap())]
  })
}

static Map<MinecraftVersion, Map<String, Integer>> loadLegacyMapping(File file) {
  return new JsonSlurper().parse(file).collectEntries({ version, mapping ->
    [MinecraftVersion.valueOf(version), mapping.collectEntries({ block, id ->
      [block, Integer.parseInt(id)]
    })]
  })
}

static int getBlockID(String block,
                      Map<MinecraftVersion, Map<String, Integer>> mappings,
                      Map<MinecraftVersion, Map<String, Map<String, String>>> properties,
                      Map<MinecraftVersion, Map<String, String>> fallback,
                      MinecraftVersion version) {
  Map<String, Map<String, String>> defaultProperties
  if (version >= MinecraftVersion.MINECRAFT_1_13) {
    defaultProperties = properties[version]
  } else {
    defaultProperties = properties[MinecraftVersion.MINECRAFT_1_18_2]
  }

  String[] split = block.split("\\[")
  String noArgBlock = split[0]

  MinecraftVersion fallbackVersion = MinecraftVersion.MAXIMUM_VERSION
  while (fallbackVersion != version) {
    --fallbackVersion
    noArgBlock = fallback[fallbackVersion].getOrDefault(noArgBlock, noArgBlock)
  }

  Map<String, String> blockProperties = defaultProperties[noArgBlock]
  String targetBlockID
  if (blockProperties == null) {
    targetBlockID = noArgBlock
  } else {
    Map<String, String> currentProperties = new TreeMap<>(blockProperties)
    if (split.length > 1) {
      String[] args = split[1].split(",")
      Map<String, String> input = Arrays.stream(args)
              .map(arg -> arg.replace("]", "").split("="))
              .collect(Collectors.toMap(parts -> parts[0], parts -> parts[1]))

      input.forEach({ key, value ->
        if (currentProperties.containsKey(key)) {
          currentProperties.put(key, value)
        }
      })
    }

    targetBlockID = noArgBlock + Arrays.toString(
            currentProperties.collect({ k, v -> k + "=" + v }).toArray()
    ).replace(" ", "")
  }

  Integer id = mappings[version][targetBlockID]
  if (id == null && blockProperties != null) {
    targetBlockID = noArgBlock + Arrays.toString(
            new TreeMap<>(blockProperties).collect({ k, v -> k + "=" + v }).toArray()
    ).replace(" ", "")
    id = mappings[version][targetBlockID]
  }

  if (id == null) {
    System.err.println("No ${version.getVersionName()} fallback data for ${noArgBlock}, replacing with minecraft:stone")
    id = 1
  }

  return id
}

static Map<String, Integer> getBlockMappings(Object data, Map<String, Map<String, String>> defaultPropertiesMap) {
  Map<String, Integer> mapping = new HashMap<>()

  data.forEach({ blockID, blockData ->
    for (Object blockState : blockData.states) {
      int protocolID = blockState.id

      if (blockState.containsKey("properties")) {
        Map<String, String> stateProperties = blockState["properties"]
        Map<String, String> properties = new TreeMap<>(
                defaultPropertiesMap.getOrDefault(blockID, Collections.emptyMap()))

        properties.putAll(stateProperties)

        String stateID = blockID + Arrays.toString(
                properties.collect({ k, v -> k + "=" + v }).toArray()
        ).replace(" ", "")

        mapping.put(stateID, protocolID)
      } else {
        mapping.put(blockID, protocolID)
      }
    }
  })

  return mapping
}

void generateBlockMappings(File targetDir, Map<MinecraftVersion, Object> blockReports) {
  File defaultBlockPropertiesFile = new File(targetDir, "defaultblockproperties.json")
  File blockStatesFile = new File(targetDir, "blockstates.json")
  File blockStatesMappingFile = new File(targetDir, "blockstates_mapping.json")
  File legacyBlocksFile = new File(targetDir, "legacyblocks.json")

  if (checkIsCacheValid(defaultBlockPropertiesFile) || checkIsCacheValid(blockStatesFile)
          || checkIsCacheValid(blockStatesMappingFile) || checkIsCacheValid(legacyBlocksFile)) {
    this.println("> Generating default block properties...")

    Map<MinecraftVersion, Map<String, Map<String, String>>> defaultProperties =
            blockReports.collectEntries({ version, report ->
              [version, getDefaultProperties(report)]
            })

    defaultBlockPropertiesFile.write(JsonOutput.prettyPrint(
            JsonOutput.toJson(defaultProperties[MinecraftVersion.MAXIMUM_VERSION].sort())), "UTF-8")

    this.println("> Generating blockstates...")

    Map<MinecraftVersion, Map<String, Integer>> mappings = loadLegacyMapping(
            new File(this.getProjectDir(), "mapping/legacyblockmapping.json"))

    blockReports.forEach({ version, report ->
      mappings.put(version, getBlockMappings(report, defaultProperties[version]))
    })

    Map<String, Integer> blocks = mappings[MinecraftVersion.MAXIMUM_VERSION]

    blockStatesFile.write(
            JsonOutput.prettyPrint(JsonOutput.toJson(
                    blocks.sort(Map.Entry::getValue)
                            .collectEntries({ k, v -> [k, String.valueOf(v)] })
            )), "UTF-8")



    this.println("> Generating blockstates mapping...")

    Map<MinecraftVersion, Map<String, String>> fallbackMapping = loadFallbackMapping(
            new File(this.getProjectDir(), "mapping/fallbackdata.json"))

    Map<String, Map<String, String>> blockstateMapping = new LinkedHashMap<>()
    blocks.sort(Map.Entry::getValue)
            .forEach({ block, modernID ->
              Map<String, String> blockMapping = new LinkedHashMap<>()

              int lastID = -1
              for (MinecraftVersion version : MinecraftVersion.values()) {
                int id = getBlockID(block, mappings, defaultProperties, fallbackMapping, version)
                if (lastID != id) {
                  blockMapping.put(version.getVersionName(), String.valueOf(lastID = id))
                }
              }

              blockstateMapping.put(String.valueOf(modernID), blockMapping)
            })

    blockStatesMappingFile.write(
            JsonOutput.prettyPrint(JsonOutput.toJson(blockstateMapping)), "UTF-8")

    this.println("> Generating legacy blocks...")

    Map<String, String> legacyData = new JsonSlurper().parse(
            new File(this.getProjectDir(), "mapping/legacyblocks.json"))

    legacyData = legacyData.collectEntries({ legacy, modern ->
      [legacy, String.valueOf(getBlockID(modern, mappings, defaultProperties, fallbackMapping, MinecraftVersion.MAXIMUM_VERSION))]
    })

    legacyBlocksFile.write(
            JsonOutput.prettyPrint(JsonOutput.toJson(legacyData)), "UTF-8")
  }
}

static Map<String, Map<String, String>> sortRegistryMapping(Map<String, Map<String, String>> mapping) {
  return mapping.collectEntries({ modernID, map ->
    [modernID, map.sort({
      if (it.getKey().contains(".")) {
        return MinecraftVersion.fromVersionName(it.getKey())
      } else {
        return MinecraftVersion.MINIMUM_VERSION
      }
    })]
  }).sort()
}

void generateRegistryMapping(String target, File targetDir, Map<MinecraftVersion, Object> registriesReports) {
  File targetFile = new File(targetDir, "${target}s.json");
  File targetMappingFile = new File(targetDir, "${target}s_mapping.json");
  if (checkIsCacheValid(targetFile) || checkIsCacheValid(targetMappingFile)) {
    this.println("> Generating ${target}s...")

    Map<MinecraftVersion, Map<String, String>> idMap =
            registriesReports.collectEntries({ version, registry ->
              Object entries = registry["minecraft:${target}"].entries
              return [version, entries.collectEntries({ name, id -> [name, String.valueOf(id["protocol_id"])] })]
            })

    Map<String, String> modernIDs = Collections.max(idMap.entrySet(), Map.Entry.comparingByKey()).getValue()

    targetFile.write(JsonOutput.prettyPrint(
            JsonOutput.toJson(modernIDs.sort({Integer.parseInt(it.getValue()) }))), "UTF-8")

    this.println("> Generating ${target}s mapping...")

    Map<String, Map<String, String>> mapping = new JsonSlurper()
            .parse(new File(this.getProjectDir(), "mapping/legacy_${target}s_mapping.json"))
            .collectEntries({ key, value -> {
              if (modernIDs[key] == null) {
                throw new IllegalStateException("No modern id found for $key")
              }

              return [modernIDs[key], value]
            } })

    idMap.forEach({ version, ids ->
      ids.forEach({ key, id ->
        if (!modernIDs.containsKey(key)) {
          return
        }

        mapping.computeIfAbsent(modernIDs[key], _ -> new LinkedHashMap<>()).put(version.getVersionName(), id)
      })
    })

    mapping = sortRegistryMapping(mapping)
    targetMappingFile.write(JsonOutput.prettyPrint(
            JsonOutput.toJson(mapping.sort({ Integer.parseInt(it.getKey()) }))), "UTF-8")
  }
}

void generateRegistryMappings(File targetDir, Map<MinecraftVersion, Object> registriesReports) {
  this.generateRegistryMapping("item", targetDir, registriesReports
          .findAll({ e -> MinecraftVersion.WORLD_VERSIONS.contains(e.getKey()) }))
  this.generateRegistryMapping("block", targetDir, registriesReports)
  this.generateRegistryMapping("data_component_type", targetDir, registriesReports
          .findAll({ e -> e.getKey() >= MinecraftVersion.MINECRAFT_1_20_5 }))

  File blockEntitiesMappingFile = new File(targetDir, "blockentities_mapping.json");

  if (checkIsCacheValid(blockEntitiesMappingFile)) {
    this.println("> Generating blockentities mapping...")

    Map<String, Map<String, String>> blockentities = new JsonSlurper()
            .parse(new File(this.getProjectDir(), "mapping/legacy_blockentities_mapping.json"))

    registriesReports.forEach({ version, registries ->
      if (version < MinecraftVersion.MINECRAFT_1_19) {
        return
      }

      registries["minecraft:block_entity_type"].entries.forEach({ key, value ->
        int id = value.protocol_id
        blockentities.computeIfAbsent(key, _ -> new LinkedHashMap<>())
                .put(version.getVersionName(), String.valueOf(id))
      })
    })

    blockentities = sortRegistryMapping(blockentities)
    blockEntitiesMappingFile.write(
            JsonOutput.prettyPrint(JsonOutput.toJson(blockentities)), "UTF-8")
  }
}

static Map<String, Map<String, List<String>>> getTags(File tagDir, Map<String, String> tagTypes) {
  Map<String, Map<String, List<String>>> tags = new LinkedHashMap<>()

  tagTypes.forEach({ directory, key ->
    File directoryFile = new File(tagDir, directory)
    if (!directoryFile.exists()) {
      return
    }

    Map<String, List<String>> typeTags = new HashMap<>()
    Map<String, List<String>> tempTags = new HashMap<>()

    directoryFile.eachFileRecurse(FileType.FILES, { file ->
      List<String> values = new JsonSlurper().parse(file).values
      Path relativePath = directoryFile.toPath().relativize(file.toPath())
      String name = FilenameUtils.removeExtension(relativePath.toString()).replace(File.separatorChar, '/' as char)
      typeTags.put("minecraft:" + name, values)
    })

    boolean flatten = false
    while (!flatten) {
      flatten = true

      typeTags.forEach({ name, currentTags ->
        List<String> newTags = new ArrayList<>()
        currentTags.forEach({ currentTag ->
          if (currentTag.startsWith("#")) {
            newTags.addAll(typeTags.get(currentTag.substring(1)))
            flatten = false
          } else {
            newTags.add(currentTag)
          }
        })

        tempTags.put(name, newTags)
      })

      typeTags = tempTags
      tempTags = new HashMap<>()
    }

    tags.put(key, typeTags)
  })

  return tags
}

void generateTags(File targetDir, Map<MinecraftVersion, File> tagDirs) {
  File tagsFile = new File(targetDir, "tags.json");
  if (checkIsCacheValid(tagsFile)) {
      this.println("> Generating tags...")

      Map<String, Object> tagTypes = new JsonSlurper().parse(new File(getProjectDir(), "mapping/tag_types.json"))

      Map<MinecraftVersion, Map<String, Map<String, List<String>>>> allTags =
              tagDirs.collectEntries({ version, dir ->
                  [version, getTags(dir, tagTypes.tag_types)]
              })

      Map<String, Map<String, Set<String>>> mergedTags = new LinkedHashMap<>()

      allTags.forEach({ version, tags ->
          tags.forEach({ type, typeTags -> {
              Map<String, Set<String>> mergedTypeTags = mergedTags.computeIfAbsent(type, _ -> new HashMap<>())
              typeTags.forEach({ name, values ->
                  Set<String> mergedValues = mergedTypeTags.computeIfAbsent(name, _ -> new HashSet<>())
                  if (!tagTypes.supported_tag_types.contains(type)) {
                      return
                  }

                  mergedValues.addAll(values)
              })
          }})
      })

      mergedTags = mergedTags.collectEntries({ type, typeTags ->
          [type, typeTags.collectEntries({ name, values ->
              [name, values.sort()]
          }).sort()]
      })

      tagsFile.write(JsonOutput.prettyPrint(JsonOutput.toJson(mergedTags)), "UTF-8")
  }
}

tasks.register("generateMappings") {
  dependsOn(downloadManifest)

  File targetDir = new File(generatedDir, "mapping")
  targetDir.mkdirs()

  this.println("> Generating Minecraft data...")

  Map<MinecraftVersion, File> generated = Arrays.stream(MinecraftVersion.values())
          .dropWhile({ it < MinecraftVersion.MINECRAFT_1_13 })
          .collect(Collectors.toMap(Function.identity(), this::generateData))

  Map<MinecraftVersion, Object> blockReports = generated.collectEntries({ version, directory ->
    [version, new JsonSlurper().parse(new File(directory, "reports/blocks.json"))]
  })

  this.generateBlockMappings(targetDir, blockReports)

  Map<MinecraftVersion, Object> registriesReports = generated
          .findAll({ it.getKey() >= MinecraftVersion.MINECRAFT_1_14 })
          .collectEntries({ version, directory ->
            [version, new JsonSlurper().parse(new File(directory, "reports/registries.json"))]
          })

  this.generateRegistryMappings(targetDir, registriesReports)

  Map<MinecraftVersion, File> tags = generated
          .collectEntries({ version, directory ->
            [version, new File(directory, "data/minecraft/tags")]
          })

  this.generateTags(targetDir, tags)
}

processResources.dependsOn(generateMappings)