/*  Copyright (C) 1996-1997  Id Software, Inc.

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

See file, 'COPYING', for details.
*/

#include <cstdint>

#include <common/entdata.h>
#include <common/parser.hh>
#include <common/log.hh>
#include <common/mapfile.hh>
#include <common/settings.hh>
#include <common/imglib.hh>
#include <common/bsputils.hh>
#include <common/json.hh>

#ifdef USE_LUA
extern "C"
{
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
}
#endif

using namespace mapfile;

// global map file state
map_file_t map_file;
const gamedef_t *current_game = nullptr;

struct maputil_settings : public settings::common_settings
{
private:
    template<typename TSetting, typename T>
    bool load_setting(const std::string &name, parser_base_t &parser, settings::source src, T default_value)
    {
        TSetting *setting = new TSetting(nullptr, name, "");
        bool parsed = setting->parse(name, parser, src);
        operations.push_back(std::unique_ptr<settings::setting_base>(setting));
        return parsed;
    }

    bool load_setting(const std::string &name, settings::source src)
    {
        settings::setting_func *setting = new settings::setting_func(nullptr, name, nullptr);
        operations.push_back(std::unique_ptr<settings::setting_base>(setting));
        return true;
    }

public:
    settings::setting_func load{this, "load",
        [&](const std::string &name, parser_base_t &parser, settings::source src) {
            return this->load_setting<settings::setting_string>(name, parser, src, "");
        },
        nullptr, "Load the given .map file"};
    settings::setting_func game{this, "game",
        [&](const std::string &name, parser_base_t &parser, settings::source src) {
            return this->load_setting<settings::setting_string>(name, parser, src, "");
        },
        nullptr, "Set game to process map as (must be done after load)"};
    settings::setting_func script{this, "script",
        [&](const std::string &name, parser_base_t &parser, settings::source src) {
            return this->load_setting<settings::setting_string>(name, parser, src, "");
        },
        nullptr, "Run the given script file"};
    settings::setting_func save{this, "save",
        [&](const std::string &name, parser_base_t &parser, settings::source src) {
            return this->load_setting<settings::setting_string>(name, parser, src, "");
        },
        nullptr, "Save the .map to the specified output"};
    settings::setting_func query{this, "query",
        [&](const std::string &name, parser_base_t &parser, settings::source src) {
            return this->load_setting<settings::setting_string>(name, parser, src, "");
        },
        nullptr,
        "perform a query on entities and print out matching results. see docs for more details on globals. note that query has the same access as script but is more suitable for small read-only operations."};
    settings::setting_func convert{this, "convert",
        [&](const std::string &name, parser_base_t &parser, settings::source src) {
            return this->load_setting<settings::setting_string>(name, parser, src, "");
        },
        nullptr, "convert the current map to the given format."};
    settings::setting_func strip_extended_info{this, "strip_extended_info",
        [&](const std::string &name, parser_base_t &parser, settings::source src) {
            return this->load_setting(name, src);
        },
        nullptr, "removes extended Quake II/III information on faces."};

    std::vector<std::unique_ptr<settings::setting_base>> operations;
};

maputil_settings maputil_options;

map_file_t LoadMapOrEntFile(const fs::path &source)
{
    logging::funcheader();

    auto file = fs::load(source);
    map_file_t map;

    if (!file) {
        FError("Couldn't load map/entity file \"{}\".", source);
        return map;
    }

    parser_t parser(file, {source.string()});

    map.parse(parser);

    return map;
}

#ifdef USE_LUA
using array_iterate_callback = std::function<bool(lua_State *state, size_t index)>;

void lua_iterate_array(lua_State *state, array_iterate_callback cb)
{
    size_t n = 0;

    bool keep_iterating = false;

    do {
        lua_rawgeti(state, -1, n + 1);

        if (lua_type(state, -1) != LUA_TNIL) {
            keep_iterating = cb(state, n);
        } else {
            keep_iterating = false;
        }

        lua_pop(state, 1);

        n++;
    } while (keep_iterating);
}

size_t lua_count_array(lua_State *state)
{
    size_t num = 0;

    lua_iterate_array(state, [&num](auto state, auto index) {
        num++;
        return true;
    });

    return num;
}

