package monkstone.vecmath.vec2;
/* 
* Copyright (C) 2015-16 Martin Prout
* 
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
* 
* http://creativecommons.org/licenses/LGPL/2.1/
* 
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
* Lesser General Public License for more details.
* 
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

import org.jruby.Ruby;
import org.jruby.RubyArray;
import org.jruby.RubyBoolean;
import org.jruby.RubyClass;
import org.jruby.RubyObject;
import org.jruby.anno.JRubyClass;
import org.jruby.anno.JRubyMethod;
import org.jruby.runtime.Arity;
import org.jruby.runtime.Block;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import monkstone.vecmath.JRender;

/**
*
* @author Martin Prout
*/
@JRubyClass(name = "Vec2D")
public class Vec2 extends RubyObject {
  
  static final double EPSILON = 9.999999747378752e-05; // matches processing.org EPSILON
  private static final long serialVersionUID = -7013225882277559392L;
  private double jx = 0;
  private double jy = 0;
    
  public double javax(){
        return jx;
    }
    
    public double javay(){
        return jy;
    }
    
  /**
  *
  * @param runtime
  */
  public static void createVec2(final Ruby runtime) {
      RubyClass vec2Cls = runtime.defineClass("Vec2D", runtime.getObject(), (Ruby runtime1, RubyClass rubyClass) -> new Vec2(runtime1, rubyClass));
      vec2Cls.defineAnnotatedMethods(Vec2.class);
  }
    
  
  /**
  *
  * @param context
  * @param klazz
  * @param args optional (no args jx = 0, jy = 0)
  * @return new Vec2 object (ruby)
  */
  @JRubyMethod(name = "new", meta = true, rest = true)
  public static final IRubyObject rbNew(ThreadContext context, IRubyObject klazz, IRubyObject[] args) {
    Vec2 vec2 = (Vec2) ((RubyClass) klazz).allocate();
    vec2.init(context, args);
    return vec2;
  }
  
  /**
  *
  * @param runtime
  * @param klass
  */
  public Vec2(Ruby runtime, RubyClass klass) {
    super(runtime, klass);
  }
  
  void init(ThreadContext context, IRubyObject[] args) {
    if (Arity.checkArgumentCount(context.getRuntime(), args, Arity.OPTIONAL.getValue(), 2) == 2) {
      jx = (Double) args[0].toJava(Double.class);
      jy = (Double) args[1].toJava(Double.class);
    }
  }
  
  /**
  *
  * @param context
  * @return jx float
  */
  @JRubyMethod(name = "x")
  
  public IRubyObject getX(ThreadContext context) {
    return context.getRuntime().newFloat(jx);
  }
  
  /**
  *
  * @param context
  * @return jy float
  */
  @JRubyMethod(name = "y")
  
  public IRubyObject getY(ThreadContext context) {
    return context.getRuntime().newFloat(jy);
  }
  
  /**
  *
  * @param context
  * @param other
  * @return jx float
  */
  @JRubyMethod(name = "x=")
  
  public IRubyObject setX(ThreadContext context, IRubyObject other) {
    jx = (Double) other.toJava(Double.class);
    return other;
  }
  
  /**
  *
  * @param context
  * @param other
  * @return jy float
  */
  @JRubyMethod(name = "y=")
  
  public IRubyObject setY(ThreadContext context, IRubyObject other) {
    jy = (Double) other.toJava(Double.class);
    return other;
  }
  
  /**
  *
  * @param context
  * @param other
  * @return hypotenuse float
  */
  @JRubyMethod(name = "dist", required = 1)
  
  public IRubyObject dist(ThreadContext context, IRubyObject other) {
    Vec2 b = null;
    Ruby runtime = context.getRuntime();
    if (other instanceof Vec2) {
      b = (Vec2) other.toJava(Vec2.class);
    } else {
      throw runtime.newTypeError("argument should be Vec2D");
    }
    double result = Math.hypot((jx - b.jx), (jy - b.jy));
    return runtime.newFloat(result);
  }
  
