/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.el.stream;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;

import javax.el.ELException;
import javax.el.LambdaExpression;

import org.apache.el.lang.ELArithmetic;
import org.apache.el.lang.ELSupport;
import org.apache.el.util.MessageFactory;

/**
 * A stream of elements supporting sequential operations such as filter, map,
 * reduce, and various terminal operations.
 */
public class Stream {

    private final Iterator<Object> iterator;


    /**
     * Constructs a new Stream from the given iterator.
     *
     * @param iterator the iterator providing the stream elements
     */
    public Stream(Iterator<Object> iterator) {
        this.iterator = iterator;
    }


    /**
     * Returns a stream consisting of the elements that match the given predicate.
     *
     * @param le the predicate to apply to each element
     * @return the new filtered stream
     */
    public Stream filter(final LambdaExpression le) {
        Iterator<Object> downStream = new OpIterator() {
            @Override
            protected void findNext() {
                while (iterator.hasNext()) {
                    Object obj = iterator.next();
                    if (ELSupport.coerceToBoolean(null, le.invoke(obj), true).booleanValue()) {
                        next = obj;
                        foundNext = true;
                        break;
                    }
                }
            }
        };
        return new Stream(downStream);
    }


    /**
     * Returns a stream consisting of the results of applying the given function
     * to the elements of this stream.
     *
     * @param le the function to apply to each element
     * @return the new mapped stream
     */
    public Stream map(final LambdaExpression le) {
        Iterator<Object> downStream = new OpIterator() {
            @Override
            protected void findNext() {
                if (iterator.hasNext()) {
                    Object obj = iterator.next();
                    next = le.invoke(obj);
                    foundNext = true;
                }
            }
        };
        return new Stream(downStream);
    }


    /**
     * Returns a stream consisting of the results of replacing each element of
     * this stream with the contents of a stream produced by applying the provided
     * function to each element.
     *
     * @param le the function to apply to each element which produces a new stream
     * @return the new flattened stream
     */
    public Stream flatMap(final LambdaExpression le) {
        Iterator<Object> downStream = new OpIterator() {

            private Iterator<?> inner;

            @Override
            protected void findNext() {
                while (iterator.hasNext() || (inner != null && inner.hasNext())) {
                    if (inner == null || !inner.hasNext()) {
                        Object obj = le.invoke(iterator.next());
                        if (obj instanceof Stream) {
                            inner = ((Stream) obj).iterator;
                        }
                    }

                    if (inner != null && inner.hasNext()) {
                        next = inner.next();
                        foundNext = true;
                        break;
                    }
                }
            }
        };
        return new Stream(downStream);
    }


    /**
     * Returns a stream consisting of the distinct elements of this stream.
     *
     * @return the new stream with distinct elements
     */
    public Stream distinct() {
        Iterator<Object> downStream = new OpIterator() {

            private final Set<Object> values = new HashSet<>();

            @Override
            protected void findNext() {
                while (iterator.hasNext()) {
                    Object obj = iterator.next();
                    if (values.add(obj)) {
                        next = obj;
                        foundNext = true;
                        break;
                    }
                }
            }
        };
        return new Stream(downStream);
    }


    /**
     * Returns a stream consisting of the elements of this stream, sorted
     * according to natural order.
     *
     * @return the new sorted stream
     */
    public Stream sorted() {
        Iterator<Object> downStream = new OpIterator() {

            private Iterator<Object> sorted = null;

            @Override
            protected void findNext() {
                if (sorted == null) {
                    sort();
                }
                if (sorted.hasNext()) {
                    next = sorted.next();
                    foundNext = true;
                }
            }

            @SuppressWarnings({ "rawtypes", "unchecked" })
            private void sort() {
                List list = new ArrayList<>();
                while (iterator.hasNext()) {
                    list.add(iterator.next());
                }
                Collections.sort(list);
                sorted = list.iterator();
            }
        };
        return new Stream(downStream);
    }


    /**
     * Returns a stream consisting of the elements of this stream, sorted
     * according to the order induced by the provided comparator.
     *
     * @param le the comparator to determine sort order
     * @return the new sorted stream
     */
    public Stream sorted(final LambdaExpression le) {
        Iterator<Object> downStream = new OpIterator() {

            private Iterator<Object> sorted = null;

            @Override
            protected void findNext() {
                if (sorted == null) {
                    sort(le);
                }
                if (sorted.hasNext()) {
                    next = sorted.next();
                    foundNext = true;
                }
            }

            @SuppressWarnings({ "rawtypes", "unchecked" })
            private void sort(LambdaExpression le) {
                List list = new ArrayList<>();
                Comparator<Object> c = new LambdaExpressionComparator(le);
                while (iterator.hasNext()) {
                    list.add(iterator.next());
                }
                list.sort(c);
                sorted = list.iterator();
            }
        };
        return new Stream(downStream);
    }


    /**
     * Performs an action for each element of this stream.
     *
     * @param le the action to perform for each element
     * @return null
     */
    public Object forEach(final LambdaExpression le) {
        while (iterator.hasNext()) {
            le.invoke(iterator.next());
        }
        return null;
    }


