/**
 Copyright 2013 BlackBerry Inc.
 Copyright (c) 2015-2017 Chukong Technologies
 Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd.

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.

 Original file from GamePlay3D: http://gameplay3d.org

 This file was modified to fit the cocos2d-x project
 */

#include "base/CCProperties.h"

#include <string.h>

#include "platform/CCPlatformMacros.h"
#include "platform/CCFileUtils.h"
#include "math/Vec2.h"
#include "math/Vec3.h"
#include "math/Vec4.h"
#include "math/Mat4.h"
#include "math/Quaternion.h"
#include "base/ccUTF8.h"
#include "base/CCData.h"

USING_NS_AX;

// Utility functions (shared with SceneLoader).
/** @script{ignore} */
void calculateNamespacePath(std::string_view urlString,
                            std::string& fileString,
                            std::vector<std::string_view>& namespacePath);
/** @script{ignore} */
Properties* getPropertiesFromNamespacePath(Properties* properties, const std::vector<std::string_view>& namespacePath);

Properties::Properties() : _dataIdx(nullptr), _data(nullptr), _variables(nullptr), _dirPath(nullptr), _parent(nullptr)
{
    _properties.reserve(32);
}

Properties::Properties(const Properties& copy)
    : _dataIdx(copy._dataIdx)
    , _data(copy._data)
    , _namespace(copy._namespace)
    , _id(copy._id)
    , _parentID(copy._parentID)
    , _properties(copy._properties)
    , _variables(nullptr)
    , _dirPath(nullptr)
    , _parent(copy._parent)

{
    setDirectoryPath(copy._dirPath);

    for (const auto space : copy._namespaces)
    {
        _namespaces.emplace_back(new Properties(*space));
    }
    rewind();
}

Properties::Properties(Data* data, ssize_t* dataIdx)
    : _dataIdx(dataIdx), _data(data), _variables(NULL), _dirPath(NULL), _parent(NULL)
{
    readProperties();
    rewind();
}

Properties::Properties(Data* data,
                       ssize_t* dataIdx,
                       std::string_view name,
                       const char* id,
                       const char* parentID,
                       Properties* parent)
    : _dataIdx(dataIdx), _data(data), _namespace(name), _variables(NULL), _dirPath(NULL), _parent(parent)
{
    if (id)
    {
        _id = id;
    }
    if (parentID)
    {
        _parentID = parentID;
    }
    readProperties();
    rewind();
}

Properties* Properties::createNonRefCounted(std::string_view url)
{
    if (url.empty())
    {
        AXLOGERROR("Attempting to create a Properties object from an empty URL!");
        return nullptr;
    }

    // Calculate the file and full namespace path from the specified url.
    auto& urlString = url;
    std::string fileString;
    std::vector<std::string_view> namespacePath;
    calculateNamespacePath(urlString, fileString, namespacePath);

    // data will be released automatically when 'data' goes out of scope
    // so we pass data as weak pointer
    auto data              = FileUtils::getInstance()->getDataFromFile(fileString);
    ssize_t dataIdx        = 0;
    Properties* properties = new Properties(&data, &dataIdx);
    properties->resolveInheritance();

    // Get the specified properties object.
    Properties* p = getPropertiesFromNamespacePath(properties, namespacePath);
    if (!p)
    {
        AXLOGWARN("Failed to load properties from url '%s'.", url.data());
        AX_SAFE_DELETE(properties);
        return nullptr;
    }

    // If the loaded properties object is not the root namespace,
    // then we have to clone it and delete the root namespace
    // so that we don't leak memory.
    if (p != properties)
    {
        p = p->clone();
        AX_SAFE_DELETE(properties);
    }
    // XXX
    //    p->setDirectoryPath(FileSystem::getDirectoryName(fileString));
    p->setDirectoryPath("");
    return p;
}

