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

    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 <cstring>
#include <algorithm>

#include <common/log.hh>
#include <common/aabb.hh>
#include <common/fs.hh>
#include <common/settings.hh>

#include <qbsp/brush.hh>
#include <qbsp/exportobj.hh>
#include <qbsp/map.hh>
#include <qbsp/portals.hh>
#include <qbsp/prtfile.hh>
#include <qbsp/brushbsp.hh>
#include <qbsp/faces.hh>
#include <qbsp/qbsp.hh>
#include <qbsp/writebsp.hh>
#include <qbsp/outside.hh>
#include <qbsp/tjunc.hh>
#include <qbsp/tree.hh>
#include <qbsp/csg.hh>
#include <common/mapfile.hh>

#include <fmt/chrono.h>

namespace settings
{
bool wadpath::operator<(const wadpath &other) const
{
    return path < other.path;
}

// setting_wadpathset

setting_wadpathset::setting_wadpathset(
    setting_container *dictionary, const nameset &names, const setting_group *group, const char *description)
    : setting_base(dictionary, names, group, description)
{
}

void setting_wadpathset::addPath(const wadpath &path)
{
    _paths.insert(path);
}

const std::set<wadpath> &setting_wadpathset::pathsValue() const
{
    return _paths;
}

bool setting_wadpathset::copy_from(const setting_base &other)
{
    if (auto *casted = dynamic_cast<const setting_wadpathset *>(&other)) {
        _paths = casted->_paths;
        _source = casted->_source;
        return true;
    }
    return false;
}

void setting_wadpathset::reset()
{
    _paths = {};
    _source = source::DEFAULT;
}

bool setting_wadpathset::parse(const std::string &setting_name, parser_base_t &parser, source source)
{
    if (!parser.parse_token()) {
        return false;
    }

    if (change_source(source)) {
        _paths.insert(wadpath{fs::path(parser.token), setting_name[0] == 'x'});
    }

    return true;
}

std::string setting_wadpathset::string_value() const
{
    std::string paths;

    for (auto &path : _paths) {
        if (!paths.empty()) {
            paths += " ; ";
        }

        paths += path.path.string();

        if (path.external) {
            paths += " (external)";
        }
    }

    return paths;
}

std::string setting_wadpathset::format() const
{
    return "path/to/wads";
}

// setting_tjunc

bool setting_tjunc::parse(const std::string &setting_name, parser_base_t &parser, source source)
{
    if (setting_name == "notjunc") {
        this->set_value(tjunclevel_t::NONE, source);
        return true;
    }

    return this->setting_enum<tjunclevel_t>::parse(setting_name, parser, source);
}

// setting_blocksize

setting_blocksize::setting_blocksize(setting_container *dictionary, const nameset &names, qvec3i val,
    const setting_group *group, const char *description)
    : setting_value(dictionary, names, val, group, description)
{
}

bool setting_blocksize::parse(const std::string &setting_name, parser_base_t &parser, source source)
{
    qvec3d vec = {1024, 1024, 1024};

    for (int i = 0; i < 3; i++) {
        if (!parser.parse_token(PARSE_PEEK)) {
            return false;
        }

        // don't allow negatives
        if (parser.token[0] != '-') {
            try {
                vec[i] = std::stol(parser.token);
                parser.parse_token();
                continue;
            } catch (std::exception &) {
                // intentional fall-through
            }
        }

        // if we didn't parse a valid number, fail
        if (i == 0) {
            return false;
        } else if (i == 1) {
            // we parsed one valid number; use it all the way through
            vec[1] = vec[2] = vec[0];
        }

        // for [x, y] z will be left default
    }

    set_value(vec, source);

    return true;
}

std::string setting_blocksize::string_value() const
{
    return qv::to_string(_value);
}

std::string setting_blocksize::format() const
{
    return "[x [y [z]]]";
}

// setting_debugexpand

setting_debugexpand::setting_debugexpand(
    setting_container *dictionary, const nameset &names, const setting_group *group, const char *description)
    : setting_value(dictionary, names, {}, group, description)
{
}

bool setting_debugexpand::parse(const std::string &setting_name, parser_base_t &parser, source source)
{
    std::array<double, 6> values;
    size_t i = 0;

    try {
        for (; i < 6; i++) {
            if (!parser.parse_token(PARSE_PEEK)) {
                throw std::exception();
            }

            values[i] = std::stod(parser.token);

            parser.parse_token();
        }

        this->set_value(aabb3d{{values[0], values[1], values[2]}, {values[3], values[4], values[5]}}, source);

        return true;
    } catch (std::exception &) {
        // single hull value
        if (i == 1) {
            set_value(static_cast<uint8_t>(values[0]), source);
            return true;
        }

        return false;
    }
}

std::string setting_debugexpand::string_value() const
{
    return is_hull() ? std::to_string(hull_index_value()) : fmt::format("{}", hull_bounds_value());
}

std::string setting_debugexpand::format() const
{
    return "[single hull index] or [mins_x mins_y mins_z maxs_x maxs_y maxs_z]";
}

bool setting_debugexpand::is_hull() const
{
    return std::holds_alternative<uint8_t>(_value);
}

const uint8_t &setting_debugexpand::hull_index_value() const
{
    return std::get<uint8_t>(_value);
}

const aabb3d &setting_debugexpand::hull_bounds_value() const
{
    return std::get<aabb3d>(_value);
}
} // namespace settings

static auto as_tuple(const maptexinfo_t &info)
{
    return std::tie(info.vecs, info.miptex, info.flags, info.value, info.next);
}

bool maptexinfo_t::operator<(const maptexinfo_t &other) const
{
    return as_tuple(*this) < as_tuple(other);
}

bool maptexinfo_t::operator>(const maptexinfo_t &other) const
{
    return as_tuple(*this) > as_tuple(other);
}

const maptexinfo_t &face_t::get_texinfo() const
{
    return map.mtexinfos[texinfo];
}

const qbsp_plane_t &face_t::get_plane() const
{
    return map.get_plane(planenum);
}

const qbsp_plane_t &face_t::get_positive_plane() const
{
    return map.get_plane(planenum & ~1);
}

const qbsp_plane_t &nodedata_t::get_plane() const
{
    return map.get_plane(planenum);
}

plane_type_t qbsp_plane_t::calculate_type(const qplane3d &p)
{
    return calculate_plane_type(p);
}

qbsp_plane_t::qbsp_plane_t(const qplane3d &plane, bool flip) noexcept
    : plane(plane)
{
    normalize(flip);
}

qbsp_plane_t::qbsp_plane_t(const qplane3d &plane) noexcept
    : qbsp_plane_t(plane, false)
{
}

qbsp_plane_t &qbsp_plane_t::operator=(const qplane3d &plane) noexcept
{
    this->plane = plane;
    normalize(false);
    return *this;
}

[[nodiscard]] qbsp_plane_t qbsp_plane_t::operator-() const
{
    qbsp_plane_t copy = *this;
    copy.plane = -copy.plane;
    return copy;
}

[[nodiscard]] const plane_type_t &qbsp_plane_t::get_type() const
{
    return type;
}
[[nodiscard]] double qbsp_plane_t::get_dist() const
{
    return plane.dist;
}
[[nodiscard]] const qvec3d &qbsp_plane_t::get_normal() const
{
    return plane.normal;
}
bool qbsp_plane_t::set_normal(const qvec3d &vec, bool flip)
{
    plane.normal = vec;
    return normalize(flip);
}

bool qbsp_plane_t::set_plane(const qplane3d &plane, bool flip)
{
    this->plane = plane;
    return normalize(flip);
}

