// Vita3K emulator project
// Copyright (C) 2026 Vita3K team
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along
// with this program; if not, write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

#include <module/module.h>

#include <codec/state.h>
#include <io/functions.h>
#include <kernel/state.h>

#include <util/lock_and_find.h>
#include <util/log.h>

#include <algorithm>

// Defines stop/pause behaviour. If true, GetVideo/AudioData will return false when stopped.
constexpr bool REJECT_DATA_ON_PAUSE = true;

// Uses a catchup video style if lag causes the video to go behind.
constexpr bool CATCHUP_VIDEO_PLAYBACK = true;

/*
typedef Ptr<void> (*SceAvPlayerAllocator)(uint32_t arguments, uint32_t alignment, uint32_t size);
typedef void (*SceAvPlayerDeallocator)(uint32_t arguments, Ptr<void> memory);

typedef int32_t (*SceAvPlayerOpenFile)(uint32_t arguments, Ptr<const char> filename);
typedef int32_t (*SceAvPlayerCloseFile)(uint32_t arguments);
typedef int32_t (*SceAvPlayerReadFile)(uint32_t arguments, Ptr<uint8_t> buffer, uint64_t offset, uint32_t size);
typedef uint64_t (*SceAvPlayerGetFileSize)(uint32_t arguments);

typedef void (*SceAvPlayerEventCallback)(uint32_t arguments, int32_t event_id, int32_t source_id, Ptr<void> event_data);
*/

struct PlayerInfoState;
typedef std::shared_ptr<PlayerInfoState> PlayerPtr;
typedef std::map<SceUID, PlayerPtr> PlayerStates;

struct AvPlayerState {
    std::mutex mutex;
    PlayerStates players;
};

struct SceAvPlayerMemoryAllocator {
    uint32_t user_data = 0;

    // All of these should be cast to SceAvPlayerAllocator or SceAvPlayerDeallocator types.
    Ptr<void> general_allocator;
    Ptr<void> general_deallocator;
    Ptr<void> texture_allocator;
    Ptr<void> texture_deallocator;
};

struct SceAvPlayerFileManager {
    uint32_t user_data = 0;

    // Cast to SceAvPlayerOpenFile, SceAvPlayerCloseFile, SceAvPlayerReadFile and SceAvPlayerGetFileSize.
    Ptr<void> open_file;
    Ptr<void> close_file;
    Ptr<void> read_file;
    Ptr<void> file_size;
};

struct SceAvPlayerEventManager {
    uint32_t user_data = 0;

    // Cast to SceAvPlayerEventCallback.
    Ptr<void> event_callback;
};

struct PlayerInfoState {
    PlayerState player;

    // Framebuffer count is defined in info. I'm being safe now and forcing it to 4 (even though its usually 2).
    constexpr static uint32_t RING_BUFFER_COUNT = 4;

    uint32_t video_buffer_ring_index = 0;
    uint32_t video_buffer_size = 0;
    std::array<Ptr<uint8_t>, RING_BUFFER_COUNT> video_buffer;

    uint32_t audio_buffer_ring_index = 0;
    uint32_t audio_buffer_size = 0;
    std::array<Ptr<uint8_t>, RING_BUFFER_COUNT> audio_buffer;

    bool do_loop = false;
    bool paused = false;

    uint64_t last_frame_time = 0;
    SceAvPlayerMemoryAllocator memory_allocator;
    SceAvPlayerFileManager file_manager;
    SceAvPlayerEventManager event_manager;
};

enum class DebugLevel {
    NONE,
    INFO,
    WARNINGS,
    ALL,
};

struct SceAvPlayerInfo {
    SceAvPlayerMemoryAllocator memory_allocator;
    SceAvPlayerFileManager file_manager;
    SceAvPlayerEventManager event_manager;
    DebugLevel debug_level;
    uint32_t base_priority;
    int32_t frame_buffer_count;
    int32_t auto_start;
    uint32_t unknown0;
};

struct SceAvPlayerAudio {
    uint16_t channels;
    uint16_t unknown;
    uint32_t sample_rate;
    uint32_t size;
    char language[4];
};

struct SceAvPlayerVideo {
    uint32_t width;
    uint32_t height;
    float aspect_ratio;
    char language[4];
};

struct SceAvPlayerTextPosition {
    uint16_t top;
    uint16_t left;
    uint16_t bottom;
    uint16_t right;
};

struct SceAvPlayerTimedText {
    char language[4];
    uint16_t text_size;
    uint16_t font_size;
    SceAvPlayerTextPosition position;
};

