#include "MenuWriterIW4.h"

#include "Game/IW4/MenuConstantsIW4.h"
#include "Menu/AbstractMenuWriter.h"
#include "ObjWriting.h"

#include <cassert>
#include <cmath>
#include <limits>
#include <sstream>

using namespace IW4;

namespace
{
    // Set this to true to skip interpretative expression dumping
    constexpr auto DUMP_NAIVE = false;

    size_t FindStatementClosingParenthesis(const Statement_s& statement, const size_t openingParenthesisPosition)
    {
        assert(statement.numEntries >= 0);
        assert(openingParenthesisPosition < static_cast<size_t>(statement.numEntries));

        const auto statementEnd = static_cast<size_t>(statement.numEntries);

        // The openingParenthesisPosition does not necessarily point to an actual opening parenthesis operator. That's fine though.
        // We will pretend it does since the game does sometimes leave out opening parenthesis from the entries.
        auto currentParenthesisDepth = 1;
        for (auto currentSearchPosition = openingParenthesisPosition + 1; currentSearchPosition < statementEnd; currentSearchPosition++)
        {
            const auto& expEntry = statement.entries[currentSearchPosition];
            if (expEntry.type != EET_OPERATOR)
                continue;

            // Any function means a "left out" left paren
            if (expEntry.data.op == OP_LEFTPAREN || expEntry.data.op >= OP_COUNT)
            {
                currentParenthesisDepth++;
            }
            else if (expEntry.data.op == OP_RIGHTPAREN)
            {
                if (currentParenthesisDepth > 0)
                    currentParenthesisDepth--;
                if (currentParenthesisDepth == 0)
                    return currentSearchPosition;
            }
        }

        return statementEnd;
    }

    class MenuWriter final : public menu::AbstractBaseWriter, public menu::IWriterIW4
    {
    public:
        explicit MenuWriter(std::ostream& stream)
            : AbstractBaseWriter(stream)
        {
        }

        void WriteFunctionDef(const std::string& functionName, const Statement_s* statement) override
        {
            StartFunctionDefScope();

            WriteStringProperty("name", functionName);
            WriteStatementProperty("value", statement, false);

            EndScope();
        }

        void WriteMenu(const menuDef_t& menu) override
        {
            StartMenuDefScope();
            WriteMenuData(menu);
            EndScope();
        }

        void Start() override
        {
            AbstractBaseWriter::Start();
        }

        void End() override
        {
            AbstractBaseWriter::End();
        }

        void IncludeMenu(const std::string& menuPath) const override
        {
            AbstractBaseWriter::IncludeMenu(menuPath);
        }

    private:
        static bool HasStatement(const Statement_s* statement)
        {
            return statement && statement->numEntries > 0 && statement->entries;
        }

        void WriteStatementNaive(const Statement_s& statement) const
        {
            const auto entryCount = static_cast<size_t>(statement.numEntries);

            const auto missingClosingParenthesis = statement.numEntries > 0 && statement.entries[0].type == EET_OPERATOR
                                                   && statement.entries[0].data.op == OP_LEFTPAREN
                                                   && FindStatementClosingParenthesis(statement, 0) >= static_cast<size_t>(statement.numEntries);

            for (auto i = 0uz; i < entryCount; i++)
            {
                const auto& entry = statement.entries[i];
                if (entry.type == EET_OPERAND)
                {
                    size_t pos = i;
                    bool discard = false;
                    WriteStatementOperand(statement, pos, discard);
                }
                else if (entry.data.op >= EXP_FUNC_STATIC_DVAR_INT && entry.data.op <= EXP_FUNC_STATIC_DVAR_STRING)
                {
                    switch (entry.data.op)
                    {
                    case EXP_FUNC_STATIC_DVAR_INT:
                        m_stream << "dvarint";
                        break;

                    case EXP_FUNC_STATIC_DVAR_BOOL:
                        m_stream << "dvarbool";
                        break;

                    case EXP_FUNC_STATIC_DVAR_FLOAT:
                        m_stream << "dvarfloat";
                        break;

                    case EXP_FUNC_STATIC_DVAR_STRING:
                        m_stream << "dvarstring";
                        break;

                    default:
                        break;
                    }

                    // Functions do not have opening parenthesis in the entries. We can just pretend they do though
                    const auto closingParenPos = FindStatementClosingParenthesis(statement, i);
                    m_stream << "(";

                    if (closingParenPos - i + 1u >= 1u)
                    {
                        const auto& staticDvarEntry = statement.entries[i + 1];
                        if (staticDvarEntry.type == EET_OPERAND && staticDvarEntry.data.operand.dataType == VAL_INT)
                        {
                            if (statement.supportingData && statement.supportingData->staticDvarList.staticDvars
                                && staticDvarEntry.data.operand.internals.intVal >= 0
                                && staticDvarEntry.data.operand.internals.intVal < statement.supportingData->staticDvarList.numStaticDvars)
                            {
                                const auto* staticDvar = statement.supportingData->staticDvarList.staticDvars[staticDvarEntry.data.operand.internals.intVal];
                                if (staticDvar && staticDvar->dvarName)
                                    m_stream << staticDvar->dvarName;
                            }
                            else
                            {
                                m_stream << "#INVALID_DVAR_INDEX";
                            }
                        }
                        else
                        {
                            m_stream << "#INVALID_DVAR_OPERAND";
                        }
                    }

                    m_stream << ")";
                    i = closingParenPos;
                }
                else
                {
                    assert(entry.data.op >= 0 && static_cast<unsigned>(entry.data.op) < std::extent_v<decltype(g_expFunctionNames)>);
                    if (entry.data.op >= 0 && static_cast<unsigned>(entry.data.op) < std::extent_v<decltype(g_expFunctionNames)>)
                        m_stream << g_expFunctionNames[entry.data.op];
                    if (entry.data.op >= OP_COUNT)
                        m_stream << "(";
                }
            }

            if (missingClosingParenthesis)
                m_stream << ")";
        }

