buildscript {
    repositories {
        mavenLocal()
        maven { url = 'http://files.minecraftforge.net/maven' }
        jcenter()
        mavenCentral()
        //maven {url 'http://plugins.gradle.org/m2/'} // For debug purposes
    }
    dependencies {
        classpath 'net.minecraftforge.gradle:ForgeGradle:3.0.23'
        classpath 'com.github.abrarsyed.jastyle:jAstyle:1.3'
        classpath 'com.google.code.gson:gson:2.8.0' // because groovy JsonGenerator doesn't exist wtf
        classpath 'commons-io:commons-io:2.6'
        classpath 'org.ow2.asm:asm-debug-all:5.2'
        //classpath "gradle.plugin.com.dorongold.plugins:task-tree:1.3" // For debug purposes
    }
}

import com.github.abrarsyed.jastyle.ASFormatter;
import com.github.abrarsyed.jastyle.OptParser;
import com.google.gson.GsonBuilder
import groovy.json.JsonSlurper
import java.util.zip.*
import net.minecraftforge.gradle.patcher.task.TaskExtractMCPData
import org.apache.commons.io.IOUtils
import org.apache.commons.io.input.NullInputStream
import org.objectweb.asm.*
import org.objectweb.asm.tree.*

// ===== MAIN BUILDSCRIPT ===== //

// configuration
def settings = new JsonSlurper().parseText(file('conf/settings.json').text)

apply plugin: 'eclipse'
//apply plugin: "com.dorongold.task-tree" // For debug purposes

group = 'com.example'
version = settings.modversion


// subprojects

project(':mcp') {
    apply plugin: 'net.minecraftforge.gradle.forgedev.mcp'
    mcp {
        config = 'de.oceanlabs.mcp:mcp_config:' + settings.mcpconfig + '@zip'
        pipeline = settings.pipeline
    }
}


project(':clean') {
    evaluationDependsOn(':mcp')
    apply plugin: 'eclipse'
    apply plugin: 'net.minecraftforge.gradle.forgedev.patcher'
    repositories {
        mavenCentral()
    }
    patcher {
        parent = project(':mcp')
        patchedSrc = file('src/main/java')
        mappings channel: 'snapshot', version: settings.mappings
        mcVersion = settings.mcversion
    }
    task runclient(type: JavaExec) {
        doFirst {
            mkdir 'runclient'
        }
        classpath sourceSets.main.runtimeClasspath
        args = ['--accessToken', '0', '--version', '1.13']
        main 'net.minecraft.client.main.Main'
        workingDir 'runclient'
    }
}


project(':' + settings.modname) {
    evaluationDependsOn(':clean')
    apply plugin: 'eclipse'
    apply plugin: 'net.minecraftforge.gradle.forgedev.patcher'
    sourceSets {
        main {
            java {
                srcDir "$rootDir/src/main/java"
            }
            resources {
                srcDir "$rootDir/src/main/resources"
            }
        }
    }
    repositories {
        mavenLocal()
        mavenCentral()
    }
    patcher {
        parent = project(':clean')
        patches = file("$rootDir/patches")
        patchedSrc = file('src/main/java')
        srgPatches = false
        clientRun {
            main = settings.clientmain
            properties = [
                assetDirectory: downloadAssets.output
            ]
        }
        serverRun {
            main = settings.servermain
        }
    }
    applyPatches {
        canonicalizeAccess true
        canonicalizeWhitespace true
        maxFuzz 3
    }
    dependencies {
        // Add extra dependencies here
    }

    task runclient(type: JavaExec, dependsOn: [":" + settings.modname + ":downloadAssets", ":" + settings.modname + ":extractNatives"]) {
        doFirst {
            mkdir 'runclient'
        }
        doFirst {
            copy {
                from sourceSets.main.resources
                into "$buildDir/classes/java/main"
            }
        }
        classpath sourceSets.main.runtimeClasspath
        main settings.clientmain
        systemProperties = [
                "org.lwjgl.util.Debug": "true",
                "org.lwjgl.util.DebugLoader": "true"
        ]
        environment += [
                assetDirectory: file("${gradle.getGradleUserHomeDir()}/caches/forge_gradle/assets/"),
                nativesDirectory: extractNatives.output
        ]
        workingDir 'runclient'
    }

    task runserver(type: JavaExec) {
        doFirst {
            mkdir 'runserver'
        }
        classpath sourceSets.main.runtimeClasspath
        main settings.servermain
        args 'nogui'
        workingDir 'runserver'
    }
}


