package fi.dy.masa.litematica.materials;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.IdentityHashMap;
import java.util.Map;
import javax.annotation.Nullable;
import com.google.common.collect.ImmutableList;
import net.minecraft.block.Block;
import net.minecraft.block.BlockBed;
import net.minecraft.block.BlockDoor;
import net.minecraft.block.BlockDoublePlant;
import net.minecraft.block.BlockFlowerPot;
import net.minecraft.block.BlockFlowerPot.EnumFlowerType;
import net.minecraft.block.BlockLiquid;
import net.minecraft.block.BlockSlab;
import net.minecraft.block.BlockSnow;
import net.minecraft.block.state.IBlockState;
import net.minecraft.client.Minecraft;
import net.minecraft.init.Blocks;
import net.minecraft.init.Items;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.CompressedStreamTools;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.nbt.NBTUtil;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Vec3i;
import net.minecraft.world.EnumDifficulty;
import net.minecraft.world.GameType;
import net.minecraft.world.World;
import net.minecraft.world.WorldSettings;
import net.minecraft.world.WorldType;
import fi.dy.masa.litematica.Litematica;
import fi.dy.masa.litematica.Reference;
import fi.dy.masa.litematica.util.WorldUtils;
import fi.dy.masa.litematica.world.WorldSchematic;
import fi.dy.masa.malilib.util.Constants;
import fi.dy.masa.malilib.util.FileUtils;

public class MaterialCache
{
    private static final MaterialCache INSTANCE = new MaterialCache();

    protected final IdentityHashMap<IBlockState, ItemStack> buildItemsForStates = new IdentityHashMap<>();
    protected final IdentityHashMap<IBlockState, ItemStack> displayItemsForStates = new IdentityHashMap<>();
    protected final WorldSchematic tempWorld;
    protected final BlockPos checkPos;
    protected boolean hasReadFromFile;
    protected boolean dirty;

    private MaterialCache()
    {
        WorldSettings settings = new WorldSettings(0L, GameType.CREATIVE, false, false, WorldType.FLAT);

        this.tempWorld = new WorldSchematic(null, settings, -1, EnumDifficulty.PEACEFUL, Minecraft.getMinecraft().profiler);
        this.checkPos = new BlockPos(8, 0, 8);

        WorldUtils.loadChunksClientWorld(this.tempWorld, this.checkPos, new Vec3i(1, 1, 1));
    }

    public static MaterialCache getInstance()
    {
        /*
        if (INSTANCE.hasReadFromFile == false)
        {
            INSTANCE.readFromFile();
        }
        */

        return INSTANCE;
    }

    public void clearCache()
    {
        this.buildItemsForStates.clear();
    }

    public ItemStack getRequiredBuildItemForState(IBlockState state)
    {
        return this.getRequiredBuildItemForState(state, this.tempWorld, this.checkPos);
    }

    public ItemStack getRequiredBuildItemForState(IBlockState state, World world, BlockPos pos)
    {
        ItemStack stack = this.buildItemsForStates.get(state);

        if (stack == null)
        {
            stack = this.getItemForStateFromWorld(state, world, pos, true);
        }

        return stack;
    }

    public ItemStack getItemForDisplayNameForState(IBlockState state)
    {
        ItemStack stack = this.displayItemsForStates.get(state);

        if (stack == null)
        {
            stack = this.getItemForStateFromWorld(state, this.tempWorld, this.checkPos, false);
        }

        return stack;
    }

    protected ItemStack getItemForStateFromWorld(IBlockState state, World world, BlockPos pos, boolean isBuildItem)
    {
        ItemStack stack = isBuildItem ? this.getStateToItemOverride(state) : null;

        if (stack == null)
        {
            world.setBlockState(pos, state, 0x14);
            stack = state.getBlock().getItem(world, pos, state);
        }

        if (stack == null || stack.isEmpty())
        {
            stack = ItemStack.EMPTY;
        }
        else
        {
            this.overrideStackSize(state, stack);
        }

        if (isBuildItem)
        {
            this.buildItemsForStates.put(state, stack);
        }
        else
        {
            this.displayItemsForStates.put(state, stack);
        }

        this.dirty = true;

        return stack;
    }

    public boolean requiresMultipleItems(IBlockState state)
    {
        Block block = state.getBlock();

        if (block == Blocks.FLOWER_POT && state.getValue(BlockFlowerPot.CONTENTS) != BlockFlowerPot.EnumFlowerType.EMPTY)
        {
            return true;
        }

        return false;
    }

    public ImmutableList<ItemStack> getItems(IBlockState state)
    {
        return this.getItems(state, this.tempWorld, this.checkPos);
    }

