#include <unistd.h>
#include <climits>
#include <cstdint>
#include <cmath>
#include <vector>
#include <iostream>
#include <fstream>
#include <SDL/SDL.h>
#include <SDL/SDL_image.h>
#include <SDL/SDL_gfxPrimitives.h>
 
#define COLOR 10
#define SHAPE 10
#define ORDER 8
 
#define POINTS 3
#define SHAPES 50
 
#define CLAMP(val, min, max) ((val) < (min) ? (min) : (val) > (max) ? (max) : (val))
 
using namespace std;
 
int32_t maxFit = -1;
int32_t w, h;
 
class Polygon {
	public:
		uint8_t getR(void) const { return this->r; }
		uint8_t getG(void) const { return this->g; }
		uint8_t getB(void) const { return this->b; }
		uint8_t getA(void) const { return this->a; }
 
		int16_t getX(uint32_t i) const { return this->x[i]; }
		int16_t getY(uint32_t i) const { return this->y[i]; }
 
		int16_t *getX(void) { return this->x; }
		int16_t *getY(void) { return this->y; }
 
		void setR(uint8_t r) { this->r = CLAMP(r, 0, 255); }
		void setG(uint8_t g) { this->g = CLAMP(g, 0, 255); }
		void setB(uint8_t b) { this->b = CLAMP(b, 0, 255); }
		void setA(uint8_t a) { this->a = CLAMP(a, 0, 255); }
 
		void setX(uint32_t i, int16_t x) { this->x[i] = CLAMP(x, 0, w); }
		void setY(uint32_t i, int16_t y) { this->y[i] = CLAMP(y, 0, h); }
	private:
		uint8_t r, g, b, a;
 
		int16_t x[POINTS];
		int16_t y[POINTS];
 
		int16_t bounds[2][2];
};
 
vector <Polygon> DNA;
 
void DrawDNA(SDL_Surface *surf) {
	SDL_FillRect(surf, &surf->clip_rect, SDL_MapRGB(surf->format, 255, 255, 255));
 
	for (auto p = DNA.begin(); p != DNA.end(); p++) {
		filledPolygonRGBA(surf, p->getX(), p->getY(), POINTS, p->getR(), p->getG(), p->getB(), p->getA());
	}
}
 
int32_t diff(SDL_Surface *genetic, SDL_Surface *original) {
	uint8_t *g = (uint8_t *)genetic->pixels;
	uint8_t *o = (uint8_t *)original->pixels;
 
	int32_t d = 0;
	bool mf = false;
 
	if (maxFit == -1) {
		maxFit = 0;
		mf = true;
	}
 
	for(int32_t y = 0; y < h; y += 2) {
		for(int32_t x = 0; x < w; x += 2) {
			int32_t p = y * w * 4 + x * 4;
 
			if (mf) {
				maxFit += o[p] + o[p + 1] + o[p + 2];
			}
 
			d += abs(g[p] - o[p]) + abs(g[p + 1] - o[p + 1]) + abs(g[p + 2] - o[p + 2]);
		}
	}
	return d;
}
 
