/*
    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 <qbsp/tjunc.hh>

#include <qbsp/qbsp.hh>
#include <qbsp/map.hh>
#include <atomic>

struct tjunc_stats_t : logging::stat_tracker_t
{
    // # of degenerate edges reported (with two identical input vertices)
    stat &degenerate = register_stat("edges degenerated");
    // # of faces that were degenerate and were just collapsed altogether.
    stat &facecollapse = register_stat("faces degenerated");
    // # of new edges created to close a tjunction
    // (also technically the # of points detected that lay on other faces' edges)
    stat &tjunctions = register_stat("edges added by tjunctions");
    // # of faces that were successfully topologized by MWT
    stat &mwt = register_stat("faces optimized through MWT");
    // # of triangles computed by MWT
    stat &trimwt = register_stat("triangles computed from MWT");
    // # of faces added by MWT
    stat &facemwt = register_stat("faces created via optimization of MWT triangles");
    // # of faces that could be successfully retopologized
    stat &retopology = register_stat("faces re-topologized");
    // # of faces generated by retopologization
    stat &faceretopology = register_stat("new faces added by re-topology");
    // # of faces that were able to be fixed just by rotating the start point.
    stat &rotates = register_stat("faces rotated");
    // # of faces that weren't able to be fixed with start point rotation
    stat &norotates = register_stat("faces unable to be rotated or re-topologized");
    // # of faces that were created as a result of splitting faces that are too large
    // to be contained on a single face
    stat &faceoverflows = register_stat("faces added by splitting large faces");
};

static std::optional<double> PointOnEdge(
    const qvec3d &p, const qvec3d &edge_start, const qvec3d &edge_dir, float start = 0, float end = 1)
{
    qvec3d delta = p - edge_start;
    double dist = qv::dot(delta, edge_dir);

    // check if off an end
    if (dist <= start || dist >= end) {
        return std::nullopt;
    }

    qvec3d exact = edge_start + (edge_dir * dist);
    qvec3d off = p - exact;
    double error = qv::length(off);

    // brushbsp-fixme: this was 0.5 in Q2, check?
    if (fabs(error) > DEFAULT_ON_EPSILON) {
        // not on the edge
        return std::nullopt;
    }

    return dist;
}

/*
==========
TestEdge

Can be recursively reentered
==========
*/
static void TestEdge(double start, double end, size_t p1, size_t p2, size_t startvert,
    const std::vector<size_t> &edge_verts, const qvec3d &edge_start, const qvec3d &edge_dir,
    std::vector<size_t> &superface, tjunc_stats_t &stats)
{
    if (p1 == p2) {
        // degenerate edge
        stats.degenerate++;
        return;
    }

    for (size_t k = startvert; k < edge_verts.size(); k++) {
        size_t j = edge_verts[k];

        if (j == p1 || j == p2) {
            continue;
        }

        auto dist = PointOnEdge(map.bsp.dvertexes[j], edge_start, edge_dir, start, end);

        if (!dist.has_value()) {
            continue;
        }

        // break the edge
        stats.tjunctions++;

        TestEdge(start, dist.value(), p1, j, k + 1, edge_verts, edge_start, edge_dir, superface, stats);
        TestEdge(dist.value(), end, j, p2, k + 1, edge_verts, edge_start, edge_dir, superface, stats);
        return;
    }

    // the edge p1 to p2 is now free of tjunctions
    superface.push_back(p1);
}

#if 0
/*
==========
FindEdgeVerts_BruteForce

Force a dumb check of everything
==========
*/
static void FindEdgeVerts_BruteForce(
    const node_t *, const node_t *, const qvec3d &, const qvec3d &, std::vector<size_t> &verts)
{
    verts.resize(map.bsp.dvertexes.size());

    for (size_t i = 0; i < verts.size(); i++) {
        verts[i] = i;
    }
}
#endif

/**
 * This is to prevent func_detail_wall touching solid from creating
 * tjunc fixes. func_detail_wall is meant to act like a separate mesh,
 * so it shouldn't interact with solid.
 */