    /**
     * Returns a stream consisting of the elements of this stream, additionally
     * performing the provided action on each element as elements are consumed.
     *
     * @param le the action to perform on each element
     * @return the new stream
     */
    public Stream peek(final LambdaExpression le) {
        Iterator<Object> downStream = new OpIterator() {
            @Override
            protected void findNext() {
                if (iterator.hasNext()) {
                    Object obj = iterator.next();
                    le.invoke(obj);
                    next = obj;
                    foundNext = true;
                }
            }
        };
        return new Stream(downStream);
    }


    /**
     * Returns the underlying iterator for this stream.
     *
     * @return the underlying iterator
     */
    public Iterator<?> iterator() {
        return iterator;
    }


    /**
     * Returns a stream consisting of the first {@code count} elements of this stream.
     *
     * @param count the number of elements to limit to
     * @return the new limited stream
     */
    public Stream limit(final Number count) {
        return substream(Integer.valueOf(0), count);
    }


    /**
     * Returns a stream consisting of the elements of this stream from the
     * specified start position to the end.
     *
     * @param start the start position (inclusive)
     * @return the new substream
     */
    public Stream substream(final Number start) {
        return substream(start, Integer.valueOf(Integer.MAX_VALUE));
    }

    /**
     * Returns a stream consisting of the elements of this stream from the
     * specified start position up to, but not including, the specified end position.
     *
     * @param start the start position (inclusive)
     * @param end the end position (exclusive)
     * @return the new substream
     */
    public Stream substream(final Number start, final Number end) {

        Iterator<Object> downStream = new OpIterator() {

            private final int startPos = start.intValue();
            private final int endPos = end.intValue();
            private int itemCount = 0;

            @Override
            protected void findNext() {
                while (itemCount < startPos && iterator.hasNext()) {
                    iterator.next();
                    itemCount++;
                }
                if (itemCount < endPos && iterator.hasNext()) {
                    itemCount++;
                    next = iterator.next();
                    foundNext = true;
                }
            }
        };
        return new Stream(downStream);
    }


    /**
     * Returns a list containing all the elements of this stream.
     *
     * @return the list of elements
     */
    public List<Object> toList() {
        List<Object> result = new ArrayList<>();
        while (iterator.hasNext()) {
            result.add(iterator.next());
        }
        return result;
    }


    /**
     * Returns an array containing all the elements of this stream.
     *
     * @return the array of elements
     */
    public Object[] toArray() {
        List<Object> result = new ArrayList<>();
        while (iterator.hasNext()) {
            result.add(iterator.next());
        }
        return result.toArray(new Object[0]);
    }


    /**
     * Performs a reduction on the elements of this stream, using the provided
     * accumulator function. The first element is used as the initial value.
     *
     * @param le the accumulator function
     * @return an Optional describing the reduced value, or empty if this stream is empty
     */
    public Optional reduce(LambdaExpression le) {
        Object seed = null;

        if (iterator.hasNext()) {
            seed = iterator.next();
        } else {
            return Optional.EMPTY;
        }

        return new Optional(reduce(seed, le));
    }


    /**
     * Performs a reduction on the elements of this stream, using the provided
     * initial value and accumulator function.
     *
     * @param seed the initial value for the reduction
     * @param le the accumulator function
     * @return the result of the reduction
     */
    public Object reduce(Object seed, LambdaExpression le) {
        Object result = seed;

        while (iterator.hasNext()) {
            result = le.invoke(result, iterator.next());
        }

        return result;
    }


    /**
     * Returns the maximum element of this stream according to natural order.
     *
     * @return an Optional describing the maximum element, or empty if this stream is empty
     */
    public Optional max() {
        return compare(true);
    }


    /**
     * Returns the maximum element of this stream according to the provided comparator.
     *
     * @param le the comparator to determine the maximum
     * @return an Optional describing the maximum element, or empty if this stream is empty
     */
    public Optional max(LambdaExpression le) {
        return compare(true, le);
    }


    /**
     * Returns the minimum element of this stream according to natural order.
     *
     * @return an Optional describing the minimum element, or empty if this stream is empty
     */
    public Optional min() {
        return compare(false);
    }


    /**
     * Returns the minimum element of this stream according to the provided comparator.
     *
     * @param le the comparator to determine the minimum
     * @return an Optional describing the minimum element, or empty if this stream is empty
     */
    public Optional min(LambdaExpression le) {
        return compare(false, le);
    }


    /**
     * Calculates the average of all numeric elements in the stream.
     *
     * @return an Optional containing the average, or empty if the stream is empty
     */
    public Optional average() {
        long count = 0;
        Number sum = Long.valueOf(0);

        while (iterator.hasNext()) {
            count++;
            sum = ELArithmetic.add(sum, iterator.next());
        }

        if (count == 0) {
            return Optional.EMPTY;
        } else {
            return new Optional(ELArithmetic.divide(sum, Long.valueOf(count)));
        }
    }