        void WriteStatementOperator(const Statement_s& statement, size_t& currentPos, bool& spaceNext) const
        {
            const auto& expEntry = statement.entries[currentPos];

            if (spaceNext && expEntry.data.op != OP_COMMA)
                m_stream << " ";

            if (expEntry.data.op == OP_LEFTPAREN)
            {
                const auto closingParenPos = FindStatementClosingParenthesis(statement, currentPos);
                m_stream << "(";
                WriteStatementEntryRange(statement, currentPos + 1, closingParenPos);
                m_stream << ")";

                currentPos = closingParenPos + 1;
                spaceNext = true;
            }
            else if (expEntry.data.op >= EXP_FUNC_STATIC_DVAR_INT && expEntry.data.op <= EXP_FUNC_STATIC_DVAR_STRING)
            {
                switch (expEntry.data.op)
                {
                case EXP_FUNC_STATIC_DVAR_INT:
                    m_stream << "dvarint";
                    break;

                case EXP_FUNC_STATIC_DVAR_BOOL:
                    m_stream << "dvarbool";
                    break;

                case EXP_FUNC_STATIC_DVAR_FLOAT:
                    m_stream << "dvarfloat";
                    break;

                case EXP_FUNC_STATIC_DVAR_STRING:
                    m_stream << "dvarstring";
                    break;

                default:
                    break;
                }

                // Functions do not have opening parenthesis in the entries. We can just pretend they do though
                const auto closingParenPos = FindStatementClosingParenthesis(statement, currentPos);
                m_stream << "(";

                if (closingParenPos - currentPos + 1 >= 1)
                {
                    const auto& staticDvarEntry = statement.entries[currentPos + 1];
                    if (staticDvarEntry.type == EET_OPERAND && staticDvarEntry.data.operand.dataType == VAL_INT)
                    {
                        if (statement.supportingData && statement.supportingData->staticDvarList.staticDvars
                            && staticDvarEntry.data.operand.internals.intVal >= 0
                            && staticDvarEntry.data.operand.internals.intVal < statement.supportingData->staticDvarList.numStaticDvars)
                        {
                            const auto* staticDvar = statement.supportingData->staticDvarList.staticDvars[staticDvarEntry.data.operand.internals.intVal];
                            if (staticDvar && staticDvar->dvarName)
                                m_stream << staticDvar->dvarName;
                        }
                        else
                        {
                            m_stream << "#INVALID_DVAR_INDEX";
                        }
                    }
                    else
                    {
                        m_stream << "#INVALID_DVAR_OPERAND";
                    }
                }

                m_stream << ")";
                currentPos = closingParenPos + 1;
                spaceNext = true;
            }
            else
            {
                if (expEntry.data.op >= 0 && static_cast<unsigned>(expEntry.data.op) < std::extent_v<decltype(g_expFunctionNames)>)
                    m_stream << g_expFunctionNames[expEntry.data.op];

                if (expEntry.data.op >= OP_COUNT)
                {
                    // Functions do not have opening parenthesis in the entries. We can just pretend they do though
                    const auto closingParenPos = FindStatementClosingParenthesis(statement, currentPos);
                    m_stream << "(";
                    WriteStatementEntryRange(statement, currentPos + 1, closingParenPos);
                    m_stream << ")";
                    currentPos = closingParenPos + 1;
                }
                else
                    currentPos++;

                spaceNext = expEntry.data.op != OP_NOT;
            }
        }

