/*
 *  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.causeway.commons.internal.collections;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Collectors;

import org.jspecify.annotations.Nullable;

import org.apache.causeway.commons.collections.ImmutableCollection;
import org.apache.causeway.commons.internal.base._NullSafe;
import org.apache.causeway.commons.internal.reflection._Generics;

import org.jspecify.annotations.NonNull;

/**
 * <h1>- internal use only -</h1>
 * <p>
 * Common Collection creation and adapting idioms.
 * </p>
 * <p>
 * <b>WARNING</b>: Do <b>NOT</b> use any of the classes provided by this package! <br/>
 * These may be changed or removed without notice!
 * </p>
 *
 * @since 2.0
 */
public final class _Collections {

    private _Collections(){}

    // -- PREDICATES

    /**
     * @param cls
     * @return whether {@code cls} implements the java.util.Collection interface
     */
    public static boolean isCollectionType(final @Nullable Class<?> cls) {
        return cls!=null ? java.util.Collection.class.isAssignableFrom(cls) : false;
    }

    public static boolean isImmutableCollectionType(final @Nullable Class<?> cls) {
        return cls!=null ? ImmutableCollection.class.isAssignableFrom(cls) : false;
    }

    /**
     * @param cls
     * @return whether {@code cls} implements the java.util.Collection interface
     * or represents an array or is of type {@link ImmutableCollection}
     */
    public static boolean isAnyCollectionOrArrayType(final Class<?> cls) {
        return _Collections.isCollectionType(cls)
                || _Arrays.isArrayType(cls)
                || ImmutableCollection.class.isAssignableFrom(cls);
    }

    // -- COLLECTION UNMODIFIABLE ADAPTERS (FOR LIST)

    /**
     * Adapts the {@code list} as unmodifiable collection.
     * Same as {@link Collections#unmodifiableCollection(Collection)}.
     *
     * @param list
     * @return null if {@code list} is null
     */
    public static <T> Collection<T> asUnmodifiableCollection(final @Nullable List<T> list) {
        if(list==null) {
            return null;
        }
        return Collections.unmodifiableCollection(list);
    }

    /**
     * Adapts the {@code list} as unmodifiable list.
     * Same as {@link Collections#unmodifiableList(List)}.
     *
     * @param list
     */
    public static <T> List<T> asUnmodifiableList(final @Nullable List<T> list) {
        if(list==null) {
            return null;
        }
        return Collections.unmodifiableList(list);
    }

    /**
     * Preserves order, adapts the {@code list} as Set.<br/><br/>
     *
     * Any duplicate elements of the list will not be added to the set.
     * An element e1 is a duplicate of e2 if {@code e1.equals(e2) == true}.
     *
     * @param list
     * @return null if {@code list} is null
     */
    public static <T> Set<T> asUnmodifiableSet(final @Nullable List<T> list) {
        if(list==null) {
            return null;
        }
        return Collections.unmodifiableSet(
                (Set<T>)
                list.stream()
                .collect(Collectors.toCollection(LinkedHashSet::new)));
    }

    /**
     * Preserves order, adapts the {@code list} as SortedSet.<br/><br/>
     *
     * Any duplicate elements of the list will not be added to the set.
     * An element e1 is a duplicate of e2 if {@code e1.equals(e2) == true}.
     *
     * @param list
     * @return null if {@code list} is null
     */
    public static <T> SortedSet<T> asUnmodifiableSortedSet(final @Nullable List<T> list) {
        if(list==null) {
            return null;
        }
        return _Collections_SortedSetOfList.of(list);
    }

    // -- COMMON COLLECTORS

    public static <T> Collector<T, ?, HashSet<T>> toHashSet() {
        return Collectors.toCollection(HashSet::new);
    }

    public static <T> Collector<T, ?, ArrayList<T>> toArrayList() {
        return Collectors.toCollection(ArrayList::new);
    }

    // -- STREAM TO UMODIFIABLE COLLECTION COLLECTORS

    /**
     * @return a collector that collects elements of a stream into an unmodifiable List
     */
    public static <T> Collector<T, List<T>, List<T>> toUnmodifiableList() {
        return new _Collections_Collector<>(ArrayList::new, Collections::unmodifiableList);
    }

    /**
     * @return a collector that collects elements of a stream into an unmodifiable Set
     */
    public static <T> Collector<T, Set<T>, Set<T>> toUnmodifiableSet() {
        return new _Collections_Collector<>(HashSet::new, Collections::unmodifiableSet);
    }

    /**
     * @return a collector that collects elements of a stream into an unmodifiable SortedSet
     */
    public static <T> Collector<T, SortedSet<T>, SortedSet<T>> toUnmodifiableSortedSet() {
        return new _Collections_Collector<>(TreeSet::new, Collections::unmodifiableSortedSet);
    }

