/*
    See license.txt in the root of this project.
*/

# include <luametatex.h>
# include "utilities/auxbytemaps.h"

/*tex See |lmtinterface.h| for |BYTEMAP_METATABLE_INSTANCE|. */

typedef enum bytemap_loop {
    bytemap_loop_xy, 
    bytemap_loop_yx,
} bytemap_loop;

# define first_bytemap_loop bytemap_loop_xy
# define last_bytemap_loop  bytemap_loop_yx

static inline bytemap_data * bytemaplib_aux_valid(lua_State *L, int i)
{
    bytemap_data * bytemap = lua_touserdata(L, i);
    if (bytemap && lua_getmetatable(L, i)) {
        lua_get_metatablelua(bytemap_instance);
        if (! lua_rawequal(L, -1, -2)) {
           bytemap = NULL;
        }
        lua_pop(L, 2);
    }
    return bytemap;
}

bytemap_data * bytemaplib_valid(lua_State *L, int i) /* the public one */
{
    return lua_type(L, i) == LUA_TUSERDATA ? bytemaplib_aux_valid(L, i) : NULL;
}

static inline int bytemaplib_new(lua_State *L)
{
    int nx = lmt_tointeger(L, 1);
    int ny = lmt_optinteger(L, 2, nx);
    int nz = lmt_optinteger(L, 3, 1);
    int nn = nx * ny * nz;
    if (bytemap_okay(nx, ny, nz)) {
        if (nn > 0) { 
            bytemap_data * bytemap = lua_newuserdatauv(L, sizeof(bytemap_data), 0);
            if (bytemap) { 
                lua_get_metatablelua(bytemap_instance);
                lua_setmetatable(L, -2);
                bytemap_allocate(bytemap, nx, ny, nz, NULL);
            }
        }
        return 1;
    } else { 
        /* -100 100 -1 also reports overflow .. whatever suits */
        tex_formatted_warning("bytemaplib", "new, %s: %i x %i x %i", 
            nn <= 0 ? "invalid" : "overflow", nx, ny, nz);
        return 0;
    }
}

static int bytemaplib_reset(lua_State *L)
{
    bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
    if (bytemap) { 
        bytemap_reset(bytemap, NULL);
    }
    return 0;
}

static int bytemaplib_reduce(lua_State *L)
{
    bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
    if (bytemap) { 
        int method = lmt_optinteger(L, 2, bytemap_reduction_weighted);
        bytemap_reduce(bytemap, method, NULL);
    }
    return 0;
}

static int bytemaplib_slice_gray(lua_State *L)
{
    bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
    if (bytemap) { 
        int slot = 2;
        int x, y, dx, dy, s;
        if (lua_gettop(L) == 2) {
            x = 0;
            y = 0;
            dx = bytemap->nx;
            dy = bytemap->ny;
        } else {
            x = lmt_tointeger(L, slot++);
            y = lmt_tointeger(L, slot++);
            dx = lmt_tointeger(L, slot++);
            dy = lmt_tointeger(L, slot++);
        }
        s = lmt_tointeger(L, slot);
        bytemap_slice_gray(bytemap, x, y, dx, dy, s);
    }
    return 0;
}

static int bytemaplib_slice_rgb(lua_State *L)
{
    bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
    if (bytemap) { 
        int slot = 2;
        int x, y, dx, dy, r, g, b;
        if (lua_gettop(L) == 4) {
            x = 0;
            y = 0;
            dx = bytemap->nx;
            dy = bytemap->ny;
        } else {
            x = lmt_tointeger(L, slot++);
            y = lmt_tointeger(L, slot++);
            dx = lmt_tointeger(L, slot++);
            dy = lmt_tointeger(L, slot++);
        }
        r = lmt_tointeger(L, slot++);
        g = lmt_tointeger(L, slot++);
        b = lmt_tointeger(L, slot);
        bytemap_slice_rgb(bytemap, x, y, dx, dy, r, g, b);
    }
    return 0;
}

static int bytemaplib_slice_range(lua_State *L)
{
    bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
    if (bytemap) { 
        int x = lmt_tointeger(L, 2);
        int y = lmt_tointeger(L, 3);
        int dx = lmt_tointeger(L, 4);
        int dy = lmt_tointeger(L, 5);
        int min = lmt_tointeger(L, 6);
        int max = lmt_tointeger(L, 7);
        bytemap_slice_range(bytemap, x, y, dx, dy, min, max);
    }
    return 0;
}

