/*
 * 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.lang3.stream;

import java.util.Arrays;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.lang3.StringUtils;

/**
 * Implementations of {@link Collector} that implement various reduction operations.
 * <p>
 * This class is called {@code LangCollectors} instead of {@code Collectors} to avoid clashes with {@link Collectors}.
 * </p>
 *
 * @since 3.13.0
 */
public final class LangCollectors {

    /**
     * Simple implementation class for {@code Collector}.
     *
     * @param <T> the type of elements to be collected
     * @param <R> the type of the result
     */
    private static final class SimpleCollector<T, A, R> implements Collector<T, A, R> {

        private final BiConsumer<A, T> accumulator;
        private final Set<Characteristics> characteristics;
        private final BinaryOperator<A> combiner;
        private final Function<A, R> finisher;
        private final Supplier<A> supplier;

        private SimpleCollector(final Supplier<A> supplier, final BiConsumer<A, T> accumulator, final BinaryOperator<A> combiner, final Function<A, R> finisher,
            final Set<Characteristics> characteristics) {
            this.supplier = supplier;
            this.accumulator = accumulator;
            this.combiner = combiner;
            this.finisher = finisher;
            this.characteristics = characteristics;
        }

        @Override
        public BiConsumer<A, T> accumulator() {
            return accumulator;
        }

        @Override
        public Set<Characteristics> characteristics() {
            return characteristics;
        }

        @Override
        public BinaryOperator<A> combiner() {
            return combiner;
        }

        @Override
        public Function<A, R> finisher() {
            return finisher;
        }

        @Override
        public Supplier<A> supplier() {
            return supplier;
        }
    }

    private static final Set<Collector.Characteristics> CH_NOID = Collections.emptySet();

    /**
     * Delegates to {@link Stream#collect(Collector)} for a Stream on the given array.
     *
     * @param <T>       The type of the array elements.
     * @param <R>       the type of the result.
     * @param <A>       the intermediate accumulation type of the {@code Collector}.
     * @param collector the {@code Collector} describing the reduction.
     * @param array     The array, assumed to be unmodified during use.
     * @return the result of the reduction
     * @see Stream#collect(Collector)
     * @see Arrays#stream(Object[])
     * @see Collectors
     * @since 3.16.0
     */
    public static <T, R, A> R collect(final Collector<? super T, A, R> collector, final T... array) {
        return Arrays.stream(array).collect(collector);
    }

    /**
     * Returns a {@code Collector} that concatenates the input elements, separated by the specified delimiter, in encounter order.
     * <p>
     * This is a variation of {@link Collectors#joining()} that works with any element class, not just {@code CharSequence}.
     * </p>
     * <p>
     * For example:
     * </p>
     *
     * <pre>
     * Stream.of(Long.valueOf(1), Long.valueOf(2), Long.valueOf(3))
     *    .collect(LangCollectors.joining())
     * returns "123"
     * </pre>
     *
     * @return A {@code Collector} which concatenates Object elements, separated by the specified delimiter, in encounter order.
     */
    public static Collector<Object, ?, String> joining() {
        return new SimpleCollector<>(StringBuilder::new, StringBuilder::append, StringBuilder::append, StringBuilder::toString, CH_NOID);
    }

    /**
     * Returns a {@code Collector} that concatenates the input elements, separated by the specified delimiter, in encounter order.
     * <p>
     * This is a variation of {@link Collectors#joining(CharSequence)} that works with any element class, not just {@code CharSequence}.
     * </p>
     * <p>
     * For example:
     * </p>
     *
     * <pre>
     * Stream.of(Long.valueOf(1), Long.valueOf(2), Long.valueOf(3))
     *   .collect(LangCollectors.joining("-"))
     * returns "1-2-3"
     * </pre>
     *
     * @param delimiter the delimiter to be used between each element.
     * @return A {@code Collector} which concatenates Object elements, separated by the specified delimiter, in encounter order.
     */
    public static Collector<Object, ?, String> joining(final CharSequence delimiter) {
        return joining(delimiter, StringUtils.EMPTY, StringUtils.EMPTY);
    }

    /**
     * Returns a {@code Collector} that concatenates the input elements, separated by the specified delimiter, with the
     * specified prefix and suffix, in encounter order.
     * <p>
     * This is a variation of {@link Collectors#joining(CharSequence, CharSequence, CharSequence)} that works with any
     * element class, not just {@code CharSequence}.
     * </p>
     * <p>
     * For example:
     * </p>
     *
     * <pre>
     * Stream.of(Long.valueOf(1), Long.valueOf(2), Long.valueOf(3))
     *   .collect(LangCollectors.joining("-", "[", "]"))
     * returns "[1-2-3]"
     * </pre>
     *
     * @param delimiter the delimiter to be used between each element
     * @param prefix the sequence of characters to be used at the beginning of the joined result
     * @param suffix the sequence of characters to be used at the end of the joined result
     * @return A {@code Collector} which concatenates CharSequence elements, separated by the specified delimiter, in
     *         encounter order
     */
    public static Collector<Object, ?, String> joining(final CharSequence delimiter, final CharSequence prefix, final CharSequence suffix) {
        return joining(delimiter, prefix, suffix, Objects::toString);
    }

    /**
     * Returns a {@code Collector} that concatenates the input elements, separated by the specified delimiter, with the specified prefix and suffix, in
     * encounter order.
     * <p>
     * This is a variation of {@link Collectors#joining(CharSequence, CharSequence, CharSequence)} that works with any element class, not just
     * {@code CharSequence}.
     * </p>
     * <p>
     * For example:
     * </p>
     *
     * <pre>{@code
     * Stream.of(Long.valueOf(1), null, Long.valueOf(3))
     *   .collect(LangCollectors.joining("-", "[", "]", o -> Objects.toString(o, "NUL")))
     * returns "[1-NUL-3]"
     * }</pre>
     *
     * @param delimiter the delimiter to be used between each element
     * @param prefix    the sequence of characters to be used at the beginning of the joined result
     * @param suffix    the sequence of characters to be used at the end of the joined result
     * @param toString  A function that takes an Object and returns a non-null String.
     * @return A {@code Collector} which concatenates CharSequence elements, separated by the specified delimiter, in encounter order
     */
    public static Collector<Object, ?, String> joining(final CharSequence delimiter, final CharSequence prefix, final CharSequence suffix,
        final Function<Object, String> toString) {
        return new SimpleCollector<>(() -> new StringJoiner(delimiter, prefix, suffix), (a, t) -> a.add(toString.apply(t)), StringJoiner::merge,
            StringJoiner::toString, CH_NOID);
    }

    private LangCollectors() {
        // No instance
    }

}