[[nodiscard]] const qplane3d &qbsp_plane_t::get_plane() const
{
    return plane;
}
[[nodiscard]] qbsp_plane_t::operator const qplane3d &() const
{
    return plane;
}

// normalize the given plane, optionally flipping it to face
// the positive direction. returns whether the plane was flipped or not.
bool qbsp_plane_t::normalize(bool flip) noexcept
{
    for (size_t i = 0; i < 3; i++) {
        if (plane.normal[i] == 1.0) {
            plane.normal[(i + 1) % 3] = 0;
            plane.normal[(i + 2) % 3] = 0;
            type = (i == 0 ? plane_type_t::PLANE_X : i == 1 ? plane_type_t::PLANE_Y : plane_type_t::PLANE_Z);
            return false;
        }
        if (plane.normal[i] == -1.0) {
            if (flip) {
                plane.normal[i] = 1.0;
                plane.dist = -plane.dist;
            }
            plane.normal[(i + 1) % 3] = 0;
            plane.normal[(i + 2) % 3] = 0;
            type = (i == 0 ? plane_type_t::PLANE_X : i == 1 ? plane_type_t::PLANE_Y : plane_type_t::PLANE_Z);
            return flip;
        }
    }

    double ax = fabs(plane.normal[0]);
    double ay = fabs(plane.normal[1]);
    double az = fabs(plane.normal[2]);

    size_t nearest;

    if (ax >= ay && ax >= az) {
        nearest = 0;
        type = plane_type_t::PLANE_ANYX;
    } else if (ay >= ax && ay >= az) {
        nearest = 1;
        type = plane_type_t::PLANE_ANYY;
    } else {
        nearest = 2;
        type = plane_type_t::PLANE_ANYZ;
    }

    if (flip && plane.normal[nearest] < 0) {
        plane = -plane;
        return true;
    }

    return false;
}

namespace qv
{
[[nodiscard]] bool epsilonEqual(
    const qbsp_plane_t &p1, const qbsp_plane_t &p2, double normalEpsilon, double distEpsilon)
{
    // axial planes will never match on normal, so we can skip that check entirely
    if (p1.get_type() < plane_type_t::PLANE_ANYX && p2.get_type() < plane_type_t::PLANE_ANYX) {
        // if we aren't the same type, we definitely aren't equal
        if (p1.get_type() != p2.get_type()) {
            return false;
        } else if (p1.get_normal()[static_cast<int32_t>(p1.get_type())] !=
                   p2.get_normal()[static_cast<int32_t>(p2.get_type())]) {
            // axials will always be only 1 or -1
            return false;
        }

        // check dist
        return epsilonEqual(p1.get_dist(), p2.get_dist(), distEpsilon);
    }

    // check dist
    if (!epsilonEqual(p1.get_dist(), p2.get_dist(), distEpsilon)) {
        return false;
    }

    // check normal
    return epsilonEqual(p1.get_normal(), p2.get_normal(), normalEpsilon);
}
}; // namespace qv

