/*
    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.
*/
// portals.c

#include <fstream>
#include <fmt/ostream.h>
#include <qbsp/qbsp.hh>

node_t outside_node; // portals outside the world face this

class portal_state_t
{
public:
    int num_visportals;
    int num_visleafs; // leafs the player can be in
    int num_visclusters; // clusters of leafs
    int iNodesDone;
    bool uses_detail;
};

/*
==============================================================================

PORTAL FILE GENERATION

==============================================================================
*/

static void WriteFloat(std::ofstream &portalFile, vec_t v)
{
    if (fabs(v - Q_rint(v)) < ZERO_EPSILON)
        fmt::print(portalFile, "{} ", (int)Q_rint(v));
    else
        fmt::print(portalFile, "{} ", v);
}

static contentflags_t ClusterContents(const node_t *node)
{
    /* Pass the leaf contents up the stack */
    if (node->planenum == PLANENUM_LEAF)
        return node->contents;

    return options.target_game->cluster_contents(
        ClusterContents(node->children[0]), ClusterContents(node->children[1]));
}

/* Return true if possible to see the through the contents of the portals nodes */
static bool PortalThru(const portal_t *p)
{
    contentflags_t contents0 = ClusterContents(p->nodes[0]);
    contentflags_t contents1 = ClusterContents(p->nodes[1]);

    /* Can't see through func_illusionary_visblocker */
    if ((contents0.extended | contents1.extended) & CFLAGS_ILLUSIONARY_VISBLOCKER)
        return false;

    // FIXME: we can't move this directly to portal_can_see_through because
    // "options" isn't exposed there.
    if (options.target_game->id != GAME_QUAKE_II) {
        /* If water is transparent, liquids are like empty space */
        if (options.fTranswater) {
            if (contents0.is_liquid(options.target_game) && contents1.is_empty(options.target_game))
                return true;
            if (contents1.is_liquid(options.target_game) && contents0.is_empty(options.target_game))
                return true;
        }

        /* If sky is transparent, then sky is like empty space */
        if (options.fTranssky) {
            if (contents0.is_sky(options.target_game) && contents1.is_empty(options.target_game))
                return true;
            if (contents0.is_empty(options.target_game) && contents1.is_sky(options.target_game))
                return true;
        }
    }

    // Check per-game visibility
    return options.target_game->portal_can_see_through(contents0, contents1);
}

static void WritePortals_r(node_t *node, std::ofstream &portalFile, bool clusters)
{
    const portal_t *p, *next;
    std::optional<winding_t> w;
    const qbsp_plane_t *pl;
    int i, front, back;
    plane_t plane2;

    if (node->planenum != PLANENUM_LEAF && !node->detail_separator) {
        WritePortals_r(node->children[0], portalFile, clusters);
        WritePortals_r(node->children[1], portalFile, clusters);
        return;
    }
    if (node->contents.is_solid(options.target_game))
        return;

    for (p = node->portals; p; p = next) {
        next = (p->nodes[0] == node) ? p->next[0] : p->next[1];
        if (!p->winding || p->nodes[0] != node)
            continue;
        if (!PortalThru(p))
            continue;

        w = p->winding;
        front = clusters ? p->nodes[0]->viscluster : p->nodes[0]->visleafnum;
        back = clusters ? p->nodes[1]->viscluster : p->nodes[1]->visleafnum;

        /*
         * sometimes planes get turned around when they are very near the
         * changeover point between different axis.  interpret the plane the
         * same way vis will, and flip the side orders if needed
         */
        pl = &map.planes[p->planenum];
        plane2 = w->plane();
        if (DotProduct(pl->normal, plane2.normal) < 1.0 - ANGLEEPSILON)
            fmt::print(portalFile, "{} {} {} ", w->size(), back, front);
        else
            fmt::print(portalFile, "{} {} {} ", w->size(), front, back);

        for (i = 0; i < w->size(); i++) {
            fmt::print(portalFile, "(");
            WriteFloat(portalFile, w->at(i)[0]);
            WriteFloat(portalFile, w->at(i)[1]);
            WriteFloat(portalFile, w->at(i)[2]);
            fmt::print(portalFile, ") ");
        }
        fmt::print(portalFile, "\n");
    }
}

