package com.hypixel.hytale.server.worldgen.cave.shape;

import com.hypixel.hytale.math.util.MathUtil;
import com.hypixel.hytale.math.util.TrigMathUtil;
import com.hypixel.hytale.math.vector.Vector3d;
import com.hypixel.hytale.procedurallib.supplier.IDoubleRange;
import com.hypixel.hytale.server.worldgen.cave.CaveNodeType;
import com.hypixel.hytale.server.worldgen.cave.CaveType;
import com.hypixel.hytale.server.worldgen.cave.element.CaveNode;
import com.hypixel.hytale.server.worldgen.util.bounds.IWorldBounds;
import java.util.Random;
import javax.annotation.Nonnull;

public class CylinderCaveNodeShape extends AbstractCaveNodeShape implements IWorldBounds {
   private final CaveType caveType;
   @Nonnull
   private final Vector3d o;
   @Nonnull
   private final Vector3d v;
   private final int lowBoundX;
   private final int lowBoundY;
   private final int lowBoundZ;
   private final int highBoundX;
   private final int highBoundY;
   private final int highBoundZ;
   private final double radius1;
   private final double radius2;
   private final double middleRadius;

   public CylinderCaveNodeShape(CaveType caveType, @Nonnull Vector3d o, @Nonnull Vector3d v, double radius1, double radius2, double middleRadius) {
      this.caveType = caveType;
      this.o = o;
      this.v = v;
      this.radius1 = radius1;
      this.radius2 = radius2;
      this.middleRadius = middleRadius;
      this.lowBoundX = MathUtil.floor(Math.min(o.x, o.x + v.x) - Math.max(radius1, radius2));
      this.lowBoundY = MathUtil.floor(Math.min(o.y, o.y + v.y) - Math.max(radius1, radius2));
      this.lowBoundZ = MathUtil.floor(Math.min(o.z, o.z + v.z) - Math.max(radius1, radius2));
      this.highBoundX = MathUtil.ceil(Math.max(o.x, o.x + v.x) + Math.max(radius1, radius2));
      this.highBoundY = MathUtil.ceil(Math.max(o.y, o.y + v.y) + Math.max(radius1, radius2));
      this.highBoundZ = MathUtil.ceil(Math.max(o.z, o.z + v.z) + Math.max(radius1, radius2));
   }

   @Nonnull
   @Override
   public Vector3d getStart() {
      return this.o.clone();
   }

   @Nonnull
   @Override
   public Vector3d getEnd() {
      double x = this.o.x + this.v.x;
      double y = this.o.y + this.v.y;
      double z = this.o.z + this.v.z;
      return new Vector3d(x, y, z);
   }

   @Nonnull
   @Override
   public Vector3d getAnchor(@Nonnull Vector3d vector, double t, double tv, double th) {
      double radius = this.getRadiusAt(t);
      return CaveNodeShapeUtils.getPipeAnchor(vector, this.o, this.v, radius, radius, radius, t, tv, th);
   }

   @Nonnull
   @Override
   public IWorldBounds getBounds() {
      return this;
   }

   @Override
   public int getLowBoundX() {
      return this.lowBoundX;
   }

   @Override
   public int getLowBoundZ() {
      return this.lowBoundZ;
   }

   @Override
   public int getHighBoundX() {
      return this.highBoundX;
   }

   @Override
   public int getHighBoundZ() {
      return this.highBoundZ;
   }

   @Override
   public int getLowBoundY() {
      return this.lowBoundY;
   }

   @Override
   public int getHighBoundY() {
      return this.highBoundY;
   }

   public double getRadius1() {
      return this.radius1;
   }

   public double getRadius2() {
      return this.radius2;
   }

   @Override
   public boolean shouldReplace(int seed, double x, double z, int y) {
      double t = this.projectPointOnNode(x, y, z);
      if (!(t < 0.0) && !(t > 1.0)) {
         double dx = this.o.x + this.v.x * t;
         double dy = this.o.y + this.v.y * t;
         double dz = this.o.z + this.v.z * t;
         double r = this.getRadiusAt(t) * this.caveType.getHeightRadiusFactor(seed, x, z, MathUtil.floor(dy));
         dx -= x;
         dy -= y;
         dz -= z;
         return dx * dx + dy * dy + dz * dz < r * r;
      } else {
         return false;
      }
   }

