#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>

#include <cammy/image.h>

static uint32_t bayer_matrix[256] = {
    /*
     * Black gamut
     */
    0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000,

    /*
     * Black to dark gray gradient
     */
    0x00018000, 0x00018000, 0x00018000, 0x00018000, 0x00018000,
    0x00018020, 0x00018020, 0x00018020, 0x00018020, 0x00018020,
    0x000180a0, 0x000180a0, 0x000180a0, 0x000180a0, 0x000180a0,
    0x0001a0a0, 0x0001a0a0, 0x0001a0a0, 0x0001a0a0, 0x0001a0a0,
    0x0001a4a0, 0x0001a4a0, 0x0001a4a0, 0x0001a4a0, 0x0001a4a0, 0x0001a4a0,
    0x0001a4a1, 0x0001a4a1, 0x0001a4a1, 0x0001a4a1, 0x0001a4a1,
    0x0001a4a5, 0x0001a4a5, 0x0001a4a5, 0x0001a4a5, 0x0001a4a5,
    0x0001a5a5, 0x0001a5a5, 0x0001a5a5, 0x0001a5a5, 0x0001a5a5,
    0x0001ada5, 0x0001ada5, 0x0001ada5, 0x0001ada5, 0x0001ada5, 0x0001ada5,
    0x0001ada7, 0x0001ada7, 0x0001ada7, 0x0001ada7, 0x0001ada7,
    0x0001adaf, 0x0001adaf, 0x0001adaf, 0x0001adaf, 0x0001adaf,
    0x0001afaf, 0x0001afaf, 0x0001afaf, 0x0001afaf, 0x0001afaf,
    0x0001df5f, 0x0001df5f, 0x0001df5f, 0x0001df5f, 0x0001df5f, 0x0001df5f,
    0x0001df7f, 0x0001df7f, 0x0001df7f, 0x0001df7f, 0x0001df7f,
    0x0001dfff, 0x0001dfff, 0x0001dfff, 0x0001dfff, 0x0001dfff,
    0x0001ffff, 0x0001ffff, 0x0001ffff, 0x0001ffff, 0x0001ffff,

    /*
     * Dark gray to light gray gradient
     */
    0x01028000, 0x01028000, 0x01028000, 0x01028000, 0x01028000, 0x01028000,
    0x01028020, 0x01028020, 0x01028020, 0x01028020, 0x01028020,
    0x010280a0, 0x010280a0, 0x010280a0, 0x010280a0, 0x010280a0,
    0x0102a0a0, 0x0102a0a0, 0x0102a0a0, 0x0102a0a0, 0x0102a0a0,
    0x0102a4a0, 0x0102a4a0, 0x0102a4a0, 0x0102a4a0, 0x0102a4a0, 0x0102a4a0,
    0x0102a4a1, 0x0102a4a1, 0x0102a4a1, 0x0102a4a1, 0x0102a4a1,
    0x0102a4a5, 0x0102a4a5, 0x0102a4a5, 0x0102a4a5, 0x0102a4a5,
    0x0102a5a5, 0x0102a5a5, 0x0102a5a5, 0x0102a5a5, 0x0102a5a5,
    0x0102ada5, 0x0102ada5, 0x0102ada5, 0x0102ada5, 0x0102ada5, 0x0102ada5,
    0x0102ada7, 0x0102ada7, 0x0102ada7, 0x0102ada7, 0x0102ada7,
    0x0102adaf, 0x0102adaf, 0x0102adaf, 0x0102adaf, 0x0102adaf,
    0x0102afaf, 0x0102afaf, 0x0102afaf, 0x0102afaf, 0x0102afaf,
    0x0102df5f, 0x0102df5f, 0x0102df5f, 0x0102df5f, 0x0102df5f, 0x0102df5f,
    0x0102df7f, 0x0102df7f, 0x0102df7f, 0x0102df7f, 0x0102df7f,
    0x0102dfff, 0x0102dfff, 0x0102dfff, 0x0102dfff, 0x0102dfff,
    0x0102ffff, 0x0102ffff, 0x0102ffff, 0x0102ffff, 0x0102ffff,

    /* Light gray to white gradient */
    0x02038000, 0x02038000, 0x02038000, 0x02038000, 0x02038000, 0x02038000,
    0x02038020, 0x02038020, 0x02038020, 0x02038020, 0x02038020,
    0x020380a0, 0x020380a0, 0x020380a0, 0x020380a0, 0x020380a0,
    0x0203a0a0, 0x0203a0a0, 0x0203a0a0, 0x0203a0a0, 0x0203a0a0,
    0x0203a4a0, 0x0203a4a0, 0x0203a4a0, 0x0203a4a0, 0x0203a4a0, 0x0203a4a0,
    0x0203a4a1, 0x0203a4a1, 0x0203a4a1, 0x0203a4a1, 0x0203a4a1,
    0x0203a4a5, 0x0203a4a5, 0x0203a4a5, 0x0203a4a5, 0x0203a4a5,
    0x0203a5a5, 0x0203a5a5, 0x0203a5a5, 0x0203a5a5, 0x0203a5a5,
    0x0203ada5, 0x0203ada5, 0x0203ada5, 0x0203ada5, 0x0203ada5, 0x0203ada5,
    0x0203ada7, 0x0203ada7, 0x0203ada7, 0x0203ada7, 0x0203ada7,
    0x0203adaf, 0x0203adaf, 0x0203adaf, 0x0203adaf, 0x0203adaf,
    0x0203afaf, 0x0203afaf, 0x0203afaf, 0x0203afaf, 0x0203afaf,
    0x0203df5f, 0x0203df5f, 0x0203df5f, 0x0203df5f, 0x0203df5f, 0x0203df5f,
    0x0203df7f, 0x0203df7f, 0x0203df7f, 0x0203df7f, 0x0203df7f,
    0x0203dfff, 0x0203dfff, 0x0203dfff, 0x0203dfff, 0x0203dfff,
    0x0203ffff, 0x0203ffff, 0x0203ffff, 0x0203ffff, 0x0203ffff,
};