union SceAvPlayerStreamDetails {
    SceAvPlayerAudio audio;
    SceAvPlayerVideo video;
    SceAvPlayerTimedText text;
};

struct SceAvPlayerFrameInfo {
    Ptr<uint8_t> data;
    uint32_t unknown;
    uint64_t timestamp;
    SceAvPlayerStreamDetails stream_details;
};

enum class MediaType {
    VIDEO,
    AUDIO,
};

struct SceAvPlayerStreamInfo {
    MediaType stream_type;
    uint32_t unknown;
    SceAvPlayerStreamDetails stream_details;
};

enum SceAvPlayerErrorCode : uint32_t {
    SCE_AVPLAYER_ERROR_ILLEGAL_ADDR = 0x806a0001,
    SCE_AVPLAYER_ERROR_INVALID_ARGUMENT = 0x806a0002,
    SCE_AVPLAYER_ERROR_NOT_ENOUGH_MEMORY = 0x806a0003,
    SCE_AVPLAYER_ERROR_INVALID_EVENT = 0x806a0004,
    SCE_AVPLAYER_WAR_FILE_NONINTERLEAVED = 0x806a00a0,
    SCE_AVPLAYER_WAR_LOOPING_BACK = 0x806a00a1,
    SCE_AVPLAYER_WAR_JUMP_COMPLETE = 0x806a00a3
};

enum SceAvPlayerState {
    SCE_AVPLAYER_STATE_UNKNOWN = 0,
    SCE_AVPLAYER_STATE_STOP = 1,
    SCE_AVPLAYER_STATE_READY = 2,
    SCE_AVPLAYER_STATE_PLAY = 3,
    SCE_AVPLAYER_STATE_PAUSE = 4,
    SCE_AVPLAYER_STATE_BUFFERING = 5,
    SCE_AVPLAYER_TIMED_TEXT_DELIVERY = 16,
    SCE_AVPLAYER_WARNING_ID = 32,
    SCE_AVPLAYER_ENCRYPTION = 48
};

static inline uint64_t current_time() {
    return std::chrono::duration_cast<std::chrono::microseconds>(
        std::chrono::high_resolution_clock::now().time_since_epoch())
        .count();
}

static Ptr<uint8_t> get_buffer(const PlayerPtr &player, MediaType media_type,
    MemState &mem, uint32_t size, bool new_frame = true) {
    uint32_t &buffer_size = media_type == MediaType::VIDEO ? player->video_buffer_size : player->audio_buffer_size;
    uint32_t &ring_index = media_type == MediaType::VIDEO ? player->video_buffer_ring_index : player->audio_buffer_ring_index;
    auto &buffers = media_type == MediaType::VIDEO ? player->video_buffer : player->audio_buffer;

    if (buffer_size < size) {
        buffer_size = size;
        for (uint32_t a = 0; a < PlayerInfoState::RING_BUFFER_COUNT; a++) {
            if (buffers[a])
                free(mem, buffers[a]);
            std::string alloc_name = fmt::format("AvPlayer {} Media Ring {}",
                media_type == MediaType::VIDEO ? "Video" : "Audio", a);

            buffers[a] = alloc(mem, size, alloc_name.c_str());
        }
    }

    if (new_frame)
        ring_index++;
    Ptr<uint8_t> buffer = buffers[ring_index % PlayerInfoState::RING_BUFFER_COUNT];
    return buffer;
}

static void run_event_callback(EmuEnvState &emuenv, const ThreadStatePtr &thread, const PlayerPtr &player_info, uint32_t event_id, uint32_t source_id, Ptr<void> event_data) {
    if (player_info->event_manager.event_callback) {
        thread->run_callback(player_info->event_manager.event_callback.address(), { player_info->event_manager.user_data, event_id, source_id, event_data.address() });
    }
}

