//
// Created by Dan Crosby on 12/25/20.
//

#ifndef EAGLE0_CONTAINERUTILS_HPP
#define EAGLE0_CONTAINERUTILS_HPP

#include <algorithm>
#include <functional>
#include <optional>

namespace common {

using std::allocator;
using std::back_inserter;
using std::begin;
using std::copy_if;
using std::count_if;
using std::end;
using std::find;
using std::find_if;
using std::function;
using std::optional;
using std::remove_if;
using std::vector;

template<class T, class Container>
auto Contains(const Container& container, const T& elt) -> bool {
    return find(begin(container), end(container), elt) != end(container);
}

template<class Container, class Func>
auto CountIf(const Container& container, Func fn) -> size_t {
    Container result{};
    return count_if(begin(container), end(container), fn);
}

template<class Container, class Func>
void FilterInPlace(Container& container, Func fn) {
    container.erase(
            remove_if(begin(container), end(container), [fn](const auto& elt) { return !fn(elt); }),
            end(container));
}

template<class Container, class Func>
auto Filtered(const Container& container, Func fn) -> Container {
    Container result{};
    copy_if(begin(container), end(container), back_inserter(result), fn);

    return result;
}

template<class Container, class Func>
auto FilteredToVector(const Container& container, Func fn) -> decltype(auto) {
    typedef typename Container::value_type value_type;
    vector<value_type> result{};
    copy_if(begin(container), end(container), back_inserter(result), fn);

    return result;
}

template<typename Container, typename Func>
auto FindIf(const Container& container, Func fn) -> optional<typename Container::value_type> {
    const auto& t = find_if(begin(container), end(container), fn);

    if (t == end(container)) {
        return {};
    } else {
        return optional<typename Container::value_type>(*t);
    }
}

template<typename Container, typename Func>
auto ContainsWhere(const Container& container, Func fn) -> bool {
    return find_if(begin(container), end(container), fn) != end(container);
}

template<
        template<typename, typename>
        class TwoTypeContainer,
        typename T,
        typename Allocator = allocator<T>,
        typename Func>
auto Map(const TwoTypeContainer<T, Allocator>& input, Func fn) -> decltype(auto) {
    typedef typename decltype(function(fn))::result_type result_type;

    TwoTypeContainer<result_type, allocator<result_type>> result{};
    result.reserve(input.size());

    transform(begin(input), end(input), back_inserter(result), fn);
    return result;
}

template<template<typename> class OneTypeContainer, typename T, typename Func>
auto Map(const OneTypeContainer<T>& input, Func fn) -> decltype(auto) {
    typedef typename decltype(function(fn))::result_type result_type;

    OneTypeContainer<result_type> result{};
    result.reserve(input.size());

    transform(begin(input), end(input), back_inserter(result), fn);
    return result;
}

template<typename Container, typename Func>
auto MapToVector(const Container& input, Func fn) -> decltype(auto) {
    typedef typename decltype(function(fn))::result_type result_type;

    vector<result_type> result{};

    transform(begin(input), end(input), back_inserter(result), fn);
    return result;
}

template<
        template<typename, typename>
        class TwoTypeContainer,
        typename T,
        typename Allocator = allocator<T>,
        typename Func>
auto FlatMap(const TwoTypeContainer<T, Allocator>& input, Func fn) -> decltype(auto) {
    typedef typename decltype(function(fn))::result_type::value_type result_value_type;

    TwoTypeContainer<result_value_type, allocator<result_value_type>> result{};

    for (const auto& elt : input) {
        const auto& outContainer = fn(elt);
        for (const auto& outElt : outContainer) { result.push_back(outElt); }
    }
    return result;
}

template<template<typename> class OneTypeContainer, typename T, typename Func>
auto FlatMap(const OneTypeContainer<T>& input, Func fn) -> decltype(auto) {
    typedef typename decltype(function(fn))::result_type::value_type result_value_type;

    OneTypeContainer<result_value_type> result{};

    for (const auto& elt : input) {
        const auto& outContainer = fn(elt);
        for (const auto& outElt : outContainer) { result.push_back(outElt); }
    }
    return result;
}

template<typename Container, typename Func>
auto FlatMapToVector(const Container& input, Func fn) -> decltype(auto) {
    typedef typename decltype(function(fn))::result_type::value_type value_type;

    vector<value_type> result{};

    for (const auto& elt : input) {
        const auto& outContainer = fn(elt);
        for (const auto& outElt : outContainer) { result.push_back(outElt); }
    }
    return result;
}

template<typename Container>
auto ToVector(const Container& input) -> decltype(auto) {
    typedef typename Container::value_type value_type;
    return vector<value_type>(begin(input), end(input));
}

template<typename C1, typename C2>
auto Append(C1& recipient, const C2& newItems) -> C1& {
    recipient.insert(end(recipient), begin(newItems), end(newItems));
    return recipient;
}

}  // namespace common

#endif  // EAGLE0_CONTAINERUTILS_HPP
