/*
 * Copyright 2018 Helium Systems Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "h3_nif_memory.h"
#include <erl_nif.h>
#include <h3api.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>

static ERL_NIF_TERM ATOM_TRUE;
static ERL_NIF_TERM ATOM_FALSE;

/** Comparison function for used by `h3set_sort()`.
 *
 * The comparison logic such that low resolution (large surface area)
 * indices moved to the beginning of the array. Having large hexagons
 * at the front increase the change of finding an overlap when
 * searching with a lower resolution (smaller) child. Indices with
 * identical resolution compare equal, and their ordering with a block
 * same-resolution indices is not specified.
 */
static int
h3set_sort_compare_fun(void const * lhp, void const * rhp)
{
    H3Index lhs     = *(H3Index *)lhp;
    int     lhs_res = h3GetResolution(lhs);
    H3Index rhs     = *(H3Index *)rhp;
    int     rhs_res = h3GetResolution(rhs);
    return rhs_res - lhs_res;
}

/** Sorts an array H3 indices in increasing resolution order.
 *
 * See docs for the accompanying comparison function.
 */
static void
h3set_sort(H3Index * set, size_t set_len)
{
    qsort(set, set_len, sizeof(H3Index), h3set_sort_compare_fun);
}

static void
free_geo_fence(Geofence * gf)
{
    if (gf == NULL)
    {
        return;
    }
    if (gf->verts != NULL)
    {
        h3_nif_free(gf->verts);
        gf->verts = NULL;
    }
    gf->numVerts = 0;
}

static void
free_geo_polygon(GeoPolygon * gp)
{
    if (gp == NULL)
    {
        return;
    }
    free_geo_fence(&gp->geofence);
    if (gp->holes != NULL)
    {
        for (int i = 0; i < gp->numHoles; i++)
        {
            free_geo_fence(&gp->holes[i]);
        }
        h3_nif_free(gp->holes);
        gp->holes = NULL;
    }
    gp->numHoles = 0;
}

static bool
get_h3idx(ErlNifEnv * env, ERL_NIF_TERM term, H3Index * dest)
{
    return enif_get_uint64(env, term, (unsigned long *)dest) && h3IsValid(*dest);
}

static ERL_NIF_TERM
make_h3idx(ErlNifEnv * env, H3Index index)
{
    return enif_make_uint64(env, index);
}

static bool
get_resolution(ErlNifEnv * env, ERL_NIF_TERM term, int * dest)
{
    return enif_get_int(env, term, dest) && *dest >= 0 && *dest <= 15;
}

static bool
get_geo_coord(ErlNifEnv * env, ERL_NIF_TERM term, GeoCoord * dest)
{
    const ERL_NIF_TERM * geo;
    int                  arity;
    if (!enif_is_tuple(env, term) || !enif_get_tuple(env, term, &arity, &geo)
        || arity != 2)
    {
        return false;
    }
    if (!enif_get_double(env, geo[0], &dest->lat)
        || !enif_get_double(env, geo[1], &dest->lon))
    {
        return false;
    }

    dest->lat = degsToRads(dest->lat);
    dest->lon = degsToRads(dest->lon);

    return true;
}

static bool
get_geo_fence(ErlNifEnv * env, ERL_NIF_TERM term, Geofence * gf)
{
    ERL_NIF_TERM head;
    ERL_NIF_TERM tail;
    GeoCoord *   gc = NULL;
    if (gf == NULL || !enif_get_list_length(env, term, (unsigned *)&gf->numVerts)
        || gf->numVerts < 1)
    {
        return false;
    }
    gf->verts = h3_nif_calloc(gf->numVerts, sizeof(GeoCoord));
    if (gf->verts == NULL)
    {
        return false;
    }
    gc   = gf->verts;
    tail = term;
    while (enif_get_list_cell(env, tail, &head, &tail) != 0)
    {
        if (!get_geo_coord(env, head, gc))
        {
            free_geo_fence(gf);
            return false;
        }
        gc++;
    }
    return true;
}