static bool isVariable(const char* str, char* outName, size_t outSize)
{
    size_t len = strlen(str);
    if (len > 3 && str[0] == '$' && str[1] == '{' && str[len - 1] == '}')
    {
        size_t size = len - 3;
        if (size > (outSize - 1))
            size = outSize - 1;
        strncpy(outName, str + 2, len - 3);
        outName[len - 3] = 0;
        return true;
    }

    return false;
}

void Properties::readProperties()
{
    AXASSERT(_data->getSize() > 0, "Invalid data");

    char line[2048];
    char variable[256];
    int c;
    char* name;
    char* value;
    char* parentID;
    char* rc;
    char* rcc;
    char* rccc;
    bool comment = false;

    while (true)
    {
        // Skip whitespace at the start of lines
        skipWhiteSpace();

        // Stop when we have reached the end of the file.
        if (eof())
            break;

        // Read the next line.
        rc = readLine(line, 2048);
        if (rc == NULL)
        {
            AXLOGERROR("Error reading line from file.");
            return;
        }

        // Ignore comments
        if (comment)
        {
            // Check for end of multi-line comment at either start or end of line
            if (strncmp(line, "*/", 2) == 0)
                comment = false;
            else
            {
                trimWhiteSpace(line);
                const auto len = strlen(line);
                if (len >= 2 && strncmp(line + (len - 2), "*/", 2) == 0)
                    comment = false;
            }
        }
        else if (strncmp(line, "/*", 2) == 0)
        {
            // Start of multi-line comment (must be at start of line)
            comment = true;
        }
        else if (strncmp(line, "//", 2) != 0)
        {
            // If an '=' appears on this line, parse it as a name/value pair.
            // Note: strchr() has to be called before strtok(), or a backup of line has to be kept.
            rc = strchr(line, '=');
            if (rc != NULL)
            {
                // First token should be the property name.
                name = strtok(line, "=");
                if (name == NULL)
                {
                    AXLOGERROR("Error parsing properties file: attribute without name.");
                    return;
                }

                // Remove white-space from name.
                name = trimWhiteSpace(name);

                // Scan for next token, the property's value.
                value = strtok(NULL, "");
                if (value == NULL)
                {
                    AXLOGERROR("Error parsing properties file: attribute with name ('%s') but no value.", name);
                    return;
                }

                // Remove white-space from value.
                value = trimWhiteSpace(value);

                // Is this a variable assignment?
                if (isVariable(name, variable, 256))
                {
                    setVariable(variable, value);
                }
                else
                {
                    // Normal name/value pair
                    _properties.emplace_back(Property(name, value));
                }
            }
            else
            {
                parentID = NULL;

                // Get the last character on the line (ignoring whitespace).
                const char* lineEnd = trimWhiteSpace(line) + (strlen(trimWhiteSpace(line)) - 1);

                // This line might begin or end a namespace,
                // or it might be a key/value pair without '='.

                // Check for '{' on same line.
                rc = strchr(line, '{');

                // Check for inheritance: ':'
                rcc = strchr(line, ':');

                // Check for '}' on same line.
                rccc = strchr(line, '}');

                // Get the name of the namespace.
                name = strtok(line, " \t\n{");
                name = trimWhiteSpace(name);
                if (name == NULL)
                {
                    AXLOGERROR("Error parsing properties file: failed to determine a valid token for line '%s'.", line);
                    return;
                }
                else if (name[0] == '}')
                {
                    // End of namespace.
                    return;
                }

                // Get its ID if it has one.
                value = strtok(NULL, ":{");
                value = trimWhiteSpace(value);

                // Get its parent ID if it has one.
                if (rcc != NULL)
                {
                    parentID = strtok(NULL, "{");
                    parentID = trimWhiteSpace(parentID);
                }

                if (value != NULL && value[0] == '{')
                {
                    // If the namespace ends on this line, seek back to right before the '}' character.
                    if (rccc && rccc == lineEnd)
                    {
                        if (seekFromCurrent(-1) == false)
                        {
                            AXLOGERROR("Failed to seek back to before a '}' character in properties file.");
                            return;
                        }
                        while (readChar() != '}')
                        {
                            if (seekFromCurrent(-2) == false)
                            {
                                AXLOGERROR("Failed to seek back to before a '}' character in properties file.");
                                return;
                            }
                        }
                        if (seekFromCurrent(-1) == false)
                        {
                            AXLOGERROR("Failed to seek back to before a '}' character in properties file.");
                            return;
                        }
                    }

                    // New namespace without an ID.
                    Properties* space = new Properties(_data, _dataIdx, name, NULL, parentID, this);
                    _namespaces.emplace_back(space);

                    // If the namespace ends on this line, seek to right after the '}' character.
                    if (rccc && rccc == lineEnd)
                    {
                        if (seekFromCurrent(1) == false)
                        {
                            AXLOGERROR("Failed to seek to immediately after a '}' character in properties file.");
                            return;
                        }
                    }
                }
                else
                {
                    // If '{' appears on the same line.
                    if (rc != NULL)
                    {
                        // If the namespace ends on this line, seek back to right before the '}' character.
                        if (rccc && rccc == lineEnd)
                        {
                            if (seekFromCurrent(-1) == false)
                            {
                                AXLOGERROR("Failed to seek back to before a '}' character in properties file.");
                                return;
                            }
                            while (readChar() != '}')
                            {
                                if (seekFromCurrent(-2) == false)
                                {
                                    AXLOGERROR("Failed to seek back to before a '}' character in properties file.");
                                    return;
                                }
                            }
                            if (seekFromCurrent(-1) == false)
                            {
                                AXLOGERROR("Failed to seek back to before a '}' character in properties file.");
                                return;
                            }
                        }

                        // Create new namespace.
                        Properties* space = new Properties(_data, _dataIdx, name, value, parentID, this);
                        _namespaces.emplace_back(space);

                        // If the namespace ends on this line, seek to right after the '}' character.
                        if (rccc && rccc == lineEnd)
                        {
                            if (seekFromCurrent(1) == false)
                            {
                                AXLOGERROR("Failed to seek to immediately after a '}' character in properties file.");
                                return;
                            }
                        }
                    }
                    else
                    {
                        // Find out if the next line starts with "{"
                        skipWhiteSpace();
                        c = readChar();
                        if (c == '{')
                        {
                            // Create new namespace.
                            Properties* space = new Properties(_data, _dataIdx, name, value, parentID, this);
                            _namespaces.emplace_back(space);
                        }
                        else
                        {
                            // Back up from fgetc()
                            if (seekFromCurrent(-1) == false)
                                AXLOGERROR(
                                    "Failed to seek backwards a single character after testing if the next line starts "
                                    "with '{'.");

                            // Store "name value" as a name/value pair, or even just "name".
                            if (value != NULL)
                            {
                                _properties.emplace_back(Property(name, value));
                            }
                            else
                            {
                                _properties.emplace_back(Property(name, ""));
                            }
                        }
                    }
                }
            }
        }
    }
}