    public ImmutableList<ItemStack> getItems(IBlockState state, World world, BlockPos pos)
    {
        Block block = state.getBlock();

        if (block == Blocks.FLOWER_POT && state.getValue(BlockFlowerPot.CONTENTS) != BlockFlowerPot.EnumFlowerType.EMPTY)
        {
            // Nice & clean >_>
            EnumFlowerType type = state.getValue(BlockFlowerPot.CONTENTS);
            ItemStack plant = null;

            switch (type)
            {
                case ACACIA_SAPLING:    plant = new ItemStack(Blocks.SAPLING, 1, 4); break;
                case ALLIUM:            plant = new ItemStack(Blocks.RED_FLOWER, 1, 2); break;
                case BIRCH_SAPLING:     plant = new ItemStack(Blocks.SAPLING, 1, 2); break;
                case BLUE_ORCHID:       plant = new ItemStack(Blocks.RED_FLOWER, 1, 1); break;
                case CACTUS:            plant = new ItemStack(Blocks.CACTUS, 1, 0); break;
                case DANDELION:         plant = new ItemStack(Blocks.YELLOW_FLOWER, 1, 0); break;
                case DARK_OAK_SAPLING:  plant = new ItemStack(Blocks.SAPLING, 1, 5); break;
                case DEAD_BUSH:         plant = new ItemStack(Blocks.DEADBUSH, 1, 0); break;
                case FERN:              plant = new ItemStack(Blocks.TALLGRASS, 1, 2); break;
                case HOUSTONIA:         plant = new ItemStack(Blocks.RED_FLOWER, 1, 3); break;
                case JUNGLE_SAPLING:    plant = new ItemStack(Blocks.SAPLING, 1, 3); break;
                case MUSHROOM_BROWN:    plant = new ItemStack(Blocks.BROWN_MUSHROOM, 1, 0); break;
                case MUSHROOM_RED:      plant = new ItemStack(Blocks.RED_MUSHROOM, 1, 0); break;
                case OAK_SAPLING:       plant = new ItemStack(Blocks.SAPLING, 1, 0); break;
                case ORANGE_TULIP:      plant = new ItemStack(Blocks.RED_FLOWER, 1, 5); break;
                case OXEYE_DAISY:       plant = new ItemStack(Blocks.RED_FLOWER, 1, 8); break;
                case PINK_TULIP:        plant = new ItemStack(Blocks.RED_FLOWER, 1, 7); break;
                case POPPY:             plant = new ItemStack(Blocks.RED_FLOWER, 1, 0); break;
                case RED_TULIP:         plant = new ItemStack(Blocks.RED_FLOWER, 1, 4); break;
                case SPRUCE_SAPLING:    plant = new ItemStack(Blocks.SAPLING, 1, 1); break;
                case WHITE_TULIP:       plant = new ItemStack(Blocks.RED_FLOWER, 1, 6); break;
                default:
            }

            if (plant != null)
            {
                return ImmutableList.of(new ItemStack(Items.FLOWER_POT), plant);
            }
        }

        return ImmutableList.of(this.getRequiredBuildItemForState(state, world, pos));
    }

    @Nullable
    protected ItemStack getStateToItemOverride(IBlockState state)
    {
        Block block = state.getBlock();

        if (block == Blocks.PISTON_HEAD ||
            block == Blocks.PISTON_EXTENSION ||
            block == Blocks.PORTAL ||
            block == Blocks.END_PORTAL ||
            block == Blocks.END_GATEWAY)
        {
            return ItemStack.EMPTY;
        }
        else if (block == Blocks.FARMLAND)
        {
            return new ItemStack(Blocks.DIRT);
        }
        else if (block == Blocks.GRASS_PATH)
        {
            return new ItemStack(Blocks.GRASS);
        }
        else if (block == Blocks.BROWN_MUSHROOM_BLOCK)
        {
            return new ItemStack(Blocks.BROWN_MUSHROOM_BLOCK);
        }
        else if (block == Blocks.RED_MUSHROOM_BLOCK)
        {
            return new ItemStack(Blocks.RED_MUSHROOM_BLOCK);
        }
        else if (block == Blocks.LAVA)
        {
            if (state.getValue(BlockLiquid.LEVEL) == 0)
            {
                return new ItemStack(Items.LAVA_BUCKET);
            }
            else
            {
                return ItemStack.EMPTY;
            }
        }
        else if (block == Blocks.WATER)
        {
            if (state.getValue(BlockLiquid.LEVEL) == 0)
            {
                return new ItemStack(Items.WATER_BUCKET);
            }
            else
            {
                return ItemStack.EMPTY;
            }
        }
        else if (block instanceof BlockDoor && state.getValue(BlockDoor.HALF) == BlockDoor.EnumDoorHalf.UPPER)
        {
            return ItemStack.EMPTY;
        }
        else if (block instanceof BlockBed && state.getValue(BlockBed.PART) == BlockBed.EnumPartType.HEAD)
        {
            return ItemStack.EMPTY;
        }
        else if (block instanceof BlockDoublePlant && state.getValue(BlockDoublePlant.HALF) == BlockDoublePlant.EnumBlockHalf.UPPER)
        {
            return ItemStack.EMPTY;
        }

        return null;
    }

