#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>

#define PNG_DEBUG 3
#include <png.h>

#define OUT_FILE "initials.png"
#define WIDTH 600
#define HEIGHT 600
#define COLOR_TYPE PNG_COLOR_TYPE_RGB
#define BIT_DEPTH 8

typedef struct {
    int x;
    int y;
} Point;

void abort_(const char * s, ...)
{
    va_list args;
    va_start(args, s);
    vfprintf(stderr, s, args);
    fprintf(stderr, "\n");
    va_end(args);
    abort();
}

int x, y;

int width, height;
png_byte color_type;
png_byte bit_depth;

png_structp png_ptr;
png_infop info_ptr;
int number_of_passes;
png_bytep * row_pointers;

static void set_pixel(int px, int py, png_byte r, png_byte g, png_byte b)
{
    if (px < 0 || px >= width || py < 0 || py >= height)
        return;

    png_byte *row = row_pointers[py];
    png_byte *ptr = &(row[px * 3]);
    ptr[0] = r;
    ptr[1] = g;
    ptr[2] = b;
}

static void get_pixel(int px, int py, png_byte *r, png_byte *g, png_byte *b)
{
    if (px < 0 || px >= width || py < 0 || py >= height) {
        *r = *g = *b = 0;
        return;
    }

    png_byte *row = row_pointers[py];
    png_byte *ptr = &(row[px * 3]);
    *r = ptr[0];
    *g = ptr[1];
    *b = ptr[2];
}

static int pixel_equals(int px, int py, png_byte r, png_byte g, png_byte b)
{
    png_byte pr, pg, pb;
    get_pixel(px, py, &pr, &pg, &pb);
    return pr == r && pg == g && pb == b;
}

static void fill_background(png_byte r, png_byte g, png_byte b)
{
    for (y = 0; y < height; y++) {
        for (x = 0; x < width; x++) {
            set_pixel(x, y, r, g, b);
        }
    }
}

static void draw_line(int x0, int y0, int x1, int y1, png_byte r, png_byte g, png_byte b)
{
    int dx = abs(x1 - x0);
    int sx = x0 < x1 ? 1 : -1;
    int dy = -abs(y1 - y0);
    int sy = y0 < y1 ? 1 : -1;
    int err = dx + dy;

    while (1) {
        set_pixel(x0, y0, r, g, b);
        if (x0 == x1 && y0 == y1)
            break;
        int e2 = 2 * err;
        if (e2 >= dy) {
            err += dy;
            x0 += sx;
        }
        if (e2 <= dx) {
            err += dx;
            y0 += sy;
        }
    }
}

static void draw_polygon(const Point *points, int count, png_byte r, png_byte g, png_byte b)
{
    int i;
    for (i = 0; i < count; i++) {
        Point a = points[i];
        Point c = points[(i + 1) % count];
        draw_line(a.x, a.y, c.x, c.y, r, g, b);
    }
}

static void draw_circle(int cx, int cy, int radius, png_byte r, png_byte g, png_byte b)
{
    int x = 0;
    int y = radius;
    int d = 1 - radius;

    while (y >= x) {
        set_pixel(cx + x, cy + y, r, g, b);
        set_pixel(cx - x, cy + y, r, g, b);
        set_pixel(cx + x, cy - y, r, g, b);
        set_pixel(cx - x, cy - y, r, g, b);
        set_pixel(cx + y, cy + x, r, g, b);
        set_pixel(cx - y, cy + x, r, g, b);
        set_pixel(cx + y, cy - x, r, g, b);
        set_pixel(cx - y, cy - x, r, g, b);

        x++;
        if (d < 0) {
            d += 2 * x + 1;
        } else {
            y--;
            d += 2 * (x - y) + 1;
        }
    }
}