static bool
get_geo_polygon(ErlNifEnv * env, ERL_NIF_TERM term, GeoPolygon * gp)
{
    ERL_NIF_TERM head;
    ERL_NIF_TERM tail;
    Geofence *   gf = NULL;
    if (gp == NULL || !enif_get_list_length(env, term, (unsigned *)&gp->numHoles)
        || gp->numHoles < 1)
    {
        return false;
    }
    gp->numHoles -= 1;
    tail = term;
    if (!enif_get_list_cell(env, term, &head, &tail)
        || !get_geo_fence(env, head, &gp->geofence))
    {
        return false;
    }
    if (gp->numHoles == 0)
    {
        gp->holes = NULL;
        return true;
    }
    gp->holes = h3_nif_calloc(gp->numHoles, sizeof(Geofence));
    if (gp->holes == NULL)
    {
        free_geo_polygon(gp);
        return false;
    }
    gf = gp->holes;
    while (enif_get_list_cell(env, tail, &head, &tail))
    {
        if (!get_geo_fence(env, head, gf))
        {
            free_geo_polygon(gp);
            return false;
        }
        gf++;
    }
    return true;
}

static ERL_NIF_TERM
make_geo_coord(ErlNifEnv * env, GeoCoord * coord)
{
    double lat = radsToDegs(coord->lat);
    double lon = radsToDegs(coord->lon);

    return enif_make_tuple2(env,
                            enif_make_double(env, lat > 90 ? lat - 180 : lat),
                            enif_make_double(env, lon > 180 ? lon - 360 : lon));
}

static bool
get_k(ErlNifEnv * env, ERL_NIF_TERM term, int * dest)
{
    return enif_get_int(env, term, dest) && *dest >= 0;
}


static ERL_NIF_TERM
erl_num_hexagons(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    int res;
    if (!get_resolution(env, argv[0], &res))
    {
        return enif_make_badarg(env);
    }

    int64_t result = numHexagons(res);
    return enif_make_int64(env, result);
}

static ERL_NIF_TERM
erl_edge_length_meters(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    int res;
    if (!get_resolution(env, argv[0], &res))
    {
        return enif_make_badarg(env);
    }

    double result = edgeLengthM(res);
    return enif_make_double(env, result);
}

static ERL_NIF_TERM
erl_edge_length_kilometers(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    int res;
    if (!get_resolution(env, argv[0], &res))
    {
        return enif_make_badarg(env);
    }

    double result = edgeLengthKm(res);
    return enif_make_double(env, result);
}

static ERL_NIF_TERM
erl_hex_area_km2(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    int res;
    if (!get_resolution(env, argv[0], &res))
    {
        return enif_make_badarg(env);
    }

    double result = hexAreaKm2(res);
    return enif_make_double(env, result);
}

static ERL_NIF_TERM
erl_hex_area_m2(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    int res;
    if (!get_resolution(env, argv[0], &res))
    {
        return enif_make_badarg(env);
    }

    double result = hexAreaM2(res);
    return enif_make_double(env, result);
}

static ERL_NIF_TERM
erl_geo_to_h3(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    GeoCoord coord;
    if (!get_geo_coord(env, argv[0], &coord))
    {
        return enif_make_badarg(env);
    }
    int res;
    if (!get_resolution(env, argv[1], &res))
    {
        return enif_make_badarg(env);
    }

    H3Index result = geoToH3(&coord, res);
    return make_h3idx(env, result);
}

static ERL_NIF_TERM
erl_h3_to_geo(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    H3Index h3idx;
    if (!get_h3idx(env, argv[0], &h3idx))
    {
        return enif_make_badarg(env);
    }

    GeoCoord coord;
    h3ToGeo(h3idx, &coord);
    return make_geo_coord(env, &coord);
}

static ERL_NIF_TERM
erl_h3_to_geo_boundary(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    H3Index h3idx;
    if (!get_h3idx(env, argv[0], &h3idx))
    {
        return enif_make_badarg(env);
    }

    GeoBoundary boundary;
    h3ToGeoBoundary(h3idx, &boundary);

    ERL_NIF_TERM verts[MAX_CELL_BNDRY_VERTS];
    for (int i = 0; i < boundary.numVerts; i++)
    {
        verts[i] = make_geo_coord(env, &boundary.verts[i]);
    }

    return enif_make_list_from_array(env, verts, boundary.numVerts);
}

static ERL_NIF_TERM
erl_h3_to_string(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    H3Index h3idx;
    if (!get_h3idx(env, argv[0], &h3idx))
    {
        return enif_make_badarg(env);
    }

    char out[17];
    h3ToString(h3idx, out, 17);
    return enif_make_string(env, out, ERL_NIF_LATIN1);
}