// pushes value onto stack
static void json_to_lua(lua_State *state, const Json::Value &value)
{
    switch (value.type()) {
        case Json::ValueType::objectValue: {
            lua_newtable(state);

            for (auto &key : value.getMemberNames()) {
                const Json::Value &val = value[key];
                lua_pushstring(state, key.c_str());
                json_to_lua(state, val);
                lua_settable(state, -3);
            }
            return;
        }
        case Json::ValueType::arrayValue: {
            lua_newtable(state);

            size_t i = 1;

            for (auto &v : value) {
                json_to_lua(state, v);
                lua_rawseti(state, -2, i++);
            }
            return;
        }
        case Json::ValueType::stringValue: {
            lua_pushstring(state, value.asCString());
            return;
        }
        case Json::ValueType::uintValue: {
            lua_pushnumber(state, value.asUInt64());
            return;
        }
        case Json::ValueType::intValue: {
            lua_pushnumber(state, value.asInt64());
            return;
        }
        case Json::ValueType::realValue: {
            lua_pushnumber(state, value.asDouble());
            return;
        }
        case Json::ValueType::booleanValue: {
            lua_pushboolean(state, value.asBool());
            return;
        }
        case Json::ValueType::nullValue: {
            lua_pushnil(state);
            return;
        }
        default: {
            luaL_error(state, "invalid JSON object type\n");
            return;
        }
    }
}

static int l_load_json(lua_State *state)
{
    const char *path = lua_tostring(state, -1);
    lua_pop(state, 1);

    auto result = fs::load(path);

    if (!result) {
        return luaL_error(state, "can't load JSON file: %s\n", path);
    }

    try {
        Json::Value json = parse_json(result->data(), result->data() + result->size());

        json_to_lua(state, json);
    } catch (std::exception &e) {
        return luaL_error(state, "JSON load exception for %s: %s\n", path, e.what());
    }

    return 1;
}

/*
 * Lua layout:
 * entities = table[]
 *  [E].dict = array
 *      [D] = [ key, value ]
 *  [E].brushes = table[]
 *   [S].texture = string
 *   [S].plane_points = [ [ x, y, z ] [ x, y, z ] [ x, y, z ] ]
 *   [S].raw = table (can only contain ONE member:)
 *       .quaked = table
 *        .shift = [ x, y ]
 *        .rotate = number
 *        .scale = [ x, y ]
 *       .valve = table
 *        .axis = [ [ x, y, z ] [ x, y, z ] ]
 *        .shift = [ x, y ]
 *        .rotate = number
 *        .scale = [ x, y ]
 *       .bp = table
 *        .axis = [ [ x, y, z ] [ x, y, z ] ]
 *       .etp = table
 *        .shift = [ x, y ]
 *        .rotate = number
 *        .scale = [ x, y ]
 *        .tx2 = boolean
 *   [S].info = table or nil
 *       .contents = number
 *       .value = number
 *       .flags = number
 *   [S].plane = [ x, y, z, d ] (read-only)
 *   [S].vecs = [ [ x, y, z, d ] [ x, y, z, d ] ] (read-only)
 */