static bool Welds(contentflags_t a, contentflags_t b)
{
    // FIXME: no clipping same type?

    // all types weld with themselves
    if (a.types_equal(b))
        return true;

    // detail wall only welds with detail wall
    if (a.is_detail_wall() || b.is_detail_wall())
        return false;

    // no need to weld translucent to opaque
    // (because they could have void behind them due to visblocking.
    // e.g. opaque water meeting solid)
    if (!a.is_opaque(qbsp_options.target_game, qbsp_options.transwater.value()) &&
        b.is_opaque(qbsp_options.target_game, qbsp_options.transwater.value()))
        return false;
    if (!b.is_opaque(qbsp_options.target_game, qbsp_options.transwater.value()) &&
        a.is_opaque(qbsp_options.target_game, qbsp_options.transwater.value()))
        return false;

    // never weld with backfaces
    if (a.is_empty() || b.is_empty())
        return false;

    // otherwise, weld
    return true;
}

static bool HasTJuncInteraction(const face_t *f1, const face_t *f2)
{
    // FIXME: handle func_detail_fence, func_detail_illusionary,
    // liquids? make sure a combination of solid + func_detail_wall
    // is treated as solid?

    return Welds(f1->contents.back, f2->contents.back);
}

/*
==========
FindEdgeVerts_FaceBounds_R

Recursive function for matching nodes that intersect the aabb
for vertex checking.
==========
*/
static void FindEdgeVerts_FaceBounds_R(
    const node_t *node, const face_t *f, const aabb3d &aabb, std::vector<size_t> &verts)
{
    if (node->is_leaf()) {
        return;
    } else if (node->bounds.disjoint(aabb, 0.0)) {
        return;
    }

    auto *nodedata = node->get_nodedata();
    for (auto &face : nodedata->facelist) {
        if (!HasTJuncInteraction(f, face.get()))
            continue;
        for (auto &v : face->original_vertices) {
            if (aabb.containsPoint(map.bsp.dvertexes[v])) {
                verts.push_back(v);
            }
        }
    }

    FindEdgeVerts_FaceBounds_R(nodedata->children[0], f, aabb, verts);
    FindEdgeVerts_FaceBounds_R(nodedata->children[1], f, aabb, verts);
}

/*
==========
FindEdgeVerts_FaceBounds

Use a loose AABB around the line and only capture vertices that intersect it.

f is the face we're fixing; this will affect the candidate other faces
because not everything has tjunc interactions (e.g. func_detail_wall and worldspawn.)
==========
*/
static void FindEdgeVerts_FaceBounds(
    const node_t *headnode, const face_t *f, const qvec3d &p1, const qvec3d &p2, std::vector<size_t> &verts)
{
    // magic number, average of "usual" points per edge
    verts.reserve(8);

    FindEdgeVerts_FaceBounds_R(headnode, f, (aabb3d{} + p1 + p2).grow(qvec3d(1.0, 1.0, 1.0)), verts);
}

/*
==================
SplitFaceIntoFragments

The face was created successfully, but may have way too many edges.
Cut it down to the minimum amount of faces that are within the
max edge count.

Modifies `superface`. Adds the results to the end of `output`.
==================
*/
static void SplitFaceIntoFragments(
    std::vector<size_t> &superface, std::list<std::vector<size_t>> &output, tjunc_stats_t &stats)
{
    const int32_t &maxedges = qbsp_options.maxedges.value();

    // split into multiple fragments, because of vertex overload
    while (superface.size() > maxedges) {
        stats.faceoverflows++;

        // copy MAXEDGES from our current face
        std::vector<size_t> &newf = output.emplace_back(maxedges);
        std::copy_n(superface.begin(), maxedges, newf.begin());

        // remove everything in-between from the superface
        // except for the last edge we just wrote (0 and MAXEDGES-1)
        std::copy(superface.begin() + maxedges - 1, superface.end(), superface.begin() + 1);

        // resize superface; we need enough room to store the two extra verts
        superface.resize(superface.size() - maxedges + 2);
    }

    // move the first face to the end, since that's logically where it belongs now
    output.splice(output.end(), output, output.begin());
}