static ERL_NIF_TERM
erl_string_to_h3(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    char in[17];
    if (!enif_get_string(env, argv[0], in, 17, ERL_NIF_LATIN1))
    {
        return enif_make_badarg(env);
    }

    H3Index h3idx = stringToH3(in);

    if (!h3IsValid(h3idx))
    {
        return enif_make_badarg(env);
    }
    return make_h3idx(env, h3idx);
}

static ERL_NIF_TERM
erl_get_resolution(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    H3Index h3idx;
    if (!get_h3idx(env, argv[0], &h3idx))
    {
        return enif_make_badarg(env);
    }

    int result = h3GetResolution(h3idx);
    return enif_make_int(env, result);
}


static ERL_NIF_TERM
erl_get_base_cell(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    H3Index h3idx;
    if (!get_h3idx(env, argv[0], &h3idx))
    {
        return enif_make_badarg(env);
    }

    int result = h3GetBaseCell(h3idx);
    return enif_make_int(env, result);
}

static ERL_NIF_TERM
erl_is_valid(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    H3Index h3idx;
    return get_h3idx(env, argv[0], &h3idx) ? ATOM_TRUE : ATOM_FALSE;
}


static ERL_NIF_TERM
erl_is_class3(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    H3Index h3idx;
    if (!get_h3idx(env, argv[0], &h3idx))
    {
        return enif_make_badarg(env);
    }

    return h3IsResClassIII(h3idx) ? ATOM_TRUE : ATOM_FALSE;
}


static ERL_NIF_TERM
erl_is_pentagon(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    H3Index h3idx;
    if (!get_h3idx(env, argv[0], &h3idx))
    {
        return enif_make_badarg(env);
    }

    return h3IsPentagon(h3idx) ? ATOM_TRUE : ATOM_FALSE;
}

static ERL_NIF_TERM
erl_parent(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    H3Index h3idx;
    if (!get_h3idx(env, argv[0], &h3idx))
    {
        return enif_make_badarg(env);
    }

    int res;
    if (!get_resolution(env, argv[1], &res))
    {
        return enif_make_badarg(env);
    }

    if (res > h3GetResolution(h3idx))
    {
        // asking for a parent with a higher resolution is nonsense
        return enif_make_badarg(env);
    }

    return make_h3idx(env, h3ToParent(h3idx, res));
}

static ERL_NIF_TERM
erl_children(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    H3Index h3idx;
    if (!get_h3idx(env, argv[0], &h3idx))
    {
        return enif_make_badarg(env);
    }

    int res;
    if (!get_resolution(env, argv[1], &res))
    {
        return enif_make_badarg(env);
    }

    if (res <= h3GetResolution(h3idx))
    {
        // asking for children with lower resolution is nonsense
        return enif_make_badarg(env);
    }

    int childcount = maxH3ToChildrenSize(h3idx, res);

    H3Index * children = h3_nif_calloc(childcount, sizeof(H3Index));

    if (children == NULL)
    {
        return enif_raise_exception(env,
                                    enif_make_string(env,
                                                     "allocation",
                                                     ERL_NIF_LATIN1));
    }

    h3ToChildren(h3idx, res, children);

    ERL_NIF_TERM list = enif_make_list(env, 0);
    for (int i = childcount - 1; i >= 0; i--)
    {
        list = enif_make_list_cell(env, make_h3idx(env, children[i]), list);
    }
    h3_nif_free(children);
    return list;
}

static ERL_NIF_TERM
erl_k_ring(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    H3Index h3idx;
    if (!get_h3idx(env, argv[0], &h3idx))
    {
        return enif_make_badarg(env);
    }

    int k;
    if (!get_k(env, argv[1], &k))
    {
        return enif_make_badarg(env);
    }

    int       kringsize = maxKringSize(k);
    H3Index * h3indices = h3_nif_calloc(kringsize, sizeof(H3Index));

    if (h3indices == NULL)
    {
        return enif_raise_exception(env,
                                    enif_make_string(env,
                                                     "allocation",
                                                     ERL_NIF_LATIN1));
    }

    kRing(h3idx, k, h3indices);

    ERL_NIF_TERM list = enif_make_list(env, 0);
    // Output is placed in the provided array in no particular order. Elements
    // of the output array may be left zero, as can happen when crossing a pentagon.
    for (int i = kringsize - 1; i >= 0; i--)
    {
        if (h3indices[i] != 0)
        {
            list = enif_make_list_cell(env, make_h3idx(env, h3indices[i]), list);
        }
    }

    h3_nif_free(h3indices);
    return list;
}