// command line flags
namespace settings
{
setting_group game_target_group{"Game/BSP Target", -1, expected_source::commandline};
setting_group map_development_group{"Map development", 1, expected_source::commandline};
setting_group common_format_group{"Common format options", 2, expected_source::commandline};
setting_group debugging_group{"Advanced/tool debugging", 500, expected_source::commandline};

inline void set_target_version(const bspversion_t *version)
{
    if (qbsp_options.target_version) {
        FError("BSP version was set by multiple flags; currently {}, tried to change to {}\n",
            qbsp_options.target_version->name, version->name);
    }

    qbsp_options.target_version = version;
}

qbsp_settings::qbsp_settings()
    : hexen2{this, "hexen2", false, &game_target_group, "target Hexen II's BSP format"}, // tests, docs
      hlbsp{this, "hlbsp", false, &game_target_group, "target Half Life's BSP format"}, // tests, docs
      q2bsp{this, "q2bsp", false, &game_target_group, "target Quake II's BSP format"}, // tests, docs
      qbism{this, "qbism", false, &game_target_group, "target Qbism's extended Quake II BSP format"}, // tests, docs
      bsp2{this, "bsp2", false, &game_target_group, "target Quake's extended BSP2 format"}, // tests, docs
      bsp2rmq{this, "2psb", false, &game_target_group, // tests, docs
          "target Quake's extended 2PSB format (RMQ compatible)"},
      nosubdivide{this, "nosubdivide", // tests, docs
          [&](const std::string &, parser_base_t &, source src) {
              subdivide.set_value(0, src);
              return true;
          },
          &common_format_group, "disable subdivision"},
      software{this, "software", true, &common_format_group,
          "change settings to allow for (or make adjustments to optimize for the lack of) software support"},
      subdivide{this, "subdivide", 240, &common_format_group,
          "change the subdivide threshold, in luxels. 0 will disable subdivision entirely"},
      nofill{this, "nofill", false, &debugging_group, "don't perform outside filling"}, // tests, docs
      nomerge{this, "nomerge", false, &debugging_group, "don't perform face merging"}, // tests, docs
      nomergeacrossliquids{this, "nomergeacrossliquids", false, &common_format_group, "deprecated, no effect"},
      noedgereuse{this, "noedgereuse", false, &debugging_group, "don't reuse edges (for debugging software rendering)"},
      noclip{this, "noclip", false, &common_format_group, "don't write clip nodes (Q1-like BSP formats)"},
      noskip{this, "noskip", false, &debugging_group, "don't remove faces with the 'skip' texture"},
      nodetail{this, "nodetail", false, &debugging_group, "treat all detail brushes to structural"}, // tests, docs
      chop{this, "chop", false, &debugging_group, "adjust brushes to remove intersections if possible"},
      chopfragment{this, "chopfragment", false, &debugging_group, "always do full fragmentation for chop"},
      onlyents{this, "onlyents", false, &map_development_group, "only updates .MAP entities"}, // tests, docs
      splitsky{this, "splitsky", false, &debugging_group, "doesn't combine sky faces into one large face"},
      splitturb{this, {"litwater", "splitturb"}, true, &common_format_group,
          "doesn't combine water faces into one large face"},
      splitspecial{this, "splitspecial", {&splitsky, &splitturb}, &debugging_group,
          "doesn't combine sky and water faces into one large face (splitturb + splitsky)"},
      transwater{this, "transwater", true, &common_format_group, // tests, docs
          "compute portal information for transparent water"},
      notextures{this, "notex", false, &common_format_group,
          "write only placeholder textures to depend upon replacements, keep file sizes down, or to skirt copyrights"}, // tests, docs
      missing_textures_as_zero_size{
          this, "missing_textures_as_zero_size", false, &common_format_group, "write missing textures as 0x0"}, // tests, docs
      convertmapformat{this, "convert", conversion_t::none,
          {{"quake", conversion_t::quake}, {"quake2", conversion_t::quake2}, {"valve", conversion_t::valve},
              {"bp", conversion_t::bp}},
          &common_format_group, "convert a .MAP to a different .MAP format"},
      oldaxis{this, "oldaxis", true, &debugging_group,
          "uses alternate texture alignment which was default in tyrutils-ericw v0.15.1 and older"},
      forcegoodtree{
          this, "forcegoodtree", false, &debugging_group, "force use of expensive processing for BrushBSP stage"},
      midsplitsurffraction{this, "midsplitsurffraction", 0.f, 0.f, 1.f, &debugging_group,
          "if 0 (default), use `maxnodesize` for deciding when to switch to midsplit bsp heuristic.\nif 0 < midsplitSurfFraction <= 1, switch to midsplit if the node contains more than this fraction of the model's\ntotal surfaces. Try 0.15 to 0.5. Works better than maxNodeSize for maps with a 3D skybox (e.g. +-128K unit maps)"},
      maxnodesize{this, "maxnodesize", 1024, &debugging_group,
          "triggers simpler BSP Splitting when node exceeds size (default 1024, 0 to disable)"},
      oldrottex{this, "oldrottex", false, &debugging_group, "use old rotate_ brush texturing aligned at (0 0 0)"},
      epsilon{this, "epsilon", 0.0001, 0.0, 1.0, &debugging_group, "customize epsilon value for point-on-plane checks"},
      microvolume{this, "microvolume", 0.0, 0.0, 1000.0, &debugging_group, "microbrush volume"},
      leaktest{this, "leaktest", false, &map_development_group, "make compilation fail if the map leaks"}, // tests, docs
      outsidedebug{this, "outsidedebug", false, &debugging_group,
          "write a .map after outside filling showing non-visible brush sides"},
      debugchop{this, "debugchop", false, &debugging_group, "write a .map after ChopBrushes"},
      debugleak{this, "debugleak", false, &debugging_group, "write more diagnostic files for debugging leaks"},
      debugbspbrushes{this, "debugbspbrushes", false, &debugging_group,
          "save bsp brushes after BrushBSP to a .map, for visualizing BSP splits"},
      debugleafvolumes{this, "debugleafvolumes", false, &debugging_group,
          "save bsp leaf volumes after BrushBSP to a .map, for visualizing BSP splits"},
      debugexpand{this, "debugexpand", &debugging_group,
          "write expanded hull .map for debugging/inspecting hulls/brush bevelling"},
      keepprt{this, "keepprt", false, &debugging_group, "avoid deleting the .prt file on leaking maps"},
      includeskip{this, "includeskip", false, &common_format_group,
          "don't cull skip faces from the list of renderable surfaces (Q2RTX)"},
      worldextent{this, "worldextent", 0.0, &debugging_group, "explicitly provide world extents; 0 will auto-detect"},
      leakdist{this, "leakdist", 0, &debugging_group, "space between leakfile points (default 0: no inbetween points)"},
      forceprt1{
          this, "forceprt1", false, &debugging_group, "force a PRT1 output file even if PRT2 is required for vis"},
      tjunc{this, {"tjunc", "notjunc"}, tjunclevel_t::MWT,
          {{"none", tjunclevel_t::NONE}, {"rotate", tjunclevel_t::ROTATE}, {"retopologize", tjunclevel_t::RETOPOLOGIZE},
              {"mwt", tjunclevel_t::MWT}},
          &debugging_group, "T-junction fix level"}, // tests, docs
      objexport{
          this, "objexport", false, &debugging_group, "export the map file as .OBJ models during various CSG phases"},
      noextendedsurfflags{this, "noextendedsurfflags", false, &debugging_group, "suppress writing a .texinfo file"}, // tests, docs
      noextendedcontentflags{
          this, "noextendedcontentflags", false, &debugging_group, "suppress writing a .content.json file"}, // tests
      wrbrushes{this, {"wrbrushes", "bspx"}, false, &common_format_group,
          "includes a list of brushes for brush-based collision"}, // tests, docs
      wrbrushesonly{this, {"wrbrushesonly", "bspxonly"}, {&wrbrushes, &noclip}, &common_format_group,
          "includes BSPX brushes and does not output clipping hulls (wrbrushes + noclip)"},
      bmodelcontents{this, "bmodelcontents", false, &common_format_group,
          "allow control over brush contents in bmodels, don't force CONTENTS_SOLID"}, // tests, docs
      omitdetail{this, "omitdetail", false, &map_development_group, "omit *all* detail brushes from the compile"}, // tests, docs
      omitdetailwall{this, "omitdetailwall", false, &map_development_group,
          "func_detail_wall brushes are omitted from the compile"},
      omitdetailillusionary{this, "omitdetailillusionary", false, &map_development_group,
          "func_detail_illusionary brushes are omitted from the compile"},
      omitdetailfence{this, "omitdetailfence", false, &map_development_group,
          "func_detail_fence brushes are omitted from the compile"},
      wadpaths{this, {"wadpath", "xwadpath"}, &map_development_group,
          "add a path to the wad search paths; wads found in xwadpath's will not be embedded, otherwise they will be embedded (if not -notex)"},
      notriggermodels{this, "notriggermodels", false, &common_format_group,
          "for supported game code only: triggers will not write a model\nout, and will instead just write out their mins/maxs."},
      aliasdefs{this, "aliasdef", "\"path/to/file.def\" <multiple allowed>", &map_development_group,
          "path to an alias definition file, which can transform entities in the .map into other entities."},
      texturedefs{this, "texturedefs", "\"path/to/file.def\" <multiple allowed>", &map_development_group,
          "path to a texture definition file, which can transform textures in the .map into other textures."},
      lmscale{this, "lmscale", 1.0, &common_format_group,
          "change global lmscale (force _lmscale key on all entities). outputs the LMSCALE BSPX lump."},
      filltype{this, "filltype", filltype_t::INSIDE,
          {{"auto", filltype_t::AUTO}, {"inside", filltype_t::INSIDE}, {"outside", filltype_t::OUTSIDE}},
          &common_format_group,
          "whether to fill the map from the outside in (lenient), from the inside out (aggressive), or to automatically decide based on the hull being used."},
      filldetail{this, "filldetail", true, &common_format_group,
          "whether to fill in empty spaces which are fully enclosed by detail solid"},
      allow_upgrade{this, "allowupgrade", true, &common_format_group,
          "allow formats to \"upgrade\" to compatible extended formats when a limit is exceeded (ie Quake BSP to BSP2)"},
      maxedges{[](setting_int32 &setting) { return setting.value() == 0 || setting.value() >= 3; }, this, "maxedges",
          64, &map_development_group,
          "the max number of edges/vertices on a single face before it is split into another face"}, // tests, docs
      midsplitbrushfraction{this, "midsplitbrushfraction", 0.0, &common_format_group,
          "switch to cheaper partitioning if a node contains this % of brushes in the map"},
      add{this, "add", "", "", &common_format_group, "the given map file will be appended to the base map"}, // tests, docs
      scale{this, "scale", 1.0, &map_development_group,
          "scales the map brushes and point entity origins by a give factor"},
      loghulls{this, {"loghulls"}, false, &logging_group, "print log output for collision hulls"},
      logbmodels{this, {"logbmodels"}, false, &logging_group, "print log output for bmodels"},
      debug_missing_portal_sides{this, {"debug_missing_portal_sides"}, false, &logging_group,
          "output debug .prt files for missing portal sides"},
      fixupdetailfence{this, {"fixupdetailfence"}, true, &debugging_group, "fixup detail fence"}
{
}

void qbsp_settings::set_parameters(int argc, const char **argv)
{
    common_settings::set_parameters(argc, argv);
    program_description =
        "qbsp performs geometric level processing of Quake .MAP files to create\nQuake .BSP files.\n\n";
    remainder_name = "sourcefile.map [destfile.bsp]";
}

void qbsp_settings::initialize(int argc, const char **argv)
{
    if (auto file = fs::load("qbsp.ini")) {
        logging::print("Loading options from qbsp.ini\n");
        parser_t p(file, {"qbsp.ini"});
        parse(p);
    }

    try {
        common_settings::initialize(argc - 1, argv + 1);

        if (remainder.size() <= 0 || remainder.size() > 2) {
            print_help(true);
        }

        qbsp_options.map_path = remainder[0];

        if (remainder.size() == 2) {
            qbsp_options.bsp_path = remainder[1];
        }
    } catch (parse_exception &ex) {
        print_help(false);
        logging::print("ERROR OCCURRED WHEN TRYING TO PARSE ARGUMENTS:\n");
        logging::print(ex.what());
        logging::print("\n\n");
        throw settings::quit_after_help_exception();
    }
}

void qbsp_settings::load_texture_def(const std::string &pathname)
{
    if (!fs::exists(pathname)) {
        FError("can't find texturedef file {}", pathname);
    }

    fs::data data = fs::load(pathname);
    parser_t parser(data, {pathname});

    while (true) {
        if (!parser.parse_token() || parser.at_end()) {
            break;
        }

        std::string from = std::move(parser.token);

        if (!parser.parse_token(PARSE_SAMELINE)) {
            break;
        }

        std::string to = std::move(parser.token);
        std::optional<extended_texinfo_t> texinfo;

        // FIXME: why is this necessary? is it a trailing \0? only happens on release
        // repro with a texdef with no newline at the end
        while (std::isspace(to[to.size() - 1])) {
            to.resize(to.size() - 1);
        }

        if (parser.parse_token(PARSE_SAMELINE | PARSE_OPTIONAL)) {
            uint32_t native = std::stoul(parser.token);

            texinfo = extended_texinfo_t{.contents_native = native};

            if (parser.parse_token(PARSE_SAMELINE | PARSE_OPTIONAL)) {
                texinfo->flags.native_q2 = static_cast<q2_surf_flags_t>(std::stoi(parser.token));
            }

            if (parser.parse_token(PARSE_SAMELINE | PARSE_OPTIONAL)) {
                texinfo->value = std::stoi(parser.token);
            }
        }

        loaded_texture_defs[from] = {to, texinfo};
    }
}

void qbsp_settings::load_entity_def(const std::string &pathname)
{
    if (!fs::exists(pathname)) {
        FError("can't find aliasdef file {}", pathname);
    }

    fs::data data = fs::load(pathname);
    parser_t parser(data, {pathname});

    while (true) {
        if (!parser.parse_token() || parser.at_end()) {
            break;
        }

        std::string classname = std::move(parser.token);

        if (!parser.parse_token(PARSE_PEEK)) {
            FError("expected {{ in alias def {}, got end of file", pathname);
        }

        if (parser.token != "{") {
            FError("expected {{ in alias def {}, got {}", pathname, parser.token);
        }

        // parse ent dict
        loaded_entity_defs[classname] = parser;
    }
}

void qbsp_settings::postinitialize(int argc, const char **argv)
{
    // set target BSP type
    if (hlbsp.value()) {
        set_target_version(&bspver_hl);
    }

    if (q2bsp.value() || (q2rtx.value() && !q2bsp.is_changed() && !qbism.is_changed())) {
        set_target_version(&bspver_q2);
    }

    if (qbism.value()) {
        set_target_version(&bspver_qbism);
    }

    if (bsp2.value()) {
        set_target_version(&bspver_bsp2);
    }

    if (bsp2rmq.value()) {
        set_target_version(&bspver_bsp2rmq);
    }

    if (!qbsp_options.target_version) {
        set_target_version(&bspver_q1);
    }

    // if we wanted hexen2, update it now
    if (hexen2.value()) {
        if (qbsp_options.target_version == &bspver_bsp2) {
            qbsp_options.target_version = &bspver_h2bsp2;
        } else if (qbsp_options.target_version == &bspver_bsp2rmq) {
            qbsp_options.target_version = &bspver_h2bsp2rmq;
        } else {
            qbsp_options.target_version = &bspver_h2;
        }
    } else {
        if (!qbsp_options.target_version) {
            qbsp_options.target_version = &bspver_q1;
        }
    }

    // update target game
    qbsp_options.target_game = qbsp_options.target_version->game;

    /* If no wadpath given, default to the map directory */
    if (wadpaths.pathsValue().empty()) {
        wadpath wp{qbsp_options.map_path.parent_path(), false};

        // If options.map_path is a relative path, StrippedFilename will return the empty string.
        // In that case, don't add it as a wad path.
        if (!wp.path.empty()) {
            wadpaths.addPath(wp);
        }
    }

    // side effects from q2rtx
    if (q2rtx.value()) {
        if (!includeskip.is_changed()) {
            includeskip.set_value(true, settings::source::GAME_TARGET);
        }

        if (!software.is_changed()) {
            software.set_value(false, settings::source::GAME_TARGET);
        }
    }

    // side effects from Quake II
    if (qbsp_options.target_game->id == GAME_QUAKE_II) {
        if (!maxedges.is_changed()) {
            maxedges.set_value(0, settings::source::GAME_TARGET);
        }

        if (qbsp_options.target_version == &bspver_qbism) {
            if (!software.is_changed()) {
                software.set_value(false, settings::source::GAME_TARGET);
            }
        }

        if (!software.value() && !subdivide.is_changed()) {
            subdivide.set_value(496, settings::source::GAME_TARGET);
        }

        if (!qbsp_options.chop.is_changed()) {
            qbsp_options.chop.set_value(true, settings::source::GAME_TARGET);
        }
    }

    // load texture defs
    for (auto &def : texturedefs.values()) {
        load_texture_def(def);
    }

    for (auto &def : aliasdefs.values()) {
        load_entity_def(def);
    }

    common_settings::postinitialize(argc, argv);
}

void qbsp_settings::reset()
{
    common_settings::reset();

    target_version = nullptr;
    target_game = nullptr;
    map_path = fs::path();
    bsp_path = fs::path();
    loaded_texture_defs.clear();
    loaded_entity_defs.clear();
}
}; // namespace settings

