#ifndef KMEANS_MATRIX_HPP
#define KMEANS_MATRIX_HPP

#include <memory>
#include <utility>
#include <cstddef>

#include "utils.hpp"

/**
 * @file Matrix.hpp
 * @brief Interface for matrix inputs.
 */

namespace kmeans {

/**
 * @brief Extractor for accessing random observations.
 *
 * This is typically generated by the `Matrix::new_extractor()` overload that accepts no arguments.
 * It can be assumed that the lifetime of each extractor will not exceed that of its parent `Matrix`.
 *
 * @tparam Index_ Integer type of the observation indices.
 * @tparam Data_ Numeric type of the data.
 */
template<typename Index_, typename Data_>
class RandomAccessExtractor {
public:
    /**
     * @cond
     */
    RandomAccessExtractor() = default;
    RandomAccessExtractor(const RandomAccessExtractor&) = default;
    RandomAccessExtractor(RandomAccessExtractor&&) = default;
    RandomAccessExtractor& operator=(const RandomAccessExtractor&) = default;
    RandomAccessExtractor& operator=(RandomAccessExtractor&&) = default;
    virtual ~RandomAccessExtractor() = default;
    /**
     * @endcond
     */

    /**
     * @param i Index of the observation.
     * @return Pointer to an array of length equal to `Matrix::num_dimensions()`, containing the coordinates for observation `i`.
     *
     * The pointer returned by this method should not be used after the next call to `get_observation()` for the same `RandomAccessExtractor` instance.
     * Similarly, it should not be used after the instance is destroyed.
     *
     * This method will only be called within a single thread and may modify internal data members of its `RandomAccessExtractor` instance.
     */
    virtual const Data_* get_observation(Index_ i) = 0;
};

/**
 * @brief Extractor for accessing consecutive observations.
 *
 * This is typically generated by the `Matrix::new_extractor()` overload the accepts a start and length parameter.
 * It can be assumed that the lifetime of each extractor will not exceed that of its parent `Matrix`.
 *
 * @tparam Index_ Integer type of the observation indices.
 * @tparam Data_ Numeric type of the data.
 */
template<typename Index_, typename Data_>
class ConsecutiveAccessExtractor {
public:
    /**
     * @cond
     */
    ConsecutiveAccessExtractor() = default;
    ConsecutiveAccessExtractor(const ConsecutiveAccessExtractor&) = default;
    ConsecutiveAccessExtractor(ConsecutiveAccessExtractor&&) = default;
    ConsecutiveAccessExtractor& operator=(const ConsecutiveAccessExtractor&) = default;
    ConsecutiveAccessExtractor& operator=(ConsecutiveAccessExtractor&&) = default;
    virtual ~ConsecutiveAccessExtractor() = default;
    /**
     * @endcond
     */

    /**
     * @return Pointer to an array of length equal to `Matrix::num_dimensions()`, containing the coordinates for the next observation.
     *
     * The first call to this method should return the coordinates of the `start` observation from `Matrix::new_extractor()`.
     * The next call should return `start + 1`, etc. until a maximum of `length` calls have been performed.
     *
     * The pointer returned by this method should not be used after the next call to `get_observation()` for the same `ConsecutiveAccessExtractor` instance.
     * Similarly, it should not be used after the instance is destroyed.
     *
     * This method will only be called within a single thread and may modify internal data members of its `ConsecutiveAccessExtractor` instance.
     */
    virtual const Data_* get_observation() = 0;
};

/**
 * @brief Extractor for accessing indexed observations.
 *
 * This is typically generated by the `Matrix::new_extractor()` overload that accepts a pointer to a sequence of indices.
 * It can be assumed that the lifetime of each extractor will not exceed that of its parent `Matrix`.
 *
 * @tparam Index_ Integer type of the observation indices.
 * @tparam Data_ Numeric type of the data.
 */
template<typename Index_, typename Data_>
class IndexedAccessExtractor {
public:
    /**
     * @cond
     */
    IndexedAccessExtractor() = default;
    IndexedAccessExtractor(const IndexedAccessExtractor&) = default;
    IndexedAccessExtractor(IndexedAccessExtractor&&) = default;
    IndexedAccessExtractor& operator=(const IndexedAccessExtractor&) = default;
    IndexedAccessExtractor& operator=(IndexedAccessExtractor&&) = default;
    virtual ~IndexedAccessExtractor() = default;
    /**
     * @endcond
     */

