/*-
 * ~~~~~~licensing~~~~~~
 * object-factory
 * ==========
 * Copyright (C) 2020 - 2025 EmDev LLC
 * ==========
 * You may not use this file except in accordance with the License Terms of the Copyright
 * Holder located at: https://entaxy.ru/eula . All copyrights, all intellectual property
 * rights to the Software and any copies are the property of the Copyright Holder. Unless
 * it is explicitly allowed the Copyright Holder, the User is prohibited from using the
 * Software for commercial purposes to provide services to third parties.
 * 
 * The Copyright Holder hereby declares that the Software is provided on an "AS IS".
 * Under no circumstances does the Copyright Holder guarantee or promise that the
 * Software provided by him will be suitable or not suitable for the specific purposes
 * of the User, that the Software will meet all commercial and personal subjective
 * expectations of the User, that the Software will work properly, without technical
 * errors, quickly and uninterruptedly.
 * 
 * Under no circumstances shall the Copyright Holder or its Affiliates is not liable
 * to the User for any direct or indirect losses of the User, his expenses or actual
 * damage, including, downtime; loss of bussines; lost profit; lost earnings; loss
 * or damage to data, property, etc.
 * ~~~~~~/licensing~~~~~~
 */
package ru.entaxy.platform.base.objects.factory;

import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;

import ru.entaxy.platform.base.objects.factory.EntaxyFactory.CONFIGURATION.DIRECTIVES;
import ru.entaxy.platform.base.objects.factory.EntaxyFactory.CONFIGURATION.DIRECTIVES.INHERIT_MODE;
import ru.entaxy.platform.base.objects.factory.EntaxyFactory.CONFIGURATION.DIRECTIVES.OVERRIDE_MODE;
import ru.entaxy.platform.base.objects.factory.EntaxyFactory.FieldInfo;
import ru.entaxy.platform.base.objects.factory.EntaxyFactory.OutputInfo;
import ru.entaxy.platform.base.objects.factory.Importer.ImportInfo;
import ru.entaxy.platform.base.support.CommonUtils;
import ru.entaxy.platform.base.support.JSONUtils;

public class EntaxyFactoryUtils {

    private static final Logger log = LoggerFactory.getLogger(EntaxyFactoryUtils.class);

    public static final String PROP_HIERARCHY = "hierarchy";

    public static Gson gson = new Gson();

    public static String getEffectiveJson(EntaxyFactory factory) {
        JsonObject result = new JsonObject();

        // factory
        JsonObject factoryData = null;

        JsonObject originJson = JSONUtils.getJsonRootObject(factory.getJsonOrigin());
        boolean failback = false;
        if (!originJson.has(EntaxyFactory.CONFIGURATION.FACTORY_SECTION_NAME)) {
            failback = true;
        } else {
            JsonElement je = originJson.get(EntaxyFactory.CONFIGURATION.FACTORY_SECTION_NAME);
            if (!je.isJsonObject()) {
                failback = true;
            } else {
                factoryData = je.getAsJsonObject().deepCopy();
            }
        }

        if (failback) {
            factoryData = new JsonObject();
            factoryData.addProperty(EntaxyFactory.CONFIGURATION.FACTORY.ID, factory.getId());
            factoryData.addProperty(EntaxyFactory.CONFIGURATION.FACTORY.TYPE, factory.getType());
            factoryData.addProperty(EntaxyFactory.CONFIGURATION.FACTORY.CATEGORY, factory.getCategory());
            factoryData.addProperty(EntaxyFactory.CONFIGURATION.FACTORY.LABEL, factory.getLabel());
            factoryData.addProperty(EntaxyFactory.CONFIGURATION.FACTORY.DESCRIPTION, factory.getDescription());
            factoryData.addProperty(EntaxyFactory.CONFIGURATION.FACTORY.PARENT, factory.getParent());
            factoryData.addProperty(EntaxyFactory.CONFIGURATION.FACTORY.SHORT_NAME, factory.getShortName());
            factoryData.addProperty(EntaxyFactory.CONFIGURATION.FACTORY.IS_ABSTRACT, factory.isAbstract());
            factoryData.addProperty(EntaxyFactory.CONFIGURATION.FACTORY.IS_DEPRECATED, factory.isDeprecated());
            factoryData.addProperty(EntaxyFactory.CONFIGURATION.FACTORY.DISPLAY_NAME, factory.getDisplayName());
        }

        result.add(EntaxyFactory.CONFIGURATION.FACTORY_SECTION_NAME, factoryData);

        // typed data
        String type = factory.getType();
        JsonElement typeData = gson.toJsonTree(factory.getTypeInfo());
        result.add(type, typeData);

        // fields
        JsonObject fieldsData = new JsonObject();
        for (FieldInfo fi : factory.getFields()) {
            fieldsData.add(fi.getName(), fi.getJsonOrigin());
        }
        result.add(EntaxyFactory.CONFIGURATION.FIELDS_SECTION_NAME, fieldsData);

        // outputs
        JsonObject outputsData = new JsonObject();
        for (OutputInfo oi : factory.getOutputs()) {
            JsonObject outputData = new JsonObject();
            outputData.addProperty(EntaxyFactory.CONFIGURATION.OUTPUTS.ATTRIBUTES.IS_DEFAULT, oi.isDefault());
            outputData.addProperty(EntaxyFactory.CONFIGURATION.OUTPUTS.ATTRIBUTES.GENERATOR, oi.getGenerator());
            outputData.add(EntaxyFactory.CONFIGURATION.OUTPUTS.ATTRIBUTES.SCOPES, gson.toJsonTree(oi.getScopes()));
            if (oi.getConfig() != null)
                outputData.add(EntaxyFactory.CONFIGURATION.OUTPUTS.ATTRIBUTES.CONFIG, gson.toJsonTree(oi.getConfig()));
            JsonObject outputFields = new JsonObject();
            for (FieldInfo fi : oi.getFields()) {
                outputFields.add(fi.getName(), fi.getJsonOrigin());
            }
            outputData.add(EntaxyFactory.CONFIGURATION.OUTPUTS.ATTRIBUTES.FIELDS, outputFields);
            outputsData.add(oi.getType(), outputData);
        }
        result.add(EntaxyFactory.CONFIGURATION.OUTPUTS_SECTION_NAME, outputsData);

        return result.toString();
    }