static void flood_fill(int start_x, int start_y,
                       png_byte target_r, png_byte target_g, png_byte target_b,
                       png_byte fill_r, png_byte fill_g, png_byte fill_b)
{
    int max = width * height;
    Point *stack;
    int top = 0;

    if (target_r == fill_r && target_g == fill_g && target_b == fill_b)
        return;

    if (!pixel_equals(start_x, start_y, target_r, target_g, target_b))
        return;

    stack = (Point *)malloc((size_t)max * sizeof(Point));
    if (!stack)
        abort_("[flood_fill] Not enough memory");

    stack[top++] = (Point){start_x, start_y};

    while (top > 0) {
        Point p = stack[--top];

        if (p.x < 0 || p.x >= width || p.y < 0 || p.y >= height)
            continue;

        if (!pixel_equals(p.x, p.y, target_r, target_g, target_b))
            continue;

        set_pixel(p.x, p.y, fill_r, fill_g, fill_b);

        stack[top++] = (Point){p.x + 1, p.y};
        stack[top++] = (Point){p.x - 1, p.y};
        stack[top++] = (Point){p.x, p.y + 1};
        stack[top++] = (Point){p.x, p.y - 1};
    }

    free(stack);
}

void create_png_file()
{
    width = WIDTH;
    height = HEIGHT;
    bit_depth = BIT_DEPTH;
    color_type = COLOR_TYPE;

    row_pointers = (png_bytep*) malloc(sizeof(png_bytep) * height);
    for (y = 0; y < height; y++)
        row_pointers[y] = (png_byte*) malloc(width * 3 * sizeof(png_byte));
}

void write_png_file(char* file_name)
{
    FILE *fp = fopen(file_name, "wb");
    if (!fp)
        abort_("[write_png_file] File %s could not be opened for writing", file_name);

    png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
    if (!png_ptr)
        abort_("[write_png_file] png_create_write_struct failed");

    info_ptr = png_create_info_struct(png_ptr);
    if (!info_ptr)
        abort_("[write_png_file] png_create_info_struct failed");

    if (setjmp(png_jmpbuf(png_ptr)))
        abort_("[write_png_file] Error during init_io");

    png_init_io(png_ptr, fp);

    if (setjmp(png_jmpbuf(png_ptr)))
        abort_("[write_png_file] Error during writing header");

    png_set_IHDR(png_ptr, info_ptr, width, height,
                 bit_depth, color_type, PNG_INTERLACE_NONE,
                 PNG_COMPRESSION_TYPE_BASE, PNG_FILTER_TYPE_BASE);

    png_write_info(png_ptr, info_ptr);

    if (setjmp(png_jmpbuf(png_ptr)))
        abort_("[write_png_file] Error during writing bytes");

    png_write_image(png_ptr, row_pointers);

    if (setjmp(png_jmpbuf(png_ptr)))
        abort_("[write_png_file] Error during end of write");

    png_write_end(png_ptr, NULL);

    for (y = 0; y < height; y++)
        free(row_pointers[y]);
    free(row_pointers);

    fclose(fp);
}

void process_file(void)
{
    Point m_points[] = {
        {150, 420}, {150, 180}, {190, 180}, {225, 300},
        {260, 180}, {300, 180}, {300, 420}, {260, 420},
        {260, 265}, {235, 360}, {215, 360}, {190, 265}, {190, 420}
    };

    Point c_points[] = {
        {360, 180}, {470, 180}, {470, 220}, {395, 220},
        {370, 245}, {370, 355}, {395, 380}, {470, 380},
        {470, 420}, {360, 420}, {330, 390}, {330, 210}
    };

    fill_background(10, 20, 70);

    draw_circle(300, 300, 220, 0, 0, 0);
    flood_fill(300, 300, 10, 20, 70, 140, 190, 255);

    draw_polygon(m_points, (int)(sizeof(m_points) / sizeof(m_points[0])), 0, 0, 0);
    draw_polygon(c_points, (int)(sizeof(c_points) / sizeof(c_points[0])), 0, 0, 0);

    flood_fill(170, 300, 140, 190, 255, 220, 40, 40);
    flood_fill(350, 300, 140, 190, 255, 255, 40, 40);
}

int main(int argc, char **argv)
{
    create_png_file();
    process_file();
    write_png_file(OUT_FILE);

    return 0;
}
