/************************************************************************************

Filename    :   Util_MagCalibration.cpp
Content     :   Procedures for calibrating the magnetometer
Created     :   April 16, 2013
Authors     :   Steve LaValle, Andrew Reisse

Copyright   :   Copyright 2013 Oculus VR, Inc. All Rights reserved.

Use of this software is subject to the terms of the Oculus license
agreement provided at the time of installation or download, or which
otherwise accompanies this software in either electronic or hard copy form.

*************************************************************************************/

#include "Util_MagCalibration.h"

namespace OVR { namespace Util {

void MagCalibration::BeginAutoCalibration(SensorFusion& sf)
{
    Stat = Mag_AutoCalibrating;
    // This is a "hard" reset of the mag, so need to clear stored values
    sf.ClearMagCalibration();
    SampleCount = 0;

    // reset the statistics
    MinMagValues = Vector3f(10000.0f,10000.0f,10000.0f);
    MaxMagValues = Vector3f(-10000.0f,-10000.0f,-10000.0f);
	MinQuatValues = Quatf(1.0f,1.0f,1.0f,1.0f);
	MaxQuatValues = Quatf(0.0f,0.0f,0.0f,0.0f);
}

unsigned MagCalibration::UpdateAutoCalibration(SensorFusion& sf)
{
    if (Stat != Mag_AutoCalibrating)
        return Stat;

    Quatf q = sf.GetOrientation();
    Vector3f m = sf.GetMagnetometer();

    InsertIfAcceptable(q, m);

    if ((SampleCount == 4) && (Stat == Mag_AutoCalibrating))
	{
		//LogText("Magnetometer Output Spread: %f %f %f\n",MagSpread.x,MagSpread.y,MagSpread.z);
		//LogText("Quaternion Spread: %f %f %f %f\n",QuatSpread.x,QuatSpread.y,QuatSpread.z,QuatSpread.w);
        SetCalibration(sf);
	}

    return Stat;

}

void MagCalibration::BeginManualCalibration(SensorFusion& sf)
{
    Stat = Mag_ManuallyCalibrating;
    sf.ClearMagCalibration();
    SampleCount = 0;
}

bool MagCalibration::IsAcceptableSample(const Quatf& q, const Vector3f& m)
{
    switch (SampleCount)
    {
        // Initial sample is always acceptable
    case 0:
        return true;
        break;
    case 1:
        return (q.DistanceSq(QuatSamples[0]) > MinQuatDistanceSq)&&
               ((m - MagSamples[0]).LengthSq() > MinMagDistanceSq);
        break;
    case 2:
        return (q.DistanceSq(QuatSamples[0]) > MinQuatDistanceSq)&&
               (q.DistanceSq(QuatSamples[1]) > MinQuatDistanceSq)&&
               ((m - MagSamples[0]).LengthSq() > MinMagDistanceSq)&&
               ((m - MagSamples[1]).LengthSq() > MinMagDistanceSq);
        break;
    case 3:
        return (q.DistanceSq(QuatSamples[0]) > MinQuatDistanceSq)&&
               (q.DistanceSq(QuatSamples[1]) > MinQuatDistanceSq)&&
               (q.DistanceSq(QuatSamples[2]) > MinQuatDistanceSq)&&
               ((PointToPlaneDistance(MagSamples[0],MagSamples[1],MagSamples[2],m) > MinMagDistance)||
                (PointToPlaneDistance(MagSamples[1],MagSamples[2],m,MagSamples[0]) > MinMagDistance)||
                (PointToPlaneDistance(MagSamples[2],m,MagSamples[0],MagSamples[1]) > MinMagDistance)||
                (PointToPlaneDistance(m,MagSamples[0],MagSamples[1],MagSamples[2]) > MinMagDistance));
    }

    return false;
}


bool MagCalibration::InsertIfAcceptable(const Quatf& q, const Vector3f& m)
{
	// Update some statistics
	if (m.x < MinMagValues.x)
		MinMagValues.x = m.x;
	if (m.y < MinMagValues.y)
		MinMagValues.y = m.y;
	if (m.z < MinMagValues.z)
		MinMagValues.z = m.z;
	if (m.x > MaxMagValues.x)
		MaxMagValues.x = m.x;
	if (m.y > MaxMagValues.y)
		MaxMagValues.y = m.y;
	if (m.z > MaxMagValues.z)
		MaxMagValues.z = m.z;
	if (q.x < MinQuatValues.x)
		MinQuatValues.x = q.x;
	if (q.y < MinQuatValues.y)
		MinQuatValues.y = q.y;
	if (q.z < MinQuatValues.z)
		MinQuatValues.z = q.z;
	if (q.w < MinQuatValues.w)
		MinQuatValues.w = q.w;
	if (q.x > MaxQuatValues.x)
		MaxQuatValues.x = q.x;
	if (q.y > MaxQuatValues.y)
		MaxQuatValues.y = q.y;
	if (q.z > MaxQuatValues.z)
		MaxQuatValues.z = q.z;
	if (q.w > MaxQuatValues.w)
		MaxQuatValues.w = q.w;
	MagSpread = MaxMagValues - MinMagValues;
	QuatSpread = MaxQuatValues - MinQuatValues;

	if (IsAcceptableSample(q, m))
    {
        MagSamples[SampleCount] = m;
        QuatSamples[SampleCount] = q;
        SampleCount++;
        return true;
    }

    return false;
}

Matrix4f MagCalibration::GetMagCalibration() const
{
    Matrix4f calMat = Matrix4f();
    calMat.M[0][3] = -MagCenter.x;
    calMat.M[1][3] = -MagCenter.y;
    calMat.M[2][3] = -MagCenter.z;
    return calMat;
}

bool MagCalibration::SetCalibration(SensorFusion& sf)
{
    if (SampleCount < 4)
        return false;

    MagCenter = CalculateSphereCenter(MagSamples[0],MagSamples[1],MagSamples[2],MagSamples[3]);
    Matrix4f calMat = GetMagCalibration();
    sf.SetMagCalibration(calMat);
    Stat = Mag_Calibrated;
    //LogText("MagCenter: %f %f %f\n",MagCenter.x,MagCenter.y,MagCenter.z);

    return true;
}


// Calculate the center of a sphere that passes through p1, p2, p3, p4
Vector3f MagCalibration::CalculateSphereCenter(const Vector3f& p1, const Vector3f& p2,
                                               const Vector3f& p3, const Vector3f& p4) 
{
    Matrix4f A;
    int i;
    Vector3f p[4];
    p[0] = p1;
    p[1] = p2;
    p[2] = p3;
    p[3] = p4;

    for (i = 0; i < 4; i++) 
    {
        A.M[i][0] = p[i].x;
        A.M[i][1] = p[i].y;
        A.M[i][2] = p[i].z;
        A.M[i][3] = 1.0f;
    }
    float m11 = A.Determinant();
    OVR_ASSERT(m11 != 0.0f);

    for (i = 0; i < 4; i++) 
    {
        A.M[i][0] = p[i].x*p[i].x + p[i].y*p[i].y + p[i].z*p[i].z;
        A.M[i][1] = p[i].y;
        A.M[i][2] = p[i].z;
        A.M[i][3] = 1.0f;
    }
    float m12 = A.Determinant();

    for (i = 0; i < 4; i++) 
    {
        A.M[i][0] = p[i].x*p[i].x + p[i].y*p[i].y + p[i].z*p[i].z;
        A.M[i][1] = p[i].x;
        A.M[i][2] = p[i].z;
        A.M[i][3] = 1.0f;
    }
    float m13 = A.Determinant();

    for (i = 0; i < 4; i++) 
    {
        A.M[i][0] = p[i].x*p[i].x + p[i].y*p[i].y + p[i].z*p[i].z;
        A.M[i][1] = p[i].x;
        A.M[i][2] = p[i].y;
        A.M[i][3] = 1.0f;
    }
    float m14 = A.Determinant();

    float c = 0.5f / m11;
    return Vector3f(c*m12, -c*m13, c*m14);
}

// Distance from p4 to the nearest point on a plane through p1, p2, p3
float MagCalibration::PointToPlaneDistance(const Vector3f& p1, const Vector3f& p2,
                                           const Vector3f& p3, const Vector3f& p4) 
{
    Vector3f v1 = p1 - p2;
    Vector3f v2 = p1 - p3;
    Vector3f planeNormal = v1.Cross(v2);
    planeNormal.Normalize();
	 return fabs (planeNormal.Dot (p4 - p1));
    //return fabs((planeNormal * p4) - planeNormal * p1);
}

}}