static uint8_t level_2bpp_to_8bpp[4] = {
    0, 85, 171, 255
};

static uint8_t rgb_to_grayscale(uint8_t r, uint8_t g, uint8_t b) {
    return (uint8_t)((CAMMY_IMAGE_Y_COEFFICIENT_R * (float)r)
                   + (CAMMY_IMAGE_Y_COEFFICIENT_G * (float)g)
                   + (CAMMY_IMAGE_Y_COEFFICIENT_B * (float)b));
}

static uint8_t rgb_to_tile_2bpp(uint8_t r, uint8_t g, uint8_t b, size_t x, size_t y) {
    uint8_t gray = rgb_to_grayscale(r, g, b);

    uint32_t slot = bayer_matrix[gray];
     uint8_t from = (slot & 0x03000000) >> 24;
     uint8_t to   = (slot & 0x00030000) >> 16;

    return (slot & (0x8000 >> ((y & 3) << 2) >> (x & 3)))? to: from;
}

static inline uint8_t tile_read(cammy_tile *tiles,
                                size_t x,
                                size_t y,
                                int stride) {
    cammy_tile *tile = CAMMY_TILE_INDEXED(tiles, x, y, stride);

    int tile_x = x & 7,
        tile_y = y & 7;

    return
        ((tile->data[ tile_y<<1]    & (0x80 >> tile_x)) >> (tile_x ^ 7)
      | ((tile->data[(tile_y<<1)|1] & (0x80 >> tile_x)) >> (tile_x ^ 7) << 1));
}

static inline void tile_write(cammy_tile *tiles,
                              size_t x,
                              size_t y,
                              int stride,
                              uint8_t value) {
    cammy_tile *tile = CAMMY_TILE_INDEXED(tiles, x, y, stride);

    int tile_x = x & 7,
        tile_y = y & 7;

    tile->data[ tile_y<<1]    &= ~(1 << (tile_x ^ 7));
    tile->data[(tile_y<<1)|1] &= ~(1 << (tile_x ^ 7));

    tile->data[ tile_y<<1]    |=  (value & 0x01)       << (tile_x ^ 7);
    tile->data[(tile_y<<1)|1] |= ((value & 0x02) >> 1) << (tile_x ^ 7);
}

