// • ▌ ▄ ·.  ▄▄▄·  ▄▄ • ▪   ▄▄· ▄▄▄▄·  ▄▄▄·  ▐▄▄▄  ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀  █▪▀▀▀ ▀  ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀  ▀  ▀ ▀▀  █▪ ▀▀▀
//      Magicbane Emulator Project © 2013 - 2022
//                www.magicbane.com

package engine.math;

import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.concurrent.ThreadLocalRandom;

import static engine.math.FastMath.sqr;

public class Vector3fImmutable {

    public static final Vector3fImmutable ZERO = new Vector3fImmutable(0, 0, 0);
    public final float x, y, z;

    public Vector3fImmutable() {
        x = y = z = 0.0f;
    }

    public Vector3fImmutable(float x, float y, float z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public Vector3fImmutable(Vector3f original) {
        this.x = original.x;
        this.y = original.y;
        this.z = original.z;
    }

    public Vector3fImmutable(Vector3fImmutable original) {
        this.x = original.x;
        this.y = original.y;
        this.z = original.z;
    }

    public static Vector3fImmutable scaleAdd(float scalar, Vector3fImmutable mult,
                                             Vector3fImmutable add) {
        return new Vector3fImmutable(mult.x * scalar + add.x, mult.y * scalar
                + add.y, mult.z * scalar + add.z);
    }

    public static boolean isValidVector(Vector3fImmutable vector) {
        if (vector == null)
            return false;
        if (Float.isNaN(vector.x) || Float.isNaN(vector.y)
                || Float.isNaN(vector.z))
            return false;
        return !Float.isInfinite(vector.x) && !Float.isInfinite(vector.y)
                && !Float.isInfinite(vector.z);
    }

    public static Vector3fImmutable readExternal(ObjectInput in)
            throws IOException, ClassNotFoundException {
        return new Vector3fImmutable(in.readFloat(), in.readFloat(), in
                .readFloat());
    }

    public static String toString(Vector3fImmutable vector) {

        return vector.toString();
    }

    public static Vector3fImmutable ClosestPointOnLine(Vector3fImmutable lineStart, Vector3fImmutable lineEnd, Vector3fImmutable sourcePoint) {

        Vector3fImmutable closestPoint;
        Vector3fImmutable lineStartToTarget;
        Vector3fImmutable lineDirection;
        float lineLength;
        float dotProduct;

        lineStartToTarget = sourcePoint.subtract(lineStart);
        lineDirection = lineEnd.subtract(lineStart).normalize();
        lineLength = lineStart.distance2D(lineEnd);

        dotProduct = lineDirection.dot(lineStartToTarget);

        if (dotProduct <= 0)
            return lineStart;

        if (dotProduct >= lineLength)
            return lineEnd;

        // Project the point by advancing it along the line from
        // the starting point.

        closestPoint = lineDirection.mult(dotProduct);
        closestPoint = lineStart.add(closestPoint);

        return closestPoint;
    }

    public static Vector3fImmutable rotateAroundPoint(Vector3fImmutable origin, Vector3fImmutable point, int angle) {

        float angleRadians;
        int modifiedAngle;

        // Convert angle to radians

        modifiedAngle = angle;

        if (angle < 0)
            modifiedAngle = 360 + modifiedAngle;

        angleRadians = (float) Math.toRadians(modifiedAngle);

        return rotateAroundPoint(origin, point, angleRadians);
    }

    public static Vector3fImmutable rotateAroundPoint(Vector3fImmutable origin, Vector3fImmutable point, float radians) {

        Vector3fImmutable outVector;
        Vector3f directionVector;
        Quaternion angleRotation;

        // Build direction vector relative to origin

        directionVector = new Vector3f(point.subtract(origin));

        // Build quaternion rotation

        angleRotation = new Quaternion().fromAngleAxis(radians, new Vector3f(0, 1, 0));
        // Apply rotation to direction vector

        directionVector = angleRotation.mult(directionVector);

        // Translate from origin back to new rotated point

        outVector = origin.add(directionVector);

        return outVector;

    }

    public static Vector3fImmutable rotateAroundPoint(Vector3fImmutable origin, Vector3fImmutable point, Quaternion angleRotation) {

        Vector3fImmutable outVector;
        Vector3f directionVector;
        // Build direction vector relative to origin
        directionVector = new Vector3f(point.subtract(origin));

        // Build quaternion rotation


        // Apply rotation to direction vector


        directionVector = angleRotation.mult(directionVector);

        // Translate from origin back to new rotated point

        outVector = origin.add(directionVector);

        return outVector;

    }

    public static Vector3fImmutable rotateAroundPoint(Vector3fImmutable origin, Vector3fImmutable point, float w, Vector3f axis) {

        Vector3fImmutable outVector;
        Vector3f directionVector;
        Quaternion angleRotation;

        // Build direction vector relative to origin

        directionVector = new Vector3f(point.subtract(origin));

        // Build quaternion rotation

        angleRotation = new Quaternion().fromAngleAxis(w, axis);
        // Apply rotation to direction vector

        directionVector = angleRotation.mult(directionVector);

        // Translate from origin back to new rotated point

        outVector = origin.add(directionVector);

        return outVector;

    }

    public static Vector3fImmutable getRandomPointInCircle(Vector3fImmutable origin, float radius) {
        // Member variables

        float targetAngle;
        float targetRadius;
        Vector3fImmutable targetPosition;

        targetAngle = (float) (ThreadLocalRandom.current().nextFloat() * Math.PI * 2);
        targetRadius = (float) (Math.sqrt(ThreadLocalRandom.current().nextFloat()) * radius);
        targetPosition = new Vector3fImmutable((float) (origin.x + targetRadius * Math.cos(targetAngle)), origin.y, (float) (origin.z + targetRadius * Math.sin(targetAngle)));
        return targetPosition;
    }

    public static Vector3fImmutable getLocBetween(Vector3fImmutable start, Vector3fImmutable end) {
        // Member variables

        Vector3fImmutable faceDirection = end.subtract(start).normalize();
        float distance = end.distance(start) * .5f;
        return faceDirection.scaleAdd(distance, start);
    }

    public static Vector3fImmutable getRandomPointOnCircle(Vector3fImmutable origin, float radius) {

        // Member variables

        int randomAngle;
        Vector3fImmutable targetPosition;

        randomAngle = ThreadLocalRandom.current().nextInt(360);

        targetPosition = new Vector3fImmutable((float) (origin.x + radius * Math.cos(randomAngle)), origin.y, (float) (origin.z + radius * Math.sin(randomAngle)));
        return targetPosition;
    }

    public static Vector3fImmutable transform(Vector3fImmutable origin, Vector3fImmutable point, float angle) {

        //TRANSLATE TO ORIGIN
        float x1 = point.x - origin.x;
        float y1 = point.z - origin.z;

        //APPLY ROTATION
        float temp_x1 = (float) (x1 * Math.cos(angle) - y1 * Math.sin(angle));
        float temp_z1 = (float) (x1 * Math.sin(angle) + y1 * Math.cos(angle));

        temp_x1 += origin.x;
        temp_z1 += origin.z;

        return new Vector3fImmutable(temp_x1, point.y, temp_z1);
    }

    public boolean isInsideCircle(Vector3fImmutable circleCenter, float radius) {

        return (circleCenter.distanceSquared2D(this) < sqr(radius));
    }

    public Vector3fImmutable add(Vector3f vec) {
        if (null == vec)
            return null;
        return new Vector3fImmutable(x + vec.x, y + vec.y, z + vec.z);
    }

    public Vector3fImmutable add(Vector3fImmutable vec) {
        if (null == vec)
            return null;
        return new Vector3fImmutable(x + vec.x, y + vec.y, z + vec.z);
    }

    public Vector3fImmutable add(float x, float y, float z) {
        return new Vector3fImmutable(this.x + x, this.y + y, this.z + z);
    }

    public Vector3fImmutable scaleAdd(float scalar, Vector3fImmutable add) {
        return new Vector3fImmutable(x * scalar + add.x, y * scalar + add.y, z
                * scalar + add.z);
    }

    public float dot(Vector3fImmutable vec) {
        if (null == vec) {
            return 0.0f;
        }

        return x * vec.x + y * vec.y + z * vec.z;
    }

    public float dot2D(Vector3fImmutable vec) {
        if (null == vec) {
            return 0.0f;
        }

        return x * vec.x + z * vec.z;
    }

    public Vector3fImmutable cross(Vector3fImmutable v) {
        return cross(v.x, v.y, v.z);
    }

    public Vector3fImmutable cross(float x, float y, float z) {
        return new Vector3fImmutable(this.y * z - this.z * y, this.z * x
                - this.x * z, this.x * y - this.y * x);
    }

    public float length() {
        return FastMath.sqrt(lengthSquared());
    }

    public float lengthSquared() {
        return x * x + y * y + z * z;
    }

    public float distanceSquared(Vector3fImmutable v) {
        double dx = x - v.x;
        double dy = y - v.y;
        double dz = z - v.z;
        return (float) (dx * dx + dy * dy + dz * dz);
    }

    public float magnitude() {
        return FastMath.sqrt(sqrMagnitude());
    }

    public float sqrMagnitude() {
        return x * x + y * y + z * z;
    }

    public Vector3fImmutable moveTowards(Vector3fImmutable target, float maxDistanceDelta) {
        Vector3fImmutable outVector;

        Vector3fImmutable direction = target.subtract2D(this);
        float magnitude = direction.magnitude();

        if (magnitude <= maxDistanceDelta || magnitude == 0f) {
            return target;
        }

        outVector = direction.divide(magnitude).mult(maxDistanceDelta);
        outVector = this.add(outVector);
        return outVector;
    }

    public float distanceSquared2D(Vector3fImmutable v) {
        double dx = x - v.x;
        double dz = z - v.z;
        return (float) (dx * dx + dz * dz);
    }

    public float distance(Vector3fImmutable v) {
        return FastMath.sqrt(distanceSquared(v));
    }

    public float distance2D(Vector3fImmutable v) {
        return FastMath.sqrt(distanceSquared2D(v));
    }

    public Vector3fImmutable mult(float scalar) {
        return new Vector3fImmutable(x * scalar, y * scalar, z * scalar);
    }

    public Vector3fImmutable mult(Vector3fImmutable vec) {
        if (null == vec) {
            return null;
        }

        return new Vector3fImmutable(x * vec.x, y * vec.y, z * vec.z);
    }

    public Vector3fImmutable divide(float scalar) {
        scalar = 1f / scalar;
        return new Vector3fImmutable(x * scalar, y * scalar, z * scalar);
    }

    public Vector3fImmutable divide(Vector3fImmutable scalar) {
        return new Vector3fImmutable(x / scalar.x, y / scalar.y, z / scalar.z);
    }

    public Vector3fImmutable negate() {
        return new Vector3fImmutable(-x, -y, -z);
    }

    public Vector3fImmutable subtract(Vector3fImmutable vec) {
        return new Vector3fImmutable(x - vec.x, y - vec.y, z - vec.z);
    }

    public Vector3fImmutable subtract2D(Vector3fImmutable vec) {
        return new Vector3fImmutable(x - vec.x, 0, z - vec.z);
    }

    public Vector3fImmutable subtract(float x, float y, float z) {
        return new Vector3fImmutable(this.x - x, this.y - y, this.z - z);
    }

    public Vector3fImmutable normalize() {
        float length = length();
        if (length != 0) {
            return divide(length);
        }

        return divide(1);
    }

    public float angleBetween(Vector3fImmutable otherVector) {
        float dotProduct = dot(otherVector);
        return FastMath.acos(dotProduct);
    }

    public float angleBetween2D(Vector3fImmutable otherVector) {
        float dotProduct = dot(otherVector);
        return FastMath.acos(dotProduct);
    }

    public Vector3fImmutable interpolate(Vector3f finalVec, float changeAmnt) {
        return new Vector3fImmutable((1 - changeAmnt) * this.x + changeAmnt
                * finalVec.x, (1 - changeAmnt) * this.y + changeAmnt
                * finalVec.y, (1 - changeAmnt) * this.z + changeAmnt
                * finalVec.z);
    }

    public Vector3fImmutable interpolate(Vector3fImmutable finalVec, float changeAmnt) {
        return new Vector3fImmutable((1 - changeAmnt) * this.x + changeAmnt
                * finalVec.x, (1 - changeAmnt) * this.y + changeAmnt
                * finalVec.y, (1 - changeAmnt) * this.z + changeAmnt
                * finalVec.z);
    }

    @Override
    public Vector3fImmutable clone() throws CloneNotSupportedException {
        return (Vector3fImmutable) super.clone();
    }

    public float[] toArray(float[] floats) {
        if (floats == null) {
            floats = new float[3];
        }
        floats[0] = x;
        floats[1] = y;
        floats[2] = z;
        return floats;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Vector3fImmutable)) {
            return false;
        }

