/*
 * Copyright (c) 2022 Huawei Device 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.
 */

#include "system_graph_loader.h"

#include <charconv>
#include <cstddef>
#include <cstdint>

#include <base/containers/array_view.h>
#include <base/containers/iterator.h>
#include <base/containers/string.h>
#include <base/containers/string_view.h>
#include <base/containers/type_traits.h>
#include <base/containers/unique_ptr.h>
#include <base/math/quaternion.h>
#include <base/math/vector.h>
#include <base/namespace.h>
#include <base/util/uid.h>
#include <core/ecs/entity.h>
#include <core/ecs/intf_ecs.h>
#include <core/ecs/intf_system.h>
#include <core/ecs/intf_system_graph_loader.h>
#include <core/io/intf_file.h>
#include <core/io/intf_file_manager.h>
#include <core/log.h>
#include <core/namespace.h>
#include <core/plugin/intf_plugin.h>
#include <core/plugin/intf_plugin_register.h>
#include <core/property/intf_property_api.h>
#include <core/property/intf_property_handle.h>
#include <core/property/property.h>
#include <core/property/property_types.h>

#define JSON_IMPL
#include <core/json/json.h>

#include "json_util.h"

CORE_BEGIN_NAMESPACE()
using BASE_NS::array_view;
using BASE_NS::string;
using BASE_NS::string_view;
using BASE_NS::Uid;
using BASE_NS::Math::Quat;
using BASE_NS::Math::Vec2;
using BASE_NS::Math::Vec3;
using BASE_NS::Math::Vec4;