static ERL_NIF_TERM
erl_k_ring_distances(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    H3Index h3idx;
    if (!get_h3idx(env, argv[0], &h3idx))
    {
        return enif_make_badarg(env);
    }

    int k;
    if (!get_k(env, argv[1], &k))
    {
        return enif_make_badarg(env);
    }

    int       kringsize = maxKringSize(k);
    H3Index * h3indices = h3_nif_calloc(kringsize, sizeof(H3Index));

    if (h3indices == NULL)
    {
        return enif_raise_exception(env,
                                    enif_make_string(env,
                                                     "allocation",
                                                     ERL_NIF_LATIN1));
    }

    int * h3distances = h3_nif_calloc(kringsize, sizeof(int));

    if (h3distances == NULL)
    {
        h3_nif_free(h3indices);
        return enif_raise_exception(env,
                                    enif_make_string(env,
                                                     "allocation",
                                                     ERL_NIF_LATIN1));
    }

    kRingDistances(h3idx, k, h3indices, h3distances);

    ERL_NIF_TERM list = enif_make_list(env, 0);
    // Output is placed in the provided array in no particular order. Elements
    // of the output array may be left zero, as can happen when crossing a pentagon.
    for (int i = kringsize - 1; i >= 0; i--)
    {
        if (h3indices[i] != 0)
        {
            ERL_NIF_TERM entry =
                enif_make_tuple2(env,
                                 make_h3idx(env, h3indices[i]),
                                 enif_make_int(env, h3distances[i]));
            list = enif_make_list_cell(env, entry, list);
        }
    }

    h3_nif_free(h3indices);
    h3_nif_free(h3distances);
    return list;
}


static ERL_NIF_TERM
erl_max_k_ring_size(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    int k;
    if (!enif_get_int(env, argv[0], &k))
    {
        return enif_make_badarg(env);
    }

    int result = maxKringSize(k);
    return enif_make_int(env, result);
}

static bool
get_h3indexes(ErlNifEnv * env, ERL_NIF_TERM term, H3Index * dest, unsigned dest_len)
{
    unsigned len;
    if (!enif_get_list_length(env, term, &len) || len > dest_len)
    {
        return false;
    }

    ERL_NIF_TERM head;
    while (enif_get_list_cell(env, term, &head, &term))
    {
        if (!get_h3idx(env, head, dest))
        {
            return false;
        }
        dest += 1;
    }

    return true;
}

static ERL_NIF_TERM
make_h3indexes(ErlNifEnv * env, H3Index * src, unsigned src_len)
{
    ERL_NIF_TERM list = enif_make_list(env, 0);
    for (int i = 0; i < src_len; i++)
    {
        if (src[i] != 0)
        {
            ERL_NIF_TERM entry = make_h3idx(env, src[i]);
            list               = enif_make_list_cell(env, entry, list);
        }
    }

    return list;
}

static ERL_NIF_TERM
erl_compact(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    unsigned len;
    if (!enif_get_list_length(env, argv[0], &len))
    {
        return enif_make_badarg(env);
    }

    H3Index * in_indices = h3_nif_calloc(len, sizeof(H3Index));
    if (in_indices == NULL)
    {
        return enif_raise_exception(env,
                                    enif_make_string(env,
                                                     "allocation",
                                                     ERL_NIF_LATIN1));
    }

    if (!get_h3indexes(env, argv[0], in_indices, len))
    {
        h3_nif_free(in_indices);
        return enif_make_badarg(env);
    }

    H3Index * out_indices = h3_nif_calloc(len, sizeof(H3Index));
    if (out_indices == NULL)
    {
        h3_nif_free(in_indices);
        return enif_raise_exception(env,
                                    enif_make_string(env,
                                                     "allocation",
                                                     ERL_NIF_LATIN1));
    }

    int res = compact(in_indices, out_indices, len);
    if (res)
    {
        h3_nif_free(in_indices);
        h3_nif_free(out_indices);

        ERL_NIF_TERM exception_msg =
            enif_make_string(env, "libh3 compact() returned", ERL_NIF_LATIN1);
        ERL_NIF_TERM h3_err = enif_make_int(env, res);
        return enif_raise_exception(env,
                                    enif_make_tuple2(env, exception_msg, h3_err));
    }

    // Done with in_indices
    h3_nif_free(in_indices);

    // Sort indices increasing resolution order (largest area to
    // lowest area).
    h3set_sort(out_indices, len);

    ERL_NIF_TERM list = make_h3indexes(env, out_indices, len);

    h3_nif_free(out_indices);
    return list;
}