static void maputil_make_brush_side(lua_State *state, const brush_side_t &side)
{
    // make side
    lua_createtable(state, 0, 4);

    // make vecs
    lua_createtable(state, 2, 0);

    for (size_t i = 0; i < 2; i++) {
        lua_createtable(state, 4, 0);

        for (size_t v = 0; v < 4; v++) {
            lua_pushnumber(state, side.vecs.at(i, v));
            lua_rawseti(state, -2, v + 1);
        }

        lua_rawseti(state, -2, i + 1);
    }

    lua_setfield(state, -2, "vecs");

    // set raw
    lua_createtable(state, 0, 1);

    lua_createtable(state, 0, 4);

    if (std::holds_alternative<texdef_quake_ed_t>(side.raw) || std::holds_alternative<texdef_valve_t>(side.raw) ||
        std::holds_alternative<texdef_etp_t>(side.raw)) {
        const texdef_quake_ed_t &raw =
            std::holds_alternative<texdef_quake_ed_t>(side.raw) ? std::get<texdef_quake_ed_t>(side.raw)
            : std::holds_alternative<texdef_valve_t>(side.raw)
                ? reinterpret_cast<const texdef_quake_ed_t &>(std::get<texdef_valve_t>(side.raw))
                : reinterpret_cast<const texdef_quake_ed_t &>(std::get<texdef_etp_t>(side.raw));

        lua_createtable(state, 2, 0);
        lua_pushnumber(state, raw.shift[0]);
        lua_rawseti(state, -2, 1);
        lua_pushnumber(state, raw.shift[1]);
        lua_rawseti(state, -2, 2);
        lua_setfield(state, -2, "shift");

        lua_pushnumber(state, raw.rotate);
        lua_setfield(state, -2, "rotate");

        lua_createtable(state, 2, 0);
        lua_pushnumber(state, raw.scale[0]);
        lua_rawseti(state, -2, 1);
        lua_pushnumber(state, raw.scale[1]);
        lua_rawseti(state, -2, 2);
        lua_setfield(state, -2, "scale");

        if (std::holds_alternative<texdef_etp_t>(side.raw)) {
            const auto &raw_etp = std::get<texdef_etp_t>(side.raw);

            lua_pushboolean(state, raw_etp.tx2);
            lua_setfield(state, -2, "tx2");
        }
    }

    if (std::holds_alternative<texdef_valve_t>(side.raw) || std::holds_alternative<texdef_bp_t>(side.raw)) {
        const qmat<double, 2, 3> &axis = std::holds_alternative<texdef_valve_t>(side.raw)
                                             ? std::get<texdef_valve_t>(side.raw).axis
                                             : std::get<texdef_bp_t>(side.raw).axis;

        lua_createtable(state, 2, 0);

        for (size_t i = 0; i < 2; i++) {
            lua_createtable(state, 3, 0);
            for (size_t v = 0; v < 3; v++) {
                lua_pushnumber(state, axis.at(i, v));
                lua_rawseti(state, -2, v + 1);
            }
            lua_rawseti(state, -2, i + 1);
        }

        lua_setfield(state, -2, "axis");
    }

    if (std::holds_alternative<texdef_quake_ed_t>(side.raw)) {
        lua_setfield(state, -2, "quaked");
    }
    if (std::holds_alternative<texdef_etp_t>(side.raw)) {
        lua_setfield(state, -2, "etp");
    }
    if (std::holds_alternative<texdef_valve_t>(side.raw)) {
        lua_setfield(state, -2, "valve");
    } else {
        lua_setfield(state, -2, "bp");
    }

    lua_setfield(state, -2, "raw");

    // make plane
    lua_createtable(state, 4, 0);
    lua_pushnumber(state, side.plane.normal[0]);
    lua_rawseti(state, -2, 1);
    lua_pushnumber(state, side.plane.normal[1]);
    lua_rawseti(state, -2, 2);
    lua_pushnumber(state, side.plane.normal[2]);
    lua_rawseti(state, -2, 3);
    lua_pushnumber(state, side.plane.dist);
    lua_rawseti(state, -2, 4);
    lua_setfield(state, -2, "plane");

    // make plane points
    lua_createtable(state, 3, 0);

    for (size_t i = 0; i < 3; i++) {
        lua_createtable(state, 3, 0);

        for (size_t v = 0; v < 3; v++) {
            lua_pushnumber(state, side.planepts[i][v]);
            lua_rawseti(state, -2, v + 1);
        }

        lua_rawseti(state, -2, i + 1);
    }

    lua_setfield(state, -2, "plane_points");

    // set texture
    lua_pushstring(state, side.texture.c_str());
    lua_setfield(state, -2, "texture");

    if (side.extended_info) {
        // set info
        lua_createtable(state, 0, 3);
        lua_pushnumber(state, side.extended_info->contents);
        lua_setfield(state, -2, "contents");
        lua_pushnumber(state, side.extended_info->value);
        lua_setfield(state, -2, "value");
        lua_pushnumber(state, side.extended_info->flags);
        lua_setfield(state, -2, "flags");

        lua_setfield(state, -2, "info");
    }
}

static void maputil_make_brush(lua_State *state, const brush_t &brush)
{
    // make sides
    lua_createtable(state, brush.faces.size(), 0);

    for (size_t s = 0; s < brush.faces.size(); s++) {
        auto &side = brush.faces[s];

        maputil_make_brush_side(state, side);

        // put side into sides
        lua_rawseti(state, -2, s + 1);
    }
}

#define LUA_VERIFY_TOP_TYPE(type) Q_assert(lua_type(state, -1) == type)