namespace {
constexpr size_t VERSION_SIZE{5u};
constexpr uint32_t VERSION_MAJOR{22u};

template <class TYPEINFO>
const TYPEINFO* FindTypeInfo(const string_view name, const array_view<const ITypeInfo* const>& container)
{
    for (const auto& info : container) {
        if (info && info->typeUid == TYPEINFO::UID && (static_cast<const TYPEINFO*>(info)->typeName == name)) {
            return static_cast<const TYPEINFO*>(info);
        }
    }

    return nullptr;
}

template <class TYPEINFO>
const TYPEINFO* FindTypeInfo(const Uid& uid, const array_view<const ITypeInfo* const>& container)
{
    for (const auto& info : container) {
        if (static_cast<const TYPEINFO* const>(info)->uid == uid) {
            return static_cast<const TYPEINFO*>(info);
        }
    }

    return nullptr;
}

struct PropertyValue {
    const IPropertyHandle* handle;
    void* offset;
    const Property* info;
    template <typename Type>
    Type& Get() const
    {
        return *static_cast<Type*>(offset);
    }
};

// For primitive types..
template <typename ElementType>
void ReadArrayPropertyValue(const json::value& jsonData, const PropertyValue& propertyData, string& error)
{
    ElementType* output = &propertyData.Get<ElementType>();
    if (propertyData.info->type.isArray) {
        // expecting array data.
        if (auto const array = jsonData.find(propertyData.info->name); array) {
            auto outputView = array_view<ElementType>(output, propertyData.info->count);
            from_json(*array, outputView);
        }
    } else {
        // expecting single value
        ElementType result;
        if (SafeGetJsonValue(jsonData, propertyData.info->name, error, result)) {
            *output = result;
        }
    }
}

// Specialization for char types (strings).. (null terminate the arrays)
template <>
void ReadArrayPropertyValue<char>(const json::value& jsonData, const PropertyValue& propertyData, string& error)
{
    char* output = &propertyData.Get<char>();
    string_view result;
    if (SafeGetJsonValue(jsonData, propertyData.info->name, error, result)) {
        if (propertyData.info->type.isArray) {
            const auto end = result.copy(output, propertyData.info->count - 1, 0);
            output[end] = 0;
        } else {
            *output = result[0];
        }
    }
}

template <class HandleType>
void ReadHandlePropertyValue(const json::value& jsonData, const PropertyValue& propertyData, string& error)
{
    decltype(HandleType::id) result;
    if (SafeGetJsonValue(jsonData, propertyData.info->name, error, result)) {
        auto& handle = propertyData.Get<HandleType>();
        handle.id = result;
    }
}

template <class VecType, size_t n>
void ReadVecPropertyValue(const json::value& jsonData, const PropertyValue& propertyData, string& /* error */)
{
    if (auto const array = jsonData.find(propertyData.info->name); array) {
        auto& result = propertyData.Get<VecType>();
        from_json(*array, result.data);
    }
}

bool ResolveComponentDependencies(
    IEcs& ecs, array_view<const Uid> dependencies, const array_view<const ITypeInfo* const> componentMetadata)
{
    for (const Uid& dependencyUid : dependencies) {
        // default constructed UID is used as a wildcard for dependecies not known at compile time.
        if (dependencyUid == Uid{}) {
            continue;
        }
        // See if this component type exists in ecs
        const IComponentManager* componentManager = ecs.GetComponentManager(dependencyUid);
        if (componentManager) {
            continue;
        }
        // Find component type and create such manager.
        const auto* componentTypeInfo = FindTypeInfo<ComponentManagerTypeInfo>(dependencyUid, componentMetadata);
        if (componentTypeInfo) {
            ecs.CreateComponentManager(*componentTypeInfo);
        } else {
            // Could not find this component manager, unable to continue.
            return false;
        }
    }

    return true;
}

void ReadPropertyValue(const json::value& jsonData, const PropertyValue& property, string& error)
{
    if (property.info) {
        switch (property.info->type) {
            case PropertyType::BOOL_T:
            case PropertyType::BOOL_ARRAY_T:
                ReadArrayPropertyValue<bool>(jsonData, property, error);
                break;

            case PropertyType::CHAR_T:
            case PropertyType::CHAR_ARRAY_T:
                ReadArrayPropertyValue<char>(jsonData, property, error);
                break;

            case PropertyType::INT8_T:
            case PropertyType::INT8_ARRAY_T:
                ReadArrayPropertyValue<int8_t>(jsonData, property, error);
                break;

            case PropertyType::INT16_T:
            case PropertyType::INT16_ARRAY_T:
                ReadArrayPropertyValue<int16_t>(jsonData, property, error);
                break;

            case PropertyType::INT32_T:
            case PropertyType::INT32_ARRAY_T:
                ReadArrayPropertyValue<int32_t>(jsonData, property, error);
                break;

            case PropertyType::INT64_T:
            case PropertyType::INT64_ARRAY_T:
                ReadArrayPropertyValue<int64_t>(jsonData, property, error);
                break;

            case PropertyType::UINT8_T:
            case PropertyType::UINT8_ARRAY_T:
                ReadArrayPropertyValue<uint8_t>(jsonData, property, error);
                break;

            case PropertyType::UINT16_T:
            case PropertyType::UINT16_ARRAY_T:
                ReadArrayPropertyValue<uint16_t>(jsonData, property, error);
                break;

            case PropertyType::UINT32_T:
            case PropertyType::UINT32_ARRAY_T:
                ReadArrayPropertyValue<uint32_t>(jsonData, property, error);
                break;

            case PropertyType::UINT64_T:
            case PropertyType::UINT64_ARRAY_T:
                ReadArrayPropertyValue<uint64_t>(jsonData, property, error);
                break;

            case PropertyType::DOUBLE_T:
            case PropertyType::DOUBLE_ARRAY_T:
                ReadArrayPropertyValue<double>(jsonData, property, error);
                break;

            case PropertyType::ENTITY_T:
                ReadHandlePropertyValue<Entity>(jsonData, property, error);
                break;

            case PropertyType::VEC2_T:
                ReadVecPropertyValue<Vec2, BASE_NS::extent_v<decltype(BASE_NS::Math::Vec2::data)>>(
                    jsonData, property, error);
                break;
            case PropertyType::VEC3_T:
                ReadVecPropertyValue<Vec3, BASE_NS::extent_v<decltype(BASE_NS::Math::Vec3::data)>>(
                    jsonData, property, error);
                break;
            case PropertyType::VEC4_T:
                ReadVecPropertyValue<Vec4, BASE_NS::extent_v<decltype(BASE_NS::Math::Vec4::data)>>(
                    jsonData, property, error);
                break;
            case PropertyType::QUAT_T:
                ReadVecPropertyValue<Quat, BASE_NS::extent_v<decltype(BASE_NS::Math::Quat::data)>>(
                    jsonData, property, error);
                break;
            case PropertyType::MAT4X4_T:
                // NOTE: Implement.
                break;

            case PropertyType::FLOAT_T:
            case PropertyType::FLOAT_ARRAY_T:
                ReadArrayPropertyValue<float>(jsonData, property, error);
                break;

            case PropertyType::STRING_T:
                ReadArrayPropertyValue<string>(jsonData, property, error);
                break;

            case PropertyType::ENTITY_ARRAY_T:
            case PropertyType::VEC2_ARRAY_T:
            case PropertyType::VEC3_ARRAY_T:
            case PropertyType::VEC4_ARRAY_T:
            case PropertyType::QUAT_ARRAY_T:
            case PropertyType::MAT4X4_ARRAY_T:
            case PropertyType::INVALID:
                // NOTE: Implement.
                break;
        }
    }
}

void ParseProperties(const json::value& jsonData, ISystem& system, string& error)
{
    const json::value* propertiesIt = jsonData.find("properties");
    if (!propertiesIt) {
        return;
    }
    auto systemPropertyHandle = system.GetProperties();
    if (!systemPropertyHandle) {
        return;
    }
    if (auto offset = systemPropertyHandle->WLock(); offset) {
        const IPropertyApi* propertyApi = systemPropertyHandle->Owner();
        for (size_t i = 0U, count = propertyApi->PropertyCount(); i < count; ++i) {
            if (auto* prop = propertyApi->MetaData(i)) {
                PropertyValue property{systemPropertyHandle, static_cast<uint8_t*>(offset) + prop->offset, prop};
                ReadPropertyValue(*propertiesIt, property, error);
            }
        }
    }
    systemPropertyHandle->WUnlock();
    system.SetProperties(*systemPropertyHandle);  // notify that the properties HAVE changed.
}

bool ParseSystem(const json::value& jsonData, const array_view<const ITypeInfo* const>& componentMetadata,
    const array_view<const ITypeInfo* const>& systemMetadata, IEcs& ecs, string& error)
{
    string typeName;
    SafeGetJsonValue(jsonData, "typeName", error, typeName);

    bool optional = false;
    SafeGetJsonValue(jsonData, "optional", error, optional);

    // Look up this system type from metadata.
    const auto* typeInfo = FindTypeInfo<SystemTypeInfo>(typeName, systemMetadata);
    if (typeInfo) {
        // Ensure we have all components required by this system.
        if (!ResolveComponentDependencies(ecs, typeInfo->componentDependencies, componentMetadata)) {
            error += "Failed to resolve component dependencies: (" + typeName + ".\n";
            return false;
        }
        if (!ResolveComponentDependencies(ecs, typeInfo->readOnlyComponentDependencies, componentMetadata)) {
            error += "Failed to resolve read-only component dependencies: (" + typeName + ".\n";
            return false;
        }

        // Create system and read properties.
        ISystem* system = ecs.CreateSystem(*typeInfo);
        if (system) {
            ParseProperties(jsonData, *system, error);
        }

        return true;
    }
    CORE_LOG_W("Cannot find system: %s (optional: %s)", typeName.c_str(), (optional ? "true" : "false"));
    if (!optional) {
        error += "Cannot find system: " + typeName + ".\n";
    }

    return optional;
}

SystemGraphLoader::LoadResult ParseSystemGraphVersion(const json::value& jsonData)
{
    SystemGraphLoader::LoadResult finalResult;
    string ver;
    string type;
    uint32_t verMajor{~0u};
    uint32_t verMinor{~0u};
    if (const json::value* iter = jsonData.find("compatibility_info"); iter) {
        SafeGetJsonValue(*iter, "type", finalResult.error, type);
        SafeGetJsonValue(*iter, "version", finalResult.error, ver);
        if (ver.size() == VERSION_SIZE) {
            const auto delim = ver.find('.');
            if ((delim != string::npos) && (delim < (ver.size() - 1U))) {
                std::from_chars(ver.data(), ver.data() + delim, verMajor);
                std::from_chars(ver.data() + delim + 1, ver.data() + ver.size(), verMinor);
            }
        }
    }
    if ((type != "systemgraph") || (verMajor != VERSION_MAJOR)) {
        // NOTE: we're still loading the system graph
        CORE_LOG_W("DEPRECATED SYSTEM GRAPH: invalid system graph type (%s) and / or invalid version (%s).",
            type.c_str(),
            ver.c_str());
    }
    return finalResult;
}

ISystemGraphLoader::LoadResult LoadFromNullTerminated(const string_view jsonString, IEcs& ecs)
{
    const auto json = json::parse(jsonString.data());
    if (!json) {
        return ISystemGraphLoader::LoadResult("Invalid json file.");
    }
    ISystemGraphLoader::LoadResult finalResult = ParseSystemGraphVersion(json);
    const auto& systemsArrayIt = json.find("systems");
    if (systemsArrayIt && systemsArrayIt->is_array()) {
        auto& pluginRegister = GetPluginRegister();
        auto componentMetadata = pluginRegister.GetTypeInfos(ComponentManagerTypeInfo::UID);
        auto systemMetadata = pluginRegister.GetTypeInfos(SystemTypeInfo::UID);
        for (const auto& systemJson : systemsArrayIt->array_) {
            if (!ParseSystem(systemJson, componentMetadata, systemMetadata, ecs, finalResult.error)) {
                break;
            }
        }
    }

    finalResult.success = finalResult.error.empty();

    return finalResult;
}
}  // namespace

