#pragma once

/*
 *
 * Official quasardb C++ API 2.0.02.0.1
 * c64b446 2017-10-09 20:00:48 +0000
 *
 * Copyright (c) 2009-2017, quasardb SAS
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *    * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *    * Neither the name of quasardb nor the names of its contributors may
 *      be used to endorse or promote products derived from this software
 *      without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY QUASARDB AND CONTRIBUTORS ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "batch.h"
#include "blob.h"
#include "client.h"
#include "deque.h"
#include "error.hpp"
#include "hset.h"
#include "integer.h"
#include "iterator.hpp"
#include "node.h"
#include "prefix.h"
#include "suffix.h"
#include "tag.h"

#include <algorithm>
#include <cstring>
#include <functional>
#include <iterator>
#include <limits>
#include <vector>

namespace qdb
{

static const qdb_time_t never_expires = qdb_never_expires;
static const qdb_time_t preserve_expiration = qdb_preserve_expiration;

inline std::string make_error_string(qdb_error_t error)
{
    return std::string(qdb_error(error));
}

struct remote_node
{
public:
    remote_node()
    {
    }

    remote_node(std::string address, unsigned short port) : _address(address), _port(port), _error(qdb_e_ok)
    {
    }

    std::string & address()
    {
        return _address;
    }
    const std::string & address() const
    {
        return _address;
    }

    unsigned short & port()
    {
        return _port;
    }
    unsigned short port() const
    {
        return _port;
    }

    qdb_error_t & error()
    {
        return _error;
    }
    qdb_error_t error() const
    {
        return _error;
    }

private:
    std::string _address;
    unsigned short _port;
    qdb_error_t _error;
};

class handle
{
public:
    // you can't connect in the constructor as you would be unable to know if the connection was successful
    // we don't want to encourage exceptions usage in our C++ API and therefore provide the class with a separate
    // connect() method
    handle() : _handle(NULL), _timeout(60 * 1000)
    {
    }
    ~handle()
    {
        close();
    }

#ifdef QDBAPI_RVALUE_SUPPORT
public:
    handle(handle && h) : _handle(0), _timeout(h._timeout)
    {
        std::swap(h._handle, _handle);
    }

    handle & operator=(handle && h)
    {
        _timeout = h._timeout;
        std::swap(h._handle, _handle);
        return *this;
    }
#endif

private:
    // prevent copy, we might get a dangling handle otherwise
    // use a pointer or a smart pointer instead of copying objects around
    handle(const handle & /*unused*/) : _handle(NULL), _timeout(60 * 1000)
    {
    }

public:
    // STLesque iterators with a twist,
    // you can compare with end, but also make sure the iterator is valid at each step
    // if the iterator is invalid, you should check if it's because you reached the end or because of some other
    // error (see last_error())
    // also, when the iterator reaches end() or rend() going forward or backward is currently unsupported
    // this is due to how we implemented end() and rend() iterator values
    const_iterator begin()
    {
        return const_iterator(this->_handle);
    }

    const_iterator end()
    {
        return const_iterator(this->_handle, detail::const_iterator_impl::init_end());
    }

    const_reverse_iterator rbegin()
    {
        return const_reverse_iterator(this->_handle);
    }

    const_reverse_iterator rend()
    {
        return const_reverse_iterator(this->_handle, detail::const_iterator_impl::init_end());
    }

public:
    // not thread safe
    void close()
    {
        if (connected())
        {
            // although close returns an error, we can safely ignore it as we know for sure the handle
            // we pass is an unclosed handle
            qdb_close(_handle);
            _handle = NULL;
        }
        assert(!connected());
    }

public:
    bool connected() const
    {
        return _handle != NULL;
    }

public:
    // the user may want to access the handle, make that convenient
    operator qdb_handle_t()
    {
        return _handle;
    }

    operator bool() const
    {
        return _handle != NULL;
    }

    bool operator!() const
    {
        return _handle == NULL;
    }

public:
    qdb_error_t set_timeout(int timeout_ms)
    {
        qdb_error_t err = qdb_e_ok;

        if (_handle)
        {
            err = qdb_option_set_timeout(_handle, _timeout);
            if (err) return err;

            _timeout = timeout_ms;
        }

        return err;
    }

    // set compression level
    qdb_error_t set_compression(qdb_compression_t comp_level)
    {
        return qdb_option_set_compression(_handle, comp_level);
    }

