/*
 *     Copyright © 2016 cpw
 *     This file is part of Simpleretrogen.
 *
 *     Simpleretrogen is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     Simpleretrogen is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with Simpleretrogen.  If not, see <http://www.gnu.org/licenses/>.
 */

package cpw.mods.retro;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.MapMaker;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import net.minecraft.command.CommandBase;
import net.minecraft.command.CommandException;
import net.minecraft.command.ICommandSender;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.nbt.NBTTagString;
import net.minecraft.server.MinecraftServer;
import net.minecraft.util.text.TextComponentString;
import net.minecraft.util.math.ChunkPos;
import net.minecraft.world.World;
import net.minecraft.world.WorldServer;
import net.minecraft.world.chunk.Chunk;
import net.minecraft.world.chunk.IChunkGenerator;
import net.minecraft.world.chunk.IChunkProvider;
import net.minecraft.world.gen.ChunkProviderServer;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.config.Configuration;
import net.minecraftforge.common.config.Property;
import net.minecraftforge.event.world.ChunkDataEvent;
import net.minecraftforge.fml.common.FMLLog;
import net.minecraftforge.fml.common.IWorldGenerator;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.Mod.EventHandler;
import net.minecraftforge.fml.common.ObfuscationReflectionHelper;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import net.minecraftforge.fml.common.event.FMLServerAboutToStartEvent;
import net.minecraftforge.fml.common.event.FMLServerStartingEvent;
import net.minecraftforge.fml.common.event.FMLServerStoppedEvent;
import net.minecraftforge.fml.common.eventhandler.SubscribeEvent;
import net.minecraftforge.fml.common.gameevent.TickEvent;
import net.minecraftforge.fml.common.registry.GameRegistry;
import org.apache.logging.log4j.Level;

import javax.annotation.Nonnull;
import javax.annotation.ParametersAreNonnullByDefault;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Semaphore;

@Mod(modid="simpleretrogen", name="Simple Retrogen", acceptableRemoteVersions="*", acceptedMinecraftVersions = "[1.9,1.11)")
@ParametersAreNonnullByDefault
public class WorldRetrogen {
    private List<Marker> markers = Lists.newArrayList();
    private Map<String,TargetWorldWrapper> delegates;

    private Map<World,ListMultimap<ChunkPos,String>> pendingWork;
    private Map<World,ListMultimap<ChunkPos,String>> completedWork;

    private ConcurrentMap<World,Semaphore> completedWorkLocks;

    private int maxPerTick;
    private Map<String,String> retros = Maps.newHashMap();

    private static class Marker {
        private final String marker;
        private final Set<String> classes;

