/******************************************************************************
 *
 * Copyright (c) 2017, the Perspective Authors.
 *
 * This file is part of the Perspective library, distributed under the terms of
 * the Apache License 2.0.  The full license can be found in the LICENSE file.
 *
 */

#pragma once

#include <perspective/first.h>
#include <perspective/base.h>
#include <perspective/data_table.h>
#include <tsl/hopscotch_map.h>
#include <tsl/hopscotch_set.h>
#include <perspective/mask.h>
#include <perspective/sym_table.h>
#include <perspective/rlookup.h>

namespace perspective {

std::pair<t_tscalar, t_tscalar> get_vec_min_max(
    const std::vector<t_tscalar>& vec);

class PERSPECTIVE_EXPORT t_gstate {
    /**
     * @brief A mapping of `t_tscalar` primary keys to `t_uindex` row indices.
     */
    typedef tsl::hopscotch_map<t_tscalar, t_uindex> t_mapping;

    typedef tsl::hopscotch_set<t_uindex> t_free_items;

public:
    /**
     * @brief Construct a new `t_gstate`, which manages the canonical state of
     * the `t_gnode` and associated `Table`.
     *
     * Contexts use `t_gstate` to read out columns for serialization,
     * calculate aggregates, and look up primary keys - the `t_gstate` will
     * contain the *latest* state of the dataset managed by Perspective,
     * after updates/removes/etc. are applied.
     *
     * @param input_schema
     * @param output_schema
     */
    t_gstate(const t_schema& input_schema, const t_schema& output_schema);

    ~t_gstate();

    void init();

    /**
     * @brief Look up a primary key in the `t_gstate`'s mapping of primary
     * keys to row numbers.
     *
     * @param pkey
     * @return t_rlookup a struct containing row index `m_idx` and `m_exists`,
     * whether `pkey` exists at that row index. If `pkey` is not found,
     * `m_idx` is 0 and `m_exists` is false.
     */
    t_rlookup lookup(t_tscalar pkey) const;

    /**
     * @brief If the master table has 0 rows, fill it using `flattened`.
     *
     * @param flattened
     */
    void fill_master_table(const t_data_table* flattened);

    /**
     * @brief Update the master `t_data_table` with the flattened and masked
     * `t_data_table` after an `update` has been called and fully processed
     * by `t_gnode::_process_table`.
     *
     * @param flattened
     */
    void update_master_table(const t_data_table* flattened);

    /**
     * @brief Given a column in the master data table and the corresponding
     * column in the `flattened` data table, fill the master column with data
     * from the flattened column.
     *
     * @param master_column
     * @param flattened_column
     */
    void update_master_column(t_column* master_column,
        const t_column* flattened_column, const t_column* op_column,
        const std::vector<t_uindex>& master_table_indexes, t_uindex num_rows);

    // Operations that use the gnode state's internal `m_mapping` of
    // primary keys to row indices in order to access subsets of data from
    // a column in a given `t_data_table`. Because these methods are used
    // extensively in the context objects, and because expression columns are
    // stored in tables on the context, these methods receive a pointer to
    // a `t_data_table` - usually a pointer to the gnode state's master
    // table or the expression table from the context.

    t_tscalar read_by_pkey(const t_data_table& table,
        const std::string& colname, t_tscalar& pkey) const;

    void read_column(const t_data_table& table, const std::string& colname,
        const std::vector<t_tscalar>& pkeys,
        std::vector<t_tscalar>& out_data) const;

    void read_column(const t_data_table& table, const std::string& colname,
        const std::vector<t_tscalar>& pkeys,
        std::vector<double>& out_data) const;

    void read_column(const t_data_table& table, const std::string& colname,
        const std::vector<t_tscalar>& pkeys, std::vector<double>& out_data,
        bool include_nones) const;

    void read_column(const t_data_table& table, const std::string& colname,
        t_uindex start_idx, t_uindex end_idx,
        std::vector<t_tscalar>& out_data) const;

    void read_column(const t_data_table& table, const std::string& colname,
        const std::vector<t_uindex>& row_indices,
        std::vector<t_tscalar>& out_data) const;

