package ru.yandex.autodoc.wmtools.util;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import ru.yandex.wmtools.common.error.InternalException;
import ru.yandex.wmtools.common.error.InternalProblem;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.util.ArrayList;

/**
 * @author avhaliullin
 */
public final class JsonUtil {
    private JsonUtil() {
    }

    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    //Private asserts

    private static JsonNode assertString(JsonNode node, String errPrefix) throws InternalException {
        if (!node.isTextual()) {
            throw new InternalException(InternalProblem.PROCESSING_ERROR, errPrefix + "Textual JSON node expected, but found " + String.valueOf(node));
        }
        return node;
    }

    private static JsonNode assertObject(JsonNode node, String errPrefix) throws InternalException {
        if (!node.isObject()) {
            throw new InternalException(InternalProblem.PROCESSING_ERROR, errPrefix + "Object JSON node expected, but found " + String.valueOf(node));
        }
        return node;
    }

    private static JsonNode assertArray(JsonNode node, String errPrefix) throws InternalException {
        if (!node.isArray()) {
            throw new InternalException(InternalProblem.PROCESSING_ERROR, errPrefix + "Array JSON node expected, but found " + String.valueOf(node));
        }
        return node;
    }

    private static JsonNode assertInt(JsonNode node, String errPrefix) throws InternalException {
        if (!node.isInt()) {
            throw new InternalException(InternalProblem.PROCESSING_ERROR, errPrefix + "Integer JSON node expected, but found " + String.valueOf(node));
        }
        return node;
    }

    private static JsonNode assertBoolean(JsonNode node, String errPrefix) throws InternalException {
        if (!node.isBoolean()) {
            throw new InternalException(InternalProblem.PROCESSING_ERROR, errPrefix + "Boolean JSON node expected, but found " + String.valueOf(node));
        }
        return node;
    }

    // Public asserts

    public static JsonNode assertString(JsonNode node) throws InternalException {
        return assertString(node, "");
    }

    public static JsonNode assertObject(JsonNode node) throws InternalException {
        return assertObject(node, "");
    }

    public static JsonNode assertArray(JsonNode node) throws InternalException {
        return assertArray(node, "");
    }

    public static JsonNode assertInt(JsonNode node) throws InternalException {
        return assertInt(node, "");
    }

    public static JsonNode assertBoolean(JsonNode node) throws InternalException {
        return assertBoolean(node, "");
    }

    // Private converters

    private static Iterable<JsonNode> asArray(JsonNode node, String errPrefix) throws InternalException {
        return assertArray(node, errPrefix);
    }

    private static String asString(JsonNode node, String errPrefix) throws InternalException {
        return assertString(node, errPrefix).asText();
    }

    private static int asInt(JsonNode node, String errPrefix) throws InternalException {
        return assertInt(node, errPrefix).asInt();
    }

    private static boolean asBoolean(JsonNode node, String errPrefix) throws InternalException {
        return assertBoolean(node, errPrefix).asBoolean();
    }

    // Public converters

    public static Iterable<JsonNode> asArray(JsonNode node) throws InternalException {
        return assertArray(node);
    }

    public static String asString(JsonNode node) throws InternalException {
        return assertString(node).asText();
    }

    public static int asInt(JsonNode node) throws InternalException {
        return assertInt(node).asInt();
    }

    public static boolean asBoolean(JsonNode node) throws InternalException {
        return assertBoolean(node).asBoolean();
    }

    // * Fields
    // ** Required

    public static JsonNode getRequiredField(JsonNode node, String field) throws InternalException {
        if (!assertObject(node).has(field)) {
            throw new InternalException(InternalProblem.PROCESSING_ERROR,
                    "Field \"" + field + "\" not found: " + String.valueOf(node));
        }
        return node.get(field);
    }

    public static String getRequiredFieldAsString(JsonNode node, String field) throws InternalException {
        return asString(getRequiredField(node, field), errPrefix(node, field));
    }

    public static Iterable<JsonNode> getRequiredFieldAsArray(JsonNode node, String field) throws InternalException {
        return asArray(getRequiredField(node, field), errPrefix(node, field));
    }

    public static int getRequiredFieldAsInt(JsonNode node, String field) throws InternalException {
        return asInt(getRequiredField(node, field), errPrefix(node, field));
    }

    public static boolean getRequiredFieldAsBoolean(JsonNode node, String field) throws InternalException {
        return asBoolean(getRequiredField(node, field), errPrefix(node, field));
    }

    // ** Other

    public static JsonNode getField(JsonNode node, String field) throws InternalException {
        if (!assertObject(node).has(field)) {
            return null;
        }
        JsonNode result = node.get(field);
        if (result.isNull()) {
            return null;
        }
        return result;
    }

    public static String getFieldAsString(JsonNode node, String field) throws InternalException {
        JsonNode res = getField(node, field);
        if (res == null) {
            return null;
        }
        return asString(res, errPrefix(node, field));
    }

    public static Iterable<JsonNode> getFieldAsArray(JsonNode node, String field) throws InternalException {
        JsonNode res = getField(node, field);
        if (res == null) {
            return null;
        }
        return asArray(res, errPrefix(node, field));
    }

    public static Iterable<JsonNode> getFieldAsArrayEmptyIfNull(JsonNode node, String field) throws InternalException {
        JsonNode res = getField(node, field);
        if (res == null) {
            return new ArrayList<>();
        }
        return asArray(res, errPrefix(node, field));
    }

    public static Integer getFieldAsInt(JsonNode node, String field) throws InternalException {
        JsonNode res = getField(node, field);
        if (res == null) {
            return null;
        }
        return asInt(res, errPrefix(node, field));
    }

    public static Boolean getFieldAsBoolean(JsonNode node, String field) throws InternalException {
        JsonNode res = getField(node, field);
        if (res == null) {
            return null;
        }
        return asBoolean(res, errPrefix(node, field));
    }

    // * JSON parsing methods

    public static JsonNode readTree(Reader reader) throws InternalException {
        try {
            return OBJECT_MAPPER.readTree(reader);
        } catch (IOException e) {
            throw new InternalException(InternalProblem.PROCESSING_ERROR, "Json processing error", e);
        }
    }

    public static JsonNode readTree(InputStream is) throws InternalException {
        try {
            return OBJECT_MAPPER.readTree(is);
        } catch (IOException e) {
            throw new InternalException(InternalProblem.PROCESSING_ERROR, "Json processing error", e);
        }
    }

    public static JsonNode readTree(String content) throws InternalException {
        try {
            return OBJECT_MAPPER.readTree(content);
        } catch (IOException e) {
            throw new InternalException(InternalProblem.PROCESSING_ERROR, "Json processing error", e);
        }
    }

    public static JsonNode readTree(byte[] content) throws InternalException {
        try {
            return OBJECT_MAPPER.readTree(content);
        } catch (IOException e) {
            throw new InternalException(InternalProblem.PROCESSING_ERROR, "Json processing error", e);
        }
    }

    private static String errPrefix(JsonNode node, String field) {
        return "At field \"" + field + "\" of node " + String.valueOf(node) + " :\n";
    }
}