    /**
     * @return Pointer to an array of length equal to `Matrix::num_dimensions()`, containing the coordinates for the next observation.
     *
     * The first call to this method should return the coordinates of the `sequence[0]` observation from `Matrix::new_extractor()`.
     * The next call should return `sequence[1]`, etc. until a maximum of `length` calls have been performed.
     *
     * The pointer returned by this method should not be used after the next call to `get_observation()` for the same `IndexedAccessExtractor` instance.
     * Similarly, it should not be used after the instance is destroyed.
     *
     * This method will only be called within a single thread and may modify internal data members of its `IndexedAccessExtractor` instance.
     */
    virtual const Data_* get_observation() = 0;
};

/**
 * @brief Interface for matrix data.
 *
 * This defines the expectations for a matrix of observation-level data to be used in `Initialize::run()` and `Refine::run()`.
 * Each matrix should support extraction of the vector of coordinates for each observation.
 *
 * @tparam Index_ Integer type of the observation indices.
 * @tparam Data_ Numeric type of the data.
 */
template<typename Index_, typename Data_>
class Matrix {
public:
    /**
     * @cond
     */
    Matrix() = default;
    Matrix(const Matrix&) = default;
    Matrix(Matrix&&) = default;
    Matrix& operator=(const Matrix&) = default;
    Matrix& operator=(Matrix&&) = default;
    virtual ~Matrix() = default;
    /**
     * @endcond
     */

public:
    /**
     * @return Number of observations.
     */
    virtual Index_ num_observations() const = 0;

    /**
     * @return Number of dimensions.
     */
    virtual std::size_t num_dimensions() const = 0;

public:
    /**
     * @return A new random-access extractor.
     */
    virtual std::unique_ptr<RandomAccessExtractor<Index_, Data_> > new_extractor() const = 0;

    /**
     * @param start Start of the contiguous block of observations to be accessed consecutively.
     * @param length Length of the contiguous block of observations to be accessed consecutively.
     * @return A new consecutive-access extractor.
     */
    virtual std::unique_ptr<ConsecutiveAccessExtractor<Index_, Data_> > new_extractor(Index_ start, Index_ length) const = 0;

    /**
     * @param[in] sequence Pointer to an array of sorted and unique indices of observations, to be accessed in the provided order.
     * It is assumed that the vector will not be deallocated before the destruction of the returned `IndexedAccessExtractor`.
     * @param length Number of observations in `sequence`.
     * @return A new indexed-access extractor.
     */
    virtual std::unique_ptr<IndexedAccessExtractor<Index_, Data_> > new_extractor(const Index_* sequence, std::size_t length) const = 0;

public:
    /**
     * @return A new random-access extractor.
     *
     * Subclasses may override this method to return a pointer to a specific `RandomAccessExtractor` subclass.
     * This is used for devirtualization in other **kmeans** functions. 
     * If no override is provided, `new_extractor()` is called instead.
     */
    auto new_known_extractor() const {
        return new_extractor();
    }

    /**
     * @param start Start of the contiguous block, see the equivalent `new_extractor()` overload.
     * @param length Length of the contiguous block, see the equivalent `new_extractor()` overload.
     * @return A new consecutive-access extractor.
     *
     * Subclasses may override this method to return a pointer to a specific `ConsecutiveAccessExtractor` subclass.
     * This is used for devirtualization in other **kmeans** functions. 
     * If no override is provided, the relevant `new_extractor()` overload is called instead.
     */
    auto new_known_extractor(Index_ start, Index_ length) const {
        return new_extractor(start, length);
    }

    /**
     * @param sequence Pointer to a sorted and unique array of observations, see the equivalent `new_extractor()` overload.
     * @param length Length of the array, see the equivalent `new_extractor()` overload.
     * @return A new indexed-access extractor.
     *
     * Subclasses may override this method to return a pointer to a specific `IndexedAccessExtractor` subclass.
     * This is used for devirtualization in other **kmeans** functions. 
     * If no override is provided, the relevant `new_extractor()` overload is called instead.
     */
    auto new_known_extractor(const Index_* sequence, std::size_t length) const {
        return new_extractor(sequence, length);
    }
};

/**
 * @cond
 */
// A simple alias to help determine the Index_ inside a Matrix_ instance.
template<class Matrix_>
using Index = I<decltype(std::declval<Matrix_>().num_observations())>;
/**
 * @endcond
 */
}

#endif
