package com.straice.smoothdoors.client.anim;

import com.straice.smoothdoors.config.SddConfig;
import com.straice.smoothdoors.config.SddConfigManager;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents;
import net.minecraft.block.*;
import net.minecraft.block.enums.DoorHinge;
import net.minecraft.block.enums.DoubleBlockHalf;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.OverlayTexture;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.WorldRenderer;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.math.*;

import java.lang.reflect.Field;

public final class SddAnimator {

    private static final MinecraftClient MC = MinecraftClient.getInstance();

    // Base duration (seconds) at speed = 1.0x
    private static final float BASE_DURATION_S = 0.35f;

    private static final Long2ObjectOpenHashMap<Anim> ANIMS = new Long2ObjectOpenHashMap<>();
    private static boolean hooksInit = false;

    private SddAnimator() {}

    /** Llamar desde SmoothDoubleDoorsClient#onInitializeClient(). */
    public static void initClientHooks() {
        if (hooksInit) return;
        hooksInit = true;

        WorldRenderEvents.AFTER_ENTITIES.register(ctx -> {
            if (MC.world == null || ANIMS.isEmpty()) return;
            float tickDelta = MC.getRenderTickCounter().getTickProgress(true);
            renderAll(tickDelta);
        });
    }

    // === API usada por mixins ===

    /** Oculta el bloque vanilla mientras animamos (evita doble puerta). */
    public static boolean shouldHideInChunk(BlockPos pos, BlockState state) {
        synchronized (ANIMS) {
            if (ANIMS.containsKey(pos.asLong())) return true;

            if (state.getBlock() instanceof DoorBlock && state.contains(DoorBlock.HALF)) {
                DoubleBlockHalf half = state.get(DoorBlock.HALF);
                BlockPos other = (half == DoubleBlockHalf.LOWER) ? pos.up() : pos.down();
                return ANIMS.containsKey(other.asLong());
            }

            return false;
        }
    }

    public static boolean isAnimatingAt(BlockPos pos) {
        synchronized (ANIMS) {
            return ANIMS.containsKey(pos.asLong());
        }
    }

    /** Se llama cuando cambia el bloque (abrir/cerrar). */
    public static void onBlockUpdate(BlockPos pos, BlockState oldState, BlockState newState) {
        if (MC.world == null) return;

        Kind kind = kindOf(oldState, newState);
        if (kind == null) {
            synchronized (ANIMS) {
                ANIMS.remove(pos.asLong());
            }
            return;
        }

        if (!isKindBlock(kind, newState)) {
            removeKindAt(pos, kind);
            return;
        }

        // DOOR: arrancamos solo desde la mitad LOWER para no duplicar.
        if (kind == Kind.DOOR && newState.contains(DoorBlock.HALF)) {
            if (newState.get(DoorBlock.HALF) == DoubleBlockHalf.UPPER) return;
        }

        boolean oldOpen = isOpen(oldState);
        boolean newOpen = isOpen(newState);
        if (oldOpen == newOpen) return;

        SddConfig cfg = SddConfigManager.get();
        if (!isAnimationEnabled(cfg, kind)) {
            removeKindAt(pos, kind);
            return;
        }

        float speed = speedFor(cfg, kind);
        if (speed <= 0.001f) speed = 1.0f;

        long startTick = MC.world.getTime();

        if (kind == Kind.DOOR) {
            startDoorAnimBothHalves(pos, newState, oldOpen, newOpen, speed, startTick);
        } else {
            BlockState base = forceClosedModel(newState, kind);
            synchronized (ANIMS) {
                ANIMS.put(pos.asLong(), new Anim(pos.toImmutable(), base, kind, oldOpen, newOpen, speed, startTick));
            }
            requestRerender(pos);
        }
    }

    // === Render ===

    private static void renderAll(float tickDelta) {
        if (MC.world == null || ANIMS.isEmpty()) return;

        Vec3d camPos = MC.gameRenderer.getCamera().getPos();
        MatrixStack matrices = new MatrixStack();
        VertexConsumerProvider.Immediate consumers = MC.getBufferBuilders().getEntityVertexConsumers();

        long worldTick = MC.world.getTime();
        double nowTick = worldTick + (double) MathHelper.clamp(tickDelta, 0.0f, 1.0f);

        Anim[] snapshot;
        synchronized (ANIMS) {
            snapshot = ANIMS.values().toArray(new Anim[0]);
        }
        for (Anim a : snapshot) {
            float t = a.progress01(nowTick);
            renderOne(a, t, camPos, matrices, consumers);
        }

        consumers.draw();
        cleanupFinished(nowTick);
    }