settings::qbsp_settings qbsp_options;

struct brush_list_stats_t : logging::stat_tracker_t
{
    stat &total_brushes = register_stat("total brushes");
    stat &total_brush_sides = register_stat("total brush sides");
    stat &total_leaf_brushes = register_stat("total leaf brushes");
    stat &total_optimized_faces = register_stat("optimized brush side texinfos");
};

static void ExportBrushList_r(const mapentity_t &entity, node_t *node, brush_list_stats_t &stats)
{
    if (node->is_leaf()) {
        auto *leafdata = node->get_leafdata();
        int native = qbsp_options.target_game->contents_to_native(
            qbsp_options.target_game->contents_remap_for_export(leafdata->contents, gamedef_t::remap_type_t::leaf));
        if (native) {
            if (leafdata->original_brushes.size()) {
                leafdata->numleafbrushes = leafdata->original_brushes.size();
                stats.total_leaf_brushes += leafdata->numleafbrushes;
                leafdata->firstleafbrush = map.bsp.dleafbrushes.size();
                for (auto &b : leafdata->original_brushes) {

                    if (!b->mapbrush->outputnumber.has_value()) {
                        b->mapbrush->outputnumber = {static_cast<uint32_t>(map.bsp.dbrushes.size())};

                        int brushcontents = qbsp_options.target_game->contents_to_native(
                            qbsp_options.target_game->contents_remap_for_export(
                                b->contents, gamedef_t::remap_type_t::brush));

                        dbrush_t &brush = map.bsp.dbrushes.emplace_back(
                            dbrush_t{.firstside = static_cast<int32_t>(map.bsp.dbrushsides.size()),
                                .numsides = 0,
                                .contents = brushcontents});

                        for (auto &side : b->mapbrush->faces) {

                            maptexinfo_t &texinfo = map.mtexinfos[side.texinfo];
                            int texinfo_id = side.texinfo;

                            if (!texinfo.outputnum.has_value()) {
                                // optimize BSP by using existing world-aligned texinfo
                                // if they exist
                                maptexinfo_t copy = texinfo;
                                mapfile::brush_side_t temp;
                                temp.plane = side.get_plane();
                                temp.set_texinfo(mapfile::texdef_quake_ed_t{{0, 0}, 0, {1, 1}});
                                copy.vecs = temp.vecs;

                                texinfo_id = FindTexinfo(copy, temp.plane, false);

                                if (texinfo_id == -1) {
                                    texinfo_id = side.texinfo;
                                } else if (texinfo_id != side.texinfo) {
                                    stats.total_optimized_faces++;
                                }
                            }

                            map.bsp.dbrushsides.push_back(
                                {(uint32_t)ExportMapPlane(side.planenum), (int32_t)ExportMapTexinfo(texinfo_id)});
                            brush.numsides++;
                            stats.total_brush_sides++;
                        }

                        stats.total_brushes++;
                    }

                    map.bsp.dleafbrushes.push_back(b->mapbrush->outputnumber.value());
                }
            }
        }

        return;
    }

    auto *nodedata = node->get_nodedata();
    ExportBrushList_r(entity, nodedata->children[0], stats);
    ExportBrushList_r(entity, nodedata->children[1], stats);
}

