//
//  Copyright (C) 2025 Guilherme Miranda
//
//  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.
//
//  DESCRIPTION:
//    Universal Doom Map Format support.
//

#include "p_udmf.h"
#include "doomdata.h"
#include "doomdef.h"
#include "doomstat.h"
#include "doomtype.h"
#include "i_system.h"
#include "m_arena.h"
#include "m_argv.h"
#include "m_array.h"
#include "m_fixed.h"
#include "m_misc.h"
#include "m_scanner.h"
#include "m_swap.h"
#include "p_bsp.h"
#include "p_mobj.h"
#include "p_setup.h"
#include "p_spec.h"
#include "r_data.h"
#include "r_defs.h"
#include "r_state.h"
#include "r_tranmap.h"
#include "tables.h"
#include "w_wad.h"
#include <math.h>

//
// Universal Doom Map Format (UDMF) support
//

typedef enum
{
    UDMF_BASE = (0), // shut compiler up, also reset when parsing new map

    UDMF_THING_FRIEND  = (1u << 0), // Marine's Best Friend :)
    UDMF_THING_SPECIAL = (1u << 1), // Death/Pickup/etc-activated actions
    UDMF_THING_PARAM   = (1u << 2), // ditto, also customizes some MObjs
    UDMF_THING_HEALTH  = (1u << 3), // positive is a multiple, negative is override
    UDMF_THING_GRAVITY = (1u << 4), // per-mobj custom gravity
    UDMF_THING_ALPHA   = (1u << 5), // opacity percentage
    UDMF_THING_TRANMAP = (1u << 6), // ditto, also customizable LUT
    UDMF_THING_TINT    = (1u << 7), // view-agnostic colormap for the given mobj

    UDMF_LINE_PARAM    = (1u << 8), // Hexen-style param actions
    UDMF_LINE_PASSUSE  = (1u << 9), // Boom's "Pass Use Through" line flag
    UDMF_LINE_BLOCK    = (1u << 10), // MBF21's entity blocking flags
    UDMF_LINE_3DMIDTEX = (1u << 11), // EE's 3D middle texture
    UDMF_LINE_ALPHA    = (1u << 12), // opacity percentage
    UDMF_LINE_TRANMAP  = (1u << 13), // ditto, also customizable LUT
    UDMF_LINE_STYLE    = (1u << 14), // custom automap style

    UDMF_SIDE_OFFSET   = (1u << 15), // texture X/Y alignment
    UDMF_SIDE_SCROLL   = (1u << 16), // texture scrolling property
    UDMF_SIDE_LIGHT    = (1u << 17), // independent light levels
    UDMF_SIDE_TINT     = (1u << 18), // view-agnostic colormap for the given sidedef

    UDMF_SEC_ANGLE     = (1u << 19), // plane rotation
    UDMF_SEC_OFFSET    = (1u << 20), // plane X/Y alignment
    UDMF_SEC_EE_SCROLL = (1u << 21), // EE's original plane scrolling property
    UDMF_SEC_SCROLL    = (1u << 22), // DSDA's later plane scrolling property
    UDMF_SEC_LIGHT     = (1u << 23), // independent light levels
    UDMF_SEC_GRAVITY   = (1u << 24), // WIP
    UDMF_SEC_COLORMAP  = (1u << 25), // viewplayer's colormap on this given frame
    UDMF_SEC_TINT      = (1u << 26), // view-agnostic colormap for the given sector
    UDMF_SEC_SILENCE   = (1u << 27), // WIP

    // Compatibility
    UDMF_COMP_NO_ARG0  = (1u << 31),
} UDMF_Features_t;

typedef struct
{
    // Base spec
    int32_t tid;
    int32_t type;
    double x, y;
    double height;
    int32_t angle;
    int32_t options;
    int32_t special;
    int32_t args[5];

    // Extensions
    double health;
    char tranmap[9];
    double alpha;
    char tint[9];
} UDMF_Thing_t;

typedef struct
{
    // Base spec
    double x;
    double y;
} UDMF_Vertex_t;

typedef struct
{
    // Base spec
    int32_t id;
    int32_t v1_id, v2_id;
    int32_t special;
    int32_t args[5];
    int32_t sidefront, sideback;
    int32_t flags;

    // Extensions
    char tranmap[9];
    double alpha;
    amls_t amls;
} UDMF_Linedef_t;

// Important note about line tag/id/arg0, in the Doom/Heretic/Strife namespaces:
// The base UDMF spec makes a distinction between the value used to identify a
// specific line (id), and the value used when an action is executed (arg0),
// as opposed to the Doom map format, that used both as the same (tag).

typedef struct
{
    // Base spec
    int32_t sector_id;
    char texturetop[9];
    char texturemiddle[9];
    char texturebottom[9];
    int32_t offsetx, offsety;

    // Extensions
    int32_t flags;

    int32_t xscroll, yscroll;

    int32_t light;
    int32_t light_top;
    int32_t light_mid;
    int32_t light_bottom;

    char tint[9];

    double offsetx_top,    offsety_top;
    double offsetx_mid,    offsety_mid;
    double offsetx_bottom, offsety_bottom;

    double xscrolltop,    yscrolltop;
    double xscrollmid,    yscrollmid;
    double xscrollbottom, yscrollbottom;
} UDMF_Sidedef_t;

typedef struct
{
    // Base spec
    int32_t tag;
    int32_t heightfloor;
    int32_t heightceiling;
    char texturefloor[9];
    char textureceiling[9];
    int32_t lightlevel;
    int32_t special;

    // Extensions
    int32_t flags;

    int32_t lightfloor, lightceiling;

    char colormap[9];
    char tint[9], tintceiling[9], tintfloor[9];

    double xpanningfloor,   ypanningfloor;
    double xpanningceiling, ypanningceiling;
    double rotationfloor, rotationceiling;

    double xscrollfloor,   yscrollfloor;
    double xscrollceiling, yscrollceiling;
    int32_t scrollfloormode, scrollceilingmode;

    double scroll_floor_x, scroll_floor_y;
    double scroll_ceil_x,  scroll_ceil_y;
    int32_t scroll_floor_type, scroll_ceil_type;
} UDMF_Sector_t;

