/*
 * Copyright (c) Contributors to the Open 3D Engine Project.
 * For complete copyright and license terms please see the LICENSE at the root of this distribution.
 *
 * SPDX-License-Identifier: Apache-2.0 OR MIT
 *
 */

#include <EditorDefs.h>
#include "ProjectSettingsSerialization.h"

#include "PlatformSettings_common.h"

#include <AzToolsFramework/UI/PropertyEditor/InstanceDataHierarchy.h>
#include <AzToolsFramework/UI/PropertyEditor/PropertyEditorAPI.h>

// Needed to overwrite ios icons and launchscreens
#include <Util/FileUtil.h>

namespace ProjectSettingsTool
{
    const char* stringStr = "string";
    const char* arrayStr = "array";
    const char* trueStr = "true";
    const char* noDocumentError = "No json or xml document to use for Project Settings Tool serialization.";

    Serializer::Serializer(AzToolsFramework::InstanceDataNode* root)
        : m_root(root)
        , m_jsonDoc(nullptr)
        , m_jsonRoot(nullptr)
        , m_idString(AZ::SerializeTypeInfo<AZStd::string>::GetUuid())
        , m_idInt(AZ::SerializeTypeInfo<int>::GetUuid())
        , m_idBool(AZ::SerializeTypeInfo<bool>::GetUuid())
    {}

    Serializer::Serializer(AzToolsFramework::InstanceDataNode* root, rapidjson::Document* doc, rapidjson::Value* jsonRoot)
        : Serializer(root)
    {
        SetDocumentRoot(doc);
        if (jsonRoot != nullptr)
        {
            SetJsonRoot(jsonRoot);
        }
        else
        {
            SetJsonRoot(doc);
        }
    }

    Serializer::Serializer(AzToolsFramework::InstanceDataNode* root, AZStd::unique_ptr<PlistDictionary> dict)
        : Serializer(root)
    {
        SetDocumentRoot(AZStd::move(dict));
    }

    void Serializer::SetDocumentRoot(rapidjson::Document* doc)
    {
        m_jsonDoc = doc;
    }

    void Serializer::SetJsonRoot(rapidjson::Value* jsonRoot)
    {
        m_jsonRoot = jsonRoot;
    }

    void Serializer::SetDocumentRoot(AZStd::unique_ptr<PlistDictionary> dict)
    {
        m_plistDict = AZStd::move(dict);
    }

    bool Serializer::UiEqualToSettings() const
    {
        if (m_jsonRoot != nullptr)
        {
            return UiEqualToJson(m_jsonRoot);
        }
        else if (m_plistDict)
        {
            return UiEqualToPlist(m_root);
        }
        else
        {
            AZ_Assert(false, noDocumentError);
            return false;
        }
    }

    void Serializer::LoadFromSettings()
    {
        if (m_jsonRoot != nullptr)
        {
            LoadFromSettings(m_jsonRoot);
        }
        else if (m_plistDict)
        {
            LoadFromSettings(m_root);
        }
        else
        {
            AZ_Assert(false, noDocumentError);
        }
    }

    void Serializer::SaveToSettings()
    {
        if (m_jsonRoot != nullptr)
        {
            SaveToSettings(m_jsonRoot);
        }
        else if (m_plistDict)
        {
            SaveToSettings(m_root);
        }
        else
        {
            AZ_Assert(false, noDocumentError);
        }
    }

    bool Serializer::UiEqualToJson(rapidjson::Value* root) const
    {
        return UiEqualToJson(root, m_root);
    }

    void Serializer::LoadFromSettings(rapidjson::Value* root)
    {
        LoadFromSettings(root, m_root);
    }

    void Serializer::SaveToSettings(rapidjson::Value* root)
    {
        // This value needs to be an object to add members
        if (!root->IsObject())
        {
            root->SetObject();
        }
        SaveToSettings(root, m_root);
    }