        void WriteStatementOperandFunction(const Statement_s& statement, const size_t currentPos) const
        {
            const auto& operand = statement.entries[currentPos].data.operand;

            if (!operand.internals.function)
                return;

            if (!ObjWriting::Configuration.MenuLegacyMode)
            {
                int functionIndex = -1;
                if (statement.supportingData && statement.supportingData->uifunctions.functions)
                {
                    for (auto supportingFunctionIndex = 0; supportingFunctionIndex < statement.supportingData->uifunctions.totalFunctions;
                         supportingFunctionIndex++)
                    {
                        if (statement.supportingData->uifunctions.functions[supportingFunctionIndex] == operand.internals.function)
                        {
                            functionIndex = supportingFunctionIndex;
                            break;
                        }
                    }
                }

                if (functionIndex >= 0)
                    m_stream << "FUNC_" << functionIndex;
                else
                    m_stream << "INVALID_FUNC";
                m_stream << "()";
            }
            else
            {
                m_stream << "(";
                WriteStatementSkipInitialUnnecessaryParenthesis(*operand.internals.function);
                m_stream << ")";
            }
        }

        void WriteStatementOperand(const Statement_s& statement, size_t& currentPos, bool& spaceNext) const
        {
            const auto& expEntry = statement.entries[currentPos];

            if (spaceNext)
                m_stream << " ";

            const auto& operand = expEntry.data.operand;

            switch (operand.dataType)
            {
            case VAL_FLOAT:
                m_stream << operand.internals.floatVal;
                break;

            case VAL_INT:
                m_stream << operand.internals.intVal;
                break;

            case VAL_STRING:
                WriteEscapedString(operand.internals.stringVal.string);
                break;

            case VAL_FUNCTION:
                WriteStatementOperandFunction(statement, currentPos);
                break;

            default:
                break;
            }

            currentPos++;
            spaceNext = true;
        }

        void WriteStatementEntryRange(const Statement_s& statement, const size_t startOffset, const size_t endOffset) const
        {
            assert(startOffset <= endOffset);
            assert(endOffset <= static_cast<size_t>(statement.numEntries));

            auto currentPos = startOffset;
            auto spaceNext = false;
            while (currentPos < endOffset)
            {
                const auto& expEntry = statement.entries[currentPos];

                if (expEntry.type == EET_OPERATOR)
                {
                    WriteStatementOperator(statement, currentPos, spaceNext);
                }
                else
                {
                    WriteStatementOperand(statement, currentPos, spaceNext);
                }
            }
        }

        void WriteStatement(const Statement_s& statement) const
        {
            if (!statement.entries || statement.numEntries < 0)
                return;

            WriteStatementEntryRange(statement, 0, static_cast<size_t>(statement.numEntries));
        }

        void WriteStatementSkipInitialUnnecessaryParenthesis(const Statement_s& statement) const
        {
            if (!statement.entries || statement.numEntries < 0)
                return;

            const auto statementEnd = static_cast<size_t>(statement.numEntries);

            if (statement.numEntries >= 1 && statement.entries[0].type == EET_OPERATOR && statement.entries[0].data.op == OP_LEFTPAREN)
            {
                const auto parenthesisEnd = FindStatementClosingParenthesis(statement, 0);

                if (parenthesisEnd >= statementEnd)
                    WriteStatementEntryRange(statement, 1, statementEnd);
                else if (parenthesisEnd == statementEnd - 1)
                    WriteStatementEntryRange(statement, 1, statementEnd - 1);
                else
                    WriteStatementEntryRange(statement, 0, statementEnd);
            }
            else
            {
                WriteStatementEntryRange(statement, 0, statementEnd);
            }
        }

        void WriteStatementProperty(const std::string& propertyKey, const Statement_s* statement, const bool isBooleanStatement) const
        {
            if (!HasStatement(statement))
                return;

            Indent();
            WriteKey(propertyKey);

            if (isBooleanStatement)
            {
                m_stream << "when(";
                if constexpr (DUMP_NAIVE)
                    WriteStatementNaive(*statement);
                else
                    WriteStatementSkipInitialUnnecessaryParenthesis(*statement);
                m_stream << ");\n";
            }
            else
            {
                if constexpr (DUMP_NAIVE)
                    WriteStatementNaive(*statement);
                else
                    WriteStatement(*statement);
                m_stream << ";\n";
            }
        }

