package ru.yandex.webmaster3.storage.util.ydb.querybuilder.typesafe;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Collections;
import java.util.Date;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.google.protobuf.GeneratedMessageV3;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Parser;
import com.yandex.ydb.table.result.ResultSetReader;
import com.yandex.ydb.table.values.PrimitiveType;
import com.yandex.ydb.table.values.PrimitiveValue;
import com.yandex.ydb.table.values.Type;
import com.yandex.ydb.table.values.Value;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.joda.time.DateTime;
import org.joda.time.Duration;

import ru.yandex.webmaster3.core.util.enums.EnumResolver;
import ru.yandex.webmaster3.core.util.enums.IntEnum;
import ru.yandex.webmaster3.core.util.enums.IntEnumResolver;
import ru.yandex.webmaster3.core.WebmasterException;
import ru.yandex.webmaster3.core.data.WebmasterHostId;
import ru.yandex.webmaster3.core.http.WebmasterErrorResponse;
import ru.yandex.webmaster3.core.util.GzipUtils;
import ru.yandex.webmaster3.core.util.IdUtils;
import ru.yandex.webmaster3.core.util.functional.Bijection;
import ru.yandex.webmaster3.core.util.json.JsonMapping;
import ru.yandex.webmaster3.storage.util.JsonDBMapping;
import ru.yandex.webmaster3.storage.util.yt.YtPath;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * ishalaru
 * 16.06.2020
 **/
public class Fields {

    public static final TypeReference<List<YtPath>> YT_PATH_LIST_REFERENCE = new TypeReference<>() {
    };
    public static final TypeReference<List<UUID>> UUID_LIST_REFERENCE = new TypeReference<>() {
    };

    protected static abstract class AbstractAnyField<T> implements Field<T> {
        protected final String name;
        private final T defaultValue;
        private final Type type;


        public AbstractAnyField(String name, Type type, T defaultValue) {
            this.name = name;
            this.defaultValue = defaultValue;
            this.type = type;
        }

        @Override
        public Type getType() {
            return type;
        }

        @Override
        public String getName() {
            return name;
        }

        @Override
        public T asDBValue(T value) {
            return value;
        }

        @Override
        public final T get(ResultSetReader row) {
            return getWithDefault(row, defaultValue);
        }

        protected abstract T getWithDefault(ResultSetReader row, T defaultValue);

        @Override
        public String toString() {
            return getName();
        }

        public Field<T> withDefault(T value) {
            Fields.AbstractAnyField<T> outer = this;
            return new Fields.AbstractAnyField<T>(name, type, value) {
                @Override
                protected T getWithDefault(ResultSetReader row, T defaultValue) {
                    return outer.getWithDefault(row, defaultValue);
                }

                @Override
                public Field<T> withDefault(T value) {
                    return outer.withDefault(value);
                }

                @Override
                public Value get(T item) {
                    return outer.get(item);
                }
            };
        }
    }

    protected static class DelegateAnyField<S, T> implements Field<T> {
        private final Field<S> delegate;
        private final Bijection<S, T> bijection;

        public DelegateAnyField(Field<S> delegate, Bijection<S, T> bijection) {
            this.delegate = delegate;
            this.bijection = bijection;
        }

        @Override
        public String getName() {
            return delegate.getName();
        }

        @Override
        public Object asDBValue(T value) {
            return delegate.asDBValue(unwrapNullable(value));
        }


        @Override
        public final T get(ResultSetReader row) {
            return wrapNullable(delegate.get(row));
        }

        @Override
        public Value get(T item) {
            return delegate.get(bijection.rightToLeft(item));
        }

        @Override
        public Field<T> withDefault(T t) {
            return new DelegateAnyField<>(delegate.withDefault(bijection.rightToLeft(t)), bijection);
        }

        protected S unwrapNullable(T t) {
            if (t == null) {
                return null;
            } else {
                return bijection.rightToLeft(t);
            }
        }