EXPORT(int32_t, sceAvPlayerAddSource, SceUID player_handle, Ptr<const char> path) {
    const auto state = emuenv.kernel.obj_store.get<AvPlayerState>();
    const PlayerPtr &player_info = lock_and_find(player_handle, state->players, state->mutex);

    if (!player_info) {
        return RET_ERROR(SCE_AVPLAYER_ERROR_INVALID_ARGUMENT);
    }

    const auto thread = emuenv.kernel.get_thread(thread_id);

    auto file_path = expand_path(emuenv.io, path.get(emuenv.mem), emuenv.vita_fs_path);
    if (!fs::exists(file_path) && player_info->file_manager.open_file && player_info->file_manager.close_file && player_info->file_manager.read_file && player_info->file_manager.file_size) {
        fs::create_directories(emuenv.cache_path);

        // Create temp media file
        const auto temp_file_path = emuenv.cache_path / "temp_vita_media.mp4";
        fs::ofstream temp_file(temp_file_path, std::ios::out | std::ios::binary);

        const Address buf = alloc(emuenv.mem, KiB(512), "AvPlayer buffer");
        const auto buf_ptr = Ptr<char>(buf).get(emuenv.mem);
        thread->run_callback(player_info->file_manager.open_file.address(), { player_info->file_manager.user_data, path.address() });
        // TODO: support file_size > 4GB (callback function returns uint64_t, but I dont know how to get high dword of uint64_t)
        const uint32_t file_size = thread->run_callback(player_info->file_manager.file_size.address(), { player_info->file_manager.user_data });
        auto remaining = file_size;
        uint32_t offset = 0;
        while (remaining) {
            const auto buf_size = std::min((uint32_t)KiB(512), remaining);
            // zero in 5 parameter means high dword of uint64_t parameter. see previous todo
            thread->run_callback(player_info->file_manager.read_file.address(), { player_info->file_manager.user_data, buf, offset, 0, buf_size });
            temp_file.write(buf_ptr, buf_size);
            offset += buf_size;
            remaining -= buf_size;
        }
        free(emuenv.mem, buf);
        temp_file.close();
        thread->run_callback(player_info->file_manager.close_file.address(), { player_info->file_manager.user_data });
        if (fs::file_size(temp_file_path) != file_size) {
            LOG_ERROR("File is corrupted or incomplete: {}", temp_file_path);
            return -1;
        }
        file_path = temp_file_path;
    }

    player_info->player.queue(file_path.string());
    run_event_callback(emuenv, thread, player_info, SCE_AVPLAYER_STATE_BUFFERING, 0, Ptr<void>(0)); // may be important for sound
    run_event_callback(emuenv, thread, player_info, SCE_AVPLAYER_STATE_READY, 0, Ptr<void>(0));
    return 0;
}

EXPORT(int, sceAvPlayerClose, SceUID player_handle) {
    const auto state = emuenv.kernel.obj_store.get<AvPlayerState>();
    const PlayerPtr &player_info = lock_and_find(player_handle, state->players, state->mutex);
    const auto thread = emuenv.kernel.get_thread(thread_id);
    run_event_callback(emuenv, thread, player_info, SCE_AVPLAYER_STATE_STOP, 0, Ptr<void>(0));
    std::lock_guard<std::mutex> lock(state->mutex);
    state->players.erase(player_handle);
    return 0;
}

EXPORT(uint64_t, sceAvPlayerCurrentTime, SceUID player_handle) {
    const auto state = emuenv.kernel.obj_store.get<AvPlayerState>();
    const PlayerPtr &player_info = lock_and_find(player_handle, state->players, state->mutex);

    return player_info->player.last_timestamp;
}

EXPORT(int, sceAvPlayerDisableStream) {
    return UNIMPLEMENTED();
}

EXPORT(int32_t, sceAvPlayerStreamCount, SceUID player_handle) {
    STUBBED("ALWAYS RETURN 2 (VIDEO AND AUDIO)");
    return 2;
}

EXPORT(int32_t, sceAvPlayerEnableStream, SceUID player_handle, uint32_t stream_no) {
    if (player_handle == 0) {
        return SCE_AVPLAYER_ERROR_ILLEGAL_ADDR;
    }
    if (stream_no > (uint32_t)(CALL_EXPORT(sceAvPlayerStreamCount, player_handle))) {
        return SCE_AVPLAYER_ERROR_INVALID_ARGUMENT;
    }
    return UNIMPLEMENTED();
}

