/*************************************************************************
 *
 * Copyright 2020 Realm Inc.
 *
 * Licensed 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.
 *
 **************************************************************************/

#ifndef REALM_BSON_INDEXED_MAP_HPP
#define REALM_BSON_INDEXED_MAP_HPP

#include <stdio.h>
#include <string>
#include <unordered_map>
#include <vector>

namespace realm {
namespace bson {

/// A map type that orders based on insertion order.
template <typename T>
class IndexedMap {
public:
    using entry = std::pair<std::string, T>;

    class iterator {
        size_t m_idx = 0;
        const IndexedMap* m_map;

    public:
        iterator(const IndexedMap* map, size_t idx);
        entry operator*()
        {
            return m_map->operator[](m_idx);
        }
        iterator& operator++();
        iterator& operator--();
        iterator operator++(int);
        iterator operator--(int);

        bool operator!=(const typename IndexedMap<T>::iterator& rhs) const noexcept
        {
            return !(m_idx == rhs.m_idx);
        }
        bool operator==(const typename IndexedMap<T>::iterator& rhs) const noexcept
        {
            return m_idx == rhs.m_idx;
        }
    };


    constexpr IndexedMap() noexcept;
    IndexedMap(const IndexedMap&) = default;
    IndexedMap(IndexedMap&&) = default;
    IndexedMap& operator=(IndexedMap const&) = default;
    IndexedMap& operator=(IndexedMap&&) = default;

    IndexedMap(std::initializer_list<entry> entries);
    ~IndexedMap() = default;

    /// The size of the map
    size_t size() const;

    /// Find an entry by index
    entry operator[](size_t idx) const;

    iterator begin() const;

    iterator end() const;

    entry front();

    entry back();

    iterator find(const std::string& k) const;

    /// Find or add a given key
    T& operator[](const std::string& k);

    T& at(const std::string& k)
    {
        return m_map.at(k);
    }
    const T& at(const std::string& k) const
    {
        return m_map.at(k);
    }

    /// Whether or not this map is empty
    bool empty();

    /// Pop the last entry of the map
    void pop_back();

    const std::vector<std::string>& keys() const noexcept
    {
        return m_keys;
    }
    const std::unordered_map<std::string, T>& entries() const noexcept
    {
        return m_map;
    }
    std::unordered_map<std::string, T>& entries() noexcept
    {
        return m_map;
    }

private:
    template <typename V>
    friend bool operator==(const IndexedMap<V>& lhs, const IndexedMap<V>& rhs) noexcept;
    template <typename V>
    friend bool operator!=(const IndexedMap<V>& lhs, const IndexedMap<V>& rhs) noexcept;
    std::unordered_map<std::string, T> m_map;
    std::vector<std::string> m_keys;
};

template <typename T>
bool operator==(const typename IndexedMap<T>::iterator& lhs, const typename IndexedMap<T>::iterator& rhs) noexcept
{
    return lhs.m_idx == rhs.m_idx;
}

template <typename T>
bool operator!=(const typename IndexedMap<T>::iterator lhs, const typename IndexedMap<T>::iterator rhs) noexcept
{
    return !(lhs.m_idx == rhs.m_idx);
}

template <typename T>
bool operator==(const IndexedMap<T>& lhs, const IndexedMap<T>& rhs) noexcept
{
    return lhs.m_map == rhs.m_map && lhs.m_keys == rhs.m_keys;
}

template <typename T>
bool operator!=(const IndexedMap<T>& lhs, const IndexedMap<T>& rhs) noexcept
{
    return lhs.m_map != rhs.m_map && lhs.m_keys != rhs.m_keys;
}

template <typename T>
IndexedMap<T>::iterator::iterator(const IndexedMap<T>* map, size_t idx)
    : m_idx(idx)
    , m_map(map)
{
}

template <typename T>
typename IndexedMap<T>::iterator& IndexedMap<T>::iterator::operator++()
{
    m_idx++;
    return *this;
}

template <typename T>
typename IndexedMap<T>::iterator& IndexedMap<T>::iterator::operator--()
{
    m_idx--;
    return *this;
}

template <typename T>
typename IndexedMap<T>::iterator IndexedMap<T>::iterator::operator++(int)
{
    return IndexedMap<T>::iterator(m_map, m_idx++);
}

template <typename T>
typename IndexedMap<T>::iterator IndexedMap<T>::iterator::operator--(int)
{
    return IndexedMap<T>::iterator(m_map, m_idx--);
}

template <typename T>
constexpr IndexedMap<T>::IndexedMap() noexcept
{
}

template <typename T>
IndexedMap<T>::IndexedMap(std::initializer_list<entry> entries)
{
    for (auto& entry : entries) {
        m_keys.push_back(entry.first);
        m_map[entry.first] = entry.second;
    }
}

template <typename T>
size_t IndexedMap<T>::IndexedMap::size() const
{
    return m_map.size();
}

template <typename T>
std::pair<std::string, T> IndexedMap<T>::operator[](size_t idx) const
{
    auto key = m_keys[idx];
    return {key, m_map.at(key)};
}

template <typename T>
typename IndexedMap<T>::iterator IndexedMap<T>::begin() const
{
    return iterator(this, 0);
}

template <typename T>
typename IndexedMap<T>::iterator IndexedMap<T>::end() const
{
    return iterator(this, m_map.size());
}

template <typename T>
std::pair<std::string, T> IndexedMap<T>::front()
{
    return *this->begin();
}

template <typename T>
std::pair<std::string, T> IndexedMap<T>::back()
{
    return *(--this->end());
}

template <typename T>
typename IndexedMap<T>::iterator IndexedMap<T>::find(const std::string& k) const
{
    auto it = begin();
    while (it != end()) {
        if ((*it).first == k)
            return it;
        it++;
    }
    return it;
}

template <typename T>
T& IndexedMap<T>::operator[](const std::string& k)
{
    if (auto entry = m_map.find(k); entry != m_map.end()) {
        return entry->second;
    }

    m_keys.push_back(k);
    return m_map[k];
}

template <typename T>
bool IndexedMap<T>::empty()
{
    return m_map.empty();
}

template <typename T>
void IndexedMap<T>::pop_back()
{
    auto last_key = m_keys.back();
    m_keys.pop_back();
    m_map.erase(last_key);
}

} // namespace bson
} // namespace realm

#endif /* REALM_BSON_INDEXED_MAP_HPP */