static void maputil_copy_dict(lua_State *state, map_entity_t &entity)
{
    LUA_VERIFY_TOP_TYPE(LUA_TTABLE);

    // check for dict
    if (lua_getfield(state, -1, "dict") == LUA_TTABLE) {

        // iterate kvps
        size_t n = 0;

        while (true) {
            lua_rawgeti(state, -1, n + 1);

            if (lua_type(state, -1) == LUA_TNIL) {
                lua_pop(state, 1);
                break;
            }

            LUA_VERIFY_TOP_TYPE(LUA_TTABLE);
            n++;

            lua_rawgeti(state, -1, 1);
            const char *key = lua_tostring(state, -1);
            lua_pop(state, 1);
            lua_rawgeti(state, -1, 2);
            const char *value = lua_tostring(state, -1);
            lua_pop(state, 1);
            entity.epairs.set(key, value);

            lua_pop(state, 1);
        }
    }

    lua_pop(state, 1);
}

static texdef_quake_ed_t maputil_load_quaked(lua_State *state)
{
    texdef_quake_ed_t quaked;

    lua_getfield(state, -1, "shift");
    for (size_t i = 0; i < 2; i++) {
        lua_rawgeti(state, -1, i + 1);
        quaked.shift[i] = lua_tonumber(state, -1);
        lua_pop(state, 1);
    }
    lua_pop(state, 1);

    lua_getfield(state, -1, "rotate");
    quaked.rotate = lua_tonumber(state, -1);
    lua_pop(state, 1);

    lua_getfield(state, -1, "scale");
    for (size_t i = 0; i < 2; i++) {
        lua_rawgeti(state, -1, i + 1);
        quaked.scale[i] = lua_tonumber(state, -1);
        lua_pop(state, 1);
    }
    lua_pop(state, 1);

    return quaked;
}

static texdef_bp_t maputil_load_bp(lua_State *state)
{
    texdef_bp_t bp;

    lua_getfield(state, -1, "axis");

    for (size_t i = 0; i < 2; i++) {
        lua_rawgeti(state, -1, i + 1);

        for (size_t v = 0; v < 3; v++) {
            lua_rawgeti(state, -1, v + 1);
            bp.axis.at(i, v) = lua_tonumber(state, -1);
            lua_pop(state, 1);
        }

        lua_pop(state, 1);
    }

    lua_pop(state, 1);

    return bp;
}

static void maputil_copy_side(lua_State *state, brush_side_t &side)
{
    // texture
    lua_getfield(state, -1, "texture");
    side.texture = lua_tostring(state, -1);
    lua_pop(state, 1);

    // plane points
    lua_getfield(state, -1, "plane_points");

    for (size_t i = 0; i < 3; i++) {
        lua_rawgeti(state, -1, i + 1);

        for (size_t z = 0; z < 3; z++) {
            lua_rawgeti(state, -1, z + 1);
            side.planepts[i][z] = lua_tonumber(state, -1);
            lua_pop(state, 1);
        }

        lua_pop(state, 1);
    }

    lua_pop(state, 1);

    // raw
    lua_getfield(state, -1, "raw");

    if (lua_getfield(state, -1, "quaked") != LUA_TNIL) {
        side.raw = maputil_load_quaked(state);
    }
    lua_pop(state, 1);

    if (lua_getfield(state, -1, "valve") != LUA_TNIL) {
        texdef_bp_t bp = maputil_load_bp(state);
        texdef_quake_ed_t qed = maputil_load_quaked(state);

        side.raw = texdef_valve_t{.shift = qed.shift, .rotate = qed.rotate, .scale = qed.scale, .axis = bp.axis};
    }
    lua_pop(state, 1);

    if (lua_getfield(state, -1, "bp") != LUA_TNIL) {
        side.raw = maputil_load_bp(state);
    }
    lua_pop(state, 1);

    if (lua_getfield(state, -1, "etp") != LUA_TNIL) {
        texdef_quake_ed_t qed = maputil_load_quaked(state);

        lua_getfield(state, -1, "tx2");
        bool b = !!lua_toboolean(state, -1);
        lua_pop(state, 1);

        side.raw = texdef_etp_t{qed, b};
    }
    lua_pop(state, 1);

    lua_pop(state, 1);

    // extra info
    lua_getfield(state, -1, "info");

    if (lua_type(state, -1) == LUA_TTABLE) {
        texinfo_quake2_t q2;

        lua_getfield(state, -1, "contents");
        q2.contents = lua_tonumber(state, -1);
        lua_pop(state, 1);

        lua_getfield(state, -1, "value");
        q2.value = lua_tonumber(state, -1);
        lua_pop(state, 1);

        lua_getfield(state, -1, "flags");
        q2.flags = lua_tonumber(state, -1);
        lua_pop(state, 1);

        side.extended_info = q2;
    }

    lua_pop(state, 1);
}