EXPORT(bool, sceAvPlayerGetAudioData, SceUID player_handle, SceAvPlayerFrameInfo *frame_info) {
    const auto state = emuenv.kernel.obj_store.get<AvPlayerState>();
    const PlayerPtr &player_info = lock_and_find(player_handle, state->players, state->mutex);
    if (!player_info) {
        return false;
    }
    Ptr<uint8_t> buffer;

    if (player_info->paused) {
        if (REJECT_DATA_ON_PAUSE) {
            return false;
        } else {
            // This is probably incorrect and will make weird noises :P
            buffer = get_buffer(player_info, MediaType::AUDIO, emuenv.mem,
                player_info->player.last_sample_count * sizeof(int16_t) * player_info->player.last_channels, true);
        }
    } else {
        std::vector<int16_t> data = player_info->player.receive_audio();

        if (data.empty())
            return false;

        buffer = get_buffer(player_info, MediaType::AUDIO, emuenv.mem, (uint32_t)data.size() * sizeof(int16_t), false);
        std::memcpy(buffer.get(emuenv.mem), data.data(), data.size() * sizeof(int16_t));
    }

    frame_info->timestamp = player_info->player.last_timestamp;
    frame_info->stream_details.audio.channels = player_info->player.last_channels;
    frame_info->stream_details.audio.sample_rate = player_info->player.last_sample_rate;
    frame_info->stream_details.audio.size = player_info->player.last_channels * player_info->player.last_sample_count * sizeof(int16_t);
    frame_info->data = buffer;

    strcpy(frame_info->stream_details.audio.language, "ENG");
    return true;
}

EXPORT(uint32_t, sceAvPlayerGetStreamInfo, SceUID player_handle, SceUInt32 stream_no, SceAvPlayerStreamInfo *stream_info) {
    if (!stream_info) {
        return SCE_AVPLAYER_ERROR_ILLEGAL_ADDR;
    }
    if (player_handle == 0) {
        return SCE_AVPLAYER_ERROR_ILLEGAL_ADDR;
    }
    STUBBED("ALWAYS SUSPECTS 2 STREAMS: VIDEO AND AUDIO");
    const auto state = emuenv.kernel.obj_store.get<AvPlayerState>();
    const PlayerPtr &player_info = lock_and_find(player_handle, state->players, state->mutex);
    if (stream_no == 0) { // suspect always two streams: audio and video //first is video
        DecoderSize size = player_info->player.get_size();
        stream_info->stream_type = MediaType::VIDEO;
        stream_info->stream_details.video.width = size.width;
        stream_info->stream_details.video.height = size.height;
        stream_info->stream_details.video.aspect_ratio = static_cast<float>(size.width) / static_cast<float>(size.height);
        strcpy(stream_info->stream_details.video.language, "ENG");
    } else if (stream_no == 1) { // audio
        player_info->player.receive_audio(); // TODO: Get audio info without skipping data frames
        stream_info->stream_type = MediaType::AUDIO;
        stream_info->stream_details.audio.channels = player_info->player.last_channels;
        stream_info->stream_details.audio.sample_rate = player_info->player.last_sample_rate;
        stream_info->stream_details.audio.size = player_info->player.last_channels * player_info->player.last_sample_count * sizeof(int16_t);
        strcpy(stream_info->stream_details.audio.language, "ENG");
    } else {
        return SCE_AVPLAYER_ERROR_INVALID_ARGUMENT;
    }
    return 0;
}

EXPORT(bool, sceAvPlayerGetVideoData, SceUID player_handle, SceAvPlayerFrameInfo *frame_info) {
    const auto state = emuenv.kernel.obj_store.get<AvPlayerState>();
    const PlayerPtr &player_info = lock_and_find(player_handle, state->players, state->mutex);
    if (!player_info) {
        return false;
    }

    Ptr<uint8_t> buffer;

    DecoderSize size = player_info->player.get_size();

    uint64_t framerate = player_info->player.get_framerate_microseconds();

    // needs new frame
    if (player_info->last_frame_time + framerate < current_time()) {
        if (CATCHUP_VIDEO_PLAYBACK)
            player_info->last_frame_time += framerate;
        else
            player_info->last_frame_time = current_time();

        if (player_info->paused) {
            if (REJECT_DATA_ON_PAUSE)
                return false;
            else
                buffer = get_buffer(player_info, MediaType::VIDEO, emuenv.mem, H264DecoderState::buffer_size(size), false);
        } else {
            buffer = get_buffer(player_info, MediaType::VIDEO, emuenv.mem, H264DecoderState::buffer_size(size), true);

            std::vector<uint8_t> data = player_info->player.receive_video();
            std::memcpy(buffer.get(emuenv.mem), data.data(), data.size());
        }
    } else {
        buffer = get_buffer(player_info, MediaType::VIDEO, emuenv.mem, H264DecoderState::buffer_size(size), false);
    }
    // TODO: catch eof error and call
    // uint32_t buf = SCE_AVPLAYER_ERROR_MAYBE_EOF;
    // run_event_callback(emuenv, thread_id, player_info, SCE_AVPLAYER_STATE_ERROR, 0, &buf);

    frame_info->timestamp = player_info->player.last_timestamp;
    frame_info->stream_details.video.width = size.width;
    frame_info->stream_details.video.height = size.height;
    frame_info->stream_details.video.aspect_ratio = static_cast<float>(size.width) / static_cast<float>(size.height);
    strcpy(frame_info->stream_details.video.language, "ENG");
    frame_info->data = buffer;
    return true;
}