    public static JsonObject cleanJson(JsonObject origin) {
        JsonObject result = origin.deepCopy();

        cleanObject(result, EntaxyFactory.CONFIGURATION.DIRECTIVES.getDesignDirectives());

        return result;
    }

    public static void cleanObject(JsonObject origin, Set<String> toRemove) {
        for (String name : toRemove)
            origin.remove(name);
        for (Entry<String, JsonElement> entry : origin.entrySet())
            if (entry.getValue().isJsonObject())
                cleanObject(entry.getValue().getAsJsonObject(), toRemove);
    }

    public static interface FactoryConfigurationStorage {
        JsonObject getConfiguration(String factoryId);
    }

    public static JsonObject calculateEffectiveJson(JsonObject origin, FactoryConfigurationStorage storage) {
        JsonObject originCopy = origin.deepCopy();
        JsonObject result = originCopy;
        JsonObject error = new JsonObject();
        JsonObject errorData = new JsonObject();
        JsonArray errorReqs = new JsonArray();

        error.add("#CALC_ERROR", errorData);
        errorData.add("#REQS", errorReqs);

        // process imports in originCopy

        Importer importer = new Importer(originCopy);
        List<ImportInfo> imports = importer.findImports();
        if (!imports.isEmpty()) {
            final Map<String, JsonObject> factoryJsonMap = new HashMap<>();
            imports.stream()
                    .forEach(imp -> imp.importDescriptors.stream()
                            .filter(id -> CommonUtils.isValid(id.factoryId))
                            .forEach(id -> factoryJsonMap.put(id.factoryId, null)));
            boolean inconsistent = false;
            for (String factoryId : factoryJsonMap.keySet()) {
                JsonObject factoryJson;
                if ("#".equals(factoryId))
                    factoryJson = origin;
                else
                    factoryJson = storage.getConfiguration(factoryId);
                if (factoryJson == null) {
                    inconsistent = true;
                    errorReqs.add(factoryId);
                } else {
                    factoryJsonMap.put(factoryId, factoryJson);
                }

            }
            if (inconsistent)
                return error;

            importer.addImportedSources(factoryJsonMap);
            importer.processImports(imports);

        }

        // process inheritance
        String parent = getParentFromJson(origin);
        if (CommonUtils.isValid(parent)) {
            JsonObject parentObject = storage.getConfiguration(parent);
            if (parentObject == null) {
                errorReqs.add(parent);
                return error;
            }
            result = parentObject.deepCopy();
            prepareTypeInfo(result, originCopy);
            processObjectOverriding(result, originCopy, OVERRIDE_MODE.UPDATE);
        } else {
            result = originCopy;
        }

        return result;
    }

