// SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include <chrono>

#include "core/core.h"
#include "core/core_timing.h"

#include "common/settings.h"
#include "common/time_zone.h"
#include "core/file_sys/vfs/vfs.h"
#include "core/hle/kernel/svc.h"
#include "core/hle/service/glue/time/manager.h"
#include "core/hle/service/glue/time/time_zone_binary.h"
#include "core/hle/service/psc/time/service_manager.h"
#include "core/hle/service/psc/time/static.h"
#include "core/hle/service/psc/time/system_clock.h"
#include "core/hle/service/psc/time/time_zone_service.h"
#include "core/hle/service/set/system_settings_server.h"
#include "core/hle/service/sm/sm.h"

namespace Service::Glue::Time {
namespace {

template <typename T>
T GetSettingsItemValue(std::shared_ptr<Service::Set::ISystemSettingsServer>& set_sys,
                       const char* category, const char* name) {
    std::vector<u8> interval_buf;
    auto res = set_sys->GetSettingsItemValue(interval_buf, category, name);
    ASSERT(res == ResultSuccess);

    T v{};
    std::memcpy(&v, interval_buf.data(), sizeof(T));
    return v;
}

s64 CalendarTimeToEpoch(Service::PSC::Time::CalendarTime calendar) {
    constexpr auto is_leap = [](s32 year) -> bool {
        return (((year) % 4) == 0 && (((year) % 100) != 0 || ((year) % 400) == 0));
    };
    constexpr std::array<s32, 12> MonthStartDayOfYear{
        0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334,
    };

    s16 month_s16{calendar.month};
    s8 month{static_cast<s8>(((month_s16 * 43) & ~std::numeric_limits<s16>::max()) +
                             ((month_s16 * 43) >> 9))};
    s8 month_index{static_cast<s8>(calendar.month - 12 * month)};
    if (month_index == 0) {
        month_index = 12;
    }
    s32 year{(month + calendar.year) - !month_index};
    s32 v8{year >= 0 ? year : year + 3};

    s64 days_since_epoch = calendar.day + MonthStartDayOfYear[month_index - 1];
    days_since_epoch += (year * 365) + (v8 / 4) - (year / 100) + (year / 400) - 365;

    if (month_index <= 2 && is_leap(year)) {
        days_since_epoch--;
    }
    auto epoch_s{((24ll * days_since_epoch + calendar.hour) * 60ll + calendar.minute) * 60ll +
                 calendar.second};
    return epoch_s - 62135683200ll;
}

s64 GetEpochTimeFromInitialYear(std::shared_ptr<Service::Set::ISystemSettingsServer>& set_sys) {
    Service::PSC::Time::CalendarTime calendar{
        .year = GetSettingsItemValue<s16>(set_sys, "time", "standard_user_clock_initial_year"),
        .month = 1,
        .day = 1,
        .hour = 0,
        .minute = 0,
        .second = 0,
    };
    return CalendarTimeToEpoch(calendar);
}

Service::PSC::Time::LocationName GetTimeZoneString(Service::PSC::Time::LocationName& in_name) {
    auto configured_zone = Settings::GetTimeZoneString(Settings::values.time_zone_index.GetValue());

    Service::PSC::Time::LocationName configured_name{};
    std::memcpy(configured_name.data(), configured_zone.data(),
                std::min(configured_name.size(), configured_zone.size()));

    if (!IsTimeZoneBinaryValid(configured_name)) {
        configured_zone = Common::TimeZone::FindSystemTimeZone();
        configured_name = {};
        std::memcpy(configured_name.data(), configured_zone.data(),
                    std::min(configured_name.size(), configured_zone.size()));
    }

    ASSERT_MSG(IsTimeZoneBinaryValid(configured_name), "Invalid time zone {}!",
               configured_name.data());

    return configured_name;
}

} // namespace

TimeManager::TimeManager(Core::System& system)
    : m_steady_clock_resource{system}, m_worker{system, m_steady_clock_resource,
                                                m_file_timestamp_worker} {
    m_time_m =
        system.ServiceManager().GetService<Service::PSC::Time::ServiceManager>("time:m", true);

    auto res = m_time_m->GetStaticServiceAsServiceManager(&m_time_sm);
    ASSERT(res == ResultSuccess);

    m_set_sys =
        system.ServiceManager().GetService<Service::Set::ISystemSettingsServer>("set:sys", true);

    res = MountTimeZoneBinary(system);
    ASSERT(res == ResultSuccess);

    m_worker.Initialize(m_time_sm, m_set_sys);

    res = m_time_sm->GetStandardUserSystemClock(&m_file_timestamp_worker.m_system_clock);
    ASSERT(res == ResultSuccess);

    res = m_time_sm->GetTimeZoneService(&m_file_timestamp_worker.m_time_zone);
    ASSERT(res == ResultSuccess);

    res = SetupStandardSteadyClockCore();
    ASSERT(res == ResultSuccess);

    Service::PSC::Time::SystemClockContext user_clock_context{};
    res = m_set_sys->GetUserSystemClockContext(user_clock_context);
    ASSERT(res == ResultSuccess);

    // TODO the local clock should initialise with this epoch time, and be updated somewhere else on
    // first boot to update it, but I haven't been able to find that point (likely via ntc's auto
    // correct as it's defaulted to be enabled). So to get a time that isn't stuck in the past for
    // first boot, grab the current real seconds.
    auto epoch_time{GetEpochTimeFromInitialYear(m_set_sys)};
    if (user_clock_context == Service::PSC::Time::SystemClockContext{}) {
        m_steady_clock_resource.GetRtcTimeInSeconds(epoch_time);
    }

    res = m_time_m->SetupStandardLocalSystemClockCore(user_clock_context, epoch_time);
    ASSERT(res == ResultSuccess);

    Service::PSC::Time::SystemClockContext network_clock_context{};
    res = m_set_sys->GetNetworkSystemClockContext(network_clock_context);
    ASSERT(res == ResultSuccess);

    auto network_accuracy_m{GetSettingsItemValue<s32>(
        m_set_sys, "time", "standard_network_clock_sufficient_accuracy_minutes")};
    auto one_minute_ns{
        std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::minutes(1)).count()};
    s64 network_accuracy_ns{network_accuracy_m * one_minute_ns};