// Main setup task
task setup() {
    dependsOn ':clean:extractMapped'
    dependsOn ':' + settings.modname + ':extractMapped' //These must be strings so that we can do lazy resolution. Else we need evaluationDependsOnChildren above
}

// ===== UTILITY ===== //
class JarFilter {
    String filename
    final InputStream istream
    final boolean newFile
    JarFilter(String filename, InputStream istream, boolean newFile) {
        this.filename = filename
        this.istream = istream
        this.newFile = newFile
    }
    static def filterJar(File jarIn, File jarOut, Closure transformer, Map<File, String> additionalDirs = [:]) {
        def existing = [] as HashSet
        def zipOut = new ZipOutputStream(new FileOutputStream(jarOut))
        additionalDirs.each { additionalDir, prefix ->
            additionalDir.eachFileRecurse { file ->
                def filter = new JarFilter(prefix + additionalDir.toPath().relativize(file.toPath()).toString().replace(File.separator, '/'), file.isDirectory() ? new NullInputStream(0) : file.newInputStream(), true)
                transformer.delegate = filter
                def content = transformer.call()
                if (content != null && !existing.contains(filter.filename)) {
                    def entryOut = new ZipEntry(filter.filename)
                    existing << filter.filename
                    zipOut.putNextEntry(entryOut)
                    zipOut << content
                    zipOut.closeEntry()
                }
            }
        }
        def zipIn = new ZipFile(jarIn)
        zipIn.entries().each { entryIn ->
            def filter = new JarFilter(entryIn.getName(), zipIn.getInputStream(entryIn), false)
            transformer.delegate = filter
            def content = transformer.call()
            if (content != null && !existing.contains(filter.filename)) {
                def entryOut = new ZipEntry(filter.filename)
                existing << filter.filename
                zipOut.putNextEntry(entryOut)
                zipOut << content
                zipOut.closeEntry()
            }
        }
        zipIn.close()
        zipOut.close()
    }
}

// ===== FIXES ===== //

// Fix recompilation requiring extractSrg output
setup.dependsOn ':clean:extractSrg'

// Generic fixMcpConfig task for anything that needs to modify the mcpconfig
class FixMcpConfigTask extends DefaultTask {
    List<Closure> configActions = []
    List<Closure> filterActions = []
    Map<File, String> additionalDirs = [:]
    void configAction(Closure action) {
        configActions.add(action)
    }
    void filterAction(Closure action) {
        filterActions.add(action)
    }
    void additionalDir(Object dir, String prefix) {
        additionalDirs.put(project.file(dir), prefix)
    }
    @TaskAction
    void filterMcpConfig() {
        JarFilter.filterJar(project.file(project.downloadConfig.output), project.file('build/mcp_config.zip'), {
            if (filename == 'config.json') {
                def cfg = new JsonSlurper().parse(istream)
                for (def action : configActions) {
                    action.call(cfg)
                }
                return new GsonBuilder().setPrettyPrinting().create().toJson(cfg)
            } else {
                for (def action : filterActions) {
                    action.delegate = delegate
                    def newistream = action.call()
                    if (!istream.is(newistream))
                        return newistream
                }
                return istream
            }
        }, additionalDirs)
    }
    
    static def register(def proj) {
        if (!proj.ext.properties.containsKey('registered')) {
            proj.ext.registered = true
            proj.downloadConfig.output = proj.file('build/mcp_config_broken.zip')
            proj.task('fixMcpConfig', type: FixMcpConfigTask) {
                dependsOn proj.downloadConfig
                inputs.file proj.downloadConfig.output
                outputs.file 'build/mcp_config.zip'
            }
            proj.loadConfig.dependsOn proj.fixMcpConfig
        }
    }
}