static ERL_NIF_TERM
erl_uncompact(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    unsigned in_len;
    if (!enif_get_list_length(env, argv[0], &in_len))
    {
        return enif_make_badarg(env);
    }

    int res;
    if (!get_resolution(env, argv[1], &res))
    {
        return enif_make_badarg(env);
    }

    H3Index * in_indices = h3_nif_calloc(in_len, sizeof(H3Index));
    if (in_indices == NULL)
    {
        return enif_raise_exception(env,
                                    enif_make_string(env,
                                                     "allocation",
                                                     ERL_NIF_LATIN1));
    }

    if (!get_h3indexes(env, argv[0], in_indices, in_len))
    {
        h3_nif_free(in_indices);
        return enif_make_badarg(env);
    }

    unsigned  out_len     = maxUncompactSize(in_indices, in_len, res);
    H3Index * out_indices = h3_nif_calloc(out_len, sizeof(H3Index));
    if (out_indices == NULL)
    {
        h3_nif_free(in_indices);
        return enif_raise_exception(env,
                                    enif_make_string(env,
                                                     "allocation",
                                                     ERL_NIF_LATIN1));
    }

    if (uncompact(in_indices, in_len, out_indices, out_len, res) != 0)
    {
        h3_nif_free(in_indices);
        h3_nif_free(out_indices);
        return enif_make_badarg(env);
    }

    // Done with in_indices
    h3_nif_free(in_indices);

    ERL_NIF_TERM list = make_h3indexes(env, out_indices, out_len);

    h3_nif_free(out_indices);
    return list;
}


static ERL_NIF_TERM
erl_indices_are_neighbors(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    H3Index h3idx_origin;
    if (!get_h3idx(env, argv[0], &h3idx_origin))
    {
        return enif_make_badarg(env);
    }

    H3Index h3idx_destination;
    if (!get_h3idx(env, argv[1], &h3idx_destination))
    {
        return enif_make_badarg(env);
    }

    return h3IndexesAreNeighbors(h3idx_origin, h3idx_destination) ? ATOM_TRUE
                                                                  : ATOM_FALSE;
}

static ERL_NIF_TERM
erl_grid_distance(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    H3Index h3idx_src;
    if (!get_h3idx(env, argv[0], &h3idx_src))
    {
        return enif_make_badarg(env);
    }

    H3Index h3idx_dest;
    if (!get_h3idx(env, argv[1], &h3idx_dest))
    {
        return enif_make_badarg(env);
    }

    int64_t result = h3Distance(h3idx_src, h3idx_dest);
    if (result < 0)
    {
        return enif_make_badarg(env);
    }

    return enif_make_int64(env, result);
}

static ERL_NIF_TERM
erl_get_unidirectional_edge(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    H3Index h3idx_origin;
    if (!get_h3idx(env, argv[0], &h3idx_origin))
    {
        return enif_make_badarg(env);
    }

    H3Index h3idx_destination;
    if (!get_h3idx(env, argv[1], &h3idx_destination))
    {
        return enif_make_badarg(env);
    }

    H3Index h3idx = getH3UnidirectionalEdge(h3idx_origin, h3idx_destination);
    if (h3idx == 0)
    {
        return enif_make_badarg(env);
    }

    return make_h3idx(env, h3idx);
}

static ERL_NIF_TERM
erl_get_res0_indexes(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    int     res0count = res0IndexCount();
    H3Index res0[res0count];
    getRes0Indexes(res0);

    ERL_NIF_TERM list = enif_make_list(env, 0);
    for (int i = res0count - 1; i >= 0; i--)
    {
        list = enif_make_list_cell(env, make_h3idx(env, res0[i]), list);
    }
    return list;
}