    res = m_time_m->SetupStandardNetworkSystemClockCore(network_clock_context, network_accuracy_ns);
    ASSERT(res == ResultSuccess);

    bool is_automatic_correction_enabled{};
    res = m_set_sys->IsUserSystemClockAutomaticCorrectionEnabled(is_automatic_correction_enabled);
    ASSERT(res == ResultSuccess);

    Service::PSC::Time::SteadyClockTimePoint automatic_correction_time_point{};
    res = m_set_sys->GetUserSystemClockAutomaticCorrectionUpdatedTime(
        automatic_correction_time_point);
    ASSERT(res == ResultSuccess);

    res = m_time_m->SetupStandardUserSystemClockCore(is_automatic_correction_enabled,
                                                     automatic_correction_time_point);
    ASSERT(res == ResultSuccess);

    res = m_time_m->SetupEphemeralNetworkSystemClockCore();
    ASSERT(res == ResultSuccess);

    res = SetupTimeZoneServiceCore();
    ASSERT(res == ResultSuccess);

    s64 rtc_time_s{};
    res = m_steady_clock_resource.GetRtcTimeInSeconds(rtc_time_s);
    ASSERT(res == ResultSuccess);

    // TODO system report "launch"
    //      "rtc_reset" = m_steady_clock_resource.m_rtc_reset
    //      "rtc_value" = rtc_time_s

    m_worker.StartThread();

    m_file_timestamp_worker.m_initialized = true;