    private static void cleanupFinished(double nowTick) {
        synchronized (ANIMS) {
            var it = ANIMS.long2ObjectEntrySet().iterator();
            while (it.hasNext()) {
                var entry = it.next();
                if (entry.getValue().isFinished(nowTick)) {
                    BlockPos pos = entry.getValue().pos;
                    it.remove();
                    requestRerender(pos);
                }
            }
        }
    }

    private static void renderOne(Anim anim, float t, Vec3d camPos, MatrixStack matrices, VertexConsumerProvider consumers) {
        if (MC.world == null) return;

        BlockPos pos = anim.pos;
        BlockState state = anim.baseState;

        matrices.push();

        // Fijo en mundo: bloque - camara
        matrices.translate(
                pos.getX() - camPos.x,
                pos.getY() - camPos.y,
                pos.getZ() - camPos.z
        );

        float eased = easeInOut(t);
        float angleDeg = lerpAngleDeg(anim.fromOpen, anim.toOpen, eased, anim.kind, state);

        int light = WorldRenderer.getLightmapCoordinates((net.minecraft.world.BlockRenderView) MC.world, pos);
        if (anim.kind == Kind.FENCE_GATE) {
            FenceGateAnimation.render(state, angleDeg, matrices, consumers, light, OverlayTexture.DEFAULT_UV);
        } else {
            applyTransform(anim.kind, state, angleDeg, matrices);
            MC.getBlockRenderManager().renderBlockAsEntity(state, matrices, consumers, light, OverlayTexture.DEFAULT_UV);
        }

        matrices.pop();
    }

    // === DOOR (dos mitades) ===

    private static void startDoorAnimBothHalves(BlockPos lowerPos, BlockState lowerNewState, boolean oldOpen, boolean newOpen, float speed, long startTick) {
        BlockState baseLower = forceClosedModel(lowerNewState, Kind.DOOR);

        BlockPos upperPos = lowerPos.up();
        BlockState baseUpper = baseLower.with(DoorBlock.HALF, DoubleBlockHalf.UPPER);

        Anim aLower = new Anim(lowerPos.toImmutable(), baseLower, Kind.DOOR, oldOpen, newOpen, speed, startTick);
        Anim aUpper = new Anim(upperPos.toImmutable(), baseUpper, Kind.DOOR, oldOpen, newOpen, speed, startTick);

        synchronized (ANIMS) {
            ANIMS.put(lowerPos.asLong(), aLower);
            ANIMS.put(upperPos.asLong(), aUpper);
        }
        requestRerender(lowerPos);
        requestRerender(upperPos);
    }

    private static void removeKindAt(BlockPos pos, Kind kind) {
        synchronized (ANIMS) {
            ANIMS.remove(pos.asLong());
            if (kind == Kind.DOOR) {
                ANIMS.remove(pos.up().asLong());
                ANIMS.remove(pos.down().asLong());
            }
        }
    }

    // === Angulos y transforms ===

    private static float lerpAngleDeg(boolean fromOpen, boolean toOpen, float t, Kind kind, BlockState state) {
        float a = angleFor(kind, state, fromOpen);
        float b = angleFor(kind, state, toOpen);
        return MathHelper.lerp(t, a, b);
    }

    private static float angleFor(Kind kind, BlockState state, boolean open) {
        if (!open) return 0.0f;

        return switch (kind) {
            case DOOR -> {
                DoorHinge hinge = state.get(DoorBlock.HINGE);
                yield (hinge == DoorHinge.RIGHT) ? -90.0f : 90.0f;
            }
            case TRAPDOOR -> 90.0f;
            case FENCE_GATE -> 90.0f;
        };
    }

    private static void applyTransform(Kind kind, BlockState state, float angleDeg, MatrixStack matrices) {
        switch (kind) {
            case DOOR -> DoorAnimation.apply(state, angleDeg, matrices, MC.world);
            case TRAPDOOR -> TrapdoorAnimation.apply(state, angleDeg, matrices, MC.world);
            case FENCE_GATE -> {
            }
        }
    }

    // === Utilidades ===

    private static BlockState forceClosedModel(BlockState s, Kind kind) {
        return switch (kind) {
            case DOOR -> s.with(DoorBlock.OPEN, false);
            case TRAPDOOR -> s.with(TrapdoorBlock.OPEN, false);
            case FENCE_GATE -> s.with(FenceGateBlock.OPEN, false);
        };
    }

