package com.hypixel.hytale.server.worldgen.climate;

import com.hypixel.hytale.math.util.MathUtil;
import com.hypixel.hytale.server.worldgen.climate.util.DistanceTransform;
import com.hypixel.hytale.server.worldgen.climate.util.DoubleMap;
import com.hypixel.hytale.server.worldgen.climate.util.IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nonnull;

public class ClimateGraph {
   public static final int RESOLUTION = 512;
   private static final double ONE_MINUS_EPS = 1.0 - MathUtil.EPSILON_DOUBLE;
   private final double width;
   private final double height;
   private final double fadeRadius;
   private final double fadeDistance;
   private final ClimateGraph.FadeMode fadeMode;
   @Nonnull
   private final ClimateType[] parents;
   @Nonnull
   private final ClimateType[] children;
   @Nonnull
   private final ClimateType[] id2TypeLookup;
   @Nonnull
   private final IntMap table;
   @Nonnull
   private final DoubleMap fade;
   @Nonnull
   private final Object2IntMap<ClimateType> type2IdLookup;

   public ClimateGraph(int resolution, @Nonnull ClimateType[] parents, @Nonnull ClimateGraph.FadeMode fadeMode, double fadeRadius, double fadeDistance) {
      this.table = new IntMap(resolution, resolution);
      this.fade = new DoubleMap(resolution, resolution);
      this.width = resolution * ONE_MINUS_EPS;
      this.height = resolution * ONE_MINUS_EPS;
      AtomicInteger typeCount = new AtomicInteger();
      ClimateType.walk(parents, climate -> typeCount.getAndIncrement());
      this.fadeMode = fadeMode;
      this.fadeRadius = fadeRadius;
      this.fadeDistance = fadeDistance;
      this.parents = parents;
      this.children = new ClimateType[typeCount.intValue() - parents.length];
      this.id2TypeLookup = new ClimateType[typeCount.intValue()];
      this.type2IdLookup = new Object2IntOpenHashMap<>();
      AtomicInteger typeIndex = new AtomicInteger();
      AtomicInteger childIndex = new AtomicInteger();

      for (ClimateType parent : parents) {
         this.id2TypeLookup[typeIndex.get()] = parent;
         this.type2IdLookup.put(parent, typeIndex.get());
         typeIndex.getAndIncrement();
         ClimateType.walk(parent.children, climate -> {
            this.children[childIndex.get()] = climate;
            this.id2TypeLookup[typeIndex.get()] = climate;
            this.type2IdLookup.put(climate, typeIndex.get());
            typeIndex.getAndIncrement();
            childIndex.getAndIncrement();
         });
      }

      this.populateTable(this.table, this.fade);
   }

   public double fadeRadius() {
      return this.fadeRadius;
   }

   public double fadeDistance() {
      return this.fadeDistance;
   }

   public ClimateGraph.FadeMode fadeMode() {
      return this.fadeMode;
   }

   public void refresh() {
      this.populateTable(this.table, this.fade);
   }

   public ClimateType[] getParents() {
      return this.parents;
   }

   public ClimateType[] getChildren() {
      return this.children;
   }

   public IntMap getTable() {
      return this.table;
   }

   public DoubleMap getFade() {
      return this.fade;
   }

   public int indexOf(double x, double y) {
      int cx = MathUtil.floor(x * this.width);
      int cy = MathUtil.floor(y * this.height);
      return this.table.index(cx, cy);
   }

   public int getId(int index) {
      return this.table.at(index);
   }

   public double getFade(int index) {
      return this.fade.at(index) * this.fadeDistance;
   }

   public double getFadeRaw(int index) {
      return this.fade.at(index);
   }

   public int getId(double x, double y) {
      int cx = MathUtil.floor(x * this.width);
      int cy = MathUtil.floor(y * this.height);
      return this.table.at(cx, cy);
   }

   public double getFade(double x, double y) {
      return this.getFadeRaw(x, y) * this.fadeDistance;
   }

   public double getFadeRaw(double x, double y) {
      int cx = MathUtil.floor(x * this.width);
      int cy = MathUtil.floor(y * this.height);
      return this.fade.at(cx, cy);
   }

   public ClimateType getType(double x, double y) {
      int cx = MathUtil.floor(x * this.width);
      int cy = MathUtil.floor(y * this.height);
      int id = this.table.at(cx, cy);
      return this.id2TypeLookup[id];
   }

   public boolean validate(int id) {
      return (id & 536870911) < this.id2TypeLookup.length;
   }

   public ClimateType getType(int id) {
      return this.id2TypeLookup[id & 536870911];
   }

   private void populateTable(IntMap table, DoubleMap fade) {
      if (this.fadeMode == ClimateGraph.FadeMode.PARENTS) {
         for (int x = 0; x < table.width; x++) {
            for (int y = 0; y < table.height; y++) {
               this.populatePixel(x, y, table, false);
            }
         }

         DistanceTransform.apply(table, fade, this.fadeRadius);
      }

      for (int x = 0; x < table.width; x++) {
         for (int y = 0; y < table.height; y++) {
            this.populatePixel(x, y, table, true);
         }
      }

      if (this.fadeMode == ClimateGraph.FadeMode.CHILDREN) {
         DistanceTransform.apply(table, fade, this.fadeRadius);
      }
   }

   private void populatePixel(int x, int y, IntMap table, boolean recursive) {
      ClimateType selected = null;
      ClimateType[] climates = this.parents;

      while (climates.length > 0) {
         ClimateType nearest = null;
         double minDist1 = Double.MAX_VALUE;
         double minDist2 = Double.MAX_VALUE;

         for (ClimateType climate : climates) {
            for (ClimatePoint point : climate.points) {
               double dx = x - point.temperature * this.width;
               double dy = y - point.intensity * this.height;
               double dist2 = MathUtil.lengthSquared(dx, dy) / point.modifier;
               if (dist2 < minDist1) {
                  minDist2 = minDist1;
                  minDist1 = dist2;
                  nearest = climate;
               } else if (dist2 < minDist2) {
                  minDist2 = dist2;
               }
            }
         }

         if (nearest == null) {
            break;
         }

         selected = nearest;
         climates = nearest.children;
         if (!recursive) {
            break;
         }
      }

      if (selected == null) {
         table.set(x, y, -1);
      } else {
         table.set(x, y, this.type2IdLookup.getOrDefault(selected, -1));
      }
   }

   public static enum FadeMode {
      PARENTS,
      CHILDREN;
   }
}
