/*
 * Copyright (c) 2022 MiLaboratories Inc.
 *
 * 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.
 */
package com.milaboratory.util;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.databind.node.ObjectNode;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public final class JsonOverrider {
    /** Global setting */
    public static boolean suppressSameValueOverride = false;

    public static <T> T override(T object, Class<? super T> clazz, String... commands) {
        JsonNode node = GlobalObjectMappers.getOneLine().valueToTree(object);
        for (String command : commands)
            if (!overrideNode(node, command))
                return null;
        try {
            return (T) GlobalObjectMappers.getOneLine().treeToValue(node, clazz);
        } catch (JsonProcessingException e) {
            throw new IllegalArgumentException();
        }
    }

    public static <T> T override(T object, Class<? super T> clazz, Map<String, String> overrideMap) {
        return override(GlobalObjectMappers.getOneLine(), object, clazz, overrideMap);
    }

    public static <T> T override(ObjectMapper objectMapper, T object, Class<? super T> clazz, Map<String, String> overrideMap) {
        JsonNode node = objectMapper.valueToTree(object);
        for (Map.Entry<String, String> entry : overrideMap.entrySet())
            if (!overrideNode(node, entry.getKey(), entry.getValue()))
                return null;
        try {
            return (T) objectMapper.treeToValue(node, clazz);
        } catch (JsonProcessingException e) {
            throw new IllegalArgumentException(e);
        }
    }

    public static boolean overrideNode(JsonNode node, String command) {
        String[] split = command.split("=", 2);
        String path = split[0];
        String value = split[1];
        return overrideNode(node, path, value);
    }

    public static boolean overrideNode(JsonNode node, String path, String value) {
        return overrideNode1(node, path, value, false);
    }

    private static boolean overrideNode1(JsonNode node, String path, String value, boolean v) {
        if (node == null)
            return false;
        value = value.replaceAll("^[\'\"]", "").replaceAll("[\'\"]$", "");
        boolean b = false;
        if (override0(node, path, value))
            b = true;
        else {
            Iterator<JsonNode> iterator = node.iterator();
            while (iterator.hasNext())
                if (overrideNode1(iterator.next(), path, value, b || v))
                    b = true;
        }
        if (v && b)
            throw new IllegalArgumentException("Multiple matches of parameter " + path);
        return b;
    }

    private static void overrideWarn(String fieldName, String newValue) {
        if (!suppressSameValueOverride)
            System.out.printf("WARNING: unnecessary override -O%s=%s with the same value.\n", fieldName, newValue);
    }

    public static boolean override0(JsonNode node, String path, String value) {
        String[] pathArray = path.split("[.\\[\\]]");
        for (int i = 0; i < pathArray.length - 1; ++i) {
            String pathPart = pathArray[i];
            if (pathPart.equals("")) continue;
            try {
                int index = Integer.decode(pathPart);
                if ((node = node.get(index)) == null)
                    return false;
            } catch (NumberFormatException e) {
                if ((node = node.get(pathPart)) == null)
                    return false;
            }
        }

        String fieldName = pathArray[pathArray.length - 1];

        boolean setToNull = value.equalsIgnoreCase("null");

        if (!(node instanceof ObjectNode))
            return false;

        ObjectNode oNode = (ObjectNode) node;

        JsonNode valueNode = oNode.get(fieldName);
        if (valueNode == null) {
            if (setToNull)
                overrideWarn(fieldName, value);
            return setToNull;
        }

        JsonNode valueTree = null;
        if (value.startsWith("{") && value.endsWith("}")) {
            try {
                valueTree = GlobalObjectMappers.getOneLine().readTree(value);
            } catch (Throwable ignored) {
            }
        }

        if (valueNode instanceof ArrayNode) {
            ArrayNode arrayNode = (ArrayNode) valueNode;
            List<String> oldValues = new ArrayList<>();
            final Iterator<JsonNode> it = arrayNode.elements();
            while (it.hasNext())
                oldValues.add(it.next().asText());

            arrayNode.removeAll();

            boolean settingTheSame;
            if (!value.startsWith("[") || !value.endsWith("]")) {
                arrayNode.add(value);
                settingTheSame = oldValues.size() == 1 && oldValues.get(0).equalsIgnoreCase(value);
            } else {
                value = value.substring(1, value.length() - 1);
                String[] values = ParseUtil.splitWithBrackets(value, ',', "(){}[]");
                settingTheSame = true;
                for (int i = 0; i < values.length; i++) {
                    arrayNode.add(values[i]);
                    if (settingTheSame && oldValues.size() > i)
                        settingTheSame = oldValues.get(i).equalsIgnoreCase(values[i]);
                }
            }
            if (settingTheSame)
                overrideWarn(fieldName, value);
            return true;
        } else if (valueTree != null) {
            oNode.set(fieldName, valueTree);
            return true;
        } else if (valueNode.isTextual()) {
            if (valueNode.asText().equals(value))
                overrideWarn(fieldName, value);
            oNode.put(fieldName, value);
            return true;
        } else if (valueNode.isBoolean()) {
            boolean v;
            if (value.equalsIgnoreCase("true"))
                v = true;
            else if (value.equalsIgnoreCase("false"))
                v = false;
            else
                return false;
            if (v == valueNode.asBoolean())
                overrideWarn(fieldName, value);
            oNode.put(fieldName, v);
            return true;
        } else if (valueNode.isIntegralNumber()) {
            long v;
            try {
                v = Long.parseLong(value);
            } catch (NumberFormatException e) {
                return false;
            }
            if (v == valueNode.asLong())
                overrideWarn(fieldName, value);
            oNode.put(fieldName, v);
            return true;
        } else if (valueNode.isFloatingPointNumber()) {
            double v;
            try {
                v = Double.parseDouble(value);
            } catch (NumberFormatException e) {
                return false;
            }
            if (v == valueNode.asDouble())
                overrideWarn(fieldName, value);
            oNode.put(fieldName, v);
            return true;
        } else if (valueNode.isObject() && setToNull) {
            if (valueNode.isNull())
                overrideWarn(fieldName, value);
            oNode.set(fieldName, NullNode.getInstance());
            return true;
        } else if (valueNode.isNull()) {
            oNode.put(fieldName, value);
            return true;
        }
        return false;
    }

    public static JsonNode getNodeByPath(JsonNode node, String path) {
        return getNodeByPath(node, path.split("\\."));
    }

    public static JsonNode getNodeByPath(JsonNode node, String[] pathArray) {
        for (int i = 0; i < pathArray.length; ++i)
            if ((node = node.get(pathArray[i])) == null)
                return null;
        return node;
    }
}
