#include "AC_PrecLand_StateMachine.h"
#include <AC_PrecLand/AC_PrecLand.h>
#include <AP_AHRS/AP_AHRS.h>

static const float MAX_POS_ERROR_M = 0.75f;  // Maximum possition error for retry locations
static const uint32_t FAILSAFE_INIT_TIMEOUT_MS = 7000;   // Timeout in ms before failsafe measures are started. During this period vehicle is completely stopped to give user the time to take over
static const float RETRY_OFFSET_ALT_M = 1.5f;  // This gets added to the altitude of the retry location

// Initialize the state machine. This is called everytime vehicle switches mode
void AC_PrecLand_StateMachine::init()
{
    AC_PrecLand *_precland = AP::ac_precland();
    if (_precland == nullptr) {
        // precland not enabled
        return;
    }

    if (!_precland->enabled()) {
        // precland is not enabled, prec land state machine methods should not be called!
        return;
    }
    // init is only called ONCE per mode change. So in a particuar mode we can retry only a finite times.
    // The counter will be reset if the statemachine is called from a different mode
    _retry_count = 0;
    // reset every other statemachine
    reset_failed_landing_statemachine();
}

// Reset the landing statemachines. This needs to be called everytime the landing target is back in sight.
// So that if the landing target goes out of sight again, we can start the failed landing procedure back from the beginning stage
void AC_PrecLand_StateMachine::reset_failed_landing_statemachine()
{
    landing_target_lost_action = TargetLostAction::INIT;
    _retry_state = RetryLanding::INIT;
    failsafe_initialized = false;
}

// Run Prec Land State Machine. During Prec Landing, we might encounter four scenarios:
// 1. We had the target in sight, but have lost it now. 2. We never had the target in sight and user wants to land.
// 3. We have the target in sight and can continue landing. 4. The sensor is out of range
// This method deals with all of these scenarios
// Returns the action needed to be done by the vehicle.
// Parameters: Vector3f "retry_pos_m" is filled with the required location if we need to retry landing.
AC_PrecLand_StateMachine::Status AC_PrecLand_StateMachine::update(Vector3f &retry_pos_m)
{

    // grab the current status of Landing Target
    AC_PrecLand *_precland = AP::ac_precland();
    if (_precland == nullptr) {
        // should never happen
        return Status::ERROR;
    }

    if (!_precland->enabled()) {
        // precland is not enabled, prec land state machine should not be called!
        return Status::ERROR;
    }

    AC_PrecLand::TargetState precland_target_state =  _precland->get_target_state();

    switch (precland_target_state) {
    case AC_PrecLand::TargetState::TARGET_RECENTLY_LOST:
        // we have lost the target but had it in sight at least once recently
        // action will depend on what user wants
        return get_target_lost_actions(retry_pos_m);

    case AC_PrecLand::TargetState::TARGET_NEVER_SEEN:
        // we have no clue where we are supposed to be landing
        // let user decide how strict our failsafe actions need to be
        return Status::FAILSAFE;

    case AC_PrecLand::TargetState::TARGET_OUT_OF_RANGE:
        // The target isn't in sight, but we can't run any fail safe measures or do landing retry
        // Therefore just descend for now, and check again later if retry is allowed
    case AC_PrecLand::TargetState::TARGET_FOUND:
        // no action required, target is in sight
        reset_failed_landing_statemachine();
        return Status::DESCEND;
    }

    // should never reach here, all values are handled above
    return Status::ERROR;
}


// Target is lost (i.e we had it in sight some time back), this method helps decide on what needs to be done next
// The chosen action depends on user set landing strictness and will be returned by this function
// Parameters: Vector3f "retry_pos_m" is filled with the required location if we need to retry landing.
AC_PrecLand_StateMachine::Status AC_PrecLand_StateMachine::get_target_lost_actions(Vector3f &retry_pos_m)
{
    AC_PrecLand *_precland = AP::ac_precland();
    if (_precland == nullptr) {
        // should never happen
        return Status::ERROR;
    }

    switch (landing_target_lost_action) {
    case TargetLostAction::INIT: {
        // figure out how strict the user is with the landing
        const RetryStrictness strictness =_precland->get_retry_strictness();
        switch (strictness) {
            case RetryStrictness::NORMAL:
            case RetryStrictness::VERY_STRICT:
                // We eventually want to retry landing, but lets descend for some time and hope the target gets in sight
                // If not, we will retry landing
                landing_target_lost_action = TargetLostAction::DESCEND;
                break;
            case RetryStrictness::NOT_STRICT:
                // User just wants to land, prec land isn't a priority
                landing_target_lost_action = TargetLostAction::LAND_VERTICALLY;
                break;
        }
        // at this stage we will be descending no matter what
        // so no special action required
        return Status::DESCEND;
    }

    case TargetLostAction::DESCEND:
        if (AP_HAL::millis() - _precland->get_last_valid_target_ms() >=_precland->get_min_retry_time_sec() * 1000) {
            // we have descended for some time and the target still isn't in sight
            // lets retry
            landing_target_lost_action = TargetLostAction::RETRY_LANDING;
            _retry_state = RetryLanding::INIT;
        }
        // still descending, no other action
        return Status::DESCEND;

    case TargetLostAction::RETRY_LANDING:
        // retry the landing by going to another position
        return retry_landing(retry_pos_m);

    case TargetLostAction::LAND_VERTICALLY:
        // Just land vertically
        // we will not be retrying to any location here on, so return false
        return Status::DESCEND;
    }

    // should never reach here, all cases are handled above
    return Status::ERROR;
}