        protected T wrapNullable(S s) {
            if (s == null) {
                return null;
            } else {
                return bijection.leftToRight(s);
            }
        }

        public <R> Field<R> map(Bijection<T, R> mapper) {
            return new DelegateAnyField<>(this, mapper);
        }

        @Override
        public Type getType() {
            return delegate.getType();
        }
    }

    public interface MapValueElementDriver<S, T> extends Bijection<S, T> {
        Class<S> getElementClass();

        S elemToDB(T value);

        T elemFromDB(S value);

        @Override
        default T leftToRight(S s) {
            return elemFromDB(s);
        }

        @Override
        default S rightToLeft(T t) {
            return elemToDB(t);
        }
    }

    public interface ListElementDriver<S, T> extends MapValueElementDriver<S, T> {
        List<S> listToDB(List<T> elems);

        List<T> listFromDB(List<S> elems);
    }

    private static abstract class AbstractListElementDriver<S, T> implements ListElementDriver<S, T> {
        private final Class<S> elementClass;

        public AbstractListElementDriver(Class<S> elementClass) {
            this.elementClass = elementClass;
        }

        @Override
        public Class<S> getElementClass() {
            return elementClass;
        }
    }

    public interface SetElementDriver<S, T> extends ListElementDriver<S, T> {
        Set<S> setToDB(Set<T> elems);

        Set<T> setFromDB(Set<S> elems);
    }

    public interface MapKeyElementDriver<KS, K> extends SetElementDriver<KS, K> {
        <V, VS> Map<KS, VS> mapToDB(Map<K, V> map, Function<V, VS> valueMapper);

        <V, VS> Map<K, V> mapFromDB(Map<KS, VS> map, Function<VS, V> valueMapper);
    }

    private static class IdentitySetElementDriver<T> extends AbstractListElementDriver<T, T> implements SetElementDriver<T, T> {
        public IdentitySetElementDriver(Class<T> elementClass) {
            super(elementClass);
        }

        @Override
        public List<T> listToDB(List<T> elems) {
            return elems;
        }

        @Override
        public List<T> listFromDB(List<T> elems) {
            return elems;
        }

        @Override
        public T elemToDB(T t) {
            return t;
        }

        @Override
        public T elemFromDB(T t) {
            return t;
        }

        @Override
        public Set<T> setToDB(Set<T> elems) {
            return elems;
        }

        @Override
        public Set<T> setFromDB(Set<T> elems) {
            return elems;
        }
    }

    private static class MappingListElementDriver<S, T> extends AbstractListElementDriver<S, T> {
        protected final Bijection<S, T> bijection;

        public MappingListElementDriver(Class<S> elementClass, Bijection<S, T> bijection) {
            super(elementClass);
            this.bijection = bijection;
        }

        @Override
        public S elemToDB(T value) {
            return bijection.rightToLeft(value);
        }

        @Override
        public T elemFromDB(S value) {
            return bijection.leftToRight(value);
        }

        @Override
        public List<S> listToDB(List<T> elems) {
            if (CollectionUtils.isEmpty(elems)) {
                return Collections.emptyList();
            }
            return elems.stream().map(bijection::rightToLeft).collect(Collectors.toList());
        }

        @Override
        public List<T> listFromDB(List<S> elems) {
            return elems.stream().map(bijection::leftToRight).collect(Collectors.toList());
        }
    }

    private static class MappingSetElementDriver<S, T> extends MappingListElementDriver<S, T> implements SetElementDriver<S, T> {
        protected final Supplier<Set<T>> setSupplier;

        public MappingSetElementDriver(Class<S> elementClass, Bijection<S, T> bijection, Supplier<Set<T>> setSupplier) {
            super(elementClass, bijection);
            this.setSupplier = setSupplier;
        }

        @Override
        public Set<S> setToDB(Set<T> elems) {
            if (CollectionUtils.isEmpty(elems)) {
                return Collections.emptySet();
            }
            return elems.stream().map(bijection::rightToLeft).collect(Collectors.toSet());
        }