static void maputil_copy_brush(lua_State *state, brush_t &brush)
{
    // count sides
    size_t num_sides = lua_count_array(state);
    brush.faces.resize(num_sides);

    // iterate brushes
    lua_iterate_array(state, [&brush](auto state, auto index) {
        brush_side_t &side = brush.faces[index];
        maputil_copy_side(state, side);
        return true;
    });
}

static void maputil_copy_brushes(lua_State *state, map_entity_t &entity)
{
    LUA_VERIFY_TOP_TYPE(LUA_TTABLE);

    // check for dict
    if (lua_getfield(state, -1, "brushes") == LUA_TTABLE) {

        // count brushes
        size_t num_brushes = lua_count_array(state);
        entity.brushes.resize(num_brushes);

        // iterate brushes
        lua_iterate_array(state, [&entity](auto state, auto index) {
            brush_t &brush = entity.brushes[index];
            maputil_copy_brush(state, brush);
            return true;
        });
    }

    lua_pop(state, 1);
}

static int l_commit_map(lua_State *state)
{
    map_file.entities.clear();

    // verify entities global
    lua_getglobal(state, "entities");

    LUA_VERIFY_TOP_TYPE(LUA_TTABLE);

    // count entities
    size_t num_entities = lua_count_array(state);

    // create entities
    map_file.entities.resize(num_entities);

    for (size_t i = 0; i < num_entities; i++) {
        auto &entity = map_file.entities[i];

        lua_rawgeti(state, -1, i + 1);

        maputil_copy_dict(state, entity);

        maputil_copy_brushes(state, entity);

        lua_pop(state, 1);
    }

    lua_pop(state, 1);

    return 0;
}

static inline qplane3d pop_plane_from_side(lua_State *state)
{
    qplane3d plane;

    lua_getfield(state, -1, "plane");

    for (size_t i = 0; i < 3; i++) {
        lua_rawgeti(state, -1, i + 1);
        plane.normal[i] = lua_tonumber(state, -1);
        lua_pop(state, 1);
    }
    lua_rawgeti(state, -1, 4);
    plane.dist = lua_tonumber(state, -1);
    lua_pop(state, 1);

    lua_pop(state, 1);

    return plane;
}

static int l_create_winding(lua_State *state)
{
    // -3 = face
    // -2 = brush
    // -1 = extents
    bool found_face = false;

    double extents = lua_tonumber(state, -1);
    lua_pop(state, 1);

    lua_pushvalue(state, -2);
    qplane3d side_plane = pop_plane_from_side(state);
    lua_pop(state, 1);

    using winding_t = polylib::winding_base_t<polylib::winding_storage_hybrid_t<double, 16>>;
    std::optional<winding_t> winding = winding_t::from_plane(side_plane, extents);

    // loop through sides on brush
    {
        if (lua_type(state, -1) == LUA_TTABLE) {
            lua_iterate_array(state, [&found_face, &winding](auto state, auto index) {
                LUA_VERIFY_TOP_TYPE(LUA_TTABLE);

                // check that the face is part of the brush
                if (lua_rawequal(state, -1, -3)) {
                    found_face = true;
                } else if (winding) {
                    qplane3d plane = pop_plane_from_side(state);
                    winding = winding->clip_front(-plane, 0.0f);
                }

                return true;
            });
        }

        lua_pop(state, 1);
    }

    if (!winding) {
        lua_pushnil(state);
    } else {
        lua_createtable(state, winding->size(), 0);

        for (size_t i = 0; i < winding->size(); i++) {
            lua_createtable(state, 3, 0);

            auto &p = winding->at(i);
            for (size_t v = 0; v < 3; v++) {
                lua_pushnumber(state, p[v]);
                lua_rawseti(state, -2, v + 1);
            }

            lua_rawseti(state, -2, i + 1);
        }
    }

    return 1;
}