static void ExportBrushList(mapentity_t &entity, node_t *node)
{
    logging::funcheader();

    brush_list_stats_t stats;

    ExportBrushList_r(entity, node, stats);
}

static bool IsTrigger(const mapentity_t &entity)
{
    auto &tex = entity.mapbrushes.front().faces[0].texname;

    if (tex.length() < 6) {
        return false;
    }

    size_t trigger_pos = tex.rfind("trigger");

    if (trigger_pos == std::string::npos) {
        return false;
    }

    return trigger_pos == (tex.size() - strlen("trigger"));
}

static void CountLeafs_r(node_t *node, content_stats_t &stats)
{
    if (auto *leafdata = node->get_leafdata()) {
        stats.count_contents_in_stats(leafdata->contents);
        return;
    }

    auto *nodedata = node->get_nodedata();
    CountLeafs_r(nodedata->children[0], stats);
    CountLeafs_r(nodedata->children[1], stats);
}

static int NodeHeight(node_t *node)
{
    if (node->parent) {
        return 1 + NodeHeight(node->parent);
    }
    return 1;
}

static void CountLeafHeights_r(node_t *node, std::vector<int> &heights)
{
    if (node->is_leaf()) {
        heights.push_back(NodeHeight(node));
        return;
    }
    auto *nodedata = node->get_nodedata();
    CountLeafHeights_r(nodedata->children[0], heights);
    CountLeafHeights_r(nodedata->children[1], heights);
}

void CountLeafs(node_t *headnode)
{
    logging::funcheader();

    auto stats = content_stats_t();
    CountLeafs_r(headnode, stats);
    stats.print_content_stats("leafs");

    // count the heights of the tree at each leaf
    logging::stat_tracker_t stat_print;

    std::vector<int> leaf_heights;
    CountLeafHeights_r(headnode, leaf_heights);

    const int max_height = *std::max_element(leaf_heights.begin(), leaf_heights.end());
    stat_print.register_stat("max tree height").count += max_height;

    double avg_height = 0;
    for (int height : leaf_heights) {
        avg_height += (height / static_cast<double>(leaf_heights.size()));
    }
    stat_print.register_stat("avg tree height").count += static_cast<int>(avg_height);
}

static void GatherBspbrushes_r(node_t *node, bspbrush_t::container &container)
{
    if (auto *leafdata = node->get_leafdata()) {
        for (auto &brush : leafdata->bsp_brushes) {
            container.push_back(brush);
        }
        return;
    }

    auto *nodedata = node->get_nodedata();
    GatherBspbrushes_r(nodedata->children[0], container);
    GatherBspbrushes_r(nodedata->children[1], container);
}

static void GatherLeafVolumes_r(node_t *node, bspbrush_t::container &container)
{
    if (auto *leafdata = node->get_leafdata()) {
        if (!leafdata->contents.is_empty()) {
            container.push_back(node->volume);
        }
        return;
    }

    auto *nodedata = node->get_nodedata();
    GatherLeafVolumes_r(nodedata->children[0], container);
    GatherLeafVolumes_r(nodedata->children[1], container);
}

/* Returns true if the user requested to generate an entity bmodel clipnodes
 * for a given hull. */
static bool ShouldGenerateClipnodes(mapentity_t &entity, hull_index_t hullnum)
{
    // Default to generating clipnodes for all hulls.
    if (!entity.epairs.has("_hulls")) {
        return true;
    }

    const int hulls = entity.epairs.get_int("_hulls");
    // Ensure 0 means all hulls even in the case we have more than 32 hulls.
    if (hulls == 0) {
        return true;
    }

    return hulls & (1 << hullnum.value_or(0));
}

