package com.hypixel.hytale.math.hitdetection;

import com.hypixel.hytale.logger.HytaleLogger;
import com.hypixel.hytale.math.matrix.Matrix4d;
import com.hypixel.hytale.math.shape.Quad4d;
import com.hypixel.hytale.math.shape.Triangle4d;
import com.hypixel.hytale.math.vector.Vector4d;
import java.util.Arrays;
import java.util.logging.Level;
import javax.annotation.Nonnull;

public class HitDetectionExecutor {
   public static final HytaleLogger log = HytaleLogger.forEnclosingClass();
   private static final Vector4d[] VERTEX_POINTS = new Vector4d[]{
      Vector4d.newPosition(0.0, 1.0, 1.0),
      Vector4d.newPosition(0.0, 1.0, 0.0),
      Vector4d.newPosition(1.0, 1.0, 1.0),
      Vector4d.newPosition(1.0, 1.0, 0.0),
      Vector4d.newPosition(0.0, 0.0, 1.0),
      Vector4d.newPosition(0.0, 0.0, 0.0),
      Vector4d.newPosition(1.0, 0.0, 1.0),
      Vector4d.newPosition(1.0, 0.0, 0.0)
   };
   public static final Quad4d[] CUBE_QUADS = new Quad4d[]{
      new Quad4d(VERTEX_POINTS, 0, 1, 3, 2),
      new Quad4d(VERTEX_POINTS, 0, 4, 5, 1),
      new Quad4d(VERTEX_POINTS, 4, 5, 7, 6),
      new Quad4d(VERTEX_POINTS, 2, 3, 7, 6),
      new Quad4d(VERTEX_POINTS, 1, 3, 7, 5),
      new Quad4d(VERTEX_POINTS, 0, 2, 6, 4)
   };
   @Nonnull
   private final Matrix4d pvmMatrix = new Matrix4d();
   @Nonnull
   private final Matrix4d invPvMatrix = new Matrix4d();
   @Nonnull
   private final Vector4d origin = new Vector4d();
   @Nonnull
   private final HitDetectionBuffer buffer = new HitDetectionBuffer();
   private MatrixProvider projectionProvider;
   private MatrixProvider viewProvider;
   private LineOfSightProvider losProvider = LineOfSightProvider.DEFAULT_TRUE;
   private int maxRayTests = 10;

   public Vector4d getHitLocation() {
      return this.buffer.hitPosition;
   }

   @Nonnull
   public HitDetectionExecutor setProjectionProvider(MatrixProvider provider) {
      this.projectionProvider = provider;
      return this;
   }

   @Nonnull
   public HitDetectionExecutor setViewProvider(MatrixProvider provider) {
      this.viewProvider = provider;
      return this;
   }

   @Nonnull
   public HitDetectionExecutor setLineOfSightProvider(LineOfSightProvider losProvider) {
      this.losProvider = losProvider;
      return this;
   }

   @Nonnull
   public HitDetectionExecutor setMaxRayTests(int maxRayTests) {
      this.maxRayTests = maxRayTests;
      return this;
   }

   @Nonnull
   public HitDetectionExecutor setOrigin(double x, double y, double z) {
      this.origin.assign(x, y, z, 1.0);
      return this;
   }

   private void setupMatrices(@Nonnull Matrix4d modelMatrix) {
      Matrix4d projectionMatrix = this.projectionProvider.getMatrix();
      Matrix4d viewMatrix = this.viewProvider.getMatrix();
      this.pvmMatrix.assign(projectionMatrix).multiply(viewMatrix);
      this.invPvMatrix.assign(this.pvmMatrix).invert();
      this.pvmMatrix.multiply(modelMatrix);
   }

   public boolean test(@Nonnull Vector4d point, @Nonnull Matrix4d modelMatrix) {
      this.setupMatrices(modelMatrix);
      return this.testPoint(point);
   }

   public boolean test(@Nonnull Quad4d[] model, @Nonnull Matrix4d modelMatrix) {
      try {
         this.setupMatrices(modelMatrix);
         return this.testModel(model);
      } catch (Throwable var4) {
         log.at(Level.SEVERE).withCause(var4).log("Error occured during Hit Detection execution. Dumping parameters!");
         log.at(Level.SEVERE).log("this = %s", this);
         log.at(Level.SEVERE).log("model = %s", Arrays.toString((Object[])model));
         log.at(Level.SEVERE).log("modelMatrix = %s", modelMatrix);
         log.at(Level.SEVERE).log("thread = %s", Thread.currentThread().getName());
         throw var4;
      }
   }

