#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3/SDL_init.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <SDL3_mixer/SDL_mixer.h>
#include <SDL3_image/SDL_image.h>
#include <cmath>
#include <string_view>
#include <filesystem>

#include "adventura.cpp"
#include "events.hpp"

using std::pair;

constexpr uint32_t windowStartWidth = 1200;
constexpr uint32_t windowStartHeight = 800;

void loadWorld(void* appstate, string worldFile);
void resetWorld(void* appstate);
void loadNextWorld(void* appstate);
void playSound(void* appstate, string soundFile);

struct AppContext {
    SDL_Window* window;
    SDL_Renderer* renderer;
    SDL_Texture* messageTex, *imageTex;
    SDL_FRect messageDest;
    SDL_AudioDeviceID audioDevice;
    Mix_Music* music;
    SDL_AppResult app_quit = SDL_APP_CONTINUE;
    Player* player;
    World* world;
    TTF_Font* font;
    std::filesystem::path basePath;
};

SDL_AppResult SDL_Fail(){
    SDL_LogError(SDL_LOG_CATEGORY_CUSTOM, "Error %s", SDL_GetError());
    return SDL_APP_FAILURE;
}

SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) {
    // init the library, here we make a window so we only need the Video capabilities.
    if (not SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)){
        return SDL_Fail();
    }
    registerEvents();
    
    // init TTF
    if (not TTF_Init()) {
        return SDL_Fail();
    }
    
    // create a window
   
    SDL_Window* window = SDL_CreateWindow("Adventura", windowStartWidth, windowStartHeight, SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY);
    if (not window){
        return SDL_Fail();
    }
    
    // create a renderer
    SDL_Renderer* renderer = SDL_CreateRenderer(window, NULL);
    if (not renderer){
        return SDL_Fail();
    }
    
    // load the font
#if __ANDROID__
    std::filesystem::path basePath = "";   // on Android we do not want to use basepath. Instead, assets are available at the root directory.
#else
    auto basePathPtr = SDL_GetBasePath();
     if (not basePathPtr){
        return SDL_Fail();
    }
     const std::filesystem::path basePath = basePathPtr;
#endif

    const auto fontPath = basePath / "assets/MartianMono-VariableFont.ttf";
    TTF_Font* font = TTF_OpenFont(fontPath.string().c_str(), 36);
    if (not font) {
        return SDL_Fail();
    }

    // render the font to a surface
    const std::string_view text = "Hello SDL!";
    SDL_Surface* surfaceMessage = TTF_RenderText_Solid(font, text.data(), text.length(), { 255,255,255 });

    // make a texture from the surface
    SDL_Texture* messageTex = SDL_CreateTextureFromSurface(renderer, surfaceMessage);

    // we no longer need the font or the surface, so we can destroy those now.
    //TTF_CloseFont(font);
    SDL_DestroySurface(surfaceMessage);

    // load the SVG
    auto svg_surface = IMG_Load((basePath / "assets/gs_tiger.svg").string().c_str());
    SDL_Texture* tex = SDL_CreateTextureFromSurface(renderer, svg_surface);
    SDL_DestroySurface(svg_surface);
    

    // get the on-screen dimensions of the text. this is necessary for rendering it
    auto messageTexProps = SDL_GetTextureProperties(messageTex);
    SDL_FRect text_rect{
            .x = 0,
            .y = 0,
            .w = float(SDL_GetNumberProperty(messageTexProps, SDL_PROP_TEXTURE_WIDTH_NUMBER, 0)),
            .h = float(SDL_GetNumberProperty(messageTexProps, SDL_PROP_TEXTURE_HEIGHT_NUMBER, 0))
    };

    // init SDL Mixer
    auto audioDevice = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, NULL);
    if (not audioDevice) {
        return SDL_Fail();
    }
    if (not Mix_OpenAudio(audioDevice, NULL)) {
        return SDL_Fail();
    }

    // load the music
    auto musicPath = basePath / "assets/sunset_on_the_beach.ogg";
    auto music = Mix_LoadMUS(musicPath.string().c_str());
    if (not music) {
        return SDL_Fail();
    }

    // play the music (does not loop)
    Mix_PlayMusic(music, 99999);
    
    // print some information about the window
    SDL_ShowWindow(window);
    {
        int width, height, bbwidth, bbheight;
        SDL_GetWindowSize(window, &width, &height);
        SDL_GetWindowSizeInPixels(window, &bbwidth, &bbheight);
        SDL_Log("Window size: %ix%i", width, height);
        SDL_Log("Backbuffer size: %ix%i", bbwidth, bbheight);
        if (width != bbwidth){
            SDL_Log("This is a highdpi environment.");
        }
    }
    BlockRegistry blockRegistry = BlockRegistry();
    World *world = new World(blockRegistry);
    Player *player = new Player(world->getStartPos(), *world);

    // set up the application data
    *appstate = new AppContext{
       .window = window,
       .renderer = renderer,
       .messageTex = messageTex,
       .imageTex = tex,
       .messageDest = text_rect,
       .audioDevice = audioDevice,
       .music = music,
       .player = player,
       .world = world,
       .font = font,
       .basePath = basePath
    };
    loadNextWorld(*appstate);
    
    SDL_SetRenderVSync(renderer, -1);   // enable vysnc
    
    SDL_Log("Application started successfully!");

    //start(argc, argv);

    return SDL_APP_CONTINUE;
}

SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event* event) {
    auto* app = (AppContext*)appstate;

    if (event->type == SDL_EVENT_KEY_DOWN) {
        if (event->key.scancode == SDL_SCANCODE_W) {
            onInput('w', *(app->world), *(app->player));
        } else if (event->key.scancode == SDL_SCANCODE_A) {
            onInput('a', *(app->world), *(app->player));
        } else if (event->key.scancode == SDL_SCANCODE_S) {
            onInput('s', *(app->world), *(app->player));
        } else if (event->key.scancode == SDL_SCANCODE_D) {
            onInput('d', *(app->world), *(app->player));
        }
    }

    if ((*app->player).hasReachedGoal()) {
        playSound(appstate, "assets/success.ogg");
        loadNextWorld(appstate);
    }
    if (event->type == playerDeathEvent) {
        playSound(appstate, "assets/failure.ogg");
        resetWorld(appstate);
    }
    
    if (event->type == SDL_EVENT_QUIT) {
        app->app_quit = SDL_APP_SUCCESS;
    }

    return SDL_APP_CONTINUE;
}
vector<pair<vector<char>, SDL_Texture*>> textureCache;

SDL_AppResult SDL_AppIterate(void *appstate) {
    auto* app = (AppContext*)appstate;

    // draw a color
    auto time = SDL_GetTicks() / 1000.f;
    auto red = (std::sin(time) + 1) / 2.0 * 255;
    auto green = (std::sin(time / 2) + 1) / 2.0 * 255;
    auto blue = (std::sin(time) * 2 + 1) / 2.0 * 255;
    SDL_SetRenderDrawBlendMode(app->renderer, SDL_BLENDMODE_ADD);
    SDL_SetRenderDrawColor(app->renderer, red, green, blue, 1);
    SDL_SetRenderDrawColor(app->renderer, 0, 0, 0, 20);

    SDL_RenderClear(app->renderer);

    // Renderer uses the painter's algorithm to make the text appear above the image, we must render the image first.
    //SDL_RenderTexture(app->renderer, app->imageTex, NULL, NULL);
    vector<vector<char>> worldState = render(*(app->world), (*app->player).mapToWorldspace());
    for (int i = 0; i < worldState.size(); i++) {
        vector<char> line = worldState.at(i);
        SDL_Texture* messageTex;
        if (i < textureCache.size() && textureCache.at(i).first == line) {
            messageTex = textureCache.at(i).second;
        } 
        else {
            if (i < textureCache.size()) {
                SDL_DestroyTexture(textureCache.at(i).second);
            }
            SDL_Surface* surfaceMessage = TTF_RenderText_Solid(app->font, line.data(), line.size(), { 255,255,255 });

            // make a texture from the surface
            messageTex = SDL_CreateTextureFromSurface(app->renderer, surfaceMessage);
            
            SDL_DestroySurface(surfaceMessage);
            while (textureCache.size() <= i) {
                textureCache.push_back({{}, nullptr});
            }
            textureCache.at(i) = {line, messageTex};
        }
        auto messageTexProps = SDL_GetTextureProperties(messageTex);
        SDL_FRect text_rect{
            .x = 0,
            .y = float(i * SDL_GetNumberProperty(messageTexProps, SDL_PROP_TEXTURE_HEIGHT_NUMBER, 0)*2),
            .w = float(SDL_GetNumberProperty(messageTexProps, SDL_PROP_TEXTURE_WIDTH_NUMBER, 0))*2,
            .h = float(SDL_GetNumberProperty(messageTexProps, SDL_PROP_TEXTURE_HEIGHT_NUMBER, 0))*2
        };
        
        SDL_RenderTexture(app->renderer, messageTex, NULL, &text_rect);
    }
    //SDL_RenderTexture(app->renderer, app->messageTex, NULL, &app->messageDest);

    SDL_RenderPresent(app->renderer);

    return app->app_quit;
}

void SDL_AppQuit(void* appstate, SDL_AppResult result) {
    auto* app = (AppContext*)appstate;
    if (app) {
        SDL_DestroyRenderer(app->renderer);
        SDL_DestroyWindow(app->window);

        Mix_FadeOutMusic(1000);  // prevent the music from abruptly ending.
        Mix_FreeMusic(app->music); // this call blocks until the music has finished fading
        Mix_CloseAudio();
        SDL_CloseAudioDevice(app->audioDevice);

        delete app;
    }
    TTF_Quit();
    Mix_Quit();

    SDL_Log("Application quit successfully!");
    SDL_Quit();
}

string currentWorld = "";

void loadWorld(void* appstate, string worldFile) {
    auto* app = (AppContext*)appstate;
    
    (*app->world).loadFromFile(worldFile);
    (app -> player) = new Player((*app->world).getStartPos(), *app->world);
    currentWorld = worldFile;
}

void resetWorld(void* appstate) {
    auto* app = (AppContext*)appstate;
    
    loadWorld(appstate, currentWorld);
}
void loadNextWorld(void* appstate) {
    auto* app = (AppContext*)appstate;
    
    vector<string> worldFiles = getOrderedFileNames("./worlds/", ".txt");
    if (currentWorld == "") {
        loadWorld(app, worldFiles.at(0));
        return;
    }

    int index = std::find(worldFiles.begin(), worldFiles.end(), currentWorld) - worldFiles.begin();
    if (index + 1 < worldFiles.size()) {
        loadWorld(appstate, worldFiles.at(index + 1));
    }
    else {
        resetWorld(appstate);
        // You won
    }
}
void playSound(void* appstate, string soundFile) {
    auto* app = (AppContext*)appstate;
    // load the sound
    auto soundPath = app->basePath / soundFile;
    auto sound = Mix_LoadWAV(soundPath.string().c_str());
    if (not sound) {
        return;
    }
    Mix_PlayChannel(-1, sound, 0);
}