#ifndef PGCONTAINER_H
#define PGCONTAINER_H

#include "Pgsql_declare.h"
#include "Pgsql_Result.h"
#include <QString>
#include <functional>
#include <memory>
#include <vector>
#include <libpq-fe.h>


class PgDatabaseCatalog;

class IPgContainer {
public:
	IPgContainer(PgDatabaseCatalog& cat);
	virtual ~IPgContainer() = default;

	virtual std::string getLoadQuery() const = 0;
	virtual void load(const Pgsql::Result &res) = 0;
	virtual void loadAll(Pgsql::Connection &conn);

	bool minimumVersion(int required_version) const;
	bool lessThenVersion(int required_version) const;

protected:
	PgDatabaseCatalog& m_catalog;
};

template<typename T, typename K=Oid>
class PgContainer: public IPgContainer {
public:
	using t_Container = std::vector<T>; ///< Do not assume it will stay a vector only expect bidirectional access

	PgContainer() = default;

	explicit PgContainer(PgDatabaseCatalog& cat)
		: IPgContainer(cat)
	{
	}

	typename t_Container::const_iterator begin() const
	{
		return m_container.begin();
	}

	typename t_Container::const_iterator end() const
	{
		return m_container.end();
	}

	void clear()
	{
		m_container.clear();
	}

	size_t count() const
	{
		return m_container.size();
	}

	const T* getByKey(const K &key) const
	{
		auto lb_result = std::lower_bound(m_container.begin(), m_container.end(), key);
		if (lb_result != m_container.end() && *lb_result == key)
			return &*lb_result;

		return nullptr;
	}

    const T* findIf(std::function<bool(const T&)> func) const
    {
        auto findResult = std::find_if(m_container.begin(), m_container.end(), func);
        if (findResult != m_container.end())
            return &*findResult;

        return nullptr;
    }

	const T* getByName(const QString &name) const
	{
		auto find_res = std::find(m_container.begin(), m_container.end(), name);

		if (find_res != m_container.end())
			return &*find_res;

		return nullptr;
	}

    const T* getByObjectName(const QString &name) const
    {
        auto find_res = std::find_if(m_container.begin(), m_container.end(),
                                     [&name](const T& item)
                                     {
                                         return item.objectName() == name;
                                     });

        if (find_res != m_container.end())
            return &*find_res;

        return nullptr;
    }

    const T* getByObjectNsAndName(const QString &ns, const QString &name) const
    {
        auto find_res = std::find_if(m_container.begin(), m_container.end(),
            [&ns, &name](const T& item)
            {
                // check name first as it is less likely to pass
                return item.objectName() == name
                    && item.nsName() == ns;
            });

        if (find_res != m_container.end())
            return &*find_res;

        return nullptr;
    }

	/// Retrieve element by index
	///
	/// This function throws when idx is out of range
	/// otherwise it always returns a valid object.
	const T& getByIdx(int idx) const
	{
		return m_container.at(idx);
	}

	/** Override to implement complete loading logic.
	 *
	 * Do not override this function if you only want to implement
	 * the loading of a single element. Override loadElem instead.
	 */
	virtual void load(const Pgsql::Result &res) override
	{
		m_container.clear();
		m_container.reserve(res.rows());
		for (auto row : res)
			m_container.push_back(loadElem(row));

		std::sort(m_container.begin(), m_container.end());
	}

	// Meant for mocking during testing
	void add(const T &elem)
	{
		m_container.push_back(elem);
		std::sort(m_container.begin(), m_container.end());
	}
protected:
	t_Container m_container;

	/** Override the implementation for this function to implement loading of single row.
	 *
	 * When overriding this function there is no need to override load.
	 */
	virtual T loadElem(const Pgsql::Row &) = 0;

};


template<typename T, typename K=Oid>
class PgSPtrContainer: public IPgContainer {
public:
	using t_Elem = std::shared_ptr<T>;
	using t_Container = std::vector<t_Elem>; ///< Do not assume it will stay a vector only expect bidirectional access

	explicit PgSPtrContainer(PgDatabaseCatalog& cat)
		: IPgContainer(cat)
	{}


	typename t_Container::const_iterator begin() const
	{
		return m_container.begin();
	}

	typename t_Container::const_iterator end() const
	{
		return m_container.end();
	}

	void clear()
	{
		m_container.clear();
	}

	int count() const
	{
		return static_cast<int>(m_container.size());
	}

	const t_Elem getByKey(const K &key) const
	{
		auto lb_result = std::lower_bound(m_container.begin(), m_container.end(), key);
		if (lb_result != m_container.end() && **lb_result == key)
			return *lb_result;

		return nullptr;
	}

	const t_Elem getByName(const QString name) const
	{
		auto find_res = std::find_if(m_container.begin(), m_container.end(),
						[name](auto e) -> bool { return *e = name; } );

		if (find_res != m_container.end())
			return *find_res;

		return nullptr;
	}

	const t_Elem getByIdx(int idx) const
	{
		return m_container.at(idx);
	}

	/** Override to implement complete loading logic.
	 *
	 * Do not override this function if you only want to implement
	 * the loading of a single element. Override loadElem instead.
	 */
	virtual void load(const Pgsql::Result &res) override
	{
		m_container.clear();
		m_container.reserve(res.rows());
		for (auto row : res)
			m_container.push_back(loadElem(row));

		std::sort(m_container.begin(), m_container.end());
	}
protected:
	t_Container m_container;

	/** Override the implementation for this function to implement loading of single row.
	 *
	 * When overriding this function there is no need to override load.
	 */
	virtual t_Elem loadElem(const Pgsql::Row &) = 0;
private:

};


#endif // PGCONTAINER_H