static ERL_NIF_TERM
erl_polyfill(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    int          resolution;
    GeoPolygon   polygon;
    int          polyfillsize;
    H3Index *    h3indices = NULL;
    ERL_NIF_TERM list;
    int          i;
    if (argc != 2 || !get_resolution(env, argv[1], &resolution))
    {
        return enif_make_badarg(env);
    }
    if (!get_geo_polygon(env, argv[0], &polygon))
    {
        return enif_make_badarg(env);
    }
    polyfillsize = maxPolyfillSize(&polygon, resolution);
    h3indices    = h3_nif_calloc(polyfillsize, sizeof(H3Index));
    if (h3indices == NULL)
    {
        free_geo_polygon(&polygon);
        return enif_raise_exception(env,
                                    enif_make_string(env,
                                                     "allocation",
                                                     ERL_NIF_LATIN1));
    }
    polyfill(&polygon, resolution, h3indices);
    list = enif_make_list(env, 0);
    for (i = polyfillsize - 1; i >= 0; i--)
    {
        if (h3indices[i] != 0)
        {
            list = enif_make_list_cell(env, make_h3idx(env, h3indices[i]), list);
        }
    }
    h3_nif_free(h3indices);
    free_geo_polygon(&polygon);
    return list;
}

static ERL_NIF_TERM
erl_set_to_multi_polygon(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    unsigned           len;
    H3Index *          h3Set = NULL;
    LinkedGeoPolygon   out;
    LinkedGeoPolygon * polygon = NULL;
    LinkedGeoLoop *    loop    = NULL;
    LinkedGeoCoord *   coord   = NULL;
    ERL_NIF_TERM       polygon_list;
    ERL_NIF_TERM       loop_list;
    ERL_NIF_TERM       coord_list;

    if (argc != 1 || !enif_get_list_length(env, argv[0], &len))
    {
        return enif_make_badarg(env);
    }

    h3Set = h3_nif_calloc(len, sizeof(H3Index));
    if (h3Set == NULL)
    {
        return enif_raise_exception(env,
                                    enif_make_string(env,
                                                     "allocation",
                                                     ERL_NIF_LATIN1));
    }

    if (!get_h3indexes(env, argv[0], h3Set, len))
    {
        h3_nif_free(h3Set);
        return enif_make_badarg(env);
    }

    h3SetToLinkedGeo(h3Set, (int)len, &out);

    polygon_list = enif_make_list(env, 0);
    polygon      = &out;
    while (polygon != NULL)
    {
        loop_list = enif_make_list(env, 0);
        loop      = polygon->first;
        while (loop != NULL)
        {
            coord_list = enif_make_list(env, 0);
            coord      = loop->first;
            while (coord != NULL)
            {
                coord_list =
                    enif_make_list_cell(env,
                                        make_geo_coord(env, &coord->vertex),
                                        coord_list);
                coord = coord->next;
            }
            loop_list = enif_make_list_cell(env, coord_list, loop_list);
            loop      = loop->next;
        }
        polygon_list = enif_make_list_cell(env, loop_list, polygon_list);
        polygon      = polygon->next;
    }

    destroyLinkedPolygon(&out);
    h3_nif_free(h3Set);

    return polygon_list;
}

