package net.minecraft.world;

import com.mojang.serialization.Codec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiConsumer;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.RandomSource;
import net.minecraft.util.datafix.DataFixTypes;
import net.minecraft.world.level.levelgen.PositionalRandomFactory;
import net.minecraft.world.level.saveddata.SavedData;
import net.minecraft.world.level.saveddata.SavedDataType;

public class RandomSequences extends SavedData {
    public static final SavedDataType<RandomSequences> TYPE = new SavedDataType<>(
        "random_sequences",
        context -> new RandomSequences(context.worldSeed()),
        context -> codec(context.worldSeed()),
        DataFixTypes.SAVED_DATA_RANDOM_SEQUENCES
    );
    private final long worldSeed;
    private int salt;
    private boolean includeWorldSeed = true;
    private boolean includeSequenceId = true;
    private final Map<ResourceLocation, RandomSequence> sequences = new java.util.concurrent.ConcurrentHashMap<>(); // Folia - region threading

    public RandomSequences(long seed) {
        this.worldSeed = seed;
    }

    private RandomSequences(long worldSeed, int salt, boolean includeWorldSeed, boolean includeSequenceId, Map<ResourceLocation, RandomSequence> sequences) {
        this.worldSeed = worldSeed;
        this.salt = salt;
        this.includeWorldSeed = includeWorldSeed;
        this.includeSequenceId = includeSequenceId;
        this.sequences.putAll(sequences);
    }

    public static Codec<RandomSequences> codec(long worldSeed) {
        return RecordCodecBuilder.create(
            instance -> instance.group(
                    RecordCodecBuilder.point(worldSeed),
                    Codec.INT.fieldOf("salt").forGetter(randomSequences -> randomSequences.salt),
                    Codec.BOOL.optionalFieldOf("include_world_seed", true).forGetter(randomSequences -> randomSequences.includeWorldSeed),
                    Codec.BOOL.optionalFieldOf("include_sequence_id", true).forGetter(randomSequences -> randomSequences.includeSequenceId),
                    Codec.unboundedMap(ResourceLocation.CODEC, RandomSequence.CODEC)
                        .fieldOf("sequences")
                        .forGetter(randomSequences -> randomSequences.sequences)
                )
                .apply(instance, RandomSequences::new)
        );
    }

    public RandomSource get(ResourceLocation location) {
        RandomSource randomSource = this.sequences.computeIfAbsent(location, this::createSequence).random();
        return new RandomSequences.DirtyMarkingRandomSource(randomSource);
    }

    private RandomSequence createSequence(ResourceLocation location) {
        return this.createSequence(location, this.salt, this.includeWorldSeed, this.includeSequenceId);
    }

    private RandomSequence createSequence(ResourceLocation location, int salt, boolean includeWorldSeed, boolean includeSequenceId) {
        long l = (includeWorldSeed ? this.worldSeed : 0L) ^ salt;
        return new RandomSequence(l, includeSequenceId ? Optional.of(location) : Optional.empty());
    }

    public void forAllSequences(BiConsumer<ResourceLocation, RandomSequence> action) {
        this.sequences.forEach(action);
    }

    public void setSeedDefaults(int salt, boolean includeWorldSeed, boolean includeSequenceId) {
        this.salt = salt;
        this.includeWorldSeed = includeWorldSeed;
        this.includeSequenceId = includeSequenceId;
    }

    public int clear() {
        int size = this.sequences.size();
        this.sequences.clear();
        return size;
    }

    public void reset(ResourceLocation sequence) {
        this.sequences.put(sequence, this.createSequence(sequence));
    }

    public void reset(ResourceLocation sequence, int seed, boolean includeWorldSeed, boolean includeSequenceId) {
        this.sequences.put(sequence, this.createSequence(sequence, seed, includeWorldSeed, includeSequenceId));
    }

    class DirtyMarkingRandomSource implements RandomSource {
        private final RandomSource random;

        DirtyMarkingRandomSource(final RandomSource random) {
            this.random = random;
        }

        @Override
        public RandomSource fork() {
            RandomSequences.this.setDirty();
            synchronized (this.random) { return this.random.fork(); } // Folia - region threading
        }

        @Override
        public PositionalRandomFactory forkPositional() {
            RandomSequences.this.setDirty();
            synchronized (this.random) { return this.random.forkPositional(); } // Folia - region threading
        }

        @Override
        public void setSeed(long seed) {
            RandomSequences.this.setDirty();
            synchronized (this.random) { this.random.setSeed(seed); } // Folia - region threading
        }

        @Override
        public int nextInt() {
            RandomSequences.this.setDirty();
            synchronized (this.random) { return this.random.nextInt(); } // Folia - region threading
        }

        @Override
        public int nextInt(int bound) {
            RandomSequences.this.setDirty();
            synchronized (this.random) { return this.random.nextInt(bound); } // Folia - region threading
        }

        @Override
        public long nextLong() {
            RandomSequences.this.setDirty();
            synchronized (this.random) { return this.random.nextLong(); } // Folia - region threading
        }

        @Override
        public boolean nextBoolean() {
            RandomSequences.this.setDirty();
            synchronized (this.random) { return this.random.nextBoolean(); } // Folia - region threading
        }

        @Override
        public float nextFloat() {
            RandomSequences.this.setDirty();
            synchronized (this.random) { return this.random.nextFloat(); } // Folia - region threading
        }

        @Override
        public double nextDouble() {
            RandomSequences.this.setDirty();
            synchronized (this.random) { return this.random.nextDouble(); } // Folia - region threading
        }

        @Override
        public double nextGaussian() {
            RandomSequences.this.setDirty();
            synchronized (this.random) { return this.random.nextGaussian(); } // Folia - region threading
        }

        @Override
        public boolean equals(Object other) {
            return this == other
                || other instanceof RandomSequences.DirtyMarkingRandomSource dirtyMarkingRandomSource && this.random.equals(dirtyMarkingRandomSource.random);
        }
    }
}