        void WriteSetLocalVarData(const std::string& setFunction, const SetLocalVarData* setLocalVarData) const
        {
            if (!setLocalVarData || !setLocalVarData->expression)
                return;

            Indent();
            m_stream << setFunction << " " << setLocalVarData->localVarName << " ";
            WriteStatement(*setLocalVarData->expression);
            m_stream << ";\n";
        }

        // #define WRITE_ORIGINAL_SCRIPT
        void WriteUnconditionalScript(const char* script) const
        {
#ifdef WRITE_ORIGINAL_SCRIPT
            Indent();
            m_stream << script << "\n";
            return;
#endif

            const auto tokenList = CreateScriptTokenList(script);

            auto isNewStatement = true;
            for (const auto& token : tokenList)
            {
                if (isNewStatement)
                {
                    if (token == ";")
                        continue;
                    Indent();
                }

                if (token == ";")
                {
                    m_stream << ";\n";
                    isNewStatement = true;
                    continue;
                }

                if (!isNewStatement)
                    m_stream << " ";
                else
                    isNewStatement = false;

                if (DoesTokenNeedQuotationMarks(token))
                    WriteEscapedString(token);
                else
                    m_stream << token;
            }

            if (!isNewStatement)
                m_stream << ";\n";
        }

        void WriteMenuEventHandlerSet(const MenuEventHandlerSet* eventHandlerSet)
        {
            Indent();
            m_stream << "{\n";
            IncIndent();

            for (auto eventHandlerIndex = 0; eventHandlerIndex < eventHandlerSet->eventHandlerCount; eventHandlerIndex++)
            {
                const auto* eventHandler = eventHandlerSet->eventHandlers[eventHandlerIndex];
                if (!eventHandler)
                    continue;

                switch (eventHandler->eventType)
                {
                case EVENT_UNCONDITIONAL:
                    WriteUnconditionalScript(eventHandler->eventData.unconditionalScript);
                    break;

                case EVENT_IF:
                    if (!eventHandler->eventData.conditionalScript || !eventHandler->eventData.conditionalScript->eventExpression
                        || !eventHandler->eventData.conditionalScript->eventHandlerSet)
                    {
                        continue;
                    }

                    Indent();
                    m_stream << "if (";
                    WriteStatementSkipInitialUnnecessaryParenthesis(*eventHandler->eventData.conditionalScript->eventExpression);
                    m_stream << ")\n";
                    WriteMenuEventHandlerSet(eventHandler->eventData.conditionalScript->eventHandlerSet);
                    break;

                case EVENT_ELSE:
                    if (!eventHandler->eventData.elseScript)
                        continue;

                    Indent();
                    m_stream << "else\n";
                    WriteMenuEventHandlerSet(eventHandler->eventData.elseScript);
                    break;

                case EVENT_SET_LOCAL_VAR_BOOL:
                    WriteSetLocalVarData("setLocalVarBool", eventHandler->eventData.setLocalVarData);
                    break;

                case EVENT_SET_LOCAL_VAR_INT:
                    WriteSetLocalVarData("setLocalVarInt", eventHandler->eventData.setLocalVarData);
                    break;

                case EVENT_SET_LOCAL_VAR_FLOAT:
                    WriteSetLocalVarData("setLocalVarFloat", eventHandler->eventData.setLocalVarData);
                    break;

                case EVENT_SET_LOCAL_VAR_STRING:
                    WriteSetLocalVarData("setLocalVarString", eventHandler->eventData.setLocalVarData);
                    break;

                default:
                    break;
                }
            }

            DecIndent();
            Indent();
            m_stream << "}\n";
        }

        void WriteMenuEventHandlerSetProperty(const std::string& propertyKey, const MenuEventHandlerSet* eventHandlerSetValue)
        {
            if (!eventHandlerSetValue)
                return;

            Indent();
            m_stream << propertyKey << "\n";
            WriteMenuEventHandlerSet(eventHandlerSetValue);
        }

        void WriteRectProperty(const std::string& propertyKey, const rectDef_s& rect) const
        {
            Indent();
            WriteKey(propertyKey);
            m_stream << rect.x << " " << rect.y << " " << rect.w << " " << rect.h << " " << static_cast<int>(rect.horzAlign) << " "
                     << static_cast<int>(rect.vertAlign) << "\n";
        }

        void WriteMaterialProperty(const std::string& propertyKey, const Material* material) const
        {
            if (!material || !material->info.name)
                return;

            const auto* materialName = material->info.name;
            if (materialName[0] == ',')
                materialName++;
            WriteStringProperty(propertyKey, materialName);
        }