static int WriteClusters_r(node_t *node, std::ofstream &portalFile, int viscluster)
{
    if (node->planenum != PLANENUM_LEAF) {
        viscluster = WriteClusters_r(node->children[0], portalFile, viscluster);
        viscluster = WriteClusters_r(node->children[1], portalFile, viscluster);
        return viscluster;
    }
    if (node->contents.is_solid(options.target_game))
        return viscluster;

    /* If we're in the next cluster, start a new line */
    if (node->viscluster != viscluster) {
        fmt::print(portalFile, "-1\n");
        viscluster++;
    }

    /* Sanity check */
    if (node->viscluster != viscluster)
        FError("Internal error: Detail cluster mismatch");

    fmt::print(portalFile, "{} ", node->visleafnum);

    return viscluster;
}

static void CountPortals(const node_t *node, portal_state_t *state)
{
    const portal_t *portal;

    for (portal = node->portals; portal;) {
        /* only write out from first leaf */
        if (portal->nodes[0] == node) {
            if (PortalThru(portal))
                state->num_visportals++;
            portal = portal->next[0];
        } else {
            portal = portal->next[1];
        }
    }
}

/*
================
NumberLeafs_r
- Assigns leaf numbers and cluster numbers
- If cluster < 0, assign next available global cluster number and increment
- Otherwise, assign the given cluster number because parent splitter is detail
================
*/
static void NumberLeafs_r(node_t *node, portal_state_t *state, int cluster)
{
    /* decision node */
    if (node->planenum != PLANENUM_LEAF) {
        node->visleafnum = -99;
        node->viscluster = -99;
        if (cluster < 0 && node->detail_separator) {
            state->uses_detail = true;
            cluster = state->num_visclusters++;
            node->viscluster = cluster;
            CountPortals(node, state);
        }
        NumberLeafs_r(node->children[0], state, cluster);
        NumberLeafs_r(node->children[1], state, cluster);
        return;
    }

    if (node->contents.is_solid(options.target_game)) {
        /* solid block, viewpoint never inside */
        node->visleafnum = -1;
        node->viscluster = -1;
        return;
    }

    node->visleafnum = state->num_visleafs++;
    node->viscluster = (cluster < 0) ? state->num_visclusters++ : cluster;
    CountPortals(node, state);
}

/*
================
WritePortalfile
================
*/
static void WritePortalfile(node_t *headnode, portal_state_t *state)
{
    int check;

    /*
     * Set the visleafnum and viscluster field in every leaf and count the
     * total number of portals.
     */
    state->num_visleafs = 0;
    state->num_visclusters = 0;
    state->num_visportals = 0;
    state->uses_detail = false;
    NumberLeafs_r(headnode, state, -1);

    // write the file
    options.szBSPName.replace_extension("prt");

    std::ofstream portalFile(options.szBSPName, std::ios_base::binary | std::ios_base::out);
    if (!portalFile)
        FError("Failed to open {}: {}", options.szBSPName, strerror(errno));

    // q2 uses a PRT1 file, but with clusters.
    // (Since q2bsp natively supports clusters, we don't need PRT2.)
    if (options.target_game->id == GAME_QUAKE_II) {
        fmt::print(portalFile, "PRT1\n");
        fmt::print(portalFile, "{}\n", state->num_visclusters);
        fmt::print(portalFile, "{}\n", state->num_visportals);
        WritePortals_r(headnode, portalFile, true);
        return;
    }

    /* If no detail clusters, just use a normal PRT1 format */
    if (!state->uses_detail) {
        fmt::print(portalFile, "PRT1\n");
        fmt::print(portalFile, "{}\n", state->num_visleafs);
        fmt::print(portalFile, "{}\n", state->num_visportals);
        WritePortals_r(headnode, portalFile, false);
    } else {
        if (options.fForcePRT1) {
            /* Write a PRT1 file for loading in the map editor. Vis will reject it. */
            fmt::print(portalFile, "PRT1\n");
            fmt::print(portalFile, "{}\n", state->num_visclusters);
            fmt::print(portalFile, "{}\n", state->num_visportals);
            WritePortals_r(headnode, portalFile, true);
        } else {
            /* Write a PRT2 */
            fmt::print(portalFile, "PRT2\n");
            fmt::print(portalFile, "{}\n", state->num_visleafs);
            fmt::print(portalFile, "{}\n", state->num_visclusters);
            fmt::print(portalFile, "{}\n", state->num_visportals);
            WritePortals_r(headnode, portalFile, true);
            check = WriteClusters_r(headnode, portalFile, 0);
            if (check != state->num_visclusters - 1)
                FError("Internal error: Detail cluster mismatch");
            fmt::print(portalFile, "-1\n");
        }
    }
}