  /**
  *
  * @param context
  * @param other
  * @return cross product as a new Vec3D
  */
  @JRubyMethod(name = "cross", required = 1)
  
  public IRubyObject cross(ThreadContext context, IRubyObject other) {
    Vec2 b = null;
    Ruby runtime = context.getRuntime();
    if (other instanceof Vec2) {
      b = (Vec2) other.toJava(Vec2.class);
    } else {
      throw runtime.newTypeError("argument should be Vec2D");
    }
    return runtime.newFloat(jx * b.jy - jy * b.jx);
  }
  
  /**
  *
  * @param context
  * @param other
  * @return do product as a float
  */
  @JRubyMethod(name = "dot", required = 1)
  
  public IRubyObject dot(ThreadContext context, IRubyObject other) {
    Vec2 b = null;
    Ruby runtime = context.getRuntime();
    if (other instanceof Vec2) {
      b = (Vec2) other.toJava(Vec2.class);
    } else {
      throw runtime.newTypeError("argument should be Vec2D");
    }
    return runtime.newFloat(jx * b.jx + jy * b.jy);
  }
  
  /**
  *
  * @param context
  * @param other
  * @return new Vec2 object (ruby)
  */
  @JRubyMethod(name = "+", required = 1)
  
  public IRubyObject op_plus(ThreadContext context, IRubyObject other) {
    Vec2 b = null;
    Ruby runtime = context.getRuntime();
    if (other instanceof Vec2) {
      b = (Vec2) other.toJava(Vec2.class);
    } else {
      throw runtime.newTypeError("argument should be Vec2D");
    }
    return Vec2.rbNew(context, other.getMetaClass(), new IRubyObject[]{
      runtime.newFloat(jx + b.jx), 
      runtime.newFloat(jy + b.jy)});
  }
  
  /**
  *
  * @param context
  * @param other
  * @return new Vec2 object (ruby)
  */
  @JRubyMethod(name = "-", required = 1)
  
  public IRubyObject op_minus(ThreadContext context, IRubyObject other) {
    Vec2 b = null;
    Ruby runtime = context.getRuntime();
    if (other instanceof Vec2) {
      b = (Vec2) other.toJava(Vec2.class);
    } else {
      throw runtime.newTypeError("argument should be Vec2D");
    }
    return Vec2.rbNew(context, other.getMetaClass(), new IRubyObject[]{
      runtime.newFloat(jx - b.jx),
      runtime.newFloat(jy - b.jy)});
  }
  
  /**
  *
  * @param context
  * @param other
  * @return new Vec2 object (ruby)
  */
  @JRubyMethod(name = "*")
  
  public IRubyObject op_mul(ThreadContext context, IRubyObject other) {
    Ruby runtime = context.getRuntime();
    double scalar = (Double) other.toJava(Double.class);
    return Vec2.rbNew(context, this.getMetaClass(),
      new IRubyObject[]{runtime.newFloat(jx * scalar),
      runtime.newFloat(jy * scalar)});
  }
  
  /**
  *
  * @param context
  * @param other
  * @return new Vec2 object (ruby)
  */
  @JRubyMethod(name = "/", required = 1)
  
  public IRubyObject op_div(ThreadContext context, IRubyObject other) {
    Ruby runtime = context.getRuntime();
    double scalar = (Double) other.toJava(Double.class);
    if (Math.abs(scalar) < Vec2.EPSILON) {
      return this;
    }
    return Vec2.rbNew(context, this.getMetaClass(), new IRubyObject[]{
        runtime.newFloat(jx / scalar),
        runtime.newFloat(jy / scalar)});
  }
  
  /**
  *
  * @param context
  * @return angle radians as a float
  */
  @JRubyMethod(name = "heading")
  public IRubyObject heading(ThreadContext context) {
    return context.getRuntime().newFloat(Math.atan2(jy, jx));
  }
  
  /**
  *
  * @param context
  * @return magnitude float
  */
  @JRubyMethod(name = "mag")
  
