package ru.simplex_software.wicket_springdata;

import org.apache.wicket.injection.Injector;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.LoadableDetachableModel;
import org.apache.wicket.spring.injection.annot.SpringBean;
import org.hibernate.Hibernate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Persistable;
import org.springframework.data.repository.CrudRepository;

import java.io.Serializable;
import java.util.Iterator;
import java.util.Optional;
import java.util.stream.StreamSupport;

/**
 * Модель содержит:
 * Persistable сущность в прикреплённом состоянии,
 * id и класс Persistable сущности в откреплённом состоянии.
 */
public class HibernateModel<T extends Persistable<? extends Serializable>> extends LoadableDetachableModel<T> {
    private static final Logger LOG = LoggerFactory.getLogger(HibernateModel.class);

    @SpringBean
    private RepositoryFinder repositoryFinder;

    /**
     * Первичный кдюч хранимой сущности
     */
    private Serializable id;

    /**
     * Класс хранимой сущности
     */
    private Class<T> tClass;

    public HibernateModel() {
        init();
    }

    /**
     * Конструктор.
     *
     * @param entity может быть NULL.
     */
    public HibernateModel(T entity) {
        super(entity);
        init();
        setObject(entity);
    }

    public void setObject(T entity) {
        if (entity != null && entity.isNew()) {
            throw new IllegalArgumentException("primary key can't be null");
        }
        super.setObject(entity);
    }

    /**
     * Открепление сущности.
     */
    public void onDetach() {
        LOG.debug("onDetach start");

        // Получаем Persistable сущность.
        T entity = getObject();
        if (entity == null) {
            id = null;
            tClass = null;
        } else {
            id = entity.getId();
            tClass = (Class<T>) Hibernate.getClass(entity);
        }

        LOG.debug("onDetach end");
    }

    private void init() {
        Injector.get().inject(this);
    }

    @Override
    protected T load() {
        LOG.debug("load start");

        if (isAttached()) {
            return getObject();
        }
        if (tClass == null) {
            return null;
        }
        final CrudRepository<T, Serializable> repository = repositoryFinder.findRepository(tClass);
        LOG.trace("repository={}", repository);

        Optional<T> optional = repository.findById(id);
        return optional.orElseThrow(() -> new RuntimeException("Entity with type " + tClass + " and id  " + id + " not found."));
    }

    /**
     * Оборачивает в HibernateModel элементы коллекции, преобразует их к IModel и возвращает итератор.
     */
    public static <E extends Persistable<? extends Serializable>> Iterator<IModel<E>> getItemsModels(Iterable<E> iterable) {
        return StreamSupport.stream(iterable.spliterator(), false)
                .map(HibernateModel::new)
                .map(item -> (IModel<E>) item)
                .iterator();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (id == null) {
            onDetach();
        }
        if (id == null) {//if no object set
            if (o instanceof HibernateModel) {
                Object obj = ((HibernateModel) o).getObject();
                return obj == null;
            }
            return false;
        }

        if (o == null || getClass() != o.getClass()) return false;

        HibernateModel<?> that = (HibernateModel<?>) o;

        if (that.id == null) {
            that.onDetach();
        }

        if (!id.equals(that.id)) {
            return false;
        }
        return tClass.equals(that.tClass);
    }

    @Override
    public int hashCode() {
        if (id == null) {
            onDetach(); // Для установки Id
        }
        if (id == null) { // if no object set
            return -1;
        } else {
            return id.hashCode();
        }
    }

    public RepositoryFinder getRepositoryFinder() {
        return repositoryFinder;
    }

    public void setRepositoryFinder(RepositoryFinder repositoryFinder) {
        this.repositoryFinder = repositoryFinder;
    }
}
