/**
 * SPDX-License-Identifier: GPL-2.0-or-later
 *
 * This file is part of osm2pgsql (https://osm2pgsql.org/).
 *
 * Copyright (C) 2006-2026 by the osm2pgsql developer community.
 * For a full list of authors see the git log.
 */

#include "flex-lua-geom.hpp"
#include "flex-table-column.hpp"
#include "flex-write.hpp"
#include "geom-functions.hpp"
#include "json-writer.hpp"
#include "lua-utils.hpp"
#include "wkb.hpp"

#include <osmium/osm/timestamp.hpp>

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <limits>
#include <vector>

namespace {

int sgn(double val) noexcept
{
    if (val > 0) {
        return 1;
    }
    if (val < 0) {
        return -1;
    }
    return 0;
}

void write_null(db_copy_mgr_t<db_deleter_by_type_and_id_t> *copy_mgr,
                flex_table_column_t const &column)
{
    if (column.not_null()) {
        throw not_null_exception_t{
            fmt::format("Can not add NULL to column '{}' declared NOT NULL.",
                        column.name()),
            &column};
    }
    copy_mgr->add_null_column();
}

void write_boolean(db_copy_mgr_t<db_deleter_by_type_and_id_t> *copy_mgr,
                   flex_table_column_t const &column, char const *str)
{
    if ((std::strcmp(str, "yes") == 0) || (std::strcmp(str, "true") == 0) ||
        std::strcmp(str, "1") == 0) {
        copy_mgr->add_column(true);
        return;
    }

    if ((std::strcmp(str, "no") == 0) || (std::strcmp(str, "false") == 0) ||
        std::strcmp(str, "0") == 0) {
        copy_mgr->add_column(false);
        return;
    }

    write_null(copy_mgr, column);
}

void write_direction(db_copy_mgr_t<db_deleter_by_type_and_id_t> *copy_mgr,
                     flex_table_column_t const &column, char const *str)
{
    if ((std::strcmp(str, "yes") == 0) || (std::strcmp(str, "1") == 0)) {
        copy_mgr->add_column(1);
        return;
    }

    if ((std::strcmp(str, "no") == 0) || (std::strcmp(str, "0") == 0)) {
        copy_mgr->add_column(0);
        return;
    }

    if (std::strcmp(str, "-1") == 0) {
        copy_mgr->add_column(-1);
        return;
    }

    write_null(copy_mgr, column);
}

template <typename T>
void write_integer(db_copy_mgr_t<db_deleter_by_type_and_id_t> *copy_mgr,
                   flex_table_column_t const &column, char const *str)
{
    if (*str == '\0') {
        write_null(copy_mgr, column);
        return;
    }

    char *end = nullptr;
    errno = 0;
    auto const value = std::strtoll(str, &end, 10);

    if (errno != 0 || *end != '\0') {
        write_null(copy_mgr, column);
        return;
    }

    if (value >= std::numeric_limits<T>::min() &&
        value <= std::numeric_limits<T>::max()) {
        copy_mgr->add_column(value);
        return;
    }

    write_null(copy_mgr, column);
}

void write_double(db_copy_mgr_t<db_deleter_by_type_and_id_t> *copy_mgr,
                  flex_table_column_t const &column, char const *str)
{
    if (*str == '\0') {
        write_null(copy_mgr, column);
        return;
    }

    char *end = nullptr;
    double const value = std::strtod(str, &end);

    if (end && *end != '\0') {
        write_null(copy_mgr, column);
        return;
    }

    copy_mgr->add_column(value);
}

using table_register_type = std::vector<void const *>;

void write_json(json_writer_t *writer, lua_State *lua_state,
                table_register_type *tables);

void write_json_table(json_writer_t *writer, lua_State *lua_state,
                      table_register_type *tables)
{
    void const *table_ptr = lua_topointer(lua_state, -1);
    assert(table_ptr);
    auto const it = std::find(tables->cbegin(), tables->cend(), table_ptr);
    if (it != tables->cend()) {
        throw std::runtime_error{"Loop detected in table"};
    }
    tables->push_back(table_ptr);

    if (luaX_is_empty_table(lua_state)) {
        // An empty lua table could be both, we decide here that it is not
        // stored as a JSON array but as a JSON object.
        writer->start_object();
        writer->end_object();
    } else if (luaX_is_array(lua_state)) {
        writer->start_array();
        luaX_for_each(lua_state, [&]() {
            write_json(writer, lua_state, tables);
            writer->next();
        });
        writer->end_array();
    } else {
        writer->start_object();
        luaX_for_each(lua_state, [&]() {
            int const ltype_key = lua_type(lua_state, -2);
            if (ltype_key != LUA_TSTRING) {
                throw fmt_error("Incorrect data type '{}' as key.",
                                lua_typename(lua_state, ltype_key));
            }
            char const *const key = lua_tostring(lua_state, -2);
            writer->key(key);
            write_json(writer, lua_state, tables);
            writer->next();
        });
        writer->end_object();
    }
}

void write_json_number(json_writer_t *writer, lua_State *lua_state)
{
#if LUA_VERSION_NUM >= 503
    int okay = 0;
    auto const num = lua_tointegerx(lua_state, -1, &okay);
    if (okay) {
        writer->number(num);
    } else {
        writer->number(lua_tonumber(lua_state, -1));
    }
#else
    double const num = lua_tonumber(lua_state, -1);
    double intpart = 0.0;
    if (std::modf(num, &intpart) == 0.0) {
        writer->number(static_cast<int64_t>(num));
    } else {
        writer->number(num);
    }
#endif
}

void write_json(json_writer_t *writer, lua_State *lua_state,
                table_register_type *tables)
{
    assert(writer);
    assert(lua_state);

    int const ltype = lua_type(lua_state, -1);
    switch (ltype) {
    case LUA_TNIL:
        writer->null();
        break;
    case LUA_TBOOLEAN:
        writer->boolean(lua_toboolean(lua_state, -1) != 0);
        break;
    case LUA_TNUMBER:
        write_json_number(writer, lua_state);
        break;
    case LUA_TSTRING:
        writer->string(lua_tostring(lua_state, -1));
        break;
    case LUA_TTABLE:
        write_json_table(writer, lua_state, tables);
        break;
    default:
        throw fmt_error("Invalid type '{}' for json/jsonb column.",
                        lua_typename(lua_state, ltype));
    }
}

bool is_compatible(geom::geometry_t const &geom,
                   table_column_type type) noexcept
{
    switch (type) {
    case table_column_type::geometry:
        return true;
    case table_column_type::point:
        return geom.is_point();
    case table_column_type::linestring:
        return geom.is_linestring();
    case table_column_type::polygon:
        return geom.is_polygon();
    case table_column_type::multipoint:
        return geom.is_point() || geom.is_multipoint();
    case table_column_type::multilinestring:
        return geom.is_linestring() || geom.is_multilinestring();
    case table_column_type::multipolygon:
        return geom.is_polygon() || geom.is_multipolygon();
    case table_column_type::geometrycollection:
        return geom.is_collection();
    default:
        break;
    }
    return false;
}

} // anonymous namespace