  public IRubyObject mag(ThreadContext context) {
    double result = 0;
    if (Math.abs(jx) > EPSILON && Math.abs(jy) > EPSILON) {
      result = Math.hypot(jx, jy);
    }
    else{
      if (Math.abs(jy) > EPSILON) {
        result = Math.abs(jy);
      }
      if (Math.abs(jx) > EPSILON) {
        result = Math.abs(jx);
      }
    }
    return context.getRuntime().newFloat(result);
  }
  
  /**
  * Call yield if block given, do nothing if yield == false else set_mag to
  * given scalar
  *
  * @param context
  * @param scalar double value to set
  * @param block should return a boolean (optional)
  * @return this Vec2D with the new magnitude
  */
  @JRubyMethod(name = "set_mag")
  
  public IRubyObject set_mag(ThreadContext context, IRubyObject scalar, Block block) {
    double new_mag = (Double) scalar.toJava(Double.class);
    if (block.isGiven()) {
      if (!(boolean) block.yield(context, scalar).toJava(Boolean.class)) {
        return this;
      }
    }
    double current = 0;
    if (Math.abs(jx) > EPSILON && Math.abs(jy) > EPSILON) {
      current = Math.hypot(jx, jy);
    }
    else{
      if (Math.abs(jy) > EPSILON) {
        current = Math.abs(jy);
      }
      if (Math.abs(jx) > EPSILON) {
        current = Math.abs(jx);
      }
    }
    if (current > 0) {
      jx *= new_mag / current;
      jy *= new_mag / current;
    }
    return this;
  }
  
  /**
  *
  * @param context
  * @return this as a ruby object
  */
  @JRubyMethod(name = "normalize!")
  
  public IRubyObject normalize_bang(ThreadContext context) {
    double mag = 0;
    if (Math.abs(jx) > EPSILON && Math.abs(jy) > EPSILON) {
      mag = Math.hypot(jx, jy);
    }
    else{
      if (Math.abs(jx) > EPSILON)  {
        mag = Math.abs(jx);
      }
      if (Math.abs(jy) > EPSILON)  {
        mag = Math.abs(jy);
      }
    }
    if (mag > 0) {
      jx /= mag;
      jy /= mag;
    }
    return this;
  }
  
  /**
  *
  * @param context
  * @return new Vec2 object (ruby)
  */
  @JRubyMethod(name = "normalize")
  
  public IRubyObject normalize(ThreadContext context) {
    double mag = 0;
    Ruby runtime = context.getRuntime();
    if (Math.abs(jx) > EPSILON && Math.abs(jy) > EPSILON) {
      mag = Math.hypot(jx, jy);
    }
    else{
      if (Math.abs(jx) > EPSILON)  {
        mag = jx;
      }
      if (Math.abs(jy) > EPSILON)  {
        mag = jy;
      }
    }
    if (mag < EPSILON) {
      mag = 1.0;
    }
    return Vec2.rbNew(context, this.getMetaClass(), new IRubyObject[]{
        runtime.newFloat(jx / mag),
        runtime.newFloat(jy / mag)});
  }
  
  /**
  * Example of a regular ruby class method Use Math rather than RadLut
  * here!!!
  *
  * @param context
  * @param klazz
  * @param other input angle in radians
  * @return new Vec2 object (ruby)
  */
  @JRubyMethod(name = "from_angle", meta = true)
  public static IRubyObject from_angle(ThreadContext context, IRubyObject klazz, IRubyObject other) {
    Ruby runtime = context.getRuntime();
    double scalar = (Double) other.toJava(Double.class);
    return Vec2.rbNew(context, klazz, new IRubyObject[]{
        runtime.newFloat(Math.cos(scalar)),
        runtime.newFloat(Math.sin(scalar))});
  }
  
