/*
 * 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.commons.io.function;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.IntFunction;
import java.util.function.ToDoubleFunction;
import java.util.function.ToIntFunction;
import java.util.function.ToLongFunction;
import java.util.function.UnaryOperator;
import java.util.stream.Collector;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import org.apache.commons.io.IOExceptionList;

/**
 * Like {@link Stream} but throws {@link IOException}.
 *
 * @param <T> the type of the stream elements.
 * @since 2.12.0
 */
public interface IOStream<T> extends IOBaseStream<T, IOStream<T>, Stream<T>> {

    /**
     * Constructs a new IOStream for the given Stream.
     *
     * @param <T> the type of the stream elements.
     * @param stream The stream to delegate.
     * @return a new IOStream.
     */
    static <T> IOStream<T> adapt(final Stream<T> stream) {
        return IOStreamAdapter.adapt(stream);
    }

    /**
     * This class' version of {@link Stream#empty()}.
     *
     * @param <T> the type of the stream elements
     * @return an empty sequential {@code IOStreamImpl}.
     * @see Stream#empty()
     */
    static <T> IOStream<T> empty() {
        return IOStreamAdapter.adapt(Stream.empty());
    }

    /**
     * Like {@link Stream#iterate(Object, UnaryOperator)} but for IO.
     *
     * @param <T> the type of stream elements.
     * @param seed the initial element.
     * @param f a function to be applied to the previous element to produce a new element.
     * @return a new sequential {@code IOStream}.
     */
    static <T> IOStream<T> iterate(final T seed, final IOUnaryOperator<T> f) {
        Objects.requireNonNull(f);
        final Iterator<T> iterator = new Iterator<T>() {
            @SuppressWarnings("unchecked")
            T t = (T) IOStreams.NONE;

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

            @Override
            public T next() throws NoSuchElementException {
                try {
                    return t = t == IOStreams.NONE ? seed : f.apply(t);
                } catch (final IOException e) {
                    final NoSuchElementException nsee = new NoSuchElementException();
                    nsee.initCause(e);
                    throw nsee;
                }
            }
        };
        return adapt(StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, Spliterator.ORDERED | Spliterator.IMMUTABLE), false));
    }

    /**
     * Null-safe version of {@link StreamSupport#stream(java.util.Spliterator, boolean)}.
     *
     * Copied from Apache Commons Lang.
     *
     * @param <T> the type of stream elements.
     * @param values the elements of the new stream, may be {@code null}.
     * @return the new stream on {@code values} or {@link Stream#empty()}.
     */
    @SuppressWarnings("resource") // call to #empty()
    static <T> IOStream<T> of(final Iterable<T> values) {
        return values == null ? empty() : adapt(StreamSupport.stream(values.spliterator(), false));
    }

    /**
     * Null-safe version of {@link Stream#of(Object[])} for an IO stream.
     *
     * @param <T> the type of stream elements.
     * @param values the elements of the new stream, may be {@code null}.
     * @return the new stream on {@code values} or {@link Stream#empty()}.
     */
    @SuppressWarnings("resource")
    @SafeVarargs // Creating a stream from an array is safe
    static <T> IOStream<T> of(final T... values) {
        return values == null || values.length == 0 ? empty() : adapt(Arrays.stream(values));
    }

    /**
     * Returns a sequential {@code IOStreamImpl} containing a single element.
     *
     * @param t the single element
     * @param <T> the type of stream elements
     * @return a singleton sequential stream
     */
    static <T> IOStream<T> of(final T t) {
        return adapt(Stream.of(t));
    }

    /**
     * Like {@link Stream#allMatch(java.util.function.Predicate)} but throws {@link IOException}.
     *
     * @param predicate {@link Stream#allMatch(java.util.function.Predicate)}.
     * @return Like {@link Stream#allMatch(java.util.function.Predicate)}.
     * @throws IOException if an I/O error occurs.
     */
    @SuppressWarnings("unused") // thrown by Erase.
    default boolean allMatch(final IOPredicate<? super T> predicate) throws IOException {
        return unwrap().allMatch(t -> Erase.test(predicate, t));
    }

    /**
     * Like {@link Stream#anyMatch(java.util.function.Predicate)} but throws {@link IOException}.
     *
     * @param predicate {@link Stream#anyMatch(java.util.function.Predicate)}.
     * @return Like {@link Stream#anyMatch(java.util.function.Predicate)}.
     * @throws IOException if an I/O error occurs.
     */
    @SuppressWarnings("unused") // thrown by Erase.
    default boolean anyMatch(final IOPredicate<? super T> predicate) throws IOException {
        return unwrap().anyMatch(t -> Erase.test(predicate, t));
    }

    /**
     * TODO Package-private for now, needs IOCollector?
     *
     * Adding this method now and an IO version later is an issue because call sites would have to type-cast to pick one. It
     * would be ideal to have only one.
     *
     * Like {@link Stream#collect(Collector)}.
     *
     * Package private for now.
     *
     * @param <R> Like {@link Stream#collect(Collector)}.
     * @param <A> Like {@link Stream#collect(Collector)}.
     * @param collector Like {@link Stream#collect(Collector)}.
     * @return Like {@link Stream#collect(Collector)}.
     */
    default <R, A> R collect(final Collector<? super T, A, R> collector) {
        return unwrap().collect(collector);
    }

    /**
     * Like
     * {@link Stream#collect(java.util.function.Supplier, java.util.function.BiConsumer, java.util.function.BiConsumer)}.
     *
     * @param <R> Like
     *        {@link Stream#collect(java.util.function.Supplier, java.util.function.BiConsumer, java.util.function.BiConsumer)}.
     * @param supplier Like
     *        {@link Stream#collect(java.util.function.Supplier, java.util.function.BiConsumer, java.util.function.BiConsumer)}.
     * @param accumulator Like
     *        {@link Stream#collect(java.util.function.Supplier, java.util.function.BiConsumer, java.util.function.BiConsumer)}.
     * @param combiner Like
     *        {@link Stream#collect(java.util.function.Supplier, java.util.function.BiConsumer, java.util.function.BiConsumer)}.
     * @return Like
     *         {@link Stream#collect(java.util.function.Supplier, java.util.function.BiConsumer, java.util.function.BiConsumer)}.
     * @throws IOException if an I/O error occurs.
     */
    @SuppressWarnings("unused") // thrown by Erase.
    default <R> R collect(final IOSupplier<R> supplier, final IOBiConsumer<R, ? super T> accumulator, final IOBiConsumer<R, R> combiner) throws IOException {
        return unwrap().collect(() -> Erase.get(supplier), (t, u) -> Erase.accept(accumulator, t, u), (t, u) -> Erase.accept(combiner, t, u));
    }

    /**
     * Like {@link Stream#count()}.
     *
     * @return Like {@link Stream#count()}.
     */
    default long count() {
        return unwrap().count();
    }

    /**
     * Like {@link Stream#distinct()}.
     *
     * @return Like {@link Stream#distinct()}.
     */
    default IOStream<T> distinct() {
        return adapt(unwrap().distinct());
    }

    /**
     * Like {@link Stream#filter(java.util.function.Predicate)}.
     *
     * @param predicate Like {@link Stream#filter(java.util.function.Predicate)}.
     * @return Like {@link Stream#filter(java.util.function.Predicate)}.
     * @throws IOException if an I/O error occurs.
     */
    @SuppressWarnings("unused") // thrown by Erase.
    default IOStream<T> filter(final IOPredicate<? super T> predicate) throws IOException {
        return adapt(unwrap().filter(t -> Erase.test(predicate, t)));
    }

    /**
     * Like {@link Stream#findAny()}.
     *
     * @return Like {@link Stream#findAny()}.
     */
    default Optional<T> findAny() {
        return unwrap().findAny();
    }

    /**
     * Like {@link Stream#findFirst()}.
     *
     * @return Like {@link Stream#findFirst()}.
     */
    default Optional<T> findFirst() {
        return unwrap().findFirst();
    }

    /**
     * Like {@link Stream#flatMap(java.util.function.Function)}.
     *
     * @param <R> Like {@link Stream#flatMap(java.util.function.Function)}.
     * @param mapper Like {@link Stream#flatMap(java.util.function.Function)}.
     * @return Like {@link Stream#flatMap(java.util.function.Function)}.
     * @throws IOException if an I/O error occurs.
     */
    @SuppressWarnings("unused") // thrown by Erase.
    default <R> IOStream<R> flatMap(final IOFunction<? super T, ? extends IOStream<? extends R>> mapper) throws IOException {
        return adapt(unwrap().flatMap(t -> Erase.apply(mapper, t).unwrap()));
    }

    /**
     * TODO Package-private for now, needs IODoubleStream?
     *
     * Adding this method now and an IO version later is an issue because call sites would have to type-cast to pick one. It
     * would be ideal to have only one.
     *
     * Like {@link Stream#flatMapToDouble(java.util.function.Function)}.
     *
     * @param mapper Like {@link Stream#flatMapToDouble(java.util.function.Function)}.
     * @return Like {@link Stream#flatMapToDouble(java.util.function.Function)}.
     * @throws IOException if an I/O error occurs.
     */
    @SuppressWarnings("unused") // thrown by Erase.
    default DoubleStream flatMapToDouble(final IOFunction<? super T, ? extends DoubleStream> mapper) throws IOException {
        return unwrap().flatMapToDouble(t -> Erase.apply(mapper, t));
    }

    /**
     * TODO Package-private for now, needs IOIntStream?
     *
     * Adding this method now and an IO version later is an issue because call sites would have to type-cast to pick one. It
     * would be ideal to have only one.
     *
     * Like {@link Stream#flatMapToInt(java.util.function.Function)}.
     *
     * @param mapper Like {@link Stream#flatMapToInt(java.util.function.Function)}.
     * @return Like {@link Stream#flatMapToInt(java.util.function.Function)}.
     * @throws IOException if an I/O error occurs.
     */
    @SuppressWarnings("unused") // thrown by Erase.
    default IntStream flatMapToInt(final IOFunction<? super T, ? extends IntStream> mapper) throws IOException {
        return unwrap().flatMapToInt(t -> Erase.apply(mapper, t));
    }

    /**
     * TODO Package-private for now, needs IOLongStream?
     *
     * Adding this method now and an IO version later is an issue because call sites would have to type-cast to pick one. It
     * would be ideal to have only one.
     *
     * Like {@link Stream#flatMapToLong(java.util.function.Function)}.
     *
     * @param mapper Like {@link Stream#flatMapToLong(java.util.function.Function)}.
     * @return Like {@link Stream#flatMapToLong(java.util.function.Function)}.
     * @throws IOException if an I/O error occurs.
     */
    @SuppressWarnings("unused") // thrown by Erase.
    default LongStream flatMapToLong(final IOFunction<? super T, ? extends LongStream> mapper) throws IOException {
        return unwrap().flatMapToLong(t -> Erase.apply(mapper, t));
    }

    /**
     * Performs an action for each element gathering any exceptions.
     *
     * @param action The action to apply to each element.
     * @throws IOExceptionList if any I/O errors occur.
     */
    default void forAll(final IOConsumer<T> action) throws IOExceptionList {
        forAll(action, (i, e) -> e);
    }

    /**
     * Performs an action for each element gathering any exceptions.
     *
     * @param action The action to apply to each element.
     * @param exSupplier The exception supplier.
     * @throws IOExceptionList if any I/O errors occur.
     */
    default void forAll(final IOConsumer<T> action, final BiFunction<Integer, IOException, IOException> exSupplier) throws IOExceptionList {
        final AtomicReference<List<IOException>> causeList = new AtomicReference<>();
        final AtomicInteger index = new AtomicInteger();
        final IOConsumer<T> safeAction = IOStreams.toIOConsumer(action);
        unwrap().forEach(e -> {
            try {
                safeAction.accept(e);
            } catch (final IOException innerEx) {
                if (causeList.get() == null) {
                    // Only allocate if required
                    causeList.set(new ArrayList<>());
                }
                if (exSupplier != null) {
                    causeList.get().add(exSupplier.apply(index.get(), innerEx));
                }
            }
            index.incrementAndGet();
        });
        IOExceptionList.checkEmpty(causeList.get(), null);
    }

    /**
     * Like {@link Stream#forEach(java.util.function.Consumer)} but throws {@link IOException}.
     *
     * @param action Like {@link Stream#forEach(java.util.function.Consumer)}.
     * @throws IOException if an I/O error occurs.
     */
    @SuppressWarnings("unused") // thrown by Erase.
    default void forEach(final IOConsumer<? super T> action) throws IOException {
        unwrap().forEach(e -> Erase.accept(action, e));
    }

    /**
     * Like {@link Stream#forEachOrdered(java.util.function.Consumer)}.
     *
     * @param action Like {@link Stream#forEachOrdered(java.util.function.Consumer)}.
     * @throws IOException if an I/O error occurs.
     */
    @SuppressWarnings("unused") // thrown by Erase.
    default void forEachOrdered(final IOConsumer<? super T> action) throws IOException {
        unwrap().forEachOrdered(e -> Erase.accept(action, e));
    }

    /**
     * Like {@link Stream#limit(long)}.
     *
     * @param maxSize Like {@link Stream#limit(long)}.
     * @return Like {@link Stream#limit(long)}.
     */
    default IOStream<T> limit(final long maxSize) {
        return adapt(unwrap().limit(maxSize));
    }

    /**
     * Like {@link Stream#map(java.util.function.Function)}.
     *
     * @param <R> Like {@link Stream#map(java.util.function.Function)}.
     * @param mapper Like {@link Stream#map(java.util.function.Function)}.
     * @return Like {@link Stream#map(java.util.function.Function)}.
     * @throws IOException if an I/O error occurs.
     */
    @SuppressWarnings("unused") // thrown by Erase.
    default <R> IOStream<R> map(final IOFunction<? super T, ? extends R> mapper) throws IOException {
        return adapt(unwrap().map(t -> Erase.apply(mapper, t)));
    }

    /**
     * TODO Package-private for now, needs IOToDoubleFunction?
     *
     * Adding this method now and an IO version later is an issue because call sites would have to type-cast to pick one. It
     * would be ideal to have only one.
     *
     * Like {@link Stream#mapToDouble(ToDoubleFunction)}.
     *
     * Package private for now.
     *
     * @param mapper Like {@link Stream#mapToDouble(ToDoubleFunction)}.
     * @return Like {@link Stream#mapToDouble(ToDoubleFunction)}.
     */
    default DoubleStream mapToDouble(final ToDoubleFunction<? super T> mapper) {
        return unwrap().mapToDouble(mapper);
    }

    /**
     * TODO Package-private for now, needs IOToIntFunction?
     *
     * Adding this method now and an IO version later is an issue because call sites would have to type-cast to pick one. It
     * would be ideal to have only one.
     *
     * Like {@link Stream#mapToInt(ToIntFunction)}.
     *
     * Package private for now.
     *
     * @param mapper Like {@link Stream#mapToInt(ToIntFunction)}.
     * @return Like {@link Stream#mapToInt(ToIntFunction)}.
     */
    default IntStream mapToInt(final ToIntFunction<? super T> mapper) {
        return unwrap().mapToInt(mapper);
    }

    /**
     * TODO Package-private for now, needs IOToLongFunction?
     *
     * Adding this method now and an IO version later is an issue because call sites would have to type-cast to pick one. It
     * would be ideal to have only one.
     *
     * Like {@link Stream#mapToLong(ToLongFunction)}.
     *
     * Package private for now.
     *
     * @param mapper Like {@link Stream#mapToLong(ToLongFunction)}.
     * @return Like {@link Stream#mapToLong(ToLongFunction)}.
     */
    default LongStream mapToLong(final ToLongFunction<? super T> mapper) {
        return unwrap().mapToLong(mapper);
    }

    /**
     * Like {@link Stream#max(java.util.Comparator)}.
     *
     * @param comparator Like {@link Stream#max(java.util.Comparator)}.
     * @return Like {@link Stream#max(java.util.Comparator)}.
     * @throws IOException if an I/O error occurs.
     */
    @SuppressWarnings("unused") // thrown by Erase.
    default Optional<T> max(final IOComparator<? super T> comparator) throws IOException {
        return unwrap().max((t, u) -> Erase.compare(comparator, t, u));
    }

    /**
     * Like {@link Stream#min(java.util.Comparator)}.
     *
     * @param comparator Like {@link Stream#min(java.util.Comparator)}.
     * @return Like {@link Stream#min(java.util.Comparator)}.
     * @throws IOException if an I/O error occurs.
     */
    @SuppressWarnings("unused") // thrown by Erase.
    default Optional<T> min(final IOComparator<? super T> comparator) throws IOException {
        return unwrap().min((t, u) -> Erase.compare(comparator, t, u));
    }

    /**
     * Like {@link Stream#noneMatch(java.util.function.Predicate)}.
     *
     * @param predicate Like {@link Stream#noneMatch(java.util.function.Predicate)}.
     * @return Like {@link Stream#noneMatch(java.util.function.Predicate)}.
     * @throws IOException if an I/O error occurs.
     */
    @SuppressWarnings("unused") // thrown by Erase.
    default boolean noneMatch(final IOPredicate<? super T> predicate) throws IOException {
        return unwrap().noneMatch(t -> Erase.test(predicate, t));
    }

    /**
     * Like {@link Stream#peek(java.util.function.Consumer)}.
     *
     * @param action Like {@link Stream#peek(java.util.function.Consumer)}.
     * @return Like {@link Stream#peek(java.util.function.Consumer)}.
     * @throws IOException if an I/O error occurs.
     */
    @SuppressWarnings("unused") // thrown by Erase.
    default IOStream<T> peek(final IOConsumer<? super T> action) throws IOException {
        return adapt(unwrap().peek(t -> Erase.accept(action, t)));
    }

    /**
     * Like {@link Stream#reduce(java.util.function.BinaryOperator)}.
     *
     * @param accumulator Like {@link Stream#reduce(java.util.function.BinaryOperator)}.
     * @return Like {@link Stream#reduce(java.util.function.BinaryOperator)}.
     * @throws IOException if an I/O error occurs.
     */
    @SuppressWarnings("unused") // thrown by Erase.
    default Optional<T> reduce(final IOBinaryOperator<T> accumulator) throws IOException {
        return unwrap().reduce((t, u) -> Erase.apply(accumulator, t, u));
    }

    /**
     * Like {@link Stream#reduce(Object, java.util.function.BinaryOperator)}.
     *
     * @param identity Like {@link Stream#reduce(Object, java.util.function.BinaryOperator)}.
     * @param accumulator Like {@link Stream#reduce(Object, java.util.function.BinaryOperator)}.
     * @return Like {@link Stream#reduce(Object, java.util.function.BinaryOperator)}.
     * @throws IOException if an I/O error occurs.
     */
    @SuppressWarnings("unused") // thrown by Erase.
    default T reduce(final T identity, final IOBinaryOperator<T> accumulator) throws IOException {
        return unwrap().reduce(identity, (t, u) -> Erase.apply(accumulator, t, u));
    }

    /**
     * Like {@link Stream#reduce(Object, BiFunction, java.util.function.BinaryOperator)}.
     *
     * @param <U> Like {@link Stream#reduce(Object, BiFunction, java.util.function.BinaryOperator)}.
     * @param identity Like {@link Stream#reduce(Object, BiFunction, java.util.function.BinaryOperator)}.
     * @param accumulator Like {@link Stream#reduce(Object, BiFunction, java.util.function.BinaryOperator)}.
     * @param combiner Like {@link Stream#reduce(Object, BiFunction, java.util.function.BinaryOperator)}.
     * @return Like {@link Stream#reduce(Object, BiFunction, java.util.function.BinaryOperator)}.
     * @throws IOException if an I/O error occurs.
     */
    @SuppressWarnings("unused") // thrown by Erase.
    default <U> U reduce(final U identity, final IOBiFunction<U, ? super T, U> accumulator, final IOBinaryOperator<U> combiner) throws IOException {
        return unwrap().reduce(identity, (t, u) -> Erase.apply(accumulator, t, u), (t, u) -> Erase.apply(combiner, t, u));
    }

    /**
     * Like {@link Stream#skip(long)}.
     *
     * @param n Like {@link Stream#skip(long)}.
     * @return Like {@link Stream#skip(long)}.
     */
    default IOStream<T> skip(final long n) {
        return adapt(unwrap().skip(n));
    }

    /**
     * Like {@link Stream#sorted()}.
     *
     * @return Like {@link Stream#sorted()}.
     */
    default IOStream<T> sorted() {
        return adapt(unwrap().sorted());
    }

    /**
     * Like {@link Stream#sorted(java.util.Comparator)}.
     *
     * @param comparator Like {@link Stream#sorted(java.util.Comparator)}.
     * @return Like {@link Stream#sorted(java.util.Comparator)}.
     * @throws IOException if an I/O error occurs.
     */
    @SuppressWarnings("unused") // thrown by Erase.
    default IOStream<T> sorted(final IOComparator<? super T> comparator) throws IOException {
        return adapt(unwrap().sorted((t, u) -> Erase.compare(comparator, t, u)));
    }

    /**
     * Like {@link Stream#toArray()}.
     *
     * @return {@link Stream#toArray()}.
     */
    default Object[] toArray() {
        return unwrap().toArray();
    }

    /**
     * TODO Package-private for now, needs IOIntFunction?
     *
     * Adding this method now and an IO version later is an issue because call sites would have to type-cast to pick one. It
     * would be ideal to have only one.
     *
     * Like {@link Stream#toArray(IntFunction)}.
     *
     * Package private for now.
     *
     * @param <A> Like {@link Stream#toArray(IntFunction)}.
     * @param generator Like {@link Stream#toArray(IntFunction)}.
     * @return Like {@link Stream#toArray(IntFunction)}.
     */
    default <A> A[] toArray(final IntFunction<A[]> generator) {
        return unwrap().toArray(generator);
    }

}