static int bytemaplib_bounds(lua_State *L)
{
    bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
    if (bytemap) { 
        int value = lmt_tointeger(L, 2);
        int llx = 0;
        int lly = 1;
        int urx = bytemap->nx - 1;
        int ury = bytemap->ny - 1;
        bytemap_bounds(bytemap, value, &llx, &lly, &urx, &ury, 0);
        lua_pushinteger(L, llx);
        lua_pushinteger(L, lly);
        lua_pushinteger(L, urx);
        lua_pushinteger(L, ury);
        return 4;
    }
    return 0;
}

static int bytemaplib_clip(lua_State *L)
{
    bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
    if (bytemap) {
        int value = lmt_tointeger(L, 2);
        bytemap_clip(bytemap, value, NULL);
    }
    return 0;
}

static int bytemaplib_copy(lua_State *L)
{
    bytemap_data *source = bytemaplib_aux_valid(L, 1);
    if (source) { 
        bytemap_data *target = NULL;
        if (lua_gettop(L) > 1) {
            target = bytemaplib_aux_valid(L, 2);
        } else { 
            target = lua_newuserdatauv(L, sizeof(bytemap_data), 0);
            if (target) { 
                lua_get_metatablelua(bytemap_instance);
                lua_setmetatable(L, -2);
                bytemap_wipe(target);
            }
        }
        if (target) { 
            bytemap_copy(source, target, NULL);
            return 1;
        }
    }
    return 0;
}

static int bytemaplib_has_byte_gray(lua_State *L)
{
    bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
    if (bytemap) { 
        int s = lmt_tointeger(L, 2);
        lua_pushboolean(L, bytemap_has_byte_gray(bytemap, s));
        return 1;
    }
    return 0;
}

static int bytemaplib_has_byte_range(lua_State *L)
{
    bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
    if (bytemap) { 
        int min = lmt_tointeger(L, 2);
        int max = lmt_tointeger(L, 3);
        lua_pushboolean(L, bytemap_has_byte_range(bytemap, min, max));
        return 1;
    }
    return 0;
}

static int bytemaplib_has_byte_rgb(lua_State *L)
{
    bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
    if (bytemap) { 
        int r = lmt_tointeger(L, 2);
        int g = lmt_tointeger(L, 3);
        int b = lmt_tointeger(L, 4);
        lua_pushboolean(L, bytemap_has_byte_rgb(bytemap, r, g, b));
    }
    return 0;
}

static int bytemaplib_set_gray(lua_State *L)
{
    bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
    if (bytemap) { 
        int x = lmt_tointeger(L, 2);
        int y = lmt_tointeger(L, 3);
        int s = lmt_tointeger(L, 4);
        bytemap_set_gray(bytemap, x, y, s);
    }
    return 0;
}

static int bytemaplib_set_rgb(lua_State *L)
{
    bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
    if (bytemap) { 
        int x = lmt_tointeger(L, 2);
        int y = lmt_tointeger(L, 3);
        int r = lmt_tointeger(L, 4);
        int g = lmt_tointeger(L, 5);
        int b = lmt_tointeger(L, 6);
        bytemap_set_rgb(bytemap, x, y, r, g, b);
    }
    return 0;
}

static int bytemaplib_set_gray_add(lua_State *L)
{
    bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
    if (bytemap) { 
        int x = lmt_tointeger(L, 2);
        int y = lmt_tointeger(L, 3);
        int s1 = lmt_tointeger(L, 4);
        int s2 = lmt_tointeger(L, 5);
        int s3 = lmt_tointeger(L, 6);
        bytemap_set_gray_add(bytemap, x, y, s1, s2, s3);
    }
    return 0;
}

static int bytemaplib_set_gray_min(lua_State *L)
{
    bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
    if (bytemap) { 
        int x = lmt_tointeger(L, 2);
        int y = lmt_tointeger(L, 3);
        int s1 = lmt_tointeger(L, 4);
        int s2 = lmt_tointeger(L, 5);
        int s3 = lmt_tointeger(L, 6);
        bytemap_set_gray_min(bytemap, x, y, s1, s2, s3);
    }
    return 0;
}

static int bytemaplib_get_byte(lua_State *L)
{
    bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
    if (bytemap) { 
        int x = lmt_tointeger(L, 2);
        int y = lmt_tointeger(L, 3);
        int z = lmt_tointeger(L, 3);
        lua_pushinteger(L, bytemap_get_byte(bytemap, x, y, z));
        return 1;
    }
    return 0;
}

