#include <SFML/Graphics.hpp>
#include <algorithm>
#include <vector>
#include <map>
#include <cstdlib>
#include <ctime>
#include <cmath>

void noise(sf::Image & image) {

    sf::Vector2u imageSize = image.getSize() ;

    for (unsigned i = 0; i < imageSize.x; ++ i) {
        for (unsigned j = 0; j < imageSize.y; ++ j) {

            int val = std::rand() % 256;

            image.setPixel(i, j, sf::Color(val, val, val));
        }
    }
}

void wall(sf::Image & image, bool cool) {

    std::map<int, int> interiorId2offset;

    const sf::Vector2u imageSize = image.getSize() ;

    for (unsigned i = 0; i < imageSize.x ; ++ i) {
        for (unsigned j = 0; j < imageSize.y; ++ j) {

            int val = 0;
            int offset = 0;

            if (j % 128 < 64) offset = 40;

            bool one = ((i + offset) % 128 < 2 || (i + offset) % 128 > 126);
            bool two = ( j           %  64 < 2 ||  j           %  64 >  62);

            bool nearOne = (i % 128 < 8 || i % 128 > 120);
            bool nearTwo = (j %  64 < 8 || j %  64 >  56);

            if (one && !nearTwo || two && !nearOne) val = 225;

            bool interior = !one && !two;

            if (interior) {

                unsigned x = (i + offset) / 128;
                unsigned y =  j           /  64;

                if (x >= imageSize.x / 128) x = 0; // in order to be tileable

                int interiorOffset = interiorId2offset[x + 100 * y];

                if (interiorOffset == 0) {

                    interiorOffset = std::rand() % 3 * 30 + 1;
                    interiorId2offset[x + 100 * y] = interiorOffset;
                }

                val = ((i + offset) % 128 + j % 64 + interiorOffset) % 256;

                if (val <  25) val =  25;
                if (val > 175) val = 175;
            }

            if (cool) val = (val + std::rand() % 15 + 35) % 256;

            image.setPixel(i, j, sf::Color(val, val, val));
        }
    }
}

void whiteDrops(sf::Image & image, int count, int minSize, int maxSize, bool filled = true, int thickness = 0) {

    const sf::Vector2u imageSize = image.getSize() ;

    for (int i = 0; i < count; ++ i) {

        int cx = std::rand() % imageSize.x;
        int cy = std::rand() % imageSize.y;

        int size = std::rand() % (maxSize - minSize + 1) + minSize;

        for (int x = cx - size; x <= cx + size; ++ x) {
            for (int y = cy - size; y <= cy + size; ++ y) {

                int d2 = (x - cx) * (x - cx) + (y - cy) * (y - cy);
                int s2 = size * size;

                if (d2 <= s2) {

                    int px = (x % imageSize.x + imageSize.x) % imageSize.x;
                    int py = (y % imageSize.y + imageSize.y) % imageSize.y;

                    if (d2 <= s2 - thickness) {
                        if (filled)
                            image.setPixel(px, py, sf::Color::Black);
                    } else {
                        image.setPixel(px, py, sf::Color::White);
                    }
                }
            }
        }
    }
}

void bit(sf::Image & image, int a, int b, int c) {

    const sf::Vector2u imageSize = image.getSize() ;

    for (unsigned i = 0; i < imageSize.x; ++ i) {
        for (unsigned j = 0; j < imageSize.y; ++ j) {

            int val = (a * (i & j) + b * (i ^ j) + c * (i | j)) % 256;

            image.setPixel(i, j, sf::Color(val, val, val));
        }
    }
}

void median(sf::Image & image, int size, bool red = true, bool green = true, bool blue = true) {

    sf::Image image2(image);

    const sf::Vector2u imageSize = image.getSize() ;

    for (unsigned i = 0; i < imageSize.x; ++ i) {
        for (unsigned j = 0; j < imageSize.y; ++ j) {

            std::vector<int> rs, gs, bs;

            sf::Color c;

            for (int x = -size; x <= size; ++ x) {
                for (int y = -size; y <= size; ++ y) {

                    int a = ((x + i) % imageSize.x + imageSize.x) % imageSize.x;
                    int b = ((y + j) % imageSize.y + imageSize.y) % imageSize.y;

                    c = image2.getPixel(a, b);

                    rs.push_back(c.r);
                    gs.push_back(c.g);
                    bs.push_back(c.b);
                }
            }

            c = image2.getPixel(i, j);

            if (red) {
                std::sort(rs.begin(), rs.end());
                c.r = rs[rs.size() / 2];
            }

            if (green) {
                std::sort(gs.begin(), gs.end());
                c.g = gs[gs.size() / 2];
            }

            if (blue) {
                std::sort(bs.begin(), bs.end());
                c.b = bs[bs.size() / 2];
            }

            image.setPixel(i, j, c);
        }
    }
}

void blend(sf::Image & dest, sf::Image source1, sf::Image source2, int w1, int w2) {

    const sf::Vector2u destSize = dest.getSize() ;
 
    for (unsigned i = 0; i < destSize.x ; ++ i) {
        for (unsigned j = 0; j < destSize.y ; ++ j) {

            int r = 0, g = 0, b = 0;

            sf::Color p1 = source1.getPixel(i, j);
            sf::Color p2 = source2.getPixel(i, j);

            r += w1 * p1.r;
            r += w2 * p2.r;

            g += w1 * p1.g;
            g += w2 * p2.g;

            b += w1 * p1.b;
            b += w2 * p2.b;

            r /= (w1 + w2);
            g /= (w1 + w2);
            b /= (w1 + w2);

            dest.setPixel(i, j, sf::Color(r, g, b));
        }
    }
}

