// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include "car_simulation.hpp"

#include "constants.hpp"

#include <everest/logging.hpp>

constexpr double MS_FACTOR = (1.0 / 60.0 / 60.0 / 1000.0);

void CarSimulation::state_machine() {
    using types::ev_board_support::EvCpState;

    const auto state_has_changed = sim_data.state != sim_data.last_state;
    sim_data.last_state = sim_data.state;

    switch (sim_data.state) {
    case SimState::UNPLUGGED:
        if (state_has_changed) {

            r_ev_board_support->call_set_cp_state(EvCpState::A);
            r_ev_board_support->call_allow_power_on(false);
            // Wait for physical plugin (ev BSP sees state A on CP and not Disconnected)

            sim_data.slac_state = types::slac::State::UNMATCHED;
            if (!r_ev.empty()) {
                r_ev[0]->call_stop_charging();
            }
        }
        break;
    case SimState::PLUGGED_IN:
        if (state_has_changed) {
            r_ev_board_support->call_set_cp_state(EvCpState::B);
            r_ev_board_support->call_allow_power_on(false);
            simulate_soc();
        }
        break;
    case SimState::CHARGING_REGULATED:
        if (state_has_changed || sim_data.pwm_duty_cycle != sim_data.last_pwm_duty_cycle) {
            sim_data.last_pwm_duty_cycle = sim_data.pwm_duty_cycle;
            // do not draw power if EVSE paused by stopping PWM
            if (sim_data.pwm_duty_cycle > 7.0 && sim_data.pwm_duty_cycle < 97.0) {
                r_ev_board_support->call_set_cp_state(EvCpState::C);
                r_ev_board_support->call_allow_power_on(true);
            } else {
                r_ev_board_support->call_set_cp_state(EvCpState::B);
                r_ev_board_support->call_allow_power_on(false);
            }
        }
        break;
    case SimState::CHARGING_FIXED:
        // Todo(sl): What to do here
        if (state_has_changed) {
            // Also draw power if EVSE stopped PWM - this is a break the rules simulator->mode to test the charging
            // implementation!
            r_ev_board_support->call_set_cp_state(EvCpState::C);
            r_ev_board_support->call_allow_power_on(true);
        }
        break;

    case SimState::ERROR_E:
        if (state_has_changed) {
            r_ev_board_support->call_set_cp_state(EvCpState::E);
            r_ev_board_support->call_allow_power_on(false);
        }
        break;
    case SimState::DIODE_FAIL:
        if (state_has_changed) {
            r_ev_board_support->call_diode_fail(true);
            r_ev_board_support->call_allow_power_on(false);
        }
        break;
    case SimState::ISO_POWER_READY:
        if (state_has_changed) {
            r_ev_board_support->call_set_cp_state(EvCpState::C);
        }
        break;
    case SimState::ISO_CHARGING_REGULATED:
        if (state_has_changed) {
            r_ev_board_support->call_set_cp_state(EvCpState::C);
            r_ev_board_support->call_allow_power_on(true);
        }
        break;
    case SimState::BCB_TOGGLE:
        if (sim_data.bcb_toggle_C) {
            r_ev_board_support->call_set_cp_state(EvCpState::C);
            sim_data.bcb_toggle_C = false;
        } else {
            r_ev_board_support->call_set_cp_state(EvCpState::B);
            sim_data.bcb_toggle_C = true;
            ++sim_data.bcb_toggles;
        }
        break;
    default:
        sim_data.state = SimState::UNPLUGGED;
        break;
    }

    if (not state_has_changed and
        (sim_data.state == SimState::CHARGING_REGULATED or sim_data.state == SimState::CHARGING_FIXED or
         sim_data.state == SimState::ISO_CHARGING_REGULATED)) {
        simulate_soc();
    }
    timepoint_last_update = std::chrono::steady_clock::now();
};