// Retry landing based on a previously known location of the landing target
// Returns the action that should be taken by the vehicle
// Vector3f "retry_pos_m" is filled with the required location.
AC_PrecLand_StateMachine::Status AC_PrecLand_StateMachine::retry_landing(Vector3f &retry_pos_m)
{
    AC_PrecLand *_precland = AP::ac_precland();
    if (_precland == nullptr) {
        // should never happen
        return Status::ERROR;
    }

    if (_precland->get_max_retry_allowed() == 0) {
        // user does not want retry
        return Status::FAILSAFE;
    }

    if (_retry_count > _precland->get_max_retry_allowed()) {
        // we have exhausted the amount of times vehicle was allowed to retry landing
        // do failsafe measure so the vehicle isn't stuck in a constant loop
        return Status::FAILSAFE;
    }

    // get the retry position. This depends on what retry behavior has been set by user
    Vector3f go_to_pos;
    const RetryAction retry_action = _precland->get_retry_behaviour();
    if (retry_action == RetryAction::GO_TO_TARGET_LOC) {
        _precland->get_last_detected_landing_pos(go_to_pos);
    } else if (retry_action == RetryAction::GO_TO_LAST_LOC) {
        _precland->get_last_vehicle_pos_when_target_detected(go_to_pos);
    }

    // add a little bit offset so the vehicle climbs slightly higher than where it was
    // remember this is "D" frame and in meters's
    go_to_pos.z -= RETRY_OFFSET_ALT_M;

    switch (_retry_state) {
    case RetryLanding::INIT:
        // Init the Retry
        _retry_count ++;
        _retry_state = RetryLanding::IN_PROGRESS;
        // inform the user what we are doing
        if (_retry_count <= _precland->get_max_retry_allowed()) {
            gcs().send_text(MAV_SEVERITY_INFO, "PrecLand: Retrying");
        }
        retry_pos_m = go_to_pos;
        return Status::RETRYING;

    case RetryLanding::IN_PROGRESS: {
        // continue converging towards the target till we are close by
        retry_pos_m = go_to_pos;
        Vector3f pos;
        if (!AP::ahrs().get_relative_position_NED_origin(pos)) {
            return Status::ERROR;
        }
        const float dist_to_target = (go_to_pos-pos).length();
        if ((dist_to_target < MAX_POS_ERROR_M)) {
            // we have approx reached landing location previously detected
            _retry_state = RetryLanding::DESCEND;
        }
        return Status::RETRYING;
    }

    case RetryLanding::DESCEND: {
        // descend a little bit before completing the retry
        // This will descend to the original height of where landing target was first detected
        Vector3f pos;
        if (!AP::ahrs().get_relative_position_NED_origin(pos)) {
            return Status::ERROR;
        }
        // z_target is in "D" frame
        const float z_target = go_to_pos.z + RETRY_OFFSET_ALT_M;
        retry_pos_m = Vector3f{pos.x, pos.y, z_target};
        if (fabsf(pos.z - retry_pos_m.z) < MAX_POS_ERROR_M) {
            // we have descended to the original height where we started the climb from
            _retry_state = RetryLanding::COMPLETE;
            gcs().send_text(MAV_SEVERITY_INFO, "PrecLand: Retry Completed");
        }
        return Status::RETRYING;
    }

    case RetryLanding::COMPLETE:
        // Vehicle has completed a retry, and most likely the landing location still isn't sight
        // we have no choice but to force a failsafe action
        return Status::FAILSAFE;
    }

    // should never reach here
    return Status::ERROR;
}

// This is only called when the current status of the state machine returns "failsafe" and will return the action that the vehicle should do
// At the moment this method only allows you to stop in air permanently, or land vertically
// Failsafe will only trigger as a last resort
AC_PrecLand_StateMachine::FailSafeAction AC_PrecLand_StateMachine::get_failsafe_actions()
{
    AC_PrecLand *_precland = AP::ac_precland();
    if (_precland == nullptr) {
        // should never happen, just descend
        return FailSafeAction::DESCEND;
    }

    if (!failsafe_initialized) {
        // start the timer
        failsafe_start_ms = AP_HAL::millis();
        failsafe_initialized = true;
        gcs().send_text(MAV_SEVERITY_INFO, "PrecLand: Failsafe Measures");
    }

    // Depending on the strictness we will either land vertically, wait for some time and then land vertically, not land at all
    const RetryStrictness strictness= _precland->get_retry_strictness();
    switch (strictness) {
    case RetryStrictness::VERY_STRICT:
        // user does not want to land on anything but the target
        // stop landing (hover)
        return FailSafeAction::HOLD_POS;

    case RetryStrictness::NORMAL:
        if (AP_HAL::millis() - failsafe_start_ms < FAILSAFE_INIT_TIMEOUT_MS) {
            // stop the vehicle for at least a few seconds before descending
            // this might give user the chance to take over
            // we do not want to be too linent in landing vertically because of the strictness set by the user
            return FailSafeAction::HOLD_POS;
        }
        // land the vehicle vertically
        return FailSafeAction::DESCEND;

    case RetryStrictness::NOT_STRICT:
        // User wants to prioritize landing over staying in the air
        return FailSafeAction::DESCEND;
    }

    // should never reach here
    return FailSafeAction::DESCEND;
}
