// This file is distributed under the BSD License.
// See "license.txt" for details.
// Copyright 2009-2012, Jonathan Turner (jonathan@emptycrate.com)
// Copyright 2009-2015, Jason Turner (jason@emptycrate.com)
// http://www.chaiscript.com

#ifndef CHAISCRIPT_BOXED_VALUE_HPP_
#define CHAISCRIPT_BOXED_VALUE_HPP_

#include <functional>
#include <map>
#include <memory>
#include <type_traits>

#include "../chaiscript_threading.hpp"
#include "../chaiscript_defines.hpp"
#include "any.hpp"
#include "type_info.hpp"

namespace chaiscript 
{

  /// \brief A wrapper for holding any valid C++ type. All types in ChaiScript are Boxed_Value objects
  /// \sa chaiscript::boxed_cast
  class Boxed_Value
  {
    public:
      /// used for explicitly creating a "void" object
      struct Void_Type
      {
      };

    private:
      /// structure which holds the internal state of a Boxed_Value
      /// \todo Get rid of Any and merge it with this, reducing an allocation in the process
      struct Data
      {
        Data(const Type_Info &ti,
            chaiscript::detail::Any to,
            bool tr,
            const void *t_void_ptr)
          : m_type_info(ti), m_obj(std::move(to)), m_data_ptr(ti.is_const()?nullptr:const_cast<void *>(t_void_ptr)), m_const_data_ptr(t_void_ptr),
            m_is_ref(tr)
        {
        }

        Data &operator=(const Data &rhs)
        {
          m_type_info = rhs.m_type_info;
          m_obj = rhs.m_obj;
          m_is_ref = rhs.m_is_ref;
          m_data_ptr = rhs.m_data_ptr;
          m_const_data_ptr = rhs.m_const_data_ptr;

          if (rhs.m_attrs)
          {
            m_attrs = std::unique_ptr<std::map<std::string, Boxed_Value>>(new std::map<std::string, Boxed_Value>(*rhs.m_attrs));
          }

          return *this;
        }

        Data(const Data &) = delete;

#if !defined(__APPLE__) && (!defined(_MSC_VER) || _MSC_VER != 1800)
        Data(Data &&) = default;
        Data &operator=(Data &&rhs) = default;
#endif


        Type_Info m_type_info;
        chaiscript::detail::Any m_obj;
        void *m_data_ptr;
        const void *m_const_data_ptr;
        std::unique_ptr<std::map<std::string, Boxed_Value>> m_attrs;
        bool m_is_ref;
      };

      struct Object_Data
      {
        static std::shared_ptr<Data> get(Boxed_Value::Void_Type)
        {
          return std::make_shared<Data>(
                detail::Get_Type_Info<void>::get(),
                chaiscript::detail::Any(), 
                false,
                nullptr)
              ;
        }

        template<typename T>
          static std::shared_ptr<Data> get(const std::shared_ptr<T> *obj)
          {
            return get(*obj);
          }

        template<typename T>
          static std::shared_ptr<Data> get(const std::shared_ptr<T> &obj)
          {
            return std::make_shared<Data>(
                  detail::Get_Type_Info<T>::get(), 
                  chaiscript::detail::Any(obj), 
                  false,
                  obj.get()
                );
          }

        template<typename T>
          static std::shared_ptr<Data> get(std::shared_ptr<T> &&obj)
          {
            auto ptr = obj.get();
            return std::make_shared<Data>(
                  detail::Get_Type_Info<T>::get(), 
                  chaiscript::detail::Any(std::move(obj)), 
                  false,
                  ptr
                );
          }

        template<typename T>
          static std::shared_ptr<Data> get(T *t)
          {
            return get(std::ref(*t));
          }

        template<typename T>
          static std::shared_ptr<Data> get(const T *t)
          {
            return get(std::cref(*t));
          }


