#include "luaharfbuzz.h"

#ifdef LuajitTeX

static int lua_absindex (lua_State *L, int i) {
  if (i < 0 && i > LUA_REGISTRYINDEX)
    i += lua_gettop(L) + 1;
  return i;
}
static void lua_seti (lua_State *L, int index, lua_Integer i) {
  index = lua_absindex(L, index);
  lua_pushinteger(L, i);
  lua_insert(L, -2);
  lua_settable(L, index);
}
#endif

/* Size of static arrays we use to avoid heap allocating memory when reading
 * data from HarfBuzz. */
#define STATIC_ARRAY_SIZE 128

static int face_new(lua_State *L) {
  Face *f;
  hb_blob_t *blob;
  hb_face_t *face;
  const char *file_name = luaL_checkstring(L, 1);
  unsigned int face_index = (unsigned int) luaL_optinteger(L, 2, 0);

  blob = hb_blob_create_from_file(file_name);
  face = hb_face_create(blob, face_index);

  if (blob == hb_blob_get_empty() || face == hb_face_get_empty()) {
    lua_pushnil(L);
  } else {
    f = (Face *)lua_newuserdata(L, sizeof(*f));
    *f = face;
    luaL_getmetatable(L, "harfbuzz.Face");
    lua_setmetatable(L, -2);
  }
  return 1;
}

static int face_new_from_blob(lua_State *L) {
  Face *f;
  hb_face_t *face;
  Blob *blob = luaL_checkudata(L, 1, "harfbuzz.Blob");
  unsigned int face_index = (unsigned int) luaL_optinteger(L, 2, 0);

  face = hb_face_create(*blob, face_index);

  if (*blob == hb_blob_get_empty() || face == hb_face_get_empty()) {
    lua_pushnil(L);
  } else {
    f = (Face *)lua_newuserdata(L, sizeof(*f));
    *f = face;
    luaL_getmetatable(L, "harfbuzz.Face");
    lua_setmetatable(L, -2);
  }
  return 1;
}

static int face_get_glyph_count(lua_State *L) {
  Face *f = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");

  lua_pushinteger(L, hb_face_get_glyph_count(*f));
  return 1;
}

static int face_get_name(lua_State *L) {
  Face *f = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");
  hb_ot_name_id_t name_id = (hb_ot_name_id_t) luaL_checkinteger(L, 2);
  hb_language_t lang = HB_LANGUAGE_INVALID;
  char name[STATIC_ARRAY_SIZE];
  unsigned int text_size = STATIC_ARRAY_SIZE, len;

  if (lua_gettop(L) > 2)
    lang = *((Language*)luaL_checkudata(L, 3, "harfbuzz.Language"));

  len = hb_ot_name_get_utf8(*f, name_id, lang, &text_size, name);
  if (len) {
    if (len < STATIC_ARRAY_SIZE) {
      lua_pushstring(L, name);
    } else {
      char *name = malloc(len + 1);
      text_size = len + 1;
      hb_ot_name_get_utf8(*f, name_id, lang, &text_size, name);
      lua_pushstring(L, name);
      free(name);
    }
  } else {
    lua_pushnil(L);
  }

  return 1;
}

static int face_get_table(lua_State *L) {
  Face *f = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");
  Tag *t = (Tag *)luaL_checkudata(L, 2, "harfbuzz.Tag");
  Blob *b;

  b = (Blob *)lua_newuserdata(L, sizeof(*b));
  luaL_getmetatable(L, "harfbuzz.Blob");
  lua_setmetatable(L, -2);

  *b = hb_face_reference_table(*f, *t);

  return 1;
}