    public static void prepareTypeInfo(JsonObject parent, JsonObject child) {
        JsonElement parentTypeE = JSONUtils.findElement(parent, "factory.type");
        JsonElement childTypeE = JSONUtils.findElement(child, "factory.type");
        String parentType = "";
        String childType = "";
        if ((parentTypeE != null) && parentTypeE.isJsonPrimitive())
            parentType = parentTypeE.getAsString();
        if ((childTypeE != null) && childTypeE.isJsonPrimitive())
            childType = childTypeE.getAsString();

        JsonObject parentProperties = getTypeProperties(parent);
        JsonObject childProperties = getTypeProperties(child);
        if (childProperties == null)
            return;
        if (!parentType.equals(childType) && CommonUtils.isValid(childType)) {
            // copy parent type info with child type
            parent.add(childType, getTypeProperties(parent).deepCopy());
        }
        JsonElement parentHierarchy = JSONUtils.findElement(parentProperties, PROP_HIERARCHY);
        JsonArray childHierarchy = new JsonArray();
        if ((parentHierarchy != null) && parentHierarchy.isJsonArray()) {
            childHierarchy.addAll(parentHierarchy.getAsJsonArray());
        }
        childHierarchy.add(JSONUtils.findElement(parent, "factory.id"));
        childProperties.add(PROP_HIERARCHY, childHierarchy);
    }

    public static JsonObject resolveVariants(JsonObject root) {
        JsonObject result = root.deepCopy();
        processObjectVariants(result, root);
        return result;
    }

    public static void processObjectVariants(JsonObject currentObject, JsonObject root) {
        JsonObject variants = getVariants(currentObject);
        if (variants != null) {
            String property = variants.has("property")
                    ? variants.get("property").getAsString()
                    : "";
            if (CommonUtils.isValid(property)) {
                JsonObject values = (variants.has("values") && variants.get("values").isJsonObject())
                        ? variants.get("values").getAsJsonObject()
                        : null;
                if (values != null) {
                    JsonObject typeProperties = getTypeProperties(root);
                    JsonElement je = JSONUtils.findElement(typeProperties, property);
                    if (je != null) {
                        String value = je.getAsString();
                        if (values.has(value) && values.get(value).isJsonObject()) {
                            JsonObject variant = values.get(value).getAsJsonObject();
                            for (Entry<String, JsonElement> entry : variant.entrySet()) {
                                currentObject.remove(entry.getKey());
                                currentObject.add(entry.getKey(), entry.getValue().deepCopy());
                            }
                        }
                    }
                }
            }
            currentObject.remove(DIRECTIVES.VARIANTS);
        }
        for (Entry<String, JsonElement> entry : currentObject.entrySet()) {
            if (entry.getValue().isJsonObject())
                processObjectVariants(entry.getValue().getAsJsonObject(), root);
        }
    }

    public static JsonObject getVariants(JsonObject currentObject) {
        if (currentObject.has(DIRECTIVES.VARIANTS) && currentObject.get(DIRECTIVES.VARIANTS).isJsonObject())
            return currentObject.get(DIRECTIVES.VARIANTS).getAsJsonObject();
        return null;
    }

    public static JsonObject getTypeProperties(JsonObject currentObject) {
        JsonElement type = JSONUtils.findElement(currentObject, "factory.type");
        if (type == null)
            return new JsonObject();
        String typeValue = type.getAsString();
        if (!CommonUtils.isValid(typeValue))
            return new JsonObject();
        JsonElement typeProperties = JSONUtils.findElement(currentObject, typeValue);
        if (typeProperties.isJsonObject())
            return typeProperties.getAsJsonObject();
        return new JsonObject();
    }

    public static String getParentFromJson(JsonObject jsonObject) {
        if (!jsonObject.has(EntaxyFactory.CONFIGURATION.FACTORY_SECTION_NAME))
            return null;
        JsonObject fs = jsonObject.get(EntaxyFactory.CONFIGURATION.FACTORY_SECTION_NAME).getAsJsonObject();
        if (fs.has(EntaxyFactory.CONFIGURATION.FACTORY.PARENT))
            return fs.get(EntaxyFactory.CONFIGURATION.FACTORY.PARENT).getAsString();
        return null;
    }

