#include "Navigation.h"

Navigation::Navigation(GPS *withGPS, Waypoints *withWP) :
    _gps(withGPS),
    _wp(withWP),
    _hold_course(-1)
{
}

void
Navigation::update_gps()
{
    location.alt            = _gps->altitude;
    location.lng            = _gps->longitude;
    location.lat            = _gps->latitude;

    // target_bearing is where we should be heading
    bearing                         = get_bearing(&location, &next_wp);

    // waypoint distance from plane
    distance                        = get_distance(&location, &next_wp);

    calc_bearing_error();
    calc_altitude_error();
    altitude_above_home = location.alt - home.alt;

    // check if we have missed the WP
    _loiter_delta           = (bearing - _old_bearing) / 100;

    // reset the old value
    _old_bearing            = bearing;

    // wrap values
    if (_loiter_delta > 170) _loiter_delta -= 360;
    if (_loiter_delta < -170) _loiter_delta += 360;
    loiter_sum += abs(_loiter_delta);

    if (distance <= 0) {
        distance = -1;
        Serial.print("MSG wp error  ");
        Serial.println(distance, DEC);
    }
}

void
Navigation::load_first_wp(void)
{
    set_next_wp(_wp->get_waypoint_with_index(1));
}

void
Navigation::load_home(void)
{
    home = _wp->get_waypoint_with_index(0);
}

void
Navigation::return_to_home_with_alt(uint32_t alt)
{
    Waypoints::WP loc = _wp->get_waypoint_with_index(0);
    loc.alt += alt;
    set_next_wp(loc);
}

void
Navigation::reload_wp(void)
{
    set_next_wp(_wp->get_current_waypoint());
}

void
Navigation::load_wp_index(uint8_t i)
{
    _wp->set_index(i);
    set_next_wp(_wp->get_current_waypoint());
}

void
Navigation::hold_location()
{
    //	set_next_wp()	XXX needs to be implemented
}

void
Navigation::set_home(Waypoints::WP loc)
{
    _wp->set_waypoint_with_index(loc, 0);
    home            = loc;
    //location  = home;
}

void
Navigation::set_next_wp(Waypoints::WP loc)
{
    prev_wp = next_wp;
    next_wp = loc;

    if(_scaleLongDown == 0)
        calc_long_scaling(loc.lat);

    total_distance          = get_distance(&location, &next_wp);
    distance                        = total_distance;

    bearing                         = get_bearing(&location, &next_wp);
    _old_bearing            = bearing;

    // clear loitering code
    _loiter_delta           = 0;
    loiter_sum                      = 0;

    // set a new crosstrack bearing
    // ----------------------------
    reset_crosstrack();
}

void
Navigation::calc_long_scaling(int32_t lat)
{
    // this is used to offset the shrinking longitude as we go towards the poles
    float rads                      = (fabs(lat) / T7) * 0.0174532925;
    _scaleLongDown          = cos(rads);
    _scaleLongUp            = 1.0f / cos(rads);
}

void
Navigation::set_hold_course(int16_t b)
{
    if(b)
        _hold_course = bearing;
    else
        _hold_course = -1;
}

int16_t
Navigation::get_hold_course(void)
{
    return _hold_course;
}

void
Navigation::calc_distance_error()
{
    //distance_estimate     += (float)_gps->ground_speed * .0002 * cos(radians(bearing_error * .01));
    //distance_estimate     -= DST_EST_GAIN * (float)(distance_estimate - GPS_distance);
    //distance          = max(distance_estimate,10);
}

void
Navigation::calc_bearing_error(void)
{
    if(_hold_course == -1) {
        bearing_error = wrap_180(bearing - _gps->ground_course);
    }else{
        bearing_error = _hold_course;
    }
}

int32_t
Navigation::wrap_180(int32_t error)
{
    if (error > 18000) error -= 36000;
    if (error < -18000) error += 36000;
    return error;
}

int32_t
Navigation::wrap_360(int32_t angle)
{
    if (angle > 36000) angle -= 36000;
    if (angle < 0) angle += 36000;
    return angle;
}

void
Navigation::set_bearing_error(int32_t error)
{
    bearing_error = wrap_180(error);
}


/*****************************************
* Altitude error with Airspeed correction
*****************************************/
void
Navigation::calc_altitude_error(void)
{
    // limit climb rates
    _target_altitude = next_wp.alt - ((float)((distance -20) * _offset_altitude) / (float)(total_distance - 20));
    if(prev_wp.alt > next_wp.alt) {
        _target_altitude = constrain(_target_altitude, next_wp.alt, prev_wp.alt);
    }else{
        _target_altitude = constrain(_target_altitude, prev_wp.alt, next_wp.alt);
    }
    altitude_error  = _target_altitude - location.alt;
}

void
Navigation::set_loiter_vector(int16_t v)
{
    //	_vector = constrain(v, -18000, 18000);	XXX needs to be implemented
}

void
Navigation::update_crosstrack(void)
{
    // Crosstrack Error
    // ----------------
    if (abs(bearing - _crosstrack_bearing) < 4500) {             // If we are too far off or too close we don't do track following
        _crosstrack_error = sin(radians((bearing - _crosstrack_bearing) / 100)) * distance;              // Meters we are off track line
        bearing += constrain(_crosstrack_error * XTRACK_GAIN, -XTRACK_ENTRY_ANGLE, XTRACK_ENTRY_ANGLE);
    }
}

void
Navigation::reset_crosstrack(void)
{
    _crosstrack_bearing     = get_bearing(&location, &next_wp);         // Used for track following
}

long
Navigation::get_distance(Waypoints::WP *loc1, Waypoints::WP *loc2)
{
    if(loc1->lat == 0 || loc1->lng == 0)
        return -1;
    if(loc2->lat == 0 || loc2->lng == 0)
        return -1;
    if(_scaleLongDown == 0)
        calc_long_scaling(loc2->lat);
    float dlat      = (float)(loc2->lat - loc1->lat);
    float dlong = ((float)(loc2->lng - loc1->lng)) * _scaleLongDown;
    return sqrt(sq(dlat) + sq(dlong)) * .01113195;
}

long
Navigation::get_bearing(Waypoints::WP *loc1, Waypoints::WP *loc2)
{
    if(loc1->lat == 0 || loc1->lng == 0)
        return -1;
    if(loc2->lat == 0 || loc2->lng == 0)
        return -1;
    if(_scaleLongDown == 0)
        calc_long_scaling(loc2->lat);
    long off_x                      = loc2->lng - loc1->lng;
    long off_y                      = (loc2->lat - loc1->lat) * _scaleLongUp;
    long bearing            =       9000 + atan2(-off_y, off_x) * 5729.57795;
    if (bearing < 0)
        bearing                 += 36000;
    return bearing;
}