static int face_get_table_tags(lua_State *L) {
  Face *f = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");
  hb_tag_t tags[STATIC_ARRAY_SIZE];
  unsigned int count = hb_face_get_table_tags(*f, 0, NULL, NULL);

  if (count) {
    unsigned int i = 0, offset = 0;
    lua_createtable(L, count, 0);
    do {
      count = STATIC_ARRAY_SIZE;
      hb_face_get_table_tags(*f, offset, &count, tags);
      for (i = 0; i < count; i++) {
        lua_pushnumber(L, offset + i + 1);

        Tag *tp = (Tag *)lua_newuserdata(L, sizeof(*tp));
        luaL_getmetatable(L, "harfbuzz.Tag");
        lua_setmetatable(L, -2);
        *tp = tags[i];

        lua_rawset(L, -3);
      }
      offset += count;
    } while (count == STATIC_ARRAY_SIZE);
  } else {
    lua_pushnil(L);
  }

  return 1;
}

static int face_ot_layout_get_script_tags(lua_State *L) {
  Face *face = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");
  Tag *table = (Tag *)luaL_checkudata(L, 2, "harfbuzz.Tag");

  hb_tag_t tags[STATIC_ARRAY_SIZE];
  unsigned int count = hb_ot_layout_table_get_script_tags(*face, *table, 0, NULL, NULL);

  if (count) {
    unsigned int i = 0, offset = 0;
    lua_createtable(L, count, 0);
    do {
      count = STATIC_ARRAY_SIZE;
      hb_ot_layout_table_get_script_tags(*face, *table, offset, &count, tags);
      for (i = 0; i < count; i++) {
        lua_pushnumber(L, offset + i + 1);

        Tag *tp = (Tag *)lua_newuserdata(L, sizeof(*tp));
        luaL_getmetatable(L, "harfbuzz.Tag");
        lua_setmetatable(L, -2);
        *tp = tags[i];

        lua_rawset(L, -3);
      }
      offset += count;
    } while (count == STATIC_ARRAY_SIZE);
  } else {
    lua_pushnil(L);
  }

  return 1;
}

static int face_ot_layout_get_language_tags(lua_State *L) {
  Face *face = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");
  Tag *table = (Tag *)luaL_checkudata(L, 2, "harfbuzz.Tag");
  unsigned int script_index = (unsigned int) luaL_checkinteger(L, 3);

  hb_tag_t tags[STATIC_ARRAY_SIZE];
  unsigned int count = hb_ot_layout_script_get_language_tags(*face, *table, script_index, 0, NULL, NULL);

  if (count) {
    unsigned int i = 0, offset = 0;
    lua_createtable(L, count, 0);
    do {
      count = STATIC_ARRAY_SIZE;
      hb_ot_layout_script_get_language_tags(*face, *table, script_index, offset, &count, tags);
      for (i = 0; i < count; i++) {
        lua_pushnumber(L, offset + i + 1);

        Tag *tp = (Tag *)lua_newuserdata(L, sizeof(*tp));
        luaL_getmetatable(L, "harfbuzz.Tag");
        lua_setmetatable(L, -2);
        *tp = tags[i];

        lua_rawset(L, -3);
      }
      offset += count;
    } while (count == STATIC_ARRAY_SIZE);
  } else {
    lua_pushnil(L);
  }

  return 1;
}

static int face_ot_layout_get_feature_tags(lua_State *L) {
  Face *face = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");
  Tag *table = (Tag *)luaL_checkudata(L, 2, "harfbuzz.Tag");
  unsigned int script_index = (unsigned int) luaL_checkinteger(L, 3);
  unsigned int language_index = (unsigned int) luaL_checkinteger(L, 4);

  hb_tag_t tags[STATIC_ARRAY_SIZE];
  unsigned int count = hb_ot_layout_language_get_feature_tags(*face, *table, script_index, language_index, 0, NULL, NULL);

  if (count) {
    unsigned int i = 0, offset = 0;
    lua_createtable(L, count, 0);
    do {
      count = STATIC_ARRAY_SIZE;
      hb_ot_layout_language_get_feature_tags(*face, *table, script_index, language_index, offset, &count, tags);
      for (i = 0; i < count; i++) {
        lua_pushnumber(L, offset + i + 1);

        Tag *tp = (Tag *)lua_newuserdata(L, sizeof(*tp));
        luaL_getmetatable(L, "harfbuzz.Tag");
        lua_setmetatable(L, -2);
        *tp = tags[i];

        lua_rawset(L, -3);
      }
      offset += count;
    } while (count == STATIC_ARRAY_SIZE);
  } else {
    lua_pushnil(L);
  }

  return 1;
}