        void WriteSoundAliasProperty(const std::string& propertyKey, const snd_alias_list_t* soundAlias) const
        {
            if (soundAlias)
                WriteStringProperty(propertyKey, soundAlias->aliasName);
        }

        void WriteDecodeEffectProperty(const std::string& propertyKey, const itemDef_s& item) const
        {
            if (!item.decayActive)
                return;

            Indent();
            WriteKey(propertyKey);
            m_stream << item.fxLetterTime << " " << item.fxDecayStartTime << " " << item.fxDecayDuration << "\n";
        }

        void WriteItemKeyHandlers(const ItemKeyHandler* handler)
        {
            for (const auto* current = handler; current; current = current->next)
            {
                std::string key;
                if (current->key >= '!' && current->key <= '~' && current->key != '"')
                    key = std::format("execKey \"{}\"", static_cast<char>(current->key));
                else
                    key = std::format("execKeyInt {}", current->key);

                WriteMenuEventHandlerSetProperty(key, current->action);
            }
        }

        void WriteMultiTokenStringProperty(const std::string& propertyKey, const char* value) const
        {
            if (!value)
                return;

            Indent();
            WriteKey(propertyKey);
            m_stream << "{ ";

            const auto tokenList = CreateScriptTokenList(value);
            auto firstToken = true;
            for (const auto& token : tokenList)
            {
                if (firstToken)
                    firstToken = false;
                else
                    m_stream << ";";

                WriteEscapedString(token);
            }
            if (!firstToken)
                m_stream << " ";
            m_stream << "}\n";
        }

        void WriteFloatExpressionsProperty(const ItemFloatExpression* floatExpressions, int floatExpressionCount) const
        {
            if (!floatExpressions)
                return;

            for (int i = 0; i < floatExpressionCount; i++)
            {
                const auto& floatExpression = floatExpressions[i];

                if (floatExpression.target < 0 || floatExpression.target >= ITEM_FLOATEXP_TGT_COUNT)
                    continue;

                std::string propertyName = std::string("exp ") + floatExpressionTargetBindings[floatExpression.target].name + std::string(" ")
                                           + floatExpressionTargetBindings[floatExpression.target].componentName;

                WriteStatementProperty(propertyName, floatExpression.expression, false);
            }
        }

        void WriteColumnProperty(const listBoxDef_s& listBox) const
        {
            if (listBox.numColumns <= 0)
                return;

            Indent();
            WriteKey("columns");
            m_stream << listBox.numColumns << "\n";

            const auto columnCount = std::min<size_t>(listBox.numColumns, std::size(listBox.columnInfo));
            for (size_t columnIndex = 0u; columnIndex < columnCount; columnIndex++)
            {
                const auto& column = listBox.columnInfo[columnIndex];

                Indent();
                for (auto i = 0u; i < MENU_KEY_SPACING; i++)
                    m_stream << " ";

                m_stream << column.pos << " " << column.width << " " << column.maxChars << " " << column.alignment << "\n";
            }
        }

        void WriteListBoxProperties(const itemDef_s& item)
        {
            if (item.type != ITEM_TYPE_LISTBOX || !item.typeData.listBox)
                return;

            const auto& listBox = *item.typeData.listBox;
            WriteKeywordProperty("notselectable", listBox.notselectable != 0);
            WriteKeywordProperty("noscrollbars", listBox.noScrollBars != 0);
            WriteKeywordProperty("usepaging", listBox.usePaging != 0);
            WriteFloatProperty("elementwidth", listBox.elementWidth, 0.0f);
            WriteFloatProperty("elementheight", listBox.elementHeight, 0.0f);
            WriteFloatProperty("feeder", item.special, 0.0f);
            WriteIntProperty("elementtype", listBox.elementStyle, 0);
            WriteColumnProperty(listBox);
            WriteMenuEventHandlerSetProperty("doubleclick", listBox.onDoubleClick);
            WriteColorProperty("selectBorder", listBox.selectBorder, COLOR_0000);
            WriteMaterialProperty("selectIcon", listBox.selectIcon);
        }

        void WriteDvarFloatProperty(const itemDef_s& item, const editFieldDef_s& editField) const
        {
            if (!item.dvar)
                return;

            Indent();
            WriteKey("dvarFloat");
            WriteEscapedString(item.dvar);
            m_stream << " " << editField.defVal << " " << editField.minVal << " " << editField.maxVal << "\n";
        }