SystemGraphLoader::LoadResult SystemGraphLoader::Load(const string_view uri, IEcs& ecs)
{
    IFile::Ptr file = fileManager_.OpenFile(uri);
    if (!file) {
        CORE_LOG_D("Error loading '%s'", string(uri).c_str());
        return LoadResult("Failed to open file.");
    }

    const uint64_t byteLength = file->GetLength();

    constexpr uint64_t MAX_SYSTEM_GRAPH_SIZE = 16u * 1024u * 1024u;
    if (byteLength > MAX_SYSTEM_GRAPH_SIZE) {
        return LoadResult("System graph file too large.");
    }

    string raw(static_cast<size_t>(byteLength), string::value_type());
    if (file->Read(raw.data(), byteLength) != byteLength) {
        CORE_LOG_D("Error loading '%s'", string(uri).c_str());
        return LoadResult("Failed to read file.");
    }

    return LoadFromNullTerminated(raw, ecs);
}

SystemGraphLoader::LoadResult SystemGraphLoader::LoadString(const string_view jsonString, IEcs& ecs)
{
    // make sure the input is zero terminated before parsing.
    const auto asString = string(jsonString);
    return LoadFromNullTerminated(asString, ecs);
}

SystemGraphLoader::SystemGraphLoader(IFileManager& fileManager) : fileManager_{fileManager}
{}

void SystemGraphLoader::Destroy()
{
    delete this;
}

// SystemGraphLoader factory
ISystemGraphLoader::Ptr SystemGraphLoaderFactory::Create(IFileManager& fileManager)
{
    return ISystemGraphLoader::Ptr{new SystemGraphLoader(fileManager)};
}

const IInterface* SystemGraphLoaderFactory::GetInterface(const Uid& uid) const
{
    if ((uid == ISystemGraphLoaderFactory::UID) || (uid == IInterface::UID)) {
        return this;
    }
    return nullptr;
}

IInterface* SystemGraphLoaderFactory::GetInterface(const Uid& uid)
{
    if ((uid == ISystemGraphLoaderFactory::UID) || (uid == IInterface::UID)) {
        return this;
    }
    return nullptr;
}

void SystemGraphLoaderFactory::Ref()
{}

void SystemGraphLoaderFactory::Unref()
{}
CORE_END_NAMESPACE()