        template<typename T>
          static std::shared_ptr<Data> get(std::reference_wrapper<T> obj)
          {
            auto p = &obj.get();
            return std::make_shared<Data>(
                  detail::Get_Type_Info<T>::get(),
                  chaiscript::detail::Any(std::move(obj)),
                  true,
                  p
                );
          }

        template<typename T>
          static std::shared_ptr<Data> get(T t)
          {
            auto p = std::make_shared<T>(std::move(t));
            auto ptr = p.get();
            return std::make_shared<Data>(
                  detail::Get_Type_Info<T>::get(), 
                  chaiscript::detail::Any(std::move(p)),
                  false,
                  ptr
                );
          }

        static std::shared_ptr<Data> get()
        {
          return std::make_shared<Data>(
                Type_Info(),
                chaiscript::detail::Any(),
                false,
                nullptr 
              );
        }

      };

    public:
      /// Basic Boxed_Value constructor
        template<typename T,
          typename = typename std::enable_if<!std::is_same<Boxed_Value, typename std::decay<T>::type>::value>::type>
        explicit Boxed_Value(T &&t)
          : m_data(Object_Data::get(std::forward<T>(t)))
        {
        }

      /// Unknown-type constructor
      Boxed_Value()
        : m_data(Object_Data::get())
      {
      }

#if !defined(_MSC_VER) || _MSC_VER != 1800
      Boxed_Value(Boxed_Value&&) = default;
      Boxed_Value& operator=(Boxed_Value&&) = default;
#endif

      Boxed_Value(const Boxed_Value&) = default;
      Boxed_Value& operator=(const Boxed_Value&) = default;

      void swap(Boxed_Value &rhs)
      {
        std::swap(m_data, rhs.m_data);
      }

      /// Copy the values stored in rhs.m_data to m_data.
      /// m_data pointers are not shared in this case
      Boxed_Value assign(const Boxed_Value &rhs)
      {
        (*m_data) = (*rhs.m_data);
        return *this;
      }

      const Type_Info &get_type_info() const CHAISCRIPT_NOEXCEPT
      {
        return m_data->m_type_info;
      }

      /// return true if the object is uninitialized
      bool is_undef() const CHAISCRIPT_NOEXCEPT
      {
        return m_data->m_type_info.is_undef();
      }

      bool is_const() const CHAISCRIPT_NOEXCEPT
      {
        return m_data->m_type_info.is_const();
      }

      bool is_type(const Type_Info &ti) const CHAISCRIPT_NOEXCEPT
      {
        return m_data->m_type_info.bare_equal(ti);
      }

      bool is_null() const CHAISCRIPT_NOEXCEPT
      {
        return (m_data->m_data_ptr == nullptr && m_data->m_const_data_ptr == nullptr);
      }

      const chaiscript::detail::Any & get() const CHAISCRIPT_NOEXCEPT
      {
        return m_data->m_obj;
      }

      bool is_ref() const CHAISCRIPT_NOEXCEPT
      {
        return m_data->m_is_ref;
      }

      bool is_pointer() const CHAISCRIPT_NOEXCEPT
      {
        return !is_ref();
      }

      void *get_ptr() const CHAISCRIPT_NOEXCEPT
      {
        return m_data->m_data_ptr;
      }

      const void *get_const_ptr() const CHAISCRIPT_NOEXCEPT
      {
        return m_data->m_const_data_ptr;
      }

      Boxed_Value get_attr(const std::string &t_name)
      {
        if (!m_data->m_attrs)
        {
          m_data->m_attrs = std::unique_ptr<std::map<std::string, Boxed_Value>>(new std::map<std::string, Boxed_Value>());
        }

        return (*m_data->m_attrs)[t_name];
      }

      Boxed_Value &copy_attrs(const Boxed_Value &t_obj)
      {
        if (t_obj.m_data->m_attrs)
        {
          m_data->m_attrs = std::unique_ptr<std::map<std::string, Boxed_Value>>(new std::map<std::string, Boxed_Value>(*t_obj.m_data->m_attrs));
        }
        return *this;
      }