static float AngleOfTriangle(const qvec3d &a, const qvec3d &b, const qvec3d &c)
{
    double num = (b[0] - a[0]) * (c[0] - a[0]) + (b[1] - a[1]) * (c[1] - a[1]) + (b[2] - a[2]) * (c[2] - a[2]);
    double den = sqrt(pow((b[0] - a[0]), 2) + pow((b[1] - a[1]), 2) + pow((b[2] - a[2]), 2)) *
                 sqrt(pow((c[0] - a[0]), 2) + pow((c[1] - a[1]), 2) + pow((c[2] - a[2]), 2));

    return acos(num / den) * (180.0 / 3.141592653589793238463);
}

// Check whether the given input triangle would be valid
// on the given face and not have any other points
// intersecting it.
static bool TriangleIsValid(size_t v0, size_t v1, size_t v2, double angle_epsilon)
{
    if (AngleOfTriangle(map.bsp.dvertexes[v0], map.bsp.dvertexes[v1], map.bsp.dvertexes[v2]) < angle_epsilon ||
        AngleOfTriangle(map.bsp.dvertexes[v1], map.bsp.dvertexes[v2], map.bsp.dvertexes[v0]) < angle_epsilon ||
        AngleOfTriangle(map.bsp.dvertexes[v2], map.bsp.dvertexes[v0], map.bsp.dvertexes[v1]) < angle_epsilon) {
        return false;
    }

    return true;
}

/*
==================
CreateSuperFace

Generate a superface (the input face `f` but with all of the
verts in the world added that lay on the line) and return it
==================
*/
static std::vector<size_t> CreateSuperFace(node_t *headnode, face_t *f, tjunc_stats_t &stats)
{
    std::vector<size_t> superface;

    superface.reserve(f->original_vertices.size() * 2);

    // stores all of the verts in the world that are close to
    // being on a given edge
    std::vector<size_t> edge_verts;

    // find all of the extra vertices that lay on edges,
    // place them in superface
    for (size_t i = 0; i < f->original_vertices.size(); i++) {
        auto v1 = f->original_vertices[i];
        auto v2 = f->original_vertices[(i + 1) % f->original_vertices.size()];

        qvec3d v1_pos = map.bsp.dvertexes[v1];
        qvec3d v2_pos = map.bsp.dvertexes[v2];

        edge_verts.clear();
        FindEdgeVerts_FaceBounds(headnode, f, v1_pos, v2_pos, edge_verts);

        double len;
        qvec3d edge_dir = qv::normalize(v2_pos - v1_pos, len);

        TestEdge(0, len, v1, v2, 0, edge_verts, v1_pos, edge_dir, superface, stats);
    }

    return superface;
}

#include <common/bsputils.hh>
#include <fstream>

using qvectri = qvec<size_t, 3>;

// check if the given triangle exists in the set of triangles
// in any permutation
static std::optional<size_t> triangle_exists(const std::vector<qvectri> &triangles, size_t a, size_t b, size_t c)
{
    for (size_t i = 0; i < triangles.size(); i++) {
        auto &tri = triangles[i];

        for (size_t s = 0; s < 3; s++) {
            if (tri[s] == a && tri[(s + 1) % 3] == b && tri[(s + 2) % 3] == c) {
                return i;
            }
        }
    }

    return std::nullopt;
}

// find the triangles best suited to create a
// fan out of in the given set of triangles.
static std::vector<size_t> find_best_fan(const std::vector<qvectri> &triangles, size_t num_vertices)
{
    // find the triangle with the most fannable vertices.
    std::vector<size_t> best_triangles;

    for (auto &tri : triangles) {
        // try all three permutations
        for (size_t perm = 0; perm < 3; perm++) {
            size_t first = tri[perm];
            size_t mid = tri[(perm + 1) % 3];
            size_t last = tri[(perm + 2) % 3];

            std::vector<size_t> my_tri;

            // find any other that can be wound from this edge
            // TODO: can optimize by only looping around the verts
            // included in the triangle
            for (; last != first; last = (last + 1) % num_vertices) {
                auto ftri = triangle_exists(triangles, first, mid, last);

                // no triangle found for A B C, so try again
                // with A B D, etc.
                if (ftri == std::nullopt) {
                    continue;
                }

                // found A B C, so go next (A C D)
                my_tri.push_back(ftri.value());
                mid = last;
            }

            if (best_triangles.empty() || my_tri.size() > best_triangles.size()) {
                best_triangles = std::move(my_tri);
            }
        }
    }

    return best_triangles;
}

