package ru.simplex_software.wicket_springdata.kryo;

import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.springframework.data.domain.Persistable;

import java.io.Serializable;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * Модель отображения.
 *
 * @param <K> тип ключей.
 * @param <V> тип значений.
 */
public class NewKryoMapModel<K, V> implements IModel<Map<K, V>> {

    private final Map<IModel<K>, IModel<V>> modelMap;

    /**
     * Загружена ли модель.
     */
    private boolean attached;

    /**
     * Создание новой модели отображеия.
     *
     * @param map исходное отображение.
     */
    public NewKryoMapModel(Map<K, V> map) {
        this.modelMap = wrap(map);
        attached = true;
    }

    @Override
    public Map<K, V> getObject() {
        checkLoad();
        return unwrap(modelMap);
    }

    /**
     * Открепление модели.
     */
    @Override
    public void detach() {
        modelMap.keySet().forEach(IModel::detach);
        modelMap.values().forEach(IModel::detach);
        attached = false;
    }

    /**
     * Получение значения по модели ключа.
     *
     * @param keyModel модель ключа.
     * @return значение.
     */
    public V get(IModel<K> keyModel) {
        return getValueModel(keyModel).getObject();
    }

    /**
     * Получение модели значения по модели ключа.
     *
     * @param keyModel модель ключа.
     * @return модель значения.
     */
    public IModel<V> getValueModel(IModel<K> keyModel) {
        checkLoad();
        return modelMap.get(keyModel);
    }

    /**
     * Получение итератора ключей.
     */
    public Iterator<IModel<K>> getKeysIterator() {
        checkLoad();
        return modelMap.keySet().iterator();
    }

    /**
     * Проверка загрузки и загрузка при необходимости.
     */
    private void checkLoad() {
        if (!attached) {
            modelMap.keySet().forEach(this::loadModel);
            modelMap.values().forEach(this::loadModel);
        }
        attached = true;
    }

    /**
     * Загрузка модели.
     *
     * @param model модель.
     */
    private void loadModel(IModel<?> model) {
        if (model instanceof KryoModel) {
            ((KryoModel<?>) model).load();
        }
    }

    /**
     * Оборачивание ключа в модель.
     *
     * @param key ключ.
     * @return модель ключа.
     */
    @SuppressWarnings("unchecked")
    private IModel<K> wrapKey(K key) {
        return (IModel<K>) wrapObject(key);
    }

    /**
     * Оборачивание значения в модель.
     *
     * @param value значение.
     * @return модель значения.
     */
    @SuppressWarnings("unchecked")
    private IModel<V> wrapValue(V value) {
        return (IModel<V>) wrapObject(value);
    }

    /**
     * Оборачивание объекта в модель.
     * Объект должен реализовывать интерфейс {@link Persistable} или {@link Serializable}.
     *
     * @param object объект.
     * @return модель объекта.
     * @throws IllegalArgumentException если объект не {@link Persistable} и не {@link Serializable}.
     * @see Persistable
     * @see Serializable
     */
    @SuppressWarnings("unchecked")
    private IModel<?> wrapObject(Object object) throws IllegalArgumentException {
        if (object instanceof Persistable<?>) {
            return new KryoModel<>((Persistable<? extends Serializable>) object);
        } else if (object instanceof Serializable) {
            return Model.of((Serializable) object);
        } else throw new IllegalArgumentException("object must be Persistable or Serializable");
    }

    /**
     * Оборачивание ключей и значений отображения в модели.
     *
     * @param map отображение.
     * @return отображение моделей.
     */
    private Map<IModel<K>, IModel<V>> wrap(Map<K, V> map) {
        Map<IModel<K>, IModel<V>> modelMap = new LinkedHashMap<>();
        map.forEach((key, value) -> modelMap.put(wrapKey(key), wrapValue(value)));
        return modelMap;
    }

    /**
     * Получение отображения с ключами и значениями из моделей.
     *
     * @param modelMap отображение моделей.
     * @return отображение ключей и значений.
     */
    private Map<K, V> unwrap(Map<IModel<K>, IModel<V>> modelMap) {
        return modelMap.entrySet().stream()
                .collect(Collectors.toMap(
                        entry -> entry.getKey().getObject(),
                        entry -> entry.getValue().getObject()));
    }
}