        @Override
        public Set<T> setFromDB(Set<S> elems) {
            return elems
                    .stream()
                    .map(bijection::leftToRight)
                    .filter(x -> x != null)
                    .collect(Collectors.toCollection(setSupplier));
        }
    }

    private interface MapSupplier<K> {
        <V> Map<K, V> createMap();
    }

    private static class MappingMapKeyElementDriver<KS, K> extends MappingSetElementDriver<KS, K> implements MapKeyElementDriver<KS, K> {
        private final MapSupplier<K> mapSupplier;

        public MappingMapKeyElementDriver(Class<KS> keyClass, Bijection<KS, K> bijection, Supplier<Set<K>> setSupplier, MapSupplier<K> mapSupplier) {
            super(keyClass, bijection, setSupplier);
            this.mapSupplier = mapSupplier;
        }

        @Override
        public <V, VS> Map<KS, VS> mapToDB(Map<K, V> map, Function<V, VS> valueMapper) {
            if (MapUtils.isEmpty(map)) {
                return Collections.emptyMap();
            }
            Map<KS, VS> result = new HashMap<>();
            for (Map.Entry<K, V> entry : map.entrySet()) {
                result.put(bijection.rightToLeft(entry.getKey()), valueMapper.apply(entry.getValue()));
            }
            return result;
        }

        @Override
        public <V, VS> Map<K, V> mapFromDB(Map<KS, VS> map, Function<VS, V> valueMapper) {
            Map<K, V> result = mapSupplier.createMap();
            for (Map.Entry<KS, VS> entry : map.entrySet()) {
                K key = bijection.leftToRight(entry.getKey());
                if (key != null) {
                    result.put(key, valueMapper.apply(entry.getValue()));
                }
            }
            return result;
        }
    }

    private static class IdentityMapKeyElementDriver<K> extends IdentitySetElementDriver<K> implements MapKeyElementDriver<K, K> {
        public IdentityMapKeyElementDriver(Class<K> elementClass) {
            super(elementClass);
        }

        @Override
        public <V, VS> Map<K, VS> mapToDB(Map<K, V> map, Function<V, VS> valueMapper) {
            Map<K, VS> result = new HashMap<>();
            for (Map.Entry<K, V> entry : map.entrySet()) {
                result.put(entry.getKey(), valueMapper.apply(entry.getValue()));
            }
            return result;
        }

        @Override
        public <V, VS> Map<K, V> mapFromDB(Map<K, VS> map, Function<VS, V> valueMapper) {
            Map<K, V> result = new HashMap<>();
            for (Map.Entry<K, VS> entry : map.entrySet()) {
                result.put(entry.getKey(), valueMapper.apply(entry.getValue()));
            }
            return result;
        }
    }