        Marker(String marker, Set<String> classes) {
            this.marker = marker;
            this.classes = classes;
        }
    }
    @EventHandler
    public void preInit(FMLPreInitializationEvent evt)
    {
        Configuration cfg = new Configuration(evt.getSuggestedConfigurationFile(), null, true);
        cfg.load();

        Property property = cfg.get(Configuration.CATEGORY_GENERAL, "maxPerTick", 100);
        property.setComment("Maximum number of retrogens to run in a single tick");
        this.maxPerTick = property.getInt(100);
        Property amProperty = cfg.get(Configuration.CATEGORY_GENERAL, "markerList", new String[0]);
        amProperty.setComment("Active markers");
        final List<String> activeMarkerList = Lists.newArrayList(amProperty.getStringList());
        Set<String> categories = cfg.getCategoryNames();

        if (categories.size() == 1) // only the general category - version 1 of config file
        {
            property = cfg.get(Configuration.CATEGORY_GENERAL, "worldGens", new String[0]);
            String[] retros = property.getStringList();
            property = cfg.get(Configuration.CATEGORY_GENERAL, "marker", "CPWRGMARK");
            Marker m = new Marker(property.getString(), Sets.newHashSet(retros));
            this.markers.add(m);
        }
        else
        {
            for (String marker : activeMarkerList) {
                if (categories.contains(marker)) {
                    final Property property1 = cfg.get(marker, "worldGens", new String[0]);
                    property1.setComment("World Generator classes for marker");
                    this.markers.add(new Marker(marker, Sets.newHashSet(property1.getStringList())));
                    cfg.getCategory(marker).setComment("Marker definition\nYou can create as many of these as you wish\nActivate by adding to active list");
                } else {
                    evt.getModLog().log(Level.INFO, "Ignoring missing marker definition for active marker %s", marker);
                }
            }
        }

        // clean up leftovers
        cfg.getCategory(Configuration.CATEGORY_GENERAL).remove("worldGens");
        cfg.getCategory(Configuration.CATEGORY_GENERAL).remove("marker");

        for (Marker m : markers) {
            for (String clz : m.classes) {
                if (retros.put(clz, m.marker) != null) {
                    evt.getModLog().log(Level.ERROR, "Configuration error, duplicate class for multiple markers found : %s", clz);
                }
            }
            if (!categories.contains(m.marker)) {
                Property p = cfg.get(m.marker, "worldGens",new String[0]);
                p.setComment("World Generator classes for marker");
                p.set(m.classes.toArray(new String[0]));
                cfg.getCategory(m.marker).setComment("Marker definition\nYou can create as many of these as you wish\nActivate by adding to active list");
                if (!activeMarkerList.contains(m.marker)) {
                    activeMarkerList.add(m.marker);
                    amProperty.set(activeMarkerList.toArray(new String[0]));
                }
            }
        }

        if (cfg.hasChanged())
        {
            cfg.save();
        }

        MinecraftForge.EVENT_BUS.register(this);
        MinecraftForge.EVENT_BUS.register(new LastTick());
        this.delegates = Maps.newHashMap();
    }

    @EventHandler
    public void serverStarting(FMLServerStartingEvent evt) {
        evt.registerServerCommand(new CommandBase()
        {
            @Override
            @Nonnull
            public String getCommandName()
            {
                return "listretrogenclasstargets";
            }

            @Override
            @Nonnull
            public String getCommandUsage(ICommandSender sender)
            {
                return "List retrogens";
            }

            @Override
            public int getRequiredPermissionLevel()
            {
                return 0;
            }

            @Override
            public void execute(MinecraftServer server, ICommandSender sender, String[] args) throws CommandException
            {
                Set<IWorldGenerator> worldGens = ObfuscationReflectionHelper.getPrivateValue(GameRegistry.class, null, "worldGenerators");
                List<String> targets = Lists.newArrayList();
                for (IWorldGenerator worldGen : worldGens) {
                    if (!(worldGen instanceof TargetWorldWrapper))
                    {
                        targets.add(worldGen.getClass().getName());
                    }
                }
                if (targets.isEmpty()) {
                    sender.addChatMessage(new TextComponentString("There are no retrogen target classes"));
                } else {
                    sender.addChatMessage(new TextComponentString(CommandBase.joinNiceStringFromCollection(targets)));
                }
            }
        });
    }
    @EventHandler
    public void serverAboutToStart(FMLServerAboutToStartEvent evt)
    {
        this.pendingWork = new MapMaker().weakKeys().makeMap();
        this.completedWork = new MapMaker().weakKeys().makeMap();
        this.completedWorkLocks = new MapMaker().weakKeys().makeMap();

        Set<IWorldGenerator> worldGens = ObfuscationReflectionHelper.getPrivateValue(GameRegistry.class, null, "worldGenerators");
        Map<IWorldGenerator,Integer> worldGenIdx = ObfuscationReflectionHelper.getPrivateValue(GameRegistry.class, null, "worldGeneratorIndex");

        for (String retro : ImmutableSet.copyOf(retros.keySet()))
        {
            if (!delegates.containsKey(retro))
            {
                FMLLog.info("Substituting worldgenerator %s with delegate", retro);
                for (Iterator<IWorldGenerator> iterator = worldGens.iterator(); iterator.hasNext();)
                {
                    IWorldGenerator wg = iterator.next();
                    if (wg.getClass().getName().equals(retro))
                    {
                        iterator.remove();
                        TargetWorldWrapper tww = new TargetWorldWrapper();
                        tww.delegate = wg;
                        tww.tag = retro;
                        worldGens.add(tww);
                        Integer idx = worldGenIdx.remove(wg);
                        worldGenIdx.put(tww, idx);
                        FMLLog.info("Successfully substituted %s with delegate", retro);
                        delegates.put(retro, tww);
                        break;
                    }
                }

                if (!delegates.containsKey(retro))
                {
                    FMLLog.warning("WorldRetrogen was not able to locate world generator class %s, it will be skipped, found %s", retro, worldGens);
                    retros.remove(retro);
                }
            }
        }
    }