    // Also called extensively in contexts during aggregate calculation

    bool apply(const t_data_table& table, const std::string& colname,
        const std::vector<t_tscalar>& pkeys, t_tscalar& value,
        std::function<bool(const t_tscalar&, t_tscalar&)> fn) const;

    /**
     * @brief Reduce the column's values at the specified primary keys, and
     * return a single, reduced value.
     *
     * @tparam FN_T
     * @param pkeys
     * @param colname
     * @param fn
     * @return FN_T::result_type
     */
    template <typename FN_T>
    typename FN_T::result_type reduce(const t_data_table& table,
        const std::string& colname, const std::vector<t_tscalar>& pkeys,
        FN_T fn) const;

    bool is_unique(const t_data_table& table, const std::string& colname,
        const std::vector<t_tscalar>& pkeys, t_tscalar& value) const;

    /**
     * @brief Returns the scalar value at column `colname` with primary key
     * `pkey`.
     *
     * @param pkey
     * @param colname
     * @return t_tscalar
     */
    t_tscalar get(const t_data_table& table, const std::string& colname,
        t_tscalar pkey) const;

    /**
     * @brief Get the scalar value for `pkey` in `colname`.
     *
     * @param pkey
     * @param colname
     * @return t_tscalar
     */
    t_tscalar get_value(const t_data_table& table, const std::string& colname,
        const t_tscalar& pkey) const;

    /**
     * @brief Return the number of rows on the master `t_data_table`.
     *
     * @return t_uindex
     */
    t_uindex num_rows() const;

    /**
     * @brief Return the number of columns on the master `t_data_table`.
     *
     * @return t_uindex
     */
    t_uindex num_columns() const;

    /**
     * @brief Returns the size of the underlying primary key map.
     *
     * @return t_uindex
     */
    t_uindex mapping_size() const;

    /**
     * @brief Resets the gnode state and its master `t_data_table` and
     * mapping.
     *
     */
    void reset();

    // Getters
    std::shared_ptr<t_data_table> get_table() const;
    std::shared_ptr<t_data_table> get_pkeyed_table() const;

    const t_schema& get_input_schema() const;
    const t_schema& get_output_schema() const;

    std::vector<t_tscalar> get_row_data_pkeys(
        const std::vector<t_tscalar>& pkeys) const;

    void pprint() const;

protected:
    /**
     * @brief If the pkey exists in the state, return its row index. Otherwise,
     * add a new row to the mapping and return its index.
     *
     * @param pkey
     * @return t_uindex
     */
    t_uindex lookup_or_create(const t_tscalar& pkey);

    /**
     * @brief Clear the value at `pkey` for every column in the table.
     *
     * @param pkey
     */
    void erase(const t_tscalar& pkey);

    /**
     * @brief Generate a `t_mask` bitset set to true for every value in the
     * underlying `m_mapping`.
     *
     * @return t_mask
     */
    t_mask get_cpp_mask() const;

    void _mark_deleted(t_uindex idx);
    bool has_pkey(t_tscalar pkey) const;
    t_dtype get_pkey_dtype() const;

private:
    // Unused methods
    std::vector<t_uindex> get_pkeys_idx(
        const std::vector<t_tscalar>& pkeys) const;
    std::vector<t_tscalar> has_pkeys(const std::vector<t_tscalar>& pkeys) const;
    std::vector<t_tscalar> get_pkeys() const;

    t_schema m_input_schema;  // pkeyed
    t_schema m_output_schema; // tblschema

    bool m_init;
    std::shared_ptr<t_data_table> m_table;
    t_mapping m_mapping;
    t_free_items m_free;
    t_symtable m_symtable;
    std::shared_ptr<t_column> m_pkcol;
    std::shared_ptr<t_column> m_opcol;
};

template <typename FN_T>
typename FN_T::result_type
t_gstate::reduce(const t_data_table& table, const std::string& colname,
    const std::vector<t_tscalar>& pkeys, FN_T fn) const {
    std::vector<t_tscalar> data;
    read_column(table, colname, pkeys, data);
    return fn(data);
}

} // end namespace perspective