// find the seed vertex (vertex referenced by the most edges) of
// the fan.
static size_t find_seed_vertex(const std::vector<qvectri> &triangles, const std::vector<size_t> &fan)
{
    std::unordered_set<size_t> verts{triangles[fan[0]].begin(), triangles[fan[0]].end()};

    for (size_t i = 1; i < fan.size(); i++) {
        auto &tri = triangles[fan[i]];

        // produce intersection
        for (auto it = verts.begin(); it != verts.end();) {
            if (std::find(tri.begin(), tri.end(), *it) == tri.end()) {
                it = verts.erase(it);
            } else {
                it++;
            }
        }

        // if there's only one vert left it has to be that one
        if (verts.size() == 1) {
            return *verts.begin();
        }
    }

    // just pick whatever's left
    return *verts.begin();
}

static std::list<std::vector<size_t>> compress_triangles_into_fans(
    std::vector<qvectri> &triangles, const std::vector<size_t> &vertices)
{
    std::list<std::vector<size_t>> tris_compiled;

    while (triangles.size()) {
        auto fan = find_best_fan(triangles, vertices.size());

        Q_assert(fan.size());

        // when we run into only 1 triangle fans left,
        // just add the rest directly.
        if (fan.size() == 1) {
            for (auto &tri : triangles) {
                tris_compiled.push_back(std::vector<size_t>{vertices[tri[0]], vertices[tri[1]], vertices[tri[2]]});
            }

            triangles.clear();
            break;
        }

        // a fan can be made! find the seed vertex
        auto seed = find_seed_vertex(triangles, fan);

        struct tri_verts_less_pred
        {
            size_t seed, vert_count;

            bool operator()(size_t a, size_t b) const
            {
                size_t ka = a < seed ? vert_count + a : a;
                size_t kb = b < seed ? vert_count + b : b;

                return ka < kb;
            }
        };

        // add all the verts and order them so they match
        // the proper winding
        std::set<size_t, tri_verts_less_pred> verts(tri_verts_less_pred{seed, vertices.size()});

        for (auto tri_index : fan) {
            auto &tri = triangles[tri_index];

            for (auto &v : tri) {
                verts.insert(v);
            }
        }

        Q_assert(verts.size() >= 3);

        // add the new winding
        auto &out_tri = tris_compiled.emplace_back(verts.begin(), verts.end());

        for (auto &v : out_tri) {
            v = vertices[v];
        }

        // remove all of the fans from the triangle list
        std::sort(fan.begin(), fan.end(), [](auto &l, auto &r) { return l > r; });

        for (auto tri_index : fan) {
            triangles.erase(triangles.begin() + tri_index);
        }
    }

    return tris_compiled;
}

#include <queue>