    @EventHandler
    public void serverStopped(FMLServerStoppedEvent evt)
    {
        Set<IWorldGenerator> worldGens = ObfuscationReflectionHelper.getPrivateValue(GameRegistry.class, null, "worldGenerators");
        Map<IWorldGenerator,Integer> worldGenIdx = ObfuscationReflectionHelper.getPrivateValue(GameRegistry.class, null, "worldGeneratorIndex");

        for (TargetWorldWrapper tww : delegates.values())
        {
            worldGens.remove(tww);
            Integer idx = worldGenIdx.remove(tww);
            worldGens.add(tww.delegate);
            worldGenIdx.put(tww.delegate,idx);
        }

        delegates.clear();
    }

    private Semaphore getSemaphoreFor(World w)
    {
        completedWorkLocks.putIfAbsent(w, new Semaphore(1));
        return completedWorkLocks.get(w);
    }

    private class LastTick {
        private int counter = 0;
        @SubscribeEvent
        public void tickStart(TickEvent.WorldTickEvent tick)
        {
            World w = tick.world;
            if (!(w instanceof WorldServer))
            {
                return;
            }
            if (tick.phase == TickEvent.Phase.START)
            {
                counter = 0;
                getSemaphoreFor(w);
            }
            else
            {
                ListMultimap<ChunkPos, String> pending = pendingWork.get(w);
                if (pending == null)
                {
                    return;
                }
                ImmutableList<Entry<ChunkPos, String>> forProcessing = ImmutableList.copyOf(Iterables.limit(pending.entries(), maxPerTick + 1));
                for (Entry<ChunkPos, String> entry : forProcessing)
                {
                    if (counter++ > maxPerTick)
                    {
                        FMLLog.fine("Completed %d retrogens this tick. There are %d left for world %s", counter, pending.size(), w.getWorldInfo().getWorldName());
                        return;
                    }
                    runRetrogen((WorldServer)w, entry.getKey(), entry.getValue());
                }
            }
        }
    }

    private class TargetWorldWrapper implements IWorldGenerator {
        private IWorldGenerator delegate;
        private String tag;

        @Override
        public void generate(Random random, int chunkX, int chunkZ, World world, IChunkGenerator chunkGenerator, IChunkProvider chunkProvider)
        {
            FMLLog.fine("Passing generation for %s through to underlying generator", tag);
            delegate.generate(random, chunkX, chunkZ, world, chunkGenerator, chunkProvider);
            ChunkPos chunkCoordIntPair = new ChunkPos(chunkX, chunkZ);
            completeRetrogen(chunkCoordIntPair, world, tag);
        }
    }

    @SubscribeEvent
    public void onChunkLoad(ChunkDataEvent.Load chunkevt)
    {
        World w = chunkevt.getWorld();
        if (!(w instanceof WorldServer))
        {
            return;
        }
        getSemaphoreFor(w);

        Chunk chk = chunkevt.getChunk();
        Set<String> existingGens = Sets.newHashSet();
        NBTTagCompound data = chunkevt.getData();
        for (Marker m : markers)
        {
            NBTTagCompound marker = data.getCompoundTag(m.marker);
            NBTTagList tagList = marker.getTagList("list", 8);
            for (int i = 0; i < tagList.tagCount(); i++)
            {
                existingGens.add(tagList.getStringTagAt(i));
            }

            SetView<String> difference = Sets.difference(m.classes, existingGens);
            for (String retro : difference)
            {
                if (retros.containsKey(retro))
                {
                    queueRetrogen(retro, w, chk.getChunkCoordIntPair());
                }
            }
        }

        for (String retro : existingGens)
        {
            completeRetrogen(chk.getChunkCoordIntPair(), w, retro);
        }
    }