/*
===============
ProcessEntity
===============
*/
static void ProcessEntity(mapentity_t &entity, hull_index_t hullnum)
{
    /* No map brushes means non-bmodel entity.
       We need to handle worldspawn containing no brushes, though. */
    if (!entity.mapbrushes.size() && !map.is_world_entity(entity)) {
        return;
    }

    /*
     * func_group and func_detail entities get their brushes added to the
     * worldspawn
     */
    if (IsWorldBrushEntity(entity) || IsNonRemoveWorldBrushEntity(entity))
        return;

    // for notriggermodels: if we have at least one trigger-like texture, do special trigger stuff
    bool discarded_trigger = !map.is_world_entity(entity) && qbsp_options.notriggermodels.value() && IsTrigger(entity);

    // Export a blank model struct, and reserve the index (only do this once, for all hulls)
    if (!discarded_trigger) {
        if (!entity.outputmodelnumber.has_value()) {
            entity.outputmodelnumber = map.bsp.dmodels.size();
            map.bsp.dmodels.emplace_back();
        }

        if (!map.is_world_entity(entity)) {
            if (&entity == &map.entities[1]) {
                logging::header("Internal Entities");
            }

            std::string mod = fmt::format("*{}", entity.outputmodelnumber.value());

            if (qbsp_options.verbose.value()) {
                PrintEntity(entity);
            }

            if (!hullnum.value_or(0) || qbsp_options.loghulls.value()) {
                logging::print(logging::flag::STAT, "     MODEL: {}\n", mod);
            }

            entity.epairs.set("model", mod);
        }
    }

    if (qbsp_options.lmscale.is_changed() && !entity.epairs.has("_lmscale")) {
        entity.epairs.set("_lmscale", std::to_string(qbsp_options.lmscale.value()));
    }

    // Init the entity
    entity.bounds = {};

    // reserve enough brushes; we would only make less,
    // never more
    bspbrush_t::container brushes;
    brushes.reserve(entity.mapbrushes.size());

    /*
     * Convert the map brushes (planes) into BSP brushes (polygons)
     */
    size_t num_clipped = 0;
    Brush_LoadEntity(entity, hullnum, brushes, num_clipped);

    if (num_clipped && !qbsp_options.verbose.value()) {
        logging::print(logging::flag::STAT,
            "WARNING: {} faces were crunched away by being too small. {}Use -verbose to see which faces were affected.\n",
            num_clipped, hullnum.value_or(0) ? "This is normal for the hulls. " : "");
    }

    size_t num_sides = 0;
    for (size_t i = 0; i < brushes.size(); ++i) {
        num_sides += brushes[i]->sides.size();
    }

    logging::print(
        logging::flag::STAT, "INFO: calculating BSP for {} brushes with {} sides\n", brushes.size(), num_sides);

    // sort by ascending (chop_index, line_number) pair
    std::ranges::sort(
        brushes, [](const auto &a, const auto &b) { return a->mapbrush->sort_key() < b->mapbrush->sort_key(); });

    // always chop the other hulls to reduce brush tests
    if (qbsp_options.chop.value() || hullnum.value_or(0)) {
        ChopBrushes(brushes, qbsp_options.chopfragment.value());
    }

    // we're discarding the brush
    if (discarded_trigger) {
        if (!hullnum.value_or(0)) {
            entity.epairs.set("mins", fmt::to_string(entity.bounds.mins()));
            entity.epairs.set("maxs", fmt::to_string(entity.bounds.maxs()));
        }
        return;
    }

    // corner case, -omitdetail with all detail in an bmodel
    if (brushes.empty() && entity.bounds == aabb3d()) {
        return;
    }

    // _hulls key
    if (!ShouldGenerateClipnodes(entity, hullnum)) {
        // We still need to emit an empty tree otherwise hull 0 will point past
        // the clipnode array (FIXME?).
        bspbrush_t::container empty;
        tree_t tree;
        BrushBSP(tree, entity, empty, tree_split_t::FAST);
        if (hullnum.value_or(0)) {
            ExportClipNodes(entity, tree.headnode, hullnum.value());
        } else {
            MakeTreePortals(tree); // needed to assign leaf bounds
            ExportDrawNodes(entity, tree.headnode, map.bsp.dfaces.size());
        }
        return;
    }

    // simpler operation for hulls
    if (hullnum.value_or(0)) {
        tree_t tree;
        BrushBSP(tree, entity, brushes, tree_split_t::FAST);
        if (map.is_world_entity(entity) && !qbsp_options.nofill.value()) {
            // assume non-world bmodels are simple
            MakeTreePortals(tree);
            if (FillOutside(tree, hullnum, brushes)) {
                if (qbsp_options.filldetail.value())
                    FillDetail(tree, hullnum, brushes);

                // make a really good tree
                tree.clear();
                BrushBSP(tree, entity, brushes, tree_split_t::PRECISE);

                // fill again so PruneNodes works
                MakeTreePortals(tree);
                FillOutside(tree, hullnum, brushes);
                if (qbsp_options.filldetail.value())
                    FillDetail(tree, hullnum, brushes);

                FreeTreePortals(tree);
                PruneNodes(tree.headnode);
            }
            CountLeafs(tree.headnode);
        }
        ExportClipNodes(entity, tree.headnode, hullnum.value());
        return;
    }

    // full operation for collision (or main hull)
    tree_t tree;

    BrushBSP(tree, entity, brushes,
        qbsp_options.forcegoodtree.value() ? tree_split_t::PRECISE : // we asked for the slow method
            !map.is_world_entity(entity) ? tree_split_t::FAST
                                         : // brush models are assumed to be simple
            tree_split_t::AUTO);

    // build all the portals in the bsp tree
    // some portals are solid polygons, and some are paths to other leafs
    MakeTreePortals(tree);

    if (map.is_world_entity(entity)) {
        // debug output of bspbrushes
        if (!hullnum.value_or(0)) {
            if (qbsp_options.debugbspbrushes.value()) {
                bspbrush_t::container all_bspbrushes;
                GatherBspbrushes_r(tree.headnode, all_bspbrushes);
                WriteBspBrushMap("first-brushbsp", all_bspbrushes);
            }
            if (qbsp_options.debugleafvolumes.value()) {
                bspbrush_t::container all_bspbrushes;
                GatherLeafVolumes_r(tree.headnode, all_bspbrushes);
                WriteBspBrushMap("first-brushbsp-volumes", all_bspbrushes);
            }
        }

        // flood fills from the void.
        // marks brush sides which are *only* touching void;
        // we can skip using them as BSP splitters on the "really good tree"
        // (effectively expanding those brush sides outwards).
        if (!qbsp_options.nofill.value() && FillOutside(tree, hullnum, brushes)) {
            if (qbsp_options.filldetail.value())
                FillDetail(tree, hullnum, brushes);

            // make a really good tree
            tree.clear();
            BrushBSP(tree, entity, brushes, tree_split_t::PRECISE);

            // debug output of bspbrushes
            if (!hullnum.value_or(0)) {
                if (qbsp_options.debugbspbrushes.value()) {
                    bspbrush_t::container all_bspbrushes;
                    GatherBspbrushes_r(tree.headnode, all_bspbrushes);
                    WriteBspBrushMap("second-brushbsp", all_bspbrushes);
                }
                if (qbsp_options.debugleafvolumes.value()) {
                    bspbrush_t::container all_bspbrushes;
                    GatherLeafVolumes_r(tree.headnode, all_bspbrushes);
                    WriteBspBrushMap("second-brushbsp-volumes", all_bspbrushes);
                }
            }

            // make the real portals for vis tracing
            MakeTreePortals(tree);

            // fill again so PruneNodes works
            FillOutside(tree, hullnum, brushes);

            if (qbsp_options.filldetail.value())
                FillDetail(tree, hullnum, brushes);
        }

        // Area portals
        if (qbsp_options.target_game->id == GAME_QUAKE_II) {
            EmitAreaPortals(tree);
        }
    } else {
        FillBrushEntity(tree, hullnum, brushes);

        // rebuild BSP now that we've marked invisible brush sides
        tree.clear();
        BrushBSP(tree, entity, brushes, tree_split_t::PRECISE);
    }

    MakeTreePortals(tree);

    MarkVisibleSides(tree, brushes);
    MakeFaces(tree.headnode);

    FreeTreePortals(tree);
    PruneNodes(tree.headnode);

    // write out .prt for main hull
    if (!hullnum.value_or(0) && map.is_world_entity(entity) && (!map.leakfile || qbsp_options.keepprt.value())) {
        WritePortalFile(tree);
    }

    auto MakeFaceFromSide = [](node_t *node, mapface_t &side) -> std::unique_ptr<face_t> {
        if (!side.winding.size()) {
            return nullptr;
        }

        auto f = std::make_unique<face_t>();

        f->texinfo = side.texinfo;
        f->planenum = side.planenum ^ 1;
        f->portal = nullptr;
        f->original_side = &side;

        f->w = side.winding.clone();
        f->contents = {side.contents, side.contents};

        UpdateFaceSphere(f.get());

        return f;
    };

    // super-detail
    if (map.is_world_entity(entity)) {
        if (!hullnum.value_or(0)) {
            for (int i = 1; i < map.entities.size(); i++) {
                mapentity_t &source = map.entities.at(i);

                if (!source.epairs.get_int("_super_detail")) {
                    continue;
                }

                for (auto &brush : source.mapbrushes) {
                    for (auto &side : brush.faces) {
                        {
                            auto face = MakeFaceFromSide(tree.headnode, side);

                            if (face) {
                                tree.headnode->get_nodedata()->facelist.push_back(std::move(face));
                            }
                        }
                    }
                }
            }
        }
    }

    // needs to come after any face creation
    MakeMarkFaces(tree.headnode);
    FixupDetailFence(tree);

    CountLeafs(tree.headnode);

    // output vertices first, since TJunc needs it
    EmitVertices(tree.headnode);

    TJunc(tree.headnode);

    if (qbsp_options.objexport.value() && map.is_world_entity(entity)) {
        ExportObj_Nodes("pre_makefaceedges_plane_faces", tree.headnode);
        ExportObj_Marksurfaces("pre_makefaceedges_marksurfaces", tree.headnode);
    }

    Q_assert(!entity.firstoutputfacenumber.has_value());

    entity.firstoutputfacenumber = EmitFaces(tree.headnode);

    if (qbsp_options.target_game->has_brushes) {
        ExportBrushList(entity, tree.headnode);
    }

    ExportDrawNodes(entity, tree.headnode, entity.firstoutputfacenumber.value());
    FreeTreePortals(tree);
}