Properties::~Properties()
{
    AX_SAFE_DELETE(_dirPath);
    for (size_t i = 0, count = _namespaces.size(); i < count; ++i)
    {
        AX_SAFE_DELETE(_namespaces[i]);
    }

    AX_SAFE_DELETE(_variables);
}

//
// Stream simulation
//
signed char Properties::readChar()
{
    if (eof())
        return EOF;
    return _data->_bytes[(*_dataIdx)++];
}

char* Properties::readLine(char* output, int num)
{
    if (eof())
        return nullptr;

    // little optimization: avoid unneeded dereferences
    const ssize_t dataIdx = *_dataIdx;
    int i;

    for (i = 0; i < num && dataIdx + i < _data->_size; i++)
    {
        auto c = _data->_bytes[dataIdx + i];
        if (c == '\n')
            break;
        output[i] = c;
    }

    output[i] = '\0';

    // restore value
    *_dataIdx = dataIdx + i;

    return output;
}

bool Properties::seekFromCurrent(int offset)
{
    (*_dataIdx) += offset;

    return (!eof() && *_dataIdx >= 0);
}

bool Properties::eof()
{
    return (*_dataIdx >= _data->_size);
}

void Properties::skipWhiteSpace()
{
    signed char c;
    do
    {
        c = readChar();
    } while (isspace(c) && c != EOF);

    // If we are not at the end of the file, then since we found a
    // non-whitespace character, we put the cursor back in front of it.
    if (c != EOF)
    {
        if (seekFromCurrent(-1) == false)
        {
            AXLOGERROR("Failed to seek backwards one character after skipping whitespace.");
        }
    }
}

