#include <engine.h>
#include <SparrowInput/input.h>
#include <SparrowRenderer/scene.h>
#include <SparrowRenderer/mesh.h>
#include <SparrowRenderer/image.h>
#include <SparrowRenderer/deferredpipeline.h>
#include <SparrowRenderer/pbrmaterial.h>
#include <resourcemanager.h>
#include <SparrowRenderer/sparrowrenderer.h>
#include <SparrowRenderer/texture.h>
#include <SparrowRenderer/chunk.h>
#include "defaultkeysmap.h"

#include <scene/scenetree.h>
#include <scene/textnode.h>
#include <scene/playercharacternode.h>
#include <scene/scenenode.h>
#include <tools/graph.h>
#include <tools/pathfinder.h>
#include <tools/loader.h>

#include <tools/font.h>
#include <tools/utils.h>

#include <sparrowshell/sparrowshell.h>
#include <btBulletDynamicsCommon.h>

#include <glm/ext.hpp>

#include "scene/gui/buttonnode.h"
#include "scene/gui/buttonshape.h"

#include "scene/gui/backgroundnode.h"

//#include "potator.h"
#include "SparrowSerializer/serializationmanager.h"
#include "SparrowSerializer/serializable.h"
#include <cereal/archives/json.hpp>
#include <fstream>

#include "resourcemanager.h"

#include "scene/gui/callback.h"

class TestGen : public TerrainGenerator
{
    float map[64*64];
public:
    TestGen()
    {
        for(int i=0; i<64*64; ++i)
            map[i] = (rand()%64)/64.f;
    }

    float getHeight(float x, float z, float zoom)
    {
        x /= zoom;
        z /= zoom;
        int i1 = int(floor(x)+64)%64;
        int j1 = int(floor(z)+64)%64;
        int i2 = (i1+1)%64;
        int j2 = (j1+1)%64;
        float v1 = map[i1*64+j1];
        float v2 = map[i2*64+j1];
        float v3 = map[i1*64+j2];
        float v4 = map[i2*64+j2];
        float ph = x - floor(x);
        float pv = z - floor(z);
        ph = 0.5f-cos(3.1416*ph)/2;
        pv = 0.5f-cos(3.1416*pv)/2;
        float v5 = v1*(1-ph) + v2*ph;
        float v6 = v3*(1-ph) + v4*ph;
        return (v5*(1-pv) + v6*pv)*zoom;
    }

    virtual float func(float x, float y, float z)
    {
        float height = getHeight(x+6.7, z+15.7, 2.5f);
        height += getHeight(x+5.76, z+2.14, 5.4f);
        height += getHeight(x, z, 10.f);
        return y+8.f - height;
    }
};

void generateTerrain(SceneTree *scene)
{
    SceneNode* terrainContainer = new SceneNode();
    TestGen gen;
    PBRMaterial *mat = new PBRMaterial();
    mat->albedo = glm::vec3(0.1f, 0.4f, 0.2f);
    mat->metallic = 0.2f;
    mat->roughness = 0.95f;

    for(int x=-3; x<3; ++x)
    for(int y=-2; y<2; ++y)
    for(int z=-3; z<3; ++z)
    {
        Chunk *chunk = new Chunk(&gen); // ! WARNING ! : chunk pointer is lost and never deleted
        glm::vec3 pos(x, y, z);
        chunk->generate(pos);
        if(chunk->mesh->m_positions3D.empty())
            delete chunk;
        else
        {
            chunk->mesh->setMaterial(mat);
            chunk->mesh->initGL();
            SceneNode *node = new SceneNode();
            node->setMesh(chunk->mesh);
            node->getGeometryNode()->modelMatrix = glm::translate(glm::scale(glm::mat4(), glm::vec3(2.f)), pos*8.f);
            node->setTransform(node->getGeometryNode()->modelMatrix);
            btRigidBody* body = utils::buildStaticCollider(node->getGeometryNode());
            node->setRigidBody(body);
            scene->getPhysics()->addRigidBody(body);
            terrainContainer->addChild(node);
        }
    }
    scene->getRootObject()->addChild(terrainContainer);
}