   private double projectPointOnNode(double px, double py, double pz) {
      double t = (px - this.o.x) * this.v.x + (py - this.o.y) * this.v.y + (pz - this.o.z) * this.v.z;
      return t / (this.v.x * this.v.x + this.v.y * this.v.y + this.v.z * this.v.z);
   }

   private double getRadiusAt(double t) {
      return t < 0.5 ? (this.middleRadius - this.radius1) * 2.0 * t + this.radius1 : (this.radius2 - this.middleRadius) * 2.0 * (t - 0.5) + this.middleRadius;
   }

   @Override
   public double getFloorPosition(int seed, double x, double z) {
      for (int y = this.getLowBoundY(); y < this.getHighBoundY(); y++) {
         if (this.shouldReplace(seed, x, z, y)) {
            return y - 1;
         }
      }

      return -1.0;
   }

   @Override
   public double getCeilingPosition(int seed, double x, double z) {
      for (int y = this.getHighBoundY(); y > this.getLowBoundY(); y--) {
         if (this.shouldReplace(seed, x, z, y)) {
            return y + 1;
         }
      }

      return -1.0;
   }

   @Nonnull
   @Override
   public String toString() {
      return "CylinderCaveNodeShape{caveType="
         + this.caveType
         + ", o="
         + this.o
         + ", v="
         + this.v
         + ", lowBoundX="
         + this.lowBoundX
         + ", lowBoundY="
         + this.lowBoundY
         + ", lowBoundZ="
         + this.lowBoundZ
         + ", highBoundX="
         + this.highBoundX
         + ", highBoundY="
         + this.highBoundY
         + ", highBoundZ="
         + this.highBoundZ
         + ", radius1="
         + this.radius1
         + ", radius2="
         + this.radius2
         + ", middleRadius="
         + this.middleRadius
         + "}";
   }

   public static class CylinderCaveNodeShapeGenerator implements CaveNodeShapeEnum.CaveNodeShapeGenerator {
      private final IDoubleRange radius;
      private final IDoubleRange middleRadius;
      private final IDoubleRange length;
      private final boolean inheritParentRadius;

      public CylinderCaveNodeShapeGenerator(IDoubleRange radius, IDoubleRange middleRadius, IDoubleRange length, boolean inheritParentRadius) {
         this.radius = radius;
         this.middleRadius = middleRadius;
         this.length = length;
         this.inheritParentRadius = inheritParentRadius;
      }

      @Nonnull
      @Override
      public CaveNodeShape generateCaveNodeShape(
         @Nonnull Random random,
         CaveType caveType,
         CaveNode parentNode,
         @Nonnull CaveNodeType.CaveNodeChildEntry childEntry,
         @Nonnull Vector3d origin,
         float yaw,
         float pitch
      ) {
         double l = this.length.getValue(random.nextDouble());
         Vector3d direction = new Vector3d(
               TrigMathUtil.sin(pitch) * TrigMathUtil.cos(yaw), TrigMathUtil.cos(pitch), TrigMathUtil.sin(pitch) * TrigMathUtil.sin(yaw)
            )
            .scale(l);
         double radius1;
         if (this.inheritParentRadius) {
            radius1 = CaveNodeShapeUtils.getEndRadius(parentNode, this.radius, random);
         } else {
            radius1 = this.radius.getValue(random.nextDouble());
         }

         double radius2 = this.radius.getValue(random.nextDouble());
         double radius3 = (radius2 - radius1) * 0.5 + radius1;
         if (this.middleRadius != null) {
            radius3 = this.middleRadius.getValue(random.nextDouble());
         }

         Vector3d offset = CaveNodeShapeUtils.getOffset(parentNode, childEntry);
         origin.add(offset);
         return new CylinderCaveNodeShape(caveType, origin, direction, radius1, radius2, radius3);
      }
   }
}