static int face_ot_layout_find_script(lua_State *L) {
  Face *face = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");
  Tag *table = (Tag *)luaL_checkudata(L, 2, "harfbuzz.Tag");
  Tag *script = (Tag *)luaL_checkudata(L, 3, "harfbuzz.Tag");
  unsigned int index = 0;

  int found = hb_ot_layout_table_find_script(*face, *table, *script, &index);
  lua_pushboolean(L, found);
  lua_pushinteger(L, index);
  return 2;
}

static int face_ot_layout_find_language(lua_State *L) {
  Face *face = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");
  Tag *table = (Tag *)luaL_checkudata(L, 2, "harfbuzz.Tag");
  unsigned int script_index = (unsigned int) luaL_checkinteger(L, 3);
  Tag *language = (Tag *)luaL_checkudata(L, 4, "harfbuzz.Tag");
  unsigned int index = 0;

  int found = hb_ot_layout_script_select_language(*face, *table, script_index, 1, &(*language), &index);
  lua_pushboolean(L, found);
  lua_pushinteger(L, index);
  return 2;
}

static int face_ot_layout_find_feature(lua_State *L) {
  Face *face = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");
  Tag *table = (Tag *)luaL_checkudata(L, 2, "harfbuzz.Tag");
  unsigned int script_index = (unsigned int) luaL_checkinteger(L, 3);
  unsigned int language_index = (unsigned int) luaL_checkinteger(L, 4);
  Tag *feature = (Tag *)luaL_checkudata(L, 5, "harfbuzz.Tag");
  unsigned int index = 0;

  int found = hb_ot_layout_language_find_feature(*face, *table, script_index, language_index, *feature, &index);
  lua_pushboolean(L, found);
  lua_pushinteger(L, index);
  return 2;
}

static int face_collect_unicodes(lua_State *L) {
  Face *f = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");
  hb_set_t *codes = hb_set_create();

  hb_face_collect_unicodes (*f, codes);
  lua_createtable(L, hb_set_get_population(codes), 0);
  if (!hb_set_is_empty(codes)) {
    unsigned int i = 0;
    hb_codepoint_t c = HB_SET_VALUE_INVALID;

    while (hb_set_next(codes, &c)) {
      lua_pushnumber(L, ++i);
      lua_pushnumber(L, c);
      lua_rawset(L, -3);
    }
  }

  hb_set_destroy(codes);

  return 1;
}

static int face_get_upem(lua_State *L) {
  Face *f = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");

  lua_pushinteger(L, hb_face_get_upem(*f));
  return 1;
}

static int face_ot_color_has_palettes(lua_State *L) {
  Face *f = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");

  lua_pushboolean(L, hb_ot_color_has_palettes(*f));
  return 1;
}

static int face_ot_color_palette_get_count(lua_State *L) {
  Face *f = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");

  lua_pushinteger(L, hb_ot_color_palette_get_count(*f));
  return 1;
}