void generateSponza(SceneTree *scene, btDiscreteDynamicsWorld *world)
{
    Loader::setTexDirectory("data/sponza/");
    SceneNode* sponzaContainer = Loader::loadMesh("sponza.obj");
    Loader::setTexDirectory("data/");
    if(sponzaContainer != nullptr && sponzaContainer->getChildren().size() > 0)
    {
        const std::vector<SceneNode*> children = sponzaContainer->getChildren();
        for(SceneNode* node : children)
        {
            if(node->getMesh()->getName().find("fabric") != std::string::npos)
            {
                // moyen crade de se débarasser des rideaux
                sponzaContainer->removeChild(node);
                node->destroyWhenOrphan();
            }
            else
            {
                // scale sponza before generating the static collider
                glm::mat4 scaleMatrix = glm::scale(glm::mat4(), glm::vec3(0.01f));
                node->getGeometryNode()->modelMatrix = scaleMatrix;
                node->setTransform(scaleMatrix);
                btRigidBody* body = utils::buildStaticCollider(node->getGeometryNode());
                node->setRigidBody(body);
                world->addRigidBody(body);
            }
        }
        scene->getRootObject()->addChild(sponzaContainer);
    }
    else
    {
        fprintf(stderr, "failed to load sponza");
        if(sponzaContainer)
            delete sponzaContainer;
    }
}

struct Config
{
    std::string mode; // fullscreen / windowed / borderless
    std::string scene; // terrain / sponza / none
    bool vsync;
    int width;
    int height;

    Config()
    {
        mode = "windowed";
        scene = "sandbox";
        vsync = false;
        width = 800;
        height = 600;
    }

    void save()
    {
        std::fstream configFile;
        configFile.open("config.ini", std::ios_base::out);
        cereal::JSONOutputArchive output(configFile);
        output(cereal::make_nvp("mode", mode));
        output(cereal::make_nvp("scene", scene));
        output(cereal::make_nvp("vsync", vsync));
        output(cereal::make_nvp("width", width));
        output(cereal::make_nvp("height", height));
        output.finishNode();
        configFile << std::endl;
        configFile.close();
    }

    void load()
    {
        std::fstream configFile;
        configFile.open("config.ini", std::ios_base::in);
        if(configFile.is_open())
        {
            try
            {
                cereal::JSONInputArchive input(configFile);
                input(cereal::make_nvp("mode", mode));
                input(cereal::make_nvp("scene", scene));
                input(cereal::make_nvp("vsync", vsync));
                input(cereal::make_nvp("width", width));
                input(cereal::make_nvp("height", height));
            }
            catch(cereal::Exception e)
            {
                std::cerr << "Config file contains errors : " << e.what() << std::endl;
            }
            configFile.close();
        }
        save();
    }
};

class Demo {
    std::string m_demo_scene;
    Engine* m_engine;
    Config* m_config;
    PlayerCharacterNode* m_player;
public:
    Demo(Engine* engine,Config* config):m_demo_scene("demo"),m_engine(engine),m_config(config){
        m_engine->createScene(m_demo_scene);
    }

    void initScene(){
        //player
        m_player = new PlayerCharacterNode(false);
        m_player->setInputs(DefaultKeysMap::MOVE_FORWARD, DefaultKeysMap::MOVE_BACKWARD, DefaultKeysMap::STRAFE_LEFT, DefaultKeysMap::STRAFE_RIGHT, DefaultKeysMap::JUMP, DefaultKeysMap::RUN, DefaultKeysMap::TOGGLE_NOCLIP);
        SceneTree* scene = RESOURCE_GET(SceneTree, m_demo_scene);
        scene->getRootObject()->addChild(m_player);
        scene->setMainCamera(m_player->getCamera());

        //potator
        //Potator *potator = new Potator(m_player, DefaultKeysMap::MAIN_ACTION, DefaultKeysMap::SECONDARY_ACTION, DefaultKeysMap::TERTIARY_ACTION);
        //scene->getRootObject()->addChild(potator);

        //lighting
        Texture* skyboxTexture = RESOURCE_GET(Texture, "radiance");
        Texture* ambientTexture = RESOURCE_GET(Texture, "irradiance");
        SceneNode *ambientLight = new SceneNode();
        ambientLight->setLight(new AmbientLight(ambientTexture, skyboxTexture));

        DeferredPipeline* pipeline = dynamic_cast<DeferredPipeline*>(scene->getPipeline());
        pipeline->setSkybox(RESOURCE_GET(Texture, "skybox"));

        DirectionnalLight* sun = new DirectionnalLight(glm::vec3(5, 8, -2), glm::vec3(4.f));
        SceneNode *sunLight = new SceneNode();
        sunLight->setLight(sun);

        scene->getRootObject()->addChild(ambientLight);

        scene->initPhysics();

        if(m_config->scene == "sponza")
        {
            sun->initShadowMap(4096);
            generateSponza(scene, m_engine->getScene()->getPhysics());
            SceneNode* light_1 = new SceneNode();
            light_1->setLight(new PointLight(glm::vec3(-3.5, 2, 1.8), 15, glm::vec3(0.35f)));
            scene->getRootObject()->addChild(light_1);
            SceneNode* light_2 = new SceneNode();
            light_2->setLight(new PointLight(glm::vec3(-5, 6, 2), 15, glm::vec3(0.35f)));
            scene->getRootObject()->addChild(light_2);
            m_player->setPosition(0.f, 2.f, 0.f);
            sun->setShadowView(glm::vec3(30, 30, 50));
        }
        else if(m_config->scene == "terrain")
        {
            sun->initShadowMap(4096);
            generateTerrain(scene);
            m_player->setPosition(0.f, 15.f, 0.f);
            sun->setShadowView(glm::vec3(130, 130, 70));
        }
        else if(m_config->scene == "sandbox")
        {
            // load terrain

            Loader::setTexDirectory("data/");
            SceneNode* terrain = utils::createTerrain("HeightMap.png", "ColorMap.png", 100);
            terrain->translate(glm::vec3(353.96, -71.51, -60.79));
            //sandbox->scale(glm::vec3(0.5));
            terrain->getGeometryNode()->modelMatrix = terrain->getTransform();
            btRigidBody* terrainBody = utils::buildStaticCollider(terrain->getGeometryNode());
            terrain->setRigidBody(terrainBody);
            scene->getPhysics()->addRigidBody(terrainBody);
            scene->getRootObject()->addChild(terrain);

            // load sandbox

            SceneNode* sandbox = Loader::loadMesh("sandbox.obj");
            btRigidBody* sandboxBody = utils::buildStaticCollider(sandbox->getGeometryNode());
            sandbox->setRigidBody(sandboxBody);
            scene->getPhysics()->addRigidBody(sandboxBody);
            scene->getRootObject()->addChild(sandbox);

            // adding tree
            SceneNode* tree = Loader::loadMesh("Tree01.obj");
            for(int i=0; i<8; ++i)
            {
                tree->moveTo(glm::vec3(-(10 + rand()%20), 0, rand()%40 - 5));
                sandbox->addChild(tree->clone());
            }
            delete tree;

            // init player and shadows

            sun->initShadowMap(4096);
            m_player->setPosition(0.f, 1.4f, 0.f);
            sun->setShadowView(glm::vec3(80));
        }

        scene->getRootObject()->addChild(sunLight);
    }

