/*
 * 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/odbc/common_types.h"
#include "ignite/odbc/type_traits.h"

#include <ignite/common/big_decimal.h>
#include <ignite/common/bytes_view.h>
#include <ignite/common/ignite_date.h>
#include <ignite/common/ignite_date_time.h>
#include <ignite/common/ignite_duration.h>
#include <ignite/common/ignite_period.h>
#include <ignite/common/ignite_time.h>
#include <ignite/common/ignite_timestamp.h>
#include <ignite/common/uuid.h>

#include <cstdint>
#include <map>

namespace ignite {

/**
 * Conversion result
 */
enum class conversion_result {
    /** Conversion successful. No data lost. */
    AI_SUCCESS,

    /** Conversion successful, but fractional truncation occurred. */
    AI_FRACTIONAL_TRUNCATED,

    /** Conversion successful, but right-side variable length data truncation occurred. */
    AI_VARLEN_DATA_TRUNCATED,

    /** Conversion is not supported. */
    AI_UNSUPPORTED_CONVERSION,

    /** Indicator buffer needed to complete the operation but it is NULL. */
    AI_INDICATOR_NEEDED,

    /** No data found. */
    AI_NO_DATA,

    /** General operation failure. */
    AI_FAILURE
};

/**
 * User application data buffer.
 */
class application_data_buffer {
public:
    // Default
    application_data_buffer() = default;

    /**
     * Constructor.
     *
     * @param type Underlying data type.
     * @param buffer Data buffer pointer.
     * @param buf_len Data buffer length.
     * @param res_len Resulting data length.
     */
    application_data_buffer(odbc_native_type type, void *buffer, SQLLEN buf_len, SQLLEN *res_len);

    /**
     * Set offset in bytes for all bound pointers.
     *
     * @param offset Offset.
     */
    void set_byte_offset(int offset) { this->m_byte_offset = offset; }

    /**
     * Set offset in elements for all bound pointers.
     *
     * @param offset Offset.
     */
    void set_element_offset(SQLULEN offset) { this->m_element_offset = offset; }

    /**
     * Put in buffer value of type int8_t.
     *
     * @param value Value.
     * @return Conversion result.
     */
    conversion_result put_int8(int8_t value);

    /**
     * Put in buffer value of type int16_t.
     *
     * @param value Value.
     * @return Conversion result.
     */
    conversion_result put_int16(int16_t value);

    /**
     * Put in buffer value of type int32_t.
     *
     * @param value Value.
     * @return Conversion result.
     */
    conversion_result put_int32(int32_t value);

    /**
     * Put in buffer value of type int64_t.
     *
     * @param value Value.
     * @return Conversion result.
     */
    conversion_result put_int64(int64_t value);

    /**
     * Put in buffer value of type float.
     *
     * @param value Value.
     * @return Conversion result.
     */
    conversion_result put_float(float value);

    /**
     * Put in buffer value of type double.
     *
     * @param value Value.
     * @return Conversion result.
     */
    conversion_result put_double(double value);

    /**
     * Put in buffer value of type bool.
     *
     * @param value Value.
     * @return Conversion result.
     */
    conversion_result put_bool(bool value);

    /**
     * Put in buffer value of type string.
     *
     * @param value Value.
     * @return Conversion result.
     */
    conversion_result put_string(const std::optional<std::string> &value) {
        return value ? put_string(*value) : put_null();
    }

    /**
     * Put in buffer value of type string.
     *
     * @param value Value.
     * @return Conversion result.
     */
    conversion_result put_string(const char *value) { return put_string(std::string(value)); }

    /**
     * Put in buffer value of type string.
     *
     * @param value Value.
     * @return Conversion result.
     */
    conversion_result put_string(const std::string &value);

    /**
     * Put in buffer value of type string.
     *
     * @param value Value.
     * @param written Number of written characters.
     * @return Conversion result.
     */
    conversion_result put_string(const std::string &value, int32_t &written);

