// 
// Decompiled by Procyon v0.6.0
// 

package com.hypixel.hytale.builtin.portals.utils.spatial;

import javax.annotation.Nullable;
import java.util.Iterator;
import java.util.HashSet;
import java.util.function.Predicate;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import java.util.Set;
import java.util.Collections;
import java.util.Collection;
import com.hypixel.hytale.math.util.MathUtil;
import com.hypixel.hytale.math.vector.Vector3d;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import java.util.List;
import com.hypixel.hytale.math.vector.Vector3i;
import java.util.Map;

public class SpatialHashGrid<T>
{
    private final double cellSize;
    private final Map<Vector3i, List<Entry<T>>> grid;
    private final Map<T, Entry<T>> index;
    
    public SpatialHashGrid(final double cellSize) {
        this.grid = new Object2ObjectOpenHashMap<Vector3i, List<Entry<T>>>();
        this.index = new Object2ObjectOpenHashMap<T, Entry<T>>();
        this.cellSize = cellSize;
    }
    
    private Vector3i cellFor(final Vector3d p) {
        return new Vector3i(MathUtil.floor(p.x / this.cellSize), MathUtil.floor(p.y / this.cellSize), MathUtil.floor(p.z / this.cellSize));
    }
    
    public Collection<? extends T> getAll() {
        return (Collection<? extends T>)Collections.unmodifiableSet((Set<?>)this.index.keySet());
    }
    
    public int size() {
        return this.index.size();
    }
    
    public boolean isEmpty() {
        return this.index.isEmpty();
    }
    
    public void add(final Vector3d pos, final T value) {
        final Vector3i cell = this.cellFor(pos);
        final Entry<T> entry = new Entry<T>(pos.clone(), cell, value);
        this.index.put(value, entry);
        this.grid.computeIfAbsent(cell, x -> new ObjectArrayList()).add(entry);
    }
    
    public boolean remove(final T value) {
        final Entry<T> entry = this.index.remove(value);
        if (entry == null) {
            return false;
        }
        final List<Entry<T>> bucket = this.grid.get(entry.cell);
        if (bucket != null) {
            bucket.remove(entry);
            if (bucket.isEmpty()) {
                this.grid.remove(entry.cell);
            }
        }
        return true;
    }
    
    public void removeIf(final Predicate<T> predicate) {
        final Set<T> toRemove = new HashSet<T>();
        for (final T elem : this.index.keySet()) {
            if (predicate.test(elem)) {
                toRemove.add(elem);
            }
        }
        for (final T elem : toRemove) {
            this.remove(elem);
        }
    }
    
    public void move(final T value, final Vector3d newPos) {
        final Entry<T> entry = this.index.get(value);
        if (entry == null) {
            return;
        }
        final Vector3i oldCell = entry.cell;
        final Vector3i newCell = this.cellFor(newPos);
        entry.pos.assign(newPos);
        if (!oldCell.equals(newCell)) {
            final List<Entry<T>> oldBucket = this.grid.get(oldCell);
            oldBucket.remove(entry);
            if (oldBucket.isEmpty()) {
                this.grid.remove(oldCell);
            }
            entry.cell = newCell;
            this.grid.computeIfAbsent(newCell, x -> new ObjectArrayList()).add(entry);
        }
    }
    
    public Map<T, Vector3d> queryRange(final Vector3d center, final double radius) {
        final Map<T, Vector3d> out = new Object2ObjectOpenHashMap<T, Vector3d>();
        final double radiusSq = radius * radius;
        this.query(center, radius, bucket -> {
            for (final Entry<T> entry : bucket) {
                if (entry.pos.distanceSquaredTo(center) <= radiusSq) {
                    out.put(entry.value, entry.pos);
                }
            }
            return true;
        });
        return out;
    }
    
    @Nullable
    public T findClosest(final Vector3d center, final double searchRadius) {
        final CellVisitor<T> closestVisitor = new CellVisitor<T>(this) {
            double closestDist = Double.MAX_VALUE;
            T closest = null;
            
            @Override
            public boolean visit(final List<Entry<T>> bucket) {
                for (final Entry<T> entry : bucket) {
                    final double dist = entry.pos.distanceSquaredTo(center);
                    if (dist <= this.closestDist) {
                        this.closestDist = dist;
                        this.closest = entry.value;
                    }
                }
                return true;
            }
        };
        this.query(center, searchRadius, closestVisitor);
        return closestVisitor.closest;
    }
    
    public boolean hasAnyWithin(final Vector3d center, final double radius) {
        final CellVisitor<T> withinVisitor = new CellVisitor<T>(this) {
            final double radiusSq = radius * radius;
            boolean hasWithin = false;
            
            @Override
            public boolean visit(final List<Entry<T>> bucket) {
                for (final Entry<T> entry : bucket) {
                    final double dist = entry.pos.distanceSquaredTo(center);
                    if (dist <= this.radiusSq) {
                        this.hasWithin = true;
                        return false;
                    }
                }
                return true;
            }
        };
        this.query(center, radius, withinVisitor);
        return withinVisitor.hasWithin;
    }
    
    private void query(final Vector3d center, final double radius, final CellVisitor<T> visitor) {
        final int minX = MathUtil.floor((center.x - radius) / this.cellSize);
        final int minY = MathUtil.floor((center.y - radius) / this.cellSize);
        final int minZ = MathUtil.floor((center.z - radius) / this.cellSize);
        final int maxX = MathUtil.floor((center.x + radius) / this.cellSize);
        final int maxY = MathUtil.floor((center.y + radius) / this.cellSize);
        final int maxZ = MathUtil.floor((center.z + radius) / this.cellSize);
        final Vector3i lookup = new Vector3i();
        for (int x = minX; x <= maxX; ++x) {
            for (int y = minY; y <= maxY; ++y) {
                for (int z = minZ; z <= maxZ; ++z) {
                    lookup.assign(x, y, z);
                    final List<Entry<T>> bucket = this.grid.get(lookup);
                    if (bucket != null) {
                        final boolean keepGoing = visitor.visit(bucket);
                        if (!keepGoing) {
                            return;
                        }
                    }
                }
            }
        }
    }
    
    private static final class Entry<T>
    {
        private final Vector3d pos;
        private Vector3i cell;
        private final T value;
        
        private Entry(final Vector3d pos, final Vector3i cell, final T value) {
            this.pos = pos;
            this.cell = cell;
            this.value = value;
        }
    }
    
    private interface CellVisitor<T>
    {
        boolean visit(final List<Entry<T>> p0);
    }
}