char* Properties::trimWhiteSpace(char* str)
{
    if (str == NULL)
    {
        return str;
    }

    // Trim leading space.
    while (*str != '\0' && isspace(*str))
        str++;

    // All spaces?
    if (*str == 0)
    {
        return str;
    }

    // Trim trailing space.
    char* end = str + strlen(str) - 1;
    while (end > str && isspace(*end))
        end--;

    // Write new null terminator.
    *(end + 1) = 0;

    return str;
}

void Properties::resolveInheritance(std::string_view id)
{
    // Namespaces can be defined like so:
    // "name id : parentID { }"
    // This method merges data from the parent namespace into the child.

    // Get a top-level namespace.
    Properties* derived;
    if (!id.empty())
    {
        derived = getNamespace(id);
    }
    else
    {
        derived = getNextNamespace();
    }
    while (derived)
    {
        // If the namespace has a parent ID, find the parent.
        if (!derived->_parentID.empty())
        {
            Properties* parent = getNamespace(derived->_parentID.c_str());
            if (parent)
            {
                resolveInheritance(parent->getId());

                // Copy the child.
                Properties* overrides = new Properties(*derived);

                // Delete the child's data.
                for (size_t i = 0, count = derived->_namespaces.size(); i < count; i++)
                {
                    AX_SAFE_DELETE(derived->_namespaces[i]);
                }

                // Copy data from the parent into the child.
                derived->_properties = parent->_properties;
                derived->_namespaces.clear();
                std::vector<Properties*>::const_iterator itt;
                for (const auto space : parent->_namespaces)
                {
                    derived->_namespaces.emplace_back(new Properties(*space));
                }
                derived->rewind();

                // Take the original copy of the child and override the data copied from the parent.
                derived->mergeWith(overrides);

                // Delete the child copy.
                AX_SAFE_DELETE(overrides);
            }
        }

        // Resolve inheritance within this namespace.
        derived->resolveInheritance();

        // Get the next top-level namespace and check again.
        if (id.empty())
        {
            derived = getNextNamespace();
        }
        else
        {
            derived = NULL;
        }
    }
}

void Properties::mergeWith(Properties* overrides)
{
    AXASSERT(overrides, "Invalid overrides");

    // Overwrite or add each property found in child.
    overrides->rewind();
    const char* name = overrides->getNextProperty();
    while (name)
    {
        this->setString(name, overrides->getString());
        name = overrides->getNextProperty();
    }
    this->_propertiesItr = this->_properties.end();

    // Merge all common nested namespaces, add new ones.
    Properties* overridesNamespace = overrides->getNextNamespace();
    while (overridesNamespace)
    {
        bool merged = false;

        rewind();
        Properties* derivedNamespace = getNextNamespace();
        while (derivedNamespace)
        {
            if (derivedNamespace->getNamespace() == overridesNamespace->getNamespace() &&
                derivedNamespace->getId() == overridesNamespace->getId())
            {
                derivedNamespace->mergeWith(overridesNamespace);
                merged = true;
            }

            derivedNamespace = getNextNamespace();
        }

        if (!merged)
        {
            // Add this new namespace.
            Properties* newNamespace = new Properties(*overridesNamespace);

            this->_namespaces.emplace_back(newNamespace);
            this->_namespacesItr = this->_namespaces.end();
        }

        overridesNamespace = overrides->getNextNamespace();
    }
}