/*
=================
UpdateEntLump

=================
*/
static void UpdateEntLump()
{
    logging::print(logging::flag::STAT, "     Updating entities lump...\n");

    if (qbsp_options.target_game->id == GAME_QUAKE_II) {
        FError("this won't work on Q2 maps; for Q2, please use bsputil --extract-entities & --replace-entities.");
        return;
    }

    size_t modnum = 1;

    for (size_t i = 1; i < map.entities.size(); i++) {
        mapentity_t &entity = map.entities.at(i);

        /* Special handling for misc_external_map.
           Duplicates some logic from ProcessExternalMapEntity. */
        bool is_misc_external_map = false;

        if (!Q_strcasecmp(entity.epairs.get("classname"), "misc_external_map")) {
            const std::string &new_classname = entity.epairs.get("_external_map_classname");

            entity.epairs.set("classname", new_classname);
            entity.epairs.set("origin", "0 0 0");

            /* Note: the classname could have switched to
             * a IsWorldBrushEntity entity (func_group, func_detail),
             * or a bmodel entity (func_wall
             */
            is_misc_external_map = true;
        }

        bool isBrushEnt = (entity.mapbrushes.size() > 0) || is_misc_external_map;
        if (!isBrushEnt) {
            continue;
        }

        if (IsWorldBrushEntity(entity) || IsNonRemoveWorldBrushEntity(entity)) {
            continue;
        }

        entity.epairs.set("model", fmt::format("*{}", modnum));
        modnum++;

        /* Do extra work for rotating entities if necessary */
        const std::string &classname = entity.epairs.get("classname");

        if (!classname.compare(0, 7, "rotate_")) {
            FixRotateOrigin(entity);
        }
    }

    WriteEntitiesToString();
    UpdateBSPFileEntitiesLump();
}

/*
WriteBrushes
Generates a submodel's direct brush information to a separate file, so the engine doesn't need to depend upon specific
hull sizes
*/

static bspxbrushes_permodel BSPX_Brushes_AddModel(int modelnum, const std::vector<mapbrush_t *> &brushes)
{
    bspxbrushes_permodel permodel{.ver = 1, .modelnum = modelnum};

    for (auto &b : brushes) {
        bspxbrushes_perbrush &perbrush = permodel.brushes.emplace_back();

        perbrush.bounds = b->bounds;

        const auto &contents = b->contents;

        if (modelnum) {
            // bmodel brushes all become solid for compatibility with maps built for vanilla Quake
            if (contents.is_clip())
                perbrush.contents = BSPXBRUSHES_CONTENTS_CLIP;
            else
                perbrush.contents = CONTENTS_SOLID;
        } else {
            // world model case

            const int native = qbsp_options.target_game->contents_to_native(contents);

            switch (native) {
                // contents should match the engine.
                case CONTENTS_EMPTY: // really an error, but whatever
                case CONTENTS_SOLID: // these are okay
                case CONTENTS_WATER:
                case CONTENTS_SLIME:
                case CONTENTS_LAVA:
                case CONTENTS_SKY:
                    if (contents.is_clip()) {
                        perbrush.contents = BSPXBRUSHES_CONTENTS_CLIP;
                    } else {
                        perbrush.contents = native;
                    }
                    break;
                    //              case CONTENTS_LADDER:
                    //                      perbrush.contents = -16;
                    //                      break;
                default: {
                    if (contents.is_clip()) {
                        perbrush.contents = BSPXBRUSHES_CONTENTS_CLIP;
                    } else {
                        logging::print("WARNING: Unknown contents: {}. Translating to solid.\n", contents.to_string());
                        perbrush.contents = CONTENTS_SOLID;
                    }
                    break;
                }
            }
        }

        for (auto &f : b->faces) {
            /*skip axial*/
            const auto &plane = f.get_plane();
            if (plane.get_type() < plane_type_t::PLANE_ANYX)
                continue;

            bspxbrushes_perface perface = qplane3f(plane.get_normal(), plane.get_dist());
            perbrush.faces.push_back(perface);
        }
    }

    return permodel;
}

/* for generating BRUSHLIST bspx lump */
static void BSPX_CreateBrushList()
{
    bspxbrushes ctx;

    if (!qbsp_options.wrbrushes.value())
        return;

    for (size_t entnum = 0; entnum < map.entities.size(); ++entnum) {
        mapentity_t &ent = map.entities.at(entnum);
        size_t modelnum;

        if (IsWorldBrushEntity(ent)) {
            continue;
        }

        if (map.is_world_entity(ent)) {
            modelnum = 0;
        } else {
            const std::string &mod = ent.epairs.get("model");
            if (mod[0] != '*') {
                continue;
            }
            modelnum = std::stoi(mod.substr(1));
        }

        std::vector<mapbrush_t *> brushes;

        brushes.reserve(ent.mapbrushes.size());

        for (auto &b : ent.mapbrushes) {
            brushes.push_back(&b);
        }

        if (modelnum == 0) {
            // add brushes from world brush entities (func_group, etc.) to the worldspawn model
            for (size_t e = 1; e < map.entities.size(); ++e) {
                mapentity_t &bent = map.entities.at(e);

                brushes.reserve(brushes.size() + ent.mapbrushes.size());

                if (IsWorldBrushEntity(bent)) {
                    // skip illusionary entities
                    const std::string &classname = bent.epairs.get("classname");
                    if (!Q_strcasecmp(classname, "func_detail_illusionary"))
                        continue;
                    if (!Q_strcasecmp(classname, "func_illusionary_visblocker"))
                        continue;

                    for (auto &b : bent.mapbrushes) {
                        brushes.push_back(&b);
                    }
                }
            }
        }

        if (!brushes.empty()) {
            ctx.models.push_back(BSPX_Brushes_AddModel(modelnum, brushes));
        }
    }

    // Actually written in WriteBSPFile()
    std::ostringstream str(std::ios_base::out | std::ios_base::binary);
    str << endianness<std::endian::little>;
    str <= ctx;
    map.exported_bspxbrushes = StringToVector(str.str());
}

/*
=================
CreateSingleHull
=================
*/
static void CreateSingleHull(hull_index_t hullnum)
{
    if (hullnum.has_value()) {
        logging::print("Processing hull {}...\n", hullnum.value());
    } else {
        logging::print("Processing map...\n");
    }

    // for each entity in the map file that has geometry
    for (auto &entity : map.entities) {
        bool wants_logging = true;

        // decide if we want to log this entity / hull combination
        if (!map.is_world_entity(entity)) {
            wants_logging = wants_logging && qbsp_options.logbmodels.value();
        }
        if (hullnum.value_or(0)) {
            wants_logging = wants_logging && qbsp_options.loghulls.value();
        }

        // update logging mask if requested
        const auto prev_logging_mask = logging::mask;
        if (!wants_logging) {
            logging::mask &= ~(
                bitflags<logging::flag>(logging::flag::STAT) | logging::flag::PROGRESS | logging::flag::CLOCK_ELAPSED);
        }

        ProcessEntity(entity, hullnum);

        // restore logging
        logging::mask = prev_logging_mask;
    }
}