static int bytemaplib_get_bytes(lua_State *L)
{
    bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
    if (bytemap) { 
        int x = lmt_tointeger(L, 2);
        int y = lmt_tointeger(L, 3);
        unsigned char b1, b2, b3;
        bytemap_get_bytes(bytemap, x, y, &b1, &b2, &b3);
        if (bytemap->nz == 1) { 
            lua_pushinteger(L, b1);
            return 1;
        } else {
            lua_pushinteger(L, b1);
            lua_pushinteger(L, b2);
            lua_pushinteger(L, b3);
            return 3;
        }
    }
    return 0;
}

static int bytemaplib_get_value(lua_State *L)
{
    bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
    if (bytemap) { 
        int nx = 0;
        int ny = 0;
        int nz = 0;
        char * s = bytemap_get_value(bytemap, &nx, &ny, &nz);
        lua_pushlstring(L, s, nx * ny * nz);
        lua_pushinteger(L, nx);
        lua_pushinteger(L, ny);
        lua_pushinteger(L, nz);
        lmt_memory_free(s);
        return 4;
    }
    return 0;
}

/* 
    This one is modelled after the effects octave one but here there is only some gain when we
    want access to the values so when the third argument in true because then we save a call 
    and a userdata lookup (which we can actually speed up anyway). 
*/

/* bytemap function [existing] [looptype] */

static inline unsigned char clipped(int i)
{
    return i < 0 ? 0 : i > 255 ? 255 : (unsigned char) i;
}

static void bytemaplib_aux_loop(lua_State * L,  unsigned char * bytemap, int nx, int ny, int nz, int slot)
{
    if (bytemap) {
        int fn = lua_type(L, slot) == LUA_TFUNCTION;
        if (fn) {
            int existing = lua_toboolean(L, slot + 1) ? nz : 0;
            int loop = lmt_tointeger(L, slot + 2);
            lua_settop(L, slot);
            if (loop == bytemap_loop_yx) {
                unsigned char *p = bytemap;
                for (int y = 0; y < ny; y++) {
                    for (int x = 0; x < nx; x++) {
                        /* we need to retain the function */
                        lua_pushvalue(L, slot);
                        /* pass to function */
                        lua_pushinteger(L, x);
                        lua_pushinteger(L, y);
                        if (existing == 3) {
                            lua_pushinteger(L, (int) *p);
                            lua_pushinteger(L, (int) *(p+1));
                            lua_pushinteger(L, (int) *(p+2));
                        } else if (existing == 1) { 
                            lua_pushinteger(L, (int) *p);
                        }
                        /* call function */
                        if (lua_pcall(L, existing + 2, nz, 0) != 0) { 
                            tex_formatted_warning("bytemaplib", "run bytemap: %s", lua_tostring(L, -1));
                        }
                        /* use results */
                        if (nz == 3) { 
                            *p++ = clipped(lmt_roundnumber(L, -3));
                            *p++ = clipped(lmt_roundnumber(L, -2));
                        }
                        *p++ = clipped(lmt_roundnumber(L, -1));
                        /* wrap up */
                        lua_settop(L, slot);
                    }
                }
            } else {
                unsigned char *p = bytemap;
                for (int x = 0; x < nx; x++) {
                    for (int y = 0; y < ny; y++) {
                        int i = ((ny - y - 1) * nx * nz) + x * nz;
                        /* we need to retain the function */
                        lua_pushvalue(L, slot);
                        /* pass to function */
                        lua_pushinteger(L, x);
                        lua_pushinteger(L, y);
                        if (existing == 3) {
                            lua_pushinteger(L, (int) p[i  ]);
                            lua_pushinteger(L, (int) p[i+1]);
                            lua_pushinteger(L, (int) p[i+2]);
                        } else if (existing == 1) {  
                            lua_pushinteger(L, (int) p[i  ]);
                        }
                        /* call function */
                        if (lua_pcall(L, existing + 2, nz, 0) != 0) { 
                            tex_formatted_warning("bytemaplib", "run bytemap: %s", lua_tostring(L, -1));
                        }
                        /* use results */
                        if (nz == 3) { 
                            p[i++] = clipped(lmt_roundnumber(L, -3)); /* slot + 1 */
                            p[i++] = clipped(lmt_roundnumber(L, -2)); /* slot + 2 */
                        }
                        p[i] = clipped(lmt_roundnumber(L, -1)); /* slot + 1 */
                        /* wrap up */
                        lua_settop(L, slot);
                    }
                }
            }
        }
    }
}

static int bytemaplib_process(lua_State *L)
{
    bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
    if (bytemap && bytemap->data) {
        bytemaplib_aux_loop(L, bytemap->data, bytemap->nx, bytemap->ny, bytemap->nz, 2);
    }
    return 0;
}

