<commit_before>// For conditions of distribution and use, see copyright notice in LICENSE

#include "StableHeaders.h"
#include "Framework.h"
#include "ConfigAPI.h"
#include "LoggingFunctions.h"

#include <Context.h>
#include <File.h>
#include <FileSystem.h>

using namespace Urho3D;

namespace Tundra
{

String ConfigAPI::FILE_FRAMEWORK        = "tundra";
String ConfigAPI::SECTION_FRAMEWORK     = "framework";
String ConfigAPI::SECTION_SERVER        = "server";
String ConfigAPI::SECTION_CLIENT        = "client";
String ConfigAPI::SECTION_RENDERING     = "rendering";
String ConfigAPI::SECTION_GRAPHICS      = "graphics";
String ConfigAPI::SECTION_UI            = "ui";
String ConfigAPI::SECTION_SOUND         = "sound";

/* Prepare string for config usage. Removes spaces from end and start, replaces mid string spaces with '_' and forces to lower case.
   @param str String to prepare.
   @param isKey If true "[" and "]" are replaces by "(" and ")".
   They are not allowed in keys as the parsing code will mistake them as sections */
void PrepareString(String &str, bool isKey = false)
{
    if (!str.Empty())
    {
        str = str.Trimmed().ToLower();   // Remove spaces from start/end, force to lower case
        str = str.Replaced(" ", "_");    // Replace ' ' with '_', so we don't get %20 in the config as spaces
        str = str.Replaced("=", "_");    // Replace '=' with '_', as = has special meaning in .ini files
        str = str.Replaced("/", "_");    // Replace '/' with '_', as / has a special meaning in .ini file keys/sections. Also file name cannot have a forward slash.
        if (isKey)
        {
            // Keys cannot contain the "[" or "]" tokens so they wont be mistaken as sections.
            str = str.Replaced("[", "(");
            str = str.Replaced("]", ")");
        }
    }
}

VariantType GetVariantTypeFromString(const String& in)
{
    if (in == "true" || in == "false")
        return VAR_BOOL;
    else if ((in.Length() && IsDigit(in[0])) || (in.Length() >= 2 && in[0] == '-' && IsDigit(in[1])))
    {
        uint num = in.Trimmed().Split(' ').Size();
        if (in.Contains('.'))
            return (num <= 1 ? VAR_FLOAT : (num == 2 ? VAR_VECTOR2 : (num == 3 ? VAR_VECTOR3 : VAR_VECTOR4)));
        else
            return (num <= 1 ? VAR_INT : VAR_INTVECTOR2);
    }    
    else
        return VAR_STRING;
}

// ConfigFile

bool ConfigFile::HasKey(const String &section, const String &key) const
{
    HashMap<String, ConfigSection>::ConstIterator iter = sections_.Find(section);
    if (iter != sections_.End())
        return iter->second_.keys.Contains(key);
    return false;
}

void ConfigFile::Set(const String &section, const String &key, const Variant &value)
{
    if (section.Empty() || key.Empty())
        return;

    sections_[section].keys[key] = value;
    modified_ = true;
}

void ConfigFile::Set(const String section, const HashMap<String, Variant> &values)
{
    if (section.Empty() || values.Empty())
        return;

    ConfigSection &s = sections_[section];
    for(HashMap<String, Variant>::ConstIterator iter = values.Begin(); iter != values.End(); ++iter)
    {
        String key = iter->first_;
        PrepareString(key, true);
        s.keys[key] = iter->second_;
    }
    modified_ = true;
}

Variant ConfigFile::Get(const String &section, const String &key, const Variant &defaultValue)
{
    if (HasKey(section, key))
        return sections_[section].keys[key];
    return defaultValue;
}

void ConfigFile::Load(Context* ctx, const String& fileName)
{
    // Already loaded to memory from disk?
    if (loaded_)
        return;
    loaded_ = true;

    if (!ctx->GetSubsystem<FileSystem>()->FileExists(fileName))
        return;
    SharedPtr<File> file(new File(ctx, fileName, FILE_READ));
    if (!file->IsOpen())
        return;

    String currentSection;
    while (!file->IsEof())
    {
        String line = file->ReadLine().Trimmed();
        if (line.Length() && line[0] == ('['))
            currentSection = line.Substring(1, line.Length() - 2);
        else
        {
            Vector<String> parts = line.Split('=');
            if (parts.Size() == 2)
                sections_[currentSection].keys[parts[0]].FromString(GetVariantTypeFromString(parts[1]), parts[1]);
        }
    }
}

void ConfigFile::Save(Context* ctx, const String& fileName)
{
    // No in memory changes done. Skip saving to disk.
    if (!modified_)
        return;

    SharedPtr<File> file(new File(ctx, fileName, FILE_WRITE));
    if (!file->IsOpen())
        return;

    for (HashMap<String, ConfigSection>::ConstIterator i = sections_.Begin(); i != sections_.End(); ++i)
    {
        String sectionStr = "[" + i->first_ + "]";
        file->WriteLine(sectionStr);
        for (HashMap<String, Variant>::ConstIterator j = i->second_.keys.Begin(); j != i->second_.keys.End(); ++j)
            file->WriteLine(j->first_ + "=" + j->second_.ToString());
        file->WriteLine("");
    }
    modified_ = false;
}

// ConfigAPI

ConfigAPI::ConfigAPI(Framework *framework) :
    Object(framework->GetContext()),
    owner(framework)
{
}

ConfigAPI::~ConfigAPI()
{
    for(HashMap<String, ConfigFile>::Iterator iter = configFiles_.Begin(); iter != configFiles_.End(); ++iter)
        iter->second_.Save(GetContext(), GetFilePath(iter->first_));
}

void ConfigAPI::PrepareDataFolder(String configFolder)
{
    FileSystem* fs = GetSubsystem<FileSystem>();
    String configPath = owner->ParseWildCardFilename(configFolder.Trimmed());
    
    if (!fs->DirExists(configPath))
    {
        bool success = fs->CreateDir(configPath);
        if (!success)<commit_msg>ConfigAPI: Write Variant type to disk. This allows us to read any variant type instead of the earlier 'hack' to detect it from the value. Fix bug so that value can contain '=' character. More trimming to get valid values if humans format/edit the configs.<commit_after>// For conditions of distribution and use, see copyright notice in LICENSE

#include "StableHeaders.h"
#include "Framework.h"
#include "ConfigAPI.h"
#include "LoggingFunctions.h"

#include <Context.h>
#include <File.h>
#include <FileSystem.h>

using namespace Urho3D;

namespace Tundra
{

String ConfigAPI::FILE_FRAMEWORK        = "tundra";
String ConfigAPI::SECTION_FRAMEWORK     = "framework";
String ConfigAPI::SECTION_SERVER        = "server";
String ConfigAPI::SECTION_CLIENT        = "client";
String ConfigAPI::SECTION_RENDERING     = "rendering";
String ConfigAPI::SECTION_GRAPHICS      = "graphics";
String ConfigAPI::SECTION_UI            = "ui";
String ConfigAPI::SECTION_SOUND         = "sound";

/* Prepare string for config usage. Removes spaces from end and start, replaces mid string spaces with '_' and forces to lower case.
   @param str String to prepare.
   @param isKey If true "[" and "]" are replaces by "(" and ")".
   They are not allowed in keys as the parsing code will mistake them as sections */
void PrepareString(String &str, bool isKey = false)
{
    if (!str.Empty())
    {
        str = str.Trimmed().ToLower();   // Remove spaces from start/end, force to lower case
        str = str.Replaced(" ", "_");    // Replace ' ' with '_', so we don't get %20 in the config as spaces
        str = str.Replaced("=", "_");    // Replace '=' with '_', as = has special meaning in .ini files
        str = str.Replaced("/", "_");    // Replace '/' with '_', as / has a special meaning in .ini file keys/sections. Also file name cannot have a forward slash.
        if (isKey)
        {
            // Keys cannot contain the "[" or "]" tokens so they wont be mistaken as sections.
            str = str.Replaced("[", "(");
            str = str.Replaced("]", ")");
        }
    }
}

VariantType GetVariantTypeFromString(String &value)
{
    /* Empty values are ok, but if not prefixed with
       type information it is an invalid config entry.
       Type info is at minimum "@1" */
    if (value.Length() < 2 || value[0] != '@')
        return VAR_NONE;

    // "@<VAR_TYPE_AS_INT> <value>
    int valueLength = (!IsDigit(value[2]) ? 1 : 2);
    int type = ToInt(value.Substring(1, valueLength));
    value = value.Substring(1+valueLength).Trimmed();
    return (type > VAR_NONE && type < MAX_VAR_TYPES ? static_cast<VariantType>(type) : VAR_NONE);
}

// ConfigFile

bool ConfigFile::HasKey(const String &section, const String &key) const
{
    HashMap<String, ConfigSection>::ConstIterator iter = sections_.Find(section);
    if (iter != sections_.End())
        return iter->second_.keys.Contains(key);
    return false;
}

void ConfigFile::Set(const String &section, const String &key, const Variant &value)
{
    if (section.Empty() || key.Empty())
        return;

    sections_[section].keys[key] = value;
    modified_ = true;
}

void ConfigFile::Set(const String section, const HashMap<String, Variant> &values)
{
    if (section.Empty() || values.Empty())
        return;

    ConfigSection &s = sections_[section];
    for(HashMap<String, Variant>::ConstIterator iter = values.Begin(); iter != values.End(); ++iter)
    {
        String key = iter->first_;
        PrepareString(key, true);
        s.keys[key] = iter->second_;
    }
    modified_ = true;
}

Variant ConfigFile::Get(const String &section, const String &key, const Variant &defaultValue)
{
    if (HasKey(section, key))
        return sections_[section].keys[key];
    return defaultValue;
}

void ConfigFile::Load(Context* ctx, const String& fileName)
{
    // Already loaded to memory from disk?
    if (loaded_)
        return;
    loaded_ = true;

    if (!ctx->GetSubsystem<FileSystem>()->FileExists(fileName))
        return;
    SharedPtr<File> file(new File(ctx, fileName, FILE_READ));
    if (!file->IsOpen())
        return;

    String currentSection;
    while (!file->IsEof())
    {
        String line = file->ReadLine().Trimmed();
        if (line.Length() == 0)
            continue;

        if (line[0] == ('['))
            currentSection = line.Substring(1, line.Length() - 2);
        else
        {
            // Find first '='. It is allowed characted in value of eg. the String type.
            uint splitIndex = line.Find('=');
            if (splitIndex != String::NPOS)
            {
                // Trim the parts if human has edited the config width indentation etc.
                String key = line.Substring(0, splitIndex).Trimmed();
                String value = line.Substring(splitIndex+1).Trimmed();
                // Will remove the type information from value.
                VariantType type = GetVariantTypeFromString(value);

                if (type > VAR_NONE && type < MAX_VAR_TYPES)
                    sections_[currentSection].keys[key].FromString(type, value);
                else
                    LogError(Urho3D::ToString("ConfigAPI: Failed to determine value type for '%s' in section '%s' of '%s'.",
                        value.CString(), currentSection.CString(), fileName.CString()));
            }
            else
                LogError(Urho3D::ToString("ConfigAPI: Invalid line '%s' in section '%s' of '%s'.",
                    line.CString(), currentSection.CString(), fileName.CString()));
        }
    }
}

void ConfigFile::Save(Context* ctx, const String& fileName)
{
    // No in memory changes done. Skip saving to disk.
    if (!modified_)
        return;

    SharedPtr<File> file(new File(ctx, fileName, FILE_WRITE));
    if (!file->IsOpen())
        return;

    for (HashMap<String, ConfigSection>::ConstIterator i = sections_.Begin(); i != sections_.End(); ++i)
    {
        String sectionStr = "[" + i->first_ + "]";
        file->WriteLine(sectionStr);
        for (HashMap<String, Variant>::ConstIterator j = i->second_.keys.Begin(); j != i->second_.keys.End(); ++j)
        {
            String value = j->second_.ToString();
            file->WriteLine(Urho3D::ToString("%s=@%d %s", j->first_ .CString(), static_cast<int>(j->second_.GetType()), value.CString()));
        }
        file->WriteLine("");
    }
    modified_ = false;
}

// ConfigAPI

ConfigAPI::ConfigAPI(Framework *framework) :
    Object(framework->GetContext()),
    owner(framework)
{
}

ConfigAPI::~ConfigAPI()
{
    for(HashMap<String, ConfigFile>::Iterator iter = configFiles_.Begin(); iter != configFiles_.End(); ++iter)
        iter->second_.Save(GetContext(), GetFilePath(iter->first_));
}

void ConfigAPI::PrepareDataFolder(String configFolder)
{
    FileSystem* fs = GetSubsystem<FileSystem>();
    String configPath = owner->ParseWildCardFilename(configFolder.Trimmed());
    
    if (!fs->DirExists(configPath))
    {
        bool success = fs->CreateDir(configPath);
        if (!success)
{'fraction_non_alphanumeric': 0.1430487612434906, 'fraction_numerical': 0.00331387091683762, 'mean_word_length': 2.869047619047619, 'pattern_counts': {'":': 0, '<': 39, '<?xml version=': 0, '>': 84, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4317760', 'n_tokens_mistral': 3770, 'n_tokens_neox': 3698, 'n_words': 1044}