static inline void buf_read(uint8_t *buf,
                            size_t x, size_t y, size_t stride,
                            uint8_t *r, uint8_t *g, uint8_t *b,
                            int depth) {
    *r = buf[depth*stride*y + depth*x],
    *g = buf[depth*stride*y + depth*x + 1],
    *b = buf[depth*stride*y + depth*x + 2];
}

static inline void buf_write(uint8_t *buf,
                             size_t x, size_t y, size_t stride,
                             uint8_t r, uint8_t g, uint8_t b,
                             int depth) {
    buf[depth*stride*y + depth*x]     = r;
    buf[depth*stride*y + depth*x + 1] = g;
    buf[depth*stride*y + depth*x + 2] = b;
}

cammy_image *cammy_image_new(cammy_image_format format,
                             size_t width,
                             size_t height) {
    cammy_image *image;
    size_t size;

    switch (format) {
        case CAMMY_IMAGE_TILE: {
            size_t tiles_width  = width  >> 3,
                   tiles_height = height >> 3;

            if (width  & 7) tiles_width++;
            if (height & 7) tiles_height++;

            size = CAMMY_TILE_SIZE * tiles_width * tiles_height;

            break;
        }

        case CAMMY_IMAGE_RGB: {
            size = 3 * width * height;
            break;
        }

        case CAMMY_IMAGE_RGBA: {
            size = 4 * width * height;
            break;
        }

        default: {
            errno = EINVAL;
            goto error_invalid_format;
        }
    }

    if ((image = malloc(sizeof(*image))) == NULL) {
        goto error_malloc_image;
    }

    if ((image->buf = malloc(size)) == NULL) {
        goto error_malloc_buf;
    }

    image->format = format;
    image->size   = size;
    image->width  = width;
    image->height = height;

    return image;

error_malloc_buf:
    free(image);

error_malloc_image:
error_invalid_format:
    return NULL;
}

cammy_image *cammy_image_open_tile(const char *filename,
                                   size_t width,
                                   size_t height) {
    cammy_image *image;
    int fd;

    size_t off;

    if ((fd = open(filename, O_RDONLY)) < 0) {
        goto error_open;
    }

    if ((image = cammy_image_new(CAMMY_IMAGE_TILE, width, height)) == NULL) {
        goto error_image_new;
    }

    for (off=0; off<image->size;) {
        ssize_t len;

        if ((len = read(fd, &image->buf[off], CAMMY_IMAGE_CHUNK_SIZE)) < 0) {
            goto error_read;
        }

        off += len;
    }

    close(fd);

    return image;

error_read:
    cammy_image_destroy(image);

error_image_new:
    close(fd);

error_open:
    return NULL;
}

void cammy_image_close(cammy_image *image) {
    free(image);
}

void cammy_image_destroy(cammy_image *image) {
    free(image->buf);
    free(image);
}

int cammy_image_save_tile(cammy_image *image, const char *filename) {
    size_t off,
           rest = image->size;

    int fd;

    if ((fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0644)) < 0) {
        goto error_open;
    }

    for (off=0; off<image->size; off+=CAMMY_IMAGE_CHUNK_SIZE) {
        size_t chunk = rest > CAMMY_IMAGE_CHUNK_SIZE?
                              CAMMY_IMAGE_CHUNK_SIZE: rest;

        ssize_t len;

        if ((len = write(fd, image->buf + off, chunk)) < 0) {
            goto error_write;
        }

        rest -= len;
    }

    close(fd);

    return 0;

error_write:
    close(fd);

error_open:
    return -1;
}

