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

package com.hypixel.hytale.math.hitdetection;

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

public class HitDetectionExecutor
{
    public static final HytaleLogger log;
    private static final Vector4d[] VERTEX_POINTS;
    public static final Quad4d[] CUBE_QUADS;
    @Nonnull
    private final Matrix4d pvmMatrix;
    @Nonnull
    private final Matrix4d invPvMatrix;
    @Nonnull
    private final Vector4d origin;
    @Nonnull
    private final HitDetectionBuffer buffer;
    private MatrixProvider projectionProvider;
    private MatrixProvider viewProvider;
    private LineOfSightProvider losProvider;
    private int maxRayTests;
    
    public HitDetectionExecutor() {
        this.pvmMatrix = new Matrix4d();
        this.invPvMatrix = new Matrix4d();
        this.origin = new Vector4d();
        this.buffer = new HitDetectionBuffer();
        this.losProvider = LineOfSightProvider.DEFAULT_TRUE;
        this.maxRayTests = 10;
    }
    
    public Vector4d getHitLocation() {
        return this.buffer.hitPosition;
    }
    
    @Nonnull
    public HitDetectionExecutor setProjectionProvider(final MatrixProvider provider) {
        this.projectionProvider = provider;
        return this;
    }
    
    @Nonnull
    public HitDetectionExecutor setViewProvider(final MatrixProvider provider) {
        this.viewProvider = provider;
        return this;
    }
    
    @Nonnull
    public HitDetectionExecutor setLineOfSightProvider(final LineOfSightProvider losProvider) {
        this.losProvider = losProvider;
        return this;
    }
    
    @Nonnull
    public HitDetectionExecutor setMaxRayTests(final int maxRayTests) {
        this.maxRayTests = maxRayTests;
        return this;
    }
    
    @Nonnull
    public HitDetectionExecutor setOrigin(final double x, final double y, final double z) {
        this.origin.assign(x, y, z, 1.0);
        return this;
    }
    
    private void setupMatrices(@Nonnull final Matrix4d modelMatrix) {
        final Matrix4d projectionMatrix = this.projectionProvider.getMatrix();
        final 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 final Vector4d point, @Nonnull final Matrix4d modelMatrix) {
        this.setupMatrices(modelMatrix);
        return this.testPoint(point);
    }
    
    public boolean test(@Nonnull final Quad4d[] model, @Nonnull final Matrix4d modelMatrix) {
        try {
            this.setupMatrices(modelMatrix);
            return this.testModel(model);
        }
        catch (final Throwable t) {
            HitDetectionExecutor.log.at(Level.SEVERE).withCause(t).log("Error occured during Hit Detection execution. Dumping parameters!");
            HitDetectionExecutor.log.at(Level.SEVERE).log("this = %s", this);
            HitDetectionExecutor.log.at(Level.SEVERE).log("model = %s", Arrays.toString(model));
            HitDetectionExecutor.log.at(Level.SEVERE).log("modelMatrix = %s", modelMatrix);
            HitDetectionExecutor.log.at(Level.SEVERE).log("thread = %s", Thread.currentThread().getName());
            throw t;
        }
    }
    
    private boolean testPoint(@Nonnull final Vector4d point) {
        this.pvmMatrix.multiply(point, this.buffer.transformedPoint);
        if (!this.buffer.transformedPoint.isInsideFrustum()) {
            return false;
        }
        final 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 final Quad4d[] model) {
        int testsDone = 0;
        double minDistanceSquared = Double.POSITIVE_INFINITY;
        for (final Quad4d quad : model) {
            if (testsDone++ == this.maxRayTests) {
                return false;
            }
            quad.multiply(this.pvmMatrix, this.buffer.transformedQuad);
            if (this.insideFrustum()) {
                final 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();
                final double dx = this.origin.x - hit.x;
                final double dy = this.origin.y - hit.y;
                final double dz = this.origin.z - hit.z;
                final double distanceSquared = dx * dx + dy * dy + dz * dz;
                if (distanceSquared < minDistanceSquared) {
                    if (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() {
        final Quad4d quad = this.buffer.transformedQuad;
        if (quad.isFullyInsideFrustum()) {
            return this.buffer.containsFully = true;
        }
        this.buffer.containsFully = false;
        final Vector4dBufferList vertices = this.buffer.vertexList1;
        final 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)) {
            final Vector4d initialVertex = vertices.get(0);
            final int i = 1;
            if (i < vertices.size() - 1) {
                final Triangle4d triangle = this.buffer.visibleTriangle;
                triangle.assign(initialVertex, vertices.get(i), vertices.get(i + 1));
                return true;
            }
        }
        return false;
    }
    
    private boolean clipPolygonAxis(final int componentIndex) {
        clipPolygonComponent(this.buffer.vertexList1, componentIndex, 1.0, this.buffer.vertexList2);
        this.buffer.vertexList1.clear();
        if (this.buffer.vertexList2.isEmpty()) {
            return false;
        }
        clipPolygonComponent(this.buffer.vertexList2, componentIndex, -1.0, this.buffer.vertexList1);
        this.buffer.vertexList2.clear();
        return !this.buffer.vertexList1.isEmpty();
    }
    
    private static void clipPolygonComponent(@Nonnull final Vector4dBufferList vertices, final int componentIndex, final double componentFactor, @Nonnull final 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) {
            final Vector4d vertex = vertices.get(i);
            final double component = vertex.get(componentIndex) * componentFactor;
            final boolean inside = component <= vertex.w;
            if (inside ^ previousInside) {
                final 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=" + String.valueOf(this.pvmMatrix) + ", invPvMatrix=" + String.valueOf(this.invPvMatrix) + ", origin=" + String.valueOf(this.origin) + ", buffer=" + String.valueOf(this.buffer) + ", projectionProvider=" + String.valueOf(this.projectionProvider) + ", viewProvider=" + String.valueOf(this.viewProvider) + ", losProvider=" + String.valueOf(this.losProvider) + ", maxRayTests=" + this.maxRayTests;
    }
    
    static {
        log = HytaleLogger.forEnclosingClass();
        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) };
        CUBE_QUADS = new Quad4d[] { new Quad4d(HitDetectionExecutor.VERTEX_POINTS, 0, 1, 3, 2), new Quad4d(HitDetectionExecutor.VERTEX_POINTS, 0, 4, 5, 1), new Quad4d(HitDetectionExecutor.VERTEX_POINTS, 4, 5, 7, 6), new Quad4d(HitDetectionExecutor.VERTEX_POINTS, 2, 3, 7, 6), new Quad4d(HitDetectionExecutor.VERTEX_POINTS, 1, 3, 7, 5), new Quad4d(HitDetectionExecutor.VERTEX_POINTS, 0, 2, 6, 4) };
    }
}