        void WriteEditFieldProperties(const itemDef_s& item) const
        {
            switch (item.type)
            {
            case ITEM_TYPE_TEXT:
            case ITEM_TYPE_EDITFIELD:
            case ITEM_TYPE_NUMERICFIELD:
            case ITEM_TYPE_SLIDER:
            case ITEM_TYPE_YESNO:
            case ITEM_TYPE_BIND:
            case ITEM_TYPE_VALIDFILEFIELD:
            case ITEM_TYPE_DECIMALFIELD:
            case ITEM_TYPE_UPREDITFIELD:
            case ITEM_TYPE_EMAILFIELD:
            case ITEM_TYPE_PASSWORDFIELD:
                break;

            default:
                return;
            }

            if (!item.typeData.editField)
                return;

            const auto& editField = *item.typeData.editField;
            if (std::fabs(-1.0f - editField.defVal) >= std::numeric_limits<float>::epsilon()
                || std::fabs(-1.0f - editField.minVal) >= std::numeric_limits<float>::epsilon()
                || std::fabs(-1.0f - editField.maxVal) >= std::numeric_limits<float>::epsilon())
            {
                WriteDvarFloatProperty(item, editField);
            }
            else
            {
                WriteStringProperty("dvar", item.dvar);
            }
            WriteStringProperty("localvar", item.localVar);
            WriteIntProperty("maxChars", editField.maxChars, 0);
            WriteKeywordProperty("maxCharsGotoNext", editField.maxCharsGotoNext != 0);
            WriteIntProperty("maxPaintChars", editField.maxPaintChars, 0);
        }

        void WriteMultiValueProperty(const multiDef_s& multi) const
        {
            if (multi.count <= 0)
                return;

            Indent();
            WriteKey(multi.strDef ? "dvarStrList" : "dvarFloatList");
            m_stream << "{";
            const auto valueCount = std::min<size_t>(multi.count, std::size(multi.dvarValue));
            for (size_t valueIndex = 0u; valueIndex < valueCount; valueIndex++)
            {
                if (!multi.dvarList[valueIndex] || (multi.strDef && !multi.dvarStr[valueIndex]))
                    continue;

                m_stream << " ";
                WriteEscapedString(multi.dvarList[valueIndex]);
                m_stream << " ";
                if (multi.strDef)
                    WriteEscapedString(multi.dvarStr[valueIndex]);
                else
                    m_stream << multi.dvarValue[valueIndex];
            }
            m_stream << " }\n";
        }

        void WriteMultiProperties(const itemDef_s& item) const
        {
            if (item.type != ITEM_TYPE_MULTI || !item.typeData.multi)
                return;

            WriteStringProperty("dvar", item.dvar);
            WriteStringProperty("localvar", item.localVar);
            WriteMultiValueProperty(*item.typeData.multi);
        }

        void WriteEnumDvarProperties(const itemDef_s& item) const
        {
            if (item.type != ITEM_TYPE_DVARENUM)
                return;

            WriteStringProperty("dvar", item.dvar);
            WriteStringProperty("localvar", item.localVar);
            WriteStringProperty("dvarEnumList", item.typeData.enumDvarName);
        }

        void WriteTickerProperties(const itemDef_s& item) const
        {
            if (item.type != ITEM_TYPE_NEWS_TICKER || item.typeData.ticker == nullptr)
                return;

            const auto* newsTickerDef = item.typeData.ticker;
            WriteIntProperty("spacing", newsTickerDef->spacing, 0);
            WriteIntProperty("speed", newsTickerDef->speed, 0);
            WriteIntProperty("newsfeed", newsTickerDef->feedId, 0);
        }

        void WriteItemTextProperty(const char* text) const
        {
            // This distinguishes explicitly empty text from null text, which falls back to the item's dvar.
            if (!text)
                return;

            Indent();
            WriteKey("text");
            WriteEscapedString(text);
            m_stream << "\n";
        }

