package fi.dy.masa.enderutilities.client.renderer.model.block;

import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import javax.annotation.Nullable;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import net.minecraft.block.state.IBlockState;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.renderer.block.model.IBakedModel;
import net.minecraft.client.renderer.block.model.ItemCameraTransforms;
import net.minecraft.client.renderer.block.model.ItemOverrideList;
import net.minecraft.client.renderer.block.model.ModelResourceLocation;
import net.minecraft.client.renderer.block.statemap.StateMapperBase;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.renderer.vertex.VertexFormat;
import net.minecraft.client.resources.IResourceManager;
import net.minecraft.init.Blocks;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.client.model.ICustomModelLoader;
import net.minecraftforge.client.model.IModel;
import net.minecraftforge.client.model.ModelLoaderRegistry;
import net.minecraftforge.common.model.IModelState;
import net.minecraftforge.common.model.TRSRTransformation;
import net.minecraftforge.common.property.IExtendedBlockState;
import fi.dy.masa.enderutilities.EnderUtilities;
import fi.dy.masa.enderutilities.block.BlockBarrel;
import fi.dy.masa.enderutilities.block.base.BlockEnderUtilitiesTileEntity;
import fi.dy.masa.enderutilities.reference.Reference;
import fi.dy.masa.enderutilities.registry.EnderUtilitiesBlocks;
import fi.dy.masa.enderutilities.util.PositionUtils;

public class BakedModelBarrel implements IBakedModel
{
    private static final Map<Optional<IBlockState>, ImmutableMap<Optional<EnumFacing>, ImmutableList<BakedQuad>>> QUAD_CACHE_NORMAL = new HashMap<>();
    private static final Map<Optional<IBlockState>, ImmutableMap<Optional<EnumFacing>, ImmutableList<BakedQuad>>> QUAD_CACHE_CAMO = new HashMap<>();
    private static final ImmutableList<BakedQuad> EMPTY_QUADS = ImmutableList.<BakedQuad>of();
    private final ImmutableMap<String, String> textures;
    private final IModel baseModel;
    private final IModel overlayModel;
    private final VertexFormat format;
    private final IBakedModel bakedBaseModel;
    private final Function<ResourceLocation, TextureAtlasSprite> bakedTextureGetter;
    private final TextureAtlasSprite particle;
    private final ImmutableList<BakedQuad> itemQuads;

    private BakedModelBarrel(IModel baseModel, IModel overlayModel, IModelState state, VertexFormat format,
            Function<ResourceLocation, TextureAtlasSprite> bakedTextureGetter, ImmutableMap<String, String> textures)
    {
        IBlockState defaultState = EnderUtilitiesBlocks.BARREL.getDefaultState().withProperty(BlockBarrel.LABEL_FRONT, true);
        this.baseModel = baseModel.retexture(textures);
        this.overlayModel = overlayModel.retexture(textures);
        this.bakedBaseModel = this.baseModel.retexture(this.getTexturesBaseModel(defaultState, textures)).bake(state, format, bakedTextureGetter);
        this.format = format;
        this.bakedTextureGetter = bakedTextureGetter;
        this.textures = textures;
        String particleName = this.textures.get("particle");
        this.particle = particleName != null ? bakedTextureGetter.apply(new ResourceLocation(particleName)) : this.bakedBaseModel.getParticleTexture();
        this.itemQuads = BakedModelCamouflageBlock.buildItemModel(this.bakedBaseModel, null);
    }

    @Override
    public boolean isAmbientOcclusion()
    {
        return this.bakedBaseModel.isAmbientOcclusion();
    }

    @Override
    public boolean isGui3d()
    {
        return this.bakedBaseModel.isGui3d();
    }

    @Override
    public boolean isBuiltInRenderer()
    {
        return this.bakedBaseModel.isBuiltInRenderer();
    }

    @SuppressWarnings("deprecation")
    @Override
    public ItemCameraTransforms getItemCameraTransforms()
    {
        return this.bakedBaseModel.getItemCameraTransforms();
    }

    @Override
    public ItemOverrideList getOverrides()
    {
        return this.bakedBaseModel.getOverrides();
    }

    @Override
    public TextureAtlasSprite getParticleTexture()
    {
        return this.particle;
    }

    @Override
    synchronized public List<BakedQuad> getQuads(@Nullable IBlockState state, @Nullable EnumFacing side, long rand)
    {
        // Item model
        if (state == null)
        {
            return this.itemQuads;
        }

        IExtendedBlockState extendedState = (IExtendedBlockState) state;
        IBlockState actualState = extendedState.getClean();
        IBlockState camoState = extendedState.getValue(BlockEnderUtilitiesTileEntity.CAMOBLOCKSTATE);
        boolean validCamo = camoState != null && camoState.getBlock() != Blocks.AIR;

        Optional<IBlockState> key = Optional.of(actualState);
        Map<Optional<IBlockState>, ImmutableMap<Optional<EnumFacing>, ImmutableList<BakedQuad>>> cache = validCamo ? QUAD_CACHE_CAMO : QUAD_CACHE_NORMAL;
        ImmutableMap<Optional<EnumFacing>, ImmutableList<BakedQuad>> map = cache.get(key);

        if (map == null)
        {
            IBakedModel bakedModel = validCamo ? this.getBakedOverlayModel(actualState) : this.getBakedBaseModel(actualState);
            map = this.getQuadsForState(bakedModel, extendedState, rand, validCamo);
            cache.put(key, map);
        }

        return map.get(Optional.ofNullable(side));
    }