void flex_write_column(lua_State *lua_state, geometry_cache_t *geom_cache,
                       db_copy_mgr_t<db_deleter_by_type_and_id_t> *copy_mgr,
                       flex_table_column_t const &column)
{
    lua_getfield(lua_state, -1, column.name().c_str());
    int const ltype = lua_type(lua_state, -1);

    // Certain Lua types can never be added to the database
    if (ltype == LUA_TFUNCTION || ltype == LUA_TTHREAD) {
        throw std::runtime_error{
            "Can not add Lua objects of type function or thread."};
    }

    // A Lua nil value is always translated to a database NULL
    if (ltype == LUA_TNIL) {
        write_null(copy_mgr, column);
        lua_pop(lua_state, 1);
        return;
    }

    if (column.type() == table_column_type::text) {
        auto const *const str = lua_tolstring(lua_state, -1, nullptr);
        if (!str) {
            throw fmt_error("Invalid type '{}' for text column.",
                            lua_typename(lua_state, ltype));
        }
        copy_mgr->add_column(str);
    } else if (column.type() == table_column_type::boolean) {
        switch (ltype) {
        case LUA_TBOOLEAN:
            copy_mgr->add_column(lua_toboolean(lua_state, -1) != 0);
            break;
        case LUA_TNUMBER:
            copy_mgr->add_column(lua_tonumber(lua_state, -1) != 0);
            break;
        case LUA_TSTRING:
            write_boolean(copy_mgr, column,
                          lua_tolstring(lua_state, -1, nullptr));
            break;
        default:
            throw fmt_error("Invalid type '{}' for boolean column.",
                            lua_typename(lua_state, ltype));
        }
    } else if (column.type() == table_column_type::int2) {
        if (ltype == LUA_TNUMBER) {
            int64_t const value = lua_tointeger(lua_state, -1);
            if (value >= std::numeric_limits<int16_t>::min() &&
                value <= std::numeric_limits<int16_t>::max()) {
                copy_mgr->add_column(value);
            } else {
                write_null(copy_mgr, column);
            }
        } else if (ltype == LUA_TSTRING) {
            write_integer<int16_t>(copy_mgr, column,
                                   lua_tolstring(lua_state, -1, nullptr));
        } else if (ltype == LUA_TBOOLEAN) {
            copy_mgr->add_column(lua_toboolean(lua_state, -1));
        } else {
            throw fmt_error("Invalid type '{}' for int2 column.",
                            lua_typename(lua_state, ltype));
        }
    } else if (column.type() == table_column_type::int4) {
        if (ltype == LUA_TNUMBER) {
            int64_t const value = lua_tointeger(lua_state, -1);
            if (value >= std::numeric_limits<int32_t>::min() &&
                value <= std::numeric_limits<int32_t>::max()) {
                copy_mgr->add_column(value);
            } else {
                write_null(copy_mgr, column);
            }
        } else if (ltype == LUA_TSTRING) {
            write_integer<int32_t>(copy_mgr, column,
                                   lua_tolstring(lua_state, -1, nullptr));
        } else if (ltype == LUA_TBOOLEAN) {
            copy_mgr->add_column(lua_toboolean(lua_state, -1));
        } else {
            throw fmt_error("Invalid type '{}' for int4 column.",
                            lua_typename(lua_state, ltype));
        }
    } else if (column.type() == table_column_type::int8) {
        if (ltype == LUA_TNUMBER) {
            copy_mgr->add_column(lua_tointeger(lua_state, -1));
        } else if (ltype == LUA_TSTRING) {
            write_integer<int64_t>(copy_mgr, column,
                                   lua_tolstring(lua_state, -1, nullptr));
        } else if (ltype == LUA_TBOOLEAN) {
            copy_mgr->add_column(lua_toboolean(lua_state, -1));
        } else {
            throw fmt_error("Invalid type '{}' for int8 column.",
                            lua_typename(lua_state, ltype));
        }
    } else if (column.type() == table_column_type::real || column.type() == table_column_type::double_precision) {
        if (ltype == LUA_TNUMBER) {
            copy_mgr->add_column(lua_tonumber(lua_state, -1));
        } else if (ltype == LUA_TSTRING) {
            write_double(copy_mgr, column,
                         lua_tolstring(lua_state, -1, nullptr));
        } else {
            throw fmt_error("Invalid type '{}' for real/double precision column.",
                            lua_typename(lua_state, ltype));
        }
    } else if (column.type() == table_column_type::timestamp) {
        if (ltype == LUA_TNUMBER) {
            auto const ts = osmium::Timestamp{lua_tointeger(lua_state, -1)};
            copy_mgr->add_column(ts.to_iso());
        } else if (ltype == LUA_TSTRING) {
            copy_mgr->add_column(lua_tolstring(lua_state, -1, nullptr));
        } else {
            throw fmt_error("Invalid type '{}' for timestamp column.",
                            lua_typename(lua_state, ltype));
        }
    } else if (column.type() == table_column_type::timestamptz) {
        if (ltype == LUA_TNUMBER) {
            auto const ts = osmium::Timestamp{lua_tointeger(lua_state, -1)};
            copy_mgr->add_column(ts.to_iso());
        } else if (ltype == LUA_TSTRING) {
            copy_mgr->add_column(lua_tolstring(lua_state, -1, nullptr));
        } else {
            throw fmt_error("Invalid type '{}' for timestamptz column.",
                            lua_typename(lua_state, ltype));
        }
    } else if (column.type() == table_column_type::hstore) {
        if (ltype == LUA_TTABLE) {
            copy_mgr->new_hash();

            luaX_for_each(lua_state, [&]() {
                char const *const key = lua_tostring(lua_state, -2);
                char const *const val = lua_tostring(lua_state, -1);
                if (key == nullptr) {
                    int const ltype_key = lua_type(lua_state, -2);
                    throw fmt_error(
                        "NULL key for hstore. Possibly this is due to"
                        " an incorrect data type '{}' as key.",
                        lua_typename(lua_state, ltype_key));
                }
                if (val == nullptr) {
                    int const ltype_value = lua_type(lua_state, -1);
                    throw fmt_error(
                        "NULL value for hstore. Possibly this is due to"
                        " an incorrect data type '{}' for key '{}'.",
                        lua_typename(lua_state, ltype_value), key);
                }
                copy_mgr->add_hash_elem(key, val);
            });

            copy_mgr->finish_hash();
        } else {
            throw fmt_error("Invalid type '{}' for hstore column.",
                            lua_typename(lua_state, ltype));
        }
    } else if ((column.type() == table_column_type::json) ||
               (column.type() == table_column_type::jsonb)) {
        json_writer_t writer;
        table_register_type tables;
        write_json(&writer, lua_state, &tables);
        copy_mgr->add_column(writer.json());
    } else if (column.type() == table_column_type::direction) {
        switch (ltype) {
        case LUA_TBOOLEAN:
            copy_mgr->add_column(lua_toboolean(lua_state, -1));
            break;
        case LUA_TNUMBER:
            copy_mgr->add_column(sgn(lua_tonumber(lua_state, -1)));
            break;
        case LUA_TSTRING:
            write_direction(copy_mgr, column,
                            lua_tolstring(lua_state, -1, nullptr));
            break;
        default:
            throw fmt_error("Invalid type '{}' for direction column.",
                            lua_typename(lua_state, ltype));
        }
    } else if (column.is_geometry_column()) {
        if (ltype == LUA_TUSERDATA) {
            auto const *const geom = unpack_geometry(lua_state, -1);
            if (geom && !geom->is_null()) {
                auto const type = column.type();
                if (!is_compatible(*geom, type)) {
                    throw fmt_error("Geometry data for geometry column '{}'"
                                    " has the wrong type ({}).",
                                    column.name(), geometry_type(*geom));
                }
                bool const wrap_multi =
                    (type == table_column_type::multipoint ||
                     type == table_column_type::multilinestring ||
                     type == table_column_type::multipolygon);
                if (geom->srid() == column.srid()) {
                    copy_mgr->add_hex_geom(geom_to_ewkb(*geom, wrap_multi));
                    geom_cache->add_new(&column, *geom);
                } else {
                    auto const &proj = get_projection(column.srid());
                    auto tgeom = geom::transform(*geom, proj);
                    copy_mgr->add_hex_geom(geom_to_ewkb(tgeom, wrap_multi));
                    geom_cache->add_new(&column, std::move(tgeom));
                }
            } else {
                write_null(copy_mgr, column);
            }
        } else {
            throw fmt_error("Need geometry data for geometry column '{}'.",
                            column.name());
        }
    } else {
        throw fmt_error("Column type {} not implemented.",
                        static_cast<uint8_t>(column.type()));
    }

    lua_pop(lua_state, 1);
}