    /**
     * Put in buffer value of type GUID.
     *
     * @param value Value.
     * @return Conversion result.
     */
    conversion_result put_uuid(const uuid &value);

    /**
     * Put binary data in buffer.
     *
     * @param data Data pointer.
     * @param len Data length.
     * @param written Number of written characters.
     * @return Conversion result.
     */
    conversion_result put_binary_data(void *data, size_t len, std::int32_t &written);

    /**
     * Put binary data in buffer.
     *
     * @param data Data.
     * @return Conversion result.
     */
    conversion_result put_binary_data(bytes_view data) {
        std::int32_t dummy;
        return put_binary_data((void *) data.data(), data.size(), dummy);
    }

    /**
     * Put NULL.
     * @return Conversion result.
     */
    conversion_result put_null();

    /**
     * Put decimal value to buffer.
     *
     * @param value Value to put.
     * @return Conversion result.
     */
    conversion_result put_decimal(const big_decimal &value);

    /**
     * Put date to buffer.
     *
     * @param value Value to put.
     * @return Conversion result.
     */
    conversion_result put_date(const ignite_date &value);

    /**
     * Put timestamp to buffer.
     *
     * @param value Value to put.
     * @return Conversion result.
     */
    conversion_result put_timestamp(const ignite_timestamp &value);

    /**
     * Put time to buffer.
     *
     * @param value Value to put.
     * @return Conversion result.
     */
    conversion_result put_time(const ignite_time &value);

    /**
     * Put datetime to buffer.
     *
     * @param value Value to put.
     * @return Conversion result.
     */
    conversion_result put_date_time(const ignite_date_time &value);

    /**
     * Get string.
     *
     * @return String value of buffer.
     */
    [[nodiscard]] std::string get_string(size_t maxLen) const;

    /**
     * Get value of type int8_t.
     *
     * @return Integer value of type int8_t.
     */
    [[nodiscard]] int8_t get_int8() const;

    /**
     * Get value of type int16_t.
     *
     * @return Integer value of type int16_t.
     */
    [[nodiscard]] int16_t get_int16() const;

    /**
     * Get value of type int32_t.
     *
     * @return Integer value of type int32_t.
     */
    [[nodiscard]] int32_t get_int32() const;

    /**
     * Get value of type int64_t.
     *
     * @return Integer value of type int64_t.
     */
    [[nodiscard]] int64_t get_int64() const;

    /**
     * Get value of type float.
     *
     * @return Integer value of type float.
     */
    [[nodiscard]] float get_float() const;

    /**
     * Get value of type double.
     *
     * @return Value of type double.
     */
    [[nodiscard]] double get_double() const;

    /**
     * Get value of type GUID.
     *
     * @return Value of type uuid.
     */
    [[nodiscard]] uuid get_uuid() const;

    /**
     * Get value of type ignite_date.
     *
     * @return Value of type ignite_date.
     */
    [[nodiscard]] ignite_date get_date() const;

    /**
     * Get value of type ignite_date_time.
     *
     * @return Value of type ignite_date_time.
     */
    [[nodiscard]] ignite_date_time get_date_time() const;

    /**
     * Get value of type ignite_timestamp.
     *
     * @return Value of type ignite_timestamp.
     */
    [[nodiscard]] ignite_timestamp get_timestamp() const;

    /**
     * Get value of type ignite_time.
     *
     * @return Value of type ignite_timestamp.
     */
    [[nodiscard]] ignite_time get_time() const;

    /**
     * Get value of type big_decimal.
     *
     * @param val Result is placed here.
     */
    void get_decimal(big_decimal &val) const;

    /**
     * Get raw data.
     *
     * @return Buffer data.
     */
    [[nodiscard]] const void *get_data() const;

    /**
     * Get result data length.
     *
     * @return Data length pointer.
     */
    [[nodiscard]] const SQLLEN *get_result_len() const;

    /**
     * Get raw data.
     *
     * @return Buffer data.
     */
    void *get_data();