void CarSimulation::simulate_soc() {
    const double ms =
        std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - timepoint_last_update)
            .count();
    const double factor = MS_FACTOR * ms;
    double power = 0.0;
    types::evse_manager::EVInfo ev_info;
    switch (charge_mode) {
    case ChargeMode::None:
        // nothing to do
        break;
    case ChargeMode::AC:
        power = charge_current_a * config.ac_nominal_voltage;
        ev_info.target_current = charge_current_a;
        ev_info.target_voltage = 0;
        break;
    case ChargeMode::ACThreePhase:
        power = charge_current_a * config.ac_nominal_voltage * 3.0;
        ev_info.target_current = charge_current_a;
        ev_info.target_voltage = 0;
        break;
    case ChargeMode::DC:
        power = config.dc_target_current * config.dc_target_voltage;
        ev_info.target_current = config.dc_target_current;
        ev_info.target_voltage = config.dc_target_voltage;
        break;
    }

    if (sim_data.battery_charge_wh > sim_data.battery_capacity_wh) {
        sim_data.battery_charge_wh = sim_data.battery_capacity_wh;
    } else {
        sim_data.battery_charge_wh += power * factor;
    }

    auto soc = (sim_data.battery_charge_wh / sim_data.battery_capacity_wh) * 100.0;

    if (soc > 100.0) {
        soc = 100.0;
    } else if (soc <= 0.0) {
        soc = 0.0;
    }

    if (latest_soc != soc) {
        latest_soc = soc;

        if (!r_ev.empty()) {
            r_ev[0]->call_update_soc(soc);
        }
    }

    ev_info.soc = soc;
    ev_info.battery_capacity = sim_data.battery_capacity_wh;
    ev_info.battery_full_soc = 100;

    p_ev_manager->publish_ev_info(ev_info);
}

bool CarSimulation::sleep(const CmdArguments& arguments, size_t loop_interval_ms) {
    if (not sim_data.sleep_ticks_left.has_value()) {
        const auto sleep_time = std::stold(arguments[0]);
        const auto sleep_time_ms = sleep_time * 1000;
        sim_data.sleep_ticks_left = static_cast<long long>(sleep_time_ms / loop_interval_ms) + 1;
    }
    auto& sleep_ticks_left = sim_data.sleep_ticks_left.value();
    sleep_ticks_left -= 1;
    if (not(sleep_ticks_left > 0)) {
        sim_data.sleep_ticks_left.reset();
        return true;
    } else {
        return false;
    }
}

bool CarSimulation::iec_wait_pwr_ready(const CmdArguments& arguments) {
    sim_data.state = SimState::PLUGGED_IN;
    return (sim_data.pwm_duty_cycle > 7.0f && sim_data.pwm_duty_cycle < 97.0f);
}

bool CarSimulation::iso_wait_pwm_is_running(const CmdArguments& arguments) {
    sim_data.state = SimState::PLUGGED_IN;
    return (sim_data.pwm_duty_cycle > 4.0f && sim_data.pwm_duty_cycle < 97.0f);
}

bool CarSimulation::draw_power_regulated(const CmdArguments& arguments) {
    charge_current_a = std::stod(arguments[0]);
    r_ev_board_support->call_set_ac_max_current(charge_current_a);
    if (arguments[1] == constants::THREE_PHASES) {
        r_ev_board_support->call_set_three_phases(true);
        charge_mode = ChargeMode::ACThreePhase;
    } else {
        r_ev_board_support->call_set_three_phases(false);
        charge_mode = ChargeMode::AC;
    }
    sim_data.state = SimState::CHARGING_REGULATED;
    return true;
}

bool CarSimulation::draw_power_fixed(const CmdArguments& arguments) {
    charge_current_a = std::stod(arguments[0]);
    r_ev_board_support->call_set_ac_max_current(charge_current_a);
    if (arguments[1] == constants::THREE_PHASES) {
        r_ev_board_support->call_set_three_phases(true);
        charge_mode = ChargeMode::ACThreePhase;
    } else {
        r_ev_board_support->call_set_three_phases(false);
        charge_mode = ChargeMode::AC;
    }
    sim_data.state = SimState::CHARGING_FIXED;
    return true;
}

bool CarSimulation::pause(const CmdArguments& arguments) {
    sim_data.state = SimState::PLUGGED_IN;
    return true;
}

bool CarSimulation::unplug(const CmdArguments& arguments) {
    sim_data.state = SimState::UNPLUGGED;
    charge_mode = ChargeMode::None;
    return true;
}

bool CarSimulation::error_e(const CmdArguments& arguments) {
    sim_data.state = SimState::ERROR_E;
    return true;
}

bool CarSimulation::diode_fail(const CmdArguments& arguments) {
    sim_data.state = SimState::DIODE_FAIL;
    return true;
}

bool CarSimulation::rcd_current(const CmdArguments& arguments) {
    sim_data.rcd_current_ma = std::stof(arguments[0]);
    r_ev_board_support->call_set_rcd_error(sim_data.rcd_current_ma);
    return true;
}

