package io.zentity.resolution.input;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.util.ClassUtil;
import io.zentity.common.Json;
import io.zentity.common.Patterns;
import io.zentity.model.ValidationException;
import io.zentity.resolution.input.value.Value;

import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

public class Attribute {

    private final String name;
    private Map<String, String> params = new TreeMap<>();
    private String type;
    private Set<Value> values = new TreeSet<>();

    public Attribute(String name, String type, JsonNode json) throws ValidationException, JsonProcessingException {
        validateName(name);
        validateType(type);
        this.name = name;
        this.type = type;
        this.deserialize(json);
    }

    public Attribute(String name, String type, String json) throws ValidationException, IOException {
        validateName(name);
        validateType(type);
        this.name = name;
        this.type = type;
        this.deserialize(json);
    }

    public Attribute(String name, String type) throws ValidationException {
        validateName(name);
        validateType(type);
        this.name = name;
        this.type = type;
    }

    public String name() {
        return this.name;
    }

    public Map<String, String> params() {
        return this.params;
    }

    public String type() {
        return this.type;
    }

    public Set<Value> values() {
        return this.values;
    }

    private void validateName(String value) throws ValidationException {
        if (Patterns.EMPTY_STRING.matcher(value).matches())
            throw new ValidationException("'attributes' has an attribute with empty name.");
    }

    /**
     * Validate the value of "attributes".ATTRIBUTE_NAME."type".
     * Must be a non-empty string containing a recognized type.
     *
     * @param value The value of "attributes".ATTRIBUTE_NAME."type".
     * @throws ValidationException
     */
    private void validateType(String value) throws ValidationException {
        if (Patterns.EMPTY_STRING.matcher(value).matches())
            throw new ValidationException("'attributes." + this.name + ".type'' must not be empty.");
        if (!io.zentity.model.Attribute.VALID_TYPES.contains(value))
            throw new ValidationException("'attributes." + this.name + ".type' has an unrecognized type '" + value + "'.");
    }

    /**
     * Parse a single input attribute value. The following examples are all valid attribute structures, although the
     * first example would be converted to the second example.
     * <pre>
     *  {
     *   ATTRIBUTE_NAME: [
     *     ATTRIBUTE_VALUE,
     *     ...
     *   ]
     * }
     *
     * {
     *   ATTRIBUTE_NAME: {
     *     "values": [
     *       ATTRIBUTE_VALUE,
     *       ...
     *     ]
     *   }
     * }
     *
     * {
     *   ATTRIBUTE_NAME: {
     *     "values": [
     *       ATTRIBUTE_VALUE,
     *       ...
     *     ],
     *     "params": {
     *       ATTRIBUTE_PARAM_FIELD: ATTRIBUTE_PARAM_VALUE,
     *       ...
     *     }
     *   }
     * }
     *
     * {
     *   ATTRIBUTE_NAME: {
     *     "params": {
     *       ATTRIBUTE_PARAM_FIELD: ATTRIBUTE_PARAM_VALUE,
     *       ...
     *     }
     *   }
     * }
     * </pre>
     *
     * @param json Attribute object of an entity model.
     * @throws ValidationException
     */
    public void deserialize(JsonNode json) throws ValidationException, JsonProcessingException {
        if (json.isNull())
            return;
        if (!json.isObject() && !json.isArray())
            throw new ValidationException("'attributes." + this.name + "' must be an object or array.");

        Iterator<JsonNode> valuesNode = ClassUtil.emptyIterator();
        Iterator<Map.Entry<String, JsonNode>> paramsNode = ClassUtil.emptyIterator();

        // Parse values from array
        if (json.isArray()) {
            valuesNode = json.elements();

        } else if (json.isObject()) {

            // Parse values from object
            if (json.has("values")) {
                if (!json.get("values").isArray())
                    throw new ValidationException("'attributes." + this.name + ".values' must be an array.");
                valuesNode = json.get("values").elements();
            }

            // Parse params from object
            if (json.has("params")) {
                if (!json.get("params").isObject())
                    throw new ValidationException("'attributes." + this.name + ".params' must be an object.");
                paramsNode = json.get("params").fields();
            }
        } else {
            throw new ValidationException("'attributes." + this.name + "' must be an object or array.");
        }

        // Set any values or params that were specified in the input.
        while (valuesNode.hasNext()) {
            JsonNode valueNode = valuesNode.next();
            this.values().add(Value.create(this.type, valueNode));
        }

        // Set any params that were specified in the input, with the values serialized as strings.
        while (paramsNode.hasNext()) {
            Map.Entry<String, JsonNode> paramNode = paramsNode.next();
            String paramField = paramNode.getKey();
            JsonNode paramValue = paramNode.getValue();
            if (paramValue.isObject() || paramValue.isArray())
                this.params().put(paramField, Json.MAPPER.writeValueAsString(paramValue));
            else if (paramValue.isNull())
                this.params().put(paramField, "null");
            else
                this.params().put(paramField, paramValue.asText());
        }
    }

    public void deserialize(String json) throws ValidationException, IOException {
        deserialize(Json.MAPPER.readTree(json));
    }
}