   private boolean testPoint(@Nonnull Vector4d point) {
      this.pvmMatrix.multiply(point, this.buffer.transformedPoint);
      if (!this.buffer.transformedPoint.isInsideFrustum()) {
         return false;
      } else {
         Vector4d hit = this.buffer.transformedPoint;
         this.invPvMatrix.multiply(hit);
         hit.perspectiveTransform();
         return this.losProvider.test(this.origin.x, this.origin.y, this.origin.z, hit.x, hit.y, hit.z);
      }
   }

   private boolean testModel(@Nonnull Quad4d[] model) {
      int testsDone = 0;
      double minDistanceSquared = Double.POSITIVE_INFINITY;

      for (Quad4d quad : model) {
         if (testsDone++ == this.maxRayTests) {
            return false;
         }

         quad.multiply(this.pvmMatrix, this.buffer.transformedQuad);
         if (this.insideFrustum()) {
            Vector4d hit = this.buffer.tempHitPosition;
            if (this.buffer.containsFully) {
               this.buffer.transformedQuad.getRandom(this.buffer.random, hit);
            } else {
               this.buffer.visibleTriangle.getRandom(this.buffer.random, hit);
            }

            this.invPvMatrix.multiply(hit);
            hit.perspectiveTransform();
            double dx = this.origin.x - hit.x;
            double dy = this.origin.y - hit.y;
            double dz = this.origin.z - hit.z;
            double distanceSquared = dx * dx + dy * dy + dz * dz;
            if (!(distanceSquared >= minDistanceSquared) && this.losProvider.test(this.origin.x, this.origin.y, this.origin.z, hit.x, hit.y, hit.z)) {
               minDistanceSquared = distanceSquared;
               this.buffer.hitPosition.assign(hit);
            }
         }
      }

      return minDistanceSquared != Double.POSITIVE_INFINITY;
   }

   protected boolean insideFrustum() {
      Quad4d quad = this.buffer.transformedQuad;
      if (quad.isFullyInsideFrustum()) {
         this.buffer.containsFully = true;
         return true;
      } else {
         this.buffer.containsFully = false;
         Vector4dBufferList vertices = this.buffer.vertexList1;
         Vector4dBufferList auxillaryList = this.buffer.vertexList2;
         vertices.clear();
         auxillaryList.clear();
         vertices.next().assign(quad.getA());
         vertices.next().assign(quad.getB());
         vertices.next().assign(quad.getC());
         vertices.next().assign(quad.getD());
         if (this.clipPolygonAxis(0) && this.clipPolygonAxis(1) && this.clipPolygonAxis(2)) {
            Vector4d initialVertex = vertices.get(0);
            int i = 1;
            if (i < vertices.size() - 1) {
               Triangle4d triangle = this.buffer.visibleTriangle;
               triangle.assign(initialVertex, vertices.get(i), vertices.get(i + 1));
               return true;
            }
         }

         return false;
      }
   }

   private boolean clipPolygonAxis(int componentIndex) {
      clipPolygonComponent(this.buffer.vertexList1, componentIndex, 1.0, this.buffer.vertexList2);
      this.buffer.vertexList1.clear();
      if (this.buffer.vertexList2.isEmpty()) {
         return false;
      } else {
         clipPolygonComponent(this.buffer.vertexList2, componentIndex, -1.0, this.buffer.vertexList1);
         this.buffer.vertexList2.clear();
         return !this.buffer.vertexList1.isEmpty();
      }
   }

   private static void clipPolygonComponent(
      @Nonnull Vector4dBufferList vertices, int componentIndex, double componentFactor, @Nonnull Vector4dBufferList result
   ) {
      Vector4d previousVertex = vertices.get(vertices.size() - 1);
      double previousComponent = previousVertex.get(componentIndex) * componentFactor;
      boolean previousInside = previousComponent <= previousVertex.w;

      for (int i = 0; i < vertices.size(); i++) {
         Vector4d vertex = vertices.get(i);
         double component = vertex.get(componentIndex) * componentFactor;
         boolean inside = component <= vertex.w;
         if (inside ^ previousInside) {
            double lerp = (previousVertex.w - previousComponent) / (previousVertex.w - previousComponent - (vertex.w - component));
            previousVertex.lerp(vertex, lerp, result.next());
         }

         if (inside) {
            result.next().assign(vertex);
         }

         previousVertex = vertex;
         previousComponent = component;
         previousInside = inside;
      }
   }

   @Nonnull
   @Override
   public String toString() {
      return "HitDetectionExecutor{pvmMatrix="
         + this.pvmMatrix
         + ", invPvMatrix="
         + this.invPvMatrix
         + ", origin="
         + this.origin
         + ", buffer="
         + this.buffer
         + ", projectionProvider="
         + this.projectionProvider
         + ", viewProvider="
         + this.viewProvider
         + ", losProvider="
         + this.losProvider
         + ", maxRayTests="
         + this.maxRayTests
         + "}";
   }
}