    protected void overrideStackSize(IBlockState state, ItemStack stack)
    {
        if (state.getBlock() instanceof BlockSlab && ((BlockSlab) state.getBlock()).isDouble())
        {
            stack.setCount(2);
        }
        else if (state.getBlock() == Blocks.SNOW_LAYER)
        {
            stack.setCount(state.getValue(BlockSnow.LAYERS));
        }
    }

    protected NBTTagCompound writeToNBT()
    {
        NBTTagCompound nbt = new NBTTagCompound();

        nbt.setTag("MaterialCache", this.writeMapToNBT(this.buildItemsForStates));
        nbt.setTag("DisplayMaterialCache", this.writeMapToNBT(this.displayItemsForStates));

        return nbt;
    }

    protected NBTTagList writeMapToNBT(IdentityHashMap<IBlockState, ItemStack> map)
    {
        NBTTagList list = new NBTTagList();

        for (Map.Entry<IBlockState, ItemStack> entry : map.entrySet())
        {
            NBTTagCompound tag = new NBTTagCompound();
            NBTTagCompound stateTag = new NBTTagCompound();
            NBTUtil.writeBlockState(stateTag, entry.getKey());

            tag.setTag("Block", stateTag);
            tag.setTag("Item", entry.getValue().writeToNBT(new NBTTagCompound()));

            list.appendTag(tag);
        }

        return list;
    }

    protected void readFromNBT(NBTTagCompound nbt)
    {
        this.buildItemsForStates.clear();
        this.displayItemsForStates.clear();

        this.readMapFromNBT(nbt, "MaterialCache", this.buildItemsForStates);
        this.readMapFromNBT(nbt, "DisplayMaterialCache", this.displayItemsForStates);
    }

    protected void readMapFromNBT(NBTTagCompound nbt, String tagName, IdentityHashMap<IBlockState, ItemStack> map)
    {
        if (nbt.hasKey(tagName, Constants.NBT.TAG_LIST))
        {
            NBTTagList list = nbt.getTagList(tagName, Constants.NBT.TAG_COMPOUND);
            final int count = list.tagCount();

            for (int i = 0; i < count; ++i)
            {
                NBTTagCompound tag = list.getCompoundTagAt(i);

                if (tag.hasKey("Block", Constants.NBT.TAG_COMPOUND) &&
                    tag.hasKey("Item", Constants.NBT.TAG_COMPOUND))
                {
                    IBlockState state = NBTUtil.readBlockState(tag.getCompoundTag("Block"));

                    if (state != null)
                    {
                        ItemStack stack = new ItemStack(tag.getCompoundTag("Item"));
                        this.buildItemsForStates.put(state, stack);
                    }
                }
            }
        }
    }

    protected File getCacheDir()
    {
        return new File(FileUtils.getConfigDirectory(), Reference.MOD_ID);
    }

    protected File getCacheFile()
    {
        return new File(this.getCacheDir(), "material_cache.nbt");
    }

    public boolean writeToFile()
    {
        if (this.dirty == false)
        {
            return false;
        }

        File dir = this.getCacheDir();
        File file = this.getCacheFile();

        try
        {
            if (dir.exists() == false && dir.mkdirs() == false)
            {
                Litematica.logger.warn("Failed to write the material list cache to file '{}'", file.getAbsolutePath());
                return false;
            }

            FileOutputStream os = new FileOutputStream(file);
            CompressedStreamTools.writeCompressed(this.writeToNBT(), os);
            os.close();
            this.dirty = false;

            return true;
        }
        catch (Exception e)
        {
            Litematica.logger.warn("Failed to write the material list cache to file '{}'", file.getAbsolutePath(), e);
        }

        return false;
    }

    public void readFromFile()
    {
        File file = this.getCacheFile();

        if (file.exists() == false || file.canRead() == false)
        {
            return;
        }

        try
        {
            FileInputStream is = new FileInputStream(file);
            NBTTagCompound nbt = CompressedStreamTools.readCompressed(is);
            is.close();

            if (nbt != null)
            {
                this.readFromNBT(nbt);
                this.hasReadFromFile = true;
                this.dirty = false;
            }
        }
        catch (Exception e)
        {
            Litematica.logger.warn("Failed to read the material list cache from file '{}'", file.getAbsolutePath(), e);
        }
    }
}