    bool Serializer::UiEqualToJson(rapidjson::Value* root, AzToolsFramework::InstanceDataNode* node) const
    {
        const AZ::SerializeContext::ClassData* baseMeta = node->GetClassMetadata();
        if (baseMeta)
        {
            for (AzToolsFramework::InstanceDataNode& childNode : node->GetChildren())
            {
                auto childMeta = childNode.GetElementMetadata();
                if (childMeta)
                {
                    bool noDocElement = true;
                    rapidjson::Value::MemberIterator jsonMember;
                    const char* propertyName = childMeta->m_name;

                    // If there is a json object try to find the member
                    if (root != nullptr)
                    {
                        jsonMember = root->FindMember(propertyName);

                        if (jsonMember != root->MemberEnd())
                        {
                            noDocElement = false;
                        }
                    }

                    AZ::Uuid type = childMeta->m_typeId;
                    if (m_idString == type)
                    {
                        AZStd::string uiValue;
                        childNode.Read(uiValue);

                        if (!uiValue.empty() && !noDocElement)
                        {
                            if (jsonMember->value.IsString())
                            {
                                if (uiValue != jsonMember->value.GetString())
                                {
                                    return false;
                                }
                            }
                            else
                            {
                                return false;
                            }
                        }
                        else if (uiValue.empty() != noDocElement)
                        {
                            return false;
                        }
                    }
                    else if (m_idInt == type)
                    {
                        int uiValue;
                        childNode.Read(uiValue);

                        if (!noDocElement)
                        {
                            if (jsonMember->value.IsInt())
                            {
                                if (uiValue != jsonMember->value.GetInt())
                                {
                                    return false;
                                }
                            }
                            else
                            {
                                return false;
                            }
                        }
                        else
                        {
                            return false;
                        }
                    }
                    else if (m_idBool == type)
                    {
                        bool uiValue;
                        childNode.Read(uiValue);

                        if (uiValue)
                        {
                            if (!noDocElement)
                            {
                                if (jsonMember->value.IsString())
                                {
                                    AZStd::string expectedJsonValue = trueStr;
                                    if (expectedJsonValue != jsonMember->value.GetString())
                                    {
                                        return false;
                                    }
                                }
                                else
                                {
                                    return false;
                                }
                            }
                            else
                            {
                                return false;
                            }
                        }
                        else if (!noDocElement)
                        {
                            return false;
                        }
                    }
                    // Should be a class with members instead of base data type
                    else
                    {
                        rapidjson::Value* jsonNode = nullptr;
                        if (!noDocElement)
                        {
                            jsonNode = &jsonMember->value;
                        }
                        // Drill into class
                        if (!UiEqualToJson(jsonNode, &childNode))
                        {
                            return false;
                        }
                    }
                }
            }
        }

        return true;
    }

    bool Serializer::UiEqualToPlist(AzToolsFramework::InstanceDataNode* node) const
    {
        const AZ::SerializeContext::ClassData* baseMeta = node->GetClassMetadata();
        if (baseMeta)
        {
            for (AzToolsFramework::InstanceDataNode& childNode : node->GetChildren())
            {
                auto childMeta = childNode.GetElementMetadata();
                if (childMeta)
                {
                    bool noDocElement = true;
                    const char* propertyName = childMeta->m_name;
                    XmlNode* plistNode = m_plistDict->GetPropertyValueNode(propertyName);

                    if (plistNode != nullptr)
                    {
                        noDocElement = false;
                    }

                    AZ::Uuid type = childMeta->m_typeId;
                    if (m_idString == type)
                    {
                        AZStd::string uiValue;
                        childNode.Read(uiValue);

                        if (!uiValue.empty() && !noDocElement)
                        {
                            if (AZStd::string(plistNode->name()) == stringStr)
                            {
                                if (uiValue != plistNode->value())
                                {
                                    return false;
                                }
                            }
                            else
                            {
                                return false;
                            }
                        }
                        else if (uiValue.empty() != noDocElement)
                        {
                            return false;
                        }
                    }
                    else if (m_idBool == type)
                    {
                        bool uiValue;
                        childNode.Read(uiValue);

                        if (uiValue)
                        {
                            if (!noDocElement)
                            {
                                if (AZStd::string(plistNode->name()) != trueStr)
                                {
                                    return false;
                                }
                            }
                            else
                            {
                                return false;
                            }
                        }
                        else if (!noDocElement)
                        {
                            return false;
                        }
                    }
                    else if (AZStd::string(childNode.GetClassMetadata()->m_name) == "IosOrientations")
                    {
                        // Enter even if nullptr to make sure all values should be false
                        if (plistNode == nullptr || AZStd::string(plistNode->name()) == arrayStr)
                        {
                            if (!UiEqualToPlistArray(plistNode, &childNode))
                            {
                                return false;
                            }
                        }
                        else
                        {
                            return false;
                        }
                    }
                    else
                    {
                        if (!UiEqualToPlistImages(&childNode))
                        {
                            return false;
                        }
                    }
                }
            }
        }

        return true;
    }