        void WriteItemData(const itemDef_s& item)
        {
            WriteStringProperty("name", item.window.name);
            WriteItemTextProperty(item.text);
            WriteKeywordProperty("textsavegame", item.itemFlags & ITEM_FLAG_SAVE_GAME_INFO);
            WriteKeywordProperty("textcinematicsubtitle", item.itemFlags & ITEM_FLAG_CINEMATIC_SUBTITLE);
            WriteStringProperty("group", item.window.group);
            WriteRectProperty("rect", item.window.rectClient);
            WriteIntProperty("style", item.window.style, 0);
            WriteKeywordProperty("decoration", item.window.staticFlags & WINDOW_FLAG_DECORATION);
            WriteKeywordProperty("autowrapped", item.window.staticFlags & WINDOW_FLAG_AUTO_WRAPPED);
            WriteKeywordProperty("horizontalscroll", item.window.staticFlags & WINDOW_FLAG_HORIZONTAL_SCROLL);
            WriteIntProperty("type", item.type, ITEM_TYPE_TEXT);
            WriteIntProperty("border", item.window.border, 0);
            WriteFloatProperty("borderSize", item.window.borderSize, 0.0f);

            if (HasStatement(item.visibleExp))
                WriteStatementProperty("visible", item.visibleExp, true);
            else if (item.window.dynamicFlags[0] & WINDOW_FLAG_VISIBLE)
                WriteIntProperty("visible", 1, 0);

            WriteStatementProperty("disabled", item.disabledExp, true);
            WriteIntProperty("ownerdraw", item.window.ownerDraw, 0);
            WriteFlagsProperty("ownerdrawFlag", item.window.ownerDrawFlags);
            WriteIntProperty("align", item.alignment, 0);
            WriteIntProperty("textalign", item.textAlignMode, 0);
            WriteFloatProperty("textalignx", item.textalignx, 0.0f);
            WriteFloatProperty("textaligny", item.textaligny, 0.0f);
            WriteFloatProperty("textscale", item.textscale, 0.0f);
            WriteIntProperty("textstyle", item.textStyle, 0);
            WriteIntProperty("textfont", item.fontEnum, 0);
            WriteColorProperty("backcolor", item.window.backColor, COLOR_0000);
            WriteColorProperty("forecolor", item.window.foreColor, COLOR_1111);
            WriteColorProperty("bordercolor", item.window.borderColor, COLOR_0000);
            WriteColorProperty("outlinecolor", item.window.outlineColor, COLOR_0000);
            WriteColorProperty("disablecolor", item.window.disableColor, COLOR_0000);
            WriteColorProperty("glowcolor", item.glowColor, COLOR_0000);
            WriteMaterialProperty("background", item.window.background);
            WriteMenuEventHandlerSetProperty("onFocus", item.onFocus);
            WriteMenuEventHandlerSetProperty("leaveFocus", item.leaveFocus);
            WriteMenuEventHandlerSetProperty("mouseEnter", item.mouseEnter);
            WriteMenuEventHandlerSetProperty("mouseExit", item.mouseExit);
            WriteMenuEventHandlerSetProperty("mouseEnterText", item.mouseEnterText);
            WriteMenuEventHandlerSetProperty("mouseExitText", item.mouseExitText);
            WriteMenuEventHandlerSetProperty("action", item.action);
            WriteMenuEventHandlerSetProperty("accept", item.onAccept);
            // WriteFloatProperty("special", item.special, 0.0f);
            WriteSoundAliasProperty("focusSound", item.focusSound);
            WriteStringProperty("dvarTest", item.dvarTest);

            if (item.dvarFlags & ITEM_DVAR_FLAG_ENABLE)
                WriteMultiTokenStringProperty("enableDvar", item.enableDvar);
            else if (item.dvarFlags & ITEM_DVAR_FLAG_DISABLE)
                WriteMultiTokenStringProperty("disableDvar", item.enableDvar);
            else if (item.dvarFlags & ITEM_DVAR_FLAG_SHOW)
                WriteMultiTokenStringProperty("showDvar", item.enableDvar);
            else if (item.dvarFlags & ITEM_DVAR_FLAG_HIDE)
                WriteMultiTokenStringProperty("hideDvar", item.enableDvar);
            else if (item.dvarFlags & ITEM_DVAR_FLAG_FOCUS)
                WriteMultiTokenStringProperty("focusDvar", item.enableDvar);

            WriteItemKeyHandlers(item.onKey);
            WriteStatementProperty("exp text", item.textExp, false);
            WriteStatementProperty("exp material", item.materialExp, false);
            WriteFloatExpressionsProperty(item.floatExpressions, item.floatExpressionCount);
            WriteIntProperty("gamemsgwindowindex", item.gameMsgWindowIndex, 0);
            WriteIntProperty("gamemsgwindowmode", item.gameMsgWindowMode, 0);
            WriteDecodeEffectProperty("decodeEffect", item);

            WriteListBoxProperties(item);
            WriteEditFieldProperties(item);
            WriteMultiProperties(item);
            WriteEnumDvarProperties(item);
            WriteTickerProperties(item);
        }

        void WriteItemDefs(const itemDef_s* const* items, const size_t itemCount)
        {
            if (!items || itemCount <= 0)
                return;

            for (size_t itemIndex = 0u; itemIndex < itemCount; itemIndex++)
            {
                const auto* item = items[itemIndex];
                if (!item)
                    continue;

                StartItemDefScope();
                WriteItemData(*item);
                EndScope();
            }
        }

