/*-
 * ~~~~~~licensing~~~~~~
 * test-producers
 * ==========
 * 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.core.producer.executor.commands;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

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.EntaxyObject.FIELDS;
import ru.entaxy.platform.base.objects.factory.EntaxyFactory;
import ru.entaxy.platform.base.objects.factory.EntaxyFactory.CONFIGURATION.OUTPUTS;
import ru.entaxy.platform.base.objects.factory.EntaxyFactory.FieldInfo;
import ru.entaxy.platform.base.objects.factory.EntaxyFactory.RefFieldInfo;
import ru.entaxy.platform.base.support.CommonUtils;
import ru.entaxy.platform.base.support.JSONUtils;
import ru.entaxy.platform.core.producer.api.EntaxyProducerService;
import ru.entaxy.platform.core.producer.api.ExecutionPlan.ExecutionPlanUpdate;
import ru.entaxy.platform.core.producer.api.ProducerResult;
import ru.entaxy.platform.core.producer.api.ProducerResult.CommandResult;
import ru.entaxy.platform.core.producer.executor.AbstractCommandExecutor;
import ru.entaxy.platform.core.producer.executor.CommandExecutor;
import ru.entaxy.platform.core.producer.executor.objectmodel.AbstractItem;
import ru.entaxy.platform.core.producer.executor.objectmodel.Calculation;
import ru.entaxy.platform.core.producer.executor.objectmodel.FactoredObject;
import ru.entaxy.platform.core.producer.executor.objectmodel.FactoredObjectProxy;
import ru.entaxy.platform.core.producer.executor.objectmodel.FactoredObjectRef;
import ru.entaxy.platform.core.producer.executor.objectmodel.ObjectModel;
import ru.entaxy.platform.core.producer.impl.RefFieldInfoImpl;

@CommandExecutor(id = "enrich", predecessors = {"layout"})
public class Enrich extends AbstractCommandExecutor {

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

    public static final String DIRECTIVE_ENRICHED = "@ENRICHED";

    public Enrich(EntaxyProducerService entaxyProducerService) {
        super(entaxyProducerService);
        // TODO Auto-generated constructor stub
    }

    // TODO move to FieldInfo
    protected boolean isScoped(FieldInfo fi) {
        return fi.getJsonOrigin().has(EntaxyFactory.CONFIGURATION.DIRECTIVES.SCOPED);
    }

    // TODO move to FieldInfo
    protected boolean isInScope(FieldInfo fi, String currentScope) {
        if (!isScoped(fi))
            return true;
        JsonElement je = fi.getJsonOrigin().get(EntaxyFactory.CONFIGURATION.DIRECTIVES.SCOPED);
        if (!je.isJsonArray())
            return true;
        JsonArray ja = je.getAsJsonArray();
        if (ja.size() == 0)
            return true;
        for (int i = 0; i < ja.size(); i++) {
            JsonElement item = ja.get(i);
            if (!item.isJsonPrimitive())
                continue;
            JsonPrimitive jp = item.getAsJsonPrimitive();
            if (!jp.isString())
                continue;
            if (jp.getAsString().equals(currentScope))
                return true;
        }
        return false;
    }

    // TODO move to FieldInfo
    protected boolean isLookup(FieldInfo fi) {
        if (fi.getJsonOrigin().has("lookup")) {
            String lookup = fi.getJsonOrigin().get("lookup").getAsString();
            return CommonUtils.isValid(lookup);
        }
        return false;
    }

    // TODO move to FieldInfo
    public String getLookup(FieldInfo fi) {
        return fi.getJsonOrigin().has("lookup")
                ? fi.getJsonOrigin().get("lookup").getAsString()
                : null;
    }

    @Override
    protected boolean doExecute(ProducerResult currentResult, CommandResult commandResult,
            Map<String, Object> instructions) throws Exception {

        ObjectModel objectModel = currentResult.findResultObject(ObjectModel.class);
        objectModel.startTracking();

        JsonObject incomingJson = objectModel.getJsonCurrent().deepCopy();

        Gson gson = new Gson();

        // enrich type to ensure we can resolve refs
        // and enrich scope

        for (FactoredObject fo : objectModel.objects) {

            // skip proxies
            if (fo instanceof FactoredObjectProxy)
                continue;

            String factoryId = fo.factoryId;

            EntaxyFactory factory = entaxyProducerService.findFactoryById(factoryId);
            if (factory == null)
                throw new Exception("Factory not found: [" + factoryId + "]");

            String objectType = fo.getObjectType();
            if (!CommonUtils.isValid(objectType)) {
                fo.setObjectType(factory.getType());

                // and refresh refs
                for (FactoredObjectRef ref : fo.references)
                    ref.refeshTargetType();

                objectModel.setDirty();
            }

            if (!fo.origin.has(FIELDS.SCOPE)) {
                JSONUtils.setValue(fo.origin, FIELDS.SCOPE, gson.toJsonTree(fo.scope), true);
                objectModel.setDirty();
            }
        }

        // enrich fields

        for (FactoredObject fo : objectModel.objects) {

            // skip proxies
            if (fo instanceof FactoredObjectProxy)
                continue;

            String factoryId = fo.factoryId;

            EntaxyFactory factory = entaxyProducerService.findFactoryById(factoryId);

            List<FieldInfo> delayed = new LinkedList<>();

            List<String> processedFieldNames = new LinkedList<>();

            JsonObject objectOrigin = fo.origin;

            if (JSONUtils.setValue(objectOrigin, FIELDS.PROPERTIES, new JsonObject(), true)) {
                printOutput(": [DIRTY] CREATED 'properties' for [" + fo.getObjectId() + "/" + fo.getObjectType() + "]");
                objectModel.setDirty();
            }

            JSONUtils.setValue(objectOrigin, FactoredObject.IGNORE_SECTION, new JsonObject(), true);

            JsonObject objectProperties = objectOrigin.get(FIELDS.PROPERTIES).getAsJsonObject();

            List<FieldInfo> factoryFields = factory.getFields(
                    CommonUtils.getValid(fo.getOutputType(), OUTPUTS.OUTPUT_TYPE_INIT));


            for (FieldInfo fi : factoryFields) {

                FieldInfo fieldInfo = fi;

                log.debug("\n ITERATING FOR FIELD: " + fi.getName() + "\n" + fi.getJsonOrigin());


                if (FIELDS.OBJECT_ID.equals(fi.getName())
                        || FIELDS.OBJECT_TYPE.equals(fi.getName()))
                    continue;

                // check scope
                if (!isInScope(fi, fo.scope))
                    continue;

                // check if lookup
                if (isLookup(fi)) {

                    String lookup = getLookup(fi);
                    log.debug("\n LOOKUP FIELD: " + fi.getName() + " -> " + lookup);

                    // replace lookup with corresponding ref
                    Optional<FieldInfo> toLookupOpt = factoryFields.stream()
                            .filter(f -> lookup.equals(f.getName()))
                            .findFirst();
                    if (!toLookupOpt.isPresent()) {
                        // field to lookup not found
                        log.error(
                                "Factory configuration error: loooked up field [{}] referenced by [{}] not found in factory [{}]",
                                lookup, fi.getName(), factory.getId());
                        continue;
                    }
                    FieldInfo toLookup = toLookupOpt.get();
                    RefFieldInfoImpl fieldInfoImpl = new RefFieldInfoImpl();
                    fieldInfoImpl.name(fi.getName())
                            .description(fi.getDescription())
                            .displayName(fi.getDisplayName())
                            .immutable(fi.isImmutable())
                            .required(fi.isRequired());
                    fieldInfoImpl.refField(
                            fi.getJsonOrigin().has(FIELDS.REF_FIELD)
                                    ? fi.getJsonOrigin().get(FIELDS.REF_FIELD).getAsString()
                                    : "")
                            .refByValueOnly(true)
                            .backRef(false);
                    fieldInfoImpl.setConditional(fi.isConditional());
                    fieldInfoImpl.setCondition(fi.getCondition());
                    fieldInfoImpl.setRef(true);
                    fieldInfoImpl.setType(toLookup.getType());
                    JsonObject lookupJson = fi.getJsonOrigin().deepCopy();
                    JsonObject lookedUpJson = toLookup.getJsonOrigin();
                    for (Entry<String, JsonElement> entry : lookedUpJson.entrySet()) {
                        if (lookupJson.has(entry.getKey()))
                            lookupJson.remove(entry.getKey());
                        lookupJson.add(entry.getKey(), entry.getValue());
                    }
                    fieldInfoImpl.setJsonOrigin(lookupJson);

                    log.debug("\n LOOKUP FIELD JSON: \n" + lookupJson.toString());

                    fieldInfo = fieldInfoImpl;

                    if (!processedFieldNames.contains(lookup) || fi.isConditional()) {
                        delayed.add(fieldInfo);
                        continue;
                    }

                }

                // check if conditional
                if (fi.isConditional()) {
                    delayed.add(fi);
                    continue;
                }


                processField(objectModel, fieldInfo, fo, gson);
                processedFieldNames.add(fieldInfo.getName());
            }

            int delayedCount = 0;

            while (delayedCount != delayed.size()) {
                List<FieldInfo> toRemove = new ArrayList<>();

                delayedCount = delayed.size();

                for (FieldInfo fi : delayed) {

                    // first check conditionals

                    if (fi.isConditional()) {

                        String condition = fi.getCondition();
                        if (condition.startsWith("${"))
                            condition = condition.substring(2, condition.lastIndexOf("}"));


                        if (!objectProperties.has(condition))
                            continue;

                        Object conditionValue = JSONUtils.element2object(objectProperties.get(condition));
                        if (conditionValue == null) {
                            continue;
                        } else if (conditionValue instanceof String) {
                            if (!"true".equalsIgnoreCase((String) conditionValue)) {
                                toRemove.add(fi);
                                continue;
                            }
                        } else if (conditionValue instanceof Number) {
                            if (((Number) conditionValue).doubleValue() <= 0) {
                                toRemove.add(fi);
                                continue;
                            }
                        } else if (conditionValue instanceof Boolean) {
                            if (!(Boolean) conditionValue) {
                                toRemove.add(fi);
                                continue;
                            }
                        }
                    }

                    // then check lookups
                    if (isLookup(fi)) {
                        if (!processedFieldNames.contains(getLookup(fi))) {
                            continue;
                        }
                    }

                    // we're still here so process
                    processField(objectModel, fi, fo, gson);
                    processedFieldNames.add(fi.getName());

                }

                delayed.removeAll(toRemove);
            }

            JsonObject ignores = objectOrigin.get(FactoredObject.IGNORE_SECTION).getAsJsonObject();

            // try to calculate
            for (AbstractItem ai : fo.getInternals()) {
                if (!(ai instanceof Calculation))
                    continue;
                Calculation calc = (Calculation) ai;

                // leave lazy calculations until generation
                if (calc.isLazy())
                    continue;

                Object result = calc.calculate(objectModel, fo, entaxyProducerService);
                if (result != Calculation.NOT_ENOUGH_DATA) {
                    printOutput(": [CALCULATION] DONE for [" + fo.getObjectId() + "/" + calc.relativePath + "] :: "
                            + result.toString());
                    String currentPath = calc.relativePath;
                    String targetPath = calc.getTargetPath();
                    boolean remove = (!currentPath.equals(targetPath) && calc.isRemoveResolved());
                    targetPath = targetPath.replace("$.", "");
                    if (calc.isMergeToTarget()) {
                        JsonElement je = JSONUtils.getElement(objectOrigin, targetPath);
                        if (je == null) {
                            JSONUtils.replaceValue(objectOrigin, targetPath, (new Gson()).toJsonTree(result));
                        } else if (je.isJsonArray()) {
                            JsonArray ja = je.getAsJsonArray();
                            if (result instanceof List)
                                for (Object obj : (List) result)
                                    ja.add(obj.toString());

                        }

                    } else {
                        JSONUtils.replaceValue(objectOrigin, targetPath, (new Gson()).toJsonTree(result));
                    }
                    if (remove) {
                        int ind = currentPath.lastIndexOf(".");
                        String fieldName = ind >= 0
                                ? currentPath.substring(ind + 1)
                                : currentPath;
                        ignores.addProperty(fieldName, true);
                        JSONUtils.replaceValue(objectOrigin, currentPath, (new Gson()).toJsonTree("TO_REMOVE"));
                    }
                    printOutput(": [DIRTY] SET CALCULATED [" + result + "] to [" + targetPath + "]");
                    objectModel.setDirty();
                }
            }

        }



        if (objectModel.stopTracking()) {

            // remove ##embedded
            for (FactoredObject fo : objectModel.objects)
                fo.origin.remove(FactoredObject.EMBEDDED_FIELD);


            commandResult.planUpdate = ExecutionPlanUpdate.create()
                    // .updateInstructions().target("enrich").value("skip", true).complete()
                    .reset().target("analyze").complete();
        }

        JsonObject outgoingJson = objectModel.getJsonCurrent();

        printOutput("\n== INCOMING JSON ==\n");
        printOutput(incomingJson.toString());
        printOutput("\n== OUTGOING JSON ==\n");
        printOutput(outgoingJson.toString());

        commandResult.resultObject(outgoingJson);

        return true;
    }

    protected boolean processField(ObjectModel objectModel, FieldInfo fi, FactoredObject fo, Gson gson) {

        log.debug("\nPROCESSING FIELD: " + fi.getName());

        if (fi.isRef()) {
            log.debug("\nPROCESSING REF: " + fi.getName());
            return processRef(objectModel, (RefFieldInfo) fi, fo, gson);
        }

        boolean result = false;

        JsonObject objectOrigin = fo.origin;
        JsonObject objectProperties = objectOrigin.get(FIELDS.PROPERTIES).getAsJsonObject();
        JsonObject ignores = objectOrigin.get(FactoredObject.IGNORE_SECTION).getAsJsonObject();

        boolean exists = objectProperties.has(fi.getName());
        boolean ignored = ignores.has(fi.getName());
        if (!ignored) {
            if (fi.isRequired()) {
                if (exists) {

                    JsonElement currentValue = objectProperties.get(fi.getName());

                    printOutput("[CHECK] IS FIXED VALUE " + fi.getName() + " for [" + fo.getObjectId() + "/"
                            + fo.getObjectType() + "]");
                    if (fi.getJsonOrigin().has("fixedValue")) {
                        JsonElement fixedValue = fi.getJsonOrigin().get("fixedValue");
                        boolean replaceValue = false;
                        if (fixedValue != null) {
                            if (currentValue == null)
                                replaceValue = true;
                            else
                                replaceValue = !fixedValue.toString().equals(currentValue.toString());
                        }
                        if (replaceValue) {
                            printOutput(": [DIRTY] REPLACED WITH FIXED VALUE: " + fi.getName() + " for ["
                                    + fo.getObjectId() + "/" + fo.getObjectType() + "]");
                            JSONUtils.replaceValue(objectProperties, fi.getName(), fixedValue);
                            objectModel.setDirty();
                            result = true;
                        }
                    }

                    // check if there's an autocreated ref in the current value
                    // and if not enriched, then enrich it from @REF_CONFIG in the field info

                    if (currentValue.isJsonObject()) {
                        JsonObject currentObjectValue = currentValue.getAsJsonObject();
                        if (FactoredObjectRef.isRef(currentObjectValue))
                            if (!currentObjectValue.has(DIRECTIVE_ENRICHED)
                                    && currentObjectValue.has(FactoredObjectRef.DIRECTIVE_AUTO_CREATED)) {
                                currentObjectValue.addProperty(DIRECTIVE_ENRICHED, true);
                                JsonElement refConfig = fi.getJsonOrigin().get(FactoredObjectRef.DIRECTIVE_REF_CONFIG);
                                if (refConfig != null && refConfig.isJsonObject()) {
                                    boolean softUpdate = false;

                                    try {
                                        softUpdate =
                                                refConfig.getAsJsonObject().get(FactoredObjectRef.DIRECTIVE_SOFT_UPDATE)
                                                        .getAsBoolean();
                                    } catch (Exception ignore) {
                                        // NOOP
                                    }

                                    for (Map.Entry<String, JsonElement> entry : refConfig.getAsJsonObject().entrySet())
                                        if (!currentObjectValue.has(entry.getKey()) || !softUpdate)
                                            currentObjectValue.add(entry.getKey(), entry.getValue());
                                }

                                printOutput(
                                        ": [DIRTY] ENRICHED REF IN " + fi.getName() + " for [" + fo.getObjectId() + "/"
                                                + fo.getObjectType() + "]");

                                objectProperties.remove(fi.getName());
                                objectProperties.add(fi.getName(), currentObjectValue);
                                objectModel.setDirty();
                                result = true;

                            }
                    }
                } else {
                    printOutput(": [DIRTY] CREATED " + fi.getName() + " for [" + fo.getObjectId() + "/"
                            + fo.getObjectType() + "]");

                    objectProperties.add(fi.getName(), fi.getJsonOrigin().get(FIELDS.DEFAULT_VALUE));
                    objectModel.setDirty();
                    result = true;
                }
            }
        }
        return result;
    }

    protected boolean isLookupEnabled(RefFieldInfo fi) {
        JsonObject origin = fi.getJsonOrigin();
        if (!origin.has(EntaxyFactory.CONFIGURATION.DIRECTIVES.NO_LOOKUP))
            return true;
        try {
            return !origin.get(EntaxyFactory.CONFIGURATION.DIRECTIVES.NO_LOOKUP)
                    .getAsBoolean();
        } catch (Exception ignore) {

        }
        return true;
    }

    protected boolean processRef(ObjectModel objectModel, RefFieldInfo fi, FactoredObject fo, Gson gson) {
        boolean result = false;

        String attrRefMark = "@REF_MARK";

        JsonObject objectOrigin = fo.origin;
        JsonObject objectProperties = objectOrigin.get(FIELDS.PROPERTIES).getAsJsonObject();
        JsonObject ignores = objectOrigin.get(FactoredObject.IGNORE_SECTION).getAsJsonObject();

        boolean exists = objectProperties.has(fi.getName());
        boolean ignored = ignores.has(fi.getName());

        if (ignored)
            return true;

        if (!exists) {
            if (fi.isRequired()) {

                boolean lookupEnabled = isLookupEnabled(fi);

                if (fi.isBackRef()) {
                    // it's a backRef, so we'll try to find object of the given type
                    // referencing current object
                    List<FactoredObject> candidates =
                            lookupEnabled
                                    ? fo.references.stream()
                                            .filter(ref -> !ref.isBackRef())
                                            .map(ref -> ref.definedIn)
                                            .filter(obj -> fi.getType().equals(obj.getObjectType()))
                                            .collect(Collectors.toList())
                                    : Collections.emptyList();
                    if (candidates.size() > 0) {
                        // we found candidates, so just add ref to first one
                        // no matter what object is there
                        // 'cause the ref being backRef won't be ever generated
                        FactoredObjectRef ref = new FactoredObjectRef(objectModel, candidates.get(0));
                        ref.definedIn = fo;
                        fo.embed(ref);
                        ref.setBackRef(true);
                        ref.setResolved(true);
                        printOutput(": [DIRTY] CREATED " + fi.getName() + " for [" + fo.getObjectId() + "/"
                                + fo.getObjectType() + "]");
                        objectProperties.add(fi.getName(), ref.origin);
                        result = true;
                    } else {
                        // try to use default value
                        printOutput(": [DIRTY] CREATED " + fi.getName() + " for [" + fo.getObjectId() + "/"
                                + fo.getObjectType() + "]");
                        objectProperties.add(fi.getName(), fi.getJsonOrigin().get(FIELDS.DEFAULT_VALUE));
                        objectModel.setDirty();
                        result = true;
                    }

                } else {
                    // it's a direct ref

                    FactoredObject candidate = null;

                    // try to find by backRef
                    List<FactoredObjectRef> backrefs = objectModel.refs.stream()
                            .filter(ref -> (ref.isBackRef()
                                    && (ref.getTargetObject().equals(fo))
                                    && (ref.definedIn.getObjectType().equals(fi.getType()))))
                            .collect(Collectors.toList());
                    if (backrefs.size() > 0) {
                        // we can use any
                        candidate = backrefs.get(0).definedIn;
                    } else {
                        // try to find objects of required type defined in current
                        // if lookup is enabled
                        List<FactoredObject> candidates =
                                lookupEnabled
                                        ? objectModel.objects
                                                .stream()
                                                .filter(
                                                        obj -> ((obj != fo)
                                                                && (obj.getObjectType().equals(fi.getType()))))
                                                .collect(Collectors.toList())
                                        : Collections.emptyList();


                        // we can use the only one
                        if (candidates.size() == 1)
                            candidate = candidates.get(0);
                        else if (candidates.size() > 1) {
                            // search for defined in current
                            candidates = candidates.stream().filter(c -> c.isDefinedIn(fo))
                                    .collect(Collectors.toList());
                            if (candidates.size() == 1)
                                candidate = candidates.get(0);
                        }
                    }

                    if (candidate != null) {

                        FactoredObjectRef ref = new FactoredObjectRef(objectModel, candidate);
                        ref.definedIn = fo;
                        fo.embed(ref);
                        ref.origin.addProperty(FIELDS.IS_REF_BY_VALUE_ONLY, fi.isRefByValueOnly());
                        ref.origin.addProperty(FIELDS.REF_FIELD, fi.getRefField());
                        printOutput(": [DIRTY] CREATED " + fi.getName() + " for [" + fo.getObjectId() + "/"
                                + fo.getObjectType() + "]");
                        objectProperties.add(fi.getName(), ref.origin);
                        objectModel.setDirty();
                        result = true;

                    } else {
                        /*
                         * if (fi.getJsonOrigin().has(FIELDS.DEFAULT_VALUE)) {
                         * printOutput("[DIRTY] CREATED " + fi.getName() + " for [" +
                         * fo.getObjectId() + "/" + fo.getObjectType() + "]");
                         * objectProperties.add(fi.getName(),
                         * fi.getJsonOrigin().get(FIELDS.DEFAULT_VALUE)); objectModel.setDirty();
                         * result = true; } else {
                         */

                        // we just create a ref to nothing
                        // hoping it will be enriched next time

                        JsonObject refCopy = fi.getJsonOrigin().deepCopy();
                        if (isLookup(fi)) {
                            // remove default targetId, 'cause it will be resolved on next enrich
                            refCopy.remove(FactoredObjectRef.TARGET_ID_FIELD);
                        }

                        if (refCopy.has(FIELDS.DEFAULT_VALUE)) {
                            JsonElement je = refCopy.get(FIELDS.DEFAULT_VALUE);
                            if (je.isJsonObject()) {
                                JsonObject jo = je.getAsJsonObject();
                                if (jo.has(FIELDS.FACTORY_ID) && jo.has(FIELDS.OBJECT_TYPE)) {
                                    // we assume defaultValue as object desciriptor
                                    String markValue = UUID.randomUUID().toString();
                                    refCopy.remove(attrRefMark);
                                    refCopy.addProperty(attrRefMark, markValue);
                                    jo.remove(markValue);
                                    jo.addProperty(attrRefMark, markValue);
                                }
                            }
                        }

                        printOutput(": [DIRTY] CREATED " + fi.getName() + " for [" + fo.getObjectId() + "/"
                                + fo.getObjectType() + "]");
                        printOutput("\t" + refCopy.toString());
                        objectProperties.add(fi.getName(), refCopy);
                        objectModel.setDirty();
                        result = true;
                        // }
                    }

                }


            }
        } else {
            // we found something where must be ref
            JsonElement currentValue = objectProperties.get(fi.getName());
            JsonObject refCopy = fi.getJsonOrigin().deepCopy();

            if (currentValue.isJsonObject()) {
                // this is some object
                printOutput("[CHECK REF] CURRENT VALUE [" + currentValue.toString() + "] IS OBJECT IN " + fi.getName()
                        + " for ["
                        + fo.getObjectId() + "/" + fo.getObjectType() + "]");
                JsonObject currentObject = currentValue.getAsJsonObject();
                if (currentObject.has(FIELDS.IS_REF)) {
                    String targetType = currentObject.has(FactoredObjectRef.TARGET_TYPE_FIELD)
                            ? currentObject.get(FactoredObjectRef.TARGET_TYPE_FIELD).getAsString()
                            : null;
                    if (!CommonUtils.isValid(targetType)) {
                        String refType = fi.getType();
                        targetType = CommonUtils.isValid(refType) ? refType : "*";
                        currentObject.remove(FactoredObjectRef.TARGET_TYPE_FIELD);
                        currentObject.addProperty(FactoredObjectRef.TARGET_TYPE_FIELD, targetType);
                        objectModel.setDirty();
                    }
                    if (!currentObject.has(FIELDS.IS_REF_BY_VALUE_ONLY)
                            || !currentObject.has(FIELDS.REF_FIELD)
                            || !currentObject.has(FIELDS.IS_BACK_REF)) {
                        currentObject.addProperty(FIELDS.IS_REF_BY_VALUE_ONLY, fi.isRefByValueOnly());
                        currentObject.addProperty(FIELDS.REF_FIELD, fi.getRefField());
                        currentObject.addProperty(FIELDS.IS_BACK_REF, fi.isBackRef());
                        printOutput(": [DIRTY] UPDATED " + fi.getName() + " for [" + fo.getObjectId() + "/"
                                + fo.getObjectType() + "]");
                        objectModel.setDirty();
                    }

                    // TODO move 'refExtended' to constants on EntaxyFactory
                    if (refCopy.has("refExtended") && refCopy.get("refExtended").isJsonObject()) {
                        for (Map.Entry<String, JsonElement> entry : refCopy.get("refExtended").getAsJsonObject()
                                .entrySet())
                            if (!currentObject.has(entry.getKey())) {
                                currentObject.add(entry.getKey(), entry.getValue());
                                printOutput(": [DIRTY] ADDED EXTENSION " + entry.getKey() + " to " + fi.getName()
                                        + " for [" + fo.getObjectId() + "/"
                                        + fo.getObjectType() + "]");
                                objectModel.setDirty();
                            }
                    }

                    // TODO move to constant
                    if (refCopy.has("resolveAlways")) {
                        currentObject.remove("resolveAlways");
                        currentObject.add("resolveAlways", refCopy.get("resolveAlways"));
                    }
                    if (isLookup(fi)) {
                        String lookup = getLookup(fi);
                        JsonElement lookupValue = objectProperties.get(lookup);
                        if (lookupValue.isJsonObject()) {
                            JsonObject lookupObject = lookupValue.getAsJsonObject();
                            if (lookupObject.has(FactoredObjectRef.TARGET_ID_FIELD)) {
                                String lookupTargetId =
                                        lookupObject.get(FactoredObjectRef.TARGET_ID_FIELD).getAsString();
                                String lookupTargetType =
                                        lookupObject.get(FactoredObjectRef.TARGET_TYPE_FIELD).getAsString();
                                String currentTargetId = currentObject.has(FactoredObjectRef.TARGET_ID_FIELD)
                                        ? currentObject.get(FactoredObjectRef.TARGET_ID_FIELD).getAsString()
                                        : "";
                                String currentTargetType = currentObject.has(FactoredObjectRef.TARGET_TYPE_FIELD)
                                        ? currentObject.get(FactoredObjectRef.TARGET_TYPE_FIELD).getAsString()
                                        : "";
                                if ((!lookupTargetId.equals(currentTargetId)
                                        && currentTargetType.equals(lookupTargetType))
                                        || !CommonUtils.isValid(currentTargetId)) {
                                    currentObject.remove(FactoredObjectRef.TARGET_ID_FIELD);
                                    currentObject.add(FactoredObjectRef.TARGET_ID_FIELD,
                                            lookupObject.get(FactoredObjectRef.TARGET_ID_FIELD));
                                    printOutput("[DIRTY] UPDATED " + fi.getName() + " for [" + fo.getObjectId() + "/"
                                            + fo.getObjectType() + "]"
                                            + "\n\tSET LOOKUP TARGET ID TO ["
                                            + lookupObject.get(FactoredObjectRef.TARGET_ID_FIELD).toString()
                                            + "]");
                                    objectModel.setDirty();
                                }
                            }
                        }
                    }
                    result = true;
                } else if (currentObject.has(FIELDS.FACTORY_ID)) {
                    // it will be replaced with reference on next layout
                    printOutput(": [DIRTY] FOUND OBJECT " + fi.getName() + " for [" + fo.getObjectId() + "/"
                            + fo.getObjectType() + "]");
                    objectModel.setDirty();
                    result = true;
                }
            } else {
                printOutput(
                        "[CHECK REF] CURRENT VALIE [" + currentValue.toString() + "] IS NOT OBJECT IN " + fi.getName()
                                + " for [" + fo.getObjectId() + "/" + fo.getObjectType() + "]");

            }

            if (!result) {
                // we have something which is not a ref and not a object to produce
                if (fi.isRefByValueOnly()) {
                    // we'll treat it as resolved ref value
                    ignores.remove(fi.getName());
                    ignores.addProperty(fi.getName(), true);
                    printOutput(": [DIRTY] CONSIDER VALUE [" + currentValue.toString() + "] AS RESOLVED; IGNORE "
                            + fi.getName() + " for [" + fo.getObjectId() + "/" + fo.getObjectType() + "]");
                    objectModel.setDirty();
                } else {
                    // we'll treat current value as targetId if it's not null and not array
                    if (currentValue.isJsonPrimitive()) {
                        refCopy.remove(FIELDS.DEFAULT_VALUE);
                        refCopy.add(FactoredObjectRef.TARGET_ID_FIELD, currentValue);
                        refCopy.remove(FactoredObjectRef.TARGET_TYPE_FIELD);
                        refCopy.addProperty(FactoredObjectRef.TARGET_TYPE_FIELD, fi.getType());
                        printOutput(": [DIRTY] CONSIDER VALUE [" + currentValue.toString() + "] AS TARGET ID in "
                                + fi.getName() + " for [" + fo.getObjectId() + "/" + fo.getObjectType() + "]");
                        JSONUtils.replaceValue(objectProperties, fi.getName(), refCopy);
                        objectModel.setDirty();
                    } else {
                        printOutput("! [WARN] IGNORE VALUE [" + currentValue.toString() + "] in " + fi.getName()
                                + " for [" + fo.getObjectId() + "/" + fo.getObjectType() + "]");
                    }
                }
                result = true;
            }
        }
        return result;
    }
}
