package com.ThirtyNineEighty.Base.Common.Math;

import android.opengl.Matrix;

import java.io.Serializable;
import java.nio.ByteBuffer;
import java.text.NumberFormat;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;

/*
 * Operation with prefix get - immutable;
 */
public class Vector3
  extends Vector
  implements Serializable
{
  private static final long serialVersionUID = 1L;

  public final static Vector3 xAxis = new Vector3(1.0f, 0.0f, 0.0f);
  public final static Vector3 yAxis = new Vector3(0.0f, 1.0f, 0.0f);
  public final static Vector3 zAxis = new Vector3(0.0f, 0.0f, 1.0f);
  public final static Vector3 zero = new Vector3(0.0f, 0.0f, 0.0f);
  public final static int size = 3;

  public static final VectorsPool<Vector3> pool = new VectorsPool<>("Vector3", poolLimit);

  public static Vector3 getInstance(Vector other)
  {
    Vector3 vector = getInstance();
    vector.setFrom(other);
    return vector;
  }

  public static Vector3 getInstance(float... values)
  {
    Vector3 vector = getInstance();
    System.arraycopy(values, 0, vector.value, 0, values.length);
    return vector;
  }

  public static Vector3 getInstance(ByteBuffer dataBuffer)
  {
    Vector3 vector = getInstance();
    for (int i = 0; i < size; i++)
      vector.value[i] = dataBuffer.getFloat();
    return vector;
  }

  public static Vector3 getInstance()
  {
    Vector3 vector = pool.acquire();
    if (vector == null)
      vector = new Vector3();
    return vector;
  }

  public static void release(Vector3 vector)
  {
    pool.release(vector);
  }

  public static void release(Collection<Vector3> vectors)
  {
    pool.release(vectors);
  }

  public Vector3()
  {
    value = new float[4];
    value[3] = 1.0f;
  }

  public Vector3(float x, float y, float z)
  {
    this();
    setFrom(x, y, z);
  }

  public Vector3(float x, float y, float z, float f)
  {
    this(x, y, z);
    value[3] = f;
  }

  public Vector3(float[] raw)
  {
    value = raw;
  }

  public Vector3(Vector3 vec)
  {
    this();
    setFrom(vec);
  }

  /**
   * Input format {%f; %f; %f}
   */
  public Vector3(String vectorStr) throws ParseException
  {
    this();

    String[] floats = vectorStr
      .replaceAll("\\{", "")
      .replaceAll("\\}", "")
      .replaceAll(" ", "")
      .split(";");

    NumberFormat format = NumberFormat.getInstance();
    Number number;

    number = format.parse(floats[0]);
    value[0] = number.floatValue();

    number = format.parse(floats[1]);
    value[1] = number.floatValue();

    number = format.parse(floats[2]);
    value[2] = number.floatValue();
  }

  public void setFrom(float x, float y, float z)
  {
    throwIfReleased();

    value[0] = x;
    value[1] = y;
    value[2] = z;
    value[3] = 1.0f;
  }

  public void setFrom(Vector3 vec)
  {
    throwIfReleased();

    setFrom(vec.getX(), vec.getY(), vec.getZ());
  }

  public void setFromAngles(Vector3 angles)
  {
    float[] matrix = new float[16];
    Matrix.setIdentityM(matrix, 0);
    Matrix.rotateM(matrix, 0, angles.getX(), 1, 0, 0);
    Matrix.rotateM(matrix, 0, angles.getY(), 0, 1, 0);
    Matrix.rotateM(matrix, 0, angles.getZ(), 0, 0, 1);

    Matrix.multiplyMV(getRaw(), 0, matrix, 0, Vector3.xAxis.getRaw(), 0);
    normalize();
  }

  public float getX() { throwIfReleased(); return value[0]; }
  public float getY() { throwIfReleased(); return value[1]; }
  public float getZ() { throwIfReleased(); return value[2]; }

  public void setX(float v) { throwIfReleased(); value[0] = v; }
  public void setY(float v) { throwIfReleased(); value[1] = v; }
  public void setZ(float v) { throwIfReleased(); value[2] = v; }

  public void addToX(float v) { throwIfReleased(); value[0] += v; }
  public void addToY(float v) { throwIfReleased(); value[1] += v; }
  public void addToZ(float v) { throwIfReleased(); value[2] += v; }

  public void multiplyToX(float v) { throwIfReleased(); value[0] *= v; }
  public void multiplyToY(float v) { throwIfReleased(); value[1] *= v; }
  public void multiplyToZ(float v) { throwIfReleased(); value[2] *= v; }

  public float getLength()
  {
    throwIfReleased();

    double powX = Math.pow(getX(), 2);
    double powY = Math.pow(getY(), 2);
    double powZ = Math.pow(getZ(), 2);

    return (float)Math.sqrt(powX + powY + powZ);
  }

  public float getAngle(Vector3 other)
  {
    throwIfReleased();

    Vector3 normal = getCross(other);

    if (normal.isZero())
      return getScalar(other) > 0 ? 0 : 180;

    Plane plane = new Plane(normal);

    Vector2 vecOne = plane.getProjection(this);
    Vector2 vecTwo = plane.getProjection(other);

    plane.release();

    float angle = vecOne.getAngle(vecTwo);

    Vector2.release(vecOne);
    Vector2.release(vecTwo);

    return angle;
  }

  public void normalize()
  {
    throwIfReleased();

    float length = getLength();

    if (length == 0f)
      return;

    value[0] /= length;
    value[1] /= length;
    value[2] /= length;
  }

  public void scale(float coefficient)
  {
    throwIfReleased();

    value[0] *= coefficient;
    value[1] *= coefficient;
    value[2] *= coefficient;
  }

  public void cross(Vector3 other)
  {
    throwIfReleased();

    float[] otherValue = other.getRaw();

    float result1 =      value[1] * otherValue[2];
    float result2 = -1 * value[2] * otherValue[1];
    float result3 = -1 * value[0] * otherValue[2];
    float result4 =      value[2] * otherValue[0];
    float result5 =      value[0] * otherValue[1];
    float result6 = -1 * value[1] * otherValue[0];

    setFrom(result1 + result2, result3 + result4, result5 + result6);
  }

  @SuppressWarnings("SuspiciousNameCombination")
  public void orthogonal()
  {
    throwIfReleased();

    float x = getX();
    float y = getY();
    float z = getZ();

    setFrom(-y, x, 0);

    if (isZero())
      setFrom(0, z, -y);
  }

  public void add(Vector3 other)
  {
    throwIfReleased();

    addToX(other.getX());
    addToY(other.getY());
    addToZ(other.getZ());
  }

  public void subtract(Vector3 other)
  {
    throwIfReleased();

    addToX(- other.getX());
    addToY(- other.getY());
    addToZ(- other.getZ());
  }

  public void multiply(float coefficient)
  {
    throwIfReleased();

    value[0] = value[0] * coefficient;
    value[1] = value[1] * coefficient;
    value[2] = value[2] * coefficient;
  }

  public void move(float length, Vector3 angles)
  {
    throwIfReleased();

    Vector3 vector = getInstance();
    vector.setFromAngles(angles);

    value[0] += vector.getX() * length;
    value[1] += vector.getY() * length;
    value[2] += vector.getZ() * length;

    Vector3.release(vector);
  }

  public Vector2 getProjection(ArrayList<Vector3> vertices)
  {
    Vector2 result = null;

    for (Vector3 current : vertices)
    {
      float projection = getScalar(current);

      if (result == null)
        result = Vector2.getInstance(projection, projection);

      // x - max
      if (projection > result.getX())
        result.setX(projection);

      // y - min
      if (projection < result.getY())
        result.setY(projection);
    }

    return result;
  }

  public void lineProjection(Vector3 start, Vector3 end)
  {
    // a * dot(a, b) / dot(a,a)
    Vector3 lineVector = end.getSubtract(start);
    subtract(start);

    float first = lineVector.getScalar(this);
    float second = lineVector.getScalar(lineVector);

    setFrom(lineVector);
    multiply(first / second);
    add(start);

    Vector3.release(lineVector);
  }

  public float getScalar(Vector3 other)
  {
    throwIfReleased();

    float multOne   = getX() * other.getX();
    float multTwo   = getY() * other.getY();
    float multThree = getZ() * other.getZ();

    return multOne + multTwo + multThree;
  }

  public Vector3 getNormalize()
  {
    throwIfReleased();

    Vector3 result = getInstance(this);
    result.normalize();
    return result;
  }

  public Vector3 getScale(float coefficient)
  {
    throwIfReleased();

    Vector3 result = getInstance(this);
    result.scale(coefficient);
    return result;
  }

  public Vector3 getCross(Vector3 other)
  {
    throwIfReleased();

    Vector3 result = getInstance(this);
    result.cross(other);
    return result;
  }

  public Vector3 getOrthogonal()
  {
    throwIfReleased();

    Vector3 result = getInstance(this);
    result.orthogonal();
    return result;
  }

  public Vector3 getSum(Vector3 other)
  {
    throwIfReleased();

    Vector3 result = getInstance(this);
    result.add(other);
    return result;
  }

  public Vector3 getSum(float x, float y, float z)
  {
    throwIfReleased();

    Vector3 result = getInstance(this);
    result.addToX(x);
    result.addToY(y);
    result.addToZ(z);
    return result;
  }

  public Vector3 getSubtract(Vector3 other)
  {
    throwIfReleased();

    Vector3 result = getInstance(this);
    result.subtract(other);
    return result;
  }

  public Vector3 getMultiply(float coefficient)
  {
    throwIfReleased();

    Vector3 result = getInstance(this);
    result.multiply(coefficient);
    return result;
  }

  public Vector3 getMove(float length, Vector3 angles)
  {
    throwIfReleased();

    Vector3 result = getInstance(this);
    result.move(length, angles);
    return result;
  }

  public Vector3 getLineProjection(Vector3 start, Vector3 end)
  {
    throwIfReleased();

    Vector3 result = getInstance(this);
    result.lineProjection(start, end);
    return result;
  }

  @Override
  public int getSize()
  {
    return size;
  }

  @Override
  public void clear()
  {
    value[0] = 0;
    value[1] = 0;
    value[2] = 0;
    value[3] = 1;
  }

  @Override
  public boolean equals(Object o)
  {
    throwIfReleased();

    Vector3 other = o instanceof Vector3 ? (Vector3)o : null;

    return other != null
      && Math.abs(other.getX() - getX()) < epsilon
      && Math.abs(other.getY() - getY()) < epsilon
      && Math.abs(other.getZ() - getZ()) < epsilon;
  }

  @Override
  public int hashCode()
  {
    throwIfReleased();
    return Arrays.hashCode(value);
  }

  @Override
  public String toString()
  {
    throwIfReleased();
    return String.format("{%f; %f; %f}", value[0], value[1], value[2]);
  }
}