    public static final MapKeyElementDriver<Integer, Integer> INT_DRIVER = new IdentityMapKeyElementDriver<>(Integer.class);
    public static final MapKeyElementDriver<Long, Long> LONG_DRIVER = new IdentityMapKeyElementDriver<>(Long.class);
    public static final MapKeyElementDriver<String, String> STRING_DRIVER = new IdentityMapKeyElementDriver<>(String.class);
    public static final MapKeyElementDriver<UUID, UUID> UUID_DRIVER = new IdentityMapKeyElementDriver<>(UUID.class);
    public static final MapKeyElementDriver<Boolean, Boolean> BOOLEAN_DRIVER = new IdentityMapKeyElementDriver<>(Boolean.class);
    public static final MapKeyElementDriver<Date, DateTime> DATE_TIME_DRIVER = new MappingMapKeyElementDriver<Date, DateTime>(Date.class, Bijection.create(DateTime::new, DateTime::toDate), HashSet::new, HashMap::new);
    public static final MapKeyElementDriver<Date, org.joda.time.Instant> JODA_INSTANT_DRIVER = new MappingMapKeyElementDriver<Date, org.joda.time.Instant>(Date.class, Bijection.create(org.joda.time.Instant::new, org.joda.time.Instant::toDate), HashSet::new, HashMap::new);
    public static final MapKeyElementDriver<Long, Duration> JODA_DURATION_DRIVER = new MappingMapKeyElementDriver<Long, Duration>(Long.class, Bijection.create(Duration::millis, Duration::getMillis), HashSet::new, HashMap::new);
    public static final MapKeyElementDriver<String, WebmasterHostId> HOST_ID_DRIVER = new MappingMapKeyElementDriver<String, WebmasterHostId>(
            String.class,
            Bijection.create(IdUtils::stringToHostId, WebmasterHostId::toStringId),
            HashSet::new,
            HashMap::new
    );
    public static final MapKeyElementDriver<String, org.joda.time.LocalDate> LOCAL_DATE_DRIVER = new MappingMapKeyElementDriver<String, org.joda.time.LocalDate>(
            String.class,
            Bijection.create(org.joda.time.LocalDate::parse, org.joda.time.LocalDate::toString),
            HashSet::new,
            HashMap::new
    );
    public static final MapKeyElementDriver<String, org.joda.time.LocalDateTime> LOCAL_DATE_TIME_DRIVER = new MappingMapKeyElementDriver<String, org.joda.time.LocalDateTime>(
            String.class,
            Bijection.create(org.joda.time.LocalDateTime::parse, org.joda.time.LocalDateTime::toString),
            HashSet::new,
            HashMap::new
    );
    public static final ListElementDriver<String, URL> URL_DRIVER = new MappingListElementDriver<>(
            String.class,
            Bijection.create(s -> {
                        try {
                            return new java.net.URL(s);
                        } catch (MalformedURLException e) {
                            throw new WebmasterException("Failed to read url from DB", new WebmasterErrorResponse.InternalUnknownErrorResponse(Fields.class, ""), e);
                        }
                    },
                    java.net.URL::toExternalForm)
    );
    public static final MapKeyElementDriver<String, YtPath> YT_PATH_DRIVER = new MappingMapKeyElementDriver<String, YtPath>(
            String.class,
            Bijection.create(YtPath::fromString, YtPath::toString),
            HashSet::new,
            HashMap::new
    );

    public static <E extends Enum<E> & IntEnum> MapKeyElementDriver<Integer, E> intEnumDriver(IntEnumResolver<E> resolver) {
        return new MappingMapKeyElementDriver<>(
                Integer.class,
                Bijection.create(resolver::fromValueOrNull, e -> e.value() /* http://bugs.java.com/view_bug.do?bug_id=8049898 */),
                () -> EnumSet.noneOf(resolver.getEnumClass()),
                new MapSupplier<E>() {
                    @Override
                    public <V> Map<E, V> createMap() {
                        return new EnumMap<>(resolver.getEnumClass());
                    }
                });
    }

    public static <E extends Enum<E>> MapKeyElementDriver<String, E> stringEnumDriver(EnumResolver<E> resolver) {
        return new MappingMapKeyElementDriver<>(String.class, Bijection.create(resolver::valueOfOrNull, E::name), () -> EnumSet.noneOf(resolver.getEnumClass()), new MapSupplier<E>() {
            @Override
            public <V> Map<E, V> createMap() {
                return new EnumMap<>(resolver.getEnumClass());
            }
        });
    }


    private static <T> Field<T> dbField(String name, Function<ResultSetReader, T> mapper, Function<T, PrimitiveValue> invertMapper, Type type) {
        return new Fields.AbstractAnyField<T>(name, type, null) {
            @Override
            public PrimitiveValue get(T item) {
                return invertMapper.apply(item);
            }

            @Override
            public T getWithDefault(ResultSetReader row, T defaultValue) {
                return !row.getColumn(name).isOptionalItemPresent() ? defaultValue : mapper.apply(row);
            }
        };
    }