bool CarSimulation::iso_wait_slac_matched(const CmdArguments& arguments) {
    sim_data.state = SimState::PLUGGED_IN;

    if (sim_data.slac_state == types::slac::State::UNMATCHED) {
        EVLOG_debug << "Slac UNMATCHED";
        if (!r_slac.empty()) {
            EVLOG_debug << "Slac trigger matching";
            r_slac[0]->call_reset();
            r_slac[0]->call_trigger_matching();
            sim_data.slac_state = types::slac::State::MATCHING;
        }
    }
    if (sim_data.slac_state == types::slac::State::MATCHED) {
        EVLOG_debug << "Slac Matched";
        return true;
    }
    return false;
}

bool CarSimulation::iso_wait_pwr_ready(const CmdArguments& arguments) {
    if (sim_data.iso_pwr_ready) {
        sim_data.state = SimState::ISO_POWER_READY;
        return true;
    }
    return false;
}

bool CarSimulation::iso_dc_power_on(const CmdArguments& arguments) {
    sim_data.state = SimState::ISO_POWER_READY;
    if (sim_data.dc_power_on) {
        sim_data.state = SimState::ISO_CHARGING_REGULATED;
        r_ev_board_support->call_allow_power_on(true);
        charge_mode = ChargeMode::DC;
        return true;
    }

    if (sim_data.iso_charger_paused) {

        const auto cmds =
            std::array<std::string, 2>{"pause;iso_wait_v2g_session_stopped;sleep 2;iso_wait_pwm_is_running;",
                                       "iso_wait_pwr_ready;iso_wait_for_stop 36000"};

        EVLOG_info << "Charger wants to pause the session";
        r_ev_board_support->call_allow_power_on(false);

        // NOTE(sl): Change when the Energymode has more then 2 values
        const std::string energy_mode = (sim_data.energy_mode == EnergyMode::AC) ? "AC" : "DC";
        const std::string iso_start_v2g_session = "iso_start_v2g_session " + energy_mode + ";";

        auto& modify_session_cmds = sim_data.modify_charging_session_cmds.emplace();

        modify_session_cmds = cmds[0];
        modify_session_cmds += iso_start_v2g_session;
        modify_session_cmds += cmds[1];

        sim_data.iso_pwr_ready = false;
        sim_data.sleep_ticks_left.reset();
        sim_data.iso_charger_paused = false;

        // NOTE(sl): return false, otherwise the simulation will end too early before the session cmds can be adjusted
    }

    return false;
}

bool CarSimulation::iso_start_v2g_session(const CmdArguments& arguments, bool three_phases) {
    const auto& energy_mode = arguments[0];
    const auto& payment_option = arguments[1];
    const auto& departure_time = std::stoi(arguments[2]);
    const auto& e_amount = std::stoi(arguments[3]);

    const auto selected_payment_option = [payment_option,
                                          this](bool auto_payment_option) -> types::iso15118::SelectedPaymentOption {
        if (auto_payment_option) {
            return types::iso15118::SelectedPaymentOption{};
        }
        auto selected_payment_option =
            types::iso15118::SelectedPaymentOption{std::nullopt, config.force_payment_option};

        if (payment_option == "externalpayment") {
            selected_payment_option.payment_option = types::iso15118::PaymentOption::ExternalPayment;
        } else if (payment_option == "contract") {
            selected_payment_option.payment_option = types::iso15118::PaymentOption::Contract;
        } else {
            EVLOG_warning << "Go back to auto payment because payment_option was not recognized";
            selected_payment_option.enforce_payment_option.reset();
        }

        return selected_payment_option;
    }(payment_option == "auto");

    if (energy_mode == constants::AC) {
        sim_data.energy_mode = EnergyMode::AC;
        if (not three_phases) {
            r_ev[0]->call_start_charging(types::iso15118::EnergyTransferMode::AC_single_phase_core,
                                         selected_payment_option, departure_time, e_amount);
            charge_mode = ChargeMode::AC;
        } else {
            r_ev[0]->call_start_charging(types::iso15118::EnergyTransferMode::AC_three_phase_core,
                                         selected_payment_option, departure_time, e_amount);
            charge_mode = ChargeMode::ACThreePhase;
        }
    } else if (energy_mode == constants::DC) {
        r_ev[0]->call_start_charging(types::iso15118::EnergyTransferMode::DC_extended, selected_payment_option,
                                     departure_time, e_amount);
        sim_data.energy_mode = EnergyMode::DC;
        charge_mode = ChargeMode::DC;
    } else {
        return false;
    }
    return true;
}