public:
    // connect to the remote host in TCP
    // if a connection is already opened, it will be closed
    // the internal handle will be updated if and only if the connection is successful
    // not thread safe
    qdb_error_t connect(const char * uri)
    {
        close();

        if (!uri)
        {
            return qdb_e_invalid_argument;
        }

        qdb_handle_t h;
        qdb_error_t err = qdb_open(&h, qdb_p_tcp);

        if (err != qdb_e_ok)
        {
            return err;
        }

        assert(h);

        err = qdb_option_set_timeout(h, _timeout);
        if (err != qdb_e_ok)
        {
            qdb_close(h);
            return err;
        }

        err = qdb_connect(h, uri);
        if (err != qdb_e_ok)
        {
            qdb_close(h);
        }
        else
        {
            _handle = h;
        }

        return err;
    }

public:
    qdb_error_t blob_put(const char * alias, const void * content, qdb_size_t content_length, qdb_time_t expiry_time)
    {
        return qdb_blob_put(_handle, alias, content, content_length, expiry_time);
    }

public:
    // integer
    qdb_error_t int_get(const char * alias, qdb_int_t * number)
    {
        return qdb_int_get(_handle, alias, number);
    }

    qdb_error_t int_put(const char * alias, qdb_int_t number, qdb_time_t expiry_time)
    {
        return qdb_int_put(_handle, alias, number, expiry_time);
    }

    qdb_error_t int_update(const char * alias, qdb_int_t number, qdb_time_t expiry_time)
    {
        return qdb_int_update(_handle, alias, number, expiry_time);
    }

    qdb_error_t int_add(const char * alias, qdb_int_t addend, qdb_int_t * result = NULL)
    {
        return qdb_int_add(_handle, alias, addend, result);
    }

public:
    // deque
    //! \warning This function is experimental.
    qdb_error_t deque_size(const char * alias, qdb_size_t * size)
    {
        return qdb_deque_size(_handle, alias, size);
    }

    //! \warning This function is experimental.
    api_buffer_ptr
    deque_get_at(const char * alias, qdb_int_t index, qdb_error_t & error)
    {
        const void * content = NULL;
        qdb_size_t content_length = 0;

        error =
            qdb_deque_get_at(_handle, alias, index, &content, &content_length);

        return translate_result_buffer(error, content, content_length);
    }

    //! \warning This function is experimental.
    qdb_error_t deque_set_at(const char * alias,
                             qdb_int_t index,
                             const void * content,
                             qdb_size_t content_length)
    {
        return qdb_deque_set_at(_handle, alias, index, content, content_length);
    }

    //! \warning This function is experimental.
    qdb_error_t deque_push_front(const char * alias,
                                 const void * content,
                                 qdb_size_t content_length)
    {
        return qdb_deque_push_front(_handle, alias, content, content_length);
    }

    //! \warning This function is experimental.
    qdb_error_t deque_push_back(const char * alias, const void * content, qdb_size_t content_length)
    {
        return qdb_deque_push_back(_handle, alias, content, content_length);
    }

    //! \warning This function is experimental.
    api_buffer_ptr deque_pop_front(const char * alias, qdb_error_t & error)
    {
        const void * content = NULL;
        qdb_size_t content_length = 0;

        error = qdb_deque_pop_front(_handle, alias, &content, &content_length);

        return translate_result_buffer(error, content, content_length);
    }

    //! \warning This function is experimental.
    api_buffer_ptr deque_pop_back(const char * alias, qdb_error_t & error)
    {
        const void * content = NULL;
        qdb_size_t content_length = 0;

        error = qdb_deque_pop_back(_handle, alias, &content, &content_length);

        return translate_result_buffer(error, content, content_length);
    }

    //! \warning This function is experimental.
    api_buffer_ptr deque_front(const char * alias, qdb_error_t & error)
    {
        const void * content = NULL;
        qdb_size_t content_length = 0;

        error = qdb_deque_front(_handle, alias, &content, &content_length);

        return translate_result_buffer(error, content, content_length);
    }

    //! \warning This function is experimental.
    api_buffer_ptr deque_back(const char * alias, qdb_error_t & error)
    {
        const void * content = NULL;
        qdb_size_t content_length = 0;

        error = qdb_deque_back(_handle, alias, &content, &content_length);

        return translate_result_buffer(error, content, content_length);
    }