static int face_ot_color_palette_get_colors(lua_State *L) {
  Face *f = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");
  unsigned int index = (unsigned int) luaL_optinteger(L, 2, 1) - 1;

  hb_color_t colors[STATIC_ARRAY_SIZE];
  unsigned int count = hb_ot_color_palette_get_colors(*f, index, 0, NULL, NULL);

  if (count) {
    unsigned int i = 0, offset = 0;
    lua_createtable(L, count, 0); // parent table
    do {
      count = STATIC_ARRAY_SIZE;
      hb_ot_color_palette_get_colors(*f, index, offset, &count, colors);
      for (i = 0; i < count; i++) {
        hb_color_t color = colors[i];

        lua_pushnumber(L, offset + i + 1); // 1-indexed key parent table
        lua_createtable(L, 0, 4); // child table

        lua_pushinteger(L, hb_color_get_red(color));
        lua_setfield(L, -2, "red");

        lua_pushinteger(L, hb_color_get_green(color));
        lua_setfield(L, -2, "green");

        lua_pushinteger(L, hb_color_get_blue(color));
        lua_setfield(L, -2, "blue");

        lua_pushinteger(L, hb_color_get_alpha(color));
        lua_setfield(L, -2, "alpha");

        lua_settable(L, -3); // Add child table at index i+1 to parent table
      }
      offset += count;
    } while (count == STATIC_ARRAY_SIZE);
  } else {
    lua_pushnil(L);
  }

  return 1;
}

static int face_ot_color_has_layers(lua_State *L) {
  Face *f = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");

  lua_pushboolean(L, hb_ot_color_has_layers(*f));
  return 1;
}

static int face_ot_color_glyph_get_layers(lua_State *L) {
  Face *f = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");
  hb_codepoint_t gid = (hb_codepoint_t) luaL_checkinteger(L, 2);
  hb_ot_color_layer_t layers[STATIC_ARRAY_SIZE];
  unsigned int count = hb_ot_color_glyph_get_layers(*f, gid, 0, NULL, NULL);

  if (count) {
    unsigned int i = 0, offset = 0;
    lua_createtable(L, count, 0); // parent table
    do {
      count = STATIC_ARRAY_SIZE;
      hb_ot_color_glyph_get_layers(*f, gid, offset, &count, layers);
      for (i = 0; i < count; i++) {
        hb_ot_color_layer_t layer = layers[i];
        unsigned int color_index = layer.color_index;
        if (color_index != 0xFFFF)
          color_index++; // make it 1-indexed

        lua_pushnumber(L, offset + i + 1);  // 1-indexed key parent table
        lua_createtable(L, 0, 2); // child table

        lua_pushinteger(L, layer.glyph);
        lua_setfield(L, -2, "glyph");

        lua_pushinteger(L, color_index);
        lua_setfield(L, -2, "color_index");

        lua_settable(L, -3); // Add child table at index i+1 to parent table
      }
      offset += count;
    } while (count == STATIC_ARRAY_SIZE);
  } else {
    lua_pushnil(L);
  }

  return 1;
}

static int face_ot_color_has_png(lua_State *L) {
  Face *f = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");

  lua_pushboolean(L, hb_ot_color_has_png(*f));
  return 1;
}

static int face_ot_color_has_svg(lua_State *L) {
  Face *f = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");

  lua_pushboolean(L, hb_ot_color_has_svg(*f));
  return 1;
}

static int face_ot_color_glyph_get_svg(lua_State *L) {
  Face *f = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");
  hb_codepoint_t gid = (hb_codepoint_t) luaL_checkinteger(L, 2);
  hb_blob_t* blob = hb_ot_color_glyph_reference_svg(*f, gid);

  if (hb_blob_get_length(blob) != 0) {
    Blob *b = (Blob *)lua_newuserdata(L, sizeof(*b));
    luaL_getmetatable(L, "harfbuzz.Blob");
    lua_setmetatable(L, -2);

    *b = blob;
  } else {
    lua_pushnil(L);
  }

  return 1;
}

static int face_var_has_data(lua_State *L) {
  Face *f = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");

  lua_pushboolean(L, hb_ot_var_has_data(*f));
  return 1;
}