    std::string getScene(){return m_demo_scene;}
    PlayerCharacterNode* getPlayer(){return m_player;}
};

class ButtonDemoCallBack : public CallBack
{
    Engine* m_engine;
    Demo* m_demo;
public:
    ButtonDemoCallBack(Engine* engine, Demo* demo):m_engine(engine),m_demo(demo){}

    void exec(){
        m_demo->initScene();
        m_engine->setScene(m_demo->getScene());
        m_engine->getInput()->setCurrentContext("default");
        m_engine->getScene()->getPhysics()->addRigidBody(m_demo->getPlayer()->getRigidbody());
        m_engine->toggleMouseVisibility();
    }
};

class Menu {
    int m_left_click_action;
    ButtonNode* m_button_demo;
    std::string m_menu_scene;
public:
    Menu(Engine* engine,Config* config):m_menu_scene("menu"){
        engine->createScene(m_menu_scene);
        SceneTree* scene = RESOURCE_GET(SceneTree,m_menu_scene);
        m_button_demo = new ButtonNode(new RectangleButtonShape(glm::vec2(300,100)));
        m_button_demo->getLabel()->setText("Start DEMO");
        m_button_demo->getLabel()->setColor(glm::vec3(1.,1.,1.));
        m_button_demo->getBackGround()->setColor(glm::vec3(0.88,0.05,0.05));
        scene->getRootObject()->addChild(m_button_demo);
        sf::Vector2u size = engine->getWindow()->getSize();
        glm::vec2 pos = glm::vec2(size.x,size.y)/glm::vec2(2,2) - m_button_demo->getDimension()/glm::vec2(2,2);
        m_button_demo->setPosition(pos);
        m_button_demo->setVisible(true);
        scene->initPhysics();
    }

    void setLeftClickAction(int action){
        m_left_click_action = action;
        m_button_demo->setAction(m_left_click_action);
    }

    void setButtonCallBack(ButtonDemoCallBack* button_callback){
        m_button_demo->setCallBack(button_callback);
    }

    std::string getScene(){return m_menu_scene;}
};

