package ru.yandex.qe.dispenser.api.v1;

import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import ru.yandex.qe.dispenser.api.DtoBuilder;
import ru.yandex.qe.dispenser.api.util.JsonDeserializerBase;
import ru.yandex.qe.dispenser.api.util.JsonSerializerBase;

@JsonSerialize(using = DiMetaValueSet.Serializer.class)
@JsonDeserialize(using = DiMetaValueSet.Deserializer.class)
public final class DiMetaValueSet {
    public static final DiMetaValueSet EMPTY = builder().build();

    @NotNull
    private final Map<DiMetaField<?>, ?> values;

    private DiMetaValueSet(@NotNull final Builder builder) {
        values = new LinkedHashMap<>(builder.values);
    }

    @NotNull
    public static Builder builder() {
        return new Builder();
    }

    @NotNull
    public Set<DiMetaField<?>> getKeys() {
        return values.keySet();
    }

    @Nullable
    public <T> T getValue(@NotNull final DiMetaField<T> meta) {
        final Object value = values.get(meta);
        if (value == null) {
            return null;
        }
        if (!meta.getType().getDataClass().isInstance(value)) {
            final String message = String.format(
                    "Value has invalid type! Required: '%s', actual: '%s'",
                    meta.getType().getDataClass().getSimpleName(), value.getClass().getSimpleName()
            );
            throw new IllegalArgumentException(message);
        }
        return meta.getType().getDataClass().cast(value);
    }

    @NotNull
    public <T> T getValue(@NotNull final DiMetaField<T> meta, @NotNull final T defaultValue) {
        final T value = getValue(meta);
        return value != null ? value : defaultValue;
    }

    public static final class Builder implements DtoBuilder<DiMetaValueSet> {
        @NotNull
        private final Map<DiMetaField<?>, Object> values = new LinkedHashMap<>();

        private Builder() {
        }

        @NotNull
        public <T> Builder set(@NotNull final DiMetaField<? extends T> key, @NotNull final T value) {
            values.put(key, value);
            return this;
        }

        @NotNull
        @Override
        public DiMetaValueSet build() {
            return new DiMetaValueSet(this);
        }
    }

    static final class Serializer extends JsonSerializerBase<DiMetaValueSet> {
        @Override
        public void serialize(@NotNull final DiMetaValueSet metaValues,
                              @NotNull final JsonGenerator jg,
                              @NotNull final SerializerProvider sp) throws IOException {
            jg.writeStartObject();
            for (final DiMetaField<?> meta : metaValues.getKeys()) {
                jg.writeObjectFieldStart(meta.getKey());
                jg.writeStringField("type", meta.getType().getName());
                jg.writeObjectField("value", metaValues.getValue(meta));
                jg.writeEndObject();
            }
            jg.writeEndObject();
        }
    }

    static final class Deserializer extends JsonDeserializerBase<DiMetaValueSet> {
        @NotNull
        @Override
        public DiMetaValueSet deserialize(@NotNull final JsonParser jp,
                                          @NotNull final DeserializationContext dc) throws IOException {
            final Builder builder = builder();
            toJson(jp).fields().forEachRemaining(e -> {
                final String key = e.getKey();
                final JsonNode valueJson = e.getValue();
                if (!valueJson.has("type")) {
                    throw new IllegalArgumentException("Field 'type' required!");
                }
                final String typeName = valueJson.get("type").asText();
                final DiMetaField.Type<?> metaType = DiMetaField.Type.byName(typeName);
                if (!valueJson.has("value")) {
                    throw new IllegalArgumentException("Field 'value' required!");
                }
                final Object value = metaType.parse(valueJson.get("value"));
                builder.set(DiMetaField.of(key, metaType), value);
            });
            return builder.build();
        }
    }
}