    private static boolean isKindBlock(Kind kind, BlockState state) {
        Block b = state.getBlock();
        return switch (kind) {
            case DOOR -> b instanceof DoorBlock;
            case TRAPDOOR -> b instanceof TrapdoorBlock;
            case FENCE_GATE -> b instanceof FenceGateBlock;
        };
    }

    private static void requestRerender(BlockPos pos) {
        if (MC.worldRenderer == null || MC.world == null) return;
        BlockState st = MC.world.getBlockState(pos);
        MC.worldRenderer.updateBlock(MC.world, pos, st, st, 0);
        MC.worldRenderer.scheduleBlockRenders(
                pos.getX(), pos.getY(), pos.getZ(),
                pos.getX(), pos.getY(), pos.getZ()
        );
        MC.worldRenderer.scheduleChunkRender(
                pos.getX() >> 4,
                pos.getY() >> 4,
                pos.getZ() >> 4
        );
    }

    private static boolean isOpen(BlockState s) {
        Block b = s.getBlock();
        if (b instanceof DoorBlock) return s.get(DoorBlock.OPEN);
        if (b instanceof TrapdoorBlock) return s.get(TrapdoorBlock.OPEN);
        if (b instanceof FenceGateBlock) return s.get(FenceGateBlock.OPEN);
        return false;
    }

    private static Kind kindOf(BlockState oldState, BlockState newState) {
        Block b = newState.getBlock();
        if (b instanceof DoorBlock) return Kind.DOOR;
        if (b instanceof TrapdoorBlock) return Kind.TRAPDOOR;
        if (b instanceof FenceGateBlock) return Kind.FENCE_GATE;

        b = oldState.getBlock();
        if (b instanceof DoorBlock) return Kind.DOOR;
        if (b instanceof TrapdoorBlock) return Kind.TRAPDOOR;
        if (b instanceof FenceGateBlock) return Kind.FENCE_GATE;

        return null;
    }

    private static boolean isAnimationEnabled(SddConfig cfg, Kind kind) {
        return switch (kind) {
            case DOOR -> cfg.animateDoors;
            case TRAPDOOR -> cfg.animateTrapdoors;
            case FENCE_GATE -> getBool(cfg, "animateFenceGates", false);
        };
    }

    private static float speedFor(SddConfig cfg, Kind kind) {
        return switch (kind) {
            case DOOR -> cfg.doorSpeed;
            case TRAPDOOR -> cfg.trapdoorSpeed;
            case FENCE_GATE -> getFloat(cfg, "fenceGateSpeed", 1.0f);
        };
    }

    private static float easeInOut(float t) {
        return t * t * (3.0f - 2.0f * t);
    }

    // === Reflect helpers (por si el campo no existe) ===

    private static boolean getBool(Object obj, String field, boolean def) {
        try {
            Field f = obj.getClass().getDeclaredField(field);
            f.setAccessible(true);
            Object v = f.get(obj);
            return (v instanceof Boolean) ? (Boolean) v : def;
        } catch (Throwable ignored) {
            return def;
        }
    }

    private static float getFloat(Object obj, String field, float def) {
        try {
            Field f = obj.getClass().getDeclaredField(field);
            f.setAccessible(true);
            Object v = f.get(obj);
            if (v instanceof Float) return (Float) v;
            if (v instanceof Double) return ((Double) v).floatValue();
            if (v instanceof Number) return ((Number) v).floatValue();
            return def;
        } catch (Throwable ignored) {
            return def;
        }
    }

    // === Tipos ===

    public enum Kind { DOOR, TRAPDOOR, FENCE_GATE }

    private static final class Anim {
        final BlockPos pos;
        final BlockState baseState;
        final Kind kind;
        final boolean fromOpen;
        final boolean toOpen;

        final double startTick;
        final double durationTicks;

        Anim(BlockPos pos, BlockState baseState, Kind kind, boolean fromOpen, boolean toOpen, float speed, long startTick) {
            this.pos = pos;
            this.baseState = baseState;
            this.kind = kind;
            this.fromOpen = fromOpen;
            this.toOpen = toOpen;

            double durS = BASE_DURATION_S / speed;
            if (durS < 0.05) durS = 0.05;

            this.startTick = (double) startTick;
            this.durationTicks = Math.max(1.0, durS * 20.0);
        }

        float progress01(double nowTick) {
            double t = (nowTick - startTick) / durationTicks;
            if (t <= 0.0) return 0.0f;
            if (t >= 1.0) return 1.0f;
            return (float) t;
        }

        boolean isFinished(double nowTick) {
            return (nowTick - startTick) >= durationTicks;
        }
    }
}