const char* Properties::getNextProperty()
{
    if (_propertiesItr == _properties.end())
    {
        // Restart from the beginning
        _propertiesItr = _properties.begin();
    }
    else
    {
        // Move to the next property
        ++_propertiesItr;
    }

    return _propertiesItr == _properties.end() ? NULL : _propertiesItr->name.c_str();
}

Properties* Properties::getNextNamespace()
{
    if (_namespacesItr == _namespaces.end())
    {
        // Restart from the beginning
        _namespacesItr = _namespaces.begin();
    }
    else
    {
        ++_namespacesItr;
    }

    if (_namespacesItr != _namespaces.end())
    {
        Properties* ns = *_namespacesItr;
        return ns;
    }

    return nullptr;
}

void Properties::rewind()
{
    _propertiesItr = _properties.end();
    _namespacesItr = _namespaces.end();
}

Properties* Properties::getNamespace(std::string_view id, bool searchNames, bool recurse) const
{
    AXASSERT(!id.empty(), "invalid id");

    for (const auto& it : _namespaces)
    {
        Properties* p = it;
        if ((searchNames ? p->_namespace : p->_id) == id)
            return p;

        if (recurse)
        {
            // Search recursively.
            p = p->getNamespace(id, searchNames, true);
            if (p)
                return p;
        }
    }

    return nullptr;
}

const char* Properties::getNamespace() const
{
    return _namespace.c_str();
}

std::string_view Properties::getId() const
{
    return _id.c_str();
}

bool Properties::exists(const char* name) const
{
    if (name == NULL)
        return false;

    for (const auto& itr : _properties)
    {
        if (itr.name == name)
            return true;
    }

    return false;
}

static bool isStringNumeric(const char* str)
{
    AXASSERT(str, "invalid str");

    // The first character may be '-'
    if (*str == '-')
        str++;

    // The first character after the sign must be a digit
    if (!isdigit(*str))
        return false;
    str++;

    // All remaining characters must be digits, with a single decimal (.) permitted
    unsigned int decimalCount = 0;
    while (*str)
    {
        if (!isdigit(*str))
        {
            if (*str == '.' && decimalCount == 0)
            {
                // Max of 1 decimal allowed
                decimalCount++;
            }
            else
            {
                return false;
            }
        }
        str++;
    }
    return true;
}

Properties::Type Properties::getType(const char* name) const
{
    const char* value = getString(name);
    if (!value)
    {
        return Properties::NONE;
    }

    // Parse the value to determine the format
    unsigned int commaCount = 0;
    char* valuePtr          = const_cast<char*>(value);
    while ((valuePtr = strchr(valuePtr, ',')))
    {
        valuePtr++;
        commaCount++;
    }

    switch (commaCount)
    {
    case 0:
        return isStringNumeric(value) ? Properties::NUMBER : Properties::STRING;
    case 1:
        return Properties::VECTOR2;
    case 2:
        return Properties::VECTOR3;
    case 3:
        return Properties::VECTOR4;
    case 15:
        return Properties::MATRIX;
    default:
        return Properties::STRING;
    }
}

const char* Properties::getString(const char* name, const char* defaultValue) const
{
    char variable[256];
    const char* value = NULL;

    if (name)
    {
        // If 'name' is a variable, return the variable value
        if (isVariable(name, variable, 256))
        {
            return getVariable(variable, defaultValue);
        }

        for (const auto& itr : _properties)
        {
            if (itr.name == name)
            {
                value = itr.value.c_str();
                break;
            }
        }
    }
    else
    {
        // No name provided - get the value at the current iterator position
        if (_propertiesItr != _properties.end())
        {
            value = _propertiesItr->value.c_str();
        }
    }

    if (value)
    {
        // If the value references a variable, return the variable value
        if (isVariable(value, variable, 256))
            return getVariable(variable, defaultValue);

        return value;
    }

    return defaultValue;
}