static ERL_NIF_TERM
erl_contains(ErlNifEnv * env, int argc, const ERL_NIF_TERM argv[])
{
    H3Index key = 0;

    if (argc != 2 || !get_h3idx(env, argv[0], &key))
    {
        return enif_make_badarg(env);
    }

    int key_res = h3GetResolution(key);

    /*
     * In an effort to speed up membership tests in cache friendly
     * manner, we pre-convert the key index at all possible H3 parent
     * resolutions. Because it is only possible to convert a hex to
     * rougher (lower res) parent index, all elements in the LUT that
     * indices greater than the key are plain copies of the key.
     */
    H3Index key_res_lut[16];

    for (int res = 0; res < 16; ++res)
    {
        if (res < key_res)
        {
            key_res_lut[res] = h3ToParent(key, res);
        }
        else
        {
            key_res_lut[res] = key;
        }
    }

    if (enif_is_list(env, argv[1]))
    {
        ERL_NIF_TERM list = argv[1];
        ERL_NIF_TERM head;
        ERL_NIF_TERM tail;

        while (enif_get_list_cell(env, list, &head, &tail))
        {
            H3Index idx;
            if (!get_h3idx(env, head, &idx))
            {
                return enif_make_badarg(env);
            }

            int idx_res = h3GetResolution(idx);

            if (key_res_lut[idx_res] == idx)
            {
                return enif_make_tuple2(env, ATOM_TRUE, head);
            }

            list = tail;
        }
    }
    else if (enif_is_binary(env, argv[1]))
    {
        /* A binary containing little-endian serialized H3 indices (u64s) */
        ErlNifBinary serialized;
        enif_inspect_binary(env, argv[1], &serialized);
        for (unsigned char * le_idx = serialized.data;
             le_idx + sizeof(H3Index) < serialized.data + serialized.size;
             le_idx += sizeof(H3Index))
        {
            H3Index idx =
                ((H3Index)le_idx[7] << 56) | ((H3Index)le_idx[6] << 48)
                | ((H3Index)le_idx[5] << 40) | ((H3Index)le_idx[4] << 32)
                | ((H3Index)le_idx[3] << 24) | ((H3Index)le_idx[2] << 16)
                | ((H3Index)le_idx[1] << 8) | (H3Index)le_idx[0];

            int idx_res = h3GetResolution(idx);

            if (key_res_lut[idx_res] == idx)
            {
                return enif_make_tuple2(env,
                                        ATOM_TRUE,
                                        enif_make_uint64(env, idx));
            }
        }
    }
    else
    {
        return enif_make_badarg(env);
    }

    return ATOM_FALSE;
}

static ErlNifFunc nif_funcs[] =
    {{"num_hexagons", 1, erl_num_hexagons, 0},
     {"edge_length_meters", 1, erl_edge_length_meters, 0},
     {"edge_length_kilometers", 1, erl_edge_length_kilometers, 0},
     {"hex_area_km2", 1, erl_hex_area_km2, 0},
     {"hex_area_m2", 1, erl_hex_area_m2, 0},
     {"from_geo", 2, erl_geo_to_h3, 0},
     {"to_geo", 1, erl_h3_to_geo, 0},
     {"to_geo_boundary", 1, erl_h3_to_geo_boundary, 0},
     {"to_string", 1, erl_h3_to_string, 0},
     {"from_string", 1, erl_string_to_h3, 0},
     {"get_resolution", 1, erl_get_resolution, 0},
     {"get_base_cell", 1, erl_get_base_cell, 0},
     {"is_valid", 1, erl_is_valid, 0},
     {"is_class3", 1, erl_is_class3, 0},
     {"is_pentagon", 1, erl_is_pentagon, 0},
     {"parent", 2, erl_parent, 0},
     {"children", 2, erl_children, 0},
     {"compact", 1, erl_compact, ERL_NIF_DIRTY_JOB_CPU_BOUND},
     {"uncompact", 2, erl_uncompact, ERL_NIF_DIRTY_JOB_CPU_BOUND},
     {"k_ring", 2, erl_k_ring, 0},
     {"k_ring_distances", 2, erl_k_ring_distances, 0},
     {"max_k_ring_size", 1, erl_max_k_ring_size, 0},
     {"indices_are_neighbors", 2, erl_indices_are_neighbors, 0},
     {"get_unidirectional_edge", 2, erl_get_unidirectional_edge, 0},
     {"grid_distance", 2, erl_grid_distance, 0},
     {"get_res0_indexes", 0, erl_get_res0_indexes, 0},
     {"polyfill", 2, erl_polyfill, ERL_NIF_DIRTY_JOB_CPU_BOUND},
     {"set_to_multi_polygon", 1, erl_set_to_multi_polygon, 0},
     {"contains", 2, erl_contains, ERL_NIF_DIRTY_JOB_CPU_BOUND},
     {"meminfo", 0, erl_meminfo, 0}};

#define ATOM(Id, Value)                                                        \
    {                                                                          \
        Id = enif_make_atom(env, Value);                                       \
    }

static int
load(ErlNifEnv * env, void ** priv_data, ERL_NIF_TERM load_info)
{
    (void)priv_data;
    (void)load_info;

    ATOM(ATOM_TRUE, "true");
    ATOM(ATOM_FALSE, "false");

    return 0;
}

ERL_NIF_INIT(h3, nif_funcs, load, NULL, NULL, NULL);