static int l_load_texture_meta(lua_State *state)
{
    const char *path = lua_tostring(state, 1); /* get argument */
    lua_pop(state, 1);

    if (!current_game) {
        luaL_error(state, "need a game loaded with -game for this function");
    }

    auto result =
        std::get<0>(img::load_texture_meta(path, current_game, maputil_options)).value_or(img::texture_meta{});

    lua_createtable(state, 0, 5);

    lua_pushnumber(state, result.contents_native);
    lua_setfield(state, -2, "contents");
    lua_pushnumber(state, result.flags.native_q2);
    lua_setfield(state, -2, "flags");
    lua_pushnumber(state, result.value);
    lua_setfield(state, -2, "value");
    lua_pushnumber(state, result.width);
    lua_setfield(state, -2, "width");
    lua_pushnumber(state, result.height);
    lua_setfield(state, -2, "height");

    return 1;
}

static void maputil_setup_globals(lua_State *state)
{
    lua_pushcfunction(state, l_load_json);
    lua_setglobal(state, "load_json");

    lua_pushcfunction(state, l_commit_map);
    lua_setglobal(state, "commit_map");

    lua_pushcfunction(state, l_create_winding);
    lua_setglobal(state, "create_winding");

    lua_pushcfunction(state, l_load_texture_meta);
    lua_setglobal(state, "load_texture_meta");

    // constants
    lua_pushnumber(state, (int32_t)texcoord_style_t::quaked);
    lua_setglobal(state, "TEXCOORD_QUAKED");
    lua_pushnumber(state, (int32_t)texcoord_style_t::etp);
    lua_setglobal(state, "TEXCOORD_ETP");
    lua_pushnumber(state, (int32_t)texcoord_style_t::valve_220);
    lua_setglobal(state, "TEXCOORD_VALVE");
    lua_pushnumber(state, (int32_t)texcoord_style_t::brush_primitives);
    lua_setglobal(state, "TEXCOORD_BP");

    // convert map to a Lua representation.
    lua_createtable(state, map_file.entities.size(), 0);

    for (size_t i = 0; i < map_file.entities.size(); i++) {
        auto &entity = map_file.entities[i];

        // make entity table
        lua_createtable(state, 0, 2);

        // make dict
        if (entity.epairs.size()) {
            lua_createtable(state, entity.epairs.size(), 0);

            size_t ent = 0;

            for (auto &pair : entity.epairs) {
                lua_createtable(state, 2, 0);
                lua_pushstring(state, pair.first.c_str());
                lua_rawseti(state, -2, 1);
                lua_pushstring(state, pair.second.c_str());
                lua_rawseti(state, -2, 2);

                lua_rawseti(state, -2, ent + 1);
                ent++;
            }

            // push dict to entity
            lua_setfield(state, -2, "dict");
        }

        // make brushes
        if (!entity.brushes.empty()) {
            lua_createtable(state, entity.brushes.size(), 0);

            for (size_t b = 0; b < entity.brushes.size(); b++) {
                auto &brush = entity.brushes[b];

                maputil_make_brush(state, brush);

                // put brush into brushes
                lua_rawseti(state, -2, b + 1);
            }

            // push dict to entity
            lua_setfield(state, -2, "brushes");
        }

        // put entity into entities
        lua_rawseti(state, -2, i + 1);
    }

    lua_setglobal(state, "entities");
}

static lua_State *maputil_setup_lua()
{
    lua_State *state = luaL_newstate();

    luaL_openlibs(state);

    return state;
}

static void maputil_free_lua(lua_State *state)
{
    lua_close(state);
}

static int maputil_lua_error(lua_State *state)
{
    luaL_traceback(state, state, NULL, 1);

    logging::print("can't execute script: {}\n{}\n", lua_tostring(state, -1), lua_tostring(state, -2));

    return 0;
}
#endif

static void maputil_exec_script(const fs::path &file)
{
#ifdef USE_LUA
    lua_State *state = maputil_setup_lua();

    lua_pushcfunction(state, maputil_lua_error);

    int err = luaL_loadfile(state, file.string().c_str());

    if (err != LUA_OK) {
        logging::print("can't load script: {}\n", lua_tostring(state, -1));
    } else {
        maputil_setup_globals(state);

        err = lua_pcall(state, 0, 0, -2);

        if (err != LUA_OK) {

            lua_pop(state, 1);
        }
    }

    maputil_free_lua(state);
#else
    logging::print("maputil not compiled with Lua support\n");
#endif
}