public:
    // hset
    //! \warning This function is experimental.
    qdb_error_t hset_insert(const char * alias, const void * content, qdb_size_t content_length)
    {
        return qdb_hset_insert(_handle, alias, content, content_length);
    }

    //! \warning This function is experimental.
    qdb_error_t hset_erase(const char * alias, const void * content, qdb_size_t content_length)
    {
        return qdb_hset_erase(_handle, alias, content, content_length);
    }

    //! \warning This function is experimental.
    qdb_error_t hset_contains(const char * alias, const void * content, qdb_size_t content_length)
    {
        return qdb_hset_contains(_handle, alias, content, content_length);
    }

public:
    qdb_error_t blob_update(const char * alias, const void * content, qdb_size_t content_length, qdb_time_t expiry_time)
    {
        return qdb_blob_update(_handle, alias, content, content_length, expiry_time);
    }

public:
    qdb_error_t blob_get_noalloc(const char * alias, void * content, qdb_size_t * content_length)
    {
        return qdb_blob_get_noalloc(_handle, alias, content, content_length);
    }

public:
    size_t run_batch(qdb_operation_t * operations, size_t operation_count)
    {
        return qdb_run_batch(_handle, operations, operation_count);
    }

public:
    qdb_error_t run_transaction(qdb_operation_t * operations, size_t operation_count, size_t & fail_index)
    {
        return qdb_run_transaction(_handle, operations, operation_count, &fail_index);
    }

private:
    api_buffer_ptr translate_result_buffer(qdb_error_t error, const void * content, qdb_size_t content_length) const
    {
        return (error == qdb_e_ok) ? make_api_buffer_ptr(_handle, content, content_length) : api_buffer_ptr();
    }

public:
    api_buffer_ptr blob_get(const char * alias, qdb_error_t & error)
    {
        const void * content = NULL;
        qdb_size_t content_length = 0;

        error = qdb_blob_get(_handle, alias, &content, &content_length);

        return translate_result_buffer(error, content, content_length);
    }

    api_buffer_ptr blob_get_and_remove(const char * alias, qdb_error_t & error)
    {
        const void * content = NULL;
        qdb_size_t content_length = 0;

        error = qdb_blob_get_and_remove(_handle, alias, &content, &content_length);

        return translate_result_buffer(error, content, content_length);
    }

    api_buffer_ptr blob_get_and_update(const char * alias, const void * update_content, qdb_size_t update_content_length, qdb_time_t expiry_time,
                                       qdb_error_t & error)
    {
        const void * content = NULL;
        qdb_size_t content_length = 0;

        error = qdb_blob_get_and_update(_handle, alias, update_content, update_content_length, expiry_time, &content, &content_length);

        return translate_result_buffer(error, content, content_length);
    }

    api_buffer_ptr blob_compare_and_swap(const char * alias, const void * new_value, qdb_size_t new_value_length, const void * comparand,
                                         qdb_size_t comparand_length, qdb_time_t expiry_time, qdb_error_t & error)
    {
        const void * content = NULL;
        qdb_size_t content_length = 0;

        error = qdb_blob_compare_and_swap(_handle, alias, new_value, new_value_length, comparand, comparand_length, expiry_time, &content, &content_length);

        // make buffer even on error
        return make_api_buffer_ptr(_handle, content, content_length);
    }