    // ydb type Json
    public static <K, V> Field<Map<K, V>> mapField(String name, TypeReference<Map<K, V>> typeReference) {
        return jsonField2(name, typeReference);
    }

    // ydb type Json
    public static <K> Field<Set<K>> setField(String name, TypeReference<Set<K>> typeReference) {
        return jsonField2(name, typeReference);
    }

    // ydb type Json
    public static <K> Field<List<K>> listField(String name, TypeReference<List<K>> typeReference) {
        return jsonField2(name, typeReference);
    }

    public static Field<String> stringField(String name) {
        return dbField(name, row -> row.getColumn(name).getUtf8(), item -> PrimitiveValue.utf8(item), PrimitiveType.utf8());
    }

    public static Field<String> stringRawField(String name) {
        return dbField(name, row -> new String(row.getColumn(name).getString()), item -> PrimitiveValue.string(item.getBytes()), PrimitiveType.string());
    }

    public static Field<Integer> intField(String name) {
        return dbField(name, row -> row.getColumn(name).getInt32(), item -> PrimitiveValue.int32(item), PrimitiveType.int32());
    }

    public static Field<Long> unsignedIntField(String name) {
        return dbField(name, row -> row.getColumn(name).getUint32(), item -> PrimitiveValue.int64(item), PrimitiveType.int64());
    }

    public static Field<Long> longField(String name) {
        return dbField(name, row -> row.getColumn(name).getInt64(), item -> PrimitiveValue.int64(item), PrimitiveType.int64());
    }

    public static Field<Long> unsignedLongField(String name) {
        return dbField(name, row -> row.getColumn(name).getUint64(), item -> PrimitiveValue.uint64(item), PrimitiveType.int64());
    }

    public static Field<Float> floatField(String name) {
        return dbField(name, row -> row.getColumn(name).getFloat32(), item -> PrimitiveValue.float32(item), PrimitiveType.float32());
    }

    public static Field<Double> doubleField(String name) {
        return dbField(name, row -> row.getColumn(name).getFloat64(), item -> PrimitiveValue.float64(item), PrimitiveType.float64());
    }

    public static Field<Boolean> boolField(String name) {
        return dbField(name, row -> row.getColumn(name).getBool(), item -> PrimitiveValue.bool(item), PrimitiveType.bool());
    }

    public static Field<LocalDate> dateField(String name) {
        return dbField(name, row -> row.getColumn(name).getDate(), item -> PrimitiveValue.date(item), PrimitiveType.date());
    }

    public static Field<byte[]> byteArrayField(String name) {
        return dbField(name, row -> row.getColumn(name).getString(), item -> PrimitiveValue.string(item), PrimitiveType.string());
    }
    public static Field<org.joda.time.LocalDate> jodaDateField(String name) {
        return dbField(name, row -> convert(row.getColumn(name).getDate()), item -> PrimitiveValue.date(convert(item)), PrimitiveType.date());
    }

    public static Field<Duration> jodaDurationField(String name) {
        return new DelegateAnyField<>(longField(name), JODA_DURATION_DRIVER);
    }

    public static Field<org.joda.time.Instant> jodaInstantField(String name) {
        return dbField(name, row -> convertInstant(row.getColumn(name).getTimestamp()), item -> PrimitiveValue.timestamp(convertInstant(item)), PrimitiveType.timestamp());
    }

    public static Field<org.joda.time.DateTime> jodaDateTimeField(String name) {
        return dbField(name, row -> convert(row.getColumn(name).getTimestamp()), item -> PrimitiveValue.timestamp(convert(item)), PrimitiveType.timestamp());
    }

    public static Field<LocalDateTime> localDateTimeField(String name) {
        return dbField(name, row -> row.getColumn(name).getDatetime(), item -> PrimitiveValue.tzDatetime(item.atZone(ZoneId.systemDefault())), PrimitiveType.tzDatetime());
    }

    public static Field<WebmasterHostId> hostIdField(String name) {
        return dbField(name, row -> IdUtils.stringToHostId(row.getColumn(name).getUtf8()), item -> PrimitiveValue.utf8(item.toString()), PrimitiveType.utf8());
    }