bool Properties::setString(const char* name, const char* value)
{
    if (name)
    {
        for (auto&& itr : _properties)
        {
            if (itr.name == name)
            {
                // Update the first property that matches this name
                itr.value = value ? value : "";
                return true;
            }
        }

        // There is no property with this name, so add one
        _properties.emplace_back(Property(name, value ? value : ""));
    }
    else
    {
        // If there's a current property, set its value
        if (_propertiesItr == _properties.end())
            return false;

        _propertiesItr->value = value ? value : "";
    }

    return true;
}

bool Properties::getBool(const char* name, bool defaultValue) const
{
    const char* valueString = getString(name);
    if (valueString)
    {
        return (strcmp(valueString, "true") == 0);
    }

    return defaultValue;
}

int Properties::getInt(const char* name) const
{
    const char* valueString = getString(name);
    if (valueString)
    {
        int value;
        int scanned;
        scanned = sscanf(valueString, "%d", &value);
        if (scanned != 1)
        {
            AXLOGERROR("Error attempting to parse property '%s' as an integer.", name);
            return 0;
        }
        return value;
    }

    return 0;
}

float Properties::getFloat(const char* name) const
{
    const char* valueString = getString(name);
    if (valueString)
    {
        float value;
        int scanned;
        scanned = sscanf(valueString, "%f", &value);
        if (scanned != 1)
        {
            AXLOGERROR("Error attempting to parse property '%s' as a float.", name);
            return 0.0f;
        }
        return value;
    }

    return 0.0f;
}

bool Properties::getMat4(const char* name, Mat4* out) const
{
    AXASSERT(out, "Invalid out");

    const char* valueString = getString(name);
    if (valueString)
    {
        float m[16];
        int scanned;
        scanned = sscanf(valueString, "%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f", &m[0], &m[1], &m[2], &m[3],
                         &m[4], &m[5], &m[6], &m[7], &m[8], &m[9], &m[10], &m[11], &m[12], &m[13], &m[14], &m[15]);

        if (scanned != 16)
        {
            AXLOGERROR("Error attempting to parse property '%s' as a matrix.", name);
            out->setIdentity();
            return false;
        }

        out->set(m);
        return true;
    }

    out->setIdentity();
    return false;
}

bool Properties::getVec2(const char* name, Vec2* out) const
{
    return parseVec2(getString(name), out);
}

bool Properties::getVec3(const char* name, Vec3* out) const
{
    return parseVec3(getString(name), out);
}

bool Properties::getVec4(const char* name, Vec4* out) const
{
    return parseVec4(getString(name), out);
}

bool Properties::getQuaternionFromAxisAngle(const char* name, Quaternion* out) const
{
    return parseAxisAngle(getString(name), out);
}

bool Properties::getColor(const char* name, Vec3* out) const
{
    return parseColor(getString(name), out);
}

bool Properties::getColor(const char* name, Vec4* out) const
{
    return parseColor(getString(name), out);
}

bool Properties::getPath(const char* name, std::string* path) const
{
    AXASSERT(name && path, "Invalid name or path");
    const char* valueString = getString(name);
    if (valueString)
    {
        if (FileUtils::getInstance()->isFileExist(valueString))
        {
            path->assign(valueString);
            return true;
        }
        else
        {
            const Properties* prop = this;
            while (prop != NULL)
            {
                // Search for the file path relative to the bundle file
                const std::string* dirPath = prop->_dirPath;
                if (dirPath != NULL && !dirPath->empty())
                {
                    std::string relativePath = *dirPath;
                    relativePath.append(valueString);
                    if (FileUtils::getInstance()->isFileExist(relativePath))
                    {
                        path->assign(relativePath);
                        return true;
                    }
                }
                prop = prop->_parent;
            }
        }
    }
    return false;
}