/*
=================
CreateHulls
=================
*/
static void CreateHulls()
{
    /* create the hulls sequentially */
    auto hulls = qbsp_options.target_game->get_hull_sizes();

    // game has no hulls, so we have to export brush lists and stuff.
    if (!hulls.size()) {
        CreateSingleHull(std::nullopt);
        return;
    }

    // all the hulls
    for (size_t i = 0; i < hulls.size(); i++) {
        CreateSingleHull(i);

        // only create hull 0 if fNoclip is set
        if (qbsp_options.noclip.value()) {
            break;
        }
    }
}

// Fill the BSP's `dtex` data
static void LoadTextureData()
{
    for (size_t i = 0; i < map.miptex.size(); i++) {
        // always fill the name even if we can't find it
        auto &miptex = map.bsp.dtex.textures[i];
        miptex.name = map.miptex[i].name;

        {
            // i.e. only allow loose (non-.wad) textures if -notex is in use
            const bool mip_only = !qbsp_options.notextures.value();

            auto [tex, pos, file] =
                img::load_texture(map.miptex[i].name, true, qbsp_options.target_game, qbsp_options, false, mip_only);

            if (!tex) {
                if (pos.archive) {
                    logging::print("WARNING: unable to load texture {} in archive {}\n", map.miptex[i].name,
                        pos.archive->pathname);
                } else {
                    logging::print("WARNING: unable to find texture {}\n", map.miptex[i].name);
                }
            } else {
                miptex.width = tex->meta.width;
                miptex.height = tex->meta.height;

                // only mips can be embedded directly
                if (!qbsp_options.notextures.value() && !pos.archive->external &&
                    tex->meta.extension == img::ext::MIP) {
                    miptex.data = std::move(file.value());
                    continue;
                }
            }
        }

        // fall back to when we can't load the image.
        // construct fake data that solely contains the header.
        miptex.data.resize(sizeof(dmiptex_t));

        dmiptex_t header{};
        if (miptex.name.size() >= 16) {
            logging::print("WARNING: texture {} name too long for Quake miptex\n", miptex.name);
            std::copy_n(miptex.name.begin(), 15, header.name.begin());
        } else {
            std::copy(miptex.name.begin(), miptex.name.end(), header.name.begin());
        }

        header.width = miptex.width;
        header.height = miptex.height;
        header.offsets = {0, 0, 0, 0};

        if (!miptex.name[0])
            miptex.null_texture = true;

        if (!qbsp_options.missing_textures_as_zero_size.value()) {
            if (miptex.width == 0 || miptex.height == 0) {
                miptex.null_texture = true;
            }
        }

        omemstream stream(miptex.data.data(), miptex.data.size());
        stream <= header;
    }
}

static void AddAnimationFrames()
{
    size_t oldcount = map.miptex.size();

    for (size_t i = 0; i < oldcount; i++) {
        const std::string &existing_name = map.miptexTextureName(i);

        if (existing_name[0] != '+' && (qbsp_options.target_game->id != GAME_HALF_LIFE || existing_name[0] != '-')) {
            continue;
        }

        std::string name = map.miptexTextureName(i);

        /* Search for all animations (0-9) and alt-animations (A-J) */
        for (size_t j = 0; j < 20; j++) {
            name[1] = (j < 10) ? '0' + j : 'a' + j - 10;
            if (fs::where(name)) {
                FindMiptex(name.c_str());
            }
        }
    }

    logging::print(logging::flag::STAT, "     {:8} texture frames added\n", map.miptex.size() - oldcount);
}

static void LoadSecondaryTextures()
{
    // Q2 doesn't use any secondary textures
    if (qbsp_options.target_game->id == GAME_QUAKE_II) {
        return;
    }

    AddAnimationFrames();

    /* Default texture data to store in worldmodel */
    map.bsp.dtex.textures.resize(map.miptex.size());

    LoadTextureData();
}

/*
=================
ProcessFile
=================
*/
void ProcessFile()
{
    if (qbsp_options.convertmapformat.value() != conversion_t::none) {
        ConvertMapFile();
        return;
    }

    // load brushes and entities
    LoadMapFile();

    if (qbsp_options.onlyents.value()) {
        UpdateEntLump();
        return;
    }

    // handle load time operation on the .map
    ProcessMapBrushes();

    // initialize secondary textures
    LoadSecondaryTextures();

    // init the tables to be shared by all models
    BeginBSPFile();

    // create hulls!
    CreateHulls();

    WriteEntitiesToString();
    BSPX_CreateBrushList();
    FinishBSPFile();
}

/*
==================
MakeSkipTexinfo
==================
*/
static int MakeSkipTexinfo()
{
    maptexinfo_t mt{};

    mt.miptex = FindMiptex("skip", true);
    mt.flags.native_q2 = static_cast<q2_surf_flags_t>(mt.flags.native_q2 | Q2_SURF_NODRAW);

    return FindTexinfo(mt, qplane3d{});
}

/*
==================
InitQBSP
==================
*/
void InitQBSP(int argc, const char **argv)
{
    // In case we're launched more than once, in testqbsp
    map.reset();
    qbsp_options.reset();

    qbsp_options.preinitialize(argc, argv);
    qbsp_options.initialize(argc, argv);
    qbsp_options.postinitialize(argc, argv);

    qbsp_options.map_path.replace_extension("map");

    // The .map extension gets removed right away anyways...
    if (qbsp_options.bsp_path.empty())
        qbsp_options.bsp_path = qbsp_options.map_path;

    /* Start logging to <bspname>.log */
    logging::init(fs::path(qbsp_options.bsp_path).replace_extension("log"), qbsp_options);

    qbsp_options.print_summary();

    // Remove already existing files
    if (!qbsp_options.onlyents.value() && qbsp_options.convertmapformat.value() == conversion_t::none) {
        qbsp_options.bsp_path.replace_extension("bsp");
        remove(qbsp_options.bsp_path);

        // Probably not the best place to do this
        logging::print("Input file: {}\n", fs::absolute(qbsp_options.map_path));
        logging::print("Output file: {}\n\n", fs::absolute(qbsp_options.bsp_path));

        fs::path prtfile = qbsp_options.bsp_path;
        prtfile.replace_extension("prt");
        remove(prtfile);

        fs::path ptsfile = qbsp_options.bsp_path;
        ptsfile.replace_extension("pts");
        remove(ptsfile);

        fs::path porfile = qbsp_options.bsp_path;
        porfile.replace_extension("por");
        remove(porfile);

        // areaportal leaks
        for (int i = 0;; i++) {
            fs::path name = qbsp_options.bsp_path;
            name.replace_extension(fmt::format("areaportal_leak{}.pts", i));

            if (!remove(name)) {
                break;
            }
        }
    }

    // onlyents might not load this yet
    if (qbsp_options.target_game) {
        qbsp_options.target_game->init_filesystem(qbsp_options.map_path, qbsp_options);
    }

    // make skip texinfo, in case map needs it (it'll get culled out if not)
    map.skip_texinfo = MakeSkipTexinfo();
}

void InitQBSP(const std::vector<std::string> &args)
{
    std::vector<const char *> argPtrs;
    for (const std::string &arg : args) {
        argPtrs.push_back(arg.data());
    }

    InitQBSP(argPtrs.size(), argPtrs.data());
}

#include <fstream>

/*
==================
main
==================
*/
int qbsp_main(int argc, const char **argv)
{
    InitQBSP(argc, argv);

    // do it!
    auto start = I_FloatTime();
    ProcessFile();
    auto end = I_FloatTime();

    logging::print("\n{:.3} seconds elapsed\n", (end - start));

    logging::close();

    return 0;
}