    bool Serializer::UiEqualToPlistArray(XmlNode* array, AzToolsFramework::InstanceDataNode* node) const
    {
        const AZ::SerializeContext::ClassData* baseMeta = node->GetClassMetadata();
        if (baseMeta)
        {
            for (AzToolsFramework::InstanceDataNode& childNode : node->GetChildren())
            {
                const AZ::SerializeContext::ClassElement* childMeta = childNode.GetElementMetadata();
                if (childMeta)
                {
                    AZ::Uuid type = childMeta->m_typeId;
                    if (m_idBool == type)
                    {
                        const char* propertyName = childMeta->m_name;
                        bool uiValue;
                        childNode.Read(uiValue);

                        bool found = false;
                        if (array != nullptr)
                        {
                            for (XmlNode* plistNode = array->first_node(); plistNode != nullptr; plistNode = plistNode->next_sibling())
                            {
                                if (AZStd::string(plistNode->value()) == propertyName)
                                {
                                    if (!uiValue)
                                    {
                                        return false;
                                    }
                                    else
                                    {
                                        found = true;
                                    }
                                    break;
                                }
                            }
                        }
                        if (uiValue && !found)
                        {
                            return false;
                        }
                    }
                }
            }
        }

        return true;
    }

    bool Serializer::UiEqualToPlistImages(AzToolsFramework::InstanceDataNode* node) const
    {
        const AZ::SerializeContext::ClassData* baseMeta = node->GetClassMetadata();
        if (baseMeta)
        {
            for (AzToolsFramework::InstanceDataNode& childNode : node->GetChildren())
            {
                auto childMeta = childNode.GetElementMetadata();
                if (childMeta)
                {
                    AZ::Uuid type = childMeta->m_typeId;

                    if (m_idString == type)
                    {
                        AZStd::string uiValue;
                        childNode.Read(uiValue);

                        if (!uiValue.empty())
                        {
                            return false;
                        }
                    }
                }
            }
        }

        return true;
    }

    void Serializer::LoadFromSettings(rapidjson::Value* root, AzToolsFramework::InstanceDataNode* node)
    {
        const AZ::SerializeContext::ClassData* baseMeta = node->GetClassMetadata();
        if (baseMeta)
        {
            for (AzToolsFramework::InstanceDataNode& childNode : node->GetChildren())
            {
                auto childMeta = childNode.GetElementMetadata();
                if (childMeta)
                {
                    const char* propertyName = childMeta->m_name;
                    AZ::Uuid type = childMeta->m_typeId;

                    if (root != nullptr)
                    {
                        rapidjson::Value::MemberIterator jsonMember = root->FindMember(propertyName);

                        if (jsonMember != root->MemberEnd())
                        {
                            rapidjson::Value& jsonNode = jsonMember->value;

                            if (m_idString == type)
                            {
                                if (jsonNode.IsString())
                                {
                                    AZStd::string jsonValue = jsonNode.GetString();
                                    childNode.Write(jsonValue);
                                }
                            }
                            else if (m_idInt == type)
                            {
                                if (jsonNode.IsInt())
                                {
                                    int jsonValue = jsonNode.GetInt();
                                    childNode.Write(jsonValue);
                                }
                            }
                            else if (m_idBool == type)
                            {
                                if (jsonNode.IsString())
                                {
                                    bool value = false;
                                    AZStd::string jsonValue = jsonNode.GetString();

                                    if (jsonValue == trueStr)
                                    {
                                        value = true;
                                    }
                                    childNode.Write(value);
                                }
                            }
                            // Should be a class with members instead of base data type
                            else
                            {
                                if (jsonNode.IsObject())
                                {
                                    // Drill into class
                                    LoadFromSettings(&jsonNode, &childNode);
                                }
                            }
                        }
                        else
                        {
                            SetDefaults(childNode, type);
                        }
                    }
                    else
                    {
                        SetDefaults(childNode, type);
                    }
                }
            }
        }
    }