      /// \returns true if the two Boxed_Values share the same internal type
      static bool type_match(const Boxed_Value &l, const Boxed_Value &r) CHAISCRIPT_NOEXCEPT
      {
        return l.get_type_info() == r.get_type_info();
      }

    private:
      std::shared_ptr<Data> m_data;
  };

  /// @brief Creates a Boxed_Value. If the object passed in is a value type, it is copied. If it is a pointer, std::shared_ptr, or std::reference_type
  ///        a copy is not made.
  /// @param t The value to box
  ///
  /// Example:
  ///
  /// ~~~{.cpp}
  /// int i;
  /// chaiscript::ChaiScript chai;
  /// chai.add(chaiscript::var(i), "i");
  /// chai.add(chaiscript::var(&i), "ip");
  /// ~~~
  ///
  /// @sa @ref adding_objects
  template<typename T>
    Boxed_Value var(T t)
    {
      return Boxed_Value(t);
    }

  namespace detail {
    /// \brief Takes a value, copies it and returns a Boxed_Value object that is immutable
    /// \param[in] t Value to copy and make const
    /// \returns Immutable Boxed_Value 
    /// \sa Boxed_Value::is_const
    template<typename T>
      Boxed_Value const_var_impl(const T &t)
      {
        return Boxed_Value(std::make_shared<typename std::add_const<T>::type >(t));
      }

    /// \brief Takes a pointer to a value, adds const to the pointed to type and returns an immutable Boxed_Value.
    ///        Does not copy the pointed to value.
    /// \param[in] t Pointer to make immutable
    /// \returns Immutable Boxed_Value
    /// \sa Boxed_Value::is_const
    template<typename T>
      Boxed_Value const_var_impl(T *t)
      {
        return Boxed_Value( const_cast<typename std::add_const<T>::type *>(t) );
      }

    /// \brief Takes a std::shared_ptr to a value, adds const to the pointed to type and returns an immutable Boxed_Value.
    ///        Does not copy the pointed to value.
    /// \param[in] t Pointer to make immutable
    /// \returns Immutable Boxed_Value
    /// \sa Boxed_Value::is_const
    template<typename T>
      Boxed_Value const_var_impl(const std::shared_ptr<T> &t)
      {
        return Boxed_Value( std::const_pointer_cast<typename std::add_const<T>::type>(t) );
      }

    /// \brief Takes a std::reference_wrapper value, adds const to the referenced type and returns an immutable Boxed_Value.
    ///        Does not copy the referenced value.
    /// \param[in] t Reference object to make immutable
    /// \returns Immutable Boxed_Value
    /// \sa Boxed_Value::is_const
    template<typename T>
      Boxed_Value const_var_impl(const std::reference_wrapper<T> &t)
      {
        return Boxed_Value( std::cref(t.get()) );
      }
  }

  /// \brief Takes an object and returns an immutable Boxed_Value. If the object is a std::reference or pointer type
  ///        the value is not copied. If it is an object type, it is copied.
  /// \param[in] t Object to make immutable
  /// \returns Immutable Boxed_Value
  /// \sa chaiscript::Boxed_Value::is_const
  /// \sa chaiscript::var
  ///
  /// Example:
  /// \code
  /// enum Colors
  /// {
  ///   Blue,
  ///   Green,
  ///   Red
  /// };
  /// chaiscript::ChaiScript chai
  /// chai.add(chaiscript::const_var(Blue), "Blue"); // add immutable constant
  /// chai.add(chaiscript::const_var(Red), "Red");
  /// chai.add(chaiscript::const_var(Green), "Green");
  /// \endcode
  /// 
  /// \todo support C++11 strongly typed enums
  /// \sa \ref adding_objects
  template<typename T>
    Boxed_Value const_var(const T &t)
    {
      return detail::const_var_impl(t);
    }



}

#endif

