// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later

#include "avplayer_file_streamer.h"
#include "avplayer_source.h"
#include "avplayer_state.h"

#include "core/libraries/error_codes.h"
#include "core/libraries/kernel/time_management.h"

#include <magic_enum.hpp>

namespace Libraries::AvPlayer {

using namespace Kernel;

void PS4_SYSV_ABI AvPlayerState::AutoPlayEventCallback(void* opaque, s32 event_id, s32 source_id,
                                                       void* event_data) {
    auto const self = reinterpret_cast<AvPlayerState*>(opaque);

    if (event_id == SCE_AVPLAYER_STATE_READY) {
        s32 video_stream_index = -1;
        s32 audio_stream_index = -1;
        s32 timedtext_stream_index = -1;
        const s32 stream_count = self->GetStreamCount();
        if (AVPLAYER_IS_ERROR(stream_count)) {
            return;
        }
        if (stream_count == 0) {
            self->Stop();
            return;
        }
        for (u32 stream_index = 0; stream_index < stream_count; ++stream_index) {
            SceAvPlayerStreamInfo info{};
            self->GetStreamInfo(stream_index, info);

            const std::string_view default_language(
                reinterpret_cast<char*>(self->m_default_language));
            switch (info.type) {
            case SCE_AVPLAYER_VIDEO:
                if (video_stream_index == -1) {
                    video_stream_index = stream_index;
                }
                if (!default_language.empty() &&
                    default_language == reinterpret_cast<char*>(info.details.video.language_code)) {
                    video_stream_index = stream_index;
                }
                break;
            case SCE_AVPLAYER_AUDIO:
                if (audio_stream_index == -1) {
                    audio_stream_index = stream_index;
                }
                if (!default_language.empty() &&
                    default_language == reinterpret_cast<char*>(info.details.video.language_code)) {
                    audio_stream_index = stream_index;
                }
                break;
            case SCE_AVPLAYER_TIMEDTEXT:
                if (default_language.empty()) {
                    timedtext_stream_index = stream_index;
                    break;
                }
                if (default_language == reinterpret_cast<char*>(info.details.video.language_code)) {
                    timedtext_stream_index = stream_index;
                }
                break;
            }
        }

        if (video_stream_index != -1) {
            self->EnableStream(video_stream_index);
        }
        if (audio_stream_index != -1) {
            self->EnableStream(audio_stream_index);
        }
        if (timedtext_stream_index != -1) {
            self->EnableStream(timedtext_stream_index);
        }
        self->Start();
        return;
    }

    // Pass other events to the game
    const auto callback = self->m_event_replacement.event_callback;
    const auto ptr = self->m_event_replacement.object_ptr;
    if (callback != nullptr) {
        callback(ptr, event_id, 0, event_data);
    }
}

// Called inside GAME thread
AvPlayerState::AvPlayerState(const SceAvPlayerInitData& init_data)
    : m_init_data(init_data), m_event_replacement(init_data.event_replacement) {
    if (m_event_replacement.event_callback == nullptr || init_data.auto_start) {
        m_auto_start = true;
        m_init_data.event_replacement.event_callback = &AvPlayerState::AutoPlayEventCallback;
        m_init_data.event_replacement.object_ptr = this;
    }
    if (init_data.default_language != nullptr) {
        std::memcpy(m_default_language, init_data.default_language, sizeof(m_default_language));
    }
    SetState(AvState::Initial);
    StartControllerThread();
}

AvPlayerState::~AvPlayerState() {
    {
        std::unique_lock lock(m_source_mutex);
        m_up_source.reset();
    }
    if (m_controller_thread.joinable()) {
        m_controller_thread.request_stop();
        m_controller_thread.join();
    }
    m_event_queue.Clear();
}

// Called inside GAME thread
s32 AvPlayerState::AddSource(std::string_view path, SceAvPlayerSourceType source_type) {
    if (path.empty()) {
        LOG_ERROR(Lib_AvPlayer, "File path is empty.");
        return -1;
    }

    {
        std::unique_lock lock(m_source_mutex);
        if (m_up_source != nullptr) {
            LOG_ERROR(Lib_AvPlayer, "Only one source is supported.");
            return -1;
        }

        m_up_source = std::make_unique<AvPlayerSource>(*this, path, m_init_data, source_type);
    }
    AddSourceEvent();
    return 0;
}

// Called inside GAME thread
s32 AvPlayerState::GetStreamCount() {
    std::shared_lock lock(m_source_mutex);
    if (m_up_source == nullptr) {
        LOG_ERROR(Lib_AvPlayer, "Could not get stream count. No source.");
        return -1;
    }
    return m_up_source->GetStreamCount();
}

// Called inside GAME thread
s32 AvPlayerState::GetStreamInfo(u32 stream_index, SceAvPlayerStreamInfo& info) {
    std::shared_lock lock(m_source_mutex);
    if (m_up_source == nullptr) {
        LOG_ERROR(Lib_AvPlayer, "Could not get stream {} info. No source.", stream_index);
        return -1;
    }
    return m_up_source->GetStreamInfo(stream_index, info);
}

// Called inside GAME thread
s32 AvPlayerState::Start() {
    std::shared_lock lock(m_source_mutex);
    if (m_up_source == nullptr || m_up_source->Start() < 0) {
        LOG_ERROR(Lib_AvPlayer, "Could not start playback.");
        return -1;
    }
    SetState(AvState::Play);
    OnPlaybackStateChanged(AvState::Play);
    return 0;
}

void AvPlayerState::AvControllerThread(std::stop_token stop) {
    using std::chrono::milliseconds;

    while (!stop.stop_requested()) {
        if (m_event_queue.Size() != 0) {
            ProcessEvent();
            continue;
        }
        std::this_thread::sleep_for(milliseconds(5));
        UpdateBufferingState();
    }
}

// Called inside GAME thread
void AvPlayerState::AddSourceEvent() {
    SetState(AvState::AddingSource);
    m_event_queue.Push(AvPlayerEvent{
        .event = AvEventType::AddSource,
    });
}

void AvPlayerState::WarningEvent(s32 id) {
    m_event_queue.Push(AvPlayerEvent{
        .event = AvEventType::WarningId,
        .payload =
            {
                .error = id,
            },
    });
}

// Called inside GAME thread
void AvPlayerState::StartControllerThread() {
    m_controller_thread =
        std::jthread([this](std::stop_token stop) { this->AvControllerThread(stop); });
}

// Called inside GAME thread
bool AvPlayerState::EnableStream(u32 stream_index) {
    std::shared_lock lock(m_source_mutex);
    if (m_up_source == nullptr) {
        return false;
    }
    return m_up_source->EnableStream(stream_index);
}

// Called inside GAME thread
bool AvPlayerState::Stop() {
    std::shared_lock lock(m_source_mutex);
    if (m_up_source == nullptr || m_current_state == AvState::Stop) {
        return false;
    }
    if (!SetState(AvState::Stop)) {
        return false;
    }
    if (!m_up_source->Stop()) {
        return false;
    }
    OnPlaybackStateChanged(AvState::Stop);
    return true;
}

bool AvPlayerState::GetVideoData(SceAvPlayerFrameInfo& video_info) {
    std::shared_lock lock(m_source_mutex);
    if (m_up_source == nullptr) {
        return false;
    }
    return m_up_source->GetVideoData(video_info);
}

bool AvPlayerState::GetVideoData(SceAvPlayerFrameInfoEx& video_info) {
    std::shared_lock lock(m_source_mutex);
    if (m_up_source == nullptr) {
        return false;
    }
    return m_up_source->GetVideoData(video_info);
}

bool AvPlayerState::GetAudioData(SceAvPlayerFrameInfo& audio_info) {
    std::shared_lock lock(m_source_mutex);
    if (m_up_source == nullptr) {
        return false;
    }
    return m_up_source->GetAudioData(audio_info);
}

bool AvPlayerState::IsActive() {
    std::shared_lock lock(m_source_mutex);
    if (m_up_source == nullptr) {
        return false;
    }
    return m_current_state != AvState::Stop && m_current_state != AvState::Error &&
           m_current_state != AvState::EndOfFile && m_up_source->IsActive();
}

u64 AvPlayerState::CurrentTime() {
    std::shared_lock lock(m_source_mutex);
    if (m_up_source == nullptr) {
        LOG_ERROR(Lib_AvPlayer, "Could not get current time. No source.");
        return 0;
    }
    return m_up_source->CurrentTime();
}

bool AvPlayerState::SetLooping(bool is_looping) {
    std::shared_lock lock(m_source_mutex);
    if (m_up_source == nullptr) {
        LOG_ERROR(Lib_AvPlayer, "Could not set loop flag. No source.");
        return false;
    }
    m_up_source->SetLooping(is_looping);
    return true;
}

// May be called from different threads
void AvPlayerState::OnWarning(u32 id) {
    // Forward to CONTROLLER thread
    WarningEvent(id);
}

void AvPlayerState::OnError() {
    SetState(AvState::Error);
    OnPlaybackStateChanged(AvState::Error);
}

void AvPlayerState::OnEOF() {
    SetState(AvState::EndOfFile);
}

// Called inside CONTROLLER thread
void AvPlayerState::OnPlaybackStateChanged(AvState state) {
    switch (state) {
    case AvState::Ready: {
        EmitEvent(SCE_AVPLAYER_STATE_READY);
        break;
    }
    case AvState::Play: {
        EmitEvent(SCE_AVPLAYER_STATE_PLAY);
        break;
    }
    case AvState::Stop: {
        EmitEvent(SCE_AVPLAYER_STATE_STOP);
        break;
    }
    case AvState::Pause: {
        EmitEvent(SCE_AVPLAYER_STATE_PAUSE);
        break;
    }
    case AvState::Buffering: {
        EmitEvent(SCE_AVPLAYER_STATE_BUFFERING);
        break;
    }
    default:
        break;
    }
}

// Called inside CONTROLLER and GAME threads
bool AvPlayerState::SetState(AvState state) {
    std::lock_guard guard(m_state_machine_mutex);

    if (!IsStateTransitionValid(state)) {
        LOG_ERROR(Lib_AvPlayer, "Invalid state transition: {} -> {}",
                  magic_enum::enum_name(m_current_state.load()), magic_enum::enum_name(state));
        return false;
    }
    m_previous_state.store(m_current_state);
    m_current_state.store(state);
    return true;
}

// Called inside CONTROLLER thread
std::optional<bool> AvPlayerState::OnBufferingCheckEvent(u32 num_frames) {
    std::shared_lock lock(m_source_mutex);
    if (!m_up_source) {
        return std::nullopt;
    }
    return m_up_source->HasFrames(num_frames);
}

// Called inside CONTROLLER thread
void AvPlayerState::EmitEvent(SceAvPlayerEvents event_id, void* event_data) {
    LOG_INFO(Lib_AvPlayer, "Sending event to the game: id = {}", magic_enum::enum_name(event_id));
    const auto callback = m_init_data.event_replacement.event_callback;
    if (callback) {
        const auto ptr = m_init_data.event_replacement.object_ptr;
        callback(ptr, event_id, 0, event_data);
    }
}

// Called inside CONTROLLER thread
void AvPlayerState::ProcessEvent() {
    if (m_current_state == AvState::Jump) {
        return;
    }

    std::lock_guard guard(m_event_handler_mutex);

    auto event = m_event_queue.Pop();
    if (!event.has_value()) {
        return;
    }
    switch (event->event) {
    case AvEventType::WarningId: {
        OnWarning(event->payload.error);
        break;
    }
    case AvEventType::RevertState: {
        SetState(m_previous_state.load());
        break;
    }
    case AvEventType::AddSource: {
        std::shared_lock lock(m_source_mutex);
        if (m_up_source->FindStreamInfo()) {
            SetState(AvState::Ready);
            OnPlaybackStateChanged(AvState::Ready);
        } else {
            OnWarning(ORBIS_AVPLAYER_ERROR_NOT_SUPPORTED);
            SetState(AvState::Error);
        }
        break;
    }
    case AvEventType::Error: {
        OnWarning(event->payload.error);
        SetState(AvState::Error);
        break;
    }
    default:
        break;
    }
}

// Called inside CONTROLLER thread
void AvPlayerState::UpdateBufferingState() {
    if (m_current_state == AvState::Buffering) {
        const auto has_frames = OnBufferingCheckEvent(10);
        if (!has_frames.has_value()) {
            return;
        }
        if (has_frames.value()) {
            const auto state =
                m_previous_state >= AvState::C0x0B ? m_previous_state.load() : AvState::Play;
            SetState(state);
            OnPlaybackStateChanged(state);
        }
    } else if (m_current_state == AvState::Play) {
        const auto has_frames = OnBufferingCheckEvent(0);
        if (!has_frames.has_value()) {
            return;
        }
        if (!has_frames.value()) {
            SetState(AvState::Buffering);
            OnPlaybackStateChanged(AvState::Buffering);
        }
    }
}

bool AvPlayerState::IsStateTransitionValid(AvState state) {
    switch (state) {
    case AvState::Play: {
        switch (m_current_state.load()) {
        case AvState::Stop:
        case AvState::EndOfFile:
        // case AvState::C0x08:
        case AvState::Error:
            return false;
        default:
            return true;
        }
    }
    case AvState::Pause: {
        switch (m_current_state.load()) {
        case AvState::Stop:
        case AvState::EndOfFile:
        // case AvState::C0x08:
        case AvState::Starting:
        case AvState::Error:
            return false;
        default:
            return true;
        }
    }
    case AvState::Jump: {
        switch (m_current_state.load()) {
        case AvState::Stop:
        case AvState::EndOfFile:
        // case AvState::C0x08:
        case AvState::TrickMode:
        case AvState::Starting:
        case AvState::Error:
            return false;
        default:
            return true;
        }
    }
    case AvState::TrickMode: {
        switch (m_current_state.load()) {
        case AvState::Stop:
        case AvState::EndOfFile:
        // case AvState::C0x08:
        case AvState::Jump:
        case AvState::Starting:
        case AvState::Error:
            return false;
        default:
            return true;
        }
    }
    case AvState::Buffering: {
        switch (m_current_state.load()) {
        case AvState::Stop:
        case AvState::EndOfFile:
        case AvState::Pause:
        // case AvState::C0x08:
        case AvState::Starting:
        case AvState::Error:
            return false;
        default:
            return true;
        }
    }
    default:
        return true;
    }
}

} // namespace Libraries::AvPlayer