    public static Field<UUID> uuidField(String name) {
        return dbField(name, row -> UUID.fromString(row.getColumn(name).getUtf8()), item -> PrimitiveValue.utf8(item.toString()), PrimitiveType.utf8());
    }

    public static Field<YtPath> ytPathField(String name) {
        return dbField(name, row -> YtPath.fromString(row.getColumn(name).getUtf8()), item -> PrimitiveValue.utf8(item.toString()), PrimitiveType.utf8());
    }

    public static <E extends Enum<E> & IntEnum> Field<E> intEnumField(String name, IntEnumResolver<E> resolver) {
        return dbField(name, row -> resolver.fromValueOrNull(row.getColumn(name).getInt32()), item -> PrimitiveValue.int32(item.value()), PrimitiveType.int32());
    }

    public static <E extends Enum<E>> Field<E> stringEnumField(String name, EnumResolver<E> resolver) {
        return dbField(name, row -> resolver.valueOfOrNull(row.getColumn(name).getString(UTF_8)), item -> PrimitiveValue.string(item.name().getBytes(UTF_8)), PrimitiveType.string());
    }

    public static Field<String> jsonRawField(String name) {
        return dbField(name, row -> row.getColumn(name).getJson(), item -> PrimitiveValue.json(item), PrimitiveType.json());
    }

    public static <T> Field<T> jsonField(String name, TypeReference<T> typeReference) {
        return jsonField(name, typeReference, JsonDBMapping.OM);
    }

    public static <T> Field<T> jsonField(String name, TypeReference<T> typeReference, ObjectMapper objectMapper) {
        return new Fields.DelegateAnyField<>(stringRawField(name), jsonDriver(typeReference, objectMapper));
    }

    public static <T> Field<T> jsonField(String name, Class<T> clazz) {
        return jsonField(name, clazz, JsonDBMapping.OM);
    }

    public static <T> Field<T> jsonField(String name, Class<T> clazz, ObjectMapper objectMapper) {
        return new Fields.DelegateAnyField<>(stringRawField(name), jsonDriver(clazz, objectMapper));
    }

    public static <T> Field<T> jsonField2(String name, Class<T> clazz) {
        return new Fields.DelegateAnyField<>(jsonRawField(name), jsonDriver(clazz, JsonDBMapping.OM));
    }

    public static <T> Field<T> jsonField2(String name, TypeReference<T> clazz) {
        return new Fields.DelegateAnyField<>(jsonRawField(name), jsonDriver(clazz, JsonDBMapping.OM));
    }

    public static <T> Field<T> jsonField2(String name, TypeReference<T> clazz, ObjectMapper objectMapper) {
        return new Fields.DelegateAnyField<>(jsonRawField(name), jsonDriver(clazz, objectMapper));
    }

    public static <T> Field<T> compressedJsonField2(String name, TypeReference<T> clazz) {
        return new Fields.DelegateAnyField<>(byteArrayField(name), compressedJsonDriver(JsonDBMapping.OM.readerFor(clazz), JsonDBMapping.OM.writerFor(clazz)));
    }

    public static Field<byte[]> compressedStringField(String name) {
        return dbField(name, row -> GzipUtils.decompress(row.getColumn(name).getString()), item -> PrimitiveValue.string(GzipUtils.compress(item)), PrimitiveType.string());
    }

    public static <D extends GeneratedMessageV3> ListElementDriver<ByteBuffer, D> protobufDriver(Parser<D> parser) {
        return new MappingListElementDriver<>(ByteBuffer.class, Bijection.create(
                bb -> {
                    try {
                        return parser.parseFrom(bb.array());
                    } catch (InvalidProtocolBufferException e) {
                        throw new WebmasterException("Failed to parse proto message from cassandra",
                                new WebmasterErrorResponse.YDBErrorResponse(Fields.class, e), e);
                    }
                },
                data -> ByteBuffer.wrap(data.toByteArray())
        ));
    }