int main(){
    Config* config = new Config();
    config->load();
    Engine engine;

    Loader::setObjDirectory("data/");
    Loader::setMtlDirectory("data/");
    Loader::setTexDirectory("data/");

    // this creates the opengl context
    // the opengl context must exist before any opengl class is used (texture, pipeline, etc..)
    engine.createWindow("Sparrow Engine Demo", config->width, config->height, config->mode);
    engine.getWindow()->setVerticalSyncEnabled(config->vsync);

    // opengl context is created, we can initialize the global textures used by the rendering pipeline
    Image* img = Loader::loadImage("ibl_brdf_lut.png", 24, true);
    AmbientLight::setBrdfLut(new Texture(img));
    delete img;
    Image * imgs[6];
    imgs[0] = Loader::loadImage("skybox/posx.jpg", 24, false); // right
    imgs[1] = Loader::loadImage("skybox/negx.jpg", 24, false); // left
    imgs[2] = Loader::loadImage("skybox/posy.jpg", 24, false); // top
    imgs[3] = Loader::loadImage("skybox/negy.jpg", 24, false); // bottom
    imgs[4] = Loader::loadImage("skybox/posz.jpg", 24, false); // front
    imgs[5] = Loader::loadImage("skybox/negz.jpg", 24, false); // back
    RESOURCE_ADD(new Texture(imgs), Texture, "skybox");
    for(int i=0; i<6; ++i)
        delete imgs[i];

    imgs[0] = Loader::loadImage("skybox/irradiance/output_iem_posx.tga", 24, false); // right
    imgs[1] = Loader::loadImage("skybox/irradiance/output_iem_negx.tga", 24, false); // left
    imgs[2] = Loader::loadImage("skybox/irradiance/output_iem_posy.tga", 24, false); // top
    imgs[3] = Loader::loadImage("skybox/irradiance/output_iem_negy.tga", 24, false); // bottom
    imgs[4] = Loader::loadImage("skybox/irradiance/output_iem_posz.tga", 24, false); // front
    imgs[5] = Loader::loadImage("skybox/irradiance/output_iem_negz.tga", 24, false); // back
    RESOURCE_ADD(new Texture(imgs, false), Texture, "irradiance");
    for(int i=0; i<6; ++i)
        delete imgs[i];

    imgs[0] = Loader::loadImage("skybox/radiance/output_pmrem_posx_0_256x256.tga", 24, false); // right
    imgs[1] = Loader::loadImage("skybox/radiance/output_pmrem_negx_0_256x256.tga", 24, false); // left
    imgs[2] = Loader::loadImage("skybox/radiance/output_pmrem_posy_0_256x256.tga", 24, false); // top
    imgs[3] = Loader::loadImage("skybox/radiance/output_pmrem_negy_0_256x256.tga", 24, false); // bottom
    imgs[4] = Loader::loadImage("skybox/radiance/output_pmrem_posz_0_256x256.tga", 24, false); // front
    imgs[5] = Loader::loadImage("skybox/radiance/output_pmrem_negz_0_256x256.tga", 24, false); // back
    RESOURCE_ADD(new Texture(imgs), Texture, "radiance");
    for(int i=0; i<6; ++i)
        delete imgs[i];

//    engine.toggleMouseVisibility();

    // setting up SparrowEngine
//    engine.initPhysics();
 //   SceneTree *scene = engine.createScene();
  //  engine.setScene(scene);

    // setting up SparrowInput
    Input* input = engine.getInput();
    input->setKeysMap(DefaultKeysMap());
    input->addContext(Context("default", DefaultKeysMap::getDefaultContext()));
    input->updateKeyBindings();
    engine.setTogglePhysicsDebugAction(DefaultKeysMap::TOGGLE_PHYSICS_DEBUG);
    engine.setToggleShellAction(DefaultKeysMap::TOGGLE_CONSOLE);
    engine.setExitGameAction(DefaultKeysMap::EXIT_GAME);
    engine.setShowMouseAction(DefaultKeysMap::TOGGLE_MOUSE_CURSOR);

    // setting up SparrowShell
    SparrowShell* shell = engine.getShell();
    shell->setInputs(DefaultKeysMap::MOVE_CURSOR_LEFT,DefaultKeysMap::MOVE_CURSOR_RIGHT,DefaultKeysMap::HISTORY_UP,DefaultKeysMap::HISTORY_DOWN);
    input->addContext(Context("shell",DefaultKeysMap::getShellContext()));
    input->updateKeyBindings();

    // setup editor
    input->addContext(Context("editor",DefaultKeysMap::getObjectEditorContext()));

    // setup menu
    Menu* menu = new Menu(&engine,config);
    Demo* demo = new Demo(&engine,config);
    menu->setLeftClickAction(DefaultKeysMap::LEFT_CLICK);
    input->addContext(Context("menu",DefaultKeysMap::getMenuContext()));
    input->setCurrentContext("menu");
    input->updateKeyBindings();

    menu->setButtonCallBack(new ButtonDemoCallBack(&engine,demo));

    engine.setScene(menu->getScene());
    engine.getInput()->setMouseGrabbed(false);

    engine.start();
}