    private IBakedModel getBakedBaseModel(IBlockState actualState)
    {
        IModel model = this.baseModel.retexture(this.getTexturesBaseModel(actualState, this.textures));
        return model.bake(TRSRTransformation.from(actualState.getValue(BlockBarrel.FACING_H)), this.format, this.bakedTextureGetter);
    }

    private IBakedModel getBakedOverlayModel(IBlockState actualState)
    {
        return this.overlayModel.bake(TRSRTransformation.from(actualState.getValue(BlockBarrel.FACING_H)), this.format, this.bakedTextureGetter);
    }

    private ImmutableMap<Optional<EnumFacing>, ImmutableList<BakedQuad>> getQuadsForState(
            IBakedModel bakedModel, IExtendedBlockState extendedState, long rand, boolean validCamo)
    {
        ImmutableMap.Builder<Optional<EnumFacing>, ImmutableList<BakedQuad>> mapBuilder = ImmutableMap.builder();

        for (EnumFacing side : EnumFacing.values())
        {
            ImmutableList.Builder<BakedQuad> quads = ImmutableList.builder();

            // Camo model, only add the quads if there is a label on this side
            if (validCamo)
            {
                quads.addAll(this.getQuadsForCamoModelSide(side, extendedState, bakedModel, rand));
            }
            // Not a camo model, always return the quads of the normal model
            else
            {
                quads.addAll(bakedModel.getQuads(extendedState, side, rand));
            }

            mapBuilder.put(Optional.ofNullable(side), quads.build());
        }

        mapBuilder.put(Optional.ofNullable(null), ImmutableList.copyOf(bakedModel.getQuads(extendedState, null, rand)));

        return mapBuilder.build();
    }

    private List<BakedQuad> getQuadsForCamoModelSide(EnumFacing side, IBlockState state, IBakedModel bakedModel, long rand)
    {
        EnumFacing relativeSide = PositionUtils.getRelativeFacing(state.getValue(BlockBarrel.FACING_H), side);

        switch (relativeSide)
        {
            case DOWN:
                return state.getValue(BlockBarrel.LABEL_DOWN)  ? bakedModel.getQuads(state, side, rand) : EMPTY_QUADS;
            case UP:
                return state.getValue(BlockBarrel.LABEL_UP)    ? bakedModel.getQuads(state, side, rand) : EMPTY_QUADS;
            case NORTH:
                return state.getValue(BlockBarrel.LABEL_FRONT) ? bakedModel.getQuads(state, side, rand) : EMPTY_QUADS;
            case SOUTH:
                return state.getValue(BlockBarrel.LABEL_BACK)  ? bakedModel.getQuads(state, side, rand) : EMPTY_QUADS;
            case WEST:
                return state.getValue(BlockBarrel.LABEL_RIGHT) ? bakedModel.getQuads(state, side, rand) : EMPTY_QUADS;
            case EAST:
                return state.getValue(BlockBarrel.LABEL_LEFT)  ? bakedModel.getQuads(state, side, rand) : EMPTY_QUADS;
            default:
        }

        return EMPTY_QUADS;
    }

    private ImmutableMap<String, String> getTexturesBaseModel(IBlockState state, Map<String, String> texturesIn)
    {
        ImmutableMap.Builder<String, String> texturesOut = ImmutableMap.builder();

        String texFront = texturesIn.get("front_normal");
        String texSide  = texturesIn.get("side");
        String texUp    = texturesIn.get("top");

        texturesOut.put("particle", texturesIn.get("top"));
        texturesOut.put("up",    state.getValue(BlockBarrel.LABEL_UP)    ? texFront : texUp);
        texturesOut.put("down",  state.getValue(BlockBarrel.LABEL_DOWN)  ? texFront : texUp);
        texturesOut.put("north", state.getValue(BlockBarrel.LABEL_FRONT) ? texFront : texSide);
        texturesOut.put("south", state.getValue(BlockBarrel.LABEL_BACK)  ? texFront : texSide);
        texturesOut.put("west",  state.getValue(BlockBarrel.LABEL_RIGHT) ? texFront : texSide);
        texturesOut.put("east",  state.getValue(BlockBarrel.LABEL_LEFT)  ? texFront : texSide);

        return texturesOut.build();
    }

