#include "img.hpp"
#include <stdio.h>
#include <fcntl.h>
#include <math.h>


#define VAL_MAX 255
#define VAL_SCN "hhu"
#define VAL_SCN_WIDTH "3"


Img::Img(unsigned ww, unsigned hh): buf((rgb_t*)calloc(ww*hh, sizeof(rgb_t))), freebuf(buf), w(ww), h(hh) {
    assert(w && h);
}


Img::Img(unsigned ww, unsigned hh, rgb_t* ib, void* fb): buf(ib), freebuf(fb), w(ww), h(hh) {
    assert(w && h);
}


Img::~Img() {
    free(freebuf);
}


Img* Img::from_ppm(const char* fn) {
    size_t size;
    char* buf = file_read(fn, size);
    if (!buf) {
        return NULL;
    }
    char* p = buf;

    unsigned type;
    unsigned ww, hh, max;
    int len = 0;
    if (sscanf(p, "P%u %u %u %u %n", &type, &ww, &hh, &max, &len) == 4) {
    } else if (sscanf(p, "P%u #%*[^\n] %u %u %u %n", &type, &ww, &hh, &max, &len) == 4) {
    } else {
        LOG("'%s': invalid PPM header", fn);
        return NULL;
    }
    if (type != 3 && type != 6) {
        LOG("'%s': invalid PPM type", fn);
        return NULL;
    }
    if (!ww || !hh || max != VAL_MAX) {
        LOG("'%s': invalid PPM header values", fn);
        return NULL;
    }
    p += len;

    Img* rv = NULL;
    if (type == 6) {
        if (size != ww*hh*3+len) {
            LOG("'%s': invalid PPM size", fn);
            free(buf);
            return NULL;
        }
#ifdef USE_X
        rv = new Img(ww, hh);
        for (unsigned y=0; y<hh; y++) {
            for (unsigned x=0; x<ww; x++) {
                rgb_t& px = rv->at(x, y);
                px.r = p[0];
                px.g = p[1];
                px.b = p[2];
                p += 3;
            }
        }
        free(buf);
#else
        rv = new Img(ww, hh, (rgb_t*)p, (void*)buf);
#endif
    } else {
        rv = new Img(ww, hh);
        for (unsigned y=0; y<hh; y++) {
            for (unsigned x=0; x<ww; x++) {
                rgb_t& px = rv->at(x, y);
                if (sscanf(p, "%" VAL_SCN " %" VAL_SCN " %" VAL_SCN "%n", &px.r, &px.g, &px.b, &len) != 3) {
                    LOG("'%s': cannot parse PPM pixel", fn);
                    delete rv;
                    free(buf);
                    return NULL;
                }
                p += len;
            }
        }
        free(buf);
    }

    return rv;
}


bool Img::to_ppm(const char* fn) const {
    int fd = open(fn, O_WRONLY|O_CREAT|O_TRUNC, S_IRUSR|S_IWUSR);
    if (fd == -1) {
        LOG_ERRNO("open(%s)", fn);
        return false;
    }
    if (!to_ppm(fd)) {
        close(fd);
        return false;
    }
    close(fd);
    return true;
}


bool Img::to_ppm(int fd) const {
#ifdef WRITE_PLAIN_PPM
        dprintf(fd, "P3\n%u %u\n%u\n", w, h, VAL_MAX);
        for (unsigned y=0; y<h; y++) {
            for (unsigned x=0; x<w; x++) {
                const rgb_t& px = at(x, y);
                dprintf(fd, "%3" VAL_SCN " %3" VAL_SCN " %3" VAL_SCN " ", px.r, px.g, px.b);
            }
            dprintf(fd, "\n");
        }
#else
        dprintf(fd, "P6\n%u %u\n%u\n", w, h, VAL_MAX);
#ifdef USE_X
        for (unsigned y=0; y<h; y++) {
            for (unsigned x=0; x<w; x++) {
                const rgb_t& px = at(x, y);
                unsigned char rgb[3];
                rgb[0] = px.r;
                rgb[1] = px.g;
                rgb[2] = px.b;
                if (write(fd, rgb, sizeof(rgb)) != sizeof(rgb)) {
                    LOG("cannot write");
                    return false;
                }
            }
        }
#else
        if (write(fd, buf, w*h*3) != (ssize_t)(w*h*3)) {
            LOG("cannot write");
            return false;
        }
#endif
#endif
    return true;
}