    public static void processObjectOverriding(JsonObject currentObject, JsonObject newObject,
            OVERRIDE_MODE defaultMode) {

        OVERRIDE_MODE mode = getOverrideMode(currentObject, defaultMode);

        Map<String, INHERIT_MODE> inheritMode = getInheritMode(newObject);

        if (currentObject.has(EntaxyFactory.CONFIGURATION.DIRECTIVES.LOCALS)) {
            try {

                // Locals are not allowed to be inherited
                // so we fill

                JsonObject locals = currentObject.get(EntaxyFactory.CONFIGURATION.DIRECTIVES.LOCALS).getAsJsonObject();
                for (Entry<String, JsonElement> entry : locals.entrySet()) {

                    try {
                        JsonObject local = entry.getValue().getAsJsonObject();

                        JsonElement defaultValue;

                        if (local.has("defaultValue"))
                            defaultValue = local.get("defaultValue");
                        else
                            defaultValue = new JsonPrimitive("");

                        boolean required = false;
                        if (local.has("required"))
                            required = local.get("required").getAsBoolean();

                        if (currentObject.has(entry.getKey())) {
                            currentObject.remove(entry.getKey());
                            currentObject.add(entry.getKey(), defaultValue);
                        } else if (required)
                            currentObject.add(entry.getKey(), defaultValue);

                    } catch (Exception localException) {
                        log.warn("Error processing '@LOCALS/" + entry.getKey() + ":", localException);
                    }
                }

            } catch (Exception e) {
                log.warn("Error processing '@LOCALS':", e);
            }
        }

        if (OVERRIDE_MODE.IGNORE.equals(mode))
            return;

        if (OVERRIDE_MODE.REPLACE.equals(mode)) {
            Set<String> set = new HashSet<String>(currentObject.keySet());
            for (String entry : set)
                currentObject.remove(entry);
            for (Entry<String, JsonElement> entry : newObject.entrySet())
                currentObject.add(entry.getKey(), entry.getValue().deepCopy());
            return;
        }

        if (OVERRIDE_MODE.APPEND.equals(mode)) {
            for (Entry<String, JsonElement> entry : newObject.entrySet())
                if (!currentObject.has(entry.getKey()))
                    currentObject.add(entry.getKey(), entry.getValue().deepCopy());
            return;
        }

        if (OVERRIDE_MODE.UPDATE.equals(mode)) {


            // update existing
            Set<String> set = new HashSet<String>(currentObject.keySet());
            for (String key : set) {
                if (newObject.has(key)) {
                    JsonElement currentElement = currentObject.get(key);
                    JsonElement newElement = newObject.get(key);
                    if (currentElement.isJsonObject() && newElement.isJsonObject()) {
                        processObjectOverriding(currentElement.getAsJsonObject(), newElement.getAsJsonObject(), mode);
                    } else {
                        if (currentElement.isJsonArray() || newElement.isJsonArray()) {
                            INHERIT_MODE currentInheritMode = inheritMode.getOrDefault(key, INHERIT_MODE.REPLACE);
                            if (!INHERIT_MODE.REPLACE.equals(currentInheritMode)) {
                                processArrayOverriding(currentObject, currentElement, newObject, newElement, key,
                                        currentInheritMode);
                                continue;
                            }
                        }
                        currentObject.remove(key);
                        currentObject.add(key, newElement.deepCopy());
                    }
                }
            }

            // add new
            for (Entry<String, JsonElement> entry : newObject.entrySet())
                if (!currentObject.has(entry.getKey()))
                    currentObject.add(entry.getKey(), entry.getValue().deepCopy());

        }

    }

    public static void processArrayOverriding(JsonObject currentObject, JsonElement currentElement,
            JsonObject newObject, JsonElement newElement, String elementName, INHERIT_MODE currentInheritMode) {
        switch (currentInheritMode) {
            case IGNORE:
                break;
            case REPLACE:
                currentObject.remove(elementName);
                currentObject.add(elementName, newElement);
                break;
            case APPEND:
            case PREPEND:
                JsonArray currentArray, newArray;
                if (currentElement.isJsonArray())
                    currentArray = currentElement.getAsJsonArray();
                else {
                    currentArray = new JsonArray();
                    currentArray.add(currentElement);
                }
                if (newElement.isJsonArray())
                    newArray = newElement.getAsJsonArray();
                else {
                    newArray = new JsonArray();
                    newArray.add(newElement);
                }
                JsonArray firstArray, secondArray;
                if (INHERIT_MODE.APPEND.equals(currentInheritMode)) {
                    firstArray = currentArray;
                    secondArray = newArray;
                } else {
                    firstArray = newArray;
                    secondArray = currentArray;
                }
                JsonArray result = new JsonArray();
                result.addAll(firstArray);
                result.addAll(secondArray);
                currentObject.remove(elementName);
                currentObject.add(elementName, result);
                break;
            default:
                break;
        }
    }

