/*
 * Copyright (C) 2019-2022 LitterBox contributors
 *
 * This file is part of LitterBox.
 *
 * LitterBox is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or (at
 * your option) any later version.
 *
 * LitterBox is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with LitterBox. If not, see <http://www.gnu.org/licenses/>.
 */
package de.uni_passau.fim.se2.litterbox.utils;

import java.util.AbstractCollection;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * An immutable container class to store two elements of the same type. The class prohibits {@code
 * null} elements.
 *
 * @param <T> the type of the elements in this pair
 * @author Sebastian Schweikl
 */
public final class Pair<T> extends AbstractCollection<T> {

    /**
     * The first element of this pair.
     */
    private final T fst;

    /**
     * The second element of this pair.
     */
    private final T snd;

    /**
     * Creates a new pair that contains the same elements as the given pair.
     *
     * @param copy the pair whose elements to place into the new pair
     * @throws NullPointerException if the specified pair is {@code null}
     */
    public Pair(final Pair<? extends T> copy) {
        Objects.requireNonNull(copy);
        this.fst = copy.getFst();
        this.snd = copy.getSnd();
    }

    /**
     * Constructs a new pair from the given elements, neither of which is allowed to be {@code
     * null}.
     *
     * @param fst the first element
     * @param snd the second element
     */
    public Pair(final T fst, final T snd) {
        this.fst = Objects.requireNonNull(fst);
        this.snd = Objects.requireNonNull(snd);
    }

    /**
     * Creates a new pair containing the specified elements.
     *
     * @param fst the first component
     * @param snd the second component
     * @param <T> the type of the elements
     * @return a pair containing the specified elements
     * @throws NullPointerException if one of the element is {@code null}
     */
    public static <T> Pair<T> of(final T fst, final T snd) {
        return new Pair<>(fst, snd);
    }

    /**
     * Returns a pair where each element is generated by the provided supplier. This is suitable for
     * generating constant pairs, pairs of random elements, etc.
     *
     * @param supplier the supplier of generated elements
     * @param <T>      the element type of the pair
     * @return the generated pair
     */
    public static <T> Pair<T> generate(final Supplier<T> supplier) {
        return new Pair<>(supplier.get(), supplier.get());
    }

    /**
     * Returns a reference to the first element of this pair.
     *
     * @return the first element
     */
    public T getFst() {
        return fst;
    }

    /**
     * Returns a reference to the second element of this pair.
     *
     * @return the second element
     */
    public T getSnd() {
        return snd;
    }

    /**
     * Returns the pair consisting of the results of applying the given mapper functions to the
     * elements of this pair.
     *
     * @param mapperFst the mapper to apply to the first element of this pair
     * @param mapperSnd the mapper to apply to the second element of this pair
     * @param <U>       the result type of mapping the elements
     * @return the mapped pair
     */
    public <U> Pair<U> map(
            final Function<? super T, U> mapperFst,
            final Function<? super T, U> mapperSnd) {
        Objects.requireNonNull(mapperFst);
        Objects.requireNonNull(mapperSnd);
        return new Pair<>(mapperFst.apply(fst), mapperSnd.apply(snd));
    }

    /**
     * Returns the pair consisting of the results of applying the given mapper function to the
     * elements of this pair.
     *
     * @param mapper the mapper to apply to the each element of this pair
     * @param <U>    the result type of mapping an element
     * @return the mapped pair
     */
    public <U> Pair<U> map(final Function<? super T, U> mapper) {
        return map(mapper, mapper);
    }

    /**
     * Performs a reduction operation on this pair, using the supplied {@code combiner} function.
     *
     * @param combiner the function used to combine the two elements of this pair
     * @param <U>      the result type
     * @return the result of the reduction.
     */
    public <U> U reduce(final Function<Pair<? extends T>, U> combiner) {
        Objects.requireNonNull(combiner);
        return combiner.apply(this);
    }

    /**
     * Performs a reduction operation on the elements of this pair, using the supplied {@code
     * combiner} function.
     *
     * @param combiner the function used to combine the two elements of this pair
     * @param <U>      the result type
     * @return the result of the reduction.
     */
    public <U> U reduce(final BiFunction<? super T, ? super T, U> combiner) {
        Objects.requireNonNull(combiner);
        return combiner.apply(fst, snd);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Iterator<T> iterator() {
        return new Iterator<>() {
            private int index = 0;

            @Override
            public boolean hasNext() {
                return index < 2;
            }

            @Override
            public T next() {
                switch (index++) {
                    case 0:
                        return getFst();
                    case 1:
                        return getSnd();
                    default:
                        throw new NoSuchElementException();
                }
            }
        };
    }

    /**
     * Returns the number of elements in this collection. Since this is a pair, the result is always
     * 2.
     *
     * @return always 2
     */
    @Override
    public int size() {
        return 2;
    }

    /**
     * Compares this pair to the given other pair for equality. To this, a component-wise equality
     * comparison is performed on their elements. Two pairs are considered equal if and only if
     * their elements are equal. Returns {@code false} if the given other pair is {@code null}.
     *
     * @param other the pair to compare to
     * @return {@code true} if the elements of the given pair are equal to this one, {@code false}
     *       otherwise
     */
    @Override
    public boolean equals(final Object other) {
        if (this == other) {
            return true;
        }

        if (other == null || getClass() != other.getClass()) {
            return false;
        }

        final Pair<?> that = (Pair<?>) other;
        return this.getFst().equals(that.getFst()) && this.getSnd().equals(that.getSnd());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int hashCode() {
        return Objects.hash(getFst(), getSnd());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return "Pair(" + getFst() + ", " + getSnd() + ')';
    }
}