static UDMF_Features_t udmf_flags = UDMF_BASE;

static UDMF_Vertex_t *udmf_vertexes = NULL;
static UDMF_Linedef_t *udmf_linedefs = NULL;
static UDMF_Sidedef_t *udmf_sidedefs = NULL;
static UDMF_Sector_t *udmf_sectors = NULL;
static UDMF_Thing_t *udmf_things = NULL;

void UDMF_ClearMemory(void)
{
    array_free(udmf_vertexes);
    array_free(udmf_linedefs);
    array_free(udmf_sidedefs);
    array_free(udmf_sectors);
    array_free(udmf_things);
}

//
// UDMF parsing utils
//

// Retrieve plain integer
inline static int UDMF_ScanInt(scanner_t *s)
{
    int x = 0;
    SC_MustGetToken(s, '=');
    x = SC_GetNegativeInteger(s);
    SC_MustGetToken(s, ';');
    return x;
}

// Retrieve plain double
inline static double UDMF_ScanDouble(scanner_t *s)
{
    double x = 0;
    SC_MustGetToken(s, '=');
    x = SC_GetNegativeDecimal(s);
    SC_MustGetToken(s, ';');
    return x;
}

// Sets provided flag on, if true
inline static int UDMF_ScanFlag(scanner_t *s, int f)
{
    int x = 0;
    SC_MustGetToken(s, '=');
    SC_MustGetToken(s, TK_BoolConst);
    if (SC_GetBoolean(s))
    {
        x |= f;
    }
    SC_MustGetToken(s, ';');
    return x;
}

// Retrieve plain string
inline static void UDMF_ScanLumpName(scanner_t *s, char *x)
{
    SC_MustGetToken(s, '=');
    SC_MustGetToken(s, TK_StringConst);
    M_CopyLumpName(x, SC_GetString(s));
    SC_MustGetToken(s, ';');
}

// Property is valid in all namespaces
#define BASE_PROP(keyword) (!strcmp(prop, #keyword))