Img::rgb_t Img::avg(const unsigned x, const unsigned y, const unsigned xw, const unsigned yw) const {
    unsigned r=0, g=0, b=0;
    for (unsigned yy=y; yy<y+yw; ++yy) {
        for (unsigned xx=x; xx<x+xw; ++xx) {
            const rgb_t& p = at(xx, yy);
            #ifdef IMAGE_LIN_AVG
                r += p.r;
                g += p.g;
                b += p.b;
            #else
                r += p.r * p.r;
                g += p.g * p.g;
                b += p.b * p.b;
            #endif
        }
    }
    #ifdef IMAGE_LIN_AVG
        return rgb_t(r/(xw*yw), g/(xw*yw), b/(xw*yw));
    #else
        // http://stackoverflow.com/questions/649454/what-is-the-best-way-to-average-two-colors-that-define-a-linear-gradient/29576746#29576746
        // TODO: use lab space?
        return rgb_t(sqrt(r/(xw*yw)), sqrt(g/(xw*yw)), sqrt(b/(xw*yw)));
    #endif
}


Img::rgb_t Img::interpolate(coord_t x, coord_t y) const {
#ifdef IMAGE_INTERPOLATE
    coord_t xf = floor(x);
    coord_t xc = ceil(x);
    coord_t yf = floor(y);
    coord_t yc = ceil(y);

    Img::rgb_t p[4] = {
        at(xf, yf),
        at(xf, yc),
        at(xc, yf),
        at(xc, yc)
    };

    xc = xc - x;
    xf = 1.0 - xc;
    yc = yc - y;
    yf = 1.0 - yc;

    p[0] *= xc * yc;
    p[1] *= xc * yf;
    p[2] *= xf * yc;
    p[3] *= xf * yf;

    // TODO: support IMGAGE_LIN_AVG
    return rgb_t(
        p[0].r + p[1].r + p[2].r + p[3].r,
        p[0].g + p[1].g + p[2].g + p[3].g,
        p[0].b + p[1].b + p[2].b + p[3].b
    );
#else
    return at(x, y);
#endif
}


bool Img::supersample(unsigned fac, Img* rv) const {
    if (fac <= 1 || w % fac != 0 || h % fac != 0) {
        return false;
    }
    if (rv->w != w/fac || rv->h != h/fac) {
        return false;
    }
    for (unsigned y=0; y<rv->h; ++y) {
        for (unsigned x=0; x<rv->w; ++x) {
            rv->at(x, y) = avg(x*fac, y*fac, fac, fac);
        }
    }
    return true;
}


void Img::mirror_x() {
    for (unsigned x=0; x<w; ++x) {
        for (unsigned y=0; y<h/2; ++y) {
            rgb_t tmp = at(x, y);
            at(x, y) = at(x, h-y-1);
            at(x, h-y-1) = tmp;
        }
    }
}


void Img::mirror_y() {
    for (unsigned y=0; y<h; ++y) {
        for (unsigned x=0; x<w/2; ++x) {
            rgb_t tmp = at(x, y);
            at(x, y) = at(w-x-1, y);
            at(w-x-1, y) = tmp;
        }
    }
}


Img* Img::rotate() const {
    Img* rv = new Img(h, w);
    for (unsigned x=0; x<w; ++x) {
        for (unsigned y=0; y<h; ++y) {
            rv->at(y, w-x-1) = at(x, y);
        }
    }
    return rv;
}