    @SubscribeEvent
    public void onChunkSave(ChunkDataEvent.Save chunkevt)
    {
        World w = chunkevt.getWorld();
        if (!(w instanceof WorldServer))
        {
            return;
        }
        getSemaphoreFor(w).acquireUninterruptibly();
        try
        {
            if (completedWork.containsKey(w))
            {
                ListMultimap<ChunkPos, String> doneChunks = completedWork.get(w);
                List<String> retroClassList = doneChunks.get(chunkevt.getChunk().getChunkCoordIntPair());
                if (retroClassList.isEmpty())
                    return;
                NBTTagCompound data = chunkevt.getData();
                for (String retroClass : retroClassList)
                {
                    String marker = retros.get(retroClass);
                    if (marker == null)
                    {
                        FMLLog.log(Level.DEBUG, "Encountered retrogen class %s with no existing marker, removing from chunk. You probably removed it from the active configuration", retroClass);
                        continue;
                    }
                    NBTTagList lst;
                    if (data.hasKey(marker)) {
                        lst = data.getCompoundTag(marker).getTagList("list", 8);
                    } else {
                        NBTTagCompound retro = new NBTTagCompound();
                        lst = new NBTTagList();
                        retro.setTag("list", lst);
                        data.setTag(marker, retro);
                    }
                    lst.appendTag(new NBTTagString(retroClass));
                }
            }
        }
        finally
        {
            getSemaphoreFor(w).release();
        }
    }

    private void queueRetrogen(String retro, World world, ChunkPos chunkCoords)
    {
        if (world instanceof WorldServer)
        {
            ListMultimap<ChunkPos, String> currentWork = pendingWork.get(world);
            if (currentWork == null)
            {
                currentWork = ArrayListMultimap.create();
                pendingWork.put(world, currentWork);
            }

            currentWork.put(chunkCoords, retro);
        }
    }
    private void completeRetrogen(ChunkPos chunkCoords, World world, String retroClass)
    {
        ListMultimap<ChunkPos, String> pendingMap = pendingWork.get(world);
        if (pendingMap != null && pendingMap.containsKey(chunkCoords))
        {
            pendingMap.remove(chunkCoords, retroClass);
        }

        getSemaphoreFor(world).acquireUninterruptibly();
        try
        {
            ListMultimap<ChunkPos, String> completedMap = completedWork.get(world);
            if (completedMap == null)
            {
                completedMap = ArrayListMultimap.create();
                completedWork.put(world, completedMap);
            }

            completedMap.put(chunkCoords, retroClass);
        }
        finally
        {
            getSemaphoreFor(world).release();
        }
    }

    private void runRetrogen(WorldServer world, ChunkPos chunkCoords, String retroClass)
    {
        long worldSeed = world.getSeed();
        Random fmlRandom = new Random(worldSeed);
        long xSeed = fmlRandom.nextLong() >> 2 + 1L;
        long zSeed = fmlRandom.nextLong() >> 2 + 1L;
        long chunkSeed = (xSeed * chunkCoords.chunkXPos + zSeed * chunkCoords.chunkZPos) ^ worldSeed;

        fmlRandom.setSeed(chunkSeed);
        ChunkProviderServer providerServer = world.getChunkProvider();
        IChunkGenerator generator = ObfuscationReflectionHelper.getPrivateValue(ChunkProviderServer.class, providerServer, "field_186029_c", "chunkGenerator");
        delegates.get(retroClass).delegate.generate(fmlRandom, chunkCoords.chunkXPos, chunkCoords.chunkZPos, world, generator, providerServer);
        FMLLog.fine("Retrogenerated chunk for %s", retroClass);
        completeRetrogen(chunkCoords, world, retroClass);
    }
}