void pixelize(sf::Image & image, int size) {

    const sf::Vector2u imageSize = image.getSize() ;

    sf::Image image2(image);

    for (unsigned i = 0; i < imageSize.x; i += size) {
        for (unsigned j = 0; j < imageSize.y; j += size) {

            sf::Color curColor = image2.getPixel(i, j);

            for (int x = 0; x < size; ++ x) {
                for (int y = 0; y < size; ++ y) {

                    int a = ((x + i) % imageSize.x + imageSize.x) % imageSize.x;
                    int b = ((y + j) % imageSize.y + imageSize.y) % imageSize.y;

                    image.setPixel(a, b, curColor);
                }
            }
        }
    }
}

void magnifyingDrops(sf::Image & image, int count, int minSize, int maxSize, double exponent) {

    const sf::Vector2u imageSize = image.getSize() ;

    sf::Image image2(image);

    for (int i = 0; i < count; ++ i) {

        int cx = std::rand() % imageSize.x;
        int cy = std::rand() % imageSize.y;

        int size = std::rand() % (maxSize - minSize + 1) + minSize;

        int s2 = size * size;

        double m = std::pow(size, 1 - exponent);

        for (int x = - size; x <= size; ++ x) {
            for (int y = - size; y <= size; ++ y) {

                int signx = x < 0 ? -1 : 1;
                int signy = y < 0 ? -1 : 1;

                int d2 = x * x + y * y;

                if (d2 > s2) continue;

                int sx = (int(cx + signx * std::pow(signx * x, exponent) * m) % imageSize.x + imageSize.x) % imageSize.x;
                int sy = (int(cy + signy * std::pow(signy * y, exponent) * m) % imageSize.y + imageSize.y) % imageSize.y;

                int tx = ((cx + x) % imageSize.x + imageSize.x) % imageSize.x;
                int ty = ((cy + y) % imageSize.y + imageSize.y) % imageSize.y;

                image.setPixel(tx, ty, image2.getPixel(sx, sy));
            }
        }
    }
}

int main()
{
    std::srand(static_cast<unsigned>(std::time(0)));

    const int width  = 512;
    const int height = 256;

    sf::Image txt_bit;
    sf::Image txt_wall;
    sf::Image txt_noise;
    sf::Image txt_drops;
    sf::Image txt_result;

    txt_bit.create(width, height, sf::Color::Black) ;
    txt_wall.create(width, height, sf::Color::Black) ;
    txt_noise.create(width, height, sf::Color::Black) ;
    txt_drops.create(width, height, sf::Color::Black) ;
    txt_result.create(width, height, sf::Color::Black) ;

    //txt_bit.setSmooth(false);
    //txt_wall.SetSmooth(false);
    //txt_noise.SetSmooth(false);
    //txt_drops.SetSmooth(false);
    //txt_result.SetSmooth(false);

    /*
    bit(txt_bit, 16, 64, 32);
    median(txt_bit, 2);
    //*/

    /*
    bit(txt_bit, 32, 64, 16);
    median(txt_bit, 2);
    //*/

    /*
    bit(txt_bit, 64, 32, 16);
    median(txt_bit, 2);
    //*/

    /*
    bit(txt_bit, 4, 8, 16);
    median(txt_bit, 1);
    median(txt_bit, 1);
    //*/

    /*
    bit(txt_bit, 16, 8, 16);
    median(txt_bit, 1);
    median(txt_bit, 1);
    //*/

    /*
    bit(txt_bit, 15, 16, 17);
    median(txt_bit, 1);
    median(txt_bit, 1);
    median(txt_bit, 1);
    //*/

    ///*
    bit(txt_bit, 35, 70, 140);
    median(txt_bit, 1);
    median(txt_bit, 2);
    //*/

    wall(txt_wall, true);
    noise(txt_noise);
    whiteDrops(txt_drops, 250, 40, 80, false, 125);
    blend(txt_result, txt_drops, txt_noise, 15, 5);
    median(txt_result, 1);
    blend(txt_result, txt_wall, txt_result, 13, 2);
    magnifyingDrops(txt_result, 10, 40, 50, 1.15);
    pixelize(txt_result, 4);

    sf::Texture resultTexture ;
    resultTexture.loadFromImage(txt_result) ;
    resultTexture.setSmooth(true) ;
    sf::Sprite sprite(resultTexture);


    //pixelize(txt_bit, 2);
    //sf::Sprite sprite(txt_bit);

    sf::RenderWindow window(sf::VideoMode(width * 2, height * 2, 32), "Procedural Textures!");

    window.setVerticalSyncEnabled(true);

    bool Running = true;

    while (Running)
    {
        sf::Event Event;

        while (window.pollEvent(Event))
        {
            if (Event.type == sf::Event::Closed)
                Running = false;

            if ((Event.type == sf::Event::KeyPressed) && (Event.key.code == sf::Keyboard::Escape))
                Running = false;
        }


        window.clear();

        window.draw(sprite);
        sprite.move(static_cast<float>(width), 0.0f);
        window.draw(sprite);
        sprite.move(0.0f, static_cast<float>(height));
        window.draw(sprite);
        sprite.move(static_cast<float>(- width), 0.0f);
        window.draw(sprite);
        sprite.move(0.0f, static_cast<float>(- height));

        window.display();
    }
}