    void Serializer::SaveToSettings(rapidjson::Value* root, AzToolsFramework::InstanceDataNode* node)
    {
        const AZ::SerializeContext::ClassData* baseMeta = node->GetClassMetadata();
        if (baseMeta)
        {
            for (AzToolsFramework::InstanceDataNode& childNode : node->GetChildren())
            {
                auto childMeta = childNode.GetElementMetadata();
                if (childMeta)
                {
                    const char* propertyName = childMeta->m_name;
                    rapidjson::Value::MemberIterator jsonMember = root->FindMember(propertyName);

                    if (jsonMember == root->MemberEnd())
                    {
                        root->AddMember(rapidjson::Value(propertyName, m_jsonDoc->GetAllocator())
                            , rapidjson::Value(rapidjson::Type::kNullType)
                            , m_jsonDoc->GetAllocator());
                        jsonMember = root->FindMember(propertyName);
                    }
                    rapidjson::Value& jsonNode = jsonMember->value;

                    AZ::Uuid type = childMeta->m_typeId;
                    if (m_idString == type)
                    {
                        AZStd::string uiValue;
                        childNode.Read(uiValue);

                        if (!uiValue.empty())
                        {
                            jsonNode.SetString(uiValue.data(), m_jsonDoc->GetAllocator());
                        }
                        else
                        {
                            root->RemoveMember(propertyName);
                        }
                    }
                    else if (m_idInt == type)
                    {
                        int uiValue;
                        childNode.Read(uiValue);
                        jsonNode.SetInt(uiValue);
                    }
                    else if (m_idBool == type)
                    {
                        bool uiValue;
                        childNode.Read(uiValue);

                        if (uiValue)
                        {
                            jsonNode.SetString(trueStr, m_jsonDoc->GetAllocator());
                        }
                        else
                        {
                            root->RemoveMember(propertyName);
                        }
                    }
                    // Should be a class with members instead of base data type
                    else
                    {
                        // This value needs to be an object to add members
                        if (!jsonNode.IsObject())
                        {
                            jsonNode.SetObject();
                        }
                        // Drill into class
                        SaveToSettings(&jsonNode, &childNode);

                        if (jsonNode.ObjectEmpty())
                        {
                            root->RemoveMember(propertyName);
                        }
                    }
                }
            }
        }
    }

    void Serializer::LoadFromSettings(AzToolsFramework::InstanceDataNode* node)
    {
        const AZ::SerializeContext::ClassData* baseMeta = node->GetClassMetadata();
        if (baseMeta)
        {
            for (AzToolsFramework::InstanceDataNode& childNode : node->GetChildren())
            {
                const AZ::SerializeContext::ClassElement* childMeta = childNode.GetElementMetadata();
                if (childMeta)
                {
                    const char* propertyName = childMeta->m_name;
                    XmlNode* plistNode = m_plistDict->GetPropertyValueNode(propertyName);
                    AZ::Uuid type = childMeta->m_typeId;

                    if (plistNode != nullptr)
                    {
                        if (m_idString == type)
                        {
                            if (AZStd::string(plistNode->name()) == stringStr)
                            {
                                AZStd::string plistValue = plistNode->value();
                                childNode.Write(plistValue);
                            }
                        }
                        else if (m_idBool == type)
                        {
                            if (AZStd::string(plistNode->name()) == trueStr)
                            {
                                childNode.Write(true);
                            }
                        }
                        else if (AZStd::string(childNode.GetClassMetadata()->m_name) == "IosOrientations")
                        {
                            // Make sure it seems like an array in plist as well
                            if (AZStd::string(plistNode->name()) == arrayStr)
                            {
                                LoadOrientations(plistNode, &childNode);
                            }
                        }
                        else
                        {
                            SetClassToDefaults(&childNode);
                        }
                    }
                    else
                    {
                        SetDefaults(childNode, type);
                    }
                }
            }
        }
    }