//=============================================================================

/*
=============
AddPortalToNodes
=============
*/
static void AddPortalToNodes(portal_t *p, node_t *front, node_t *back)
{
    if (p->nodes[0] || p->nodes[1])
        FError("portal already included");

    p->nodes[0] = front;
    p->next[0] = front->portals;
    front->portals = p;

    p->nodes[1] = back;
    p->next[1] = back->portals;
    back->portals = p;
}

/*
=============
RemovePortalFromNode
=============
*/
static void RemovePortalFromNode(portal_t *portal, node_t *l)
{
    portal_t **pp, *t;

    // remove reference to the current portal
    pp = &l->portals;
    while (1) {
        t = *pp;
        if (!t)
            FError("Portal not in leaf");

        if (t == portal)
            break;

        if (t->nodes[0] == l)
            pp = &t->next[0];
        else if (t->nodes[1] == l)
            pp = &t->next[1];
        else
            FError("Portal not bounding leaf");
    }

    if (portal->nodes[0] == l) {
        *pp = portal->next[0];
        portal->nodes[0] = NULL;
    } else if (portal->nodes[1] == l) {
        *pp = portal->next[1];
        portal->nodes[1] = NULL;
    }
}

/*
================
MakeHeadnodePortals

The created portals will face the global outside_node
================
*/
static void MakeHeadnodePortals(const mapentity_t *entity, node_t *node)
{
    int i, j, n;
    portal_t *p, *portals[6];
    qbsp_plane_t bplanes[6], *pl;
    int side;

    // pad with some space so there will never be null volume leafs
    aabb3d bounds = entity->bounds.grow(SIDESPACE);

    outside_node.planenum = PLANENUM_LEAF;
    outside_node.contents = options.target_game->create_solid_contents();
    outside_node.portals = NULL;

    // create 6 portals forming a cube around the bounds of the map.
    // these portals will have `outside_node` on one side, and headnode on the other.
    for (i = 0; i < 3; i++)
        for (j = 0; j < 2; j++) {
            n = j * 3 + i;

            p = new portal_t{};
            portals[n] = p;

            pl = &bplanes[n];
            memset(pl, 0, sizeof(*pl));
            if (j) {
                pl->normal[i] = -1;
                pl->dist = -bounds[j][i];
            } else {
                pl->normal[i] = 1;
                pl->dist = bounds[j][i];
            }
            p->planenum = FindPlane(&pl->normal[0], pl->dist, &side);

            p->winding = BaseWindingForPlane(pl);
            if (side)
                AddPortalToNodes(p, &outside_node, node);
            else
                AddPortalToNodes(p, node, &outside_node);
        }

    // clip the basewindings by all the other planes
    for (i = 0; i < 6; i++) {
        for (j = 0; j < 6; j++) {
            if (j == i)
                continue;
            portals[i]->winding =
                portals[i]->winding->clip(bplanes[j].normal, bplanes[j].dist, ON_EPSILON, true)[SIDE_FRONT];
        }
    }
}

