/*
   HARK Library
   Copyright (C) 2021 Honda Research Institute Japan Inc.
 */

#if !defined(_HARKLIB_DATA_H_INCLUDED_)
#define _HARKLIB_DATA_H_INCLUDED_ 1

#include "hark-lib/hark-lib.h"
#include "hark-lib/error.h"

#include <sstream>
#include <typeinfo>
#include <type_traits>

#include <map>
#include <vector>
#include <iostream>
#include <typeindex>

namespace hark
{
    extern std::string data_typename(const std::type_info &ti);

    template<typename K, typename V>
    std::ostream &
    operator << (std::ostream &os, const std::map<K, V> &src)
    {
        size_t count = 0;
        os << "{";
        for(auto &it: src){
            if(count > 0){
                os << ", ";
            }
            os << it.first << ": " << it.second;
            count++;
        }
        os << "}";
        return os;
    }

    template<typename V>
    std::ostream &
    operator << (std::ostream &os, const std::vector<V> &src)
    {
        size_t count = 0;
        os << "[";
        for(auto &&it: src){
            if(count > 0){
                os << ", ";
            }
            os << it;
            count++;
        }
        os << "]";
        return os;
    }

    template<typename T>
    struct has_ostream_dump
    {
    private:
        template<typename U>
        static auto check(const U &x)->decltype(operator << (std::declval<std::ostream &>(), x), std::true_type());
        static std::false_type check(...);

    public:
        static bool const value = decltype(check(std::declval<T>()))::value;
    };

    template<typename T>
    static auto dump_entity(std::ostream &os, const T &value)
        -> typename std::enable_if<has_ostream_dump<T>::value>::type
    {
        os << value;
    }

    // template<>
    // void dump_entity(std::ostream &os, const std::nullptr_t &value)
    // {
    //     os << "(null)";
    // }

    template<typename T>
    static auto dump_entity(std::ostream &os, const T &value)
        -> typename std::enable_if<!has_ostream_dump<T>::value>::type
    {
        os << "object[type=" << data_typename(typeid(T)) << "]";
    }

    class data
    {
    private:
        struct holder
        {
            holder(){
            }
            virtual ~holder(){
            }

            virtual holder*clone() = 0;
            virtual std::ostream&dump(std::ostream &os) const = 0;

            virtual std::type_index type() const = 0;
        };

        template<typename T>
        struct typed_holder_base: public holder
        {
            typed_holder_base(const T &value): _value(value){ /* std::cout << "typed_holder_base::ctor: type=" << typeid(T).name() << std::endl; */
            }
            typed_holder_base(T &&value): _value(std::move(value)){ /* std::cout << "typed_holder_base::ctor: type=" << typeid(T).name() << std::endl; */
            }
            virtual ~typed_holder_base(){
            }

            holder*clone()
            {
                return new typed_holder<T>(_value);
            };

            virtual std::type_index type() const {
                return typeid(this->_value);
            }

            T _value;
        };

        template<typename T>
        struct typed_holder_arithmetic_base: public typed_holder_base<T>
        {
            using typed_holder_base<T>::typed_holder_base;

            template<typename U>
            U as() const {
                return static_cast<U>(this->_value);
            }
        };

        template<bool isp, bool isa, typename T>
        struct typed_holder_base_ptr: public typed_holder_base<T>{
            using typed_holder_base<T>::typed_holder_base;
            virtual ~typed_holder_base_ptr(){ /* std::cout << "typed_holder_base::dtor: non-ptr: " << typeid(T).name() << std::endl; */
            }

            std::ostream&dump(std::ostream &os) const
            {
                dump_entity(os, this->_value);
                return os;
            }
        };

        template<typename T>
        struct typed_holder_base_ptr<false, true, T>: public typed_holder_arithmetic_base<T>{
            using typed_holder_arithmetic_base<T>::typed_holder_arithmetic_base;
            virtual ~typed_holder_base_ptr(){ /* std::cout << "typed_holder_base::dtor: non-ptr: " << typeid(T).name() << std::endl; */
            }

            std::ostream&dump(std::ostream &os) const
            {
                dump_entity(os, this->_value);
                return os;
            }
        };

        template<bool isa, typename T>
        struct typed_holder_base_ptr<true, isa, T>: public typed_holder_base<T>{
            using typed_holder_base<T>::typed_holder_base;
            virtual ~typed_holder_base_ptr(){ /* std::cout << "typed_holder_base::dtor: ptr: " << data_typename(typeid(T)) << std::endl; */
                delete typed_holder_base<T>::_value;
            }