public:
    qdb_error_t expires_at(const char * alias, qdb_time_t expiry_time)
    {
        return qdb_expires_at(_handle, alias, expiry_time);
    }

    qdb_error_t expires_from_now(const char * alias, qdb_time_t expiry_delta)
    {
        return qdb_expires_from_now(_handle, alias, expiry_delta);
    }

    qdb_error_t get_expiry_time(const char * alias, qdb_time_t & expiry_time)
    {
        qdb_entry_metadata_t meta;

        meta.expiry_time.tv_sec = 0;
        meta.expiry_time.tv_nsec = 0;

        qdb_error_t err = qdb_get_metadata(_handle, alias, &meta);

        expiry_time =
            static_cast<qdb_time_t>(meta.expiry_time.tv_sec) * 1000
            + static_cast<qdb_time_t>(meta.expiry_time.tv_nsec) / 1000000;

        return err;
    }

    qdb_error_t get_location(const char * alias, remote_node & location)
    {
        qdb_remote_node_t loc;
        qdb_error_t err = qdb_get_location(_handle, alias, &loc);
        if (err != qdb_e_ok)
        {
            return err;
        }
        location = remote_node(loc.address, loc.port);
        qdb_free_buffer(_handle, loc.address);
        return err;
    }

    qdb_error_t get_metadata(const char * alias,
                             qdb_entry_metadata_t * entry_metadata)
    {
        return qdb_get_metadata(_handle, alias, entry_metadata);
    }

    QDB_DEPRECATED("use get_metadata")
    qdb_error_t get_type(const char * alias, qdb_entry_type_t * entry_type)
    {
        if (!entry_type) return qdb_e_invalid_argument;

        qdb_entry_metadata_t meta;
        meta.type = qdb_entry_uninitialized;
        qdb_error_t err = qdb_get_metadata(_handle, alias, &meta);
        *entry_type = meta.type;
        return err;
    }

private:
    template <typename F>
    std::string node_json(const char * uri, qdb_error_t & error, F f)
    {
        const char * content = NULL;
        qdb_size_t content_length = 0;
        error = f(_handle, uri, &content, &content_length);
        std::string result;
        if (error == qdb_e_ok)
        {
            // don't include the terminal zero, std::string will add its own
            result = std::string(content, content_length - 1);
            qdb_free_buffer(_handle, content);
        }
        return result;
    }

public:
    std::string node_status(const char * uri, qdb_error_t & error)
    {
        return node_json(uri, error, &qdb_node_status);
    }

    std::string node_config(const char * uri, qdb_error_t & error)
    {
        return node_json(uri, error, &qdb_node_config);
    }

    std::string node_topology(const char * uri, qdb_error_t & error)
    {
        return node_json(uri, error, &qdb_node_topology);
    }

public:
    qdb_error_t node_stop(const char * uri, const char * reason)
    {
        return qdb_node_stop(_handle, uri, reason);
    }

public:
    qdb_error_t remove(const char * alias)
    {
        return qdb_remove(_handle, alias);
    }

    qdb_error_t blob_remove_if(const char * alias, const void * comparand, qdb_size_t comparand_length)
    {
        return qdb_blob_remove_if(_handle, alias, comparand, comparand_length);
    }

private:
    struct blob_scan_binder
    {
        blob_scan_binder(const void * pattern,
                         qdb_size_t pattern_length,
                         qdb_int_t max_count)
            : _pattern(pattern), _pattern_length(pattern_length),
              _max_count(max_count)
        {
        }

        qdb_error_t operator()(qdb_handle_t handle,
                               const char *** results,
                               size_t * result_count)
        {
            return qdb_blob_scan(handle,
                                 _pattern,
                                 _pattern_length,
                                 _max_count,
                                 results,
                                 result_count);
        }

    private:
        const void * _pattern;
        qdb_size_t _pattern_length;
        qdb_int_t _max_count;
    };

public:
    //! \warning Experimental function.
    std::vector<std::string> blob_scan(const void * pattern,
                                       qdb_size_t pattern_length,
                                       qdb_int_t max_count,
                                       qdb_error_t & error)
    {
        return get_alias_list(
            error, blob_scan_binder(pattern, pattern_length, max_count));
    }

private:
    struct blob_scan_regex_binder
    {
        blob_scan_regex_binder(const char * pattern, qdb_int_t max_count)
            : _pattern(pattern), _max_count(max_count)
        {
        }

        qdb_error_t operator()(qdb_handle_t handle,

                               const char *** results,
                               size_t * result_count)
        {
            return qdb_blob_scan_regex(
                handle, _pattern, _max_count, results, result_count);
        }

    private:
        const char * _pattern;
        qdb_int_t _max_count;
    };

public:
    //! \warning Experimental function.
    std::vector<std::string> blob_scan_regex(const char * pattern,
                                             qdb_int_t max_count,
                                             qdb_error_t & error)
    {
        return get_alias_list(error,
                              blob_scan_regex_binder(pattern, max_count));
    }

