package net.ranides.assira.generic;

import lombok.experimental.UtilityClass;
import net.ranides.assira.collection.arrays.ArrayAllocator;
import net.ranides.assira.collection.iterators.IteratorUtils;
import net.ranides.assira.reflection.IClass;

import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Supplier;

@UtilityClass
public class Wrappers {

    private static final Wrapper EMPTY = new Immutable(null);

    public static <Q> Wrapper<Q> empty() {
        return EMPTY;
    }

    public static <Q> Wrapper<Q> mutable(Q value) {
        return new Mutable<>(value);
    }

    public static <Q> Wrapper<Q> immutable(Q value) {
        return new Immutable<>(value);
    }

    public static <Q> Wrapper<Q> atomic(Q value) {
        return new Atomic<>(value);
    }

    public interface AbstractWrapper<T> extends Wrapper<T> {

        @Override
        default IClass<?> type() {
            return IClass.OBJECT;
        }

        @Override
        default void set(T value) {
            throw new UnsupportedOperationException();
        }

        default T replace(T value) {
            T prv = get();
            set(value);
            return prv;
        }

        default <R> Wrapper<R> cast(Class<R> type) {
            if( !type().isEquivalent(IClass.typeinfo(type))) {
                throw new ClassCastException(type() + " can not be cast to " + type);
            }
            return (Wrapper) this;
        }

        default <R> Wrapper<R> cast(TypeToken<R> type) {
            if( !type().isEquivalent(type)) {
                throw new ClassCastException(type() + " can not be cast to " + type);
            }
            return (Wrapper) this;
        }

        default boolean isPresent() {
            return get() != null;
        }

        @Override
        default boolean isEmpty() {
            return isAbsent();
        }

        default boolean isAbsent() {
            return !isPresent();
        }

        default void ifPresent(Consumer<? super T> consumer) {
            if (isPresent()) {
                consumer.accept(get());
            }
        }

        default void ifPresentOrElse(Consumer<? super T> action, Runnable emptyAction) {
            if(isPresent()) {
                action.accept(get());
            } else {
                emptyAction.run();
            }
        }

        default void ifAbsent(Runnable consumer) {
            if(isAbsent()) {
                consumer.run();
            }
        }

        default T orElse(T other) {
            return isPresent() ? get() : other;
        }

        default T orElseGet(Supplier<? extends T> other) {
            return isPresent() ? get() : other.get();
        }

        default <X extends Throwable> T orElseThrow(Supplier<? extends X> onEmpty) throws X {
            if (isPresent()) {
                return get();
            } else {
                throw onEmpty.get();
            }
        }

        @Override
        default int size() {
            return isPresent() ? 1 : 0;
        }

        @Override
        default Iterator<T> iterator() {
            return isPresent() ? IteratorUtils.singletonIterator(get()) : Collections.emptyIterator();
        }

        @Override
        default boolean contains(Object o) {
            return isPresent() && get().equals(o);
        }

        @Override
        default boolean add(T v) {
            if(isAbsent()) {
                set(v);
                return true;
            }
            throw new UnsupportedOperationException();
        }

        @Override
        default boolean remove(Object o) {
            if(isPresent() && get().equals(o)) {
                set(null);
                return true;
            }
            throw new UnsupportedOperationException();
        }

        @Override
        default void clear() {
            set(null);
        }

        @Override
        default Object[] toArray() {
            return isPresent() ? new Object[]{get()} : new Object[0];
        }

        @Override
        default <Q> Q[] toArray(Q[] target) {
            if(isPresent()) {
                if(target.length == 0) {
                    target = ArrayAllocator.ensureCapacity(target, 1);
                }
                ((T[])target)[0] = get();
            }
            return target;
        }

        @Override
        default boolean containsAll(Collection<?> c) {
            if(isPresent()) {
                return c.size()==1 && c.contains(get());
            } else {
                return c.isEmpty();
            }
        }

        @Override
        default boolean addAll(Collection<? extends T> c) {
            if(isPresent() || c.size()>1) {
                throw new UnsupportedOperationException();
            }
            if(c.size() == 0) {
                return false;
            }
            return add(c.iterator().next());
        }

        @Override
        default boolean retainAll(Collection<?> c) {
            if(isPresent() && !c.contains(get())) {
                clear();
                return true;
            } else {
                return false;
            }
        }

        @Override
        default boolean removeAll(Collection<?> c) {
            if(containsAll(c)) {
                clear();
                return true;
            } else {
                return false;
            }
        }

    }

    public static abstract class ValueWrapper<V> implements AbstractWrapper<V> {

        @Override
        public abstract V get();

        @Override
        public abstract void set(V value);

        @Override
        public final boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (!(obj instanceof Wrapper)) {
                return false;
            }
            Wrapper<?> other = (Wrapper<?>) obj;
            if(isPresent()) {
                return other.isPresent() && get().equals(other.get());
            } else {
                return other.isEmpty();
            }
        }

        @Override
        public final int hashCode() {
            return isPresent() ? Objects.hashCode(get()) : 0;
        }

        @Override
        public final String toString() {
            return isPresent() ? String.format("Wrapper[%s]", get()) : "Wrapper.empty";
        }

    }

    private static class Mutable<R> extends ValueWrapper<R> implements Serializable {

        private static final long serialVersionUID = 3L;

        private R value;

        public Mutable(R value) {
            this.value = value;
        }

        @Override
        public R get() {
            return value;
        }

        @Override
        public void set(R value) {
            this.value = value;
        }

    }

    private static class Immutable<R> extends ValueWrapper<R> implements Serializable {

        private static final long serialVersionUID = 3L;

        private final R value;

        public Immutable(R value) {
            this.value = value;
        }

        @Override
        public IClass<?> type() {
            return IClass.typefor(value);
        }

        @Override
        public R get() {
            return value;
        }

        @Override
        public void set(R value) {
            throw new UnsupportedOperationException();
        }
    }

    private static class Atomic<R> extends ValueWrapper<R> implements Serializable {

        private static final long serialVersionUID = 3L;

        private final AtomicReference<R> ref;

        public Atomic(R value) {
            this.ref = new AtomicReference<>(value);
        }

        @Override
        public R get() {
            return ref.get();
        }

        @Override
        public void set(R value) {
            ref.set(value);
        }

        @Override
        public R replace(R value) {
            return ref.getAndSet(value);
        }

    }

}
