/*
 *  Copyright (C) GridGain Systems. All Rights Reserved.
 *  _________        _____ __________________        _____
 *  __  ____/___________(_)______  /__  ____/______ ____(_)_______
 *  _  / __  __  ___/__  / _  __  / _  / __  _  __ `/__  / __  __ \
 *  / /_/ /  _  /    _  /  / /_/ /  / /_/ /  / /_/ / _  /  _  / / /
 *  \____/   /_/     /_/   \_,__/   \____/   \__,_/  /_/   /_/ /_/
 */

#pragma once

#include <ignite/common/ignite_error.h>
#include <ignite/common/primitive.h>

#include <initializer_list>
#include <string_view>
#include <unordered_map>
#include <utility>
#include <vector>

namespace ignite {
// Forward declaration.
class ignite_tuple;

namespace detail {
ignite_tuple concat(const ignite_tuple &left, const ignite_tuple &right);

/**
 * Parse column name.
 *
 * @param name Column name.
 * @return Parsed column name.
 */
inline std::string parse_column_name(std::string_view name) {
    if (name.size() >= 2 && name.front() == '"' && name.back() == '"')
        name = name.substr(1, name.size() - 2);

    if (name.empty())
        throw ignite_error("Column name can not be an empty string");

    std::string res;
    res.reserve(name.size());

    for (auto c : name) {
        res.push_back(char(std::toupper(c)));
    }

    return res;
}

} // namespace detail

/**
 * Ignite tuple.
 */
class ignite_tuple {
    friend ignite_tuple detail::concat(const ignite_tuple &left, const ignite_tuple &right);

public:
    // Default
    ignite_tuple() = default;

    /**
     * Constructor.
     *
     * @param capacity Capacity.
     */
    explicit ignite_tuple(size_t capacity) {
        m_pairs.reserve(capacity);
        m_indices.reserve(capacity);
    }

    /**
     * Constructor.
     *
     * @param pairs Pairs.
     */
    ignite_tuple(std::initializer_list<std::pair<std::string, primitive>> pairs)
        : m_pairs(pairs)
        , m_indices() {
        m_indices.reserve(pairs.size());
        for (size_t i = 0; i < m_pairs.size(); ++i)
            m_indices.emplace(std::make_pair(detail::parse_column_name(m_pairs[i].first), i));
    }

    /**
     * Gets a number of columns in the tuple.
     *
     * @return Number of columns in the tuple.
     */
    [[nodiscard]] std::int32_t column_count() const noexcept { return std::int32_t(m_pairs.size()); }

    /**
     * Gets the value of the specified column.
     *
     * @param idx The column index.
     * @return Column value.
     */
    [[nodiscard]] const primitive &get(uint32_t idx) const {
        if (idx > m_pairs.size()) {
            throw ignite_error(error::code::ILLEGAL_ARGUMENT,
                "Index is too large: idx=" + std::to_string(idx) + ", columns_num=" + std::to_string(m_pairs.size()));
        }
        return m_pairs[idx].second;
    }

    /**
     * Gets the value of the specified column.
     *
     * @tparam T Column type.
     * @param idx The column index.
     * @return Column value.
     */
    template<typename T>
    [[nodiscard]] T get(uint32_t idx) const {
        return get(idx).template get<T>();
    }

    /**
     * Sets the value of the specified column.
     *
     * @tparam T Column type.
     * @param idx The column index.
     * @param value Value.
     */
    template<typename T>
    void set(uint32_t idx, T &&value) {
        if (idx > m_pairs.size()) {
            throw ignite_error(error::code::ILLEGAL_ARGUMENT,
                "Index is too large: idx=" + std::to_string(idx) + ", columns_num=" + std::to_string(m_pairs.size()));
        }
        m_pairs[idx].second = std::forward<T>(value);
    }

    /**
     * Gets the value of the specified column.
     *
     * @param name The column name.
     * @return Column value.
     */
    [[nodiscard]] const primitive &get(std::string_view name) const {
        auto it = m_indices.find(detail::parse_column_name(name));
        if (it == m_indices.end())
            throw ignite_error(error::code::ILLEGAL_ARGUMENT,
                "Can not find column with the name '" + std::string(name) + "' in the tuple");
        auto idx = it->second;
        return m_pairs[idx].second;
    }

    /**
     * Gets the value of the specified column.
     *
     * @tparam T Column type.
     * @param name The column name.
     * @return Column value.
     */
    template<typename T>
    [[nodiscard]] T get(std::string_view name) const {
        return get(name).template get<T>();
    }

    /**
     * Sets the value of the specified column.
     *
     * @tparam T Column type.
     * @param name The column name.
     * @param value Value.
     */
    template<typename T>
    void set(std::string_view name, T &&value) {
        auto parsed_name = detail::parse_column_name(name);
        auto it = m_indices.find(parsed_name);
        if (it != m_indices.end()) {
            auto idx = it->second;
            m_pairs[idx].second = std::forward<T>(value);
            return;
        }

        m_pairs.emplace_back(name, std::forward<T>(value));
        m_indices.emplace(parsed_name, m_pairs.size() - 1);
    }

    /**
     * Gets the name of the column, given the zero-based column ordinal.
     *
     * @param idx The column index.
     * @return Column name.
     */
    [[nodiscard]] const std::string &column_name(uint32_t idx) const {
        if (idx > m_pairs.size()) {
            throw ignite_error(error::code::ILLEGAL_ARGUMENT,
                "Index is too large: idx=" + std::to_string(idx) + ", columns_num=" + std::to_string(m_pairs.size()));
        }
        return m_pairs[idx].first;
    }

    /**
     * Gets the column ordinal given the name of the column, or -1 when
     * the column with the given name does not exist.
     *
     * @param name The column name.
     * @return Column index.
     */
    [[nodiscard]] std::int32_t column_ordinal(std::string_view name) const {
        auto it = m_indices.find(detail::parse_column_name(name));
        if (it == m_indices.end())
            return -1;
        return std::int32_t(it->second);
    }

private:
    /**
     * Constructor.
     *
     * @param pairs Pairs.
     * @param indices Indices.
     */
    ignite_tuple(
        std::vector<std::pair<std::string, primitive>> &&pairs, std::unordered_map<std::string, size_t> indices)
        : m_pairs(std::move(pairs))
        , m_indices(std::move(indices)) {}

    /** Pairs of column names and values. */
    std::vector<std::pair<std::string, primitive>> m_pairs;

    /** Indices of the columns corresponding to their names. */
    std::unordered_map<std::string, std::size_t> m_indices;
};

} // namespace ignite