private:
    typedef qdb_error_t (*affix_get)(qdb_handle_t,
                                     const char *,
                                     qdb_int_t,
                                     const char ***,
                                     size_t *);

    struct affix_binder
    {
        affix_binder(affix_get func, const char * affix, qdb_int_t max_count)
            : _function(func), _affix(affix), _max_count(max_count)
        {
        }

        qdb_error_t operator()(qdb_handle_t handle,
                               const char *** results,
                               size_t * result_count)
        {
            return _function(
                handle, _affix, _max_count, results, result_count);
        }

    private:
        affix_get _function;
        const char * _affix;
        qdb_int_t _max_count;
    };

public:
    std::vector<std::string>
    prefix_get(const char * prefix, qdb_int_t max_count, qdb_error_t & error)
    {
        return get_alias_list(error,
                              affix_binder(qdb_prefix_get, prefix, max_count));
    }

    std::vector<std::string>
    suffix_get(const char * suffix, qdb_int_t max_count, qdb_error_t & error)
    {
        return get_alias_list(error,
                              affix_binder(qdb_suffix_get, suffix, max_count));
    }

public:
    qdb_error_t attach_tag(const char * alias, const char * tag)
    {
        return qdb_attach_tag(_handle, alias, tag);
    }

    qdb_error_t has_tag(const char * alias, const char * tag)
    {
        return qdb_has_tag(_handle, alias, tag);
    }

    qdb_error_t detach_tag(const char * alias, const char * tag)
    {
        return qdb_detach_tag(_handle, alias, tag);
    }

private:
    struct get_tagged_binder
    {
        get_tagged_binder(const char * tag) : _tag(tag)
        {
        }

        qdb_error_t operator()(qdb_handle_t handle,
                               const char *** results,
                               size_t * result_count)
        {
            return qdb_get_tagged(handle, _tag, results, result_count);
        }

    private:
        const char * _tag;
    };

public:
    // Consider using iteration on tags for large tags.
    std::vector<std::string> get_tagged(const char * tag, qdb_error_t & error)
    {
        return get_alias_list(error, get_tagged_binder(tag));
    }

private:
    struct get_tags_binder
    {
        get_tags_binder(const char * alias) : _alias(alias)
        {
        }

        qdb_error_t operator()(qdb_handle_t handle,
                               const char *** results,
                               size_t * result_count)
        {
            return qdb_get_tags(handle, _alias, results, result_count);
        }

    private:
        const char * _alias;
    };

public:
    std::vector<std::string> get_tags(const char * alias, qdb_error_t & error)
    {
        return get_alias_list(error, get_tags_binder(alias));
    }

    const_tag_iterator tag_begin(const char * tag)
    {
        return const_tag_iterator(this->_handle, tag);
    }

    const_tag_iterator tag_end()
    {
        return const_tag_iterator(this->_handle, detail::const_tag_iterator_impl::init_end());
    }

public:
    qdb_error_t purge_all(int timeout_ms)
    {
        return qdb_purge_all(_handle, timeout_ms);
    }

    qdb_error_t trim_all(int timeout_ms)
    {
        return qdb_trim_all(_handle, timeout_ms);
    }

private:
    template <typename T>
    struct cstring_translator
    {
        T operator()(const char * cstr) const
        {
            return T(cstr);
        }
    };

    template <typename Function>
    std::vector<std::string> get_alias_list(qdb_error_t & error, Function f)
    {
        const char ** results = NULL;
        size_t result_count = 0;
        error = f(_handle, &results, &result_count);

        std::vector<std::string> final_result;

        if (error == qdb_e_ok)
        {
            try
            {
                final_result.resize(result_count);
                std::transform(results, results + result_count, final_result.begin(), cstring_translator<std::string>());
            }
            catch (const std::bad_alloc &)
            {
                error = qdb_e_no_memory_local;
                final_result.clear();
            }

            qdb_free_results(_handle, results, result_count);
        }

        return final_result;
    }

private:
    qdb_handle_t _handle;
    int _timeout;
};

typedef qdb::shared_ptr<handle>::type handle_ptr;

} // namespace qdb