// Function to calculate the weight of optimal triangulation of a convex polygon
// represented by a given set of vertices
static std::vector<qvectri> minimum_weight_triangulation(
    const std::vector<size_t> &indices, const std::vector<qvec2d> &vertices)
{
    // get the number of vertices in the polygon
    size_t n = vertices.size();

    // create a table for storing the solutions to subproblems
    // `T[i][j]` stores the weight of the minimum-weight triangulation
    // of the polygon below edge `ij`
    std::vector<double> T(n * n);
    std::vector<std::optional<size_t>> K(n * n);

    // fill the table diagonally using the recurrence relation
    for (size_t diagonal = 0; diagonal < n; diagonal++) {
        for (size_t i = 0, j = diagonal; j < n; i++, j++) {
            // If the polygon has less than 3 vertices, triangulation is not possible
            if (j < i + 2) {
                continue;
            }

            T[i + (j * n)] = std::numeric_limits<double>::max();

            // consider all possible triangles `ikj` within the polygon
            for (size_t k = i + 1; k <= j - 1; k++) {
                // The weight of triangulation is the length of its perimeter
                double weight;

                if (!TriangleIsValid(indices[i], indices[j], indices[k], 0.01)) {
                    weight = std::nexttoward(std::numeric_limits<double>::max(), 0.0);
                } else {
                    weight = (qv::distance(vertices[i], vertices[j]) + qv::distance(vertices[j], vertices[k]) +
                                 qv::distance(vertices[k], vertices[i])) +
                             T[i + (k * n)] + T[k + (j * n)];
                }
                double &t_weight = T[i + (j * n)];

                // choose vertex `k` that leads to the minimum total weight
                if (weight < t_weight) {
                    t_weight = weight;
                    K[i + (j * n)] = k;
                }
            }
        }
    }

    std::vector<qvectri> triangles;
    std::queue<qvec<size_t, 2>> edge_queue;
    edge_queue.emplace(0, n - 1);

    while (!edge_queue.empty()) {
        auto edge = edge_queue.front();
        edge_queue.pop();

        if (edge[0] == edge[1]) {
            continue;
        }

        auto &c = K[edge[0] + (edge[1] * n)];

        if (!c.has_value()) {
            continue;
        }

        qvectri tri{edge[0], edge[1], c.value()};
        std::sort(tri.begin(), tri.end());
        triangles.push_back(tri);

        edge_queue.emplace(edge[0], c.value());
        edge_queue.emplace(c.value(), edge[1]);
    }

    Q_assert(triangles.size() == n - 2);

    return triangles;
}

static std::list<std::vector<size_t>> mwt_face(
    const face_t *f, const std::vector<size_t> &vertices, tjunc_stats_t &stats)
{
    const auto &p = f->get_plane();
    auto [u, v] = qv::MakeTangentAndBitangentUnnormalized(p.get_normal());
    qv::normalizeInPlace(u);
    qv::normalizeInPlace(v);

    std::vector<qvec2d> points_2d(vertices.size());

    for (size_t i = 0; i < vertices.size(); i++) {
        points_2d[i] = {qv::dot(map.bsp.dvertexes[vertices[i]], u), qv::dot(map.bsp.dvertexes[vertices[i]], v)};
    }

    auto tris = minimum_weight_triangulation(vertices, points_2d);

    stats.trimwt += tris.size();

    return compress_triangles_into_fans(tris, vertices);
}