static int push_axis_info(lua_State *L, const hb_ot_var_axis_info_t *info) {
  lua_createtable(L, 0, 7);

  lua_pushinteger(L, info->axis_index + 1);
  lua_setfield(L, -2, "axis_index");

  Tag *tp = (Tag *)lua_newuserdata(L, sizeof(*tp));
  luaL_getmetatable(L, "harfbuzz.Tag");
  lua_setmetatable(L, -2);
  *tp = info->tag;
  lua_setfield(L, -2, "tag");

  lua_pushinteger(L, info->name_id);
  lua_setfield(L, -2, "name_id");

  lua_pushinteger(L, info->flags);
  lua_setfield(L, -2, "flags");

  lua_pushnumber(L, info->min_value);
  lua_setfield(L, -2, "min_value");

  lua_pushnumber(L, info->default_value);
  lua_setfield(L, -2, "default_value");

  lua_pushnumber(L, info->max_value);
  lua_setfield(L, -2, "max_value");

  return 1;
}

static int face_var_find_axis_info(lua_State *L) {
  Face *face = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");
  Tag *tag = (Tag *)luaL_checkudata(L, 2, "harfbuzz.Tag");
  hb_ot_var_axis_info_t axis_info;

  if (hb_ot_var_find_axis_info(*face, *tag, &axis_info))
    push_axis_info(L, &axis_info);
  else
    lua_pushnil(L);
  return 1;
}

static int face_var_get_axis_infos(lua_State *L) {
  Face *face = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");
  lua_Integer start = luaL_optinteger(L, 2, 1) - 1;
  lua_Integer stop = luaL_optinteger(L, 2, -1);
  if (start < -1)
    start += hb_ot_var_get_axis_count(*face) + 1;
  if (stop < 0)
    stop += hb_ot_var_get_axis_count(*face) + 1;

  if (start < 0 || stop - start > STATIC_ARRAY_SIZE)
    lua_pushnil(L);
  else if (stop <= start)
    lua_createtable(L, 0, 0);
  else {
    unsigned int count = stop - start;
    hb_ot_var_axis_info_t axis_infos[STATIC_ARRAY_SIZE];

    hb_ot_var_get_axis_infos(*face, start, &count, axis_infos);

    lua_createtable(L, count, 0);
    for (int i = 0; i != count; i++) {
      push_axis_info(L, axis_infos + i);
      lua_seti(L, -2, i + 1);
    }
  }
  return 1;
}

static int face_var_named_instance_get_infos(lua_State *L) {
  Face *face = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");
  lua_Integer start = luaL_optinteger(L, 2, 1) - 1;
  lua_Integer stop = luaL_optinteger(L, 2, -1);

  unsigned int total = hb_ot_var_get_named_instance_count(*face);

  if (start < -1)
    start += total + 1;
  if (stop < 0)
    stop += total + 1;
  if (stop > total)
    stop = total;

  if (start < 0)
    lua_pushnil(L);
  else if (stop <= start)
    lua_createtable(L, 0, 0);
  else {
    lua_createtable(L, stop - start, 0);
    for (int i = start; i != stop; i++) {
      lua_createtable(L, 0, 3);

      lua_pushinteger(L, i + 1);
      lua_setfield(L, -2, "index");

      lua_pushinteger(L, hb_ot_var_named_instance_get_subfamily_name_id(*face, i));
      lua_setfield(L, -2, "subfamily_name_id");

      lua_pushinteger(L, hb_ot_var_named_instance_get_subfamily_name_id(*face, i));
      lua_setfield(L, -2, "subfamily_name_id");

      lua_pushinteger(L, hb_ot_var_named_instance_get_postscript_name_id(*face, i));
      lua_setfield(L, -2, "postscript_name_id");

      lua_seti(L, -2, i - start + 1);
    }
  }
  return 1;
}

static int face_var_named_instance_get_design_coords(lua_State *L) {
  Face *face = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");
  lua_Integer index = luaL_checkinteger(L, 2) - 1;

  float coords[STATIC_ARRAY_SIZE];
  unsigned int count = STATIC_ARRAY_SIZE;
  count = hb_ot_var_named_instance_get_design_coords(*face, index, &count, coords);

  for (int i = 0; i != count; i++) {
    lua_pushnumber(L, coords[i]);
  }
  return count;
}