    public static Map<String, EntaxyFactory.CONFIGURATION.DIRECTIVES.INHERIT_MODE> getInheritMode(JsonObject object) {

        Map<String, EntaxyFactory.CONFIGURATION.DIRECTIVES.INHERIT_MODE> result = new HashMap<>();

        if (object.has(EntaxyFactory.CONFIGURATION.DIRECTIVES.INHERIT)) {
            JsonElement je = object.get(EntaxyFactory.CONFIGURATION.DIRECTIVES.INHERIT);
            if (je.isJsonPrimitive()) {
                try {
                    INHERIT_MODE mode = INHERIT_MODE.valueOfLabel(je.getAsString());
                    result.put("*", mode);
                } catch (Exception e) {
                    // NOOP
                }
            } else if (je.isJsonObject()) {
                JsonObject jo = je.getAsJsonObject();
                for (Entry<String, JsonElement> entry : jo.entrySet()) {
                    try {
                        INHERIT_MODE mode = INHERIT_MODE.valueOfLabel(entry.getValue().getAsString());
                        result.put(entry.getKey(), mode);
                    } catch (Exception e) {
                        // NOOP
                    }

                }
            }
        }

        return result;
    }

    public static EntaxyFactory.CONFIGURATION.DIRECTIVES.OVERRIDE_MODE getOverrideMode(JsonObject object,
            OVERRIDE_MODE defaultMode) {
        OVERRIDE_MODE result = defaultMode;
        if (object.has(EntaxyFactory.CONFIGURATION.DIRECTIVES.OVERRIDE)) {
            try {
                OVERRIDE_MODE mode = OVERRIDE_MODE
                        .valueOfLabel(object.get(EntaxyFactory.CONFIGURATION.DIRECTIVES.OVERRIDE).getAsString());
                if (mode != null)
                    result = mode;
            } catch (Exception e) {

            }
        }
        return result;
    }

    public static Map<String, Object> getFactoryEssence(EntaxyFactory factory, String prefix) {
        Map<String, Object> result = new LinkedHashMap<>();
        JsonObject config = JSONUtils.getJsonRootObject(factory.getJsonConfiguration());

        if (config == null)
            return result;

        if (!config.has(EntaxyFactory.CONFIGURATION.FACTORY_SECTION_NAME))
            return result;

        Object obj = JSONUtils.element2object(config.get(EntaxyFactory.CONFIGURATION.FACTORY_SECTION_NAME));

        result.put(String.format("%s%s", prefix, EntaxyFactory.SEARCH.ATTR_ID), factory.getId());
        result.put(String.format("%s%s", prefix, EntaxyFactory.SEARCH.ATTR_TYPE), factory.getType());
        result.put(String.format("%s%s", prefix, EntaxyFactory.SEARCH.ATTR_PARENT), factory.getParent());

        result.put(String.format("%s%s", prefix, EntaxyFactory.SEARCH.ATTR_SHORT_NAME), factory.getShortName());
        result.put(String.format("%s%s", prefix, EntaxyFactory.SEARCH.ATTR_DISPLAY_NAME), factory.getDisplayName());
        result.put(String.format("%s%s", prefix, EntaxyFactory.SEARCH.ATTR_DESCRIPTION), factory.getDescription());
        result.put(String.format("%s%s", prefix, EntaxyFactory.SEARCH.ATTR_LABEL), factory.getLabel());
        result.put(String.format("%s%s", prefix, EntaxyFactory.SEARCH.ATTR_CATEGORY), factory.getCategory());

        result.put(String.format("%s%s", prefix, EntaxyFactory.SEARCH.ATTR_REQUIRES), null);

        result.put(String.format("%s%s", prefix, EntaxyFactory.SEARCH.ATTR_IS_ABSTRACT), factory.isAbstract());
        result.put(String.format("%s%s", prefix, EntaxyFactory.SEARCH.ATTR_IS_DEPRECATED), factory.isDeprecated());
        result.put(String.format("%s%s", prefix, EntaxyFactory.SEARCH.ATTR_IS_INTERNAL), factory.isInternal());


        if (!(obj instanceof Map))
            return result;

        for (Map.Entry<String, Object> entry : ((Map<String, Object>) obj).entrySet()) {
            String key = String.format("%s%s", prefix, entry.getKey());
            if (!result.containsKey(key))
                result.put(key, entry.getValue());
        }

        result.putAll(factory.getTypeInfo().entrySet().stream()
                .collect(() -> new LinkedHashMap<String, Object>(),
                        (m, e) -> m.put(String.format("%stypeinfo.%s", prefix, e.getKey()), e.getValue()),
                        HashMap::putAll)

        );

        return result;
    }


}