    void Serializer::LoadOrientations(XmlNode* array, AzToolsFramework::InstanceDataNode* node)
    {
        const AZ::SerializeContext::ClassData* baseMeta = node->GetClassMetadata();
        if (baseMeta)
        {
            for (AzToolsFramework::InstanceDataNode& childNode : node->GetChildren())
            {
                const AZ::SerializeContext::ClassElement* childMeta = childNode.GetElementMetadata();
                if (childMeta)
                {
                    const char* propertyName = childMeta->m_name;

                    bool found = false;
                    if (array != nullptr)
                    {
                        for (XmlNode* plistNode = array->first_node(); plistNode != nullptr; plistNode = plistNode->next_sibling())
                        {
                            if (AZStd::string(plistNode->value()) == propertyName)
                            {
                                AZ::Uuid type = childMeta->m_typeId;

                                if (m_idBool == type)
                                {
                                    if (AZStd::string(plistNode->name()) == stringStr)
                                    {
                                        childNode.Write(true);
                                        found = true;
                                    }
                                }
                                else
                                {
                                    AZ_Assert(false, "Unsupported type \"%s\" found in array.", childMeta->m_editData->m_name)
                                }
                                break;
                            }
                        }
                    }
                    if (!found)
                    {
                        childNode.Write(false);
                    }
                }
            }
        }
    }

    void Serializer::SetClassToDefaults(AzToolsFramework::InstanceDataNode* node)
    {
        const AZ::SerializeContext::ClassData* baseMeta = node->GetClassMetadata();
        if (baseMeta)
        {
            for (AzToolsFramework::InstanceDataNode& childNode : node->GetChildren())
            {
                auto childMeta = childNode.GetElementMetadata();
                if (childMeta)
                {
                    AZ::Uuid type = childMeta->m_typeId;

                    SetDefaults(childNode, type);
                }
            }
        }
    }

    void Serializer::SetDefaults(AzToolsFramework::InstanceDataNode& node, const AZ::Uuid& type)
    {
        if (m_idString == type)
        {
            node.Write(AZStd::string(""));
            return;
        }
        else if (m_idBool == type)
        {
            node.Write(false);
            return;
        }
        else if (m_idInt == type)
        {
            node.Write(0);
            return;
        }

        if (AZStd::string(node.GetClassMetadata()->m_name) == "IosOrientations")
        {
            LoadOrientations(nullptr, &node);
        }
        else
        {
            SetClassToDefaults(&node);
        }
    }

    void Serializer::SaveToSettings(AzToolsFramework::InstanceDataNode* node)
    {
        const AZ::SerializeContext::ClassData* baseMeta = node->GetClassMetadata();
        if (baseMeta)
        {
            for (AzToolsFramework::InstanceDataNode& childNode : node->GetChildren())
            {
                auto childMeta = childNode.GetElementMetadata();
                if (childMeta)
                {
                    const char* propertyName = childMeta->m_name;

                    AZ::Uuid type = childMeta->m_typeId;

                    if (m_idString == type)
                    {
                        AZStd::string uiValue;
                        childNode.Read(uiValue);

                        if (!uiValue.empty())
                        {
                            m_plistDict->SetPropertyValue(propertyName, uiValue.data());
                        }
                        else
                        {
                            m_plistDict->RemoveProperty(propertyName);
                        }
                    }
                    else if (m_idBool == type)
                    {
                        bool uiValue;
                        childNode.Read(uiValue);

                        if (uiValue)
                        {
                            m_plistDict->SetPropertyValueName(propertyName, trueStr);
                            m_plistDict->SetPropertyValue(propertyName, "");
                        }
                        else
                        {
                            m_plistDict->RemoveProperty(propertyName);
                        }
                    }
                    else if (AZStd::string(childNode.GetClassMetadata()->m_name) == "IosOrientations")
                    {
                        XmlNode* plistNode = m_plistDict->GetPropertyValueNode(propertyName);
                        if (plistNode == nullptr)
                        {
                            plistNode = m_plistDict->SetPropertyValueName(propertyName, arrayStr);
                        }
                        // Make sure the plist says this is an array type
                        if (AZStd::string(plistNode->name()) == arrayStr)
                        {
                            if (!SaveOrientations(plistNode, &childNode))
                            {
                                m_plistDict->RemoveProperty(propertyName);
                            }
                        }
                    }
                    //Assume this is a class with image overrides
                    else
                    {
                        OverwriteImages(&childNode);
                    }
                }
            }
        }
    }