    private static class ModelBarrel implements IModel
    {
        private static final ResourceLocation BARREL_BASE_MODEL    = new ResourceLocation(Reference.MOD_ID, "block/barrel_base");
        private static final ResourceLocation BARREL_OVERLAY_MODEL = new ResourceLocation(Reference.MOD_ID, "block/barrel_overlay");
        private final IModel baseModel;
        private final IModel overlayModel;
        private final ImmutableMap<String, String> textures;

        private ModelBarrel()
        {
            this.baseModel = ModelLoaderRegistry.getMissingModel();
            this.overlayModel = ModelLoaderRegistry.getMissingModel();
            this.textures = ImmutableMap.of();
        }

        private ModelBarrel(IModel baseModel, IModel overlayModel, ImmutableMap<String, String> textures)
        {
            this.baseModel = baseModel;
            this.overlayModel = overlayModel;
            this.textures = textures;
        }

        @Override
        public Collection<ResourceLocation> getDependencies()
        {
            return ImmutableList.of(BARREL_BASE_MODEL, BARREL_OVERLAY_MODEL);
        }

        @Override
        public Collection<ResourceLocation> getTextures()
        {
            ImmutableList.Builder<ResourceLocation> builder = ImmutableList.builder();

            builder.add(new ResourceLocation("enderutilities:blocks/barrel_normal_front"));
            builder.add(new ResourceLocation("enderutilities:blocks/barrel_normal_front_camo"));
            builder.add(new ResourceLocation("enderutilities:blocks/barrel_normal_side"));
            builder.add(new ResourceLocation("enderutilities:blocks/barrel_normal_top"));
            builder.add(new ResourceLocation("enderutilities:blocks/barrel_creative_front"));
            builder.add(new ResourceLocation("enderutilities:blocks/barrel_creative_front_camo"));
            builder.add(new ResourceLocation("enderutilities:blocks/barrel_creative_side"));
            builder.add(new ResourceLocation("enderutilities:blocks/barrel_creative_top"));
            /*
            // FIXME This doesn't work :/ getTextures() does not seem to get
            // called for the model returned from retexture()
            for (String tex : this.textures.values())
            {
                builder.add(new ResourceLocation(tex));
            }
            */

            return builder.build();
        }

        @Override
        public IModel retexture(ImmutableMap<String, String> textures)
        {
            IModel baseModel = ModelLoaderRegistry.getMissingModel();
            IModel overlayModel = ModelLoaderRegistry.getMissingModel();
            
            try
            {
                baseModel = ModelLoaderRegistry.getModel(BARREL_BASE_MODEL);
                baseModel = baseModel.retexture(this.textures);

                overlayModel = ModelLoaderRegistry.getModel(BARREL_OVERLAY_MODEL);
                overlayModel = overlayModel.retexture(this.textures);
            }
            catch (Exception e)
            {
                EnderUtilities.logger.warn("Failed to load the base model for a Barrel", e);
            }

            return new ModelBarrel(baseModel, overlayModel, textures);
        }

        @Override
        public IBakedModel bake(IModelState state, VertexFormat format, Function<ResourceLocation, TextureAtlasSprite> bakedTextureGetter)
        {
            return new BakedModelBarrel(this.baseModel, this.overlayModel, state, format, bakedTextureGetter, this.textures);
        }
    }

    public static class ModelLoaderBarrel implements ICustomModelLoader
    {
        private static final ResourceLocation FAKE_LOCATION_NORMAL      = new ResourceLocation(Reference.MOD_ID, "models/block/custom/barrel_normal");
        private static final ResourceLocation FAKE_LOCATION_CREATIVE    = new ResourceLocation(Reference.MOD_ID, "models/block/custom/barrel_creative");
        public static final ResourceLocation LOCATION_NORMAL            = new ResourceLocation(Reference.MOD_ID, "block/custom/barrel_normal");
        public static final ResourceLocation LOCATION_CREATIVE          = new ResourceLocation(Reference.MOD_ID, "block/custom/barrel_creative");

        @Override
        public boolean accepts(ResourceLocation modelLocation)
        {
            return modelLocation.equals(FAKE_LOCATION_NORMAL) || modelLocation.equals(FAKE_LOCATION_CREATIVE);
        }

        @Override
        public IModel loadModel(ResourceLocation modelLocation) throws Exception
        {
            return new ModelBarrel();
        }

        @Override
        public void onResourceManagerReload(IResourceManager resourceManager)
        {
            QUAD_CACHE_NORMAL.clear();
            QUAD_CACHE_CAMO.clear();
        }
    }

    public static class StateMapper extends StateMapperBase
    {
        private static final ModelResourceLocation LOCATION_NORMAL   = new ModelResourceLocation(Reference.MOD_ID + ":barrel", "creative=false");
        private static final ModelResourceLocation LOCATION_CREATIVE = new ModelResourceLocation(Reference.MOD_ID + ":barrel", "creative=true");

        @Override
        protected ModelResourceLocation getModelResourceLocation(IBlockState state)
        {
            return state.getValue(BlockBarrel.CREATIVE) ? LOCATION_CREATIVE : LOCATION_NORMAL;
        }
    }
}