/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.collection.iterators;

import java.util.*;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import lombok.experimental.UtilityClass;
import net.ranides.assira.functional.Functions.EachFunction;
import net.ranides.assira.generic.CompareUtils;
import net.ranides.assira.generic.Ref;

/**
 * Helper methods for working with legacy Enumeration type from JDK 1.0 dark ages.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@UtilityClass
public final class EnumerationUtils {

    /**
     * Creates enumeration from provided values
     *
     * @param values values
     * @param <T> T
     * @return enumeration
     */
    @SafeVarargs
    public static <T> Enumeration<T> of(T... values) {
        return enumeration(Arrays.asList(values));
    }

    /**
     * Creates enumeration from iterable
     *
     * @param values values
     * @param <T> T
     * @return enumeration
     */
    public static <T> Enumeration<T> enumeration(Iterable<? extends T> values) {
        return enumeration(values.iterator());
    }

    /**
     * Creates enumeration from iterator
     *
     * @param values values
     * @param <T> T
     * @return enumeration
     */
    public static <T> Enumeration<T> enumeration(Iterator<? extends T> values) {
        return new Enumeration<T>() {
            @Override
            public boolean hasMoreElements() {
                return values.hasNext();
            }

            @Override
            public T nextElement() {
                return values.next();
            }
        };
    }

    /**
     * Creates read-only iterator from enumeration.
     *
     * Fun fact: JDK9 implemented it as Enumeration#toIterator
     *
     * @param values values
     * @param <T> T
     * @return iterator
     */
    public static <T> Iterator<T> iterator(Enumeration<? extends T> values) {
        return new Iterator<T>() {
            @Override
            public boolean hasNext() {
                return values.hasMoreElements();
            }

            @Override
            public T next() {
                return values.nextElement();
            }
        };
    }

    /**
     * Returns first element for enumeration, or none if enumeration is empty.
     *
     * @param iterator iterator
     * @param <T> T
     * @return optional
     */
    public static <T> Ref<T> first(Enumeration<? extends T> iterator) {
		if(iterator.hasMoreElements()) {
			return Ref.of(iterator.nextElement());
		}
        return Ref.empty();
    }

    /**
     * Returns first element for enumeration which satisfies predicate.
     * Returns none if enumeration is empty or no element was accepted by predicate.
     *
     * @param iterator iterator
     * @param predicate predicate
     * @param <T> T
     * @return optional
     */
    public static <T> Ref<T> first(Enumeration<? extends T> iterator, Predicate<? super T> predicate) {
        while(iterator.hasMoreElements()) {
            T v = iterator.nextElement();
            if( predicate.test(v)) {
                return Ref.of(v);
            }
        }
        return Ref.empty();
    }

    /**
     * Returns last element for enumeration, or none if enumeration is empty.
     * It must traverse whole collection.
     * If you can, lets try ListUtils.
     *
     * @param iterator iterator
     * @param <T> T
     * @return optional
     */
    public static <T> Ref<T> last(Enumeration<? extends T> iterator) {
		if(!iterator.hasMoreElements()) {
			return Ref.empty();
		}
        T result = null;
        while(iterator.hasMoreElements()) {
            result = iterator.nextElement();
        }
        return Ref.ofNullable(result);
    }

    /**
     * Returns last element for enumeration which satisfies predicate.
     * Returns none if enumeration is empty or no element was accepted by predicate.
     * It must traverse whole collection.
     * If you can, lets try ListUtils.
     *
     * @param iterator iterator
     * @param predicate predicate
     * @param <T> T
     * @return optional
     */
    public static <T> Ref<T> last(Enumeration<? extends T> iterator, Predicate<? super T> predicate) {
        boolean found = false;
		T result = null;
        while(iterator.hasMoreElements()) {
            T v = iterator.nextElement();
            if(predicate.test(v)) {
                result = v;
				found = true;
            }
        }
		if(!found) {
			return Ref.empty();
		}
        return Ref.of(result);
    }

    /**
     * Counts number of elements for specified enumeration.
     * It must iterate through all elements, if you can, check collection size directly.
     *
     * @param iterator iterator
     * @return int
     */
    public static int size(Enumeration<?> iterator) {
		if(iterator==null) {
			return 0;
		}
        int c = 0;
        while(iterator.hasMoreElements()) {
            c++;
            iterator.nextElement();
        }
        return c;
    }

    /**
     * Returns filtered view of provided Enumeration.
     *
     * @param iterator iterator
     * @param predicate predicate
     * @param <T> T
     * @return enumeration view
     */
    public static <T> Iterator<T> filter(Enumeration<? extends T> iterator, Predicate<? super T> predicate) {
        return new FilterIterator<>(iterator, predicate, false);
    }

    /**
     * Returns limited view of provided Enumeration.
     * Limited enumeration stops at first unmatched element.
     *
     * @param iterator iterator
     * @param predicate predicate
     * @param <T> T
     * @return enumeration view
     */
    public static <T> Iterator<T> limit(Enumeration<? extends T> iterator, Predicate<? super T> predicate) {
        return new FilterIterator<>(iterator, predicate, true);
    }

    /**
     * Returns view of enumeration which maps every element using provided function.
     *
     * @param iterator iterator
     * @param function function
     * @param <T> T
     * @param <S> S
     * @return enumeration view
     */
    public static <S,T> Iterator<T> map(Enumeration<? extends S> iterator, EachFunction<? super S,? extends T> function) {
        return new EachAdapter<>(iterator, function);
    }

    /**
     * Returns view of enumeration which maps every element using provided function.
     *
     * @param iterator iterator
     * @param function function
     * @param <T> T
     * @param <S> S
     * @return enumeration view
     */
    public static <S,T> Iterator<T> map(Enumeration<? extends S> iterator, Function<? super S,? extends T> function) {
        return new Adapter<>(iterator, function);
    }

    /**
     * Returns view which lazily expands all nested enumerations and merges into one enumeration.
     * Flat enumerations move forward only as much as necessary (which means one element at time).
     *
     * @param iterator iterator
     * @param <T> T
     * @return enumeration view
     */
    public static <T> Iterator<T> flat(Enumeration<? extends Enumeration<? extends T>> iterator) {
        return new FlatIterator<>(iterator);
    }

    /**
     * Creates new sequential Stream from provided enumeration.
     *
     * @param iterator iterator
     * @param <T> T
     * @return stream
     */
    public static <T> Stream<T> stream(Enumeration<? extends T> iterator) {
		return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator(iterator), 0), false);
	}

    /**
     * Inserts all elements returned by enumeration into provided collection.
     *
     * @param iterator iterator
     * @param target target
     * @param <T> T
     * @param <C> C
     * @return target
     */
    public static <T, C extends Collection<T>> C collect(Enumeration<? extends T> iterator, C target) {
        while(iterator.hasMoreElements()) {
            target.add(iterator.nextElement());
        }
        return target;
	}

    /**
     * Tries to skip "count" elements.
     * Returns number of successfully skipped elements.
     *
     * @param iterator iterator
     * @param count count
     * @param <T> T
     * @return int
     */
    public static <T> int next(Enumeration<? extends T> iterator, int count) {
		int i=0;
		for(; i<count && iterator.hasMoreElements(); i++) {
			iterator.nextElement();
		}
		return i;
	}

    /**
     * Checks if provided enumerations contain equal elements.
     * Order of elements is important.
     *
     * @param a a
     * @param b b
     * @param <T> T
     * @return bool
     */
    public static <T> boolean equals(Enumeration<? extends T> a, Enumeration<? extends T> b) {
        return equals(a, b, CompareUtils::equals);
    }

    /**
     * Checks if provided enumerations contain equal elements using provided comparison function.
     * Order of elements is important.
     *
     * @param a a
     * @param b b
     * @param predicate predicate
     * @param <T> T
     * @return bool
     */
    public static <T> boolean equals(Enumeration<? extends T> a, Enumeration<? extends T> b, BiPredicate<? super T,? super T> predicate) {
        while(a.hasMoreElements() && b.hasMoreElements()) {
            if( !predicate.test(a.nextElement(), b.nextElement())) {
                return false;
            }
        }
        return !a.hasMoreElements() && !b.hasMoreElements();
    }

    /**
     * Checks if provided enumerations contain equal elements.
     * Order of elements is important.
     * It is three-value version, which returns +1, 0, -1
     *
     * @param a a
     * @param b b
     * @param <T> T
     * @return int
     */
    public static <T> int compare(Enumeration<? extends T> a, Enumeration<? extends T> b) {
        return compare(a, b, CompareUtils.comparator());
    }

    /**
     * Checks if provided enumerations contain equal elements using provided comparison function.
     * Order of elements is important.
     * It is three-value version, which returns +1, 0, -1
     *
     * @param a a
     * @param b b
     * @param cmp cmp
     * @param <T> T
     * @return int
     */
    public static <T> int compare(Enumeration<? extends T> a, Enumeration<? extends T> b, Comparator<? super T> cmp) {
        while(a.hasMoreElements() && b.hasMoreElements()) {
            int icmp = cmp.compare(a.nextElement(), b.nextElement());
            if(icmp !=0) {
                return icmp;
            }
        }
        return a.hasMoreElements() ? +1 : b.hasMoreElements() ? -1 : 0;
    }
    
    private static final class Adapter<S, T> implements Iterator<T> {
    
        protected final Enumeration<S> delegate;
        
        protected final Function<? super S,? extends T> function;

        protected <E> Adapter(Enumeration<S> delegate, Function<? super S,? extends T> function) {
            this.delegate = delegate;
            this.function = function;
        }

        @Override
        public boolean hasNext() {
            return delegate.hasMoreElements();
        }

        @Override
        public T next() {
            return function.apply(delegate.nextElement());
        }

    }    
    
    private static final class EachAdapter<S, T> implements Iterator<T> {
    
        protected final Enumeration<S> delegate;
        
        protected final EachFunction<? super S,? extends T> function;
        
        protected int index;

        protected <E> EachAdapter(Enumeration<S> delegate, EachFunction<? super S,? extends T> function) {
            this.delegate = delegate;
            this.function = function;
            this.index = 0;
        }

        @Override
        public boolean hasNext() {
            return delegate.hasMoreElements();
        }

        @Override
        public T next() {
            return function.apply(index++, delegate.nextElement());
        }

    } 

    private static class FilterIterator<T> implements Iterator<T> {

        private final Enumeration<? extends T> iterator;
        private final Predicate<? super T> predicate;
        private final boolean limit;
        private T next;
        private boolean hasNext;
        
        public FilterIterator(Enumeration<? extends T> iterator, Predicate<? super T> predicate, boolean limit) {
            this.iterator = iterator;
			this.predicate = predicate;
            this.limit = limit;
            nextMatch();
        }

        @Override
        public boolean hasNext() {
            return hasNext;
        }

        @Override
        public T next() {
            if (!hasNext) {
                throw new NoSuchElementException();
            }
            return nextMatch();
        }

        private T nextMatch() {
            T last = next;

            while (iterator.hasMoreElements()) {
                T value = iterator.nextElement();
                if (predicate.test(value)) {
                    hasNext = true;
                    next = value;
                    return last;
                } else if(limit) {
                    break;
                }
            }

            hasNext = false;
            return last;
        }

        @Override
        public void remove() {
            // impossible to implement both "hasNext" & "mutable methods" reliably
            throw new UnsupportedOperationException();
        }

    }

    private static final class FlatIterator<T> implements Iterator<T> {

        private final Enumeration<? extends Enumeration<? extends T>> li;
        private Enumeration<? extends T> ci;

        public FlatIterator(Enumeration<? extends Enumeration<? extends T>> data) {
            li = data;
            ci = Collections.emptyEnumeration();
        }

        @Override
        public boolean hasNext() {
            if(ci.hasMoreElements()) {
                return true;
            }
            while(li.hasMoreElements()) {
                ci = li.nextElement();
                if(ci.hasMoreElements()) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public T next() {
            if(!hasNext()) {
                throw new NoSuchElementException();
            }
            return ci.nextElement();
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }

    }
    
}