/*
==================
RetopologizeFace

A face has T-junctions that can't be resolved from rotation.
It's still a convex face with wound vertices, though, so we
can split it into several triangle fans.
==================
*/
static std::list<std::vector<size_t>> RetopologizeFace(const face_t *f, const std::vector<size_t> &vertices)
{
    std::list<std::vector<size_t>> result;
    // the copy we're working on
    std::vector<size_t> input(vertices);

    while (input.size()) {
        // failure if we somehow degenerated a triangle
        if (input.size() < 3) {
            return {};
        }

        size_t seed = 0;
        int64_t end = 0;

        // find seed triangle (allowing a wrap around,
        // because it's possible for only the last two triangles
        // to be valid).
        for (; seed < input.size(); seed++) {
            auto v0 = input[seed];
            auto v1 = input[(seed + 1) % input.size()];
            end = (seed + 2) % input.size();
            auto v2 = input[end];

            if (!TriangleIsValid(v0, v1, v2, 0.01)) {
                continue;
            }

            // if the next point lays on the edge of v0-v2, this next
            // triangle won't be valid.
            float len;
            qvec3d dir = qv::normalize(map.bsp.dvertexes[v0] - map.bsp.dvertexes[v2], len);
            auto dist =
                PointOnEdge(map.bsp.dvertexes[input[(end + 1) % input.size()]], map.bsp.dvertexes[v2], dir, 0, len);

            if (dist.has_value()) {
                continue;
            }

            // good one!
            break;
        }

        if (seed == input.size()) {
            // can't find a non-zero area triangle; failure
            return {};
        }

        // from the seed vertex, keep winding until we hit a zero-area triangle.
        // we know that triangle (seed, end - 1, end) is valid, so we wind from
        // end + 1 until we fully wrap around. We also can't include a triangle
        // that has a point after it laying on the final edge.
        size_t wrap = end;
        end = (end + 1) % input.size();

        for (; end != wrap; end = (end + 1) % input.size()) {
            auto v0 = input[seed];
            auto v2 = input[end];

            // if the next point lays on the edge of v0-v2, this next
            // triangle won't be valid.
            float len;
            qvec3d dir = qv::normalize(map.bsp.dvertexes[v0] - map.bsp.dvertexes[v2], len);
            auto dist =
                PointOnEdge(map.bsp.dvertexes[input[(end + 1) % input.size()]], map.bsp.dvertexes[v2], dir, 0, len);

            if (dist.has_value()) {
                // the next point lays on this edge, so back up and stop
                end = (end - 1) < 0 ? input.size() - 1 : (end - 1);
                break;
            }
        }

        // now we have a fan from seed to end that is valid.
        // add it to the result, clip off all of the
        // points between it and restart the algorithm
        // using that edge.
        auto &tri = result.emplace_back();

        // the whole fan can just be moved; we're finished.
        if (seed == end) {
            tri = std::move(input);
            break;
        } else if (end == wrap) {
            // we successfully wrapped around, but the
            // seed vertex isn't at the start, so rotate it.
            // copy base -> end
            tri.resize(input.size());
            auto out = std::copy(input.begin() + seed, input.end(), tri.begin());
            // copy end -> base
            std::copy(input.begin(), input.begin() + seed, out);
            break;
        }

        if (end < seed) {
            // the end point is 'behind' the seed, so we're clipping
            // off two sides of the input
            size_t x = seed;
            bool first = true;

            while (true) {
                if (x == end && !first) {
                    break;
                }

                tri.push_back(input[x]);
                x = (x + 1) % input.size();
                first = false;
            }
        } else {
            // simple case where the end point is ahead of the seed;
            // copy the range over to the output
            std::copy(input.begin() + seed, input.begin() + end + 1, std::back_inserter(tri));
        }

        Q_assert(seed != end);

        if (end < seed) {
            // slightly more complex case: the end point is behind the seed point.
            // 0 end 2 3 seed 5 6
            // end 2 3 seed
            // calculate new size
            size_t new_size = (seed + 1) - end;

            // move back the end to the start
            std::copy(input.begin() + end, input.begin() + seed + 1, input.begin());

            // clip
            input.resize(new_size);
        } else {
            // simple case: the end point is ahead of the seed point.
            // collapse the range after it backwards over top of the seed
            // and clip it off
            // 0 1 2 seed 4 5 6 end 8 9
            // 0 1 2 seed end 8 9
            // calculate new size
            size_t new_size = input.size() - (end - seed - 1);

            // move range
            std::copy(input.begin() + end, input.end(), input.begin() + seed + 1);

            // clip
            input.resize(new_size);
        }
    }

    // finished
    return result;
}