const char* Properties::getVariable(const char* name, const char* defaultValue) const
{
    if (name == NULL)
        return defaultValue;

    // Search for variable in this Properties object
    if (_variables)
    {
        for (size_t i = 0, count = _variables->size(); i < count; ++i)
        {
            Property& prop = (*_variables)[i];
            if (prop.name == name)
                return prop.value.c_str();
        }
    }

    // Search for variable in parent Properties
    return _parent ? _parent->getVariable(name, defaultValue) : defaultValue;
}

void Properties::setVariable(const char* name, const char* value)
{
    AXASSERT(name, "Invalid name");

    Property* prop = NULL;

    // Search for variable in this Properties object and parents
    Properties* current = const_cast<Properties*>(this);
    while (current)
    {
        if (current->_variables)
        {
            for (size_t i = 0, count = current->_variables->size(); i < count; ++i)
            {
                Property* p = &(*current->_variables)[i];
                if (p->name == name)
                {
                    prop = p;
                    break;
                }
            }
        }
        current = current->_parent;
    }

    if (prop)
    {
        // Found an existing property, set it
        prop->value = value ? value : "";
    }
    else
    {
        // Add a new variable with this name
        if (!_variables)
            _variables = new std::vector<Property>();
        _variables->emplace_back(name, value ? std::string_view{value} : ""sv);
    }
}

Properties* Properties::clone()
{
    Properties* p = new Properties();

    p->_namespace     = _namespace;
    p->_id            = _id;
    p->_parentID      = _parentID;
    p->_properties    = _properties;
    p->_propertiesItr = p->_properties.end();
    p->setDirectoryPath(_dirPath);

    for (size_t i = 0, count = _namespaces.size(); i < count; i++)
    {
        AXASSERT(_namespaces[i], "Invalid namespace");
        Properties* child = _namespaces[i]->clone();
        p->_namespaces.emplace_back(child);
        child->_parent = p;
    }
    p->_namespacesItr = p->_namespaces.end();

    return p;
}

void Properties::setDirectoryPath(const std::string* path)
{
    if (path)
    {
        setDirectoryPath(*path);
    }
    else
    {
        AX_SAFE_DELETE(_dirPath);
    }
}

void Properties::setDirectoryPath(std::string_view path)
{
    if (_dirPath == NULL)
    {
        _dirPath = new std::string(path);
    }
    else
    {
        _dirPath->assign(path);
    }
}

void calculateNamespacePath(std::string_view urlString,
                            std::string& fileString,
                            std::vector<std::string_view>& namespacePath)
{
    // If the url references a specific namespace within the file,
    // calculate the full namespace path to the final namespace.
    size_t loc = urlString.rfind('#');
    if (loc != std::string::npos)
    {
        fileString               = urlString.substr(0, loc);
        auto namespacePathString = urlString.substr(loc + 1);
        while ((loc = namespacePathString.find('/')) != std::string::npos)
        {
            namespacePath.emplace_back(namespacePathString.substr(0, loc));
            namespacePathString = namespacePathString.substr(loc + 1);
        }
        namespacePath.emplace_back(namespacePathString);
    }
    else
    {
        fileString = urlString;
    }
}

Properties* getPropertiesFromNamespacePath(Properties* properties, const std::vector<std::string_view>& namespacePath)
{
    // If the url references a specific namespace within the file,
    // return the specified namespace or notify the user if it cannot be found.
    if (!namespacePath.empty())
    {
        size_t size = namespacePath.size();
        properties->rewind();
        Properties* iter = properties->getNextNamespace();
        for (size_t i = 0; i < size;)
        {
            while (true)
            {
                if (iter == NULL)
                {
                    AXLOGWARN("Failed to load properties object from url.");
                    return nullptr;
                }

                if (iter->getId() == namespacePath[i])
                {
                    if (i != size - 1)
                    {
                        properties = iter->getNextNamespace();
                        iter       = properties;
                    }
                    else
                        properties = iter;

                    i++;
                    break;
                }

                iter = properties->getNextNamespace();
            }
        }

        return properties;
    }
    else
        return properties;
}

