////////////////////////////////////////////////////////////
// Headers
////////////////////////////////////////////////////////////
#include <SFML/Graphics.hpp>
#include <map>
#include <math.h>


void DisplayError();

////////////////////////////////////////////////////////////
/// A class to simplify shader selection
///
////////////////////////////////////////////////////////////
class ShaderSelector
{
public :

    // Constructor
    ShaderSelector(std::map<std::string, sf::Shader>& owner, const std::string& shader) :
    myOwner   (&owner),
    myIterator(owner.find(shader))
    {
    }

    // Select the previous shader
    void GotoPrevious()
    {
        if (myIterator == myOwner->begin())
            myIterator = myOwner->end();
        myIterator--;
    }

    // Select the next shader
    void GotoNext()
    {
        myIterator++;
        if (myIterator == myOwner->end())
            myIterator = myOwner->begin();
    }

    // Update the shader parameters
    void Update(float x, float y)
    {
        if      (myIterator->first == "blur")     myIterator->second.SetParameter("offset", x * y * 0.03f);
        else if (myIterator->first == "colorize") myIterator->second.SetParameter("color", 0.3f, x, y);
        else if (myIterator->first == "edge")     myIterator->second.SetParameter("threshold", x * y);
        else if (myIterator->first == "fisheye")  myIterator->second.SetParameter("mouse", x, y);
        else if (myIterator->first == "wave")     myIterator->second.SetParameter("offset", x, y);
        else if (myIterator->first == "pixelate") myIterator->second.SetParameter("mouse", x, y);
    }

    // Get the name of the current shader
    const std::string& GetName() const
    {
        return myIterator->first;
    }

    // Get the current shader
    const sf::Shader& GetShader() const
    {
        return myIterator->second;
    }

private :

    std::map<std::string, sf::Shader>* myOwner;
    std::map<std::string, sf::Shader>::iterator myIterator;
};