static int bytemaplib_downsample(lua_State *L)
{
    bytemap_data *source = bytemaplib_aux_valid(L, 1);
    bytemap_data *target = bytemaplib_aux_valid(L, 2);
    if (source && target) {
        int r = lmt_optinteger(L, 3, 2);
        bytemap_downsample(source, target, r);
    }
    return 0;
}

static int bytemaplib_downgrade(lua_State *L)
{
    bytemap_data *source = bytemaplib_aux_valid(L, 1);
    bytemap_data *target = bytemaplib_aux_valid(L, 2);
    if (source && target) {
        int r = lmt_optinteger(L, 3, 2);
        bytemap_downgrade(source, target, r);
    }
    return 0;
}

int bytemaplib_bytemapped(lua_State * L, unsigned char * bytemap, int nx, int ny, int nz, int slot)
{
    if (bytemap && nx > 0 && ny > 0 && (nz == 1 || nz == 3)) {
        bytemaplib_aux_loop(L, bytemap, nx, ny, nz, slot);
    }
    return 0;
}

static int bytemaplib_tostring(lua_State *L)
{
    bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
    if (bytemap) {
        lua_pushfstring(L, "<bytemap %p : %d %d %d>", bytemap, bytemap->nx, bytemap->ny, bytemap->nz); /* details */
        return 1;
    } else {
        return 0;
    }
}

static int bytemaplib_gc(lua_State *L)
{
    bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
    if (bytemap && bytemap->data) {
        lmt_memory_free(bytemap->data);
    }
    return 0;
}

static int bytemaplib_dimensions(lua_State *L)
{
    bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
    if (bytemap) { 
        lua_pushinteger(L, bytemap->nx);
        lua_pushinteger(L, bytemap->ny);
        lua_pushinteger(L, bytemap->nz);
        return 3;
    } else {
        return 0;
    }
}

// static int bytemaplib_nx(lua_State *L)
// {
//     bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
//     if (bytemap) {
//         lua_pushinteger(L, bytemap->nx);
//         return 1;
//     } else {
//         return 0;
//     }
// }
// 
// static int bytemaplib_ny(lua_State *L)
// {
//     bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
//     if (bytemap) {
//         lua_pushinteger(L, bytemap->ny);
//         return 1;
//     } else {
//         return 0;
//     }
// }
// 
// static int bytemaplib_nz(lua_State *L)
// {
//     bytemap_data *bytemap = bytemaplib_aux_valid(L, 1);
//     if (bytemap) {
//         lua_pushinteger(L, bytemap->nz);
//         return 1;
//     } else {
//         return 0;
//     }
// }

static const luaL_Reg bytemaplib_metatable[] =
{
    { "__tostring", bytemaplib_tostring },
    { "__gc",       bytemaplib_gc       },
 // { "nx",         bytemaplib_nx       },
 // { "ny",         bytemaplib_ny       },
 // { "nz",         bytemaplib_nz       },
    { NULL,         NULL                },
};
 
static struct luaL_Reg bytemaplib_function_list[] = {
    { "new",          bytemaplib_new            },
    { "reset",        bytemaplib_reset          },
    { "reduce",       bytemaplib_reduce         },
    { "slicegray",    bytemaplib_slice_gray     },
    { "slicergb",     bytemaplib_slice_rgb      },
    { "slicerange",   bytemaplib_slice_range    },
    { "bounds",       bytemaplib_bounds         },
    { "clip",         bytemaplib_clip           },
    { "copy",         bytemaplib_copy           },
    { "hasbytegray",  bytemaplib_has_byte_gray  },
    { "hasbyterange", bytemaplib_has_byte_range },
    { "hasbytergb",   bytemaplib_has_byte_rgb   },
    { "setgray",      bytemaplib_set_gray       },
    { "setrgb",       bytemaplib_set_rgb        },
    { "setgrayadd",   bytemaplib_set_gray_add   },
    { "setgraymin",   bytemaplib_set_gray_min   },
    { "getbyte",      bytemaplib_get_byte       },
    { "getbytes",     bytemaplib_get_bytes      },
    { "getvalue",     bytemaplib_get_value      },
    { "process",      bytemaplib_process        },
    { "downsample",   bytemaplib_downsample     },
    { "downgrade",    bytemaplib_downgrade      },
    { "dimensions",   bytemaplib_dimensions     },
    { NULL,           NULL                      },
};

int luaopen_bytemap(lua_State *L)
{
    luaL_newmetatable(L, BYTEMAP_METATABLE_INSTANCE);
    luaL_setfuncs(L, bytemaplib_metatable, 0);
    lua_newtable(L);
    luaL_setfuncs(L, bytemaplib_function_list, 0);
    return 1;
}