static void maputil_exec_query(const char *query)
{
#ifdef USE_LUA
    logging::print("query: {}\n", query);

    lua_State *state = maputil_setup_lua();

    int err = luaL_loadstring(state, query);

    if (err != LUA_OK) {
        logging::print("can't load query: {}\n", lua_tostring(state, -1));
        lua_pop(state, 1);
    } else {
        maputil_setup_globals(state);

        lua_pushvalue(state, 1);

        int ref = luaL_ref(state, LUA_REGISTRYINDEX);

        lua_pop(state, 1);

        for (auto &entity : map_file.entities) {
            lua_createtable(state, 0, entity.epairs.size());

            for (auto &kvp : entity.epairs) {

                lua_pushstring(state, kvp.second.c_str());
                lua_setfield(state, -2, kvp.first.c_str());
            }

            lua_setglobal(state, "entity");

            lua_rawgeti(state, LUA_REGISTRYINDEX, ref);
            err = lua_pcall(state, 0, 1, 0);

            if (err != LUA_OK) {
                logging::print("can't execute query: {}\n", lua_tostring(state, -1));
                lua_pop(state, 1);
            } else {
                int b = lua_toboolean(state, -1);
                lua_pop(state, 1);

                if (b) {
                    logging::print("MATCHED: {} @ {}\n", entity.epairs.get("classname"), entity.location);
                }
            }

            lua_gc(state, LUA_GCCOLLECT);
        }

        luaL_unref(state, LUA_REGISTRYINDEX, ref);
    }

    maputil_free_lua(state);
#else
    logging::print("maputil not compiled with Lua support\n");
#endif
}

int maputil_main(int argc, const char **argv)
{
    logging::preinitialize();

    maputil_options.preinitialize(argc, argv);
    maputil_options.initialize(argc - 1, argv + 1);
    maputil_options.postinitialize(argc, argv);

    logging::init(std::nullopt, maputil_options);

    if (maputil_options.operations.empty()) {
        maputil_options.print_help(true);
        return 1;
    }

    fs::path source;

    for (auto &operation : maputil_options.operations) {

        if (operation->primary_name() == "load") {
            source = operation->string_value();

            if (!fs::exists(source)) {
                source = DefaultExtension(source, "map");
            }

            logging::print("---------------------\n");
            logging::print("{}\n", source);

            map_file = LoadMapOrEntFile(source);
        } else if (operation->primary_name() == "game") {
            const std::string &gamename = operation->string_value();

            current_game = nullptr;

            for (auto &game : gamedef_list()) {
                if (!Q_strcasecmp(game->friendly_name, gamename)) {
                    current_game = game;
                    break;
                }
            }

            if (!current_game) {
                FError("not sure what game {} is\n", gamename);
            }

            current_game->init_filesystem(source, maputil_options);
        } else if (operation->primary_name() == "script") {
            maputil_exec_script(operation->string_value().c_str());
        } else if (operation->primary_name() == "query") {
            maputil_exec_query(operation->string_value().c_str());
        } else if (operation->primary_name() == "strip_extended_info") {
            for (auto &entity : map_file.entities) {
                for (auto &brush : entity.brushes) {
                    for (auto &face : brush.faces) {
                        face.extended_info = std::nullopt;
                    }
                }
            }
        } else if (operation->primary_name() == "convert") {

            std::string type = operation->string_value();
            texcoord_style_t dest_style;

            if (string_iequals(type, "quake")) {
                dest_style = texcoord_style_t::quaked;
            } else if (string_iequals(type, "valve")) {
                dest_style = texcoord_style_t::valve_220;
            } else if (string_iequals(type, "etp")) {
                dest_style = texcoord_style_t::etp;
            } else if (string_iequals(type, "bp")) {
                dest_style = texcoord_style_t::brush_primitives;
            } else {
                FError("unknown map style {}", type);
            }

            map_file.convert_to(dest_style, current_game, maputil_options);
        } else if (operation->primary_name() == "save") {
            fs::path dest = DefaultExtension(operation->string_value(), "map");
            logging::print("saving to {}...\n", dest);

            std::ofstream stream(dest);
            map_file.write(stream);
        } else {
            Error("option not implemented: {}", operation->primary_name());
        }
    }

    return 0;
}