EXPORT(bool, sceAvPlayerGetVideoDataEx, SceUID player_handle, SceAvPlayerFrameInfo *frame_info) {
    STUBBED("Use GetVideoData");
    return CALL_EXPORT(sceAvPlayerGetVideoData, player_handle, frame_info);
}

EXPORT(SceUID, sceAvPlayerInit, SceAvPlayerInfo *info) {
    emuenv.kernel.obj_store.create<AvPlayerState>();
    const auto state = emuenv.kernel.obj_store.get<AvPlayerState>();
    SceUID player_handle = emuenv.kernel.get_next_uid();
    PlayerPtr player = std::make_shared<PlayerInfoState>();
    state->players[player_handle] = player;

    player->last_frame_time = current_time();
    player->memory_allocator = info->memory_allocator;
    player->file_manager = info->file_manager;
    player->event_manager = info->event_manager;

    // Result is defined as a void *, but I just call it SceUID because it is easier to deal with. Same size.
    return player_handle;
}

EXPORT(bool, sceAvPlayerIsActive, SceUID player_handle) {
    if (player_handle == 0) {
        return false;
    }

    const auto state = emuenv.kernel.obj_store.get<AvPlayerState>();
    const PlayerPtr &player_info = lock_and_find(player_handle, state->players, state->mutex);

    return !player_info->player.video_playing.empty();
}

EXPORT(int, sceAvPlayerJumpToTime) {
    return UNIMPLEMENTED();
}

EXPORT(int, sceAvPlayerPause, SceUID player_handle) {
    const auto state = emuenv.kernel.obj_store.get<AvPlayerState>();
    const PlayerPtr &player_info = lock_and_find(player_handle, state->players, state->mutex);
    player_info->paused = true;
    const auto thread = emuenv.kernel.get_thread(thread_id);
    run_event_callback(emuenv, thread, player_info, SCE_AVPLAYER_STATE_PAUSE, 0, Ptr<void>(0));
    return 0;
}

EXPORT(int, sceAvPlayerPostInit) {
    return UNIMPLEMENTED();
}

EXPORT(int, sceAvPlayerResume, SceUID player_handle) {
    const auto state = emuenv.kernel.obj_store.get<AvPlayerState>();
    const PlayerPtr &player_info = lock_and_find(player_handle, state->players, state->mutex);
    if (!player_info->paused) {
        const auto thread = emuenv.kernel.get_thread(thread_id);
        run_event_callback(emuenv, thread, player_info, SCE_AVPLAYER_STATE_PLAY, 0, Ptr<void>(0));
    }
    player_info->paused = false;
    return 0;
}

EXPORT(int, sceAvPlayerSetLooping, SceUID player_handle, bool do_loop) {
    const auto state = emuenv.kernel.obj_store.get<AvPlayerState>();
    const PlayerPtr &player_info = lock_and_find(player_handle, state->players, state->mutex);
    player_info->do_loop = do_loop;

    return STUBBED("LOOPING NOT IMPLEMENTED");
}

EXPORT(int, sceAvPlayerSetTrickSpeed) {
    return UNIMPLEMENTED();
}

EXPORT(int, sceAvPlayerStart, SceUID player_handle) {
    const auto state = emuenv.kernel.obj_store.get<AvPlayerState>();
    const PlayerPtr &player_info = lock_and_find(player_handle, state->players, state->mutex);
    if (!player_info->player.videos_queue.empty()) {
        player_info->player.pop_video();
    }
    const auto thread = emuenv.kernel.get_thread(thread_id);
    run_event_callback(emuenv, thread, player_info, SCE_AVPLAYER_STATE_PLAY, 0, Ptr<void>(0));
    return 0;
}

EXPORT(int, sceAvPlayerStop, SceUID player_handle) {
    const auto state = emuenv.kernel.obj_store.get<AvPlayerState>();
    const PlayerPtr &player_info = lock_and_find(player_handle, state->players, state->mutex);
    player_info->player.free_video();
    const auto thread = emuenv.kernel.get_thread(thread_id);
    run_event_callback(emuenv, thread, player_info, SCE_AVPLAYER_STATE_STOP, 0, Ptr<void>(0));
    return 0;
}