////////////////////////////////////////////////////////////
/// Entry point of application
///
/// \return Application exit code
///
////////////////////////////////////////////////////////////
int main()
{
    // Check that the system can use shaders
    if (sf::Shader::IsAvailable() == false)
    {
        DisplayError();
        return EXIT_SUCCESS;
    }

    // Create the main window
    sf::RenderWindow window(sf::VideoMode(800, 600), "SFML Shader");

    // Create the render image
    sf::RenderImage image;
    if (!image.Create(window.GetWidth(), window.GetHeight()))
        return EXIT_FAILURE;

    // Load a background image to display
    sf::Image backgroundImage;
    if (!backgroundImage.LoadFromFile("datas/shader/background.jpg"))
        return EXIT_FAILURE;
    sf::Sprite background(backgroundImage);
    backgroundImage.SetSmooth(false);

    // Load a sprite which we'll move into the scene
    sf::Image entityImage;
    if (!entityImage.LoadFromFile("datas/shader/sprite.png"))
        return EXIT_FAILURE;
    sf::Sprite entity(entityImage);

    // Load the text font
    sf::Font font;
    if (!font.LoadFromFile("datas/shader/arial.ttf", 20))
        return EXIT_FAILURE;

    // Load the image needed for the wave shader
    sf::Image waveImage;
    if (!waveImage.LoadFromFile("datas/shader/wave.jpg"))
        return EXIT_FAILURE;

    // Load all shaders
    std::map<std::string, sf::Shader> shaders;
    if (!shaders["nothing"].LoadFromFile("datas/shader/nothing.sfx"))   return EXIT_FAILURE;
    if (!shaders["blur"].LoadFromFile("datas/shader/blur.sfx"))         return EXIT_FAILURE;
    if (!shaders["colorize"].LoadFromFile("datas/shader/colorize.sfx")) return EXIT_FAILURE;
    if (!shaders["edge"].LoadFromFile("datas/shader/edge.sfx"))         return EXIT_FAILURE;
    if (!shaders["fisheye"].LoadFromFile("datas/shader/fisheye.sfx"))   return EXIT_FAILURE;
    if (!shaders["wave"].LoadFromFile("datas/shader/wave.sfx"))         return EXIT_FAILURE;
    if (!shaders["pixelate"].LoadFromFile("datas/shader/pixelate.sfx")) return EXIT_FAILURE;
    ShaderSelector backgroundShader(shaders, "nothing");
    ShaderSelector entityShader(shaders, "nothing");
    ShaderSelector globalShader(shaders, "nothing");

    // Do specific initializations
    shaders["nothing"].SetTexture("texture", sf::Shader::CurrentTexture);
    shaders["blur"].SetTexture("texture", sf::Shader::CurrentTexture);
    shaders["blur"].SetParameter("offset", 0.f);
    shaders["colorize"].SetTexture("texture", sf::Shader::CurrentTexture);
    shaders["colorize"].SetParameter("color", 1.f, 1.f, 1.f);
    shaders["edge"].SetTexture("texture", sf::Shader::CurrentTexture);
    shaders["fisheye"].SetTexture("texture", sf::Shader::CurrentTexture);
    shaders["wave"].SetTexture("texture", sf::Shader::CurrentTexture);
    shaders["wave"].SetTexture("wave", waveImage);
    shaders["pixelate"].SetTexture("texture", sf::Shader::CurrentTexture);

    // Define a string for displaying the description of the current shader
    sf::Text shaderStr;
    shaderStr.SetFont(font);
    shaderStr.SetSize(20);
    shaderStr.SetPosition(5.f, 0.f);
    shaderStr.SetColor(sf::Color(250, 100, 30));
    shaderStr.SetString("Background shader: \"" + backgroundShader.GetName() + "\"\n"
                        "Flower shader: \"" + entityShader.GetName() + "\"\n"
                        "Global shader: \"" + globalShader.GetName() + "\"\n");

    // Define a string for displaying help
    sf::Text infoStr;
    infoStr.SetFont(font);
    infoStr.SetSize(20);
    infoStr.SetPosition(5.f, 510.f);
    infoStr.SetColor(sf::Color(250, 100, 30));
    infoStr.SetString("Move your mouse to change the shaders' parameters\n"
                      "Press numpad 1/4 to change the background shader\n"
                      "Press numpad 2/5 to change the flower shader\n"
                      "Press numpad 3/6 to change the global shader");

    // Create a clock to measure the total time elapsed
    sf::Clock clock;

    // Start the game loop
    while (window.IsOpened())
    {
        // Process events
        sf::Event event;
        while (window.GetEvent(event))
        {
            // Close window : exit
            if (event.Type == sf::Event::Closed)
                window.Close();

            if (event.Type == sf::Event::KeyPressed)
            {
                // Escape key : exit
                if (event.Key.Code == sf::Key::Escape)
                    window.Close();

                // Numpad : switch effect
                switch (event.Key.Code)
                {
                    case sf::Key::Numpad1 : backgroundShader.GotoPrevious(); break;
                    case sf::Key::Numpad4 : backgroundShader.GotoNext();     break;
                    case sf::Key::Numpad2 : entityShader.GotoPrevious();     break;
                    case sf::Key::Numpad5 : entityShader.GotoNext();         break;
                    case sf::Key::Numpad3 : globalShader.GotoPrevious();     break;
                    case sf::Key::Numpad6 : globalShader.GotoNext();         break;
                    default :                                                break;
                }

                // Update the text
                shaderStr.SetString("Background shader: \"" + backgroundShader.GetName() + "\"\n"
                                    "Entity shader: \"" + entityShader.GetName() + "\"\n"
                                    "Global shader: \"" + globalShader.GetName() + "\"\n");
            }
        }

        // Get the mouse position in the range [0, 1]
        float mouseX = window.GetInput().GetMouseX() / static_cast<float>(window.GetWidth());
        float mouseY = window.GetInput().GetMouseY() / static_cast<float>(window.GetHeight());

        // Update the shaders
        backgroundShader.Update(mouseX, mouseY);
        entityShader.Update(mouseX, mouseY);
        globalShader.Update(mouseX, mouseY);

        // Animate the entity
        float entityX = (cos(clock.GetElapsedTime() * 1.3f) + 1.2f) * 300;
        float entityY = (cos(clock.GetElapsedTime() * 0.8f) + 1.2f) * 200;
        entity.SetPosition(entityX, entityY);
        entity.Rotate(window.GetFrameTime() * 100);

        // Draw the background and the moving entity to the render image
        image.Clear();
        image.Draw(background, backgroundShader.GetShader());
        image.Draw(entity, entityShader.GetShader());
        image.Display();

        // Draw the contents of the render image to the window
        sf::Sprite screen(image.GetImage());
        window.Draw(screen, globalShader.GetShader());

        // Draw the interface texts
        window.Draw(shaderStr);
        window.Draw(infoStr);

        // Finally, display the rendered frame on screen
        window.Display();
    }

    return EXIT_SUCCESS;
}


////////////////////////////////////////////////////////////
/// Fonction called when the post-effects are not supported ;
/// Display an error message and wait until the user exits
///
////////////////////////////////////////////////////////////
void DisplayError()
{
    // Create the main window
    sf::RenderWindow window(sf::VideoMode(800, 600), "SFML Shader");

    // Define a string for displaying the error message
    sf::Text error("Sorry, your system doesn't support shaders");
    error.SetPosition(100.f, 250.f);
    error.SetColor(sf::Color(200, 100, 150));

    // Start the game loop
    while (window.IsOpened())
    {
        // Process events
        sf::Event event;
        while (window.GetEvent(event))
        {
            // Close window : exit
            if (event.Type == sf::Event::Closed)
                window.Close();

            // Escape key : exit
            if ((event.Type == sf::Event::KeyPressed) && (event.Key.Code == sf::Key::Escape))
                window.Close();
        }

        // Clear the window
        window.Clear();

        // Draw the error message
        window.Draw(error);

        // Finally, display the rendered frame on screen
        window.Display();
    }
}