  /**
  *
  * @param context
  * @param other
  * @return this Vec2 object rotated
  */
  @JRubyMethod(name = "rotate!")
  public IRubyObject rotate_bang(ThreadContext context, IRubyObject other) {
    double theta = (Double) other.toJava(Double.class);
    double x = (jx * Math.cos(theta) - jy * Math.sin(theta));
    double y = (jx * Math.sin(theta) + jy * Math.cos(theta));
    jx = x;
    jy = y;
    return this;
  }


  
  /**
  *
  * @param context
  * @param other
  * @return a new Vec2 object rotated
  */
  @JRubyMethod(name = "rotate")
  public IRubyObject rotate(ThreadContext context, IRubyObject other) {
    Ruby runtime = context.getRuntime();
    double theta = (Double) other.toJava(Double.class);
    IRubyObject[] ary = new IRubyObject[]{
      runtime.newFloat(jx * Math.cos(theta) - jy * Math.sin(theta)),
      runtime.newFloat(jx * Math.sin(theta) + jy * Math.cos(theta))};
    return Vec2.rbNew(context, this.getMetaClass(), ary);
  }
  
  /**
  *
  * @param context
  * @param args
  * @return as a new Vec2 object (ruby)
  */
  @JRubyMethod(name = "lerp", rest = true)
  public IRubyObject lerp(ThreadContext context, IRubyObject[] args) {
    Ruby runtime = context.getRuntime();
    Arity.checkArgumentCount(runtime, args, 2, 2);
    Vec2 vec = (Vec2) args[0].toJava(Vec2.class);
    double scalar = (Double) args[1].toJava(Double.class);
    assert (scalar >= 0 && scalar < 1.0) :
    "Lerp value " + scalar + " out of range 0 .. 1.0";
    return Vec2.rbNew(context, this.getMetaClass(), new IRubyObject[]{
        runtime.newFloat(jx + (vec.jx - jx) * scalar),
        runtime.newFloat(jy + (vec.jy - jy) * scalar)});
  }
  
  /**
  *
  * @param context
  * @param args
  * @return this
  */
  @JRubyMethod(name = "lerp!", rest = true)
  public IRubyObject lerp_bang(ThreadContext context, IRubyObject[] args) {
    Arity.checkArgumentCount(context.getRuntime(), args, 2, 2);
    Vec2 vec = (Vec2) args[0].toJava(Vec2.class);
    double scalar = (Double) args[1].toJava(Double.class);
    assert (scalar >= 0 && scalar < 1.0) :
    "Lerp value " + scalar + " out of range 0 .. 1.0";
    jx += (vec.jx - jx) * scalar;
    jy += (vec.jy - jy) * scalar;
    return this;
  }
  
  /**
  *
  * @param context
  * @param other
  * @return theta radians float
  */
  @JRubyMethod(name = "angle_between")
  
  public IRubyObject angleBetween(ThreadContext context, IRubyObject other) {
    Vec2 vec = null;
    Ruby runtime = context.getRuntime();
    if (other instanceof Vec2) {
      vec = (Vec2) other.toJava(Vec2.class);
    } else {
      throw runtime.newTypeError("argument should be Vec2D");
    }
    return runtime.newFloat(Math.atan2(jx - vec.jx, jy - vec.jy));
  }

/**
  * Example of a regular ruby class method Use Math rather than RadLut
  * here!!!
  *
  * @param context
  * @param klazz
  * @return new Vec2 object (ruby)
  */
  @JRubyMethod(name = "random", meta = true)
  public static IRubyObject random_direction(ThreadContext context, IRubyObject klazz) {
    Ruby runtime = context.getRuntime();
    double angle = Math.random() * Math.PI * 2;
    return Vec2.rbNew(context, klazz, new IRubyObject[]{
        runtime.newFloat(Math.cos(angle)),
        runtime.newFloat(Math.sin(angle))});
  }
  
  /**
  *
  * @param context
  * @return new copy
  */
  @JRubyMethod(name = {"copy", "dup"})
  