    s64 system_clock_time{};
    if (m_file_timestamp_worker.m_system_clock->GetCurrentTime(&system_clock_time) ==
        ResultSuccess) {
        Service::PSC::Time::CalendarTime calendar_time{};
        Service::PSC::Time::CalendarAdditionalInfo calendar_additional{};
        if (m_file_timestamp_worker.m_time_zone->ToCalendarTimeWithMyRule(
                &calendar_time, &calendar_additional, system_clock_time) == ResultSuccess) {
            // TODO IFileSystemProxy::SetCurrentPosixTime(system_clock_time,
            // calendar_additional.ut_offset)
        }
    }
}

Result TimeManager::SetupStandardSteadyClockCore() {
    Common::UUID external_clock_source_id{};
    auto res = m_set_sys->GetExternalSteadyClockSourceId(external_clock_source_id);
    ASSERT(res == ResultSuccess);

    s64 external_steady_clock_internal_offset_s{};
    res = m_set_sys->GetExternalSteadyClockInternalOffset(external_steady_clock_internal_offset_s);
    ASSERT(res == ResultSuccess);

    auto one_second_ns{
        std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::seconds(1)).count()};
    s64 external_steady_clock_internal_offset_ns{external_steady_clock_internal_offset_s *
                                                 one_second_ns};

    s32 standard_steady_clock_test_offset_m{
        GetSettingsItemValue<s32>(m_set_sys, "time", "standard_steady_clock_test_offset_minutes")};
    auto one_minute_ns{
        std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::minutes(1)).count()};
    s64 standard_steady_clock_test_offset_ns{standard_steady_clock_test_offset_m * one_minute_ns};

    auto reset_detected = m_steady_clock_resource.GetResetDetected();
    if (reset_detected) {
        external_clock_source_id = {};
    }

    Common::UUID clock_source_id{};
    m_steady_clock_resource.Initialize(&clock_source_id, &external_clock_source_id);

    if (clock_source_id != external_clock_source_id) {
        m_set_sys->SetExternalSteadyClockSourceId(clock_source_id);
    }

    res = m_time_m->SetupStandardSteadyClockCore(
        reset_detected, clock_source_id, m_steady_clock_resource.GetTime(),
        external_steady_clock_internal_offset_ns, standard_steady_clock_test_offset_ns);
    ASSERT(res == ResultSuccess);
    R_SUCCEED();
}

Result TimeManager::SetupTimeZoneServiceCore() {
    Service::PSC::Time::LocationName name{};
    auto res = m_set_sys->GetDeviceTimeZoneLocationName(name);
    ASSERT(res == ResultSuccess);

    auto configured_zone = GetTimeZoneString(name);

    if (configured_zone != name) {
        m_set_sys->SetDeviceTimeZoneLocationName(configured_zone);
        name = configured_zone;

        std::shared_ptr<Service::PSC::Time::SystemClock> local_clock;
        m_time_sm->GetStandardLocalSystemClock(&local_clock);

        Service::PSC::Time::SystemClockContext context{};
        local_clock->GetSystemClockContext(&context);
        m_set_sys->SetDeviceTimeZoneLocationUpdatedTime(context.steady_time_point);
    }

    Service::PSC::Time::SteadyClockTimePoint time_point{};
    res = m_set_sys->GetDeviceTimeZoneLocationUpdatedTime(time_point);
    ASSERT(res == ResultSuccess);

    auto location_count = GetTimeZoneCount();
    Service::PSC::Time::RuleVersion rule_version{};
    GetTimeZoneVersion(rule_version);

    std::span<const u8> rule_buffer{};
    size_t rule_size{};
    res = GetTimeZoneRule(rule_buffer, rule_size, name);
    ASSERT(res == ResultSuccess);

    res = m_time_m->SetupTimeZoneServiceCore(name, rule_version, location_count, time_point,
                                             rule_buffer);
    ASSERT(res == ResultSuccess);

    R_SUCCEED();
}

} // namespace Service::Glue::Time