static int face_var_normalize_variations(lua_State *L) {
  Face *face = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");
  unsigned int count = lua_gettop(L)-1;
  if (count > STATIC_ARRAY_SIZE) {
    return 0;
  }

  Variation variations[STATIC_ARRAY_SIZE];
  for (unsigned int i = 0; i != count; i++)
    variations[i] = *(Variation *)luaL_checkudata(L, i+2, "harfbuzz.Variation");

  unsigned int coord_count = hb_ot_var_get_axis_count(*face);
  int normalized[STATIC_ARRAY_SIZE];
  hb_ot_var_normalize_variations(*face, variations, count, normalized, coord_count);

  for (int i = 0; i != coord_count; i++) {
    lua_pushinteger(L, normalized[i]);
  }
  return coord_count;
}

static int face_var_normalize_coords(lua_State *L) {
  Face *face = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");
  unsigned int count = lua_gettop(L)-1;
  if (count > STATIC_ARRAY_SIZE) {
    return 0;
  }

  float coords[STATIC_ARRAY_SIZE];
  for (unsigned int i = 0; i != count; i++)
    coords[i] = luaL_checknumber(L, i+2);

  int normalized[STATIC_ARRAY_SIZE];
  hb_ot_var_normalize_coords(*face, count, coords, normalized);

  for (int i = 0; i != count; i++) {
    lua_pushinteger(L, normalized[i]);
  }
  return count;
}

static int face_destroy(lua_State *L) {
  Face *f = (Face *)luaL_checkudata(L, 1, "harfbuzz.Face");

  hb_face_destroy(*f);
  return 0;
}

static const struct luaL_Reg face_methods[] = {
  { "__gc", face_destroy },
  { "collect_unicodes", face_collect_unicodes },
  { "get_glyph_count", face_get_glyph_count },
  { "get_name", face_get_name },
  { "get_table", face_get_table },
  { "get_table_tags", face_get_table_tags },
  { "get_upem", face_get_upem },
  { "ot_color_has_palettes", face_ot_color_has_palettes },
  { "ot_color_palette_get_count", face_ot_color_palette_get_count },
  { "ot_color_palette_get_colors", face_ot_color_palette_get_colors },
  { "ot_color_has_layers", face_ot_color_has_layers },
  { "ot_color_glyph_get_layers", face_ot_color_glyph_get_layers },
  { "ot_color_has_png", face_ot_color_has_png },
  { "ot_color_has_svg", face_ot_color_has_svg },
  { "ot_color_glyph_get_svg", face_ot_color_glyph_get_svg },
  { "ot_layout_get_script_tags", face_ot_layout_get_script_tags },
  { "ot_layout_get_language_tags", face_ot_layout_get_language_tags },
  { "ot_layout_get_feature_tags", face_ot_layout_get_feature_tags },
  { "ot_layout_find_script", face_ot_layout_find_script },
  { "ot_layout_find_language", face_ot_layout_find_language },
  { "ot_layout_find_feature", face_ot_layout_find_feature },
  { "ot_var_has_data", face_var_has_data },
  { "ot_var_find_axis_info", face_var_find_axis_info },
  { "ot_var_get_axis_infos", face_var_get_axis_infos },
  { "ot_var_named_instance_get_infos", face_var_named_instance_get_infos },
  { "ot_var_named_instance_get_design_coords", face_var_named_instance_get_design_coords },
  { "ot_var_normalize_variations", face_var_normalize_variations },
  { "ot_var_normalize_coords", face_var_normalize_coords },
  { NULL, NULL }
};

static const struct luaL_Reg face_functions[] = {
  { "new", face_new },
  { "new_from_blob", face_new_from_blob },
  { NULL,  NULL }
};

int register_face(lua_State *L) {
  return register_class(L, "harfbuzz.Face", face_methods, face_functions, NULL);
}
