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

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

class Camera {
  public var fov( __getFov, __setFov ):Float;
    public function __getFov():Float { return( fov ); }
    public function __setFov( f:Float ):Float { fov = f; return( fov ); }
  public var zoom( __getZoom, __setZoom ):Float;
    public function __getZoom():Float { return( zoom ); }
    public function __setZoom( z:Float ):Float { zoom = z; return( zoom ); }
  public var ratio( __getRatio, __setRatio ):Float;
    public function __getRatio():Float { return( ratio ); }
    public function __setRatio( r:Float ):Float { ratio = r; return( ratio ); }
  public var near( __getNear, __setNear ):Float;
    public function __getNear():Float { return( near ); }
    public function __setNear( n:Float ):Float { near = n; return( near ); }
  public var far( __getFar, __setFar ):Float;
    public function __getFar():Float { return( far ); }
    public function __setFar( f:Float ):Float { far = f; return( far ); }

  public var mproj( __getMProj, __setMProj ):Matrix4;
    public function __getMProj():Matrix4 { return( mproj ); }
    public function __setMProj( mat:Matrix4 ):Matrix4 {
      mproj = mat.clone();
      return( mproj );
    }
  public var mcam( __getMCam, __setMCam ):Matrix4;
    public function __getMCam():Matrix4 { return( mcam ); }
    public function __setMCam( mat:Matrix4 ):Matrix4 {
      mcam = mat.clone();
      return( mcam );
    }
  public var m( __getMatrix, __setMatrix ):Matrix4;
    public function __getMatrix():Matrix4 { return( m ); }
    public function __setMatrix( mat:Matrix4 ):Matrix4 {
      m = mat.clone();
      return( m );
    }

  public var pos( __getPos, __setPos ):Point3D;
    public function __getPos():Point3D { return( pos ); }
    public function __setPos( p:Point3D ):Point3D {
      pos = p.clone();
      return( pos );
    }
  public var up( __getUpVec, __setUpVec ):Point3D;
    public function __getUpVec():Point3D { return( up ); }
    public function __setUpVec( p:Point3D ):Point3D {
      up = p.clone();
      return( up );
    }
  public var target( __getTarget, __setTarget ):Point3D;
    public function __getTarget():Point3D { return( target ); }
    public function __setTarget( 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 = 0.5,
                       ?fa:Float = 10000.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;
  }
}