/*
==================
FixFaceEdges

If the face has any T-junctions, fix them here.
==================
*/
static void FixFaceEdges(node_t *headnode, face_t *f, tjunc_stats_t &stats)
{
    // we were asked not to bother fixing any of the faces.
    if (qbsp_options.tjunc.value() == settings::tjunclevel_t::NONE) {
        f->fragments.push_back(face_fragment_t{f->original_vertices});
        return;
    }

    std::vector<size_t> superface = CreateSuperFace(headnode, f, stats);

    if (superface.size() < 3) {
        // entire face collapsed
        stats.facecollapse++;
        return;
    } else if (superface.size() == 3) {
        // no need to adjust this either
        f->fragments.push_back(face_fragment_t{f->original_vertices});
        return;
    }

    // faces with 4 or more vertices can be done better.
    // temporary storage for result faces; stored as a list
    // since a resize may steal references out from underneath us
    // as the functions do their work.
    std::list<std::vector<size_t>> faces;

    // do MWT first; it will generate optimal results for everything.
    if (qbsp_options.tjunc.value() >= settings::tjunclevel_t::MWT) {
        faces = mwt_face(f, superface, stats);

        if (faces.size()) {
            stats.mwt++;
            stats.facemwt += faces.size() - 1;
        }
    }

    // brute force rotating the start point until we find a valid winding
    // that doesn't have any T-junctions
    if (!faces.size() && qbsp_options.tjunc.value() >= settings::tjunclevel_t::ROTATE) {
        size_t i = 0;

        for (; i < superface.size(); i++) {
            size_t x = 0;

            // try vertex i as the base, see if we find any zero-area triangles
            for (; x < superface.size() - 2; x++) {
                auto v0 = superface[i];
                auto v1 = superface[(i + x + 1) % superface.size()];
                auto v2 = superface[(i + x + 2) % superface.size()];

                if (!TriangleIsValid(v0, v1, v2, 0.01)) {
                    break;
                }
            }

            if (x == superface.size() - 2) {
                // found one!
                break;
            }
        }

        if (i == superface.size()) {
            // can't simply rotate to eliminate zero-area triangles, so we have
            // to do a bit of re-topology.
            if (qbsp_options.tjunc.value() >= settings::tjunclevel_t::RETOPOLOGIZE) {
                if (auto retopology = RetopologizeFace(f, superface); retopology.size() > 1) {
                    stats.retopology++;
                    stats.faceretopology += retopology.size() - 1;
                    faces = std::move(retopology);
                }
            }

            if (!faces.size()) {
                // unable to re-topologize, so just stick with the superface.
                // it's got zero-area triangles that fill in the gaps.
                stats.norotates++;
            }
        } else if (i != 0) {
            // was able to rotate the superface to eliminate zero-area triangles.
            stats.rotates++;

            auto &output = faces.emplace_back(superface.size());
            // copy base -> end
            auto out = std::copy(superface.begin() + i, superface.end(), output.begin());
            // copy end -> base
            std::copy(superface.begin(), superface.begin() + i, out);
        }
    }

    // the other techniques all failed, or we asked to not
    // try them. just move the superface in directly.
    if (!faces.size()) {
        faces.push_back(std::move(superface));
    }

    Q_assert(faces.size());

    // split giant superfaces into subfaces if we have an edge limit.
    if (qbsp_options.maxedges.value()) {
        for (auto &face : faces) {
            Q_assert(face.size() >= 3);
            SplitFaceIntoFragments(face, faces, stats);
            Q_assert(face.size() >= 3);
        }
    }

    // move the results into the face
    f->fragments.reserve(faces.size());

    for (auto &face : faces) {
        f->fragments.push_back(face_fragment_t{std::move(face)});
    }

    for (auto &frag : f->fragments) {
        Q_assert(frag.output_vertices.size() >= 3);
    }
}

#include <common/parallel.hh>

/*
==================
FixEdges_r
==================
*/
static void FindFaces_r(node_t *node, std::unordered_set<face_t *> &faces)
{
    if (node->is_leaf()) {
        return;
    }

    auto *nodedata = node->get_nodedata();

    for (auto &f : nodedata->facelist) {
        // might have been omitted, so `original_vertices` will be empty
        if (f->original_vertices.size()) {
            faces.insert(f.get());
        }
    }

    FindFaces_r(nodedata->children[0], faces);
    FindFaces_r(nodedata->children[1], faces);
}

/*
===========
TJunc fixing entry point
===========
*/
void TJunc(node_t *headnode)
{
    logging::funcheader();

    tjunc_stats_t stats{};
    std::unordered_set<face_t *> faces;

    FindFaces_r(headnode, faces);

    logging::parallel_for_each(faces, [&](auto &face) { FixFaceEdges(headnode, face, stats); });
}
