#include "image.hpp"
#include <map>


Buf::Buf(unsigned _w, unsigned _h): buf((unsigned char*)malloc(_w*_h*3)), proxy(false), off_x(0), off_y(0), width(_w), height(_h), w(_w), h(_h) {
    assert(w && h);
}


Buf::Buf(const Buf& b): buf((unsigned char*)memcpy(malloc(b.width*b.height*3), b.buf, b.width*b.height*3)), proxy(false), off_x(0), off_y(0), width(b.width), height(b.height), w(b.w), h(b.h) {
}


Buf::Buf(Buf* b, unsigned _x, unsigned _y, unsigned _w, unsigned _h): buf(b->buf), proxy(true), off_x(_x), off_y(_y), width(b->width), height(b->height), w(_w), h(_h) {
    assert(off_x + w < width && off_y + h < height);
}


Buf::~Buf() {
    if (!proxy) {
        free(buf);
    }
}


void Image::put(unsigned x, unsigned y, const Image* other) {
    assert(x < buf.w && y < buf.h);
    for (unsigned xx=0; xx<MIN(other->buf.w, other->buf.h); ++xx) {
        for (unsigned yy=0; yy<MIN(other->buf.h, buf.h-y); ++yy) {
            buf.at(x+xx, y+yy) = other->buf.at(xx, yy);
        }
    }
}


Image* Image::box(unsigned dim) const {
    assert(dim);
    if (dim > buf.w || dim > buf.h) return NULL;
    if (dim == buf.w && dim == buf.h) return new Image(*this);

    unsigned x_skip=0, y_skip=0;
    if (buf.w > buf.h) {
        x_skip = (buf.w-buf.h)/2;
    } else if (buf.h > buf.w) {
        y_skip = (buf.h-buf.w)/2;
    }
    const unsigned sample = MIN(buf.w, buf.h)/dim; // TODO: rounding etc?

    Image* im = new Image(dim, dim);
    for (unsigned x=0; x<dim; x++) {
        for (unsigned y=0; y<dim; y++) {
            im->buf.at(x, y) = avg(x_skip+(x*sample), y_skip+(y*sample), sample, sample);
        }
    }
    return im;
}


Image* Image::boxed(unsigned x, unsigned y, unsigned w, unsigned h) {
    assert(x < buf.w && y < buf.h);
    #if 1
        return new Image(&buf, x, y, w, h);
    #else
        Image* im = new Image(w, h);
        for (unsigned xx=x; xx<MIN(buf.w, x+w); ++xx) {
            for (unsigned yy=y; yy<MIN(buf.h, y+h); ++yy) {
                im->buf.at(xx-x, yy-y) = buf.at(xx, yy);
            }
        }
        return im;
    #endif
}


rgb_t Image::avg(unsigned x, unsigned y, unsigned w, unsigned h) const {
    assert(x < buf.w && y < buf.h);
    #if 1
        unsigned num = 0;
        unsigned sum[3] = {};
        for (unsigned xx=x; xx<MIN(buf.w, x+w); ++xx) {
            for (unsigned yy=y; yy<MIN(buf.h, y+h); ++yy) {
                num++;
                sum[0] += buf.at(xx, yy).r;
                sum[1] += buf.at(xx, yy).g;
                sum[2] += buf.at(xx, yy).b;
            }
        }

        rgb_t rv = {};
        if (num) {
            rv.r = sum[0]/num;
            rv.g = sum[1]/num;
            rv.b = sum[2]/num;
        }
        return rv;
    #elif 0
        static unsigned palette[25][25][25];
        memset(palette, 0, sizeof(palette));
        unsigned max_num = 0;
        rgb_t max_rgb = {};
        for (unsigned xx=x; xx<MIN(buf.w, x+w); ++xx) {
            for (unsigned yy=y; yy<MIN(buf.h, y+h); ++yy) {
                const rgb_t& rgb = buf.at(xx, yy);
                unsigned& p = palette[rgb.r/10][rgb.g/10][rgb.b/10];
                if (++p > max_num) {
                    max_num = p;
                    max_rgb = rgb;
                }
            }
        }
        return max_rgb;
    #endif
}


rgb_t Image::med(unsigned x, unsigned y, unsigned w, unsigned h) const {
    #if 1
        assert(x < buf.w && y < buf.h);
        const unsigned char bucket_width = 1; // TODO:
        std::map<rgb_t, unsigned> buckets;
        std::map<rgb_t, unsigned>::const_iterator max_bucket = buckets.end();
        for (unsigned xx=x; xx<MIN(buf.w, x+w); ++xx) {
            for (unsigned yy=y; yy<MIN(buf.h, y+h); ++yy) {
                const rgb_t& rgb = buf.at(xx, yy);
                const rgb_t& value = {
                    (unsigned char)(rgb.r / bucket_width),
                    (unsigned char)(rgb.g / bucket_width),
                    (unsigned char)(rgb.b / bucket_width)
                };
                std::map<rgb_t, unsigned>::iterator bucket = buckets.find(value);
                if (bucket != buckets.end()) {
                    bucket->second = bucket->second + 1;
                } else {
                    bucket = buckets.insert(std::pair<rgb_t, unsigned>(value, 1)).first;
                }
                if (max_bucket == buckets.end() || bucket->second > max_bucket->second) {
                    max_bucket = bucket;
                }
            }
        }
        assert(max_bucket != buckets.end());
        return rgb_t{
            (unsigned char)(max_bucket->first.r + (bucket_width / 2)),
            (unsigned char)(max_bucket->first.g + (bucket_width / 2)),
            (unsigned char)(max_bucket->first.b + (bucket_width / 2)),
        };
    #else
        return avg(x, y, w, h);
    #endif
}


void Image::tint(unsigned x, unsigned y, unsigned w, unsigned h, const rgb_t& c, unsigned char alpha) {
    for (unsigned xx=x; xx<MIN(buf.w, x+w); ++xx) {
        for (unsigned yy=y; yy<MIN(buf.h, y+h); ++yy) {
            buf.at(xx, yy).tint(c, alpha);
        }
    }
}


void Image::tint(const Image* im, unsigned char alpha) {
    for (unsigned x=0; x<MIN(buf.w, im->buf.w); ++x) {
        for (unsigned y=0; y<MIN(buf.h, im->buf.h); ++y) {
            buf.at(x, y).tint(im->buf.at(x, y), alpha);
        }
    }
}


unsigned char Image::dist(const rgb_t& c) const {
    unsigned rv = 0;
    for (unsigned x=0; x<buf.w; ++x) {
        for (unsigned y=0; y<buf.h; ++y) {
            rv += c.dist(buf.at(x, y));
        }
    }
    return rv / (buf.w * buf.h);
}


unsigned char Image::dist(const Image* im) const {
    assert(buf.w == im->buf.w && buf.h == im->buf.h);
    unsigned rv = 0;
    for (unsigned x=0; x<buf.w; ++x) {
        for (unsigned y=0; y<buf.h; ++y) {
            rv += buf.at(x, y).dist(im->buf.at(x, y));
        }
    }
    return rv / (buf.w * buf.h);
}


double Image::fdist(const Image* im) const {
    assert(buf.w == im->buf.w && buf.h == im->buf.h);
    double rv = 0;
    for (unsigned x=0; x<buf.w; ++x) {
        for (unsigned y=0; y<buf.h; ++y) {
            rv += buf.at(x, y).fdist(im->buf.at(x, y));
        }
    }
    return rv / (buf.w * buf.h);
}