/*
 * 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.
 */

#pragma once

#include "ignite/common/config.h"
#include "ignite/common/ignite_error.h"

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

namespace ignite {

class ignite_tuple_builder;

/**
 * Ignite tuple.
 */
class ignite_tuple {
    friend class ignite_tuple_builder;
public:
    // Default
    ignite_tuple() = default;

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

    /**
     * Constructor.
     *
     * @param pairs Pairs.
     */
    ignite_tuple(std::initializer_list<std::pair<std::string, std::any>> pairs)
        : m_pairs(pairs)
        , m_indices()
    {
        for (size_t i = 0; i < m_pairs.size(); ++i)
            m_indices.emplace(std::make_pair(parse_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 std::any& get(uint32_t idx) const {
        if (idx > m_pairs.size()) {
            throw ignite_error("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 std::any_cast<T>(get(idx));
    }

    /**
     * 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("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 std::any& get(std::string_view name) const {
        auto it = m_indices.find(parse_name(name));
        if (it == m_indices.end())
            throw ignite_error("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 std::any_cast<T>(get(name));
    }

    /**
     * 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 = parse_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("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 name.
     */
    [[nodiscard]] int32_t column_ordinal(std::string_view name) const {
        auto it = m_indices.find(parse_name(name));
        if (it == m_indices.end())
            return -1;
        return int32_t(it->second);
    }

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

    /**
     * Normalize column name.
     *
     * @param name The column name.
     * @return Normalized column name.
     */
    [[nodiscard]] static std::string parse_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;
    }

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

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

} // namespace ignite
