package com.minelittlepony.client.render;

import com.minelittlepony.api.config.PonyConfig;
import com.minelittlepony.api.events.ClientChannel;
import com.minelittlepony.api.events.PonyDataCallback;
import com.minelittlepony.api.model.*;
import com.minelittlepony.api.pony.Pony;
import com.minelittlepony.api.pony.PonyData;
import com.minelittlepony.client.PonyDataLoader;
import com.minelittlepony.client.render.entity.state.PonyRenderState;
import com.minelittlepony.client.transform.PonyPosture;
import com.minelittlepony.mson.api.ModelKey;
import com.minelittlepony.util.MathUtil;
import com.mojang.blaze3d.systems.RenderSystem;

import java.util.*;
import java.util.function.Function;
import java.util.function.Supplier;

import net.fabricmc.api.EnvType;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.Frustum;
import net.minecraft.client.render.entity.model.EntityModel;
import net.minecraft.client.render.entity.state.BipedEntityRenderState;
import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.entity.EntityPose;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.player.PlayerEntity;

import org.jetbrains.annotations.Nullable;

public class EquineRenderManager<
        T extends LivingEntity,
        S extends PonyRenderState,
        M extends EntityModel<? super S> & PonyModel<S>> {

    private Models<T, M> models;
    @Nullable
    private Function<S, Models<T, M>> modelsLookup;

    private final PonyRenderContext<T, S, M> context;
    private final Transformer<? super S> transformer;

    private final FrustrumCheck<T> frustrum;

    public static void disableModelRenderProfile() {
        RenderSystem.disableBlend();
    }

    public EquineRenderManager(PonyRenderContext<T, S, M> context, Transformer<? super S> transformer, Models<T, M> models) {
        this.context = context;
        this.transformer = transformer;
        this.models = models;
        frustrum = new FrustrumCheck<>(context);
        context.setModel(models.body());
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    public EquineRenderManager(PonyRenderContext<T, S, M> context, Transformer<? super S> transformer, ModelKey<? super M> key) {
        this(context, transformer, new Models(key));
    }

    public void setModelsLookup(@Nullable Function<S, Models<T, M>> modelsLookup) {
        this.modelsLookup = modelsLookup;
    }

    public Models<T, M> getModels() {
        return models;
    }

    public Frustum getFrustrum(T entity, Frustum vanilla) {
        if (RenderPass.getCurrent() == RenderPass.HUD) {
            return FrustrumCheck.ALWAYS_VISIBLE;
        }

        if (entity.isSleeping() || !PonyConfig.getInstance().frustrum.get()) {
            return vanilla;
        }
        return frustrum.withCamera(entity, vanilla);
    }

    public void preRender(T entity, S state, ModelAttributes.Mode mode) {
        Pony pony = context.getEntityPony(entity);
        if (modelsLookup != null) {
            models = modelsLookup.apply(state);
            context.setModel(models.body());
        }
        models.applyMetadata(pony.metadata());
        state.updateState(entity, models.body(), pony, mode);
    }

    public void setupTransforms(S state, MatrixStack stack, float animationProgress, float bodyYaw) {
        float s = state.getScaleFactor();
        stack.scale(s, s, s);

        if (state instanceof PlayerEntityRenderState) {
            if (state.attributes.isSitting) {
                stack.translate(0, 0.125D, 0);
            }
        }

        transformer.setupTransforms(state, stack, animationProgress, bodyYaw);

        PonyPosture.of(state.attributes).apply(state, getModels().body(), stack, bodyYaw, state.age, 1);
    }

    public interface Transformer<S extends BipedEntityRenderState> {
        void setupTransforms(S state, MatrixStack stack, float animationProgress, float bodyYaw);
    }

    public interface RegistrationHandler {
        SyncedPony getSyncedPony();
    }

    public interface ModelHolder<S extends BipedEntityRenderState, M extends EntityModel<S> & PonyModel<S>> {
        void setModel(M model);
    }

    public static class SyncedPony {
        @Nullable
        private Pony lastRenderedPony;
        private Supplier<Optional<PonyData>> lastPonyData = PonyDataLoader.NULL;
        @Nullable
        private Pony lastTransmittedPony;

        public Pony getCachedPony() {
            return lastRenderedPony;
        }

        public PonyData getCachedPonyData() {
            return lastPonyData.get().orElse(PonyData.NULL);
        }

        public void synchronize(PlayerEntity player) {
            Pony pony = Pony.getManager().getPony(player);
            boolean changed = pony.compareTo(lastRenderedPony) != 0;

            if (changed) {
                lastRenderedPony = pony;
                lastPonyData = pony.metadataGetter();
                player.calculateDimensions();
            }

            if (!(player instanceof PreviewModel)) {
                @Nullable
                PlayerEntity clientPlayer = MinecraftClient.getInstance().player;

                if (ClientChannel.isRegistered() && pony.compareTo(lastTransmittedPony) != 0) {
                    if (clientPlayer != null && (Objects.equals(player, clientPlayer) || Objects.equals(player.getGameProfile(), clientPlayer.getGameProfile()))) {
                        if (ClientChannel.broadcastPonyData(pony.metadata())) {
                            lastTransmittedPony = pony;
                        }
                    }
                }

                if (changed) {
                    PonyDataCallback.EVENT.invoker().onPonyDataAvailable(player, pony.metadata(), EnvType.CLIENT);
                }
            }
        }
    }
}