    /**
     * Get result data length.
     *
     * @return Data length pointer.
     */
    SQLLEN *get_result_len();

    /**
     * Get buffer size in bytes.
     *
     * @return Buffer size.
     */
    [[nodiscard]] SQLLEN get_size() const { return m_buffer_len; }

    /**
     * Check if the data is going to be provided at execution.
     *
     * @return True if the data is going to be provided
     *     at execution.
     */
    [[nodiscard]] bool is_data_at_exec() const;

    /**
     * Get size of the data that is going to be provided at
     * execution.
     *
     * @return Size of the data that is going to be provided
     *     at execution.
     */
    [[nodiscard]] SQLLEN get_data_at_exec_size() const;

    /**
     * Get single element size.
     *
     * @return Size of the single element.
     */
    [[nodiscard]] SQLLEN get_element_size() const;

    /**
     * Get size of the input buffer.
     *
     * @return Input buffer size, or zero if the data is going
     *     to be provided at execution.
     */
    [[nodiscard]] SQLLEN get_input_size() const;

    /**
     * Get buffer type.
     *
     * @return Buffer type.
     */
    [[nodiscard]] odbc_native_type get_type() const { return m_type; }

private:
    /**
     * Put value of numeric type in the buffer.
     *
     * @param value Numeric value to put.
     * @return Conversion result.
     */
    template<typename T>
    conversion_result put_num(T value);

    /**
     * Put numeric value to numeric buffer.
     *
     * @param value Numeric value.
     * @return Conversion result.
     */
    template<typename TBuf, typename TIn>
    conversion_result put_num_to_num_buffer(TIn value);

    /**
     * Put value to string buffer.
     *
     * @param value Value that can be converted to string.
     * @return Conversion result.
     */
    template<typename CharT, typename Tin>
    conversion_result put_value_to_string_buffer(const Tin &value);

    /**
     * Put value to string buffer.
     * Specialisation for int8_t.
     *
     * @param value Value that can be converted to string.
     * @return Conversion result.
     */
    template<typename CharT>
    conversion_result put_value_to_string_buffer(const int8_t &value);

    /**
     * Put string to string buffer.
     *
     * @param value String value.
     * @param written Number of characters written.
     * @return Conversion result.
     */
    template<typename OutCharT, typename InCharT>
    conversion_result put_string_to_string_buffer(const std::basic_string<InCharT> &value, int32_t &written);

    /**
     * Put raw data to any buffer.
     *
     * @param data Data pointer.
     * @param len Data length.
     * @param written Number of characters written.
     * @return Conversion result.
     */
    conversion_result put_raw_data_to_buffer(void *data, size_t len, int32_t &written);

    /**
     * Put data from struct tm to a string buffer.
     *
     * @param tm_time Time.
     * @param val_len Resulting string length.
     * @param fmt Format for underlying strftime.
     * @return Result.
     */
    conversion_result put_tm_to_string(tm &tm_time, SQLLEN val_len, const char *fmt);

    /**
     * Get int of type T.
     *
     * @return Integer value of specified type.
     */
    template<typename T>
    T get_num() const;

    /**
     * Apply buffer offset to pointer.
     * Adds offset to pointer if offset pointer is not null.
     *
     * @param ptr Pointer.
     * @param elemSize Element size.
     * @return Pointer with applied offset.
     */
    template<typename T>
    T *apply_offset(T *ptr, size_t elemSize) const;

    /** Underlying data type. */
    odbc_native_type m_type{odbc_native_type::AI_UNSUPPORTED};

    /** Buffer pointer. */
    void *m_buffer{nullptr};

    /** Buffer length. */
    SQLLEN m_buffer_len{0};

    /** Result length. */
    SQLLEN *m_result_len{nullptr};

    /** Current byte offset */
    int m_byte_offset{0};

    /** Current element offset. */
    SQLULEN m_element_offset{0};
};

/** Column binging map type alias. */
typedef std::map<std::uint16_t, application_data_buffer> column_binding_map;

} // namespace ignite