    /**
     * Returns the sum of all numeric elements in the stream.
     *
     * @return the sum of all elements
     */
    public Number sum() {
        Number sum = Long.valueOf(0);

        while (iterator.hasNext()) {
            sum = ELArithmetic.add(sum, iterator.next());
        }

        return sum;
    }


    /**
     * Counts the number of elements in the stream.
     *
     * @return the number of elements
     */
    public Long count() {
        long count = 0;

        while (iterator.hasNext()) {
            iterator.next();
            count++;
        }

        return Long.valueOf(count);
    }


    /**
     * Checks if any element in the stream matches the given predicate.
     *
     * @param le the predicate lambda expression
     * @return an Optional containing true if any element matches, or empty if the stream is empty
     */
    public Optional anyMatch(LambdaExpression le) {
        if (!iterator.hasNext()) {
            return Optional.EMPTY;
        }

        Boolean match = Boolean.FALSE;

        while (!match.booleanValue() && iterator.hasNext()) {
            match = (Boolean) le.invoke(iterator.next());
        }

        return new Optional(match);
    }


    /**
     * Checks if all elements in the stream match the given predicate.
     *
     * @param le the predicate lambda expression
     * @return an Optional containing true if all elements match, or empty if the stream is empty
     */
    public Optional allMatch(LambdaExpression le) {
        if (!iterator.hasNext()) {
            return Optional.EMPTY;
        }

        Boolean match = Boolean.TRUE;

        while (match.booleanValue() && iterator.hasNext()) {
            match = (Boolean) le.invoke(iterator.next());
        }

        return new Optional(match);
    }


    /**
     * Checks if no elements of this stream match the given predicate.
     *
     * @param le the predicate lambda expression
     * @return an Optional containing true if no elements match, or empty if the stream is empty
     */
    public Optional noneMatch(LambdaExpression le) {
        if (!iterator.hasNext()) {
            return Optional.EMPTY;
        }

        Boolean match = Boolean.FALSE;

        while (!match.booleanValue() && iterator.hasNext()) {
            match = (Boolean) le.invoke(iterator.next());
        }

        return new Optional(Boolean.valueOf(!match.booleanValue()));
    }


    /**
     * Returns an Optional describing the first element of this stream,
     * or an empty Optional if the stream is empty.
     *
     * @return an Optional describing the first element of this stream
     */
    public Optional findFirst() {
        if (iterator.hasNext()) {
            return new Optional(iterator.next());
        } else {
            return Optional.EMPTY;
        }
    }


    @SuppressWarnings({ "rawtypes", "unchecked" })
    private Optional compare(boolean isMax) {
        Comparable result = null;

        if (iterator.hasNext()) {
            Object obj = iterator.next();
            if ((obj instanceof Comparable)) {
                result = (Comparable) obj;
            } else {
                throw new ELException(MessageFactory.get("stream.compare.notComparable"));
            }
        }

        while (iterator.hasNext()) {
            Object obj = iterator.next();
            if ((obj instanceof Comparable)) {
                if (isMax && ((Comparable) obj).compareTo(result) > 0) {
                    result = (Comparable) obj;
                } else if (!isMax && ((Comparable) obj).compareTo(result) < 0) {
                    result = (Comparable) obj;
                }
            } else {
                throw new ELException(MessageFactory.get("stream.compare.notComparable"));
            }
        }

        if (result == null) {
            return Optional.EMPTY;
        } else {
            return new Optional(result);
        }
    }


    private Optional compare(boolean isMax, LambdaExpression le) {
        Object result = null;

        if (iterator.hasNext()) {
            result = iterator.next();
        }

        while (iterator.hasNext()) {
            Object obj = iterator.next();
            if (isMax && ELSupport.coerceToNumber(null, le.invoke(obj, result), Integer.class).intValue() > 0) {
                result = obj;
            } else if (!isMax && ELSupport.coerceToNumber(null, le.invoke(obj, result), Integer.class).intValue() < 0) {
                result = obj;
            }
        }

        if (result == null) {
            return Optional.EMPTY;
        } else {
            return new Optional(result);
        }
    }


    private static class LambdaExpressionComparator implements Comparator<Object> {

        private final LambdaExpression le;

        LambdaExpressionComparator(LambdaExpression le) {
            this.le = le;
        }

        @Override
        public int compare(Object o1, Object o2) {
            return ELSupport.coerceToNumber(null, le.invoke(o1, o2), Integer.class).intValue();
        }
    }


    private abstract static class OpIterator implements Iterator<Object> {
        protected boolean foundNext = false;
        protected Object next;

        @Override
        public boolean hasNext() {
            if (foundNext) {
                return true;
            }
            findNext();
            return foundNext;
        }

        @Override
        public Object next() {
            if (foundNext) {
                foundNext = false;
                return next;
            }
            findNext();
            if (foundNext) {
                foundNext = false;
                return next;
            } else {
                throw new NoSuchElementException();
            }
        }

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

        protected abstract void findNext();
    }
}