// Fix https://github.com/MinecraftForge/ForgeGradle/issues/517 - FG3 server pipeline missing download version JSON
// Also fix recompile error on server due to mcp.client.Start being present
if (settings.pipeline == 'server') {
    project(':mcp') {
        FixMcpConfigTask.register(project)
        fixMcpConfig.configAction {cfg -> cfg.steps.server.add(1, [type: 'downloadJson'])}
        fixMcpConfig.filterAction {return filename.startsWith('config/inject/mcp/client/') ? null : istream}
    }
}

// Custom SRG
if (settings.customsrg != null) {
    project(':mcp') {
        FixMcpConfigTask.register(project)
        fixMcpConfig.configAction { cfg ->
            cfg.version = settings.mcversion
            cfg.data.mappings = 'config/' + settings.customsrg
        }
        fixMcpConfig.filterAction {
            if (filename == 'config/joined.tsrg') {
                filename = 'config/' + settings.customsrg
                return new FileInputStream(rootProject.file('conf/' + settings.customsrg))
            } else if (filename.startsWith('patches/') && !newFile) {
                return null
            } else {
                return istream
            }
        }
    }
}

// Custom patches
if (rootProject.file('conf/patches/').exists()) {
    project(':mcp') {
        FixMcpConfigTask.register(project)
        fixMcpConfig.additionalDir(rootProject.file('conf/patches/'), 'patches/' + settings.pipeline + '/')
    }
}

// Fix formatting, optionally
if (settings.reformat) {
    project(':mcp') {
        task reformat {
            dependsOn setupMCP
            inputs.file setupMCP.output
            outputs.file 'build/reformat/output.zip'
            doLast {
                def formatter = new ASFormatter()
                formatter.setUseProperInnerClassIndenting(false)
                def parser = new OptParser(formatter)
                parser.parseOptionFile(rootProject.file('conf/astyle.cfg'))
                
                JarFilter.filterJar(file(setupMCP.output), file('build/reformat/output.zip')) {
                    if (filename.endsWith('.java') && !filename.endsWith('package-info.java')) {
                        def sw = new StringWriter()
                        try {
                            formatter.format(new InputStreamReader(istream), sw)
                            return sw.toString()
                        } catch (RuntimeException e) {
                            println('Failed to reformat ' + filename)
                            return istream
                        }
                    } else {
                        return istream
                    }
                }
            }
        }
    }
    project(':clean') {
        applyPatches.dependsOn project(':mcp').reformat
        applyPatches.setClean(project(':mcp').file('build/reformat/output.zip'))
    }
}

// Fix reobf error due to local variable signatures (with lambdas?). Apply to both clean and modded to keep the classes the same
for (def projname : [':clean', ':' + settings.modname]) {
    project(projname) {
        def reobfArgs = []
        reobfJar.args.each {reobfArgs << it}
        reobfArgs << '--kill-lvt'
        reobfJar.args = reobfArgs.toArray([] as String[])
    }
}

// ===== RELEASE TASKS ===== //

// Utility class for dealing with @OnlyIn(Dist.XXX) using ASM
class DistUtils {
    static def containsUnwanted(List<AnnotationNode> anns, String unwanted) {
        if (anns == null)
            return false
        def annItr = anns.iterator()
        while (annItr.hasNext()) {
            def ann = annItr.next()
            if (ann.desc == 'Lnet/minecraftforge/api/distmarker/OnlyIn;') {
                annItr.remove()
                def value = ann.values[1]
                value = value[1]
                return value == unwanted
            }
        }
        return false
    }
    
    static def classUnwanted(InputStream istream, String unwanted) {
        ClassReader reader = new ClassReader(istream)
        ClassNode clazz = new ClassNode()
        reader.accept(clazz, ClassReader.SKIP_CODE)
        return containsUnwanted(clazz.visibleAnnotations, unwanted)
    }
    