    bool Serializer::SaveOrientations(XmlNode* array, AzToolsFramework::InstanceDataNode* node)
    {
        bool anyEnabled = false;

        const AZ::SerializeContext::ClassData* baseMeta = node->GetClassMetadata();
        if (baseMeta)
        {
            for (AzToolsFramework::InstanceDataNode& childNode : node->GetChildren())
            {
                const AZ::SerializeContext::ClassElement* childMeta = childNode.GetElementMetadata();
                if (childMeta)
                {
                    AZ::Uuid type = childMeta->m_typeId;
                    if (m_idBool == type)
                    {
                        const char* propertyName = childMeta->m_name;
                        bool uiValue;
                        childNode.Read(uiValue);

                        if (uiValue)
                        {
                            anyEnabled = true;
                        }

                        bool found = false;
                        for (XmlNode* plistNode = array->first_node(); plistNode != nullptr; plistNode = plistNode->next_sibling())
                        {
                            if (AZStd::string(plistNode->value()) == propertyName)
                            {
                                if (!uiValue)
                                {
                                    array->remove_node(plistNode);
                                }
                                else
                                {
                                    found = true;
                                }
                                break;
                            }
                        }
                        if (uiValue && !found)
                        {
                            array->append_node(m_plistDict->MakeNode(stringStr, propertyName));
                        }
                    }
                }
            }
        }

        return anyEnabled;
    }

    void Serializer::OverwriteImages(AzToolsFramework::InstanceDataNode* node)
    {
        const AZ::SerializeContext::ClassData* baseMeta = node->GetClassMetadata();
        if (baseMeta)
        {
            for (AzToolsFramework::InstanceDataNode& childNode : node->GetChildren())
            {
                auto childMeta = childNode.GetElementMetadata();
                if (childMeta)
                {
                    AZ::Uuid type = childMeta->m_typeId;

                    if (m_idString == type)
                    {
                        AZStd::string uiValue;
                        childNode.Read(uiValue);

                        if (!uiValue.empty())
                        {
                            AZ::Edit::ElementData* childEditMeta = childMeta->m_editData;

                            if (childEditMeta != nullptr)
                            {
                                AZ::AttributeId handlerType = childEditMeta->m_elementId;

                                // Special handling for iOS image overrides, the source images must be overwritten.
                                if (handlerType == Handlers::ImagePreview)
                                {
                                    AZ::Edit::Attribute* defaultPathAttr = childEditMeta->FindAttribute(Attributes::DefaultPath);
                                    if (defaultPathAttr != nullptr)
                                    {
                                        AzToolsFramework::PropertyAttributeReader reader(defaultPathAttr->GetContextData(), defaultPathAttr);
                                        AZStd::string defaultPath;
                                        if (reader.Read<AZStd::string>(defaultPath))
                                        {
                                            QString defaultPathQString = defaultPath.data();
                                            CFileUtil::OverwriteFile(defaultPathQString);
                                            CFileUtil::CopyFile(QString(uiValue.data()), defaultPathQString);
                                            // Clear the property so this isn't overwritten again for no reason
                                            childNode.Write(AZStd::string(""));
                                        }
                                    }
                                    else
                                    {
                                        AZ_Assert(false, "Could not find default path for \"%s\". Cannot override image.", childMeta->m_name)
                                    }
                                }
                            }
                        }
                    }
                    else
                    {
                        AZ_Assert(false, "Unsupported type \"%s\" found in what should be image overrides.", childMeta->m_editData->m_name);
                    }
                }
            }
        }
    }
}   // ProjectSettingsTool