void cammy_image_dither(uint8_t *dest,
                        uint8_t *src,
                        size_t width,
                        size_t height,
                        int depth,
                        cammy_tile_palette *palette) {
    size_t x, y;

    for (y=0; y<height; y++) {
        for (x=0; x<width; x++) {
            uint8_t r, g, b, value;

            buf_read(src, x, y, width, &r, &g, &b, depth);

            value = rgb_to_tile_2bpp(r, g, b, x, y);

            r = palette->colors[value][0];
            g = palette->colors[value][1];
            b = palette->colors[value][2];

            buf_write(dest, x, y, width, r, g, b, 3);
        }
    }
}

void cammy_image_split_to_tiles(cammy_tile *destr,
                                cammy_tile *destg,
                                cammy_tile *destb,
                                uint8_t *src,
                                size_t width,
                                size_t height,
                                int depth) {
    size_t x, y;

    for (y=0; y<height; y++) {
        for (x=0; x<width; x++) {
            uint8_t r, g, b;

            buf_read(src, x, y, width, &r, &g, &b, depth);

            tile_write(destr, x, y, width, rgb_to_tile_2bpp(r, r, r, x, y) ^ 3);
            tile_write(destg, x, y, width, rgb_to_tile_2bpp(g, g, g, x, y) ^ 3);
            tile_write(destb, x, y, width, rgb_to_tile_2bpp(b, b, b, x, y) ^ 3);
        }
    }
}

void cammy_image_copy_from_tile(uint8_t *dest,
                                cammy_tile *src,
                                size_t width,
                                size_t height,
                                int depth,
                                cammy_tile_palette *palette) {
    size_t x, y;

    for (y=0; y<height; y++) {
        for (x=0; x<width; x++) {
            uint8_t value = tile_read(src, x, y, width) ^ 3;

            buf_write(dest, x, y, width, palette->colors[value][0],
                                         palette->colors[value][1],
                                         palette->colors[value][2], depth);
        }
    }
}

void cammy_image_dither_to_tile(cammy_tile *dest,
                                uint8_t *src,
                                size_t width,
                                size_t height,
                                int depth) {
    size_t x, y;

    memset(dest, '\x00', CAMMY_TILE_SIZE * (width >> 3) * (height >> 3));

    for (y=0; y<height; y++) {
        for (x=0; x<width; x++) {
            uint8_t r, g, b;

            buf_read(src, x, y, width, &r, &g, &b, depth);

            tile_write(dest, x, y, width, rgb_to_tile_2bpp(r, g, b, x, y) ^ 3);
        }
    }
}

void cammy_image_merge_tiles(uint8_t *dest,
                             cammy_tile *srcr,
                             cammy_tile *srcg,
                             cammy_tile *srcb,
                             size_t width,
                             size_t height,
                             int depth) {
    size_t x, y;

    for (y=0; y<height; y++) {
        for (x=0; x<width; x++) {
            uint8_t r = tile_read(srcr, x, y, width) ^ 3,
                    g = tile_read(srcg, x, y, width) ^ 3,
                    b = tile_read(srcb, x, y, width) ^ 3;

            buf_write(dest, x, y, width, level_2bpp_to_8bpp[r],
                                         level_2bpp_to_8bpp[g],
                                         level_2bpp_to_8bpp[b], depth);
        }
    }
}

void cammy_image_copy(cammy_image *dest,
                      cammy_image *src,
                      size_t x_dest,
                      size_t y_dest,
                      size_t x_src,
                      size_t y_src,
                      size_t width,
                      size_t height) {
    size_t x_offset,
           y_offset;

    if (dest->format != CAMMY_IMAGE_TILE || src->format != CAMMY_IMAGE_TILE) {
        return;
    }

    for (y_offset=0; y_offset<height; y_offset++) {
        if (y_dest + y_offset > dest->height) {
            break;
        }

        for (x_offset=0; x_offset<width; x_offset++) {
            uint8_t value = tile_read(src->tiles, x_src + x_offset,
                                                  y_src + y_offset,
                                                  src->width);

            if (x_dest + x_offset > dest->width) {
                break;
            }

            tile_write(dest->tiles, x_dest + x_offset,
                                    y_dest + y_offset,
                                    dest->width,
                                    value);
        }
    }
}