#include "Base.h"

#include "EncoderArguments.h"
#include "StringUtil.h"

#ifdef WIN32
    #define PATH_MAX    _MAX_PATH
    #define realpath(A,B)    _fullpath(B,A,PATH_MAX)
#endif

// The encoder version number should be incremented when a feature is added to the encoder.
// The encoder version is not the same as the GPB version.
#define ENCODER_VERSION "3.6.0 hsp3gp"
#define HEIGHTMAP_SIZE_MAX 2049

namespace gameplay
{

static EncoderArguments* __instance;

extern int __logVerbosity = 1;

EncoderArguments::EncoderArguments(size_t argc, const char** argv) :
    _normalMap(false),
    _parseError(false),
    _fontPreview(false),
    _fontFormat(Font::BITMAP),
    _textOutput(false),
    _optimizeAnimations(false),
    _animationGrouping(ANIMATIONGROUP_PROMPT),
    _outputMaterial(false),
    _generateTextureGutter(false),
    _mergeAnimation(false)
{
    __instance = this;
	_textureOptionPath = std::string("res/");
	_shaderOptionPath = std::string("res/shaders/");

    memset(_heightmapResolution, 0, sizeof(int) * 2);

    if (argc > 1)
    {
        // read the options
        std::vector<std::string> arguments;
        for (size_t i = 1; i < argc; ++i)
        {
            arguments.push_back(argv[i]);
        }
        size_t index = 0;
        for (size_t i = 0; i < arguments.size(); ++i)
        {
            if (arguments[i][0] == '-')
            {
                readOption(arguments, &i);
                index = i + 1;
            }
        }
        if (arguments.size() - index == 2)
        {
            setInputfilePath(arguments[index]);
            setOutputfilePath(arguments[index + 1]);
        }
        else if (arguments.size() - index == 1)
        {
            setInputfilePath(arguments[index]);
        }
    }
    else
    {
        _parseError = true;
    }
}

EncoderArguments::~EncoderArguments(void)
{
}

EncoderArguments* EncoderArguments::getInstance()
{
    return __instance;
}

const std::string& EncoderArguments::getFilePath() const
{
    return _filePath;
}

const std::string EncoderArguments::getTextureOptionPath() const
{
	return _textureOptionPath;
}

const std::string EncoderArguments::getMergeAnimationFBXName() const
{
	return _mergeAnimationFBXName;
}

const std::string EncoderArguments::getFileDirPath() const
{
    int pos = _filePath.find_last_of('/');
    return (pos == -1 ? _filePath : _filePath.substr(0, pos));
}

const char* EncoderArguments::getFilePathPointer() const
{
    return _filePath.c_str();
}

const std::string EncoderArguments::getFileName() const
{
    int pos = _filePath.find_last_of('/');
    return (pos == -1 ? _filePath : _filePath.substr(pos + 1));
}

std::string EncoderArguments::getOutputDirPath() const
{
    if (_fileOutputPath.size() > 0)
    {
        int pos = _fileOutputPath.find_last_of('/');
        return (pos == -1 ? _fileOutputPath : _fileOutputPath.substr(0, pos));
    }
    else
    {
        int pos = _filePath.find_last_of('/');
        return (pos == -1 ? _filePath : _filePath.substr(0, pos));
    }
}

std::string EncoderArguments::getOutputFileExtension() const
{
    switch (getFileFormat())
    {
    case FILEFORMAT_TMX:
        return ".scene";
    case FILEFORMAT_PNG:
    case FILEFORMAT_RAW:
        if (_normalMap)
            return ".png";

    default:
        return ".gpb";
    }
}

std::string EncoderArguments::getOutputFilePath() const
{
    if (_fileOutputPath.size() > 0)
    {
        // Output file explicitly set
        return _fileOutputPath;
    }
    else
    {
        // Generate an output file path
        int pos = _filePath.find_last_of('.');
        std::string outputFilePath(pos > 0 ? _filePath.substr(0, pos) : _filePath);

        // Modify the original file name if the output extension can be the same as the input
        if (_normalMap)
        {
            outputFilePath.append("_normalmap");
        }

        outputFilePath.append(getOutputFileExtension());
        return outputFilePath;
    }
}

const std::vector<std::string>& EncoderArguments::getGroupAnimationNodeId() const
{
    return _groupAnimationNodeId;
}

const std::vector<std::string>& EncoderArguments::getGroupAnimationAnimationId() const
{
    return _groupAnimationAnimationId;
}

bool EncoderArguments::containsGroupNodeId(const std::string& nodeId) const
{
    return find(_groupAnimationNodeId.begin(), _groupAnimationNodeId.end(), nodeId) != _groupAnimationNodeId.end();
}

const std::string EncoderArguments::getAnimationId(const std::string& nodeId) const
{
    for (size_t i = 0, size = _groupAnimationNodeId.size(); i < size; ++i)
    {
        if (_groupAnimationNodeId[i].compare(nodeId) == 0)
        {
            return _groupAnimationAnimationId[i];
        }
    }
    return "";
}

EncoderArguments::AnimationGroupOption EncoderArguments::getAnimationGrouping() const
{
    return _animationGrouping;
}

const std::vector<EncoderArguments::HeightmapOption>& EncoderArguments::getHeightmapOptions() const
{
    return _heightmaps;
}

unsigned int EncoderArguments::tangentBinormalIdCount() const
{
    return _tangentBinormalId.size();
}

bool EncoderArguments::isGenerateTangentBinormalId(const std::string& id) const
{
    return _tangentBinormalId.find(id) != _tangentBinormalId.end();
}

bool EncoderArguments::normalMapGeneration() const
{
    return _normalMap;
}

void EncoderArguments::getHeightmapResolution(int* x, int* y) const
{
    *x = _heightmapResolution[0];
    *y = _heightmapResolution[1];
}

const Vector3& EncoderArguments::getHeightmapWorldSize() const
{
    return _heightmapWorldSize;
}

bool EncoderArguments::parseErrorOccured() const
{
    return _parseError;
}

bool EncoderArguments::fileExists() const
{
    if (_filePath.length() > 0)
    {
        struct stat buf;
        if (stat(_filePath.c_str(), &buf) != -1)
        {
            return true;
        }
    }
    return false;
}

void splitString(const char* str, std::vector<std::string>* tokens)
{
    // Split node id list into tokens
    unsigned int length = strlen(str);
    char* temp = new char[length + 1];
    strcpy(temp, str);
    char* tok = strtok(temp, ",");
    while (tok)
    {
        tokens->push_back(tok);
        tok = strtok(NULL, ",");
    }
    delete[] temp;
}

void EncoderArguments::printUsage() const
{
    LOG(1, "Usage: gameplay-encoder [options] <input filepath> <output filepath>\n\n" \
    "Encoder version: " ENCODER_VERSION "\n\n" \
    "Supported file extensions:\n" \
    "  .fbx\t(FBX scenes)\n" \
    "  .ttf\t(TrueType fonts)\n" \
    "\n" \
    "General options:\n" \
    "  -v <verbosity>\tVerbosity level (0-4).\n" \
    "\n" \
    "FBX file options:\n" \
    "  -i <id>\tFilter by node ID.\n" \
    "  -t\t\tWrite text/xml.\n" \
    "  -g:auto\tAutomatically group animation channels into a new animation.\n" \
    "  -g:none\tDo not prompt to group animations.\n" \
    "  -g <node id> <animation id>\n" \
        "\t\tGroup all animation channels targeting the nodes into a \n" \
        "\t\tnew animation.\n" \
    "  -m\t\tOutput material file for scene.\n" \
    "  -mt <texture path>\t\tTexture reference path.\n" \
    "  -ms <shader path>\t\tShader reference path.\n" \
    "  -e <FBX file>\t\tMerge FBX animation.\n" \
    "  -tb <node id>\n" \
        "\t\tGenerates tangents and binormals for the given node.\n" \
    "  -oa\n" \
        "\t\tOptimizes animations by analyzing animation channel data and\n" \
        "\t\tremoving any channels that contain default/identity values\n" \
        "\t\tand removing any duplicate contiguous keyframes, which are \n" \
        "\t\tcommon when exporting baked animation data.\n" \
    "  -h <size> \"<node ids>\" <filename>\n" \
        "\t\tGenerates a single heightmap image using meshes from the \n" \
        "\t\tspecified nodes. \n" \
        "\t\t<size> is two comma-separated numbers in the format \"X,Y\", \n" \
        "\t\tindicating the dimensions of the produced heightmap image.\n" \
        "\t\t<node ids> should be in quotes with a space between each id.\n" \
        "\t\tFilename is the name of the image (PNG) to be saved.\n" \
        "\t\tMultiple -h arguments can be supplied to generate more than one \n" \
        "\t\theightmap. For 24-bit packed height data use -hp instead of -h.\n" \
    "\n" \
    "TMX file options:\n" \
    "  -tg\tEnable texture gutter's around tiles. This will modify any referenced\n" \
    "  \ttile sets to add a 1px border around it to prevent seams.\n"
    "  -tg:none\tDo not priduce a texture gutter.\n"
    "\n" \
    "Normal map options:\n" \
        "  -n\t\tGenerate normal map (requires input file of type PNG or RAW)\n" \
        "  -s\t\tSize/resolution of the input heightmap image (required for RAW files)\n" \
        "  -w <size>\tSpecifies the size of an input terrain heightmap file in world\n" \
        "\t\tunits, along the X, Y and Z axes. <size> should be three \n" \
        "\t\tcomma-separated numbers in the format \"X,Y,Z\".  \n" \
        "\t\tThe Y value represents the maximum possible height value of a  \n" \
        "\t\tfull intensity heightmap pixel.\n" \
        "\n" \
        "  \t\tNormal map generation can be used to create object-space normal maps from \n" \
        "  \t\theightmap images. Heightmaps must be in either PNG format (where the \n" \
        "  \t\tintensity of each pixel represents a height value), or in RAW format \n" \
        "  \t\t(8 or 16-bit), which is a common headerless format supported by most \n" \
        "  \t\tterrain generation tools.\n" \
    "\n" \
    "TTF file options:\n" \
    "  -s <sizes>\tComma-separated list of font sizes (in pixels).\n" \
    "  -p\t\tOutput font preview.\n" \
    "  -f\t\tFormat of font. -f:b (BITMAP), -f:d (DISTANCE_FIELD).\n" \
    "\n");
    exit(8);
}


bool EncoderArguments::mergeAnimationEnabled() const
{
    return _mergeAnimation;
}

bool EncoderArguments::fontPreviewEnabled() const
{
    return _fontPreview;
}

Font::FontFormat EncoderArguments::getFontFormat() const
{
    return _fontFormat;
}

bool EncoderArguments::textOutputEnabled() const
{
    return _textOutput;
}

bool EncoderArguments::optimizeAnimationsEnabled() const
{
    return _optimizeAnimations;
}

bool EncoderArguments::outputMaterialEnabled() const
{
    return _outputMaterial;
}

bool EncoderArguments::generateTextureGutter() const
{
    return _generateTextureGutter;
}

const char* EncoderArguments::getNodeId() const
{
    if (_nodeId.length() == 0)
    {
        return NULL;
    }
    return _nodeId.c_str();
}

std::vector<unsigned int> EncoderArguments::getFontSizes() const
{
    return _fontSizes;
}

EncoderArguments::FileFormat EncoderArguments::getFileFormat() const
{
    if (_filePath.length() < 5)
    {
        return FILEFORMAT_UNKNOWN;
    }
    // Extract the extension
    std::string ext = "";
    size_t pos = _filePath.find_last_of(".");
    if (pos != std::string::npos)
    {
        ext = _filePath.substr(pos + 1);
    }
    for (size_t i = 0; i < ext.size(); ++i)
        ext[i] = (char)tolower(ext[i]);
    
    // Match every supported extension with its format constant
    if (ext.compare("fbx") == 0)
    {
        return FILEFORMAT_FBX;
    }
    if (ext.compare("tmx") == 0)
    {
        return FILEFORMAT_TMX;
    }
    if (ext.compare("ttf") == 0)
    {
        return FILEFORMAT_TTF;
    }
    if (ext.compare("otf") == 0)
    {
        return FILEFORMAT_OTF;
    }
    if (ext.compare("gpb") == 0)
    {
        return FILEFORMAT_GPB;
    }
    if (ext.compare("png") == 0)
    {
        return FILEFORMAT_PNG;
    }
    if (ext.compare("raw") == 0)
    {
        return FILEFORMAT_RAW;
    }

    return FILEFORMAT_UNKNOWN;
}

void EncoderArguments::readOption(const std::vector<std::string>& options, size_t* index)
{
    const std::string& str = options[*index];
    if (str.length() == 0 && str[0] != '-')
    {
        return;
    }
    switch (str[1])
    {
    case 'f':
        if (str.compare("-f:b") == 0)
        {
            _fontFormat = Font::BITMAP;
        }
       else  if (str.compare("-f:d") == 0)
        {
            _fontFormat = Font::DISTANCE_FIELD;
        }
        break;
    case 'g':
        if (str.compare("-groupAnimations:auto") == 0 || str.compare("-g:auto") == 0)
        {
            _animationGrouping = ANIMATIONGROUP_AUTO;
        }
        else if (str.compare("-groupAnimations:off") == 0 || str.compare("-g:off") == 0)
        {
            _animationGrouping = ANIMATIONGROUP_OFF;
        }
        else if (str.compare("-groupAnimations") == 0 || str.compare("-g") == 0)
        {
            // read two strings, make sure not to go out of bounds
            if ((*index + 2) >= options.size())
            {
                LOG(1, "Error: -g requires 2 arguments.\n");
                _parseError = true;
                return;
            }
            (*index)++;
            _groupAnimationNodeId.push_back(options[*index]);
            (*index)++;
            _groupAnimationAnimationId.push_back(options[*index]);
        }
        break;
    case 'i':
        // Node ID
        (*index)++;
        if (*index < options.size())
        {
            _nodeId.assign(options[*index]);
        }
        else
        {
            LOG(1, "Error: missing arguemnt for -%c.\n", str[1]);
            _parseError = true;
            return;
        }
        break;
    case 'o':
        // Optimization flag
        if (str == "-oa")
        {
            // Optimize animations
            _optimizeAnimations = true;
        }
        break;
    case 'h':
        {
            bool isHighPrecision = str.compare("-hp") == 0;
            if (str.compare("-heightmap") == 0 || str.compare("-h") == 0 || isHighPrecision)
            {
                (*index)++;
                if (*index < (options.size() + 2))
                {
                    _heightmaps.resize(_heightmaps.size() + 1);
                    HeightmapOption& heightmap = _heightmaps.back();
                    
                    heightmap.isHighPrecision = isHighPrecision;

                    // Read heightmap size
                    std::vector<std::string> parts;
                    splitString(options[*index].c_str(), &parts);
                    if (parts.size() != 2)
                    {
                        LOG(1, "Error: invalid size argument for -h|-heightmap.\n");
                        _parseError = true;
                        return;
                    }
                    heightmap.width = atoi(parts[0].c_str());
                    heightmap.height = atoi(parts[1].c_str());

                    // Put some artificial bounds on heightmap dimensions
                    if (heightmap.width <= 0 || heightmap.height <= 0 || heightmap.width > HEIGHTMAP_SIZE_MAX || heightmap.height > HEIGHTMAP_SIZE_MAX)
                    {
                        LOG(1, "Error: size argument for -h|-heightmap must be between (1,1) and (%d,%d).\n", (int)HEIGHTMAP_SIZE_MAX, (int)HEIGHTMAP_SIZE_MAX);
                        _parseError = true;
                        return;
                    }

                    // Split node id list into tokens
                    (*index)++;
                    splitString(options[*index].c_str(), &heightmap.nodeIds);

                    // Store output filename
                    (*index)++;
                    heightmap.filename = options[*index];
                    if (heightmap.filename.empty())
                    {
                        LOG(1, "Error: missing filename argument for -h|-heightmap.\n");
                        _parseError = true;
                        return;
                    }
                    
                    // Ensure the output filename has a .png extention
                    if (heightmap.filename.length() > 5)
                    {
                        const char* ext = heightmap.filename.c_str() + (heightmap.filename.length() - 4);
                        if (ext[0] != '.' || tolower(ext[1]) != 'p' || tolower(ext[2]) != 'n' || tolower(ext[3]) != 'g')
                            heightmap.filename += ".png";
                    }
                    else
                        heightmap.filename += ".png";
                }
                else
                {
                    LOG(1, "Error: missing argument for -h|-heightmap.\n");
                    _parseError = true;
                    return;
                }
            }
        }
        break;
    case 'm':
        if (str.compare("-mt") == 0) {
			(*index)++;
			_textureOptionPath = options[*index];
        }
        if (str.compare("-ms") == 0) {
			(*index)++;
			_shaderOptionPath = options[*index];
        }
        if (str.compare("-m") == 0)
        {
            // generate a material file
            _outputMaterial = true;
        }
        break;
    case 'n':
        _normalMap = true;
        break;
    case 'w':
        {
            // Read world size
            (*index)++;
            if (*index >= options.size())
            {
                LOG(1, "Error: missing world size argument for -w.\n");
                _parseError = true;
                return;
            }
            std::vector<std::string> parts;
            splitString(options[*index].c_str(), &parts);
            if (parts.size() != 3)
            {
                LOG(1, "Error: invalid world size argument for -w.\n");
                _parseError = true;
                return;
            }
            _heightmapWorldSize.x = (float)atof(parts[0].c_str());
            _heightmapWorldSize.y = (float)atof(parts[1].c_str());
            _heightmapWorldSize.z = (float)atof(parts[2].c_str());
            if (_heightmapWorldSize.x == 0 || _heightmapWorldSize.y == 0 || _heightmapWorldSize.z == 0)
            {
                LOG(1, "Error: invalid world size argument for -w.\n");
                _parseError = true;
                return;
            }
        }
        break;
    case 'p':
        _fontPreview = true;
        break;
    case 's':
        if (_normalMap)
        {
            (*index)++;
            if (*index >= options.size())
            {
                LOG(1, "Error: missing argument for -s.\n");
                _parseError = true;
                return;
            }
            // Heightmap size
            std::vector<std::string> parts;
            splitString(options[*index].c_str(), &parts);
            if (parts.size() != 2 ||
                (_heightmapResolution[0] = atoi(parts[0].c_str())) <= 0 ||
                (_heightmapResolution[1] = atoi(parts[1].c_str())) <= 0)
            {
                LOG(1, "Error: invalid argument for -s.\n");
                _parseError = true;
                return;
            }
        }
        else
        {
            // Font Sizes
            // old format was -s##
            const char* sizes = NULL;
            if (str.length() > 2)
            {
                char n = str[2];
                if (n > '0' && n <= '9')
                {
                    sizes = str.c_str() + 2;
                }
            }
            else
            {
                (*index)++;
                if (*index < options.size())
                {
                    sizes = options[*index].c_str();
                }
            }

            if (sizes == NULL)
            {
                LOG(1, "Error: invalid format for argument: -s");
                _parseError = true;
                return;
            }

            // Parse comma-separated list of font sizes
            char* ptr = const_cast<char*>(sizes);
            std::string sizeStr;
            while (ptr)
            {
                char* end = strchr(ptr, ',');
                if (end)
                {
                    sizeStr = std::string(ptr, end - ptr);
                    ptr = end + 1;
                }
                else
                {
                    sizeStr = ptr;
                    ptr = NULL;
                }
                if (sizeStr.length() > 0)
                {
                    int size = atoi(sizeStr.c_str());
                    if (size <= 0)
                    {
                        LOG(1, "Error: invalid font size provided: %s", sizeStr.c_str());
                        _parseError = true;
                        return;
                    }
                    _fontSizes.push_back((unsigned int)size);
                }
            }
        }
        break;
    case 't':
        if (str.compare("-t") == 0)
        {
            _textOutput = true;
        }
        else if (str.compare("-tb") == 0)
        {
            if ((*index + 1) >= options.size())
            {
                LOG(1, "Error: -tb requires 1 argument.\n");
                _parseError = true;
                return;
            }
            (*index)++;
            std::string nodeId = options[*index];
            if (nodeId.length() > 0)
            {
                _tangentBinormalId.insert(nodeId);
            }
        }
        else if (str.compare("-textureGutter:none") == 0 || str.compare("-tg:none") == 0)
        {
            _generateTextureGutter = false;
        }
        else if (str.compare("-textureGutter") == 0 || str.compare("-tg") == 0)
        {
            _generateTextureGutter = true;
        }
        break;
	case 'e':
		(*index)++;
		_mergeAnimationFBXName = options[*index];
		_mergeAnimation = true;
		break;
    case 'v':
        (*index)++;
        if (*index < options.size())
        {
            __logVerbosity = atoi(options[*index].c_str());
            if (__logVerbosity < 0)
                __logVerbosity = 0;
            else if (__logVerbosity > 4)
                __logVerbosity = 4;
        }
        break;
    default:
        break;
    }
}

void EncoderArguments::setInputfilePath(const std::string& inputPath)
{
    _filePath.assign(getRealPath(inputPath));
}

void EncoderArguments::setOutputfilePath(const std::string& outputPath)
{
    std::string ext = getOutputFileExtension();

    if (outputPath.size() > 0 && outputPath[0] != '\0')
    {
        std::string realPath = getRealPath(outputPath);
        if (endsWith(realPath.c_str(), ext.c_str()))
        {
            _fileOutputPath.assign(realPath);
        }
        else if (endsWith(outputPath.c_str(), "/"))
        {
            std::string filenameNoExt = getFilenameNoExt(getFilenameFromFilePath(_filePath));

            _fileOutputPath.assign(outputPath);
            _fileOutputPath.append(filenameNoExt);
            _fileOutputPath.append(ext);
        }
        else
        {
            std::string filenameNoExt = getFilenameNoExt(getFilenameFromFilePath(realPath));
            int pos = realPath.find_last_of("/");
            if (pos)
            {
                _fileOutputPath = realPath.substr(0, pos);
                _fileOutputPath.append("/");
                _fileOutputPath.append(filenameNoExt);
                _fileOutputPath.append(ext);
            }
        }
    }
}

std::string EncoderArguments::getRealPath(const std::string& filepath)
{
    char path[PATH_MAX + 1]; /* not sure about the "+ 1" */
    realpath(filepath.c_str(), path);
    replace_char(path, '\\', '/');
    return std::string(path);
}

void EncoderArguments::replace_char(char* str, char oldChar, char newChar)
{
    for (; *str != '\0'; ++str)
    {
        if (*str == oldChar)
        {
            *str = newChar;
        }
    }
}

std::string concat(const std::string& a, const char* b)
{
    std::string str(a);
    str.append(b);
    return str;
}

void unittestsEncoderArguments()
{
    std::string dir = EncoderArguments::getRealPath(".");
    std::string exePath = EncoderArguments::getRealPath(".");
    exePath.append("/gameplay-encoder.exe");
    const char* exe = exePath.c_str();
    {
        const char* argv[] = {exe, "-g", "root", "movements", "C:\\Git\\gaming\\GamePlay\\gameplay-encoder\\res\\duck.fbx"};
        EncoderArguments args(sizeof(argv) / sizeof(char*), (const char**)argv);
        assert(equals(args.getAnimationId("root"), ("movements")));
        assert(equals(args.getGroupAnimationNodeId()[0], ("root")));
        assert(equals(args.getOutputFilePath(), "C:/Git/gaming/GamePlay/gameplay-encoder/res/duck.gpb"));
    }
    {
        // Test with only input file name (relative)
        const char* argv[] = {exe, "input.fbx"};
        EncoderArguments args(sizeof(argv) / sizeof(char*), (const char**)argv);
        assert(equals(args.getFilePath(), concat(dir, "/input.fbx")));
        assert(equals(args.getOutputFilePath(), concat(dir, "/input.gpb")));
        equals(args.getOutputDirPath(), dir);
    }
    {
        // Test specifying a relative output path
        const char* argv[] = {exe, "input.fbx", "output.gpb"};
        EncoderArguments args(sizeof(argv) / sizeof(char*), (const char**)argv);
        assert(equals(args.getFilePath(), concat(dir, "/input.fbx")));
        assert(equals(args.getOutputFilePath(), concat(dir, "/output.gpb")));
    }
    {
        // Test specifying a relative output path
        const char* argv[] = {exe, "input.fbx", "output.gpb"};
        EncoderArguments args(sizeof(argv) / sizeof(char*), (const char**)argv);
        assert(equals(args.getFilePath(), concat(dir, "/input.fbx")));
        assert(equals(args.getOutputFilePath(), concat(dir, "/output.gpb")));
    }
    {
        // Test specifying a relative output path to a directory
        const char* argv[] = {exe, "input.fbx", "stuff/output.gpb"};
        EncoderArguments args(sizeof(argv) / sizeof(char*), (const char**)argv);
        assert(equals(args.getFilePath(), concat(dir, "/input.fbx")));
        assert(equals(args.getOutputFilePath(), concat(dir, "/stuff/output.gpb")));
    }
    {
        // Test parsing some arguments
        const char* argv[] = {exe, "test.fbx", "-t", "input.fbx", "output.gpb"};
        EncoderArguments args(sizeof(argv) / sizeof(char*), (const char**)argv);
        assert(equals(args.getFilePath(), concat(dir, "/input.fbx")));
        assert(equals(args.getOutputFilePath(), concat(dir, "/output.gpb")));
        assert(args.textOutputEnabled());
    }
    {
        // Test output file with no file extension
        const char* argv[] = {exe, "input.fbx", "output"};
        EncoderArguments args(sizeof(argv) / sizeof(char*), (const char**)argv);
        assert(equals(args.getFilePath(), concat(dir, "/input.fbx")));
        assert(equals(args.getOutputFilePath(), concat(dir, "/output.gpb")));
    }
    {
        // Test output file with wrong file extension
        const char* argv[] = {exe, "input.fbx", "output.fbx"};
        EncoderArguments args(sizeof(argv) / sizeof(char*), (const char**)argv);
        assert(equals(args.getFilePath(), concat(dir, "/input.fbx")));
        assert(equals(args.getOutputFilePath(), concat(dir, "/output.gpb")));
    }
}

}