// Property is valid in the current namespace
#define PROP(keyword, flags) \
    ((udmf_flags & (flags)) && !strcmp(prop, #keyword))

// Parse specific string properties
inline static int32_t UDMF_ScanSectorScroll(scanner_t *s)
{
    int32_t mode = 0;
    SC_MustGetToken(s, '=');
    SC_MustGetToken(s, TK_StringConst);
    const char *buf = SC_GetString(s);
    M_StringToLower((char *)buf);
    if (!strcmp(buf, "visual"))
      mode = SCROLL_TEXTURE;
    else if (!strcmp(buf, "physical"))
      mode = SCROLL_CARRY;
    else if (!strcmp(buf, "both"))
      mode = SCROLL_ALL;
    SC_MustGetToken(s, ';');
    return mode;
}

// Skip unknown keyword
static inline void UDMF_SkipScan(scanner_t *s)
{
    if (SC_CheckToken(s, '='))
    {
        while (SC_TokensLeft(s))
        {
            if (SC_CheckToken(s, ';'))
            {
                break;
            }

            SC_GetNextToken(s, true);
        }
        return;
    }

    SC_MustGetToken(s, '{');
    int brace_count = 1;
    while (SC_TokensLeft(s))
    {
        if (SC_CheckToken(s, '}'))
        {
            --brace_count;
        }
        else if (SC_CheckToken(s, '{'))
        {
            ++brace_count;
        }
        if (!brace_count)
        {
            break;
        }
        SC_GetNextToken(s, true);
    }
}

// UDMF namespace
static void UDMF_ParseNamespace(scanner_t *s)
{
    SC_MustGetToken(s, '=');
    SC_MustGetToken(s, TK_StringConst);
    const char *name = SC_GetString(s);
    udmf_flags = UDMF_BASE;

    if (!strcasecmp(name, "doom"))
    {
        udmf_flags |= UDMF_LINE_PASSUSE | UDMF_THING_FRIEND;
    }
    else if (!strcasecmp(name, "woof"))
    {
        udmf_flags |= UDMF_LINE_PASSUSE | UDMF_LINE_BLOCK | UDMF_LINE_ALPHA | UDMF_LINE_TRANMAP;
        udmf_flags |= UDMF_THING_FRIEND | UDMF_THING_PARAM | UDMF_THING_HEALTH | UDMF_THING_ALPHA | UDMF_THING_TRANMAP | UDMF_THING_TINT;
        udmf_flags |= UDMF_SIDE_OFFSET | UDMF_SIDE_SCROLL | UDMF_SIDE_LIGHT | UDMF_SIDE_TINT;
        udmf_flags |= UDMF_SEC_ANGLE | UDMF_SEC_OFFSET | UDMF_SEC_SCROLL | UDMF_SEC_LIGHT | UDMF_SEC_COLORMAP | UDMF_SEC_TINT;
    }
    else
    {
        I_Error("Unknown UDMF namespace: \"%s\".", name);
    }

    SC_MustGetToken(s, ';');
}

//
// UDMF vertex pasring
//

static void UDMF_ParseVertex(scanner_t *s)
{
    UDMF_Vertex_t vertex = {0};

    SC_MustGetToken(s, '{');
    while (!SC_CheckToken(s, '}'))
    {
        SC_MustGetToken(s, TK_Identifier);
        const char *prop = SC_GetString(s);
        M_StringToLower((char *)prop);
        if (BASE_PROP(x))
        {
            vertex.x = UDMF_ScanDouble(s);
        }
        else if (BASE_PROP(y))
        {
            vertex.y = UDMF_ScanDouble(s);
        }
        else
        {
            UDMF_SkipScan(s);
        }
    }

    array_push(udmf_vertexes, vertex);
}

//
// UDMF linedef loading
//

static void UDMF_ParseLinedef(scanner_t *s)
{
    UDMF_Linedef_t line = {0};
    line.sideback = -1;
    M_CopyLumpName(line.tranmap, "-");
    line.alpha = 1.0;

    SC_MustGetToken(s, '{');
    while (!SC_CheckToken(s, '}'))
    {
        SC_MustGetToken(s, TK_Identifier);
        const char *prop = SC_GetString(s);
        M_StringToLower((char *)prop);
        if (BASE_PROP(v1))
        {
            line.v1_id = UDMF_ScanInt(s);
        }
        else if (BASE_PROP(v2))
        {
            line.v2_id = UDMF_ScanInt(s);
        }
        else if (BASE_PROP(special))
        {
            line.special = UDMF_ScanInt(s);
        }
        else if (BASE_PROP(id))
        {
            line.id = UDMF_ScanInt(s);
        }
        else if (BASE_PROP(arg0))
        {
            // Tag -> id/arg0 split means arg0 is always enabled
            line.args[0] = UDMF_ScanInt(s);
        }
        else if (PROP(arg1, UDMF_LINE_PARAM))
        {
            line.args[1] = UDMF_ScanInt(s);
        }
        else if (PROP(arg2, UDMF_LINE_PARAM))
        {
            line.args[2] = UDMF_ScanInt(s);
        }
        else if (PROP(arg3, UDMF_LINE_PARAM))
        {
            line.args[3] = UDMF_ScanInt(s);
        }
        else if (PROP(arg4, UDMF_LINE_PARAM))
        {
            line.args[4] = UDMF_ScanInt(s);
        }
        else if (BASE_PROP(sidefront))
        {
            line.sidefront = UDMF_ScanInt(s);
        }
        else if (BASE_PROP(sideback))
        {
            line.sideback = UDMF_ScanInt(s);
        }
        else if (BASE_PROP(blocking))
        {
            line.flags |= UDMF_ScanFlag(s, ML_BLOCKING);
        }
        else if (BASE_PROP(blockmonsters))
        {
            line.flags |= UDMF_ScanFlag(s, ML_BLOCKMONSTERS);
        }
        else if (BASE_PROP(twosided))
        {
            line.flags |= UDMF_ScanFlag(s, ML_TWOSIDED);
        }
        else if (BASE_PROP(dontpegtop))
        {
            line.flags |= UDMF_ScanFlag(s, ML_DONTPEGTOP);
        }
        else if (BASE_PROP(dontpegbottom))
        {
            line.flags |= UDMF_ScanFlag(s, ML_DONTPEGBOTTOM);
        }
        else if (BASE_PROP(secret))
        {
            line.flags |= UDMF_ScanFlag(s, ML_SECRET);
        }
        else if (BASE_PROP(blocksound))
        {
            line.flags |= UDMF_ScanFlag(s, ML_SOUNDBLOCK);
        }
        else if (BASE_PROP(dontdraw))
        {
            line.flags |= UDMF_ScanFlag(s, ML_DONTDRAW);
        }
        else if (BASE_PROP(mapped))
        {
            line.flags |= UDMF_ScanFlag(s, ML_MAPPED);
        }
        else if (PROP(passuse, UDMF_LINE_PASSUSE))
        {
            line.flags |= UDMF_ScanFlag(s, ML_PASSUSE);
        }
        else if (PROP(blocklandmonsters, UDMF_LINE_BLOCK))
        {
            line.flags |= UDMF_ScanFlag(s, ML_BLOCKLANDMONSTERS);
        }
        else if (PROP(blockplayers, UDMF_LINE_BLOCK))
        {
            line.flags |= UDMF_ScanFlag(s, ML_BLOCKPLAYERS);
        }
        else if (PROP(midtex3d, UDMF_LINE_3DMIDTEX))
        {
            line.flags |= UDMF_ScanFlag(s, ML_3DMIDTEX);
        }
        else if (PROP(alpha, UDMF_LINE_ALPHA))
        {
            line.alpha = UDMF_ScanDouble(s);
        }
        else if (PROP(tranmap, UDMF_LINE_TRANMAP))
        {
            UDMF_ScanLumpName(s, line.tranmap);
        }
        else if (PROP(automapstyle, UDMF_LINE_STYLE))
        {
            line.amls = UDMF_ScanInt(s);
        }
        else
        {
            UDMF_SkipScan(s);
        }
    }
    array_push(udmf_linedefs, line);
}

//
// UDMF sidedef parsing
//

static void UDMF_ParseSidedef(scanner_t *s)
{
    UDMF_Sidedef_t side = {0};
    M_CopyLumpName(side.texturetop, "-");
    M_CopyLumpName(side.texturemiddle, "-");
    M_CopyLumpName(side.texturebottom, "-");
    M_CopyLumpName(side.tint, "-");

    SC_MustGetToken(s, '{');
    while (!SC_CheckToken(s, '}'))
    {
        SC_MustGetToken(s, TK_Identifier);
        const char *prop = SC_GetString(s);
        M_StringToLower((char *)prop);
        if (BASE_PROP(offsetx))
        {
            side.offsetx = UDMF_ScanInt(s);
        }
        else if (BASE_PROP(offsety))
        {
            side.offsety = UDMF_ScanInt(s);
        }
        else if (BASE_PROP(sector))
        {
            side.sector_id = UDMF_ScanInt(s);
        }
        else if (BASE_PROP(texturetop))
        {
            UDMF_ScanLumpName(s, side.texturetop);
        }
        else if (BASE_PROP(texturemiddle))
        {
            UDMF_ScanLumpName(s, side.texturemiddle);
        }
        else if (BASE_PROP(texturebottom))
        {
            UDMF_ScanLumpName(s, side.texturebottom);
        }
        else if (PROP(light, UDMF_SIDE_LIGHT))
        {
            side.light = UDMF_ScanInt(s);
        }
        else if (PROP(light_top, UDMF_SIDE_LIGHT))
        {
            side.light_top = UDMF_ScanInt(s);
        }
        else if (PROP(light_mid, UDMF_SIDE_LIGHT))
        {
            side.light_mid = UDMF_ScanInt(s);
        }
        else if (PROP(light_bottom, UDMF_SIDE_LIGHT))
        {
            side.light_bottom = UDMF_ScanInt(s);
        }
        else if (PROP(lightabsolute, UDMF_SIDE_LIGHT))
        {
            side.flags |= UDMF_ScanFlag(s, SF_ABS_LIGHT);
        }
        else if (PROP(lightabsolute_top, UDMF_SIDE_LIGHT))
        {
            side.flags |= UDMF_ScanFlag(s, SF_ABS_LIGHT_TOP);
        }
        else if (PROP(lightabsolute_mid, UDMF_SIDE_LIGHT))
        {
            side.flags |= UDMF_ScanFlag(s, SF_ABS_LIGHT_MID);
        }
        else if (PROP(lightabsolute_bottom, UDMF_SIDE_LIGHT))
        {
            side.flags |= UDMF_ScanFlag(s, SF_ABS_LIGHT_BOTTOM);
        }
        else if (PROP(nofakecontrast, UDMF_SIDE_LIGHT))
        {
            side.flags |= UDMF_ScanFlag(s, SF_NO_FAKE_CONTRAST);
        }
        else if (PROP(smoothlighting, UDMF_SIDE_LIGHT))
        {
            side.flags |= UDMF_ScanFlag(s, SF_SMOOTH_CONTRAST);
        }
        else if (PROP(offsetx_top, UDMF_SIDE_OFFSET))
        {
            side.offsetx_top = UDMF_ScanDouble(s);
        }
        else if (PROP(offsety_top, UDMF_SIDE_OFFSET))
        {
            side.offsety_top = UDMF_ScanDouble(s);
        }
        else if (PROP(offsetx_mid, UDMF_SIDE_OFFSET))
        {
            side.offsetx_mid = UDMF_ScanDouble(s);
        }
        else if (PROP(offsety_mid, UDMF_SIDE_OFFSET))
        {
            side.offsety_mid = UDMF_ScanDouble(s);
        }
        else if (PROP(offsetx_bottom, UDMF_SIDE_OFFSET))
        {
            side.offsetx_bottom = UDMF_ScanDouble(s);
        }
        else if (PROP(offsety_bottom, UDMF_SIDE_OFFSET))
        {
            side.offsety_bottom = UDMF_ScanDouble(s);
        }
        else if (PROP(xscroll, UDMF_SIDE_SCROLL))
        {
            side.xscroll = UDMF_ScanInt(s);
        }
        else if (PROP(yscroll, UDMF_SIDE_SCROLL))
        {
            side.yscroll = UDMF_ScanInt(s);
        }
        else if (PROP(xscrolltop, UDMF_SIDE_SCROLL))
        {
            side.xscrolltop = UDMF_ScanDouble(s);
        }
        else if (PROP(yscrolltop, UDMF_SIDE_SCROLL))
        {
            side.yscrolltop = UDMF_ScanDouble(s);
        }
        else if (PROP(xscrollmid, UDMF_SIDE_SCROLL))
        {
            side.xscrollmid = UDMF_ScanDouble(s);
        }
        else if (PROP(yscrollmid, UDMF_SIDE_SCROLL))
        {
            side.yscrollmid = UDMF_ScanDouble(s);
        }
        else if (PROP(xscrollbottom, UDMF_SIDE_SCROLL))
        {
            side.xscrollbottom = UDMF_ScanDouble(s);
        }
        else if (PROP(yscrollbottom, UDMF_SIDE_SCROLL))
        {
            side.yscrollbottom = UDMF_ScanDouble(s);
        }
        else if (PROP(tint, UDMF_SIDE_TINT))
        {
            UDMF_ScanLumpName(s, side.tint);
        }
        else
        {
            UDMF_SkipScan(s);
        }
    }

    array_push(udmf_sidedefs, side);
}

//
// UDMF sector parsing
//

static void UDMF_ParseSector(scanner_t *s)
{
    UDMF_Sector_t sector = {0};
    sector.lightlevel = 160;
    M_CopyLumpName(sector.texturefloor, "-");
    M_CopyLumpName(sector.textureceiling, "-");

    SC_MustGetToken(s, '{');
    while (!SC_CheckToken(s, '}'))
    {
        SC_MustGetToken(s, TK_Identifier);
        const char *prop = SC_GetString(s);
        M_StringToLower((char *)prop);
        if (BASE_PROP(heightfloor))
        {
            sector.heightfloor = UDMF_ScanInt(s);
        }
        else if (BASE_PROP(heightceiling))
        {
            sector.heightceiling = UDMF_ScanInt(s);
        }
        else if (BASE_PROP(texturefloor))
        {
            UDMF_ScanLumpName(s, sector.texturefloor);
        }
        else if (BASE_PROP(textureceiling))
        {
            UDMF_ScanLumpName(s, sector.textureceiling);
        }
        else if (BASE_PROP(lightlevel))
        {
            sector.lightlevel = UDMF_ScanInt(s);
        }
        else if (BASE_PROP(special))
        {
            sector.special = UDMF_ScanInt(s);
        }
        else if (BASE_PROP(id))
        {
            sector.tag = UDMF_ScanInt(s);
        }
        else if (PROP(rotationfloor, UDMF_SEC_ANGLE))
        {
            sector.rotationfloor = UDMF_ScanDouble(s);
        }
        else if (PROP(rotationceiling, UDMF_SEC_ANGLE))
        {
            sector.rotationceiling = UDMF_ScanDouble(s);
        }
        else if (PROP(xpanningfloor, UDMF_SEC_OFFSET))
        {
            sector.xpanningfloor = UDMF_ScanDouble(s);
        }
        else if (PROP(ypanningfloor, UDMF_SEC_OFFSET))
        {
            sector.ypanningfloor = UDMF_ScanDouble(s);
        }
        else if (PROP(xpanningceiling, UDMF_SEC_OFFSET))
        {
            sector.xpanningceiling = UDMF_ScanDouble(s);
        }
        else if (PROP(ypanningceiling, UDMF_SEC_OFFSET))
        {
            sector.ypanningceiling = UDMF_ScanDouble(s);
        }
        else if (PROP(scroll_floor_x, UDMF_SEC_EE_SCROLL))
        {
            sector.scroll_floor_x = UDMF_ScanDouble(s);
        }
        else if (PROP(scroll_floor_, UDMF_SEC_EE_SCROLL))
        {
            sector.scroll_floor_y = UDMF_ScanDouble(s);
        }
        else if (PROP(scroll_floor_type, UDMF_SEC_EE_SCROLL))
        {
            sector.scroll_floor_type = UDMF_ScanSectorScroll(s);
        }
        else if (PROP(scroll_ceil_x, UDMF_SEC_EE_SCROLL))
        {
            sector.scroll_ceil_x = UDMF_ScanDouble(s);
        }
        else if (PROP(scroll_ceil_y, UDMF_SEC_EE_SCROLL))
        {
            sector.scroll_ceil_y = UDMF_ScanDouble(s);
        }
        else if (PROP(scroll_ceil_type, UDMF_SEC_EE_SCROLL))
        {
            sector.scroll_ceil_type = UDMF_ScanSectorScroll(s);
        }
        else if (PROP(xscrollfloor, UDMF_SEC_SCROLL))
        {
            sector.xscrollfloor = UDMF_ScanDouble(s);
        }
        else if (PROP(yscrollfloor, UDMF_SEC_SCROLL))
        {
            sector.yscrollfloor = UDMF_ScanDouble(s);
        }
        else if (PROP(xscrollceiling, UDMF_SEC_SCROLL))
        {
            sector.xscrollceiling = UDMF_ScanDouble(s);
        }
        else if (PROP(yscrollceiling, UDMF_SEC_SCROLL))
        {
            sector.yscrollceiling = UDMF_ScanDouble(s);
        }
        else if (PROP(scrollfloormode, UDMF_SEC_SCROLL))
        {
            sector.scrollfloormode = UDMF_ScanInt(s);
        }
        else if (PROP(scrollceilingmode, UDMF_SEC_SCROLL))
        {
            sector.scrollceilingmode = UDMF_ScanInt(s);
        }
        else if (PROP(lightfloor, UDMF_SEC_LIGHT))
        {
            sector.lightfloor = UDMF_ScanInt(s);
        }
        else if (PROP(lightceiling, UDMF_SEC_LIGHT))
        {
            sector.lightceiling = UDMF_ScanInt(s);
        }
        else if (PROP(lightfloorabsolute, UDMF_SEC_LIGHT))
        {
            sector.flags |= UDMF_ScanFlag(s, SECF_ABS_LIGHT_FLOOR);
        }
        else if (PROP(lightceilingabsolute, UDMF_SEC_LIGHT))
        {
            sector.flags |= UDMF_ScanFlag(s, SECF_ABS_LIGHT_CEIL);
        }
        else if (PROP(colormap, UDMF_SEC_COLORMAP))
        {
            UDMF_ScanLumpName(s, sector.colormap);
        }
        else if (PROP(tint, UDMF_SEC_TINT))
        {
            UDMF_ScanLumpName(s, sector.tint);
        }
        else if (PROP(tintfloor, UDMF_SEC_TINT))
        {
            UDMF_ScanLumpName(s, sector.tintfloor);
        }
        else if (PROP(tintceiling, UDMF_SEC_TINT))
        {
            UDMF_ScanLumpName(s, sector.tintceiling);
        }
        else
        {
            UDMF_SkipScan(s);
        }
    }

    array_push(udmf_sectors, sector);
}

//
// UDMF thing loading
//

static void UDMF_ParseThing(scanner_t *s)
{
    UDMF_Thing_t thing = {0};
    thing.options |= MTF_NOTSINGLE | MTF_NOTCOOP | MTF_NOTDM;
    M_CopyLumpName(thing.tranmap, "-");
    thing.alpha = 1.0;
    thing.health = 1.0;

    SC_MustGetToken(s, '{');
    while (!SC_CheckToken(s, '}'))
    {
        SC_MustGetToken(s, TK_Identifier);
        const char *prop = SC_GetString(s);
        M_StringToLower((char *)prop);
        if (BASE_PROP(type))
        {
            thing.type = UDMF_ScanInt(s);
        }
        else if (PROP(id, UDMF_THING_PARAM))
        {
            thing.tid = UDMF_ScanInt(s);
        }
        else if (BASE_PROP(x))
        {
            thing.x = UDMF_ScanDouble(s);
        }
        else if (BASE_PROP(y))
        {
            thing.y = UDMF_ScanDouble(s);
        }
        else if (BASE_PROP(height))
        {
            thing.height = UDMF_ScanDouble(s);
        }
        else if (BASE_PROP(angle))
        {
            thing.angle = UDMF_ScanInt(s);
        }
        else if (BASE_PROP(skill1))
        {
            thing.options |= UDMF_ScanFlag(s, MTF_SKILL1);
        }
        else if (BASE_PROP(skill2))
        {
            thing.options |= UDMF_ScanFlag(s, MTF_SKILL2);
        }
        else if (BASE_PROP(skill3))
        {
            thing.options |= UDMF_ScanFlag(s, MTF_SKILL3);
        }
        else if (BASE_PROP(skill4))
        {
            thing.options |= UDMF_ScanFlag(s, MTF_SKILL4);
        }
        else if (BASE_PROP(skill5))
        {
            thing.options |= UDMF_ScanFlag(s, MTF_SKILL5);
        }
        else if (BASE_PROP(ambush))
        {
            thing.options |= UDMF_ScanFlag(s, MTF_AMBUSH);
        }
        else if (BASE_PROP(single))
        {
            thing.options &= ~UDMF_ScanFlag(s, MTF_NOTSINGLE);
        }
        else if (BASE_PROP(dm))
        {
            thing.options &= ~UDMF_ScanFlag(s, MTF_NOTDM);
        }
        else if (BASE_PROP(coop))
        {
            thing.options &= ~UDMF_ScanFlag(s, MTF_NOTCOOP);
        }
        else if (PROP(friend, UDMF_THING_FRIEND))
        {
            thing.options |= UDMF_ScanFlag(s, MTF_FRIEND);
        }
        else if (PROP(special, UDMF_THING_SPECIAL))
        {
            thing.special = UDMF_ScanInt(s);
        }
        else if (PROP(arg0, UDMF_THING_SPECIAL|UDMF_THING_PARAM))
        {
            thing.args[0] = UDMF_ScanInt(s);
        }
        else if (PROP(arg1, UDMF_THING_PARAM))
        {
            thing.args[1] = UDMF_ScanInt(s);
        }
        else if (PROP(arg2, UDMF_THING_PARAM))
        {
            thing.args[2] = UDMF_ScanInt(s);
        }
        else if (PROP(arg3, UDMF_THING_PARAM))
        {
            thing.args[3] = UDMF_ScanInt(s);
        }
        else if (PROP(arg4, UDMF_THING_PARAM))
        {
            thing.args[4] = UDMF_ScanInt(s);
        }
        else if (PROP(arg4, UDMF_THING_HEALTH))
        {
            thing.health = UDMF_ScanDouble(s);
        }
        else if (PROP(alpha, UDMF_THING_ALPHA))
        {
            thing.alpha = UDMF_ScanDouble(s);
        }
        else if (PROP(tranmap, UDMF_THING_TRANMAP))
        {
            UDMF_ScanLumpName(s, thing.tranmap);
        }
        else if (PROP(tint, UDMF_THING_TINT))
        {
            UDMF_ScanLumpName(s, thing.tint);
        }
        else
        {
            UDMF_SkipScan(s);
        }
    }

    array_push(udmf_things, thing);
}

//
// UDMF textmap loading
//

static void UDMF_ParseTextMap(map_t *map)
{
    scanner_t *s = SC_Open("TEXTMAP", W_CacheLumpNum(map->textmap, PU_CACHE),
                           W_LumpLength(map->textmap));

    while (SC_TokensLeft(s))
    {
        SC_MustGetToken(s, TK_Identifier);
        const char *toplevel = SC_GetString(s);
        M_StringToLower((char *)toplevel);

        if (!strcmp(toplevel, "namespace"))
        {
            UDMF_ParseNamespace(s);
        }
        else if (!strcmp(toplevel, "vertex"))
        {
            UDMF_ParseVertex(s);
        }
        else if (!strcmp(toplevel, "linedef"))
        {
            UDMF_ParseLinedef(s);
        }
        else if (!strcmp(toplevel, "sidedef"))
        {
            UDMF_ParseSidedef(s);
        }
        else if (!strcmp(toplevel, "sector"))
        {
            UDMF_ParseSector(s);
        }
        else if (!strcmp(toplevel, "thing"))
        {
            UDMF_ParseThing(s);
        }
        else
        {
            UDMF_SkipScan(s);
        }
    }

    if (array_size(udmf_vertexes) == 0 || array_size(udmf_linedefs) == 0
        || array_size(udmf_sidedefs) == 0 || array_size(udmf_sectors) == 0
        || array_size(udmf_things) == 0)
    {
        SC_Error(s, "Not enough UDMF data. Check your TEXTMAP.");
    }

    SC_Close(s);
}

static void UDMF_LoadVertexes(void)
{
    numvertexes = array_size(udmf_vertexes);
    vertexes = arena_alloc_num(world_arena, vertex_t, numvertexes);

    for (int i = 0; i < numvertexes; i++)
    {
        vertexes[i].x = DoubleToFixed(udmf_vertexes[i].x);
        vertexes[i].y = DoubleToFixed(udmf_vertexes[i].y);

        vertexes[i].r_x = vertexes[i].x;
        vertexes[i].r_y = vertexes[i].y;
    }
}

static void UDMF_LoadSectors(void)
{
    numsectors = array_size(udmf_sectors);
    sectors = arena_alloc_num(world_arena, sector_t, numsectors);

    for (int i = 0; i < numsectors; i++)
    {
        sectors[i].floorheight = IntToFixed(udmf_sectors[i].heightfloor);
        sectors[i].ceilingheight = IntToFixed(udmf_sectors[i].heightceiling);
        sectors[i].floorpic = R_FlatNumForName(udmf_sectors[i].texturefloor);
        sectors[i].ceilingpic = R_FlatNumForName(udmf_sectors[i].textureceiling);
        sectors[i].lightlevel = udmf_sectors[i].lightlevel;
        sectors[i].special = udmf_sectors[i].special;
        sectors[i].tag = udmf_sectors[i].tag;

        sectors[i].flags = udmf_sectors[i].flags;
        sectors[i].lightfloor = udmf_sectors[i].lightfloor;
        sectors[i].lightceiling = udmf_sectors[i].lightceiling;

        sectors[i].floor_rotation =
            FixedToAngle(DoubleToFixed(udmf_sectors[i].rotationfloor));
        sectors[i].ceiling_rotation =
            FixedToAngle(DoubleToFixed(udmf_sectors[i].rotationceiling));

        sectors[i].floor_xoffs = DoubleToFixed(udmf_sectors[i].xpanningfloor);
        sectors[i].floor_yoffs = DoubleToFixed(udmf_sectors[i].ypanningfloor);
        sectors[i].ceiling_xoffs = DoubleToFixed(udmf_sectors[i].xpanningceiling);
        sectors[i].ceiling_yoffs = DoubleToFixed(udmf_sectors[i].ypanningceiling);

        P_SectorInit(&sectors[i]);

        sectors[i].colormap = R_ColormapNumForName(udmf_sectors[i].colormap);
        sectors[i].tint = R_ColormapNumForName(udmf_sectors[i].tint);
        sectors[i].tintceiling = R_ColormapNumForName(udmf_sectors[i].tintceiling);
        sectors[i].tintfloor = R_ColormapNumForName(udmf_sectors[i].tintfloor);

        if (udmf_sectors[i].scroll_floor_type && (udmf_sectors[i].scroll_floor_x || udmf_sectors[i].scroll_floor_y))
        {
            Add_EESectorScroller(udmf_sectors[i].scroll_floor_type, i, false,
                                 udmf_sectors[i].scroll_floor_x,
                                 udmf_sectors[i].scroll_floor_y);
        }

        if (udmf_sectors[i].scroll_ceil_type && (udmf_sectors[i].scroll_ceil_x || udmf_sectors[i].scroll_ceil_y))
        {
            Add_EESectorScroller(udmf_sectors[i].scroll_floor_type, i, true,
                                 udmf_sectors[i].scroll_floor_x,
                                 udmf_sectors[i].scroll_floor_y);
        }

        if (udmf_sectors[i].scrollfloormode && (udmf_sectors[i].xscrollfloor || udmf_sectors[i].yscrollfloor))
        {
            Add_ParamSectorScroller(
                udmf_sectors[i].scrollfloormode, i, false,
                DoubleToFixed(udmf_sectors[i].xscrollfloor),
                DoubleToFixed(udmf_sectors[i].yscrollfloor));
        }

        if (udmf_sectors[i].scrollceilingmode && (udmf_sectors[i].xscrollceiling || udmf_sectors[i].yscrollceiling))
        {
            Add_ParamSectorScroller(
                udmf_sectors[i].scrollceilingmode, i, true,
                DoubleToFixed(udmf_sectors[i].xscrollceiling),
                DoubleToFixed(udmf_sectors[i].yscrollceiling));
        }
    }
}

static void UDMF_LoadSideDefs(void)
{
    numsides = array_size(udmf_sidedefs);
    sides = arena_alloc_num(world_arena, side_t, numsides);

    for (int i = 0; i < numsides; i++)
    {
        sides[i].sector = &sectors[udmf_sidedefs[i].sector_id];
        sides[i].textureoffset = IntToFixed(udmf_sidedefs[i].offsetx);
        sides[i].rowoffset = IntToFixed(udmf_sidedefs[i].offsety);

        sides[i].offsetx_top = DoubleToFixed(udmf_sidedefs[i].offsetx_top);
        sides[i].offsety_top = DoubleToFixed(udmf_sidedefs[i].offsety_top);
        sides[i].offsetx_mid = DoubleToFixed(udmf_sidedefs[i].offsetx_mid);
        sides[i].offsety_mid = DoubleToFixed(udmf_sidedefs[i].offsety_mid);
        sides[i].offsetx_bottom = DoubleToFixed(udmf_sidedefs[i].offsetx_bottom);
        sides[i].offsety_bottom = DoubleToFixed(udmf_sidedefs[i].offsety_bottom);

        P_SidedefInit(&sides[i]);

        sides[i].flags = udmf_sidedefs[i].flags;
        sides[i].light = udmf_sidedefs[i].light;
        sides[i].light_top = udmf_sidedefs[i].light_top;
        sides[i].light_mid = udmf_sidedefs[i].light_mid;
        sides[i].light_bottom = udmf_sidedefs[i].light_bottom;

        sides[i].tint = R_ColormapNumForName(udmf_sidedefs[i].tint);

        if (udmf_sidedefs[i].xscroll || udmf_sidedefs[i].yscroll)
        {
            Add_ScrollerStatic(sc_side, i,
                               DoubleToFixed(udmf_sidedefs[i].xscroll),
                               DoubleToFixed(udmf_sidedefs[i].yscroll));
        }

        if (udmf_sidedefs[i].xscrolltop || udmf_sidedefs[i].yscrolltop)
        {
            Add_ScrollerStatic(sc_side_top, i,
                               DoubleToFixed(udmf_sidedefs[i].xscrolltop),
                               DoubleToFixed(udmf_sidedefs[i].yscrolltop));
        }

        if (udmf_sidedefs[i].xscrollmid || udmf_sidedefs[i].yscrollmid)
        {
            Add_ScrollerStatic(sc_side_mid, i,
                               DoubleToFixed(udmf_sidedefs[i].xscrollmid),
                               DoubleToFixed(udmf_sidedefs[i].yscrollmid));
        }

        if (udmf_sidedefs[i].xscrollbottom || udmf_sidedefs[i].yscrollbottom)
        {
            Add_ScrollerStatic(sc_side_bottom, i,
                               DoubleToFixed(udmf_sidedefs[i].xscrollbottom),
                               DoubleToFixed(udmf_sidedefs[i].yscrollbottom));
        }
    }
}

static void UDMF_LoadLineDefs(void)
{
    numlines = array_size(udmf_linedefs);
    lines = arena_alloc_num(world_arena, line_t, numlines);

    for (int i = 0; i < numlines; i++)
    {
        lines[i].v1 = &vertexes[udmf_linedefs[i].v1_id];
        lines[i].v2 = &vertexes[udmf_linedefs[i].v2_id];
        lines[i].sidenum[0] = udmf_linedefs[i].sidefront;
        lines[i].sidenum[1] = udmf_linedefs[i].sideback;

        lines[i].flags = udmf_linedefs[i].flags;
        lines[i].special = udmf_linedefs[i].special;
        lines[i].id = udmf_linedefs[i].id;
        lines[i].args[0] = udmf_linedefs[i].args[0];
        lines[i].args[1] = udmf_linedefs[i].args[1];
        lines[i].args[2] = udmf_linedefs[i].args[2];
        lines[i].args[3] = udmf_linedefs[i].args[3];
        lines[i].args[4] = udmf_linedefs[i].args[4];

        // Custom automap line style
        lines[i].amls = udmf_linedefs[i].amls;
        if (lines[i].amls < amls_Default || lines[i].amls >= AMLS_COUNT)
        {
            lines[i].amls = amls_Default;
        }

        // Woof! currently does not support parameterized line specials
        if (udmf_flags & UDMF_LINE_PARAM)
        {
            udmf_linedefs[i].special = 0;
        }

        // Support for namespaces that do not make the tag -> arg0/id split
        if (udmf_flags & UDMF_COMP_NO_ARG0)
        {
            lines[i].args[0] = lines[i].id;
        }

        P_LinedefInit(&lines[i]);

        // Translucency and special effects support
        int32_t lump = W_CheckNumForName(udmf_linedefs[i].tranmap);

        if (lump == NO_INDEX && udmf_linedefs[i].alpha < 1.0)
        {
            const int32_t alpha = (int32_t)floor(udmf_linedefs[i].alpha * 100.0);
            lines[i].tranmap = GetNormalTranMap(alpha);
        }

        if (lump != NO_INDEX && W_LumpLength(lump) == tranmap_lump_length)
        {
            lines[i].tranmap = W_CacheLumpNum(lump, PU_CACHE);
        }

        // killough 11/98: fix common wad errors (missing sidedefs):
        // Substitute dummy sidedef for missing right side
        if (lines[i].sidenum[0] == NO_INDEX)
        {
            lines[i].sidenum[0] = 0;
        }

        // Clear 2s flag for missing left side
        if (lines[i].sidenum[1] == NO_INDEX && !demo_compatibility)
        {
            lines[i].flags &= ~ML_TWOSIDED;
        }

        if (lines[i].sidenum[0] != NO_INDEX)
        {
            side_t *frontside = &sides[lines[i].sidenum[0]];
            lines[i].frontsector = frontside->sector;
            frontside->special = lines[i].special;
        }

        if (lines[i].sidenum[1] != NO_INDEX)
        {
            side_t *backside = &sides[lines[i].sidenum[1]];
            lines[i].backsector = backside->sector;
        }
    }
}

static void UDMF_LoadSideDefs_Post(void)
{
    for (int i = 0; i < numsides; i++)
    {
        P_ProcessSideDefs(&sides[i], i, udmf_sidedefs[i].texturebottom,
                          udmf_sidedefs[i].texturemiddle,
                          udmf_sidedefs[i].texturetop);
    }
}

static void UDMF_LoadLineDefs_Post(void)
{
    for (int i = 0; i < numlines; i++)
    {
        // killough 4/11/98: handle special types
        switch (lines[i].special)
        {
            // killough 4/11/98: translucent 2s textures
            case 260:
            {
                // translucency from sidedef
                int32_t lump = sides[*lines[i].sidenum].special;
                const byte *tranmap =
                    !lump ? main_tranmap : W_CacheLumpNum(lump - 1, PU_STATIC);
                if (!lines[i].args[0])
                {
                    // if tag==0, affect this linedef only
                    lines[i].tranmap = tranmap;
                }
                else
                {
                    for (int j = 0; j < numlines; j++)
                    {
                        if (lines[j].id == lines[i].args[0])
                        {
                            // if tag!=0, affect all matching linedefs
                            lines[i].tranmap = tranmap;
                        }
                    }
                }
                break;
            }
        }
    }
}

void P_LoadThings_UDMF(void)
{
    for (int i = 0; i < array_size(udmf_things); i++)
    {
        // Do not spawn cool, new monsters if !commercial
        if (gamemode != commercial)
        {
            switch (udmf_things[i].type)
            {
                case 68: // Arachnotron
                case 64: // Archvile
                case 88: // Boss Brain
                case 89: // Boss Shooter
                case 69: // Hell Knight
                case 67: // Mancubus
                case 71: // Pain Elemental
                case 65: // Former Human Commando
                case 66: // Revenant
                case 84: // Wolf SS
                    continue;
            }
        }

        // Do spawn all other stuff.

        mapthing_t mt = {0};
        mt.x = DoubleToFixed(udmf_things[i].x);
        mt.y = DoubleToFixed(udmf_things[i].y);
        mt.height = DoubleToFixed(udmf_things[i].height);
        mt.angle = CLAMP(udmf_things[i].angle, 0, 360);
        mt.type = udmf_things[i].type;
        mt.options = udmf_things[i].options;

        mt.tid = udmf_things[i].tid;
        mt.special = udmf_things[i].special;
        mt.args[0] = udmf_things[i].args[0];
        mt.args[1] = udmf_things[i].args[1];
        mt.args[2] = udmf_things[i].args[2];
        mt.args[3] = udmf_things[i].args[3];
        mt.args[4] = udmf_things[i].args[4];

        mt.health = DoubleToFixed(udmf_things[i].health);
        mt.tint = R_ColormapNumForName(udmf_things[i].tint);

        // Translucency and special effects support
        int32_t lump = W_CheckNumForName(udmf_things[i].tranmap);

        if (lump == NO_INDEX && udmf_things[i].alpha < 1.0)
        {
            const int32_t alpha = (int32_t)floor(udmf_things[i].alpha * 100.0);
            mt.tranmap = GetNormalTranMap(alpha);
        }

        if (lump != NO_INDEX && W_LumpLength(lump) == tranmap_lump_length)
        {
            mt.tranmap = W_CacheLumpNum(lump, PU_CACHE);
        }


        P_SpawnMapThing(&mt);
    }
}

void UDMF_LoadMap(map_t *map)
{
    if (map->znodes < 0)
    {
        I_Error("Could not find ZNODES lump for UDMF map: %s.",
                lumpinfo[map->label].name);
    }

    if (map->bsp_format == BSP_NANO)
    {
        I_Error("Invalid format found on ZNODES lump for UDMF map: %s",
                lumpinfo[map->label].name);
    }

    // Clear everything
    UDMF_ClearMemory();

    UDMF_ParseTextMap(map);

    // note: most of this ordering is important
    UDMF_LoadVertexes();
    UDMF_LoadSectors();
    UDMF_LoadSideDefs();      // <- This needs Sectors
    UDMF_LoadLineDefs();      // <- this needs Sides
    UDMF_LoadSideDefs_Post(); // <- this needs side_t::special
    UDMF_LoadLineDefs_Post(); // <- this needs Sides Post Processing

    map->bmap_format = P_LoadBlockMap(map->blockmap);
    P_LoadBSPTree_ZDBSP(map->znodes, map->bsp_format);
    map->reject_built = P_LoadReject(map->reject, P_GroupLines());
}