//============================================================================

#ifdef PARANOID

static void CheckWindingInNode(winding_t *w, node_t *node)
{
    int i, j;

    for (i = 0; i < w->numpoints; i++) {
        for (j = 0; j < 3; j++)
            if (w->points[i][j] < node->mins[j] - 1 || w->points[i][j] > node->maxs[j] + 1) {
                LogPrint("WARNING: Winding outside node\n");
                return;
            }
    }
}

static void CheckWindingArea(winding_t *w)
{
    int i;
    vec_t total, add;
    vec3_t v1, v2, cross;

    total = 0;
    for (i = 1; i < w->numpoints; i++) {
        VectorSubtract(w->points[i], w->points[0], v1);
        VectorSubtract(w->points[i + 1], w->points[0], v2);
        CrossProduct(v1, v2, cross);
        add = VectorLength(cross);
        total += add * 0.5;
    }
    if (total < 16)
        LogPrint("WARNING: Winding with area {}\n", total);
}

static void CheckLeafPortalConsistancy(node_t *node)
{
    int side, side2;
    portal_t *p, *p2;
    qbsp_plane_t plane, plane2;
    int i;
    winding_t *w;
    vec_t dist;

    side = side2 = 0; // quiet compiler warning

    for (p = node->portals; p; p = p->next[side]) {
        if (p->nodes[0] == node)
            side = 0;
        else if (p->nodes[1] == node)
            side = 1;
        else
            FError("Mislinked portal");

        CheckWindingInNode(p->winding, node);
        CheckWindingArea(p->winding);

        // check that the side orders are correct
        plane = map.planes[p->planenum];
        plane2 = p->winding.plane();

        for (p2 = node->portals; p2; p2 = p2->next[side2]) {
            if (p2->nodes[0] == node)
                side2 = 0;
            else if (p2->nodes[1] == node)
                side2 = 1;
            else
                FError("Mislinked portal");

            w = p2->winding;
            for (i = 0; i < w->numpoints; i++) {
                dist = DotProduct(w->points[i], plane.normal) - plane.dist;
                if ((side == 0 && dist < -1) || (side == 1 && dist > 1)) {
                    LogPrint("WARNING: Portal siding direction is wrong\n");
                    return;
                }
            }
        }
    }
}

#endif /* PARANOID */

//============================================================================