        if (this == o) {
            return true;
        }

        Vector3fImmutable comp = (Vector3fImmutable) o;
        if (Float.compare(x, comp.x) != 0)
            return false;
        if (Float.compare(y, comp.y) != 0)
            return false;
        return Float.compare(z, comp.z) == 0;
    }

    @Override
    public int hashCode() {
        int hash = 37;
        hash += 37 * hash + Float.floatToIntBits(x);
        hash += 37 * hash + Float.floatToIntBits(y);
        hash += 37 * hash + Float.floatToIntBits(z);
        return hash;
    }

    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeFloat(x);
        out.writeFloat(y);
        out.writeFloat(z);
    }

    public Vector3fImmutable getOffset(float rotation, float xOffset, float yOffset, float zOffset, boolean invertZ) {
        float sin = FastMath.sin(rotation);
        float cos = FastMath.cos(rotation);
        Vector3f faceDir = new Vector3f(sin, 0f, cos);
        Vector3f crossDir = new Vector3f(cos, 0f, sin);
        faceDir.multLocal(zOffset);
        crossDir.multLocal(xOffset);
        if (invertZ) {
            faceDir.z = -faceDir.z;
            crossDir.z = -crossDir.z;
        }
        Vector3f loc = new Vector3f(this);
        loc.addLocal(faceDir);
        loc.addLocal(crossDir);
        loc.y += yOffset;
        return new Vector3fImmutable(loc);
    }

    public float getX() {
        return x;
    }

    public Vector3fImmutable setX(float x) {
        return new Vector3fImmutable(x, y, z);
    }

    public float getY() {
        return y;
    }

    public Vector3fImmutable setY(float y) {
        return new Vector3fImmutable(x, y, z);
    }

    public float getZ() {
        return z;
    }

    public Vector3fImmutable setZ(float z) {
        return new Vector3fImmutable(x, y, z);
    }

    public float get(int index) {
        switch (index) {
            case 0:
                return x;
            case 1:
                return y;
            case 2:
                return z;
        }
        throw new IllegalArgumentException("index must be either 0, 1 or 2");
    }

    public Vector2f getLatLong() {
        return new Vector2f(this.x, this.z);
    }

    public synchronized float getLat() {
        return x;
    }

    public synchronized float getLong() {
        return z;
    }

    public synchronized float getAlt() {
        return y;
    }

    public float getRotation() {
        return 3.14f + FastMath.atan2(-x, -z);
    }

    public boolean inRange2D(Vector3fImmutable otherVec, float range) {
        float distance = this.distanceSquared2D(otherVec);
        return !(distance > range * range);
    }

    @Override
    public String toString() {

        String outString;

        outString = "(" + this.x + '/' + this.y + '/' + this.z;
        return outString;

    }

    public String toString2D() {

        String outString;

        outString = "( " + (int) this.x + " , " + (int) (this.z * -1) + " )";
        return outString;

    }

    public Vector3fImmutable ClosestPointOnLine(Vector3fImmutable lineStart, Vector3fImmutable lineEnd) {

        Vector3fImmutable closestPoint;
        Vector3fImmutable lineStartToTarget;
        Vector3fImmutable lineDirection;
        float lineLength;
        float dotProduct;

        lineStartToTarget = this.subtract(lineStart);
        lineDirection = lineEnd.subtract(lineStart).normalize();
        lineLength = lineStart.distance2D(lineEnd);

        dotProduct = lineDirection.dot(lineStartToTarget);

        if (dotProduct <= 0)
            return lineStart;

        if (dotProduct >= lineLength)
            return lineEnd;

        // Project the point by advancing it along the line from
        // the starting point.

        closestPoint = lineDirection.mult(dotProduct);
        closestPoint = lineStart.add(closestPoint);

        return closestPoint;
    }

    public float Lerp(Vector3fImmutable dest, float lerpFactor) {
        return dest.subtract(this).mult(lerpFactor).add(this).y;
    }
}