    private static <T> ListElementDriver<String, T> jsonDriver(ObjectReader reader, ObjectWriter writer) {
        return new MappingListElementDriver<>(String.class, Bijection.create(
                s -> {
                    try {
                        return s == null ? null : reader.readValue(s);
                    } catch (IOException e) {
                        throw new WebmasterException("Failed to deserialize object from YDB json field: " + s,
                                new WebmasterErrorResponse.YDBErrorResponse(Fields.class, e), e);
                    }
                },
                o -> {
                    try {
                        return writer.writeValueAsString(o);
                    } catch (JsonProcessingException e) {
                        throw new WebmasterException("Failed to serialize object to YDB json field",
                                new WebmasterErrorResponse.YDBErrorResponse(Fields.class, e), e);
                    }
                }
        ));
    }

    public static <T> Fields.ListElementDriver<String, T> jsonDriver(Class<T> clazz) {
        return jsonDriver(JsonMapping.OM.readerFor(clazz), JsonDBMapping.OM.writerFor(clazz));
    }

    public static <T> Fields.ListElementDriver<String, T> jsonDriver(Class<T> clazz, ObjectMapper objectMapper) {
        return jsonDriver(objectMapper.readerFor(clazz), objectMapper.writerFor(clazz));
    }

    public static <T> Fields.ListElementDriver<String, T> jsonDriver(TypeReference<T> typeReference) {
        return jsonDriver(JsonMapping.OM.readerFor(typeReference), JsonDBMapping.OM.writerFor(typeReference));
    }

    public static <T> Fields.ListElementDriver<String, T> jsonDriver(TypeReference<T> typeReference, ObjectMapper objectMapper) {
        return jsonDriver(objectMapper.readerFor(typeReference), objectMapper.writerFor(typeReference));
    }

    private static <T> ListElementDriver<byte[], T> compressedJsonDriver(ObjectReader reader, ObjectWriter writer) {
        return new MappingListElementDriver<>(byte[].class, Bijection.create(
                s -> {
                    if (s == null) {
                        return null;
                    }
                    try (InputStream is = new GZIPInputStream(new ByteArrayInputStream(s))) {
                        return reader.readValue(is);
                    } catch (IOException e) {
                        throw new WebmasterException("Failed to deserialize object from YDB json field: " + s,
                                new WebmasterErrorResponse.YDBErrorResponse(Fields.class, e), e);
                    }
                },
                o -> {
                    try (ByteArrayOutputStream os = new ByteArrayOutputStream(); GZIPOutputStream gzipOs = new GZIPOutputStream(os)) {
                        writer.writeValue(gzipOs, o);
                        gzipOs.finish();
                        return os.toByteArray();
                    } catch (IOException e) {
                        throw new WebmasterException("Failed to serialize object to YDB json field",
                                new WebmasterErrorResponse.YDBErrorResponse(Fields.class, e), e);
                    }
                }
        ));
    }

    public static LocalDate convert(org.joda.time.LocalDate jDate) {
        return LocalDate.of(jDate.getYear(), jDate.getMonthOfYear(), jDate.getDayOfMonth());
    }

    private static org.joda.time.LocalDate convert(LocalDate date) {
        return new org.joda.time.LocalDate(date.getYear(), date.getMonthValue(), date.getDayOfMonth());
    }

    public static Instant convert(org.joda.time.DateTime jDate) {
        return Instant.ofEpochMilli(jDate.getMillis());
    }

    public static org.joda.time.DateTime convert(Instant instant) {
        return new DateTime(instant.toEpochMilli());
    }

    public static Instant convertInstant(org.joda.time.Instant jDate) {
        return Instant.ofEpochMilli(jDate.getMillis());
    }

    public static org.joda.time.Instant convertInstant(Instant instant) {
        return new org.joda.time.Instant(instant.toEpochMilli());
    }
}