bool Properties::parseVec2(const char* str, Vec2* out)
{
    if (str)
    {
        float x, y;
        if (sscanf(str, "%f,%f", &x, &y) == 2)
        {
            if (out)
                out->set(x, y);
            return true;
        }
        else
        {
            AXLOGWARN("Error attempting to parse property as a two-dimensional vector: %s", str);
        }
    }

    if (out)
        out->set(0.0f, 0.0f);
    return false;
}

bool Properties::parseVec3(const char* str, Vec3* out)
{
    if (str)
    {
        float x, y, z;
        if (sscanf(str, "%f,%f,%f", &x, &y, &z) == 3)
        {
            if (out)
                out->set(x, y, z);
            return true;
        }
        else
        {
            AXLOGWARN("Error attempting to parse property as a three-dimensional vector: %s", str);
        }
    }

    if (out)
        out->set(0.0f, 0.0f, 0.0f);
    return false;
}

bool Properties::parseVec4(const char* str, Vec4* out)
{
    if (str)
    {
        float x, y, z, w;
        if (sscanf(str, "%f,%f,%f,%f", &x, &y, &z, &w) == 4)
        {
            if (out)
                out->set(x, y, z, w);
            return true;
        }
        else
        {
            AXLOGWARN("Error attempting to parse property as a four-dimensional vector: %s", str);
        }
    }

    if (out)
        out->set(0.0f, 0.0f, 0.0f, 0.0f);
    return false;
}

bool Properties::parseAxisAngle(const char* str, Quaternion* out)
{
    if (str)
    {
        float x, y, z, theta;
        if (sscanf(str, "%f,%f,%f,%f", &x, &y, &z, &theta) == 4)
        {
            if (out)
                out->set(Vec3(x, y, z), MATH_DEG_TO_RAD(theta));
            return true;
        }
        else
        {
            AXLOGWARN("Error attempting to parse property as an axis-angle rotation: %s", str);
        }
    }

    if (out)
        out->set(0.0f, 0.0f, 0.0f, 1.0f);
    return false;
}

bool Properties::parseColor(const char* str, Vec3* out)
{
    if (str)
    {
        if (strlen(str) == 7 && str[0] == '#')
        {
            // Read the string into an int as hex.
            unsigned int color;
            if (sscanf(str + 1, "%x", &color) == 1)
            {
                if (out)
                    out->set(Vec3::fromColor(color));
                return true;
            }
            else
            {
                // Invalid format
                AXLOGWARN("Error attempting to parse property as an RGB color: %s", str);
            }
        }
        else
        {
            // Not a color string.
            AXLOGWARN("Error attempting to parse property as an RGB color (not specified as a color string): %s", str);
        }
    }

    if (out)
        out->set(0.0f, 0.0f, 0.0f);
    return false;
}

bool Properties::parseColor(const char* str, Vec4* out)
{
    if (str)
    {
        if (strlen(str) == 9 && str[0] == '#')
        {
            // Read the string into an int as hex.
            unsigned int color;
            if (sscanf(str + 1, "%x", &color) == 1)
            {
                if (out)
                    out->set(Vec4::fromColor(color));
                return true;
            }
            else
            {
                // Invalid format
                AXLOGWARN("Error attempting to parse property as an RGBA color: %s", str);
            }
        }
        else
        {
            // Not a color string.
            AXLOGWARN("Error attempting to parse property as an RGBA color (not specified as a color string): %s", str);
        }
    }

    if (out)
        out->set(0.0f, 0.0f, 0.0f, 0.0f);
    return false;
}