bool CarSimulation::iso_draw_power_regulated(const CmdArguments& arguments) {
    charge_current_a = std::stod(arguments[0]);
    r_ev_board_support->call_set_ac_max_current(charge_current_a);
    if (arguments[1] == constants::THREE_PHASES) {
        r_ev_board_support->call_set_three_phases(true);
        charge_mode = ChargeMode::ACThreePhase;
    } else {
        r_ev_board_support->call_set_three_phases(false);
        charge_mode = ChargeMode::AC;
    }
    sim_data.state = SimState::ISO_CHARGING_REGULATED;
    return true;
}

bool CarSimulation::iso_stop_charging(const CmdArguments& arguments) {
    r_ev[0]->call_stop_charging();
    r_ev_board_support->call_allow_power_on(false);
    if (sim_data.state != SimState::UNPLUGGED) {
        sim_data.state = SimState::PLUGGED_IN;
    }
    charge_mode = ChargeMode::None;
    return true;
}

bool CarSimulation::iso_wait_for_stop(const CmdArguments& arguments, size_t loop_interval_ms) {
    if (not sim_data.sleep_ticks_left.has_value()) {
        const auto sleep_time_ms = std::stold(arguments[0]) * 1000;
        sim_data.sleep_ticks_left = static_cast<long long>(sleep_time_ms / loop_interval_ms) + 1;
    }
    auto& sleep_ticks_left = sim_data.sleep_ticks_left.value();
    sleep_ticks_left -= 1;
    if (not(sleep_ticks_left > 0)) {
        r_ev[0]->call_stop_charging();
        r_ev_board_support->call_allow_power_on(false);
        sim_data.state = SimState::PLUGGED_IN;
        sim_data.sleep_ticks_left.reset();
        return true;
    }
    if (sim_data.iso_stopped) {
        EVLOG_info << "POWER OFF iso stopped";
        r_ev_board_support->call_allow_power_on(false);
        sim_data.state = SimState::PLUGGED_IN;
        sim_data.sleep_ticks_left.reset();
        return true;
    }

    if (sim_data.iso_charger_paused) {

        const auto cmds =
            std::array<std::string, 2>{"pause;iso_wait_v2g_session_stopped;sleep 2;iso_wait_pwm_is_running;",
                                       "iso_wait_pwr_ready;iso_wait_for_stop 36000"};

        EVLOG_info << "Charger wants to pause the session";
        r_ev_board_support->call_allow_power_on(false);

        // NOTE(sl): Change when the Energymode has more then 2 values
        const std::string energy_mode = (sim_data.energy_mode == EnergyMode::AC) ? "AC" : "DC";
        const std::string iso_start_v2g_session = "iso_start_v2g_session " + energy_mode + ";";

        auto& modify_session_cmds = sim_data.modify_charging_session_cmds.emplace();

        modify_session_cmds = cmds[0];
        modify_session_cmds += iso_start_v2g_session;
        modify_session_cmds += cmds[1];

        sim_data.iso_pwr_ready = false;
        sim_data.sleep_ticks_left.reset();
        sim_data.iso_charger_paused = false;

        // NOTE(sl): return false, otherwise the simulation will end too early before the session cmds can be adjusted
        return false;
    }
    return false;
}

bool CarSimulation::iso_wait_v2g_session_stopped(const CmdArguments& arguments) {
    if (sim_data.v2g_finished) {
        sim_data.v2g_finished = false;
        return true;
    }
    return false;
}

bool CarSimulation::iso_pause_charging(const CmdArguments& arguments) {
    r_ev[0]->call_pause_charging();
    sim_data.state = SimState::PLUGGED_IN;
    sim_data.iso_pwr_ready = false;
    return true;
}

bool CarSimulation::iso_wait_for_resume(const CmdArguments& arguments) {
    return false;
}

bool CarSimulation::iso_start_bcb_toggle(const CmdArguments& arguments) {
    sim_data.v2g_finished = false;
    sim_data.state = SimState::BCB_TOGGLE;
    if (sim_data.bcb_toggles >= std::stoul(arguments[0]) || sim_data.bcb_toggles == 3) {
        sim_data.bcb_toggles = 0;
        sim_data.state = SimState::PLUGGED_IN;
        return true;
    }
    return false;
}

bool CarSimulation::wait_for_real_plugin(const CmdArguments& arguments) {
    using types::board_support_common::Event;
    if (sim_data.actual_bsp_event == Event::A) {
        EVLOG_info << "Real plugin detected";
        sim_data.state = SimState::PLUGGED_IN;
        return true;
    }
    return false;
}