  public IRubyObject copy(ThreadContext context) {
    Ruby runtime = context.runtime;
    return Vec2.rbNew(context, this.getMetaClass(), new IRubyObject[]{
      runtime.newFloat(jx), 
      runtime.newFloat(jy)});
  }
  
  /**
  *
  * @param context
  * @return ruby array
  */
  @JRubyMethod(name = "to_a")
  
  public IRubyObject toArray(ThreadContext context) {
    Ruby runtime = context.runtime;
    return RubyArray.newArray(context.getRuntime(), new IRubyObject[]{
      runtime.newFloat(jx), 
      runtime.newFloat(jy)});
  }
  
  /**
  *
  * @param context
  * @param object
  */
  @JRubyMethod(name = "to_vertex")
  
  public void toVertex(ThreadContext context, IRubyObject object) {
    JRender renderer = (JRender) object.toJava(JRender.class);
    renderer.vertex(jx, jy);
  }
  
  /**
  *
  * @param context
  * @param object
  */
  @JRubyMethod(name = "to_curve_vertex")
  
  public void toCurveVertex(ThreadContext context, IRubyObject object) {
    JRender renderer = (JRender) object.toJava(JRender.class);
    renderer.curveVertex(jx, jy);
  }
  
  
  /**
  * For jruby-9000 we alias to inspect
  * @param context
  * @return custom to string (inspect)
  */
  @JRubyMethod(name = {"to_s", "inspect"})
  
  public IRubyObject to_s(ThreadContext context) {
    return context.getRuntime().newString(String.format("Vec2D(x = %4.4f, y = %4.4f)", jx, jy));
  }
  
  /**
  *
  * @return hash int
  */
  @Override
  public int hashCode() {
    int hash = 5;
    hash = 53 * hash + (int) (Double.doubleToLongBits(this.jx) ^ (Double.doubleToLongBits(this.jx) >>> 32));
    hash = 53 * hash + (int) (Double.doubleToLongBits(this.jy) ^ (Double.doubleToLongBits(this.jy) >>> 32));
    return hash;
  }
  
  /**
  *
  * @param obj
  * @return ruby boolean
  */
  @Override
  public boolean equals(Object obj) {
    if (obj instanceof Vec2){
      final Vec2 other = (Vec2) obj;
      if (!((Double)this.jx).equals(other.jx)) {
        return false;
      }
      return ((Double)this.jy).equals(other.jy);
    }
    return false;
  }
  
  /**                                                                              
  *                                                                     
  * @param context
  * @param other
  * @return ruby boolean                                                                       
  */                                                                               
  @JRubyMethod(name = "eql?", required = 1)                                                                    
  public IRubyObject eql_p(ThreadContext context, IRubyObject other) {
    Ruby runtime = context.getRuntime();
    if (other instanceof Vec2){                                                    
      Vec2 v = (Vec2) other.toJava(Vec2.class);                                              
      if (!((Double)this.jx).equals(v.jx)) {                                   
        return RubyBoolean.newBoolean(runtime, false);                                                          
      }                                                                            
      return RubyBoolean.newBoolean(runtime, ((Double)this.jy).equals(v.jy));                                   
    }                                                                            
    return RubyBoolean.newBoolean(runtime, false);                                                                     
  }
  
  /**
  *
  * @param context
  * @param other
  * @return ruby boolean
  */
  @JRubyMethod(name = "==", required = 1)
  
  @Override
  public IRubyObject op_equal(ThreadContext context, IRubyObject other) {
    Ruby runtime = context.getRuntime();
    if (other instanceof Vec2) {
      Vec2 v = (Vec2) other.toJava(Vec2.class);
      double diff = jx - v.jx;
      if ((diff < 0 ? -diff : diff) > Vec2.EPSILON) {
        return RubyBoolean.newBoolean(runtime, false);
      }
      diff = jy - v.jy;
      boolean result = ((diff < 0 ? -diff : diff) < Vec2.EPSILON);
      return RubyBoolean.newBoolean(runtime, result);
    }
    return RubyBoolean.newBoolean(runtime, false);
  }
}