    /**
     * @return a collector that collects elements of a stream into an unmodifiable Collection
     */
    public static <T> Collector<T, Collection<T>, Collection<T>> toUnmodifiableCollection() {
        return new _Collections_Collector<>(ArrayList::new, Collections::unmodifiableCollection);
    }

    /**
     * @return a collector that collects elements of a stream into an unmodifiable SortedSet
     */
    public static <T> Collector<T, SortedSet<T>, SortedSet<T>> toUnmodifiableSortedSet(
            final @Nullable Comparator<T> comparator) {

        if(comparator==null) {
            return toUnmodifiableSortedSet();
        }
        return new _Collections_Collector<>(()->new TreeSet<>(comparator), Collections::unmodifiableSortedSet);
    }

    /**
     * @return a collector that collects elements of a stream into an unmodifiable
     * List, Set, SortedSet or Collection.
     * @throws IllegalArgumentException if {@code typeOfCollection} is not one of
     * List, Set, SortedSet or Collection.
     */
    public static <T> Collector<T, ?, ? extends Collection<T>> toUnmodifiableOfType(final @NonNull Class<?> typeOfCollection) {

        if(SortedSet.class.equals(typeOfCollection)) {
            return toUnmodifiableSortedSet();
        }

        if(Set.class.equals(typeOfCollection)) {
            return toUnmodifiableSet();
        }

        if(List.class.equals(typeOfCollection)) {
            return toUnmodifiableList();
        }

        if(Collection.class.equals(typeOfCollection)) {
            return toUnmodifiableCollection();
        }

        throw new IllegalArgumentException(
                String.format("Can not collect into %s. Only List, Set, SortedSet and Collection are supported.",
                        typeOfCollection.getClass().getName()));
    }

    // -- COLLECT FROM ITERABLE

    /*
     * package private utility for a slightly heap pollution reduced collection,
     * if the iterable is a collection and we know the size of the result in advance
     *
     * @param iterable
     * @param factory
     * @param elementCollector
     * @return
     */
    static <T, R> R collectFromIterable(
            final @Nullable Iterable<T> iterable,
            final Function<Collection<T>, R> factory,
            final Supplier<Collector<T, ?, R>> elementCollector) {

        if(iterable==null) {
            return factory.apply(Collections.emptyList());
        }
        if(iterable instanceof Collection) {
            return factory.apply((Collection<T>) iterable);
        }
        return _NullSafe.stream(iterable)
                .collect(elementCollector.get());
    }

    // -- ELEMENT TYPE INFERENCE

    /**
     * Optionally returns the inferred element type for given {@code cls}, based on whether
     * it represents a collection and inference is possible.
     */
    public static Optional<Class<?>> inferElementType(final @NonNull Class<?> cls) {
        return _Collections.isCollectionType(cls)
                || _Collections.isImmutableCollectionType(cls)
                ? _Generics.streamGenericTypeArgumentsOfType(cls)
                        .findFirst()
                : Optional.empty();
    }

    /**
     * Optionally returns the inferred element type for given {@code param}, based on whether
     * it represents a collection and inference is possible.
     */
    public static Optional<Class<?>> inferElementType(final @NonNull Parameter param) {
        var parameterType = param.getType();
        return _Collections.isCollectionType(parameterType)
                || _Collections.isImmutableCollectionType(parameterType)
                ? _Generics.streamGenericTypeArgumentsOfParameter(param)
                        .findFirst()
                : Optional.empty();
    }

    /**
     * Optionally returns the inferred element type for given {@code method}'s return type,
     * based on whether
     * it represents a collection and inference is possible.
     */
    public static Optional<Class<?>> inferElementType(final @NonNull Method method) {
        var returnType = method.getReturnType();
        return _Collections.isCollectionType(returnType)
                || _Collections.isImmutableCollectionType(returnType)
                ? _Generics.streamGenericTypeArgumentsOfMethodReturnType(method)
                        .findFirst()
                : Optional.empty();
    }

    /**
     * Optionally returns the inferred element type for given {@code field}, based on whether
     * it represents a collection and inference is possible.
     */
    public static Optional<Class<?>> inferElementType(final @NonNull Field field) {
        var fieldType = field.getType();
        return _Collections.isCollectionType(fieldType)
                || _Collections.isImmutableCollectionType(fieldType)
                ? _Generics.streamGenericTypeArgumentsOfField(field)
                        .findFirst()
                : Optional.empty();
    }

    // -- TO STRING

    public static String toStringJoining(
            final @Nullable Collection<?> collection,
            final @NonNull String delimiter) {
        return _NullSafe.stream(collection)
                .map(x->""+x)
                .collect(Collectors.joining(delimiter));
    }

    public static String toStringJoiningNewLine(final @Nullable Collection<?> collection) {
        return toStringJoining(collection, "\n");
    }

}
