package ru.yandex.qe.dispenser.domain.dao;

import java.io.Serializable;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

import net.jcip.annotations.ThreadSafe;
import org.jetbrains.annotations.NotNull;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.datasource.lookup.DataSourceLookupFailureException;

import ru.yandex.qe.dispenser.domain.index.IndexConsumer;
import ru.yandex.qe.dispenser.domain.util.CollectionUtils;

@ThreadSafe
public abstract class InMemoryDaoImpl<T extends IndexConsumer<PK>, PK extends Serializable> implements InMemoryDao<T, PK> {
    @NotNull
    protected final Function<T, PK> primaryKeyProducer;
    @NotNull
    protected final Map<PK, T> id2obj = new ConcurrentHashMap<>();

    protected InMemoryDaoImpl(@NotNull final Function<T, PK> primaryKeyProducer) {
        this.primaryKeyProducer = primaryKeyProducer;
    }

    @NotNull
    @Override
    public T create(@NotNull final T newInstance) {
        final PK id = primaryKeyProducer.apply(newInstance);
        if (id2obj.put(id, newInstance) != null) {
            throw new DuplicateKeyException(newInstance.getClass().getSimpleName() + " object with id " + id + " already exists!");
        }
        newInstance.setId(id);
        return newInstance;
    }

    protected T createUnsafe(@NotNull final PK id, @NotNull final T newInstance) {
        if (id2obj.putIfAbsent(id, newInstance) != null) {
            throw new DuplicateKeyException(newInstance.getClass().getSimpleName() + " object with id " + id + " already exists!");
        }
        newInstance.setId(id);
        return newInstance;
    }

    @NotNull
    public T createIfAbsent(@NotNull final T obj) {
        final PK id = primaryKeyProducer.apply(obj);
        obj.setId(id);
        return id2obj.computeIfAbsent(id, pk -> obj);
    }

    @NotNull
    @Override
    public T read(@NotNull final PK id) {
        return Optional.ofNullable(id2obj.get(id))
                .orElseThrow(() -> new EmptyResultDataAccessException("No object in " + getClass().getSimpleName() + " with id " + id, 1));
    }

    @Override
    public boolean update(@NotNull final T obj) {
        final PK id = primaryKeyProducer.apply(obj);
        obj.setId(id);
        if (id2obj.computeIfPresent(id, (pk, t) -> obj) == null) {
            throw new IllegalArgumentException(obj.getClass().getSimpleName() + " object not exists to update!");
        }
        return true;
    }

    @Override
    public boolean delete(@NotNull final T obj) {
        final PK id = primaryKeyProducer.apply(obj);
        if (id2obj.remove(id) == null) {
            throw new DataSourceLookupFailureException(obj.getClass().getSimpleName() + " with id '" + id + "' not exists to delete!");
        }
        return true;
    }

    @NotNull
    @Override
    public Set<T> getAll() {
        return CollectionUtils.copyToUnmodifiableSet(id2obj.values());
    }

    public Set<T> readByIds(Set<PK> ids) {
        if (ids.isEmpty()) {
            return Set.of();
        }
        Set<T> result = new HashSet<>();
        ids.forEach(id -> {
            T obj = id2obj.get(id);
            if (obj != null) {
                result.add(obj);
            }
        });
        return result;
    }

}