            std::ostream&dump(std::ostream &os) const
            {
                dump_entity(os, *this->_value);
                return os;
            }
        };

        template<typename T>
        using typed_holder = typed_holder_base_ptr<std::is_pointer<T>::value, std::is_arithmetic<T>::value, T>;

        template<bool isa, typename T>
        struct acaster_t {
            bool operator() (T &dst, const std::shared_ptr<data::holder> &src){
                return false;
            }
        };

        template<typename T>
        struct acaster_t<true, T> {
            template<typename U>
            bool try_cast(T &dst, const std::shared_ptr<data::holder> &src)
            {
                auto q = std::dynamic_pointer_cast<const data::typed_holder<U>>(src);
                if(q){
                    dst = static_cast<T>(q->_value);
                    return true;
                }
                return false;
            }

            bool operator() (T &dst, const std::shared_ptr<data::holder> &src)
            {
                return
                    this->try_cast<int>(dst, src) ||
                    this->try_cast<float>(dst, src) ||
                    this->try_cast<double>(dst, src) ||
                    this->try_cast<unsigned int>(dst, src) ||
                    this->try_cast<short int>(dst, src) ||
                    this->try_cast<unsigned short int>(dst, src) ||
                    this->try_cast<long int>(dst, src) ||
                    this->try_cast<unsigned long int>(dst, src);
            }
        };

        template<typename T>
        using acaster = acaster_t<std::is_arithmetic<T>::value, T>;

    public:
        data();
        data(const data &src);
        data(data &src);
        data(data &&src);
        template<typename T>explicit data(T &&src);

        data &operator = (const data &src);
        data &operator = (data &&src);

        ~data();

    public:
        template<typename T, typename... Args>static data create(Args... args);

        template<typename T>T as() const;
        template<typename T>T&cast() const;
        template<typename T>bool is_type() const;

        bool is_empty() const;
        void clear();

    private:
        std::shared_ptr<holder>_value;

        friend std ::ostream &operator << (std::ostream &os, const data &value);
    };

    std::ostream &operator << (std::ostream &os, const data &value);

    template<typename T>
    data::data(T &&src):
        _value(new typed_holder<typename std::remove_cv<typename std::remove_reference<T>::type>::type>(std::forward<T>(src)))
    {
    }

    template<> data::data(data &&src);

    template<typename T, typename... Args>
    data
    data::create(Args... args)
    {
        return data(T(args...));
    }

    template<typename T>
    T
    data::as() const
    {
        auto p = std::dynamic_pointer_cast<typed_holder<T>>(this->_value);
        if(p){
            return p->_value;

        }else{
            acaster<T> ac;
            T dst;
            if(ac(dst, this->_value)){
                return dst;
            }
            // auto c = this->_value->find_caster();
            // if(c){
            //     return c->operator()<T>(this->_value);
            // }

            holder *q = this->_value.get();
            std::cout << "data::as: failure: value-type=" << data_typename(typeid(*q)) << ", trying to cast to=" << data_typename(typeid(T)) << std::endl;

            std::stringstream ss;
            ss << "Failed to cast value to type '" << typeid(T).name() << "'";
            throw RuntimeError(ss.str());
        }
    }

    template<typename T>
    T &
    data::cast() const
    {
        auto p = std::dynamic_pointer_cast<typed_holder<T>>(this->_value);
        if(p){
            return p->_value;
        }else{
            holder *q = this->_value.get();
            std::cout << "data::cast: failure: value-type=" << data_typename(typeid(*q)) << ", trying to cast to=" << data_typename(typeid(T)) << std::endl;

            std::cout << "data=" << *this << std::endl;

            std::stringstream ss;
            ss << "Failed to cast value to type '" << typeid(T).name() << "'";
            throw RuntimeError(ss.str());
        }
    }

    template<typename T>
    bool
    data::is_type() const
    {
        auto p = std::dynamic_pointer_cast<typed_holder<T>>(this->_value);
        return (bool)p;
    }

//    template<typename T, typename U>
//    struct caster {
//        U operator() (const T &value) {
//            return static_cast<U>(value);
//        }
//
//        static caster<T, U> the;
//    };
//
//    template<typename T, typename U>
//    const caster<T, U> *find_caster<T, U>() {
//        return nullptr;
//    }
//
//    template<>
//    const caster<int, float> *find_caster<int, float>() {
//        return &caster<int, float>().the;
//    }

    // std::map<std::pair<std::type_index, std::type_index>, > _caster_map = {
    //
    // };
}

#endif /* #if !defined(_HARKLIB_DATA_H_INCLUDED_) */

/* end of file */
