// --------------------------------------------------------------------
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// --------------------------------------------------------------------

package tinylib;

/**
  derived from Camera.hx in molehill examples written by Nicolas Cannasse

  I could not completely understand the original code ... orz
**/

class Camera {
  /**
    fov angle
  **/
  @:isVar public var fov( get, set ):Float;
    /**
      getter of `fov`
    **/
    public function get_fov():Float { return( fov ); }
    /**
      setter of `fov`
    **/
    public function set_fov( f:Float ):Float { fov = f; return( fov ); }
  /**
    zoom
  **/
  @:isVar public var zoom( get, set ):Float;
    /**
      getter of `zoom`
    **/
    public function get_zoom():Float { return( zoom ); }
    /**
      setter of `zoom`
    **/
    public function set_zoom( z:Float ):Float { zoom = z; return( zoom ); }
  /**
    aspect ratio
  **/
  @:isVar public var ratio( get, set ):Float;
    /**
      getter of `ratio`
    **/
    public function get_ratio():Float { return( ratio ); }
    /**
      setter of `ratio`
    **/
    public function set_ratio( r:Float ):Float { ratio = r; return( ratio ); }
  /**
    near plane
  **/
  @:isVar public var near( get, set ):Float;
    /**
      getter of `near`
    **/
    public function get_near():Float { return( near ); }
    /**
      setter of `near`
    **/
    public function set_near( n:Float ):Float { near = n; return( near ); }
  /**
    far plane
  **/
  @:isVar public var far( get, set ):Float;
    /**
      getter of `far`
    **/
    public function get_far():Float { return( far ); }
    /**
      setter of `far`
    **/
    public function set_far( f:Float ):Float { far = f; return( far ); }

  @:isVar public var mproj( get, set ):Matrix4;
    public function get_mproj():Matrix4 { return( mproj ); }
    public function set_mproj( mat:Matrix4 ):Matrix4 {
      mproj = mat.clone();
      return( mproj );
    }
  @:isVar public var mcam( get, set ):Matrix4;
    public function get_mcam():Matrix4 { return( mcam ); }
    public function set_mcam( mat:Matrix4 ):Matrix4 {
      mcam = mat.clone();
      return( mcam );
    }
  @:isVar public var m( get, set ):Matrix4;
    public function get_m():Matrix4 { return( m ); }
    public function set_m( mat:Matrix4 ):Matrix4 {
      m = mat.clone();
      return( m );
    }

  @:isVar public var pos( get, set ):Point3D;
    public function get_pos():Point3D { return( pos ); }
    public function set_pos( p:Point3D ):Point3D {
      pos = p.clone();
      return( pos );
    }
  @:isVar public var up( get, set ):Point3D;
    public function get_up():Point3D { return( up ); }
    public function set_up( p:Point3D ):Point3D {
      up = p.clone();
      return( up );
    }
  @:isVar public var target( get, set ):Point3D;
    public function get_target():Point3D { return( target ); }
    public function set_target( p:Point3D ):Point3D {
      target = p.clone();
      return( target );
    }

  // ##################################################################

  public function new( ?f:Float = 60.0,
                       ?z:Float = 1.0,
                       ?r:Float = 1.0,
                       ?ne:Float = 1.0,
                       ?fa:Float = 3000.0 ) {
    fov = f;
    zoom = z;
    ratio = r;
    near = ne;
    far = fa;
    m = new Matrix4();
    mcam = new Matrix4();
    pos = new Point3D( 0.0, 0.0, -600.0 );
    up = new Point3D( 0, 1, 0 );
    target = new Point3D( 0, 0, 0 );
  }

  public function clone():Camera {
    var c:Camera = new Camera();
    c.fov = fov;
    c.zoom = zoom;
    c.ratio = ratio;
    c.near = near;
    c.far = far;
    c.m = m.clone();
    c.mcam = mcam.clone();
    c.pos = pos.clone();
    c.up = up.clone();
    c.target = target.clone();
    return( c );
  }

  public function update() {
    var az = Point3D.getSub( pos, target );
    var ax = Point3D.getCross( up, az );
    az.normalize();
    ax.normalize();
    if ( ax.sqabs() == 0.0 ) {
      ax.x = az.y;
      ax.y = az.z;
      ax.z = az.x;
    }
    // az and ax are perpendicular, we can skip normalize of ay
    var ay = Point3D.getCross( az, ax );
    mcam.m11 = ax.x;
    mcam.m12 = ay.x;
    mcam.m13 = az.x;
    mcam.m21 = ax.y;
    mcam.m22 = ay.y;
    mcam.m23 = az.y;
    mcam.m31 = ax.z;
    mcam.m32 = ay.z;
    mcam.m33 = az.z;

    mcam.m14 = mcam.m24 = mcam.m34 = 0.0;
    mcam.m41 = -ax.dot( pos );
    mcam.m42 = -ay.dot( pos );
    mcam.m43 = -az.dot( pos );
    mcam.m44 = 1.0;

    mproj = makeFrustumMatrix();
    m.copy( mcam );
    m.multiply4( mproj );
  }

  private function makeFrustumMatrix():Matrix4 {
    var scale = zoom / Math.tan( fov * Math.PI / 360.0 );
    var ret = new Matrix4();
    ret.clear();
    ret.m11 = scale;
    ret.m22 = -scale * ratio;
    ret.m33 = far / ( near - far );
    ret.m34 = -1.0;
    ret.m43 = ( near * far ) / ( near - far );
    return( ret );
  }

  public function determineFov( h:Int,
                                l:Float ):Void {
    fov = Math.atan( 0.5 * h / l ) * 360.0 / Math.PI;
  }
}