/*
================
CutNodePortals_r
================
*/
static void CutNodePortals_r(node_t *node, portal_state_t *state)
{
    const qbsp_plane_t *plane;
    qbsp_plane_t clipplane;
    node_t *front, *back, *other_node;
    portal_t *portal, *new_portal, *next_portal;
    int side;

#ifdef PARANOID
    CheckLeafPortalConsistancy(node);
#endif

    /* If a leaf, no more dividing */
    if (node->planenum == PLANENUM_LEAF)
        return;

    /* No portals on detail separators */
    if (node->detail_separator)
        return;

    plane = &map.planes[node->planenum];
    front = node->children[SIDE_FRONT];
    back = node->children[SIDE_BACK];

    /*
     * create the new portal by taking the full plane winding for the cutting
     * plane and clipping it by all of the planes from the other portals
     */
    new_portal = new portal_t{};
    new_portal->planenum = node->planenum;

    std::optional<winding_t> winding = BaseWindingForPlane(plane);
    for (portal = node->portals; portal; portal = portal->next[side]) {
        clipplane = map.planes[portal->planenum];
        if (portal->nodes[0] == node)
            side = SIDE_FRONT;
        else if (portal->nodes[1] == node) {
            clipplane.dist = -clipplane.dist;
            VectorSubtract(vec3_origin, clipplane.normal, clipplane.normal);
            side = SIDE_BACK;
        } else
            FError("Mislinked portal");

        winding = winding->clip(clipplane.normal, clipplane.dist, ON_EPSILON, true)[SIDE_FRONT];
        if (!winding) {
            FLogPrint("WARNING: New portal was clipped away near ({:.3} {:.3} {:.3})\n", portal->winding->at(0)[0],
                portal->winding->at(0)[1], portal->winding->at(0)[2]);
            break;
        }
    }

    /* If the plane was not clipped on all sides, there was an error */
    if (winding) {
        new_portal->winding = winding;
        AddPortalToNodes(new_portal, front, back);
    }

    /* partition the portals */
    for (portal = node->portals; portal; portal = next_portal) {
        if (portal->nodes[0] == node)
            side = SIDE_FRONT;
        else if (portal->nodes[1] == node)
            side = SIDE_BACK;
        else
            FError("Mislinked portal");
        next_portal = portal->next[side];

        other_node = portal->nodes[!side];
        RemovePortalFromNode(portal, portal->nodes[0]);
        RemovePortalFromNode(portal, portal->nodes[1]);

        /* cut the portal into two portals, one on each side of the cut plane */
        auto windings = portal->winding->clip(plane->normal, plane->dist, ON_EPSILON);

        if (!windings[SIDE_FRONT]) {
            if (side == SIDE_FRONT)
                AddPortalToNodes(portal, back, other_node);
            else
                AddPortalToNodes(portal, other_node, back);
            continue;
        }
        if (!windings[SIDE_BACK]) {
            if (side == SIDE_FRONT)
                AddPortalToNodes(portal, front, other_node);
            else
                AddPortalToNodes(portal, other_node, front);
            continue;
        }

        /* the winding is split */
        new_portal = new portal_t{};
        *new_portal = *portal;
        new_portal->winding = std::move(windings[SIDE_BACK]);
        portal->winding = std::move(windings[SIDE_FRONT]);

        if (side == SIDE_FRONT) {
            AddPortalToNodes(portal, front, other_node);
            AddPortalToNodes(new_portal, back, other_node);
        } else {
            AddPortalToNodes(portal, other_node, front);
            AddPortalToNodes(new_portal, other_node, back);
        }
    }

    /* Display progress */
    state->iNodesDone++;
    LogPercent(state->iNodesDone, splitnodes.load());

    CutNodePortals_r(front, state);
    CutNodePortals_r(back, state);
}

/*
==================
PortalizeWorld

Builds the exact polyhedrons for the nodes and leafs
==================
*/
void PortalizeWorld(const mapentity_t *entity, node_t *headnode, const int hullnum)
{
    LogPrint(LOG_PROGRESS, "---- {} ----\n", __func__);

    portal_state_t state{};

    state.iNodesDone = 0;

    MakeHeadnodePortals(entity, headnode);
    CutNodePortals_r(headnode, &state);

    if (hullnum <= 0) {
        /* save portal file for vis tracing */
        WritePortalfile(headnode, &state);

        LogPrint(LOG_STAT, "     {:8} vis leafs\n", state.num_visleafs);
        LogPrint(LOG_STAT, "     {:8} vis clusters\n", state.num_visclusters);
        LogPrint(LOG_STAT, "     {:8} vis portals\n", state.num_visportals);
    }
}

/*
==================
FreeAllPortals

==================
*/
void FreeAllPortals(node_t *node)
{
    portal_t *p, *nextp;

    if (node->planenum != PLANENUM_LEAF) {
        FreeAllPortals(node->children[0]);
        FreeAllPortals(node->children[1]);
    }

    for (p = node->portals; p; p = nextp) {
        if (p->nodes[0] == node)
            nextp = p->next[0];
        else
            nextp = p->next[1];
        RemovePortalFromNode(p, p->nodes[0]);
        RemovePortalFromNode(p, p->nodes[1]);
        delete p;
    }
    node->portals = NULL;
}
