package net.minecraftforge.gradle.tasks.dev;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import net.minecraftforge.gradle.delayed.DelayedFile;
import net.minecraftforge.srg2source.util.io.FolderSupplier;
import net.minecraftforge.srg2source.util.io.InputSupplier;
import net.minecraftforge.srg2source.util.io.ZipInputSupplier;

import org.gradle.api.DefaultTask;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileTree;
import org.gradle.api.file.FileVisitDetails;
import org.gradle.api.file.FileVisitor;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.TaskAction;

import com.cloudbees.diff.Diff;
import com.cloudbees.diff.Hunk;
import com.cloudbees.diff.PatchException;
import com.google.common.base.Charsets;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;

public class GeneratePatches extends DefaultTask
{
    @OutputDirectory
    DelayedFile patchDir;

    @InputFiles
    DelayedFile changed;

    @InputFiles
    DelayedFile original;

    @Input
    String originalPrefix = "";
    
    @Input
    String changedPrefix = "";

    private Set<File> created = new HashSet<File>();

    @TaskAction
    public void doTask() throws IOException, PatchException
    {
        created.clear();
        getPatchDir().mkdirs();

        // fix and create patches.
        processFiles(getSupplier(changed.call()), getSupplier(original.call()));
        
        removeOld(getPatchDir());
    }

    private InputSupplier getSupplier(File file) throws IOException
    {
        if (file.isDirectory())
            return new FolderSupplier(file);

        ZipInputSupplier ret = new ZipInputSupplier();
        ret.readZip(file);
        return ret;
    }

    private void removeOld(File dir) throws IOException
    {
        final ArrayList<File> directories = new ArrayList<File>();
        FileTree tree = getProject().fileTree(dir);

        tree.visit(new FileVisitor()
        {
            @Override
            public void visitDir(FileVisitDetails dir)
            {
                directories.add(dir.getFile());
            }

            @Override
            public void visitFile(FileVisitDetails f)
            {
                File file = f.getFile();
                if (!created.contains(file))
                {
                    getLogger().debug("Removed patch: " + f.getRelativePath());
                    file.delete();
                }
            }
        });

        // We want things sorted in reverse order. Do that sub folders come before parents
        Collections.sort(directories, new Comparator<File>()
        {
            @Override
            public int compare(File o1, File o2)
            {
                int r = o1.compareTo(o2);
                if (r < 0) return  1;
                if (r > 0) return -1;
                return 0;
            }
        });

        for (File f : directories)
        {
            if (f.listFiles().length == 0)
            {
                getLogger().debug("Removing empty dir: " + f);
                f.delete();
            }
        }
    }

    public void processFiles(InputSupplier original, InputSupplier changed) throws IOException
    {
        List<String> paths = original.gatherAll("");
        for (String path : paths)
        {
            InputStream o = original.getInput(path);
            InputStream c = changed.getInput(path);
            try
            {
                processFile(path, o, c);
            }
            finally
            {
                if (o != null) o.close();
                if (c != null) c.close();
            }
        }
    }

    public void processFile(String relative, InputStream original, InputStream changed) throws IOException
    {
        getLogger().debug("Diffing: " + relative);

        File patchFile = new File(getPatchDir(), relative + ".patch");

        if (changed == null)
        {
            getLogger().debug("    Changed File does not exist");
            return;
        }

        // We have to cache the bytes because diff reads the stream twice.. why.. who knows.
        byte[] oData = ByteStreams.toByteArray(original);
        byte[] cData = ByteStreams.toByteArray(changed);

        Diff diff = Diff.diff(new InputStreamReader(new ByteArrayInputStream(oData), Charsets.UTF_8), new InputStreamReader(new ByteArrayInputStream(cData), Charsets.UTF_8), false);

        if (!relative.startsWith("/"))
            relative = "/" + relative;

        if (!diff.isEmpty())
        {
            String unidiff = diff.toUnifiedDiff(originalPrefix + relative, changedPrefix + relative, 
                    new InputStreamReader(new ByteArrayInputStream(oData), Charsets.UTF_8), 
                    new InputStreamReader(new ByteArrayInputStream(cData), Charsets.UTF_8), 3);
            unidiff = unidiff.replace("\r\n", "\n"); //Normalize lines
            unidiff = unidiff.replace("\n" + Hunk.ENDING_NEWLINE + "\n", "\n"); //We give 0 shits about this.

            String olddiff = "";
            if (patchFile.exists())
            {
                olddiff = Files.toString(patchFile, Charsets.UTF_8);
            }

            if (!olddiff.equals(unidiff))
            {
                getLogger().debug("Writing patch: " + patchFile);
                patchFile.getParentFile().mkdirs();
                Files.touch(patchFile);
                Files.write(unidiff, patchFile, Charsets.UTF_8);
            }
            else
            {
                getLogger().debug("Patch did not change");
            }
            created.add(patchFile);
        }
    }

    public FileCollection getChanged()
    {
        File f = changed.call();
        if (f.isDirectory())
            return getProject().fileTree(f);
        else
            return getProject().files(f);
    }

    public void setChanged(DelayedFile changed)
    {
        this.changed = changed;
    }

    public FileCollection getOriginal()
    {
        File f = original.call();
        if (f.isDirectory())
            return getProject().fileTree(f);
        else
            return getProject().files(f);
    }

    public void setOriginal(DelayedFile original)
    {
        this.original = original;
    }

    public File getPatchDir()
    {
        return patchDir.call();
    }

    public void setPatchDir(DelayedFile patchDir)
    {
        this.patchDir = patchDir;
    }

    public String getOriginalPrefix()
    {
        return originalPrefix;
    }

    public void setOriginalPrefix(String originalPrefix)
    {
        this.originalPrefix = originalPrefix;
    }

    public String getChangedPrefix()
    {
        return changedPrefix;
    }

    public void setChangedPrefix(String changedPrefix)
    {
        this.changedPrefix = changedPrefix;
    }
}

@@ -1706,23 +1706,24 @@
 upplier(
-changed
+original
 .call())
@@ -1732,32 +1732,31 @@
 getSupplier(
-original
+changed
 .call()));%0A 

Fix accedential inversion of changed/original in GenPatches.