        void WriteMenuData(const menuDef_t& menu)
        {
            WriteStringProperty("name", menu.window.name);
            WriteBoolProperty("fullscreen", menu.fullScreen, false);
            WriteKeywordProperty("screenSpace", menu.window.staticFlags & WINDOW_FLAG_SCREEN_SPACE);
            WriteKeywordProperty("decoration", menu.window.staticFlags & WINDOW_FLAG_DECORATION);
            WriteRectProperty("rect", menu.window.rect);
            WriteIntProperty("style", menu.window.style, 0);
            WriteIntProperty("border", menu.window.border, 0);
            WriteFloatProperty("borderSize", menu.window.borderSize, 0.0f);
            WriteColorProperty("backcolor", menu.window.backColor, COLOR_0000);
            WriteColorProperty("forecolor", menu.window.foreColor, COLOR_1111);
            WriteColorProperty("bordercolor", menu.window.borderColor, COLOR_0000);
            WriteColorProperty("focuscolor", menu.focusColor, COLOR_0000);
            WriteColorProperty("outlinecolor", menu.window.outlineColor, COLOR_0000);
            WriteMaterialProperty("background", menu.window.background);
            WriteIntProperty("ownerdraw", menu.window.ownerDraw, 0);
            WriteFlagsProperty("ownerdrawFlag", menu.window.ownerDrawFlags);
            WriteKeywordProperty("outOfBoundsClick", menu.window.staticFlags & WINDOW_FLAG_OUT_OF_BOUNDS_CLICK);
            WriteStringProperty("soundLoop", menu.soundName);
            WriteKeywordProperty("popup", menu.window.staticFlags & WINDOW_FLAG_POPUP);
            WriteFloatProperty("fadeClamp", menu.fadeClamp, 0.0f);
            WriteIntProperty("fadeCycle", menu.fadeCycle, 0);
            WriteFloatProperty("fadeAmount", menu.fadeAmount, 0.0f);
            WriteFloatProperty("fadeInAmount", menu.fadeInAmount, 0.0f);
            WriteFloatProperty("blurWorld", menu.blurRadius, 0.0f);
            WriteKeywordProperty("legacySplitScreenScale", menu.window.staticFlags & WINDOW_FLAG_LEGACY_SPLIT_SCREEN_SCALE);
            WriteKeywordProperty("hiddenDuringScope", menu.window.staticFlags & WINDOW_FLAG_HIDDEN_DURING_SCOPE);
            WriteKeywordProperty("hiddenDuringFlashbang", menu.window.staticFlags & WINDOW_FLAG_HIDDEN_DURING_FLASH_BANG);
            WriteKeywordProperty("hiddenDuringUI", menu.window.staticFlags & WINDOW_FLAG_HIDDEN_DURING_UI);
            WriteStringProperty("allowedBinding", menu.allowedBinding);
            WriteKeywordProperty("textOnlyFocus", menu.window.staticFlags & WINDOW_FLAG_TEXT_ONLY_FOCUS);

            if (HasStatement(menu.visibleExp))
                WriteStatementProperty("visible", menu.visibleExp, true);
            else if (menu.window.dynamicFlags[0] & WINDOW_FLAG_VISIBLE)
                WriteIntProperty("visible", 1, 0);

            WriteStatementProperty("exp rect X", menu.rectXExp, false);
            WriteStatementProperty("exp rect Y", menu.rectYExp, false);
            WriteStatementProperty("exp rect W", menu.rectWExp, false);
            WriteStatementProperty("exp rect H", menu.rectHExp, false);
            WriteStatementProperty("exp openSound", menu.openSoundExp, false);
            WriteStatementProperty("exp closeSound", menu.closeSoundExp, false);
            WriteMenuEventHandlerSetProperty("onOpen", menu.onOpen);
            WriteMenuEventHandlerSetProperty("onClose", menu.onClose);
            WriteMenuEventHandlerSetProperty("onRequestClose", menu.onCloseRequest);
            WriteMenuEventHandlerSetProperty("onESC", menu.onESC);
            WriteItemKeyHandlers(menu.onKey);
            WriteItemDefs(menu.items, menu.itemCount);
        }
    };
} // namespace

namespace menu
{
    std::unique_ptr<IWriterIW4> CreateMenuWriterIW4(std::ostream& stream)
    {
        return std::make_unique<MenuWriter>(stream);
    }
} // namespace menu