int main(int argc, char ** argv) {
	srandom(getpid() + time(NULL));
 
	SDL_Init(SDL_INIT_VIDEO);
 
	SDL_Surface *tgoal = IMG_Load(argv[1]);
 
	SDL_Surface *screen = SDL_SetVideoMode(tgoal->w * 2, tgoal->h, 32, SDL_HWSURFACE);
 
	SDL_Surface *goal = SDL_DisplayFormatAlpha(tgoal);
	SDL_Surface *genetic = SDL_DisplayFormat(tgoal);
 
	SDL_FreeSurface(tgoal);
 
	SDL_Event event;
 
	w = goal->w;
	h = goal->h;
 
	// Initialize the DNA
	Polygon p;
 
	for(int32_t i = 0; i < SHAPES; i++) {
		for(int32_t j = 0; j < POINTS; j++) {
			p.setX(j, rand() % (w + 1));
			p.setY(j, rand() % (h + 1));
		}
		p.setR(rand() % 256);
		p.setG(rand() % 256);
		p.setB(rand() % 256);
		p.setA(0);
		DNA.push_back(p);
	}
 
	DrawDNA(genetic);
 
	int32_t start, last;
	start = last = SDL_GetTicks();
 
	int32_t bestDiff = INT_MAX;
 
	uint32_t steps = 0; // How many mutations have we done
	uint32_t good = 0; // How many mutations have been effective
	uint32_t bad = 0; // How many mutations have made the solution worse
	int32_t wasGood = 0;
 
	bool run = true;
	while (run) {
		while (SDL_PollEvent(&event)) {
			switch (event.type) {
				case SDL_QUIT:
					run = false;
					break;
				case SDL_KEYDOWN:
					if (event.key.keysym.sym == SDLK_ESCAPE) {
						run = false;
						break;
					}
					if (event.key.keysym.sym == SDLK_SPACE) {
						SDL_SaveBMP(screen, (to_string(time(NULL)) + ".bmp").c_str());
						break;
					}
			}
		}
 
		Polygon old;
		int32_t shape1;
		int32_t shape2 = -1;
 
		// Try to mutate same shape again if wasGood variable is greater than 0
		if (wasGood <= 0) shape1 = rand() % (SHAPES + 1);
		wasGood--;
 
		bool soft = (rand() % 2 == 1);
 
		Polygon *shape = &DNA[shape1];
		old = DNA[shape1];
 
		uint32_t act = rand() % (COLOR + SHAPE + ORDER);
		if (act < COLOR) {
			switch (rand() % 4) {
				case 0: shape->setR(soft ? shape->getR() + rand() % 27 - 13 : rand() % 256); break;
				case 1: shape->setG(soft ? shape->getG() + rand() % 27 - 13 : rand() % 256); break;
				case 2: shape->setB(soft ? shape->getB() + rand() % 27 - 13 : rand() % 256); break;
				case 3: shape->setA(soft ? shape->getA() + rand() % 27 - 13 : rand() % 256); break;
			}
		}
		else if (act < COLOR + SHAPE) {
			int32_t i = rand() % (POINTS + 1);
			if (rand() % 2) {
				shape->setX(i, soft ? shape->getX(i) + rand() % (int(w / 10.0) + 1) - w / 20 : rand() % (w + 1));
			}
			else {
				shape->setY(i, soft ? shape->getY(i) + rand() % (int(h / 10.0) + 1) - h / 20 : rand() % (h + 1));
			}
		}
		else {
			int destination = rand() % (SHAPES + 1);
			swap(DNA[shape1], DNA[destination]);
			shape2 = destination;
		}
 
		DrawDNA(genetic);
 
		int32_t d = diff(genetic, goal);
		// Test if new result is better than the old one
		if(d < bestDiff) {
			good++;
			wasGood = 100;
 
			bestDiff = d;
 
			// Blit original image
			SDL_BlitSurface(goal, 0, screen, 0);
 
			SDL_Rect r;
			r.x = goal->w;
			r.y = 0;
			r.w = goal->w;
			r.h = goal->h;
 
			// Blit genetic image
			SDL_BlitSurface(genetic, 0, screen, &r);
 
			SDL_Flip(screen);
		}
		else {
			if(shape2 >= 0) {
				swap(DNA[shape1], DNA[shape2]);
			}
			else {
				DNA[shape1] = old;
			}
		}
		steps++;
 
		// Print some useful(?) info
		if (steps % 100 == 0) {
			cout << good << " / " << steps << " " << ((maxFit - bestDiff) / (float)maxFit) * 100 << " : " << SDL_GetTicks() - last << " - " << (double)(SDL_GetTicks() - start) / 1000.0 << endl;
			last = SDL_GetTicks();
		}
	}
 
	// Save BMP
	SDL_SaveBMP(screen, (to_string(time(NULL)) + ".bmp").c_str());
 
	// Dump triangle data to file
	ofstream out(to_string(time(NULL)) + "_dump");
	out << SHAPES << " " << POINTS << endl;
	for (auto i = DNA.begin(); i != DNA.end(); i++) {
		out << (int32_t)i->getR() << " " << (int32_t)i->getG() << " " << (int32_t)i->getB() << " " << (int32_t)i->getA() << endl;
		for (int32_t j = 0; j < POINTS; j++) {
			out << i->getX(j) << " " << i->getY(j) << endl;
		}
	}
 
	// Free resources
	SDL_FreeSurface(goal);
	SDL_FreeSurface(genetic);
 
	// Shutdown SDL
	SDL_Quit();
 
	return 0;
}