    static def stripDist(InputStream istream, String unwanted, Closure classProvider) {
        ClassReader reader = new ClassReader(istream)
        ClassNode clazz = new ClassNode()
        reader.accept(clazz, 0)
        
        if (containsUnwanted(clazz.visibleAnnotations, unwanted))
            return null
        
        if (clazz.interfaces != null) {
            def itfItr = clazz.interfaces.iterator()
            while (itfItr.hasNext()) {
                def itf = classProvider.call(itfItr.next())
                if (itf != null && classUnwanted(itf, unwanted))
                    itfItr.remove()
            }
        }
        
        if (clazz.fields != null) {
            def fieldItr = clazz.fields.iterator()
            while (fieldItr.hasNext())
                if (containsUnwanted(fieldItr.next().visibleAnnotations, unwanted))
                    fieldItr.remove()
        }
        
        if (clazz.methods != null) {
            def methodItr = clazz.methods.iterator()
            while (methodItr.hasNext())
                if (containsUnwanted(methodItr.next().visibleAnnotations, unwanted))
                    methodItr.remove()
        }
        
        if (clazz.innerClasses != null) {
            def innerClassItr = clazz.innerClasses.iterator()
            while (innerClassItr.hasNext()) {
                def innerClass = innerClassItr.next()
                if (innerClass.outerName == clazz.name && classUnwanted(classProvider.call(innerClass.name), unwanted))
                    innerClassItr.remove()
            }
        }
        
        ClassWriter writer = new ClassWriter(0)
        clazz.accept(writer)
        return writer.toByteArray()
    }
}

// Tasks to split joined jars into client and server jars
if (settings.pipeline == 'joined') {
    for (def projname : [':clean', ':' + settings.modname]) {
        project(projname) {
            def dists = ['Client', 'Server']
            def unwantedDists = ['DEDICATED_SERVER', 'CLIENT']
            for (def i = 0; i < 2; i++) {
                def idx = i
                task('filterDist' + dists[idx]) {
                    dependsOn reobfJar
                    inputs.file reobfJar.output
                    outputs.file 'build/filterDist' + dists[idx] + '/output.zip'
                    doLast {
                        JarFilter.filterJar(file(reobfJar.output), file('build/filterDist' + dists[idx] + '/output.zip')) {
                            if (filename.endsWith('.class')) {
                                return DistUtils.stripDist(istream, unwantedDists[idx]) { classname ->
                                    def zip = new ZipFile(file(reobfJar.output))
                                    def entry = zip.getEntry(classname + '.class')
                                    if (entry == null)
                                        return null
                                    return zip.getInputStream(entry)
                                }
                            } else {
                                return istream
                            }
                        }
                    }
                }
            }
        }
    }
}

// Tasks to produce zip files containing only the changed classes, making the final release zip
for (def dist : ['Client', 'Server']) {
    task('createRelease' + dist, type: Zip) {
        def inputTaskName = settings.pipeline == 'joined' ? 'filterDist' + dist : 'reobfJar'
        def inputArchiveName = settings.pipeline == 'joined' ? 'output.zip' : 'output.jar'
        from(zipTree('projects/' + settings.modname + '/build/' + inputTaskName + '/' + inputArchiveName)) {
            eachFile { moddedFile ->
                def entryName = moddedFile.getRelativePath().toString()
                def vanillaJar = new ZipFile(file('projects/clean/build/' + inputTaskName + '/' + inputArchiveName))
                def vanillaEntry = vanillaJar.getEntry entryName
                if (vanillaEntry != null) {
                    def moddedJar = new ZipFile(file('projects/' + settings.modname + '/build/' + inputTaskName + '/' + inputArchiveName))
                    def moddedEntry = moddedJar.getEntry entryName
                    if (IOUtils.contentEquals(vanillaJar.getInputStream(vanillaEntry), moddedJar.getInputStream(moddedEntry)))
                        moddedFile.exclude()
                }
            }
        }
        
        archiveName = "${settings.modname}_${project.version}_${dist}.zip"
        destinationDir = file('build/distributions')
        includeEmptyDirs = false
        
        dependsOn ':clean:' + inputTaskName, ':' + settings.modname + ':' + inputTaskName
    }
}

// Master task to invoke all createRelease tasks
task createRelease {
    if (settings.pipeline != 'server')
        dependsOn createReleaseClient
    if (settings.pipeline != 'client')
        dependsOn createReleaseServer
}
